Spaces:
Running on Zero
Running on Zero
bolajiev commited on
Commit ·
aee748e
1
Parent(s): eebf7cf
Sprint 4: badge_with_text deterministic template
Browse filesAdds 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
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 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":
|
| 596 |
-
"snowman":
|
| 597 |
-
"tree":
|
| 598 |
-
"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 |
|