Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
π₯
|
| 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
|
| 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
|
| 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 |
-
# =====================
|
| 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 |
-
|
| 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("β
|
| 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 |
-
"
|
| 214 |
-
|
|
|
|
|
|
|
| 215 |
"predicted_class": self.idx_to_class.get(best_idx, "unknown")
|
| 216 |
}
|
| 217 |
|
| 218 |
manager = ModelManager()
|
| 219 |
|
| 220 |
-
# =====================
|
| 221 |
-
@spaces.GPU(request_timeout=
|
| 222 |
-
def solve_gradio(challenge_img,
|
| 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 "
|
| 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 = ["π―
|
| 241 |
-
f"Best Option: {best_idx + 1}",
|
| 242 |
f"Confidence: {result['confidence']:.4f} ({result['confidence']*100:.2f}%)",
|
| 243 |
-
f"
|
| 244 |
-
"π
|
| 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(
|
| 259 |
-
return f"
|
|
|
|
| 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=
|
| 269 |
with gr.Column(scale=2):
|
| 270 |
-
gr.Markdown("### 8 Option Images")
|
| 271 |
with gr.Row():
|
| 272 |
-
|
| 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=
|
| 278 |
-
|
| 279 |
|
| 280 |
solve_btn.click(
|
| 281 |
fn=solve_gradio,
|
| 282 |
-
inputs=[challenge_input] +
|
| 283 |
-
outputs=[result_text,
|
| 284 |
)
|
| 285 |
|
| 286 |
-
|
| 287 |
-
app = FastAPI(title="Icon Solver")
|
| 288 |
-
app = gr.mount_gradio_app(app, demo, path="/")
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 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()
|
|
|