nielsr HF Staff commited on
Commit
3e3cacd
·
verified ·
1 Parent(s): d8a5bbf

Add box-prompt examples and editable prompts

Browse files
README.md CHANGED
@@ -15,9 +15,9 @@ short_description: A Space to try the new Qwen 3.8 Max for free
15
 
16
  Try Qwen 3.8 Max with text and image inputs. Thinking mode is enabled by default; its reasoning summary appears in a collapsible panel while the final answer streams token by token.
17
 
18
- The Space also includes a box-prompting playground inspired by [Roboflow's Qwen 3.8 Max evaluation](https://blog.roboflow.com/qwen3-8-max/): mark positive and optional negative examples directly on an image, then ask Qwen to find similar objects. Detection results are parsed from normalized XYXY coordinates and rendered back onto the original image.
19
 
20
- This is an unofficial community demo, generously backed by a limited pool of promotional Qwen API tokens. Availability is best-effort and the demo may be paused when the pool is exhausted.
21
 
22
  ## Configuration
23
 
 
15
 
16
  Try Qwen 3.8 Max with text and image inputs. Thinking mode is enabled by default; its reasoning summary appears in a collapsible panel while the final answer streams token by token.
17
 
18
+ The Space also includes a box-prompting playground inspired by [Roboflow's Qwen 3.8 Max evaluation](https://blog.roboflow.com/qwen3-8-max/): mark positive and optional negative examples directly on an image, then ask Qwen to find similar objects. Three ready-made presets demonstrate shape, color, and size matching; each loads an editable instruction and example boxes. Detection results are parsed from normalized XYXY coordinates and rendered back onto the original image.
19
 
20
+ This is an unofficial community demo, generously backed by a limited pool of Qwen API tokens. Availability is best-effort and the demo may be paused when the pool is exhausted.
21
 
22
  ## Configuration
23
 
app.py CHANGED
@@ -44,6 +44,35 @@ VISUAL_EXAMPLES = [
44
  "prompt": "Solve the visual logic puzzle. State the rule you infer and identify the missing tile.",
45
  },
46
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  _requests_by_session: dict[str, deque[float]] = defaultdict(deque)
49
  _rate_limit_lock = Lock()
@@ -304,15 +333,43 @@ def _draw_prompt_boxes(
304
 
305
  def load_box_image(
306
  image_path: str | None,
307
- ) -> tuple[str | None, list[Any], None, str | None, str]:
308
  if not image_path:
309
- return None, [], None, None, "Upload an image to begin."
310
  return (
311
  image_path,
312
  [],
313
  None,
314
  image_path,
315
  "Choose Positive or Negative, then click two opposite corners of an object.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  )
317
 
318
 
@@ -414,6 +471,7 @@ def _parse_detection_boxes(answer: str, width: int, height: int) -> list[tuple[t
414
  def detect_similar(
415
  original_path: str | None,
416
  boxes: list[dict[str, Any]] | None,
 
417
  enable_thinking: bool,
418
  max_output_tokens: int,
419
  request: gr.Request,
@@ -426,9 +484,14 @@ def detect_similar(
426
  _check_rate_limit(getattr(request, "session_hash", None))
427
 
428
  annotated_image = _draw_prompt_boxes(original_path, annotations)
 
 
 
429
  prompt = f"""
430
  The image contains visual box prompts drawn by the user. Green boxes marked with + are positive examples of the object to find. Red boxes marked with − are negative examples that must be ignored.
431
 
 
 
432
  Find every other unboxed object in this same image that matches the positive examples while respecting the negative examples. Return only valid JSON in this exact shape:
433
  {{"objects": [{{"label": "match", "box_2d": [x_min, y_min, x_max, y_max]}}]}}
434
 
@@ -508,6 +571,7 @@ body > gradio-app, gradio-app {
508
  #chat-shell { max-width: 840px; margin: 0 auto; width: 100%; }
509
  #box-shell { max-width: 960px; margin: 0 auto; width: 100%; }
510
  #examples-gallery { margin-top: 0.35rem; }
 
511
  #example-prompts { color: var(--body-text-color-subdued); font-size: 0.92rem; }
512
  @media (max-width: 560px) {
513
  .gradio-container { width: calc(100% - 16px) !important; }
@@ -529,7 +593,7 @@ with gr.Blocks(title="Free Qwen 3.8 Max") as demo:
529
  <img src="{QWEN_LOGO_DATA_URL}" alt="Qwen logo">
530
  <h1>Free Qwen 3.8 Max</h1>
531
  </div>
532
- <p><strong>100 million promotional tokens, shared with the community.</strong> Ask anything or attach up to three images to test Qwen's visual understanding.</p>
533
  """,
534
  elem_id="hero",
535
  )
@@ -611,6 +675,20 @@ with gr.Blocks(title="Free Qwen 3.8 Max") as demo:
611
  Upload an image, choose **Positive** or **Negative**, and click two opposite corners to mark each example. Qwen will find other objects that look like the positive examples while avoiding the negatives.
612
  """
613
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  with gr.Row():
615
  with gr.Column(scale=3):
616
  box_image = gr.Image(
@@ -622,6 +700,11 @@ with gr.Blocks(title="Free Qwen 3.8 Max") as demo:
622
  )
623
  box_status = gr.Markdown("Upload an image to begin.")
624
  with gr.Column(scale=1, min_width=220):
 
 
 
 
 
625
  box_kind = gr.Radio(
626
  ["Positive", "Negative"],
627
  value="Positive",
@@ -666,7 +749,29 @@ with gr.Blocks(title="Free Qwen 3.8 Max") as demo:
666
  box_image.upload(
667
  load_box_image,
668
  inputs=box_image,
669
- outputs=[box_original, box_annotations, pending_corner, box_image, box_status],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  queue=False,
671
  api_name=False,
672
  )
@@ -693,7 +798,7 @@ with gr.Blocks(title="Free Qwen 3.8 Max") as demo:
693
  )
694
  run_box_prompt.click(
695
  detect_similar,
696
- inputs=[box_original, box_annotations, thinking, max_tokens],
697
  outputs=[box_result, box_details],
698
  concurrency_limit=1,
699
  concurrency_id="qwen-api",
 
44
  "prompt": "Solve the visual logic puzzle. State the rule you infer and identify the missing tile.",
45
  },
46
  ]
47
+ BOX_EXAMPLES = [
48
+ {
49
+ "title": "Blue circles, not squares",
50
+ "image": str(EXAMPLES_DIR / "crowded_shapes.png"),
51
+ "prompt": "Find every other blue circle. Ignore blue squares and shapes of other colors.",
52
+ "boxes": [
53
+ {"box": [761, 81, 849, 169], "kind": "positive"},
54
+ {"box": [205, 96, 281, 172], "kind": "negative"},
55
+ ],
56
+ },
57
+ {
58
+ "title": "Matching mint bottles",
59
+ "image": str(EXAMPLES_DIR / "box_product_shelf.png"),
60
+ "prompt": "Find every other teal MINT bottle. Ignore the purple MINT bottles and all cans.",
61
+ "boxes": [
62
+ {"box": [80, 118, 142, 292], "kind": "positive"},
63
+ {"box": [780, 118, 842, 292], "kind": "negative"},
64
+ ],
65
+ },
66
+ {
67
+ "title": "Standard green cars",
68
+ "image": str(EXAMPLES_DIR / "box_parking_lot.png"),
69
+ "prompt": "Find the other standard-size green cars. Do not include the oversized green vehicle.",
70
+ "boxes": [
71
+ {"box": [88, 112, 172, 262], "kind": "positive"},
72
+ {"box": [793, 340, 905, 518], "kind": "negative"},
73
+ ],
74
+ },
75
+ ]
76
 
77
  _requests_by_session: dict[str, deque[float]] = defaultdict(deque)
78
  _rate_limit_lock = Lock()
 
333
 
334
  def load_box_image(
335
  image_path: str | None,
336
+ ) -> tuple[str | None, list[Any], None, str | None, str, str]:
337
  if not image_path:
338
+ return None, [], None, None, "Upload an image to begin.", ""
339
  return (
340
  image_path,
341
  [],
342
  None,
343
  image_path,
344
  "Choose Positive or Negative, then click two opposite corners of an object.",
345
+ "",
346
+ )
347
+
348
+
349
+ def load_box_example(
350
+ event: gr.SelectData,
351
+ ) -> tuple[str, list[dict[str, Any]], None, Image.Image, str, str, None, str]:
352
+ example = BOX_EXAMPLES[int(event.index)]
353
+ original_path = example["image"]
354
+ annotations = [
355
+ {"box": list(annotation["box"]), "kind": annotation["kind"]}
356
+ for annotation in example["boxes"]
357
+ ]
358
+ positive_count = sum(box["kind"] == "positive" for box in annotations)
359
+ negative_count = len(annotations) - positive_count
360
+ status = (
361
+ f"Loaded **{example['title']}** with {positive_count} positive and "
362
+ f"{negative_count} negative example box(es). Edit the prompt or run it as-is."
363
+ )
364
+ return (
365
+ original_path,
366
+ annotations,
367
+ None,
368
+ _draw_prompt_boxes(original_path, annotations),
369
+ status,
370
+ example["prompt"],
371
+ None,
372
+ "",
373
  )
374
 
375
 
 
471
  def detect_similar(
472
  original_path: str | None,
473
  boxes: list[dict[str, Any]] | None,
474
+ target_prompt: str,
475
  enable_thinking: bool,
476
  max_output_tokens: int,
477
  request: gr.Request,
 
484
  _check_rate_limit(getattr(request, "session_hash", None))
485
 
486
  annotated_image = _draw_prompt_boxes(original_path, annotations)
487
+ requested_target = target_prompt.strip() or (
488
+ "Find every other unboxed object that visually matches the positive examples."
489
+ )
490
  prompt = f"""
491
  The image contains visual box prompts drawn by the user. Green boxes marked with + are positive examples of the object to find. Red boxes marked with − are negative examples that must be ignored.
492
 
493
+ The user's instruction is: {requested_target}
494
+
495
  Find every other unboxed object in this same image that matches the positive examples while respecting the negative examples. Return only valid JSON in this exact shape:
496
  {{"objects": [{{"label": "match", "box_2d": [x_min, y_min, x_max, y_max]}}]}}
497
 
 
571
  #chat-shell { max-width: 840px; margin: 0 auto; width: 100%; }
572
  #box-shell { max-width: 960px; margin: 0 auto; width: 100%; }
573
  #examples-gallery { margin-top: 0.35rem; }
574
+ #box-examples-gallery { margin: 0.35rem 0 1rem; }
575
  #example-prompts { color: var(--body-text-color-subdued); font-size: 0.92rem; }
576
  @media (max-width: 560px) {
577
  .gradio-container { width: calc(100% - 16px) !important; }
 
593
  <img src="{QWEN_LOGO_DATA_URL}" alt="Qwen logo">
594
  <h1>Free Qwen 3.8 Max</h1>
595
  </div>
596
+ <p><strong>100 million tokens, shared with the community.</strong> Ask anything or attach up to three images to test Qwen's visual understanding.</p>
597
  """,
598
  elem_id="hero",
599
  )
 
675
  Upload an image, choose **Positive** or **Negative**, and click two opposite corners to mark each example. Qwen will find other objects that look like the positive examples while avoiding the negatives.
676
  """
677
  )
678
+ box_examples_gallery = gr.Gallery(
679
+ value=[
680
+ (example["image"], example["title"])
681
+ for example in BOX_EXAMPLES
682
+ ],
683
+ label="Box-prompt examples — click one to load its image, boxes, and prompt",
684
+ columns=3,
685
+ rows=1,
686
+ height=245,
687
+ object_fit="contain",
688
+ allow_preview=False,
689
+ buttons=[],
690
+ elem_id="box-examples-gallery",
691
+ )
692
  with gr.Row():
693
  with gr.Column(scale=3):
694
  box_image = gr.Image(
 
700
  )
701
  box_status = gr.Markdown("Upload an image to begin.")
702
  with gr.Column(scale=1, min_width=220):
703
+ box_target_prompt = gr.Textbox(
704
+ label="What should Qwen find?",
705
+ placeholder="For example: Find the other blue circles, but ignore blue squares.",
706
+ lines=4,
707
+ )
708
  box_kind = gr.Radio(
709
  ["Positive", "Negative"],
710
  value="Positive",
 
749
  box_image.upload(
750
  load_box_image,
751
  inputs=box_image,
752
+ outputs=[
753
+ box_original,
754
+ box_annotations,
755
+ pending_corner,
756
+ box_image,
757
+ box_status,
758
+ box_target_prompt,
759
+ ],
760
+ queue=False,
761
+ api_name=False,
762
+ )
763
+ box_examples_gallery.select(
764
+ load_box_example,
765
+ outputs=[
766
+ box_original,
767
+ box_annotations,
768
+ pending_corner,
769
+ box_image,
770
+ box_status,
771
+ box_target_prompt,
772
+ box_result,
773
+ box_details,
774
+ ],
775
  queue=False,
776
  api_name=False,
777
  )
 
798
  )
799
  run_box_prompt.click(
800
  detect_similar,
801
+ inputs=[box_original, box_annotations, box_target_prompt, thinking, max_tokens],
802
  outputs=[box_result, box_details],
803
  concurrency_limit=1,
804
  concurrency_id="qwen-api",
examples/box_parking_lot.png ADDED
examples/box_product_shelf.png ADDED
scripts/generate_examples.py CHANGED
@@ -82,7 +82,62 @@ def logic_grid() -> None:
82
  image.save(OUTPUT / "logic_grid.png")
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  if __name__ == "__main__":
86
  crowded_shapes()
87
  mini_receipt()
88
  logic_grid()
 
 
 
82
  image.save(OUTPUT / "logic_grid.png")
83
 
84
 
85
+ def product_shelf() -> None:
86
+ image = Image.new("RGB", (960, 600), "#f4efe6")
87
+ draw = ImageDraw.Draw(image)
88
+ draw.text((34, 24), "Pantry shelf", font=FONT, fill="#1f2937")
89
+ draw.rectangle((28, 92, 932, 535), fill="#e7dfd2", outline="#c2b6a3", width=3)
90
+ for y in (300, 520):
91
+ draw.rectangle((42, y, 918, y + 15), fill="#8b6f47")
92
+
93
+ def bottle(x: int, y: int, color: str = "#0f9f8f") -> None:
94
+ draw.rounded_rectangle((x + 16, y, x + 46, y + 34), radius=5, fill="#d8f3ef")
95
+ draw.rounded_rectangle((x, y + 26, x + 62, y + 174), radius=18, fill=color)
96
+ draw.rounded_rectangle((x + 8, y + 82, x + 54, y + 126), radius=6, fill="#fffaf0")
97
+ draw.text((x + 17, y + 91), "MINT", font=ImageFont.load_default(size=12), fill="#0f766e")
98
+
99
+ def can(x: int, y: int, color: str) -> None:
100
+ draw.rounded_rectangle((x, y, x + 76, y + 142), radius=12, fill=color, outline="#6b7280", width=2)
101
+ draw.rectangle((x + 8, y + 48, x + 68, y + 92), fill="#fff7ed")
102
+
103
+ bottle(80, 118)
104
+ can(205, 148, "#ef4444")
105
+ bottle(340, 118)
106
+ can(474, 148, "#38bdf8")
107
+ bottle(620, 118)
108
+ bottle(780, 118, "#7c3aed")
109
+ can(110, 365, "#f59e0b")
110
+ bottle(275, 338)
111
+ can(430, 365, "#ef4444")
112
+ bottle(590, 338, "#7c3aed")
113
+ bottle(765, 338)
114
+ image.save(OUTPUT / "box_product_shelf.png")
115
+
116
+
117
+ def parking_lot() -> None:
118
+ image = Image.new("RGB", (960, 600), "#374151")
119
+ draw = ImageDraw.Draw(image)
120
+ draw.text((34, 22), "Parking lot", font=FONT, fill="white")
121
+ for x in range(65, 930, 145):
122
+ draw.line((x, 90, x, 550), fill="#d1d5db", width=4)
123
+
124
+ def car(x: int, y: int, color: str, width: int = 84, height: int = 150) -> None:
125
+ draw.rounded_rectangle((x, y, x + width, y + height), radius=20, fill=color, outline="#111827", width=3)
126
+ draw.rounded_rectangle((x + 12, y + 32, x + width - 12, y + 82), radius=10, fill="#bfdbfe")
127
+ draw.rectangle((x + 10, y + 112, x + width - 10, y + 126), fill="#e5e7eb")
128
+
129
+ car(88, 112, "#22c55e")
130
+ car(238, 345, "#ef4444")
131
+ car(382, 125, "#22c55e")
132
+ car(522, 330, "#3b82f6")
133
+ car(674, 118, "#22c55e")
134
+ car(793, 340, "#22c55e", width=112, height=178)
135
+ image.save(OUTPUT / "box_parking_lot.png")
136
+
137
+
138
  if __name__ == "__main__":
139
  crowded_shapes()
140
  mini_receipt()
141
  logic_grid()
142
+ product_shelf()
143
+ parking_lot()
tests/test_app.py CHANGED
@@ -99,3 +99,25 @@ def test_two_clicks_create_positive_box(tmp_path) -> None:
99
 
100
  assert boxes == [{"box": [20, 15, 120, 80], "kind": "positive"}]
101
  assert pending is None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  assert boxes == [{"box": [20, 15, 120, 80], "kind": "positive"}]
101
  assert pending is None
102
+
103
+
104
+ def test_box_example_loads_image_boxes_and_prompt() -> None:
105
+ (
106
+ original_path,
107
+ boxes,
108
+ pending,
109
+ annotated_image,
110
+ status,
111
+ prompt,
112
+ result,
113
+ details,
114
+ ) = app.load_box_example(SimpleNamespace(index=1))
115
+
116
+ assert original_path.endswith("box_product_shelf.png")
117
+ assert [box["kind"] for box in boxes] == ["positive", "negative"]
118
+ assert pending is None
119
+ assert annotated_image.size == (960, 600)
120
+ assert "Matching mint bottles" in status
121
+ assert "teal MINT bottle" in prompt
122
+ assert result is None
123
+ assert details == ""