SandiyoS commited on
Commit
f8dc0c7
Β·
verified Β·
1 Parent(s): f8dee39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -40
app.py CHANGED
@@ -1,6 +1,6 @@
1
  #!/usr/bin/env python3
2
  """
3
- πŸ”₯ Icon Challenge Solver - FINAL FIX HF Spaces
4
  """
5
 
6
  import os
@@ -36,10 +36,13 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(
36
  logger = logging.getLogger(__name__)
37
 
38
  # ===================== CONFIG =====================
 
 
 
39
  class Config:
40
  device = "cuda" if torch.cuda.is_available() else "cpu"
41
- yolo_model_path = "./yolo/best.pt"
42
- lora_model_dir = "./icon_model_hybrid"
43
  adapter_subdir = "lora_adapter"
44
  model_name = "ViT-L-16-SigLIP-256"
45
  yolo_conf = 0.25
@@ -62,24 +65,50 @@ class ProjectionHead(nn.Module):
62
  self.skip = nn.Linear(in_dim, out_dim)
63
 
64
  def forward(self, x):
 
65
  if x.shape[0] <= 1:
66
  self.train(False)
67
  residual = self.skip(x)
68
- x = self.layer1(x)
69
- x = self.bn1(x)
70
- x = self.act(x)
71
- x = self.dropout(x)
72
- x = self.layer2(x)
73
- x = self.bn2(x)
74
- return F.normalize(x + residual, p=2, dim=1)
75
 
76
  class IconClassifier(nn.Module):
77
  def __init__(self, backbone, head_state, num_classes=21):
78
  super().__init__()
79
  self.backbone = backbone
80
  self.projection = ProjectionHead()
81
- self.weight = nn.Parameter(head_state['arc_margin']['weight'])
82
- self.projection.load_state_dict(head_state['projection'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  self.s = 30.0
84
 
85
  def forward(self, x):
@@ -109,19 +138,39 @@ class ModelManager:
109
  return cls._instance
110
 
111
  def init(self):
112
- if self.initialized: return
 
 
 
 
113
  logger.info("Loading models on GPU...")
114
 
115
- # YOLO
116
  yolo_path = Path(CONFIG.yolo_model_path)
117
- if yolo_path.exists():
118
- self.yolo = YOLO(str(yolo_path)).to(CONFIG.device)
119
- logger.info(f"βœ… YOLO loaded: {yolo_path}")
120
- else:
121
- raise FileNotFoundError(f"YOLO not found: {yolo_path}")
 
 
 
 
 
 
 
122
 
123
  # Config
124
  lora_base = Path(CONFIG.lora_model_dir)
 
 
 
 
 
 
 
 
 
125
  config_path = lora_base / "config.json"
126
  if not config_path.exists():
127
  raise FileNotFoundError(f"config.json not found in {lora_base}")
@@ -130,49 +179,72 @@ class ModelManager:
130
  model_config = json.load(f)
131
 
132
  num_classes = model_config.get('num_classes') or len(model_config.get('class_to_idx', {}))
 
133
 
134
  # Backbone
 
135
  backbone, _, _ = open_clip.create_model_and_transforms(CONFIG.model_name, pretrained='webli')
136
  backbone = backbone.visual.to(CONFIG.device)
 
137
 
138
  # LoRA Adapter
139
  adapter_dir = lora_base / CONFIG.adapter_subdir
140
  peft_path = str(adapter_dir) if adapter_dir.exists() else str(lora_base)
141
  logger.info(f"Loading LoRA from: {peft_path}")
142
 
143
- model_with_lora = PeftModel.from_pretrained(
144
- backbone, peft_path, is_trainable=False
145
- )
 
 
 
 
 
 
 
146
 
147
  # Head Weights
148
  head_candidates = [
149
  lora_base / "head_weights.pt",
150
- lora_base / "weights.pt"
 
 
151
  ]
152
  head_state = None
 
153
  for hpath in head_candidates:
154
  if hpath.exists():
155
  head_state = torch.load(hpath, map_location=CONFIG.device)
 
156
  logger.info(f"βœ… Head weights loaded: {hpath}")
157
  break
 
158
  if head_state is None:
159
- raise FileNotFoundError("head_weights.pt or weights.pt not found!")
160
 
161
  self.inference_model = IconClassifier(model_with_lora, head_state, num_classes).to(CONFIG.device).eval()
 
162
 
163
  self.idx_to_class = {int(k): v for k, v in model_config.get('idx_to_class', {}).items()}
 
164
  self.initialized = True
165
  logger.info("βœ… All models loaded successfully!")
166
 
167
  def yolo_detect_and_crop(self, image: Image.Image) -> Image.Image:
 
 
 
 
168
  img_np = np.array(image)
169
  img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
 
170
  results = self.yolo(img_bgr, conf=CONFIG.yolo_conf, imgsz=CONFIG.yolo_img_size, verbose=False)
171
 
172
  best_box = None
173
  best_conf = 0
174
  for result in results:
175
- if result.boxes is None: continue
 
176
  for box in result.boxes:
177
  conf = float(box.conf[0])
178
  if conf > best_conf:
@@ -182,24 +254,39 @@ class ModelManager:
182
  if best_box is not None and best_conf >= CONFIG.yolo_min_conf:
183
  x1, y1, x2, y2 = best_box
184
  h, w = img_np.shape[:2]
185
- x1, y1, x2, y2 = max(0,x1), max(0,y1), min(w,x2), min(h,y2)
186
- if (x2-x1) >= CONFIG.yolo_min_crop_size and (y2-y1) >= CONFIG.yolo_min_crop_size:
187
- return Image.fromarray(img_np[y1:y2, x1:x2])
 
 
188
  return image
189
 
190
  def extract_embedding(self, image: Image.Image) -> torch.Tensor:
 
 
 
191
  img_tensor = custom_preprocess(image).unsqueeze(0).to(CONFIG.device)
 
192
  with torch.no_grad(), autocast():
193
- return self.inference_model.get_embedding(img_tensor)
 
194
 
195
  def match_options(self, challenge_crop: Image.Image, options: List[Image.Image]):
196
  challenge_emb = self.extract_embedding(challenge_crop)
197
- option_embs = [self.extract_embedding(opt) for opt in options]
 
 
 
 
198
  option_stack = torch.cat(option_embs, dim=0)
199
  similarities = (challenge_emb @ option_stack.T).squeeze(0).cpu().numpy()
200
 
201
  best_idx = int(np.argmax(similarities))
202
- exp_sim = np.exp(similarities * 10)
 
 
 
 
203
  probs = exp_sim / exp_sim.sum()
204
 
205
  return {
@@ -217,32 +304,59 @@ manager = ModelManager()
217
  # ===================== MAIN FUNCTION =====================
218
  @spaces.GPU(request_timeout=180, enable=True)
219
  def solve_gradio(challenge_img, *options):
 
220
  if not manager.initialized:
221
- manager.init()
 
 
 
 
222
 
223
  try:
224
  if challenge_img is None or any(o is None for o in options):
225
  return "❌ Semua gambar harus diisi!", None
226
 
 
227
  if isinstance(challenge_img, np.ndarray):
228
  challenge_img = Image.fromarray(challenge_img)
229
- options_pil = [Image.fromarray(opt) if isinstance(opt, np.ndarray) else opt for opt in options]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
 
231
  challenge_crop = manager.yolo_detect_and_crop(challenge_img)
 
 
232
  result = manager.match_options(challenge_crop, options_pil)
233
 
234
  best_idx = result["predicted_index"]
235
- lines = ["🎯 HASIL\n",
236
- f"βœ… Best Option: {best_idx + 1}",
237
- f"Confidence: {result['confidence']:.4f} ({result['confidence']*100:.2f}%)",
238
- f"Class: {result['predicted_class']}\n",
239
- "πŸ“Š Skor Semua Opsi:"]
 
 
240
 
241
- for i in range(8):
242
  s = result["all_scores"][f"option_{i}"]
243
  marker = "βœ…" if i == best_idx else " "
244
  lines.append(f"{marker} Option {i+1}: {s['probability']:.4f} ({s['similarity']:.4f})")
245
 
 
246
  gc.collect()
247
  if torch.cuda.is_available():
248
  torch.cuda.empty_cache()
@@ -280,4 +394,6 @@ with gr.Blocks(title="πŸ”₯ Icon Challenge Solver", theme=gr.themes.Soft()) as de
280
 
281
  gr.Markdown("---\nMade with ❀️ for Icon Challenge")
282
 
283
- demo.launch()
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ πŸ”₯ Icon Challenge Solver - FIXED for HF Spaces
4
  """
5
 
6
  import os
 
36
  logger = logging.getLogger(__name__)
37
 
38
  # ===================== CONFIG =====================
39
+ # FIX: Gunakan absolute path berdasarkan lokasi file script ini
40
+ BASE_DIR = Path(__file__).resolve().parent
41
+
42
  class Config:
43
  device = "cuda" if torch.cuda.is_available() else "cpu"
44
+ yolo_model_path = BASE_DIR / "yolo" / "best.pt"
45
+ lora_model_dir = BASE_DIR / "icon_model_hybrid"
46
  adapter_subdir = "lora_adapter"
47
  model_name = "ViT-L-16-SigLIP-256"
48
  yolo_conf = 0.25
 
65
  self.skip = nn.Linear(in_dim, out_dim)
66
 
67
  def forward(self, x):
68
+ # FIX: Handle batch size 1 untuk BatchNorm
69
  if x.shape[0] <= 1:
70
  self.train(False)
71
  residual = self.skip(x)
72
+ out = self.layer1(x)
73
+ out = self.bn1(out)
74
+ out = self.act(out)
75
+ out = self.dropout(out)
76
+ out = self.layer2(out)
77
+ out = self.bn2(out)
78
+ return F.normalize(out + residual, p=2, dim=1)
79
 
80
  class IconClassifier(nn.Module):
81
  def __init__(self, backbone, head_state, num_classes=21):
82
  super().__init__()
83
  self.backbone = backbone
84
  self.projection = ProjectionHead()
85
+
86
+ # FIX: Load projection weights dengan strict=False untuk handle key mismatch
87
+ if 'projection' in head_state:
88
+ proj_dict = head_state['projection']
89
+ # Handle case kalau key-nya ada prefix 'projection.'
90
+ cleaned = {}
91
+ for k, v in proj_dict.items():
92
+ if k.startswith('projection.'):
93
+ cleaned[k[11:]] = v
94
+ else:
95
+ cleaned[k] = v
96
+ self.projection.load_state_dict(cleaned, strict=False)
97
+
98
+ # FIX: Handle arc_margin weight dengan berbagai kemungkinan key name
99
+ weight = None
100
+ if 'arc_margin' in head_state and 'weight' in head_state['arc_margin']:
101
+ weight = head_state['arc_margin']['weight']
102
+ elif 'weight' in head_state:
103
+ weight = head_state['weight']
104
+ elif 'fc.weight' in head_state:
105
+ weight = head_state['fc.weight']
106
+
107
+ if weight is None:
108
+ logger.warning("Head weight not found, initializing random!")
109
+ weight = torch.randn(num_classes, 256)
110
+
111
+ self.weight = nn.Parameter(weight)
112
  self.s = 30.0
113
 
114
  def forward(self, x):
 
138
  return cls._instance
139
 
140
  def init(self):
141
+ if self.initialized:
142
+ return
143
+
144
+ logger.info(f"Working directory: {os.getcwd()}")
145
+ logger.info(f"Base directory: {BASE_DIR}")
146
  logger.info("Loading models on GPU...")
147
 
148
+ # FIX: Check YOLO path dengan absolute path
149
  yolo_path = Path(CONFIG.yolo_model_path)
150
+ logger.info(f"Looking for YOLO at: {yolo_path} (exists: {yolo_path.exists()})")
151
+
152
+ if not yolo_path.exists():
153
+ # Fallback: cari di cwd juga
154
+ alt_path = Path("yolo") / "best.pt"
155
+ if alt_path.exists():
156
+ yolo_path = alt_path
157
+ else:
158
+ raise FileNotFoundError(f"YOLO not found: {yolo_path} or {alt_path}")
159
+
160
+ self.yolo = YOLO(str(yolo_path)).to(CONFIG.device)
161
+ logger.info(f"βœ… YOLO loaded: {yolo_path}")
162
 
163
  # Config
164
  lora_base = Path(CONFIG.lora_model_dir)
165
+ logger.info(f"Looking for LoRA at: {lora_base} (exists: {lora_base.exists()})")
166
+
167
+ if not lora_base.exists():
168
+ alt_lora = Path("icon_model_hybrid")
169
+ if alt_lora.exists():
170
+ lora_base = alt_lora
171
+ else:
172
+ raise FileNotFoundError(f"LoRA dir not found: {lora_base}")
173
+
174
  config_path = lora_base / "config.json"
175
  if not config_path.exists():
176
  raise FileNotFoundError(f"config.json not found in {lora_base}")
 
179
  model_config = json.load(f)
180
 
181
  num_classes = model_config.get('num_classes') or len(model_config.get('class_to_idx', {}))
182
+ logger.info(f"Num classes: {num_classes}")
183
 
184
  # Backbone
185
+ logger.info(f"Loading backbone: {CONFIG.model_name}")
186
  backbone, _, _ = open_clip.create_model_and_transforms(CONFIG.model_name, pretrained='webli')
187
  backbone = backbone.visual.to(CONFIG.device)
188
+ logger.info("βœ… Backbone loaded")
189
 
190
  # LoRA Adapter
191
  adapter_dir = lora_base / CONFIG.adapter_subdir
192
  peft_path = str(adapter_dir) if adapter_dir.exists() else str(lora_base)
193
  logger.info(f"Loading LoRA from: {peft_path}")
194
 
195
+ try:
196
+ model_with_lora = PeftModel.from_pretrained(
197
+ backbone, peft_path, is_trainable=False
198
+ )
199
+ logger.info("βœ… LoRA adapter loaded")
200
+ except Exception as e:
201
+ logger.error(f"LoRA load failed: {e}")
202
+ # Fallback: gunakan backbone tanpa LoRA
203
+ model_with_lora = backbone
204
+ logger.warning("⚠️ Using backbone without LoRA!")
205
 
206
  # Head Weights
207
  head_candidates = [
208
  lora_base / "head_weights.pt",
209
+ lora_base / "weights.pt",
210
+ lora_base / "classifier.pt",
211
+ lora_base / "model.pt"
212
  ]
213
  head_state = None
214
+ head_path = None
215
  for hpath in head_candidates:
216
  if hpath.exists():
217
  head_state = torch.load(hpath, map_location=CONFIG.device)
218
+ head_path = hpath
219
  logger.info(f"βœ… Head weights loaded: {hpath}")
220
  break
221
+
222
  if head_state is None:
223
+ raise FileNotFoundError(f"Head weights not found! Tried: {head_candidates}")
224
 
225
  self.inference_model = IconClassifier(model_with_lora, head_state, num_classes).to(CONFIG.device).eval()
226
+ logger.info("βœ… IconClassifier created")
227
 
228
  self.idx_to_class = {int(k): v for k, v in model_config.get('idx_to_class', {}).items()}
229
+ self.class_to_idx = {v: int(k) for k, v in self.idx_to_class.items()}
230
  self.initialized = True
231
  logger.info("βœ… All models loaded successfully!")
232
 
233
  def yolo_detect_and_crop(self, image: Image.Image) -> Image.Image:
234
+ # FIX: Handle PIL Image dengan berbagai mode
235
+ if image.mode != 'RGB':
236
+ image = image.convert('RGB')
237
+
238
  img_np = np.array(image)
239
  img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
240
+
241
  results = self.yolo(img_bgr, conf=CONFIG.yolo_conf, imgsz=CONFIG.yolo_img_size, verbose=False)
242
 
243
  best_box = None
244
  best_conf = 0
245
  for result in results:
246
+ if result.boxes is None:
247
+ continue
248
  for box in result.boxes:
249
  conf = float(box.conf[0])
250
  if conf > best_conf:
 
254
  if best_box is not None and best_conf >= CONFIG.yolo_min_conf:
255
  x1, y1, x2, y2 = best_box
256
  h, w = img_np.shape[:2]
257
+ x1, y1, x2, y2 = max(0, x1), max(0, y1), min(w, x2), min(h, y2)
258
+ if (x2 - x1) >= CONFIG.yolo_min_crop_size and (y2 - y1) >= CONFIG.yolo_min_crop_size:
259
+ cropped = img_np[y1:y2, x1:x2]
260
+ return Image.fromarray(cropped)
261
+
262
  return image
263
 
264
  def extract_embedding(self, image: Image.Image) -> torch.Tensor:
265
+ if image.mode != 'RGB':
266
+ image = image.convert('RGB')
267
+
268
  img_tensor = custom_preprocess(image).unsqueeze(0).to(CONFIG.device)
269
+
270
  with torch.no_grad(), autocast():
271
+ emb = self.inference_model.get_embedding(img_tensor)
272
+ return emb
273
 
274
  def match_options(self, challenge_crop: Image.Image, options: List[Image.Image]):
275
  challenge_emb = self.extract_embedding(challenge_crop)
276
+
277
+ option_embs = []
278
+ for opt in options:
279
+ option_embs.append(self.extract_embedding(opt))
280
+
281
  option_stack = torch.cat(option_embs, dim=0)
282
  similarities = (challenge_emb @ option_stack.T).squeeze(0).cpu().numpy()
283
 
284
  best_idx = int(np.argmax(similarities))
285
+
286
+ # FIX: Temperature scaling yang lebih stabil
287
+ sim_scaled = similarities * 10
288
+ sim_max = np.max(sim_scaled)
289
+ exp_sim = np.exp(sim_scaled - sim_max) # Numerical stability
290
  probs = exp_sim / exp_sim.sum()
291
 
292
  return {
 
304
  # ===================== MAIN FUNCTION =====================
305
  @spaces.GPU(request_timeout=180, enable=True)
306
  def solve_gradio(challenge_img, *options):
307
+ # FIX: Inisialisasi dengan try-except yang lebih detail
308
  if not manager.initialized:
309
+ try:
310
+ manager.init()
311
+ except Exception as e:
312
+ logger.error(f"Model init failed: {e}", exc_info=True)
313
+ return f"❌ Model initialization failed: {str(e)}\n\nCek path model di HF Spaces!", None
314
 
315
  try:
316
  if challenge_img is None or any(o is None for o in options):
317
  return "❌ Semua gambar harus diisi!", None
318
 
319
+ # FIX: Handle berbagai input format (numpy, PIL, path)
320
  if isinstance(challenge_img, np.ndarray):
321
  challenge_img = Image.fromarray(challenge_img)
322
+ elif isinstance(challenge_img, str):
323
+ challenge_img = Image.open(challenge_img)
324
+
325
+ options_pil = []
326
+ for opt in options:
327
+ if isinstance(opt, np.ndarray):
328
+ options_pil.append(Image.fromarray(opt))
329
+ elif isinstance(opt, str):
330
+ options_pil.append(Image.open(opt))
331
+ elif isinstance(opt, Image.Image):
332
+ options_pil.append(opt)
333
+ else:
334
+ options_pil.append(None)
335
+
336
+ if any(o is None for o in options_pil):
337
+ return "❌ Format gambar option tidak valid!", None
338
 
339
+ # YOLO crop
340
  challenge_crop = manager.yolo_detect_and_crop(challenge_img)
341
+
342
+ # Match
343
  result = manager.match_options(challenge_crop, options_pil)
344
 
345
  best_idx = result["predicted_index"]
346
+ lines = [
347
+ "🎯 HASIL\n",
348
+ f"βœ… Best Option: {best_idx + 1}",
349
+ f"Confidence: {result['confidence']:.4f} ({result['confidence']*100:.2f}%)",
350
+ f"Class: {result['predicted_class']}\n",
351
+ "πŸ“Š Skor Semua Opsi:"
352
+ ]
353
 
354
+ for i in range(len(options_pil)):
355
  s = result["all_scores"][f"option_{i}"]
356
  marker = "βœ…" if i == best_idx else " "
357
  lines.append(f"{marker} Option {i+1}: {s['probability']:.4f} ({s['similarity']:.4f})")
358
 
359
+ # FIX: Cleanup VRAM setelah inference
360
  gc.collect()
361
  if torch.cuda.is_available():
362
  torch.cuda.empty_cache()
 
394
 
395
  gr.Markdown("---\nMade with ❀️ for Icon Challenge")
396
 
397
+ # FIX: Gunakan if __name__ guard untuk HF Spaces
398
+ if __name__ == "__main__":
399
+ demo.launch()