$ cat node-template.py
I
Infographic Generator
// Generates a professional infographic from text content. Uses an LLM to analyze the content and select the best visualization layout. Outputs a PNG or SVG image. Supports 237 templates across charts, lists, comparisons, hierarchies, sequences, and relations.
Process
Document
template.py
1import os2import sys3import json4import traceback5import requests6import asyncio7from gais import Gais89# Environment10EMBLEMA_API_BASE_URL = os.getenv("EMBLEMA_API_BASE_URL", "http://localhost:3000")11USER_TOKEN = os.getenv("USER_TOKEN")12OUTPUT_DIR = "/data/output"13MAX_CONTENT_LENGTH = 100_0001415# ---------------------------------------------------------------------------16# System prompt — based on official AntV Infographic skill files with17# data-density, capacity, and layout-quality constraints.18# ---------------------------------------------------------------------------19SYSTEM_PROMPT = """You are an expert infographic designer using the AntV Infographic DSL.2021Given the user's content, produce ONE block of AntV Infographic DSL syntax that best visualizes the information.2223## STEP 0 — THINK BEFORE YOU GENERATE2425Before writing any DSL, silently analyse the input:261. Count distinct data items. How many items/nodes/points exist?272. Measure text: are labels short (1-3 words) or long (4+ words)?283. Decide structure type: list, sequence, compare, hierarchy, relation, chart.294. Pick a template whose capacity fits the item count (see TEMPLATE CAPACITY below).305. Plan concise labels — shorten aggressively if the source text is verbose.31Then output ONLY the DSL (no analysis, no commentary).3233## DSL FORMAT3435- First line: `infographic <template-name>`36- Blocks: `data` / `theme`, indented with two spaces.37- Key-value: `key value` (space-separated).38- Arrays: dash prefix `- ` for each entry.39- No JSON, no Markdown, no code fences, no explanation.4041Example:4243infographic list-row-horizontal-icon-arrow44data45 title Internet Technology Evolution46 desc Key milestones from Web 1.0 to AI47 lists48 - time 199149 label Web 1.050 desc First website by Tim Berners-Lee51 icon web52 - time 200453 label Web 2.054 desc Social media goes mainstream55 icon account multiple56 - time 202357 label AI Large Model58 desc ChatGPT sparks the AI revolution59 icon brain60theme61 palette #3b82f6 #8b5cf6 #f973166263## DATA FIELDS BY TEMPLATE TYPE (use ONLY the correct field)6465| Template prefix | Data field | Notes |66|---|---|---|67| `list-*` | `lists` | Flat array of items |68| `sequence-*` | `sequences` | Optional `order asc/desc` |69| `compare-binary-*` / `compare-hierarchy-left-right-*` | `compares` | MUST have exactly 2 root items, sub-items in `children` |70| `compare-swot` | `compares` | Exactly 4 root items (S/W/O/T) with `children` |71| `compare-quadrant-*` | `compares` | Exactly 4 items |72| `hierarchy-structure` / `hierarchy-structure-mirror` | `items` | Up to 3 nesting levels via `children` |73| Other `hierarchy-*` | `root` | Single root, tree via `children` nesting. Do NOT repeat `root`. |74| `relation-*` | `nodes` + `relations` | Edges: `A - label -> B` or `A -->|label| B` |75| `chart-*` | `values` | Optional `category` |76| Fallback | `items` | Only when none of the above match |7778NEVER mix data fields. Use exactly one per template.7980## TEXT DENSITY RULES (CRITICAL for visual quality)8182The rendering engine allocates fixed space per card/node. Overflow causes overlapping text. Obey these limits:8384| Field | Max chars | Guidance |85|---|---|---|86| `title` (chart-*) | 25 | Chart title. Keep concise — 3-4 words max. |87| `desc` (chart-*) | 50 | Chart subtitle. One short phrase. |88| `title` (other) | 35 | Main title. Concise headline. |89| `desc` (global) | 60 | Subtitle under the title. One line. |90| `label` | 25 | Item label. Shorten aggressively: abbreviate, drop articles/prepositions. |91| `desc` (per item) | 50 | Keep to one sentence. Omit if the label is self-explanatory. |92| `value` | 8 | Numeric only. E.g. `82%`, `3.94`, `1560` |9394Chart title + desc examples (chart-* templates):95 BAD: title "Docenti con le valutazioni più basse nel primo modulo" (52 chars — too long!)96 GOOD: title "Docenti Peggiori" desc "Valutazioni Modulo I" (16 + 20 chars)97 BAD: title "Distribuzione delle risposte al sondaggio" (40 chars — too long!)98 GOOD: title "Risposte Sondaggio" desc "Distribuzione per modulo" (18 + 25 chars)99100General shortening examples:101 BAD: "DINAMICA DELLE STRUTTURE ORGANIZZATIVE" (40 chars)102 GOOD: "Strutture Organizzative" (23 chars)103 BAD: "La lezione e stata interessante?" (31 chars)104 GOOD: "Interesse Lezione" (17 chars)105106When the source language is verbose (Italian, German, French), shorten to core nouns only. Drop verbs, articles, prepositions.107108## TEMPLATE CAPACITY LIMITS109110Exceeding these limits causes cramped, overlapping cards. If data exceeds a limit, either CHOOSE A DIFFERENT TEMPLATE or SUMMARIZE/GROUP items.111112| Template pattern | Max items | When exceeded, prefer |113|---|---|---|114| `list-grid-*` | 6 | list-column-*, list-waterfall-*, list-zigzag-* |115| `list-row-*` | 5 | list-column-*, sequence-snake-* |116| `list-column-*` | 8 | hierarchy-mindmap-*, sequence-roadmap-* |117| `list-sector-*` | 6 | list-column-*, list-waterfall-* |118| `list-pyramid-*` | 5 | sequence-pyramid-*, sequence-funnel-* |119| `sequence-steps-*` | 6 | sequence-snake-*, sequence-roadmap-* |120| `sequence-timeline-*` | 8 | sequence-roadmap-*, list-column-* |121| `sequence-roadmap-*` | 10 | Split into two infographics |122| `sequence-snake-*` | 10 | Split or use hierarchy-mindmap-* |123| `compare-binary-*` | 2 root + 5 children each | Reduce children |124| `compare-swot` | 4 root + 4 children each | Summarise children |125| `hierarchy-tree-*` | 3 levels, 4-5 nodes/level | Prune or use mindmap |126| `hierarchy-mindmap-*` | 3 levels, 6 nodes/level | OK for large data |127| `chart-pie-*` | 6 slices | Group small slices into "Other" |128| `chart-bar-*` / `chart-column-*` | 8 bars | Group or top-N |129| `relation-dagre-flow-*` | 10 nodes | Simplify graph |130131## TEMPLATE SELECTION GUIDE132133Match the content structure to a template family:134135- Strict order (process/steps/timeline/trend) -> `sequence-*`136 - Dates -> `sequence-timeline-*`137 - Step progression -> `sequence-stairs-*`, `sequence-ascending-*`, `sequence-steps-*`138 - Roadmap -> `sequence-roadmap-vertical-*`139 - Circular cycle -> `sequence-circular-simple`140 - Narrowing/filtering -> `sequence-funnel-simple`, `sequence-pyramid-simple`141 - Snake/zigzag path -> `sequence-snake-steps-*`, `sequence-zigzag-*`142- Feature listing / bullet points -> `list-row-*` (horizontal) or `list-column-*` (vertical)143- Grid of cards (FEW items) -> `list-grid-*` (max 6 items!)144- Waterfall / cascading -> `list-waterfall-*`145- Binary A-vs-B -> `compare-binary-*`146- SWOT -> `compare-swot`147- 2x2 matrix -> `compare-quadrant-*`148- Tree / categorization -> `hierarchy-tree-*`149- Mind map / brainstorm -> `hierarchy-mindmap-*`150- Org chart -> `hierarchy-structure`, `hierarchy-structure-mirror`151- Numeric data -> `chart-*` (pie=proportions, bar/column=comparison, line=trend)152- Word cloud -> `chart-wordcloud`153- Network/dependencies -> `relation-dagre-flow-*`, `relation-network-*`, `relation-circle-*`154155## HIERARCHY TREE VARIANTS156157Tree templates follow the pattern: `hierarchy-tree-{direction}-{edge}-{node}`158- Direction: (default=top-down), `bt` (bottom-up), `lr` (left-right), `rl` (right-left)159- Edge style: `curved-line`, `dashed-line`, `dashed-arrow`, `distributed-origin`, `tech-style`160- Node style: `badge-card`, `capsule-item`, `compact-card`, `ribbon-card`, `rounded-rect-node`161162## AVAILABLE TEMPLATES (237 total)163164chart (11): chart-bar-plain-text, chart-column-simple, chart-line-plain-text,165chart-pie-compact-card, chart-pie-donut-compact-card, chart-pie-donut-pill-badge,166chart-pie-donut-plain-text, chart-pie-pill-badge, chart-pie-plain-text,167chart-wordcloud, chart-wordcloud-rotate168169list (29): list-column-done-list, list-column-simple-vertical-arrow, list-column-vertical-icon-arrow,170list-grid-badge-card, list-grid-candy-card-lite, list-grid-circular-progress,171list-grid-compact-card, list-grid-done-list, list-grid-horizontal-icon-arrow,172list-grid-progress-card, list-grid-ribbon-card, list-grid-simple,173list-pyramid-badge-card, list-pyramid-compact-card, list-pyramid-rounded-rect-node,174list-row-circular-progress, list-row-horizontal-icon-arrow, list-row-horizontal-icon-line,175list-row-simple-horizontal-arrow, list-row-simple-illus,176list-sector-half-plain-text, list-sector-plain-text, list-sector-simple,177list-waterfall-badge-card, list-waterfall-compact-card,178list-zigzag-down-compact-card, list-zigzag-down-simple,179list-zigzag-up-compact-card, list-zigzag-up-simple180181sequence (47): sequence-ascending-stairs-3d-simple, sequence-ascending-stairs-3d-underline-text,182sequence-ascending-steps, sequence-circle-arrows-indexed-card,183sequence-circular-simple, sequence-circular-underline-text,184sequence-color-snake-steps-horizontal-icon-line, sequence-color-snake-steps-simple-illus,185sequence-cylinders-3d-simple, sequence-filter-mesh-simple, sequence-filter-mesh-underline-text,186sequence-funnel-simple, sequence-horizontal-zigzag-horizontal-icon-line,187sequence-horizontal-zigzag-plain-text, sequence-horizontal-zigzag-simple,188sequence-horizontal-zigzag-simple-horizontal-arrow, sequence-horizontal-zigzag-simple-illus,189sequence-horizontal-zigzag-underline-text, sequence-mountain-underline-text,190sequence-pyramid-simple, sequence-roadmap-vertical-badge-card,191sequence-roadmap-vertical-pill-badge, sequence-roadmap-vertical-plain-text,192sequence-roadmap-vertical-quarter-circular, sequence-roadmap-vertical-quarter-simple-card,193sequence-roadmap-vertical-simple, sequence-roadmap-vertical-underline-text,194sequence-snake-steps-compact-card, sequence-snake-steps-pill-badge,195sequence-snake-steps-simple, sequence-snake-steps-simple-illus,196sequence-snake-steps-underline-text, sequence-stairs-front-compact-card,197sequence-stairs-front-pill-badge, sequence-stairs-front-simple,198sequence-steps-badge-card, sequence-steps-simple, sequence-steps-simple-illus,199sequence-timeline-done-list, sequence-timeline-plain-text,200sequence-timeline-rounded-rect-node, sequence-timeline-simple,201sequence-timeline-simple-illus, sequence-zigzag-pucks-3d-indexed-card,202sequence-zigzag-pucks-3d-simple, sequence-zigzag-pucks-3d-underline-text,203sequence-zigzag-steps-underline-text204205compare (20): compare-binary-horizontal-badge-card-arrow, compare-binary-horizontal-badge-card-fold,206compare-binary-horizontal-badge-card-vs, compare-binary-horizontal-compact-card-arrow,207compare-binary-horizontal-compact-card-fold, compare-binary-horizontal-compact-card-vs,208compare-binary-horizontal-simple-arrow, compare-binary-horizontal-simple-fold,209compare-binary-horizontal-simple-vs, compare-binary-horizontal-underline-text-arrow,210compare-binary-horizontal-underline-text-fold, compare-binary-horizontal-underline-text-vs,211compare-hierarchy-left-right-circle-node-pill-badge,212compare-hierarchy-left-right-circle-node-plain-text,213compare-hierarchy-row-letter-card-compact-card,214compare-hierarchy-row-letter-card-rounded-rect-node,215compare-quadrant-quarter-circular, compare-quadrant-quarter-simple-card,216compare-quadrant-simple-illus, compare-swot217218hierarchy (112): hierarchy-mindmap-branch-gradient-capsule-item, hierarchy-mindmap-branch-gradient-circle-progress,219hierarchy-mindmap-branch-gradient-compact-card, hierarchy-mindmap-branch-gradient-lined-palette,220hierarchy-mindmap-branch-gradient-rounded-rect, hierarchy-mindmap-level-gradient-capsule-item,221hierarchy-mindmap-level-gradient-circle-progress, hierarchy-mindmap-level-gradient-compact-card,222hierarchy-mindmap-level-gradient-lined-palette, hierarchy-mindmap-level-gradient-rounded-rect,223hierarchy-structure, hierarchy-structure-mirror,224hierarchy-tree-[bt|lr|rl]-{curved-line|dashed-line|dashed-arrow|distributed-origin|tech-style}-{badge-card|capsule-item|compact-card|ribbon-card|rounded-rect-node} (100 combinations)225226relation (18): relation-circle-circular-progress, relation-circle-icon-badge,227relation-dagre-flow-lr-animated-badge-card, relation-dagre-flow-lr-animated-capsule,228relation-dagre-flow-lr-animated-compact-card, relation-dagre-flow-lr-animated-simple-circle-node,229relation-dagre-flow-lr-badge-card, relation-dagre-flow-lr-compact-card,230relation-dagre-flow-lr-simple-circle-node, relation-dagre-flow-tb-animated-badge-card,231relation-dagre-flow-tb-animated-capsule, relation-dagre-flow-tb-animated-compact-card,232relation-dagre-flow-tb-animated-simple-circle-node, relation-dagre-flow-tb-badge-card,233relation-dagre-flow-tb-compact-card, relation-dagre-flow-tb-simple-circle-node,234relation-network-icon-badge, relation-network-simple-circle-node235236## ICON KEYWORDS237238Use icon keywords (matched by the renderer). Examples:239star fill, check circle, trending up, chart bar, users, lightbulb, rocket,240shield, target, globe, code, database, search, alert triangle, clock, calendar,241brain, heart, zap, trophy, sprout, document text, web, cellphone, cloud,242application brackets, sun, moon, flash fast, secure shield check, account multiple243244## THEME OPTIONS245246Optional `theme` block for customisation:247248Dark theme + custom palette:249 theme dark250 palette251 - #61DDAA252 - #F6BD16253 - #F08BB4254255Hand-drawn style:256 theme257 stylize rough258 base259 text260 font-family 851tegakizatsu261262Available stylize types: rough, pattern, linear-gradient, radial-gradient263264## INPUT DATA FORMAT265266The input may include tool call results. Pay attention to:267- `intermediate: true` = research/helper steps. Useful context but SECONDARY.268- `intermediate: false` (or absent) = PRIMARY/FINAL data. Focus on these.269Prioritise non-intermediate content for the main data points.270271## COMPLETE DSL EXAMPLES272273List (horizontal arrow):274infographic list-row-horizontal-icon-arrow275data276 title Feature List277 lists278 - label Fast279 icon flash fast280 - label Secure281 icon secure shield check282 - label Scalable283 icon cloud284285Sequence (steps):286infographic sequence-steps-simple287data288 title Build Process289 sequences290 - label Design291 desc Create wireframes292 - label Develop293 desc Build the MVP294 - label Launch295 desc Release to users296 order asc297298Hierarchy (tree):299infographic hierarchy-tree-curved-line-rounded-rect-node300data301 root302 label Company303 children304 - label Engineering305 children306 - label Frontend307 - label Backend308 - label Marketing309310Compare (SWOT):311infographic compare-swot312data313 compares314 - label Strengths315 children316 - label Strong brand317 - label Loyal users318 - label Weaknesses319 children320 - label High cost321 - label Opportunities322 children323 - label Emerging markets324 - label Threats325 children326 - label Competitors327328Chart (column):329infographic chart-column-simple330data331 title Monthly Revenue332 values333 - label Jan334 value 1280335 - label Feb336 value 1560337 - label Mar338 value 1890339340Relation (flow):341infographic relation-dagre-flow-tb-simple-circle-node342data343 nodes344 - id A345 label Input346 - id B347 label Process348 - id C349 label Output350 relations351 A - feeds -> B352 B - produces -> C353354## CRITICAL RULES3553561. Output ONLY DSL — no markdown, no code fences, no commentary, no explanation.3572. First line MUST be `infographic <template-name>`.3583. Two-space indentation throughout.3594. Use the CORRECT data field for the chosen template (see DATA FIELDS table).3605. Respect TEXT DENSITY LIMITS: label <= 25 chars, desc <= 50 chars, title <= 35 chars.3616. For chart-* templates: title MUST be <= 25 chars (3-4 words), desc <= 50 chars. Keep chart titles concise.3627. Respect TEMPLATE CAPACITY LIMITS. If data exceeds capacity, choose a larger template or summarise.3638. When source text is verbose, SHORTEN to core nouns. Drop articles, verbs, prepositions.3649. Do NOT fabricate data unrelated to the user's content.36510. Binary compare templates MUST have exactly 2 root items.36611. Hierarchy templates use single `root` (do not repeat `root`).36712. Preserve the user's language for labels — but shorten within that language.36813. When a specific template is requested, use it even if another might fit better.369"""370371# ---------------------------------------------------------------------------372# Template DSL examples — fetched at runtime from the static manifest.373# The manifest lives at /templates/infographic/manifest.json (served by Next.js)374# and contains { id, category, syntax } for all 237 templates.375# ---------------------------------------------------------------------------376377_MANIFEST_CACHE = None378379def _fetch_manifest():380 """Fetch the template manifest once and cache it."""381 global _MANIFEST_CACHE382 if _MANIFEST_CACHE is not None:383 return _MANIFEST_CACHE384385 url = f"{EMBLEMA_API_BASE_URL}/templates/infographic/manifest.json"386 try:387 print(f"Fetching template manifest: {url}", file=sys.stderr)388 resp = requests.get(url, timeout=15)389 resp.raise_for_status()390 manifest = resp.json()391 # Build lookup dict: id -> syntax392 _MANIFEST_CACHE = {entry["id"]: entry.get("syntax", "") for entry in manifest}393 print(f"Loaded {len(_MANIFEST_CACHE)} template examples from manifest", file=sys.stderr)394 return _MANIFEST_CACHE395 except Exception as e:396 print(f"Warning: Could not fetch template manifest: {e}", file=sys.stderr)397 _MANIFEST_CACHE = {}398 return _MANIFEST_CACHE399400401def build_template_instruction(template_name):402 """When user picks a specific template, inject its DSL example into prompt."""403 if not template_name or template_name == "auto":404 return ""405406 manifest = _fetch_manifest()407 example = manifest.get(template_name, "")408409 if example:410 return (411 "\n\nYou MUST use the template '" + template_name + "'.\n"412 "Here is the reference syntax showing the correct DSL structure:\n\n" + example + "\n\n"413 "Rules:\n"414 "- Use the EXACT template name on the first line\n"415 "- Use the SAME DSL field names (e.g. 'lists', 'sequences', 'compares', 'root', 'nodes', 'values')\n"416 "- Adapt the number of items, nesting depth, and content to fit the user's data and instructions\n"417 "- The user's additional instructions take priority when they conflict with the example's structure"418 )419420 # Final fallback - no example available421 return "\n\nYou MUST use the template '" + template_name + "'."422423424def clean_dsl_output(raw_output):425 """Clean LLM output to extract pure DSL syntax."""426 text = raw_output.strip()427428 # Remove markdown code fences if present429 if text.startswith("```"):430 lines = text.split("\n")431 # Remove first line (```yaml or ```text or ```)432 lines = lines[1:]433 # Remove last line if it's closing ```434 if lines and lines[-1].strip() == "```":435 lines = lines[:-1]436 text = "\n".join(lines).strip()437438 # Ensure it starts with 'infographic '439 lines = text.split("\n")440 start_idx = 0441 for i, line in enumerate(lines):442 if line.strip().startswith("infographic "):443 start_idx = i444 break445446 text = "\n".join(lines[start_idx:]).strip()447448 if not text.startswith("infographic "):449 raise ValueError(450 "LLM output does not contain valid infographic DSL. "451 f"Output starts with: {text[:100]}"452 )453454 return text455456457# ---------------------------------------------------------------------------458# DSL Validator — enforces capacity limits, text lengths, and canvas sizing459# Runs AFTER LLM generation, BEFORE rendering.460# ---------------------------------------------------------------------------461462import re as _re463464# (max_items, swap_target_template)465_TEMPLATE_CAPACITY = {466 'list-grid-': (6, 'list-waterfall-badge-card'),467 'list-row-': (5, 'list-column-vertical-icon-arrow'),468 'list-sector-': (6, 'list-waterfall-compact-card'),469 'list-pyramid-': (5, 'list-waterfall-badge-card'),470 'sequence-steps-': (6, 'sequence-snake-steps-compact-card'),471 'sequence-stairs-': (5, 'sequence-snake-steps-compact-card'),472 'chart-pie-': (6, None),473 'chart-bar-': (8, None),474 'chart-column-': (8, None),475}476477_MAX_LABEL = 25478_MAX_ITEM_DESC = 50479_MAX_TITLE = 40480_MAX_GLOBAL_DESC = 70481482483def _count_top_items(lines):484 """Count top-level array entries in the DSL (lines starting with '- ' at the shallowest array indent)."""485 first_indent = None486 count = 0487 for line in lines:488 stripped = line.lstrip()489 if stripped.startswith('- '):490 indent = len(line) - len(stripped)491 if first_indent is None:492 first_indent = indent493 if indent == first_indent:494 count += 1495 return count496497498def _clean_value_text(text):499 """Strip verbose prefixes from value fields, keeping just the number."""500 text = text.strip()501 # Already short and numeric-looking — keep as-is502 if len(text) <= 8:503 return text504 # N/A variants505 if 'N/A' in text or 'n/a' in text:506 return 'N/A'507 # Extract the first number (with optional decimal and %)508 m = _re.search(r'(\d+\.?\d*%?)', text)509 if m:510 return m.group(1)511 # Fallback: truncate512 return text[:8]513514515def _recommend_canvas(template, item_count):516 """Suggest minimum canvas size based on template type and data volume."""517 W, H = 1920, 1080 # Base canvas (Full HD)518519 if template.startswith(('chart-column-', 'chart-bar-')):520 return (max(W, item_count * 250), H)521522 if template.startswith('chart-'):523 return (W, H)524525 if template.startswith('list-grid-'):526 cols = 3527 rows = max(2, (item_count + cols - 1) // cols)528 return (W, max(H, rows * 300))529530 if template.startswith(('list-column-', 'list-waterfall-', 'list-zigzag-')):531 return (W, max(H, item_count * 180))532533 if template.startswith('list-row-'):534 return (max(W, item_count * 280), H)535536 if template.startswith(('sequence-roadmap-', 'sequence-snake-', 'sequence-timeline-')):537 return (W, max(H, item_count * 160))538539 if template.startswith('sequence-'):540 return (max(W, item_count * 220), H)541542 if template.startswith('hierarchy-'):543 return (W, max(H, 1200))544545 return (W, H)546547548def validate_and_fix_dsl(dsl):549 """550 Post-LLM safety net: enforce template capacity, text limits, and canvas sizing.551 Returns (fixed_dsl, recommended_width, recommended_height).552 """553 lines = dsl.split('\n')554 if not lines or not lines[0].startswith('infographic '):555 return dsl, 1920, 1080556557 template = lines[0][len('infographic '):].strip()558 item_count = _count_top_items(lines)559 print(f"DSL validator: template={template}, items={item_count}", file=sys.stderr)560561 # --- 1. Template capacity enforcement ---562 for prefix, (limit, swap) in _TEMPLATE_CAPACITY.items():563 if template.startswith(prefix) and item_count > limit:564 if swap:565 print(566 f"DSL validator: {template} has {item_count} items "567 f"(max {limit}), swapping to {swap}",568 file=sys.stderr,569 )570 lines[0] = 'infographic ' + swap571 template = swap572 else:573 print(574 f"DSL validator: {template} has {item_count} items "575 f"(max {limit}), no swap available — keeping as-is",576 file=sys.stderr,577 )578 break579580 is_chart = template.startswith('chart-')581582 # --- 2. Chart-specific: strip per-item desc (bars need only label+value) ---583 if is_chart:584 to_remove = []585 for i, line in enumerate(lines):586 stripped = line.lstrip()587 depth = len(line) - len(stripped)588 if stripped.startswith('desc ') and depth > 2:589 to_remove.append(i)590 if to_remove:591 print(f"DSL validator: removing {len(to_remove)} item-level desc lines from chart", file=sys.stderr)592 for idx in reversed(to_remove):593 lines.pop(idx)594595 # --- 3. Chart-specific: enforce title (28) and desc (52) limits ---596 _TITLE_SUFFIX_RE = _re.compile(r'\s*(?:[-–—:]\s+.+|\([^)]+\))\s*$')597 _CHART_TITLE_MAX = 28598 _CHART_DESC_MAX = 52599 if is_chart:600 for i, line in enumerate(lines):601 stripped = line.lstrip()602 depth = len(line) - len(stripped)603 if stripped.startswith('title ') and depth <= 2:604 title_text = stripped[6:]605 indent_str = line[:depth]606 # Strip trailing suffix patterns if title is too long607 suffix_match = _TITLE_SUFFIX_RE.search(title_text)608 if suffix_match and len(title_text) > _CHART_TITLE_MAX:609 title_text = title_text[:suffix_match.start()].rstrip()610 print(f"DSL validator: stripped title suffix -> '{title_text}'", file=sys.stderr)611 # Hard limit — truncate at word boundary to avoid mid-word cuts612 if len(title_text) > _CHART_TITLE_MAX:613 cut = title_text[:_CHART_TITLE_MAX].rfind(' ')614 if cut > 5:615 title_text = title_text[:cut]616 else:617 title_text = title_text[:_CHART_TITLE_MAX]618 print(f"DSL validator: truncated chart title -> '{title_text}'", file=sys.stderr)619 lines[i] = indent_str + 'title ' + title_text620 break621 # Enforce chart desc limit (global desc only)622 for i, line in enumerate(lines):623 stripped = line.lstrip()624 depth = len(line) - len(stripped)625 if stripped.startswith('desc ') and depth <= 2:626 desc_text = stripped[5:]627 if len(desc_text) > _CHART_DESC_MAX:628 indent_str = line[:depth]629 cut = desc_text[:_CHART_DESC_MAX].rfind(' ')630 if cut > 10:631 desc_text = desc_text[:cut]632 else:633 desc_text = desc_text[:_CHART_DESC_MAX]634 lines[i] = indent_str + 'desc ' + desc_text635 print(f"DSL validator: truncated chart desc -> '{desc_text}'", file=sys.stderr)636 break637638 # --- 4. Text length enforcement ---639 # Track indent depth to distinguish global vs item fields.640 # Global title/desc are at indent 2 (directly under `data`).641 # Item fields (label, desc, value) are at indent 4+.642 for i, line in enumerate(lines):643 stripped = line.lstrip()644 indent_str = line[:len(line) - len(stripped)]645 depth = len(indent_str)646647 if stripped.startswith('title ') and depth <= 2:648 text = stripped[6:]649 if len(text) > _MAX_TITLE:650 lines[i] = indent_str + 'title ' + text[:_MAX_TITLE]651652 elif stripped.startswith('desc ') and depth <= 2:653 text = stripped[5:]654 if len(text) > _MAX_GLOBAL_DESC:655 lines[i] = indent_str + 'desc ' + text[:_MAX_GLOBAL_DESC]656657 elif stripped.startswith('label '):658 text = stripped[6:]659 if len(text) > _MAX_LABEL:660 lines[i] = indent_str + 'label ' + text[:_MAX_LABEL]661662 elif stripped.startswith('desc ') and depth > 2:663 text = stripped[5:]664 if len(text) > _MAX_ITEM_DESC:665 lines[i] = indent_str + 'desc ' + text[:_MAX_ITEM_DESC]666667 elif stripped.startswith('value '):668 text = stripped[6:]669 cleaned = _clean_value_text(text)670 if cleaned != text:671 print(f"DSL validator: cleaned value '{text}' -> '{cleaned}'", file=sys.stderr)672 lines[i] = indent_str + 'value ' + cleaned673674 # --- 5. Canvas sizing ---675 rec_w, rec_h = _recommend_canvas(template, item_count)676677 fixed = '\n'.join(lines)678 print(679 f"DSL validator: recommended canvas {rec_w}x{rec_h}",680 file=sys.stderr,681 )682 return fixed, rec_w, rec_h683684685def render_infographic(syntax, fmt="png", width=800, height=600, scale=2):686 """Call the Emblema infographic rendering API."""687 url = f"{EMBLEMA_API_BASE_URL}/api/v2/helpers/infographic/render"688689 headers = {"Content-Type": "application/json"}690 if USER_TOKEN:691 headers["Authorization"] = f"Bearer {USER_TOKEN}"692693 payload = {694 "syntax": syntax,695 "format": fmt,696 "width": width,697 "height": height,698 "scale": scale,699 }700701 print(f"Calling render API: {url}", file=sys.stderr)702 response = requests.post(url, headers=headers, json=payload, timeout=60)703704 if response.status_code != 200:705 try:706 error_detail = response.json()707 error_msg = error_detail.get("message", response.text)708 except Exception:709 error_msg = response.text710 raise RuntimeError(f"Render API returned {response.status_code}: {error_msg}")711712 return response.content713714715# Output-language directive (language-persistence workstream). Values are716# platform locale codes; legacy full names are accepted via the allowlist.717# Anything else (wired upstream output, free-form strings) falls back to718# auto: the value is interpolated into the system prompt, so unknown719# strings must never pass through.720LANGUAGE_NAMES = {721 "de": "German", "en": "English", "es": "Spanish", "fr": "French",722 "it": "Italian", "ja": "Japanese", "ko": "Korean", "nl": "Dutch",723 "pt": "Portuguese", "ru": "Russian", "zh": "Chinese",724}725726727def resolve_language_name(language):728 """Full display name for an allowlisted language value, else None."""729 if not isinstance(language, str):730 return None731 value = language.strip()732 if not value or value == "auto":733 return None734 if value in LANGUAGE_NAMES:735 return LANGUAGE_NAMES[value]736 if value in LANGUAGE_NAMES.values():737 return value738 print(f"[language] unrecognized value {value!r} ignored - using auto", file=sys.stderr)739 return None740741742async def generate_infographic(content, template_name, model, instruction="", temperature=0.3, language="auto"):743 """Generate infographic DSL using LLM."""744 system = SYSTEM_PROMPT745 system += build_template_instruction(template_name)746 lang_name = resolve_language_name(language)747 if lang_name:748 system += (749 f"\n\nOUTPUT LANGUAGE: {lang_name}. All labels, titles, and text in "750 f"the infographic MUST be written in {lang_name}, regardless of the "751 "source material's language (still shortened per the label rules "752 "above). Keep proper nouns in their original language."753 )754755 user_message = content756 if instruction and instruction.strip():757 user_message += f"\n\n## Additional Instructions\n{instruction.strip()}"758759 messages = [760 {"role": "system", "content": system},761 {"role": "user", "content": user_message},762 ]763764 # thinking=False: emits a parsed DSL; the reasoning on/off eval showed OFF765 # gives cleaner, predictable markup at far lower latency (reasoning wasted).766 result = await Gais.llm.chat_async(messages, model_id=model, temperature=temperature, thinking=False)767 raw_output = result.text768 print(f"LLM output length: {len(raw_output)} chars", file=sys.stderr)769770 dsl_syntax = clean_dsl_output(raw_output)771 print(f"Cleaned DSL length: {len(dsl_syntax)} chars", file=sys.stderr)772773 return dsl_syntax774775776def main():777 try:778 input_json = sys.stdin.read()779 execution_input = json.loads(input_json)780 inputs = execution_input.get("inputs", {})781782 content = inputs.get("content", "")783 instruction = inputs.get("instruction", "")784 template_name = inputs.get("template_name", "auto")785 model = inputs.get("llmModelId", "")786 temperature = float(inputs.get("temperature", 0.3) or 0.3)787 width = int(inputs.get("width", 800) or 800)788 height = int(inputs.get("height", 600) or 600)789 language = inputs.get("language", "auto")790791 if not content or not content.strip():792 raise ValueError("Content is required")793794 if len(content) > MAX_CONTENT_LENGTH:795 raise ValueError(f"Content too long ({len(content)} chars). Maximum is {MAX_CONTENT_LENGTH}.")796797 # NOTE: no model allowlist — `model` is the resolved `llmModelId` (an798 # llm.id/identity from the picker), not a name. The workspace planner +799 # WORKSPACE_LLM_MODEL injection already select a runnable model, same as800 # the other LLM node templates.801802 os.makedirs(OUTPUT_DIR, exist_ok=True)803804 # Step 1: Generate DSL via LLM805 print("Step 1: Generating infographic DSL via LLM...", file=sys.stderr)806 dsl_syntax = asyncio.run(generate_infographic(807 content, template_name, model, instruction, temperature, language808 ))809 print(f"Generated DSL:\n{dsl_syntax[:200]}...", file=sys.stderr)810811 # Step 2: Validate and fix DSL (enforce capacity, text limits, canvas sizing)812 print("Step 2: Validating DSL...", file=sys.stderr)813 dsl_syntax, rec_w, rec_h = validate_and_fix_dsl(dsl_syntax)814 width = max(width, rec_w)815 height = max(height, rec_h)816 print(f"Final canvas: {width}x{height}", file=sys.stderr)817818 # Step 3: Render DSL to image819 print("Step 3: Rendering infographic image...", file=sys.stderr)820 image_data = render_infographic(dsl_syntax, "png", width, height, scale=2)821822 # Save image823 out_filename = "infographic.png"824 out_path = os.path.join(OUTPUT_DIR, out_filename)825 with open(out_path, "wb") as f:826 f.write(image_data)827828 print(f"Saved: {out_filename} ({len(image_data)} bytes)", file=sys.stderr)829830 # Output both the image and the DSL syntax831 output = {832 "image": out_filename,833 "syntax": dsl_syntax,834 }835 print(json.dumps(output, indent=2))836837 except Exception as e:838 error_output = {839 "error": str(e),840 "errorType": type(e).__name__,841 "traceback": traceback.format_exc(),842 }843 print(json.dumps(error_output), file=sys.stderr)844 sys.exit(1)845846847if __name__ == "__main__":848 main()$ git log --oneline
v1.5.0
HEAD
2026-08-18v1.4.12026-06-27
v1.4.02026-06-24
v1.2.22026-05-22
v1.2.12026-05-07
v1.0.02026-04-09