prithivMLmods commited on
Commit
338865f
·
verified ·
1 Parent(s): d73aead

incl. — a GPU Duration slider that dynamically allocates ZeroGPU time.

Browse files
Files changed (1) hide show
  1. app.py +124 -140
app.py CHANGED
@@ -5,45 +5,46 @@ import json
5
  import ast
6
  import re
7
  from io import BytesIO
 
 
 
 
8
  import torch
9
  import spaces
10
- import numpy as np
11
  from PIL import Image, ImageDraw, ImageFont
12
  import supervision as sv
13
- from typing import Iterable
14
- import gradio as gr
15
- from gradio import Server
16
- from fastapi.responses import HTMLResponse
17
- from threading import Thread
18
  from transformers import (
19
  Qwen3_5ForConditionalGeneration,
20
  AutoProcessor,
21
  TextIteratorStreamer,
22
  )
23
 
24
- # ------------------------------------------------------------------
25
- # Config & Constants
26
- # ------------------------------------------------------------------
 
 
27
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
28
  DTYPE = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
29
  MODEL_NAME = "Qwen/Qwen3.8-27B"
30
 
 
 
 
 
31
  BRIGHT_YELLOW = sv.Color(r=255, g=230, b=0)
32
  DARK_OUTLINE = sv.Color(r=40, g=40, b=40)
33
  BLACK = sv.Color(r=0, g=0, b=0)
34
  WHITE = sv.Color(r=255, g=255, b=255)
35
 
36
- # Spatial path colors
37
  SPATIAL_LINE = (255, 69, 0) # OrangeRed
38
  SPATIAL_DOT = (255, 69, 0)
39
  SPATIAL_RING = (255, 255, 255)
40
- SPATIAL_LABEL_BG = (80, 20, 0)
41
  SPATIAL_LABEL_TXT = (255, 255, 255)
42
- SPATIAL_ARROW = (255, 165, 0) # Orange for direction
43
 
44
- # ------------------------------------------------------------------
45
- # Model Loading
46
- # ------------------------------------------------------------------
47
  print(f"Loading model: {MODEL_NAME} ...")
48
  qwen_model = Qwen3_5ForConditionalGeneration.from_pretrained(
49
  MODEL_NAME, torch_dtype=DTYPE, device_map=DEVICE, attn_implementation="kernels-community/flash-attn2@v3",
@@ -51,78 +52,22 @@ qwen_model = Qwen3_5ForConditionalGeneration.from_pretrained(
51
  qwen_processor = AutoProcessor.from_pretrained(MODEL_NAME)
52
  print("Model loaded.")
53
 
54
- # ------------------------------------------------------------------
55
- # Examples Config
56
- # ------------------------------------------------------------------
57
- EXAMPLES_CONFIG = [
58
- {"image": "examples/1.jpg", "prompt": "Detect the yellow car that is parked.", "mode": "Detect"},
59
- {"image": "examples/2.jpg", "prompt": "Point to all the red cars.", "mode": "Point"},
60
- {"image": "examples/3.jpg", "prompt": "Map a path from the door to the lamp.", "mode": "Spatial"},
61
- ]
62
-
63
- def make_thumb_b64(path, max_dim=220):
64
- if not os.path.exists(path):
65
- return ""
66
- try:
67
- img = Image.open(path).convert("RGB")
68
- img.thumbnail((max_dim, max_dim), Image.LANCZOS)
69
- buf = BytesIO()
70
- img.save(buf, format="JPEG", quality=65)
71
- return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
72
- except Exception as e:
73
- return ""
74
-
75
- def encode_full_image(path):
76
- if not os.path.exists(path):
77
- return ""
78
- try:
79
- with open(path, "rb") as f:
80
- data = f.read()
81
- ext = path.rsplit(".", 1)[-1].lower()
82
- mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
83
- return f"data:{mime};base64,{base64.b64encode(data).decode()}"
84
- except Exception as e:
85
- return ""
86
-
87
- def build_client_config():
88
- examples = []
89
- for i, ex in enumerate(EXAMPLES_CONFIG):
90
- examples.append({
91
- "idx": i,
92
- "thumb": make_thumb_b64(ex["image"]),
93
- "prompt": ex["prompt"],
94
- "mode": ex["mode"],
95
- })
96
- return {"examples": examples, "modes": ["Detect", "Point", "Spatial"], "default_mode": "Detect"}
97
-
98
- print("Building client config…")
99
- CLIENT_CONFIG = build_client_config()
100
-
101
- # ------------------------------------------------------------------
102
- # Helpers
103
- # ------------------------------------------------------------------
104
  def safe_parse_json(text: str):
105
  text = re.sub(r"```(json)?", "", text).strip()
106
  match = re.search(r'(\[.*\]|\{.*\})', text, re.DOTALL)
107
  if match:
108
  json_str = match.group(1)
109
  json_str_clean = re.sub(r',\s*([}\]])', r'\1', json_str)
110
- try:
111
- return json.loads(json_str_clean)
112
  except json.JSONDecodeError:
113
- try:
114
- return ast.literal_eval(json_str_clean)
115
- except Exception:
116
- pass
117
  text_clean = re.sub(r',\s*([}\]])', r'\1', text)
118
- try:
119
- return json.loads(text_clean)
120
- except json.JSONDecodeError:
121
- pass
122
- try:
123
- return ast.literal_eval(text_clean)
124
- except Exception:
125
- pass
126
  return []
127
 
128
  def _extract_point(item: dict):
@@ -141,23 +86,17 @@ def _extract_bbox(item: dict):
141
 
142
  def _load_font(size: int = 16):
143
  size = max(6, int(size))
144
- try:
145
- return ImageFont.truetype("arial.ttf", size)
146
  except (IOError, OSError):
147
- try:
148
- return ImageFont.truetype("DejaVuSans.ttf", size)
149
- except (IOError, OSError):
150
- return ImageFont.load_default()
151
-
152
- def pil_to_b64_png(image: Image.Image) -> str:
153
- buf = BytesIO()
154
- image.save(buf, format="PNG")
155
- return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
156
 
 
157
  def annotate_image(image: Image.Image, result: dict, point_radius: int = 6, box_thickness: int = 2, text_scale: float = 0.5):
158
  if not isinstance(image, Image.Image) or not isinstance(result, dict): return image
159
  image = image.convert("RGB")
160
  ow, oh = image.size
 
161
  point_radius = max(1, int(point_radius))
162
  box_thickness = max(1, int(box_thickness))
163
  text_scale = max(0.1, float(text_scale))
@@ -260,8 +199,10 @@ def annotate_spatial_path(image: Image.Image, result: dict, dot_radius: int = 6,
260
  halo_r = dot_radius + 8
261
  ring_r = dot_radius + 3
262
  draw.ellipse((cx - halo_r, cy - halo_r, cx + halo_r, cy + halo_r), fill=SPATIAL_LINE + (50,))
263
- draw.ellipse((cx - ring_r, cy - ring_r, cx + ring_r, cy + ring_r), outline=SPATIAL_RING, width=max(1, round(3 * scale_ratio)))
264
- draw.ellipse((cx - dot_radius, cy - dot_radius, cx + dot_radius, cy + dot_radius), fill=SPATIAL_DOT, outline=SPATIAL_DOT)
 
 
265
  num_text = str(i + 1)
266
  nbbox = draw.textbbox((0, 0), num_text, font=font_num)
267
  nw = nbbox[2] - nbbox[0]
@@ -275,7 +216,11 @@ def annotate_spatial_path(image: Image.Image, result: dict, dot_radius: int = 6,
275
  tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
276
  lx, ly = cx + dot_radius + 10, cy - th - 8
277
  pad = 5
278
- draw.rectangle((lx - pad, ly - pad, lx + tw + pad, ly + th + pad), fill=SPATIAL_LABEL_BG, outline=SPATIAL_LINE, width=1)
 
 
 
 
279
  draw.text((lx, ly), label, fill=SPATIAL_LABEL_TXT, font=font_label)
280
 
281
  n_pts = len(pts)
@@ -284,35 +229,86 @@ def annotate_spatial_path(image: Image.Image, result: dict, dot_radius: int = 6,
284
  bbox = draw.textbbox((0, 0), legend_text, font=legend_font)
285
  tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
286
  fx, fy = 10, h - th - 22
287
- draw.rectangle((fx - 8, fy - 6, fx + tw + 16, fy + th + 10), fill=SPATIAL_LABEL_BG + (220,))
 
288
  draw.text((fx, fy), legend_text, fill=SPATIAL_LABEL_TXT, font=legend_font)
289
 
290
  return image
291
 
292
- # ------------------------------------------------------------------
293
- # Gradio Server (Server mode): FastAPI + Gradio queue/API engine
294
- # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  app = Server(title="Qwen3.8-27B-Object-Detection")
296
 
297
  @app.mcp.tool(name="run_inference")
298
  @app.api(name="run_inference")
299
- @spaces.GPU(size="xlarge", duration=90)
300
  def infer(
301
  image_b64: str,
302
- mode: str,
303
  prompt: str,
 
304
  point_radius: int,
305
  box_thickness: int,
306
  text_scale: float,
 
307
  ) -> dict:
308
- """Runs object detection, point localization, or spatial mapping."""
309
  gc.collect()
310
  torch.cuda.empty_cache()
311
 
312
- if not image_b64:
313
- raise gr.Error("Please upload an image.")
314
- if not prompt or prompt.strip() == "":
315
- raise gr.Error("Please provide a prompt.")
316
 
317
  try:
318
  header, data = image_b64.split(",", 1)
@@ -321,21 +317,20 @@ def infer(
321
  raise gr.Error(f"Invalid image data: {e}")
322
 
323
  pil_image.thumbnail((512, 512))
324
- category = mode
325
 
326
- if category == "Detect":
327
  full_prompt = (
328
  f"Provide bounding box coordinates for {prompt}. "
329
  f"Report strictly in JSON format as a list of objects with 'label' and "
330
  f"'bbox_2d' (xmin, ymin, xmax, ymax in 0-1000 scale)."
331
  )
332
- elif category == "Point":
333
  full_prompt = (
334
  f"Provide 2d point coordinates for {prompt}. "
335
  f"Report strictly in JSON format as a list of objects with 'label' and "
336
  f"'point_2d' (x, y in 0-1000 scale)."
337
  )
338
- elif category == "Spatial":
339
  full_prompt = (
340
  f"Identify the key spatial waypoints to map a path/route for: {prompt}. "
341
  f"Return the points in the order they should be connected along the path, "
@@ -353,6 +348,7 @@ def infer(
353
  {"type": "text", "text": full_prompt},
354
  ],
355
  }]
 
356
  text = qwen_processor.apply_chat_template(
357
  messages, tokenize=False, add_generation_prompt=True
358
  )
@@ -381,10 +377,8 @@ def infer(
381
  full_text += tok
382
  thread.join()
383
 
384
- result_text = full_text
385
- result_image = pil_image.copy()
386
-
387
- if category == "Point":
388
  parsed = safe_parse_json(full_text)
389
  if isinstance(parsed, dict):
390
  for k in ["points", "keypoints", "point"]:
@@ -392,9 +386,7 @@ def infer(
392
  parsed = parsed[k]
393
  break
394
  else:
395
- for v in parsed.values():
396
- if isinstance(v, list): parsed = v; break
397
- else: parsed = []
398
 
399
  result = {"points": []}
400
  if isinstance(parsed, list):
@@ -405,12 +397,12 @@ def infer(
405
  result["points"].append({"label": item.get("label", ""), "x": x / 1000.0, "y": y / 1000.0})
406
 
407
  if result["points"]:
408
- result_image = annotate_image(pil_image.copy(), result, point_radius, box_thickness, text_scale)
409
- result_text = json.dumps(result, indent=2)
410
  else:
411
- result_text = f"Could not extract any points.\nRaw model output:\n{full_text}"
412
 
413
- elif category == "Detect":
414
  parsed = safe_parse_json(full_text)
415
  if isinstance(parsed, dict):
416
  for k in ["objects", "detections", "bboxes", "boxes", "results"]:
@@ -418,9 +410,7 @@ def infer(
418
  parsed = parsed[k]
419
  break
420
  else:
421
- for v in parsed.values():
422
- if isinstance(v, list): parsed = v; break
423
- else: parsed = []
424
 
425
  result = {"objects": []}
426
  if isinstance(parsed, list):
@@ -435,12 +425,12 @@ def infer(
435
  })
436
 
437
  if result["objects"]:
438
- result_image = annotate_image(pil_image.copy(), result, point_radius, box_thickness, text_scale)
439
- result_text = json.dumps(result, indent=2)
440
  else:
441
- result_text = f"Could not extract any objects.\nRaw model output:\n{full_text}"
442
 
443
- elif category == "Spatial":
444
  parsed = safe_parse_json(full_text)
445
  if isinstance(parsed, dict):
446
  for k in ["points", "waypoints", "path", "route", "nodes", "map"]:
@@ -448,9 +438,7 @@ def infer(
448
  parsed = parsed[k]
449
  break
450
  else:
451
- for v in parsed.values():
452
- if isinstance(v, list): parsed = v; break
453
- else: parsed = []
454
 
455
  result = {"points": []}
456
  if isinstance(parsed, list):
@@ -461,17 +449,13 @@ def infer(
461
  result["points"].append({"label": item.get("label", "waypoint"), "x": x / 1000.0, "y": y / 1000.0})
462
 
463
  if result["points"]:
464
- wp_lines = "\n".join(f" {i+1}. {p['label']} → ({p['x']:.3f}, {p['y']:.3f})" for i, p in enumerate(result["points"]))
465
- result_text = (
466
- f"Spatial map generated.\n"
467
- f"Waypoints ({len(result['points'])}):\n{wp_lines}\n"
468
- f"Path segments: {max(0, len(result['points']) - 1)}"
469
- )
470
- result_image = annotate_spatial_path(pil_image.copy(), result, point_radius, box_thickness * 2, text_scale)
471
  else:
472
- result_text = f"Could not extract any spatial waypoints.\nRaw model output:\n{full_text}"
 
 
473
 
474
- return {"image": pil_to_b64_png(result_image), "text": result_text}
475
 
476
  @app.api(name="load_example", queue=False)
477
  def load_example(idx: float) -> dict:
@@ -480,13 +464,13 @@ def load_example(idx: float) -> dict:
480
  except (ValueError, TypeError):
481
  i = -1
482
  if i < 0 or i >= len(EXAMPLES_CONFIG):
483
- return {"image": "", "prompt": "", "mode": "Detect", "name": "", "status": "error"}
484
  ex = EXAMPLES_CONFIG[i]
485
  b64 = encode_full_image(ex["image"])
486
  return {
487
  "image": b64,
488
  "prompt": ex["prompt"],
489
- "mode": ex["mode"],
490
  "name": os.path.basename(ex["image"]),
491
  "status": "ok" if b64 else "error"
492
  }
 
5
  import ast
6
  import re
7
  from io import BytesIO
8
+ from typing import Iterable
9
+ from threading import Thread
10
+
11
+ import numpy as np
12
  import torch
13
  import spaces
 
14
  from PIL import Image, ImageDraw, ImageFont
15
  import supervision as sv
 
 
 
 
 
16
  from transformers import (
17
  Qwen3_5ForConditionalGeneration,
18
  AutoProcessor,
19
  TextIteratorStreamer,
20
  )
21
 
22
+ import gradio as gr
23
+ from gradio import Server
24
+ from fastapi.responses import HTMLResponse
25
+
26
+ # --- Config ---
27
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
28
  DTYPE = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
29
  MODEL_NAME = "Qwen/Qwen3.8-27B"
30
 
31
+ GPU_DURATIONS = [60, 90, 120, 150, 180, 250, 300]
32
+ DEFAULT_GPU_DURATION_IDX = 1
33
+
34
+ # --- Colors for Annotations ---
35
  BRIGHT_YELLOW = sv.Color(r=255, g=230, b=0)
36
  DARK_OUTLINE = sv.Color(r=40, g=40, b=40)
37
  BLACK = sv.Color(r=0, g=0, b=0)
38
  WHITE = sv.Color(r=255, g=255, b=255)
39
 
 
40
  SPATIAL_LINE = (255, 69, 0) # OrangeRed
41
  SPATIAL_DOT = (255, 69, 0)
42
  SPATIAL_RING = (255, 255, 255)
43
+ SPATIAL_LABEL_BG = (80, 25, 0)
44
  SPATIAL_LABEL_TXT = (255, 255, 255)
45
+ SPATIAL_ARROW = (230, 149, 0)
46
 
47
+ # --- Model Loading ---
 
 
48
  print(f"Loading model: {MODEL_NAME} ...")
49
  qwen_model = Qwen3_5ForConditionalGeneration.from_pretrained(
50
  MODEL_NAME, torch_dtype=DTYPE, device_map=DEVICE, attn_implementation="kernels-community/flash-attn2@v3",
 
52
  qwen_processor = AutoProcessor.from_pretrained(MODEL_NAME)
53
  print("Model loaded.")
54
 
55
+ # --- JSON Parsing & Extraction ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def safe_parse_json(text: str):
57
  text = re.sub(r"```(json)?", "", text).strip()
58
  match = re.search(r'(\[.*\]|\{.*\})', text, re.DOTALL)
59
  if match:
60
  json_str = match.group(1)
61
  json_str_clean = re.sub(r',\s*([}\]])', r'\1', json_str)
62
+ try: return json.loads(json_str_clean)
 
63
  except json.JSONDecodeError:
64
+ try: return ast.literal_eval(json_str_clean)
65
+ except Exception: pass
 
 
66
  text_clean = re.sub(r',\s*([}\]])', r'\1', text)
67
+ try: return json.loads(text_clean)
68
+ except json.JSONDecodeError: pass
69
+ try: return ast.literal_eval(text_clean)
70
+ except Exception: pass
 
 
 
 
71
  return []
72
 
73
  def _extract_point(item: dict):
 
86
 
87
  def _load_font(size: int = 16):
88
  size = max(6, int(size))
89
+ try: return ImageFont.truetype("arial.ttf", size)
 
90
  except (IOError, OSError):
91
+ try: return ImageFont.truetype("DejaVuSans.ttf", size)
92
+ except (IOError, OSError): return ImageFont.load_default()
 
 
 
 
 
 
 
93
 
94
+ # --- Annotation Functions ---
95
  def annotate_image(image: Image.Image, result: dict, point_radius: int = 6, box_thickness: int = 2, text_scale: float = 0.5):
96
  if not isinstance(image, Image.Image) or not isinstance(result, dict): return image
97
  image = image.convert("RGB")
98
  ow, oh = image.size
99
+
100
  point_radius = max(1, int(point_radius))
101
  box_thickness = max(1, int(box_thickness))
102
  text_scale = max(0.1, float(text_scale))
 
199
  halo_r = dot_radius + 8
200
  ring_r = dot_radius + 3
201
  draw.ellipse((cx - halo_r, cy - halo_r, cx + halo_r, cy + halo_r), fill=SPATIAL_LINE + (50,))
202
+ draw.ellipse((cx - ring_r, cy - ring_r, cx + ring_r, cy + ring_r),
203
+ outline=SPATIAL_RING, width=max(1, round(3 * scale_ratio)))
204
+ draw.ellipse((cx - dot_radius, cy - dot_radius, cx + dot_radius, cy + dot_radius),
205
+ fill=SPATIAL_DOT, outline=SPATIAL_DOT)
206
  num_text = str(i + 1)
207
  nbbox = draw.textbbox((0, 0), num_text, font=font_num)
208
  nw = nbbox[2] - nbbox[0]
 
216
  tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
217
  lx, ly = cx + dot_radius + 10, cy - th - 8
218
  pad = 5
219
+ draw.rectangle(
220
+ (lx - pad, ly - pad, lx + tw + pad, ly + th + pad),
221
+ fill=SPATIAL_LABEL_BG,
222
+ outline=SPATIAL_LINE, width=1,
223
+ )
224
  draw.text((lx, ly), label, fill=SPATIAL_LABEL_TXT, font=font_label)
225
 
226
  n_pts = len(pts)
 
229
  bbox = draw.textbbox((0, 0), legend_text, font=legend_font)
230
  tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
231
  fx, fy = 10, h - th - 22
232
+ draw.rectangle((fx - 8, fy - 6, fx + tw + 16, fy + th + 10),
233
+ fill=SPATIAL_LABEL_BG + (220,))
234
  draw.text((fx, fy), legend_text, fill=SPATIAL_LABEL_TXT, font=legend_font)
235
 
236
  return image
237
 
238
+ def pil_to_b64_png(image: Image.Image) -> str:
239
+ buf = BytesIO()
240
+ image.save(buf, format="PNG")
241
+ return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
242
+
243
+ def get_gpu_duration(image_b64, prompt, task, point_radius, box_thickness, text_scale, gpu_duration_seconds):
244
+ """Dynamically returns GPU duration based on the UI slider value."""
245
+ try:
246
+ return int(gpu_duration_seconds)
247
+ except (TypeError, ValueError):
248
+ return GPU_DURATIONS[DEFAULT_GPU_DURATION_IDX]
249
+
250
+ # --- Config Examples ---
251
+ EXAMPLES_CONFIG = [
252
+ {"image": "examples/1.jpg", "prompt": "Detect the yellow car that is parked.", "task": "Detect"},
253
+ {"image": "examples/2.jpg", "prompt": "Point to all the red cars.", "task": "Point"},
254
+ {"image": "examples/3.jpg", "prompt": "Map a path from the door to the lamp.", "task": "Spatial"},
255
+ ]
256
+
257
+ def make_thumb_b64(path, max_dim=220):
258
+ if not os.path.exists(path): return ""
259
+ try:
260
+ img = Image.open(path).convert("RGB")
261
+ img.thumbnail((max_dim, max_dim), Image.LANCZOS)
262
+ buf = BytesIO()
263
+ img.save(buf, format="JPEG", quality=65)
264
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
265
+ except Exception as e:
266
+ return ""
267
+
268
+ def encode_full_image(path):
269
+ if not os.path.exists(path): return ""
270
+ try:
271
+ with open(path, "rb") as f: data = f.read()
272
+ ext = path.rsplit(".", 1)[-1].lower()
273
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
274
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
275
+ except Exception as e:
276
+ return ""
277
+
278
+ def build_client_config():
279
+ examples = []
280
+ for i, ex in enumerate(EXAMPLES_CONFIG):
281
+ examples.append({
282
+ "idx": i,
283
+ "thumb": make_thumb_b64(ex["image"]),
284
+ "prompt": ex["prompt"],
285
+ "task": ex["task"],
286
+ })
287
+ return {"examples": examples}
288
+
289
+ CLIENT_CONFIG = build_client_config()
290
+
291
+ # --- Gradio Server ---
292
  app = Server(title="Qwen3.8-27B-Object-Detection")
293
 
294
  @app.mcp.tool(name="run_inference")
295
  @app.api(name="run_inference")
296
+ @spaces.GPU(size="xlarge", duration=get_gpu_duration)
297
  def infer(
298
  image_b64: str,
 
299
  prompt: str,
300
+ task: str,
301
  point_radius: int,
302
  box_thickness: int,
303
  text_scale: float,
304
+ gpu_duration_seconds: int,
305
  ) -> dict:
306
+ """Runs Qwen3.8 vision model for detection, point localization, or spatial mapping."""
307
  gc.collect()
308
  torch.cuda.empty_cache()
309
 
310
+ if not image_b64: raise gr.Error("Please upload an image.")
311
+ if not prompt or not prompt.strip(): raise gr.Error("Please provide a prompt.")
 
 
312
 
313
  try:
314
  header, data = image_b64.split(",", 1)
 
317
  raise gr.Error(f"Invalid image data: {e}")
318
 
319
  pil_image.thumbnail((512, 512))
 
320
 
321
+ if task == "Detect":
322
  full_prompt = (
323
  f"Provide bounding box coordinates for {prompt}. "
324
  f"Report strictly in JSON format as a list of objects with 'label' and "
325
  f"'bbox_2d' (xmin, ymin, xmax, ymax in 0-1000 scale)."
326
  )
327
+ elif task == "Point":
328
  full_prompt = (
329
  f"Provide 2d point coordinates for {prompt}. "
330
  f"Report strictly in JSON format as a list of objects with 'label' and "
331
  f"'point_2d' (x, y in 0-1000 scale)."
332
  )
333
+ elif task == "Spatial":
334
  full_prompt = (
335
  f"Identify the key spatial waypoints to map a path/route for: {prompt}. "
336
  f"Return the points in the order they should be connected along the path, "
 
348
  {"type": "text", "text": full_prompt},
349
  ],
350
  }]
351
+
352
  text = qwen_processor.apply_chat_template(
353
  messages, tokenize=False, add_generation_prompt=True
354
  )
 
377
  full_text += tok
378
  thread.join()
379
 
380
+ # --- Post-process ---
381
+ if task == "Point":
 
 
382
  parsed = safe_parse_json(full_text)
383
  if isinstance(parsed, dict):
384
  for k in ["points", "keypoints", "point"]:
 
386
  parsed = parsed[k]
387
  break
388
  else:
389
+ parsed = next((v for v in parsed.values() if isinstance(v, list)), [])
 
 
390
 
391
  result = {"points": []}
392
  if isinstance(parsed, list):
 
397
  result["points"].append({"label": item.get("label", ""), "x": x / 1000.0, "y": y / 1000.0})
398
 
399
  if result["points"]:
400
+ annotated_img = annotate_image(pil_image.copy(), result, point_radius=point_radius, box_thickness=box_thickness, text_scale=text_scale)
401
+ return {"image": pil_to_b64_png(annotated_img), "text": json.dumps(result, indent=2)}
402
  else:
403
+ return {"image": pil_to_b64_png(pil_image), "text": f"Could not extract any points.\nRaw model output:\n{full_text}"}
404
 
405
+ elif task == "Detect":
406
  parsed = safe_parse_json(full_text)
407
  if isinstance(parsed, dict):
408
  for k in ["objects", "detections", "bboxes", "boxes", "results"]:
 
410
  parsed = parsed[k]
411
  break
412
  else:
413
+ parsed = next((v for v in parsed.values() if isinstance(v, list)), [])
 
 
414
 
415
  result = {"objects": []}
416
  if isinstance(parsed, list):
 
425
  })
426
 
427
  if result["objects"]:
428
+ annotated_img = annotate_image(pil_image.copy(), result, point_radius=point_radius, box_thickness=box_thickness, text_scale=text_scale)
429
+ return {"image": pil_to_b64_png(annotated_img), "text": json.dumps(result, indent=2)}
430
  else:
431
+ return {"image": pil_to_b64_png(pil_image), "text": f"Could not extract any objects.\nRaw model output:\n{full_text}"}
432
 
433
+ elif task == "Spatial":
434
  parsed = safe_parse_json(full_text)
435
  if isinstance(parsed, dict):
436
  for k in ["points", "waypoints", "path", "route", "nodes", "map"]:
 
438
  parsed = parsed[k]
439
  break
440
  else:
441
+ parsed = next((v for v in parsed.values() if isinstance(v, list)), [])
 
 
442
 
443
  result = {"points": []}
444
  if isinstance(parsed, list):
 
449
  result["points"].append({"label": item.get("label", "waypoint"), "x": x / 1000.0, "y": y / 1000.0})
450
 
451
  if result["points"]:
452
+ annotated_img = annotate_spatial_path(pil_image.copy(), result, dot_radius=point_radius, line_width=box_thickness * 2, text_scale=text_scale)
453
+ return {"image": pil_to_b64_png(annotated_img), "text": json.dumps(result, indent=2)}
 
 
 
 
 
454
  else:
455
+ return {"image": pil_to_b64_png(pil_image), "text": f"Could not extract any spatial waypoints.\nRaw model output:\n{full_text}"}
456
+
457
+ return {"image": pil_to_b64_png(pil_image), "text": full_text}
458
 
 
459
 
460
  @app.api(name="load_example", queue=False)
461
  def load_example(idx: float) -> dict:
 
464
  except (ValueError, TypeError):
465
  i = -1
466
  if i < 0 or i >= len(EXAMPLES_CONFIG):
467
+ return {"image": "", "prompt": "", "task": "", "name": "", "status": "error"}
468
  ex = EXAMPLES_CONFIG[i]
469
  b64 = encode_full_image(ex["image"])
470
  return {
471
  "image": b64,
472
  "prompt": ex["prompt"],
473
+ "task": ex["task"],
474
  "name": os.path.basename(ex["image"]),
475
  "status": "ok" if b64 else "error"
476
  }