bolajiev commited on
Commit
aee748e
·
1 Parent(s): eebf7cf

Sprint 4: badge_with_text deterministic template

Browse files

Adds a badge_with_text template that computes text size and z-position
deterministically so text and badge never drift regardless of LLM output.

- scene.py: _template_badge_with_text() builds ExtrudeNode + Text3DNode with
font-width-aware text_size and fixed z=0.15 front-face offset; registered
in TEMPLATES
- app.py: _detect_badge_template() regex routes "a <shape> badge with text X"
prompts to the template, bypassing the LLM entirely
- llm.py: SYSTEM documents badge_with_text template format; FEWSHOT updated to
use template JSON for both "star badge PRO" and new "shield badge NEW" examples

Files changed (3) hide show
  1. app.py +35 -1
  2. llm.py +44 -14
  3. scene.py +63 -4
app.py CHANGED
@@ -10,6 +10,7 @@ from __future__ import annotations
10
  import json
11
  import logging
12
  import os
 
13
 
14
  import gradio as gr
15
 
@@ -53,6 +54,22 @@ EXAMPLES = [
53
 
54
  _NESTED_SPHERE_PATTERNS = ("wireframe sphere inside", "sphere inside", "nested sphere")
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  def generate(prompt: str, glow: bool, glow_strength: float, style: str, motion: str,
58
  lighting: str, shadows: bool):
@@ -64,7 +81,24 @@ def generate(prompt: str, glow: bool, glow_strength: float, style: str, motion:
64
 
65
  lo = prompt.lower()
66
 
67
- if any(pat in lo for pat in _NESTED_SPHERE_PATTERNS):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  scene = build_scene({
69
  "background": "#0b0e14",
70
  "objects": [o.model_dump() for o in TEMPLATES["nested_spheres"]({})],
 
10
  import json
11
  import logging
12
  import os
13
+ import re
14
 
15
  import gradio as gr
16
 
 
54
 
55
  _NESTED_SPHERE_PATTERNS = ("wireframe sphere inside", "sphere inside", "nested sphere")
56
 
57
+ # Detect "a <shape> badge with the text <WORD>" patterns deterministically.
58
+ # Group 1 = shape, group 2 = text word (converted to uppercase).
59
+ _BADGE_TEXT_RE = re.compile(
60
+ r'\b(star|shield|hexagon|badge|heart)\b.{0,60}'
61
+ r'\b(?:text|saying|word|label)\b\s*["\']?([A-Za-z0-9]{1,20})["\']?',
62
+ re.IGNORECASE,
63
+ )
64
+
65
+
66
+ def _detect_badge_template(prompt: str):
67
+ """Return (shape, text) if prompt matches badge-with-text, else (None, None)."""
68
+ m = _BADGE_TEXT_RE.search(prompt)
69
+ if m:
70
+ return m.group(1).lower(), m.group(2).upper()
71
+ return None, None
72
+
73
 
74
  def generate(prompt: str, glow: bool, glow_strength: float, style: str, motion: str,
75
  lighting: str, shadows: bool):
 
81
 
82
  lo = prompt.lower()
83
 
84
+ badge_shape, badge_text = _detect_badge_template(lo)
85
+ if badge_shape and badge_text:
86
+ scene = build_scene({
87
+ "background": "#0d1117",
88
+ "template": {
89
+ "name": "badge_with_text",
90
+ "shape": badge_shape,
91
+ "text": badge_text,
92
+ },
93
+ "lights": [
94
+ {"type": "ambient", "intensity": 0.4},
95
+ {"type": "directional", "color": "#ffffff", "intensity": 2.0,
96
+ "position": [4, 6, 4]},
97
+ ],
98
+ "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
99
+ })
100
+ note = "Template (badge with text)."
101
+ elif any(pat in lo for pat in _NESTED_SPHERE_PATTERNS):
102
  scene = build_scene({
103
  "background": "#0b0e14",
104
  "objects": [o.model_dump() for o in TEMPLATES["nested_spheres"]({})],
llm.py CHANGED
@@ -76,7 +76,23 @@ For 3D text (words, numbers, short labels) use a TEXT3D object (Latin chars only
76
  "position": [x, y, z]
77
  }
78
  Use text3d for: single words, numbers, initials, short labels (max 2 text objects per scene).
79
- Combine text3d with extrude for badges with text (position text slightly in front: z+0.3).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  Material preset notes:
82
  - gold/chrome: use for shiny metallic looks; color overrides the base hue
@@ -313,25 +329,39 @@ FEWSHOT = [
313
  {"role": "user", "content": "a star badge with the text PRO"},
314
  {"role": "assistant", "content": json.dumps({
315
  "background": "#0d1117",
316
- "objects": [
317
- {
318
- "type": "extrude", "shape": "star", "depth": 0.15, "bevel": True,
319
- "color": "#3a6bc4", "metalness": 0.7, "roughness": 0.2,
320
- "position": [0, 0, 0],
321
- },
322
- {
323
- "type": "text3d", "text": "PRO",
324
- "size": 0.35, "depth": 0.1, "bevel": True,
325
- "color": "#ffffff", "metalness": 0.1, "roughness": 0.4,
326
- "position": [0, -0.15, 0.3],
327
- },
328
- ],
329
  "lights": [
330
  {"type": "ambient", "intensity": 0.4},
331
  {"type": "directional", "color": "#ffffff", "intensity": 2.0, "position": [3, 5, 4]},
332
  ],
333
  "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
334
  })},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  ]
336
 
337
  _tok = None
 
76
  "position": [x, y, z]
77
  }
78
  Use text3d for: single words, numbers, initials, short labels (max 2 text objects per scene).
79
+ For a badge/emblem with text on it, use the badge_with_text TEMPLATE never place text3d manually on an extrude:
80
+ {
81
+ "background": "#0d1117",
82
+ "template": {
83
+ "name": "badge_with_text",
84
+ "shape": "star|shield|hexagon|badge|heart",
85
+ "text": "WORD",
86
+ "color_badge": "#RRGGBB",
87
+ "color_text": "#RRGGBB",
88
+ "metalness": 0.0-1.0,
89
+ "roughness": 0.0-1.0,
90
+ "preset_badge": "gold|chrome|glass|neon|matte|plastic"
91
+ },
92
+ "lights": [...],
93
+ "animation": {...}
94
+ }
95
+ The template positions and scales the text automatically — never set text position or size.
96
 
97
  Material preset notes:
98
  - gold/chrome: use for shiny metallic looks; color overrides the base hue
 
329
  {"role": "user", "content": "a star badge with the text PRO"},
330
  {"role": "assistant", "content": json.dumps({
331
  "background": "#0d1117",
332
+ "template": {
333
+ "name": "badge_with_text",
334
+ "shape": "star",
335
+ "text": "PRO",
336
+ "color_badge": "#3a6bc4",
337
+ "color_text": "#ffffff",
338
+ "metalness": 0.7,
339
+ "roughness": 0.2,
340
+ },
 
 
 
 
341
  "lights": [
342
  {"type": "ambient", "intensity": 0.4},
343
  {"type": "directional", "color": "#ffffff", "intensity": 2.0, "position": [3, 5, 4]},
344
  ],
345
  "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
346
  })},
347
+ {"role": "user", "content": "a shield badge with the text NEW"},
348
+ {"role": "assistant", "content": json.dumps({
349
+ "background": "#0d1117",
350
+ "template": {
351
+ "name": "badge_with_text",
352
+ "shape": "shield",
353
+ "text": "NEW",
354
+ "color_badge": "#22aa66",
355
+ "color_text": "#ffffff",
356
+ "metalness": 0.5,
357
+ "roughness": 0.3,
358
+ },
359
+ "lights": [
360
+ {"type": "ambient", "intensity": 0.4},
361
+ {"type": "directional", "color": "#ffffff", "intensity": 2.0, "position": [4, 6, 4]},
362
+ ],
363
+ "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
364
+ })},
365
  ]
366
 
367
  _tok = None
scene.py CHANGED
@@ -591,11 +591,70 @@ def _template_nested_spheres(params: Dict[str, Any]) -> List[Obj]:
591
  ]
592
 
593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  TEMPLATES: Dict[str, Any] = {
595
- "burger": _template_burger,
596
- "snowman": _template_snowman,
597
- "tree": _template_tree,
598
- "nested_spheres": _template_nested_spheres,
 
599
  }
600
 
601
 
 
591
  ]
592
 
593
 
594
+ # Per-shape: safe text face width (world units after normalization to 1.5)
595
+ # and a small y nudge so text sits in the visual body, not the tapered parts.
596
+ _BADGE_SHAPE_PARAMS: Dict[str, Dict[str, float]] = {
597
+ "star": {"safe_w": 0.85, "text_y": 0.0},
598
+ "heart": {"safe_w": 0.65, "text_y": 0.2},
599
+ "hexagon": {"safe_w": 1.1, "text_y": 0.0},
600
+ "badge": {"safe_w": 1.2, "text_y": 0.0},
601
+ "shield": {"safe_w": 0.9, "text_y": 0.1},
602
+ }
603
+
604
+ _CHAR_W = 0.65 # helvetiker average char width per size=1.0 unit
605
+
606
+
607
+ def _template_badge_with_text(params: Dict[str, Any]) -> List[Any]:
608
+ """Deterministic badge+text: model supplies shape/text/colors; compiler sets layout."""
609
+ shape = str(params.get("shape", "star")).lower()
610
+ if shape not in EXTRUDE_SHAPES:
611
+ shape = "star"
612
+
613
+ text_raw = str(params.get("text", "TEXT"))
614
+ text = "".join(c for c in text_raw if c.isprintable() and ord(c) < 128)[:24].strip() or "TEXT"
615
+
616
+ color_badge = _sanitize_color(str(params.get("color_badge", "#3a6bc4")), "#3a6bc4")
617
+ color_text = _sanitize_color(str(params.get("color_text", "#ffffff")), "#ffffff")
618
+ badge_metal = _clamp(float(params.get("metalness", 0.5)), 0.0, 1.0)
619
+ badge_rough = _clamp(float(params.get("roughness", 0.25)), 0.0, 1.0)
620
+
621
+ preset_badge = str(params.get("preset_badge", "")) or None
622
+ if preset_badge not in PRESET_NAMES:
623
+ preset_badge = None
624
+ preset_text = str(params.get("preset_text", "")) or None
625
+ if preset_text not in PRESET_NAMES:
626
+ preset_text = None
627
+
628
+ sp = _BADGE_SHAPE_PARAMS.get(shape, {"safe_w": 1.0, "text_y": 0.0})
629
+
630
+ # Scale text so it fills ~80 % of the badge face width
631
+ text_size = round(
632
+ _clamp(sp["safe_w"] * 0.8 / (max(1, len(text)) * _CHAR_W), 0.12, 0.55), 3
633
+ )
634
+
635
+ return [
636
+ ExtrudeNode(
637
+ shape=shape, depth=0.15, bevel=True,
638
+ color=color_badge, preset=preset_badge,
639
+ metalness=badge_metal, roughness=badge_rough,
640
+ emissive="#000000", position=[0.0, 0.0, 0.0],
641
+ ),
642
+ Text3DNode(
643
+ text=text, size=text_size, depth=0.06, bevel=True,
644
+ color=color_text, preset=preset_text,
645
+ metalness=0.1, roughness=0.4, emissive="#000000",
646
+ # z=0.15 always clears the badge front face (badge front ≈ 0.05–0.07 after scale)
647
+ position=[0.0, sp["text_y"], 0.15],
648
+ ),
649
+ ]
650
+
651
+
652
  TEMPLATES: Dict[str, Any] = {
653
+ "burger": _template_burger,
654
+ "snowman": _template_snowman,
655
+ "tree": _template_tree,
656
+ "nested_spheres": _template_nested_spheres,
657
+ "badge_with_text": _template_badge_with_text,
658
  }
659
 
660