Spaces:
Sleeping
Sleeping
Commit
·
8779583
1
Parent(s):
220bb12
Added image upload service
Browse files- cloudzy/agents/image_analyzer.py +1 -0
- cloudzy/routes/upload.py +14 -10
- cloudzy/search_engine.py +29 -35
- cloudzy/utils/file_upload_service.py +77 -0
cloudzy/agents/image_analyzer.py
CHANGED
|
@@ -46,6 +46,7 @@ result: {
|
|
| 46 |
"caption": "a short description for the image"
|
| 47 |
}
|
| 48 |
"""
|
|
|
|
| 49 |
|
| 50 |
# Send request
|
| 51 |
completion = self.client.chat.completions.create(
|
|
|
|
| 46 |
"caption": "a short description for the image"
|
| 47 |
}
|
| 48 |
"""
|
| 49 |
+
|
| 50 |
|
| 51 |
# Send request
|
| 52 |
completion = self.client.chat.completions.create(
|
cloudzy/routes/upload.py
CHANGED
|
@@ -12,6 +12,7 @@ from cloudzy.ai_utils import ImageEmbeddingGenerator
|
|
| 12 |
from cloudzy.search_engine import SearchEngine
|
| 13 |
|
| 14 |
from cloudzy.agents.image_analyzer import ImageDescriber
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
import os
|
|
@@ -61,8 +62,8 @@ def validate_image_file(filename: str) -> bool:
|
|
| 61 |
"""Check if file has valid image extension"""
|
| 62 |
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
|
| 63 |
|
| 64 |
-
|
| 65 |
-
@router.post("/upload")
|
| 66 |
async def upload_photo(
|
| 67 |
file: UploadFile = File(...),
|
| 68 |
session: Session = Depends(get_session),
|
|
@@ -99,29 +100,32 @@ async def upload_photo(
|
|
| 99 |
filepath = f"uploads/{saved_filename}"
|
| 100 |
|
| 101 |
|
| 102 |
-
APP_DOMAIN = os.getenv("APP_DOMAIN")
|
| 103 |
-
|
| 104 |
|
| 105 |
-
image_url = f"{APP_DOMAIN}uploads/{saved_filename}"
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
try:
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
describer = ImageDescriber()
|
| 114 |
# result = describer.describe_image("https://userx2000-cloudzy-ai-challenge.hf.space/uploads/img_1_20251024_064435_667.jpg")
|
| 115 |
# result = describer.describe_image("https://userx2000-cloudzy-ai-challenge.hf.space/uploads/img_2_20251024_082115_102.jpeg")
|
| 116 |
result = describer.describe_image(image_url)
|
| 117 |
|
| 118 |
|
| 119 |
-
|
| 120 |
except Exception as e:
|
| 121 |
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
| 122 |
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
# Generate AI analysis
|
| 127 |
tags = result.get("tags", [])
|
|
|
|
| 12 |
from cloudzy.search_engine import SearchEngine
|
| 13 |
|
| 14 |
from cloudzy.agents.image_analyzer import ImageDescriber
|
| 15 |
+
from cloudzy.utils.file_upload_service import ImgBBUploader
|
| 16 |
|
| 17 |
|
| 18 |
import os
|
|
|
|
| 62 |
"""Check if file has valid image extension"""
|
| 63 |
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
|
| 64 |
|
| 65 |
+
|
| 66 |
+
@router.post("/upload", response_model=UploadResponse)
|
| 67 |
async def upload_photo(
|
| 68 |
file: UploadFile = File(...),
|
| 69 |
session: Session = Depends(get_session),
|
|
|
|
| 100 |
filepath = f"uploads/{saved_filename}"
|
| 101 |
|
| 102 |
|
|
|
|
|
|
|
| 103 |
|
|
|
|
| 104 |
|
| 105 |
+
try:
|
| 106 |
+
uploader = ImgBBUploader(expiration=600)
|
| 107 |
+
image_url = uploader.upload(filepath)
|
| 108 |
+
except Exception as e:
|
| 109 |
+
raise HTTPException(status_code=500, detail=f"Image upload failed: {str(e)}")
|
| 110 |
+
|
| 111 |
|
| 112 |
|
| 113 |
try:
|
| 114 |
+
|
|
|
|
|
|
|
| 115 |
describer = ImageDescriber()
|
| 116 |
# result = describer.describe_image("https://userx2000-cloudzy-ai-challenge.hf.space/uploads/img_1_20251024_064435_667.jpg")
|
| 117 |
# result = describer.describe_image("https://userx2000-cloudzy-ai-challenge.hf.space/uploads/img_2_20251024_082115_102.jpeg")
|
| 118 |
result = describer.describe_image(image_url)
|
| 119 |
|
| 120 |
|
|
|
|
| 121 |
except Exception as e:
|
| 122 |
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
| 123 |
|
| 124 |
|
| 125 |
+
APP_DOMAIN = os.getenv("APP_DOMAIN")
|
| 126 |
+
|
| 127 |
+
image_url = f"{APP_DOMAIN}uploads/{saved_filename}"
|
| 128 |
+
|
| 129 |
|
| 130 |
# Generate AI analysis
|
| 131 |
tags = result.get("tags", [])
|
cloudzy/search_engine.py
CHANGED
|
@@ -1,45 +1,41 @@
|
|
| 1 |
-
"""FAISS-based semantic search engine"""
|
| 2 |
import faiss
|
| 3 |
import numpy as np
|
| 4 |
-
from typing import List, Tuple
|
| 5 |
import os
|
| 6 |
-
import pickle
|
| 7 |
|
| 8 |
|
| 9 |
class SearchEngine:
|
| 10 |
"""FAISS-based search engine for image embeddings"""
|
| 11 |
-
|
| 12 |
def __init__(self, dim: int = 1024, index_path: str = "faiss_index.bin"):
|
| 13 |
self.dim = dim
|
| 14 |
self.index_path = index_path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
# Load existing index or create new one
|
| 18 |
if os.path.exists(index_path):
|
| 19 |
self.index = faiss.read_index(index_path)
|
| 20 |
else:
|
| 21 |
-
|
| 22 |
-
|
|
|
|
| 23 |
def add_embedding(self, photo_id: int, embedding: np.ndarray) -> None:
|
| 24 |
"""
|
| 25 |
Add an embedding to the index.
|
| 26 |
-
|
| 27 |
Args:
|
| 28 |
photo_id: Unique photo identifier
|
| 29 |
embedding: 1D numpy array of shape (dim,)
|
| 30 |
"""
|
| 31 |
# Ensure embedding is float32 and correct shape
|
| 32 |
embedding = embedding.astype(np.float32).reshape(1, -1)
|
| 33 |
-
|
| 34 |
-
# Add
|
| 35 |
-
self.index.
|
| 36 |
-
|
| 37 |
-
# Track photo ID
|
| 38 |
-
self.id_map.append(photo_id)
|
| 39 |
-
|
| 40 |
# Save index to disk
|
| 41 |
self.save()
|
| 42 |
-
|
| 43 |
def search(self, query_embedding: np.ndarray, top_k: int = 5) -> List[Tuple[int, float]]:
|
| 44 |
"""
|
| 45 |
Search for similar embeddings.
|
|
@@ -49,9 +45,8 @@ class SearchEngine:
|
|
| 49 |
top_k: Number of results to return
|
| 50 |
|
| 51 |
Returns:
|
| 52 |
-
List of (photo_id, distance) tuples with distance <= 0.
|
| 53 |
"""
|
| 54 |
-
|
| 55 |
self.load()
|
| 56 |
|
| 57 |
if self.index.ntotal == 0:
|
|
@@ -61,35 +56,34 @@ class SearchEngine:
|
|
| 61 |
query_embedding = query_embedding.astype(np.float32).reshape(1, -1)
|
| 62 |
|
| 63 |
# Search in FAISS index
|
| 64 |
-
distances,
|
| 65 |
|
| 66 |
-
#
|
| 67 |
results = [
|
| 68 |
-
(
|
| 69 |
-
for
|
| 70 |
-
if distance <= 0.5
|
| 71 |
]
|
| 72 |
|
| 73 |
return results
|
| 74 |
|
| 75 |
def save(self) -> None:
|
| 76 |
-
"""Save index
|
| 77 |
faiss.write_index(self.index, self.index_path)
|
| 78 |
-
with open(self.index_path + ".ids", "wb") as f:
|
| 79 |
-
pickle.dump(self.id_map, f)
|
| 80 |
|
| 81 |
def load(self) -> None:
|
| 82 |
-
"""Load index
|
| 83 |
if os.path.exists(self.index_path):
|
| 84 |
self.index = faiss.read_index(self.index_path)
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
| 89 |
def get_stats(self) -> dict:
|
| 90 |
"""Get index statistics"""
|
| 91 |
return {
|
| 92 |
"total_embeddings": self.index.ntotal,
|
| 93 |
"dimension": self.dim,
|
| 94 |
-
"
|
| 95 |
-
}
|
|
|
|
| 1 |
+
"""FAISS-based semantic search engine using ID-mapped index"""
|
| 2 |
import faiss
|
| 3 |
import numpy as np
|
| 4 |
+
from typing import List, Tuple
|
| 5 |
import os
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
class SearchEngine:
|
| 9 |
"""FAISS-based search engine for image embeddings"""
|
| 10 |
+
|
| 11 |
def __init__(self, dim: int = 1024, index_path: str = "faiss_index.bin"):
|
| 12 |
self.dim = dim
|
| 13 |
self.index_path = index_path
|
| 14 |
+
|
| 15 |
+
# Load existing index or create a new one
|
|
|
|
| 16 |
if os.path.exists(index_path):
|
| 17 |
self.index = faiss.read_index(index_path)
|
| 18 |
else:
|
| 19 |
+
base_index = faiss.IndexFlatL2(dim)
|
| 20 |
+
self.index = faiss.IndexIDMap(base_index)
|
| 21 |
+
|
| 22 |
def add_embedding(self, photo_id: int, embedding: np.ndarray) -> None:
|
| 23 |
"""
|
| 24 |
Add an embedding to the index.
|
| 25 |
+
|
| 26 |
Args:
|
| 27 |
photo_id: Unique photo identifier
|
| 28 |
embedding: 1D numpy array of shape (dim,)
|
| 29 |
"""
|
| 30 |
# Ensure embedding is float32 and correct shape
|
| 31 |
embedding = embedding.astype(np.float32).reshape(1, -1)
|
| 32 |
+
|
| 33 |
+
# Add embedding with its ID
|
| 34 |
+
self.index.add_with_ids(embedding, np.array([photo_id], dtype=np.int64))
|
| 35 |
+
|
|
|
|
|
|
|
|
|
|
| 36 |
# Save index to disk
|
| 37 |
self.save()
|
| 38 |
+
|
| 39 |
def search(self, query_embedding: np.ndarray, top_k: int = 5) -> List[Tuple[int, float]]:
|
| 40 |
"""
|
| 41 |
Search for similar embeddings.
|
|
|
|
| 45 |
top_k: Number of results to return
|
| 46 |
|
| 47 |
Returns:
|
| 48 |
+
List of (photo_id, distance) tuples with distance <= 0.5
|
| 49 |
"""
|
|
|
|
| 50 |
self.load()
|
| 51 |
|
| 52 |
if self.index.ntotal == 0:
|
|
|
|
| 56 |
query_embedding = query_embedding.astype(np.float32).reshape(1, -1)
|
| 57 |
|
| 58 |
# Search in FAISS index
|
| 59 |
+
distances, ids = self.index.search(query_embedding, top_k)
|
| 60 |
|
| 61 |
+
# Filter invalid and distant results
|
| 62 |
results = [
|
| 63 |
+
(int(photo_id), float(distance))
|
| 64 |
+
for photo_id, distance in zip(ids[0], distances[0])
|
| 65 |
+
if photo_id != -1 and distance <= 0.5
|
| 66 |
]
|
| 67 |
|
| 68 |
return results
|
| 69 |
|
| 70 |
def save(self) -> None:
|
| 71 |
+
"""Save FAISS index to disk"""
|
| 72 |
faiss.write_index(self.index, self.index_path)
|
|
|
|
|
|
|
| 73 |
|
| 74 |
def load(self) -> None:
|
| 75 |
+
"""Load FAISS index from disk"""
|
| 76 |
if os.path.exists(self.index_path):
|
| 77 |
self.index = faiss.read_index(self.index_path)
|
| 78 |
+
else:
|
| 79 |
+
# Recreate empty ID-mapped index if missing
|
| 80 |
+
base_index = faiss.IndexFlatL2(self.dim)
|
| 81 |
+
self.index = faiss.IndexIDMap(base_index)
|
| 82 |
+
|
| 83 |
def get_stats(self) -> dict:
|
| 84 |
"""Get index statistics"""
|
| 85 |
return {
|
| 86 |
"total_embeddings": self.index.ntotal,
|
| 87 |
"dimension": self.dim,
|
| 88 |
+
"index_type": type(self.index).__name__,
|
| 89 |
+
}
|
cloudzy/utils/file_upload_service.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import requests
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ImgBBUploader:
|
| 13 |
+
"""
|
| 14 |
+
Upload an image file to ImgBB and return only the direct public URL.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
API_ENDPOINT = "https://api.imgbb.com/1/upload"
|
| 18 |
+
|
| 19 |
+
def __init__(self, expiration: Optional[int] = None, timeout: int = 30):
|
| 20 |
+
api_key = os.getenv("IMAGE_UPLOAD_API_KEY")
|
| 21 |
+
if not api_key:
|
| 22 |
+
raise ValueError("api_key is required")
|
| 23 |
+
self.api_key = api_key
|
| 24 |
+
self.expiration = expiration
|
| 25 |
+
self.timeout = timeout
|
| 26 |
+
|
| 27 |
+
def _encode_file_to_base64(self, image_path: Path) -> str:
|
| 28 |
+
if not image_path.exists():
|
| 29 |
+
raise FileNotFoundError(f"Image not found: {image_path}")
|
| 30 |
+
with image_path.open("rb") as f:
|
| 31 |
+
data = f.read()
|
| 32 |
+
return base64.b64encode(data).decode("ascii")
|
| 33 |
+
|
| 34 |
+
def upload(self, image_path: str) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Upload the image and return the direct URL of the uploaded file.
|
| 37 |
+
"""
|
| 38 |
+
image_path_obj = Path(image_path)
|
| 39 |
+
b64_image = self._encode_file_to_base64(image_path_obj)
|
| 40 |
+
|
| 41 |
+
params = {"key": self.api_key}
|
| 42 |
+
if self.expiration is not None:
|
| 43 |
+
params["expiration"] = str(self.expiration)
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
resp = requests.post(
|
| 47 |
+
self.API_ENDPOINT,
|
| 48 |
+
params=params,
|
| 49 |
+
data={"image": b64_image},
|
| 50 |
+
timeout=self.timeout,
|
| 51 |
+
)
|
| 52 |
+
resp.raise_for_status()
|
| 53 |
+
data = resp.json()
|
| 54 |
+
print(data)
|
| 55 |
+
if data.get("success"):
|
| 56 |
+
return data["data"]["url"]
|
| 57 |
+
raise RuntimeError(f"Upload failed: {data}")
|
| 58 |
+
except requests.RequestException as e:
|
| 59 |
+
raise RuntimeError(f"ImgBB upload failed: {e}") from e
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
uploader = ImgBBUploader(expiration=86400)
|
| 64 |
+
result = uploader.upload("/Users/komeilfathi/Documents/hf_deploy_test/cloudzy_ai_challenge/uploads/img_1_20251024_080401_794.jpg")
|
| 65 |
+
print(result)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# resp = uploader.upload_file("/Users/komeilfathi/Documents/hf_deploy_test/cloudzy_ai_challenge/uploads/img_1_20251024_080401_794.jpg")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Example usage:
|
| 75 |
+
# uploader = FreeImageHostUploader(api_key="your_api_key_here")
|
| 76 |
+
# image_url = uploader.upload_image("path_to_your_image.jpg")
|
| 77 |
+
# print(f"Image uploaded successfully: {image_url}")
|