SandiyoS commited on
Commit
ca2cbc3
Β·
verified Β·
1 Parent(s): f402eac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +45 -58
app.py CHANGED
@@ -1,6 +1,6 @@
1
  #!/usr/bin/env python3
2
  """
3
- πŸ”₯ Triple Hybrid API + Gradio UI - HF Spaces Optimized
4
  """
5
 
6
  import os
@@ -12,7 +12,7 @@ import json
12
  import logging
13
  import warnings
14
  from pathlib import Path
15
- from typing import List, Union, Optional
16
 
17
  import torch
18
  import torch.nn as nn
@@ -27,10 +27,6 @@ import torchvision.transforms as T
27
  import gradio as gr
28
  import spaces
29
 
30
- from fastapi import FastAPI, Request, HTTPException
31
- from fastapi.middleware.cors import CORSMiddleware
32
- from contextlib import asynccontextmanager
33
-
34
  from ultralytics import YOLO
35
  import open_clip
36
  from peft import PeftModel
@@ -52,7 +48,7 @@ class Config:
52
 
53
  CONFIG = Config()
54
 
55
- # ===================== MODEL DEFINITION =====================
56
  class ProjectionHead(nn.Module):
57
  def __init__(self, in_dim=1024, hidden_dim=512, out_dim=256, dropout=0.1):
58
  super().__init__()
@@ -96,16 +92,14 @@ class IconClassifier(nn.Module):
96
  embedding = self.projection(features)
97
  return F.normalize(embedding, p=2, dim=1)
98
 
99
- # Preprocess
100
  custom_preprocess = T.Compose([
101
  T.Resize((256, 256), interpolation=T.InterpolationMode.NEAREST),
102
  T.ToTensor(),
103
  T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
104
  ])
105
 
106
- # ===================== IMAGE DECODE =====================
107
  def decode_image_advanced(data: Union[str, bytes], filename: str = "image") -> Image.Image:
108
- # ... (sama seperti sebelumnya, saya skip tulis ulang biar tidak terlalu panjang)
109
  if isinstance(data, str):
110
  clean = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
111
  clean = re.sub(r"^data:image/[a-zA-Z0-9.+-]+;base64,", "", clean)
@@ -121,7 +115,6 @@ def decode_image_advanced(data: Union[str, bytes], filename: str = "image") -> I
121
  except:
122
  pass
123
 
124
- # fallback raw bytes
125
  common_sizes = [50, 60, 64, 70, 80, 96, 100, 128]
126
  for size in common_sizes:
127
  expected = size * size * 4
@@ -136,7 +129,6 @@ def decode_image_advanced(data: Union[str, bytes], filename: str = "image") -> I
136
  # ===================== MODEL MANAGER =====================
137
  class ModelManager:
138
  _instance = None
139
-
140
  def __new__(cls):
141
  if cls._instance is None:
142
  cls._instance = super().__new__(cls)
@@ -144,12 +136,10 @@ class ModelManager:
144
  return cls._instance
145
 
146
  def init(self):
147
- if self.initialized:
148
- return
149
- logger.info("πŸ”₯ Initializing models on GPU...")
150
  self.yolo = YOLO(CONFIG.yolo_model_path).to(CONFIG.device)
151
-
152
- # Load SigLIP + LoRA
153
  config_path = Path(CONFIG.lora_model_dir) / "config.json"
154
  with open(config_path) as f:
155
  model_config = json.load(f)
@@ -160,20 +150,18 @@ class ModelManager:
160
  model_with_lora = PeftModel.from_pretrained(backbone, CONFIG.lora_model_dir)
161
 
162
  head_state = torch.load(Path(CONFIG.lora_model_dir) / "head_weights.pt", map_location=CONFIG.device)
163
-
164
  self.inference_model = IconClassifier(model_with_lora, head_state, num_classes).to(CONFIG.device).eval()
165
-
166
  self.idx_to_class = {int(k): v for k, v in model_config.get('idx_to_class', {}).items()}
167
  self.initialized = True
168
- logger.info("βœ… All models loaded successfully!")
169
 
 
170
  def yolo_detect_and_crop(self, image: Image.Image) -> Image.Image:
171
- # ... (sama seperti kode lama kamu)
172
  img_np = np.array(image)
173
  img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
174
  results = self.yolo(img_bgr, conf=CONFIG.yolo_conf, imgsz=CONFIG.yolo_img_size, verbose=False)
175
 
176
- # (saya asumsikan bagian ini tetap sama, copy dari kode lama kamu)
177
  best_box = None
178
  best_conf = 0
179
  for result in results:
@@ -185,9 +173,9 @@ class ModelManager:
185
  best_box = box.xyxy[0].cpu().numpy().astype(int)
186
 
187
  if best_box is not None and best_conf >= CONFIG.yolo_min_conf:
188
- x1,y1,x2,y2 = best_box
189
- h,w = img_np.shape[:2]
190
- x1,y1,x2,y2 = max(0,x1),max(0,y1),min(w,x2),min(h,y2)
191
  if (x2-x1) >= CONFIG.yolo_min_crop_size and (y2-y1) >= CONFIG.yolo_min_crop_size:
192
  return Image.fromarray(img_np[y1:y2, x1:x2])
193
  return image
@@ -210,23 +198,24 @@ class ModelManager:
210
  return {
211
  "predicted_index": best_idx,
212
  "confidence": float(probs[best_idx]),
213
- "similarity_score": float(similarities[best_idx]),
214
- "all_scores": {f"option_{i}": {"similarity": float(similarities[i]), "probability": float(probs[i])} for i in range(len(options))},
 
 
215
  "predicted_class": self.idx_to_class.get(best_idx, "unknown")
216
  }
217
 
218
  manager = ModelManager()
219
 
220
- # ===================== GRADIO FUNCTION (GPU) =====================
221
- @spaces.GPU(request_timeout=120, enable=True)
222
- def solve_gradio(challenge_img, opt1, opt2, opt3, opt4, opt5, opt6, opt7, opt8):
223
  if not manager.initialized:
224
  manager.init()
225
 
226
  try:
227
- options = [opt1, opt2, opt3, opt4, opt5, opt6, opt7, opt8]
228
  if challenge_img is None or any(o is None for o in options):
229
- return "Error: Semua gambar harus diupload!", None
230
 
231
  # Convert ke PIL
232
  if isinstance(challenge_img, np.ndarray):
@@ -237,17 +226,18 @@ def solve_gradio(challenge_img, opt1, opt2, opt3, opt4, opt5, opt6, opt7, opt8):
237
  result = manager.match_options(challenge_crop, options_pil)
238
 
239
  best_idx = result["predicted_index"]
240
- lines = ["🎯 RESULT\n",
241
- f"Best Option: {best_idx + 1}",
242
  f"Confidence: {result['confidence']:.4f} ({result['confidence']*100:.2f}%)",
243
- f"Predicted Class: {result['predicted_class']}\n",
244
- "πŸ“Š All Scores:"]
245
 
246
  for i in range(8):
247
  s = result["all_scores"][f"option_{i}"]
248
- marker = "βœ…" if i == best_idx else " "
249
  lines.append(f"{marker} Option {i+1}: {s['probability']:.4f} ({s['similarity']:.4f})")
250
 
 
251
  gc.collect()
252
  if torch.cuda.is_available():
253
  torch.cuda.empty_cache()
@@ -255,38 +245,35 @@ def solve_gradio(challenge_img, opt1, opt2, opt3, opt4, opt5, opt6, opt7, opt8):
255
  return "\n".join(lines), options_pil[best_idx]
256
 
257
  except Exception as e:
258
- logger.error(f"Error: {e}", exc_info=True)
259
- return f"Runtime Error: {str(e)}", None
 
260
 
 
 
 
261
 
262
- # ===================== GRADIO UI =====================
263
- with gr.Blocks(title="Icon Challenge Solver", theme=gr.themes.Soft()) as demo:
264
- gr.Markdown("# πŸ”₯ Icon Challenge Solver\n**YOLO + SigLIP LoRA**")
265
-
266
  with gr.Row():
267
  with gr.Column(scale=1):
268
- challenge_input = gr.Image(label="Challenge Image", type="numpy", height=220)
269
  with gr.Column(scale=2):
270
- gr.Markdown("### 8 Option Images")
271
  with gr.Row():
272
- opts = [gr.Image(label=f"Option {i+1}", type="numpy", height=140) for i in range(8)]
273
-
274
  solve_btn = gr.Button("πŸ” Solve Challenge", variant="primary", size="large")
275
-
276
  with gr.Row():
277
- result_text = gr.Textbox(label="Result", lines=12, interactive=False)
278
- best_img = gr.Image(label="Best Match")
279
 
280
  solve_btn.click(
281
  fn=solve_gradio,
282
- inputs=[challenge_input] + opts,
283
- outputs=[result_text, best_img]
284
  )
285
 
286
- # ===================== FASTAPI (opsional) =====================
287
- app = FastAPI(title="Icon Solver")
288
- app = gr.mount_gradio_app(app, demo, path="/")
289
 
290
- if __name__ == "__main__":
291
- import uvicorn
292
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  #!/usr/bin/env python3
2
  """
3
+ πŸ”₯ Icon Challenge Solver - Optimized for HF Spaces
4
  """
5
 
6
  import os
 
12
  import logging
13
  import warnings
14
  from pathlib import Path
15
+ from typing import List, Union
16
 
17
  import torch
18
  import torch.nn as nn
 
27
  import gradio as gr
28
  import spaces
29
 
 
 
 
 
30
  from ultralytics import YOLO
31
  import open_clip
32
  from peft import PeftModel
 
48
 
49
  CONFIG = Config()
50
 
51
+ # ===================== MODEL CLASSES =====================
52
  class ProjectionHead(nn.Module):
53
  def __init__(self, in_dim=1024, hidden_dim=512, out_dim=256, dropout=0.1):
54
  super().__init__()
 
92
  embedding = self.projection(features)
93
  return F.normalize(embedding, p=2, dim=1)
94
 
 
95
  custom_preprocess = T.Compose([
96
  T.Resize((256, 256), interpolation=T.InterpolationMode.NEAREST),
97
  T.ToTensor(),
98
  T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
99
  ])
100
 
101
+ # ===================== UTILS =====================
102
  def decode_image_advanced(data: Union[str, bytes], filename: str = "image") -> Image.Image:
 
103
  if isinstance(data, str):
104
  clean = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
105
  clean = re.sub(r"^data:image/[a-zA-Z0-9.+-]+;base64,", "", clean)
 
115
  except:
116
  pass
117
 
 
118
  common_sizes = [50, 60, 64, 70, 80, 96, 100, 128]
119
  for size in common_sizes:
120
  expected = size * size * 4
 
129
  # ===================== MODEL MANAGER =====================
130
  class ModelManager:
131
  _instance = None
 
132
  def __new__(cls):
133
  if cls._instance is None:
134
  cls._instance = super().__new__(cls)
 
136
  return cls._instance
137
 
138
  def init(self):
139
+ if self.initialized: return
140
+ logger.info("Loading models on GPU...")
 
141
  self.yolo = YOLO(CONFIG.yolo_model_path).to(CONFIG.device)
142
+
 
143
  config_path = Path(CONFIG.lora_model_dir) / "config.json"
144
  with open(config_path) as f:
145
  model_config = json.load(f)
 
150
  model_with_lora = PeftModel.from_pretrained(backbone, CONFIG.lora_model_dir)
151
 
152
  head_state = torch.load(Path(CONFIG.lora_model_dir) / "head_weights.pt", map_location=CONFIG.device)
 
153
  self.inference_model = IconClassifier(model_with_lora, head_state, num_classes).to(CONFIG.device).eval()
154
+
155
  self.idx_to_class = {int(k): v for k, v in model_config.get('idx_to_class', {}).items()}
156
  self.initialized = True
157
+ logger.info("βœ… Models loaded!")
158
 
159
+ # (Copy fungsi yolo_detect_and_crop, extract_embedding, match_options dari kode sebelumnya)
160
  def yolo_detect_and_crop(self, image: Image.Image) -> Image.Image:
 
161
  img_np = np.array(image)
162
  img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
163
  results = self.yolo(img_bgr, conf=CONFIG.yolo_conf, imgsz=CONFIG.yolo_img_size, verbose=False)
164
 
 
165
  best_box = None
166
  best_conf = 0
167
  for result in results:
 
173
  best_box = box.xyxy[0].cpu().numpy().astype(int)
174
 
175
  if best_box is not None and best_conf >= CONFIG.yolo_min_conf:
176
+ x1, y1, x2, y2 = best_box
177
+ h, w = img_np.shape[:2]
178
+ x1, y1, x2, y2 = max(0,x1), max(0,y1), min(w,x2), min(h,y2)
179
  if (x2-x1) >= CONFIG.yolo_min_crop_size and (y2-y1) >= CONFIG.yolo_min_crop_size:
180
  return Image.fromarray(img_np[y1:y2, x1:x2])
181
  return image
 
198
  return {
199
  "predicted_index": best_idx,
200
  "confidence": float(probs[best_idx]),
201
+ "all_scores": {f"option_{i}": {
202
+ "similarity": float(similarities[i]),
203
+ "probability": float(probs[i])
204
+ } for i in range(len(options))},
205
  "predicted_class": self.idx_to_class.get(best_idx, "unknown")
206
  }
207
 
208
  manager = ModelManager()
209
 
210
+ # ===================== MAIN FUNCTION =====================
211
+ @spaces.GPU(request_timeout=180, enable=True)
212
+ def solve_gradio(challenge_img, *options):
213
  if not manager.initialized:
214
  manager.init()
215
 
216
  try:
 
217
  if challenge_img is None or any(o is None for o in options):
218
+ return "❌ Semua gambar harus diisi!", None
219
 
220
  # Convert ke PIL
221
  if isinstance(challenge_img, np.ndarray):
 
226
  result = manager.match_options(challenge_crop, options_pil)
227
 
228
  best_idx = result["predicted_index"]
229
+ lines = ["🎯 HASIL\n",
230
+ f"βœ… Best Option: {best_idx + 1}",
231
  f"Confidence: {result['confidence']:.4f} ({result['confidence']*100:.2f}%)",
232
+ f"Class: {result['predicted_class']}\n",
233
+ "πŸ“Š Skor Semua Opsi:"]
234
 
235
  for i in range(8):
236
  s = result["all_scores"][f"option_{i}"]
237
+ marker = "βœ…" if i == best_idx else " "
238
  lines.append(f"{marker} Option {i+1}: {s['probability']:.4f} ({s['similarity']:.4f})")
239
 
240
+ # Cleanup
241
  gc.collect()
242
  if torch.cuda.is_available():
243
  torch.cuda.empty_cache()
 
245
  return "\n".join(lines), options_pil[best_idx]
246
 
247
  except Exception as e:
248
+ logger.error(str(e))
249
+ return f"❌ Error: {str(e)}", None
250
+
251
 
252
+ # ===================== GRADIO INTERFACE =====================
253
+ with gr.Blocks(title="πŸ”₯ Icon Challenge Solver", theme=gr.themes.Soft()) as demo:
254
+ gr.Markdown("# πŸ”₯ Icon Challenge Solver\n**YOLO + SigLIP LoRA Hybrid**")
255
 
 
 
 
 
256
  with gr.Row():
257
  with gr.Column(scale=1):
258
+ challenge_input = gr.Image(label="πŸ–ΌοΈ Challenge Image", type="numpy", height=240)
259
  with gr.Column(scale=2):
260
+ gr.Markdown("### 🎲 8 Option Images")
261
  with gr.Row():
262
+ opt_inputs = [gr.Image(label=f"Option {i+1}", type="numpy", height=130) for i in range(8)]
263
+
264
  solve_btn = gr.Button("πŸ” Solve Challenge", variant="primary", size="large")
265
+
266
  with gr.Row():
267
+ result_text = gr.Textbox(label="πŸ“‹ Result", lines=14, interactive=False)
268
+ best_image = gr.Image(label="πŸ† Best Match")
269
 
270
  solve_btn.click(
271
  fn=solve_gradio,
272
+ inputs=[challenge_input] + opt_inputs,
273
+ outputs=[result_text, best_image]
274
  )
275
 
276
+ gr.Markdown("---\nMade with ❀️ for Icon Challenge")
 
 
277
 
278
+ # Launch (HF Spaces akan handle ini otomatis)
279
+ demo.launch()