matinsn2000 commited on
Commit
8779583
·
1 Parent(s): 220bb12

Added image upload service

Browse files
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
- # response_model=UploadResponse
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
- return result
 
 
 
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, Optional
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
- self.id_map: List[int] = [] # Map FAISS indices to photo IDs
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
- self.index = faiss.IndexFlatL2(dim)
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 to FAISS index
35
- self.index.add(embedding)
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.4
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, indices = self.index.search(query_embedding, min(top_k, self.index.ntotal))
65
 
66
- # Map back to photo IDs and filter distances > 0.4
67
  results = [
68
- (self.id_map[int(idx)], float(distance))
69
- for distance, idx in zip(distances[0], indices[0])
70
- if distance <= 0.5
71
  ]
72
 
73
  return results
74
 
75
  def save(self) -> None:
76
- """Save index and id_map to disk"""
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 and id_map from disk"""
83
  if os.path.exists(self.index_path):
84
  self.index = faiss.read_index(self.index_path)
85
- if os.path.exists(self.index_path + ".ids"):
86
- with open(self.index_path + ".ids", "rb") as f:
87
- self.id_map = pickle.load(f)
88
-
 
89
  def get_stats(self) -> dict:
90
  """Get index statistics"""
91
  return {
92
  "total_embeddings": self.index.ntotal,
93
  "dimension": self.dim,
94
- "id_map_size": len(self.id_map)
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}")