$ 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_0001415ALLOWED_MODELS = {16 "qwen3.5-35b",17 "gpt-oss-120b",18}1920# ---------------------------------------------------------------------------21# System prompt — based on official AntV Infographic skill files with22# data-density, capacity, and layout-quality constraints.23# ---------------------------------------------------------------------------24SYSTEM_PROMPT = """You are an expert infographic designer using the AntV Infographic DSL.2526Given the user's content, produce ONE block of AntV Infographic DSL syntax that best visualizes the information.2728## STEP 0 — THINK BEFORE YOU GENERATE2930Before writing any DSL, silently analyse the input:311. Count distinct data items. How many items/nodes/points exist?322. Measure text: are labels short (1-3 words) or long (4+ words)?333. Decide structure type: list, sequence, compare, hierarchy, relation, chart.344. Pick a template whose capacity fits the item count (see TEMPLATE CAPACITY below).355. Plan concise labels — shorten aggressively if the source text is verbose.36Then output ONLY the DSL (no analysis, no commentary).3738## DSL FORMAT3940- First line: `infographic <template-name>`41- Blocks: `data` / `theme`, indented with two spaces.42- Key-value: `key value` (space-separated).43- Arrays: dash prefix `- ` for each entry.44- No JSON, no Markdown, no code fences, no explanation.4546Example:4748infographic list-row-horizontal-icon-arrow49data50 title Internet Technology Evolution51 desc Key milestones from Web 1.0 to AI52 lists53 - time 199154 label Web 1.055 desc First website by Tim Berners-Lee56 icon web57 - time 200458 label Web 2.059 desc Social media goes mainstream60 icon account multiple61 - time 202362 label AI Large Model63 desc ChatGPT sparks the AI revolution64 icon brain65theme66 palette #3b82f6 #8b5cf6 #f973166768## DATA FIELDS BY TEMPLATE TYPE (use ONLY the correct field)6970| Template prefix | Data field | Notes |71|---|---|---|72| `list-*` | `lists` | Flat array of items |73| `sequence-*` | `sequences` | Optional `order asc/desc` |74| `compare-binary-*` / `compare-hierarchy-left-right-*` | `compares` | MUST have exactly 2 root items, sub-items in `children` |75| `compare-swot` | `compares` | Exactly 4 root items (S/W/O/T) with `children` |76| `compare-quadrant-*` | `compares` | Exactly 4 items |77| `hierarchy-structure` / `hierarchy-structure-mirror` | `items` | Up to 3 nesting levels via `children` |78| Other `hierarchy-*` | `root` | Single root, tree via `children` nesting. Do NOT repeat `root`. |79| `relation-*` | `nodes` + `relations` | Edges: `A - label -> B` or `A -->|label| B` |80| `chart-*` | `values` | Optional `category` |81| Fallback | `items` | Only when none of the above match |8283NEVER mix data fields. Use exactly one per template.8485## TEXT DENSITY RULES (CRITICAL for visual quality)8687The rendering engine allocates fixed space per card/node. Overflow causes overlapping text. Obey these limits:8889| Field | Max chars | Guidance |90|---|---|---|91| `title` (chart-*) | 25 | Chart title. Keep concise — 3-4 words max. |92| `desc` (chart-*) | 50 | Chart subtitle. One short phrase. |93| `title` (other) | 35 | Main title. Concise headline. |94| `desc` (global) | 60 | Subtitle under the title. One line. |95| `label` | 25 | Item label. Shorten aggressively: abbreviate, drop articles/prepositions. |96| `desc` (per item) | 50 | Keep to one sentence. Omit if the label is self-explanatory. |97| `value` | 8 | Numeric only. E.g. `82%`, `3.94`, `1560` |9899Chart title + desc examples (chart-* templates):100 BAD: title "Docenti con le valutazioni più basse nel primo modulo" (52 chars — too long!)101 GOOD: title "Docenti Peggiori" desc "Valutazioni Modulo I" (16 + 20 chars)102 BAD: title "Distribuzione delle risposte al sondaggio" (40 chars — too long!)103 GOOD: title "Risposte Sondaggio" desc "Distribuzione per modulo" (18 + 25 chars)104105General shortening examples:106 BAD: "DINAMICA DELLE STRUTTURE ORGANIZZATIVE" (40 chars)107 GOOD: "Strutture Organizzative" (23 chars)108 BAD: "La lezione e stata interessante?" (31 chars)109 GOOD: "Interesse Lezione" (17 chars)110111When the source language is verbose (Italian, German, French), shorten to core nouns only. Drop verbs, articles, prepositions.112113## TEMPLATE CAPACITY LIMITS114115Exceeding these limits causes cramped, overlapping cards. If data exceeds a limit, either CHOOSE A DIFFERENT TEMPLATE or SUMMARIZE/GROUP items.116117| Template pattern | Max items | When exceeded, prefer |118|---|---|---|119| `list-grid-*` | 6 | list-column-*, list-waterfall-*, list-zigzag-* |120| `list-row-*` | 5 | list-column-*, sequence-snake-* |121| `list-column-*` | 8 | hierarchy-mindmap-*, sequence-roadmap-* |122| `list-sector-*` | 6 | list-column-*, list-waterfall-* |123| `list-pyramid-*` | 5 | sequence-pyramid-*, sequence-funnel-* |124| `sequence-steps-*` | 6 | sequence-snake-*, sequence-roadmap-* |125| `sequence-timeline-*` | 8 | sequence-roadmap-*, list-column-* |126| `sequence-roadmap-*` | 10 | Split into two infographics |127| `sequence-snake-*` | 10 | Split or use hierarchy-mindmap-* |128| `compare-binary-*` | 2 root + 5 children each | Reduce children |129| `compare-swot` | 4 root + 4 children each | Summarise children |130| `hierarchy-tree-*` | 3 levels, 4-5 nodes/level | Prune or use mindmap |131| `hierarchy-mindmap-*` | 3 levels, 6 nodes/level | OK for large data |132| `chart-pie-*` | 6 slices | Group small slices into "Other" |133| `chart-bar-*` / `chart-column-*` | 8 bars | Group or top-N |134| `relation-dagre-flow-*` | 10 nodes | Simplify graph |135136## TEMPLATE SELECTION GUIDE137138Match the content structure to a template family:139140- Strict order (process/steps/timeline/trend) -> `sequence-*`141 - Dates -> `sequence-timeline-*`142 - Step progression -> `sequence-stairs-*`, `sequence-ascending-*`, `sequence-steps-*`143 - Roadmap -> `sequence-roadmap-vertical-*`144 - Circular cycle -> `sequence-circular-simple`145 - Narrowing/filtering -> `sequence-funnel-simple`, `sequence-pyramid-simple`146 - Snake/zigzag path -> `sequence-snake-steps-*`, `sequence-zigzag-*`147- Feature listing / bullet points -> `list-row-*` (horizontal) or `list-column-*` (vertical)148- Grid of cards (FEW items) -> `list-grid-*` (max 6 items!)149- Waterfall / cascading -> `list-waterfall-*`150- Binary A-vs-B -> `compare-binary-*`151- SWOT -> `compare-swot`152- 2x2 matrix -> `compare-quadrant-*`153- Tree / categorization -> `hierarchy-tree-*`154- Mind map / brainstorm -> `hierarchy-mindmap-*`155- Org chart -> `hierarchy-structure`, `hierarchy-structure-mirror`156- Numeric data -> `chart-*` (pie=proportions, bar/column=comparison, line=trend)157- Word cloud -> `chart-wordcloud`158- Network/dependencies -> `relation-dagre-flow-*`, `relation-network-*`, `relation-circle-*`159160## HIERARCHY TREE VARIANTS161162Tree templates follow the pattern: `hierarchy-tree-{direction}-{edge}-{node}`163- Direction: (default=top-down), `bt` (bottom-up), `lr` (left-right), `rl` (right-left)164- Edge style: `curved-line`, `dashed-line`, `dashed-arrow`, `distributed-origin`, `tech-style`165- Node style: `badge-card`, `capsule-item`, `compact-card`, `ribbon-card`, `rounded-rect-node`166167## AVAILABLE TEMPLATES (237 total)168169chart (11): chart-bar-plain-text, chart-column-simple, chart-line-plain-text,170chart-pie-compact-card, chart-pie-donut-compact-card, chart-pie-donut-pill-badge,171chart-pie-donut-plain-text, chart-pie-pill-badge, chart-pie-plain-text,172chart-wordcloud, chart-wordcloud-rotate173174list (29): list-column-done-list, list-column-simple-vertical-arrow, list-column-vertical-icon-arrow,175list-grid-badge-card, list-grid-candy-card-lite, list-grid-circular-progress,176list-grid-compact-card, list-grid-done-list, list-grid-horizontal-icon-arrow,177list-grid-progress-card, list-grid-ribbon-card, list-grid-simple,178list-pyramid-badge-card, list-pyramid-compact-card, list-pyramid-rounded-rect-node,179list-row-circular-progress, list-row-horizontal-icon-arrow, list-row-horizontal-icon-line,180list-row-simple-horizontal-arrow, list-row-simple-illus,181list-sector-half-plain-text, list-sector-plain-text, list-sector-simple,182list-waterfall-badge-card, list-waterfall-compact-card,183list-zigzag-down-compact-card, list-zigzag-down-simple,184list-zigzag-up-compact-card, list-zigzag-up-simple185186sequence (47): sequence-ascending-stairs-3d-simple, sequence-ascending-stairs-3d-underline-text,187sequence-ascending-steps, sequence-circle-arrows-indexed-card,188sequence-circular-simple, sequence-circular-underline-text,189sequence-color-snake-steps-horizontal-icon-line, sequence-color-snake-steps-simple-illus,190sequence-cylinders-3d-simple, sequence-filter-mesh-simple, sequence-filter-mesh-underline-text,191sequence-funnel-simple, sequence-horizontal-zigzag-horizontal-icon-line,192sequence-horizontal-zigzag-plain-text, sequence-horizontal-zigzag-simple,193sequence-horizontal-zigzag-simple-horizontal-arrow, sequence-horizontal-zigzag-simple-illus,194sequence-horizontal-zigzag-underline-text, sequence-mountain-underline-text,195sequence-pyramid-simple, sequence-roadmap-vertical-badge-card,196sequence-roadmap-vertical-pill-badge, sequence-roadmap-vertical-plain-text,197sequence-roadmap-vertical-quarter-circular, sequence-roadmap-vertical-quarter-simple-card,198sequence-roadmap-vertical-simple, sequence-roadmap-vertical-underline-text,199sequence-snake-steps-compact-card, sequence-snake-steps-pill-badge,200sequence-snake-steps-simple, sequence-snake-steps-simple-illus,201sequence-snake-steps-underline-text, sequence-stairs-front-compact-card,202sequence-stairs-front-pill-badge, sequence-stairs-front-simple,203sequence-steps-badge-card, sequence-steps-simple, sequence-steps-simple-illus,204sequence-timeline-done-list, sequence-timeline-plain-text,205sequence-timeline-rounded-rect-node, sequence-timeline-simple,206sequence-timeline-simple-illus, sequence-zigzag-pucks-3d-indexed-card,207sequence-zigzag-pucks-3d-simple, sequence-zigzag-pucks-3d-underline-text,208sequence-zigzag-steps-underline-text209210compare (20): compare-binary-horizontal-badge-card-arrow, compare-binary-horizontal-badge-card-fold,211compare-binary-horizontal-badge-card-vs, compare-binary-horizontal-compact-card-arrow,212compare-binary-horizontal-compact-card-fold, compare-binary-horizontal-compact-card-vs,213compare-binary-horizontal-simple-arrow, compare-binary-horizontal-simple-fold,214compare-binary-horizontal-simple-vs, compare-binary-horizontal-underline-text-arrow,215compare-binary-horizontal-underline-text-fold, compare-binary-horizontal-underline-text-vs,216compare-hierarchy-left-right-circle-node-pill-badge,217compare-hierarchy-left-right-circle-node-plain-text,218compare-hierarchy-row-letter-card-compact-card,219compare-hierarchy-row-letter-card-rounded-rect-node,220compare-quadrant-quarter-circular, compare-quadrant-quarter-simple-card,221compare-quadrant-simple-illus, compare-swot222223hierarchy (112): hierarchy-mindmap-branch-gradient-capsule-item, hierarchy-mindmap-branch-gradient-circle-progress,224hierarchy-mindmap-branch-gradient-compact-card, hierarchy-mindmap-branch-gradient-lined-palette,225hierarchy-mindmap-branch-gradient-rounded-rect, hierarchy-mindmap-level-gradient-capsule-item,226hierarchy-mindmap-level-gradient-circle-progress, hierarchy-mindmap-level-gradient-compact-card,227hierarchy-mindmap-level-gradient-lined-palette, hierarchy-mindmap-level-gradient-rounded-rect,228hierarchy-structure, hierarchy-structure-mirror,229hierarchy-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)230231relation (18): relation-circle-circular-progress, relation-circle-icon-badge,232relation-dagre-flow-lr-animated-badge-card, relation-dagre-flow-lr-animated-capsule,233relation-dagre-flow-lr-animated-compact-card, relation-dagre-flow-lr-animated-simple-circle-node,234relation-dagre-flow-lr-badge-card, relation-dagre-flow-lr-compact-card,235relation-dagre-flow-lr-simple-circle-node, relation-dagre-flow-tb-animated-badge-card,236relation-dagre-flow-tb-animated-capsule, relation-dagre-flow-tb-animated-compact-card,237relation-dagre-flow-tb-animated-simple-circle-node, relation-dagre-flow-tb-badge-card,238relation-dagre-flow-tb-compact-card, relation-dagre-flow-tb-simple-circle-node,239relation-network-icon-badge, relation-network-simple-circle-node240241## ICON KEYWORDS242243Use icon keywords (matched by the renderer). Examples:244star fill, check circle, trending up, chart bar, users, lightbulb, rocket,245shield, target, globe, code, database, search, alert triangle, clock, calendar,246brain, heart, zap, trophy, sprout, document text, web, cellphone, cloud,247application brackets, sun, moon, flash fast, secure shield check, account multiple248249## THEME OPTIONS250251Optional `theme` block for customisation:252253Dark theme + custom palette:254 theme dark255 palette256 - #61DDAA257 - #F6BD16258 - #F08BB4259260Hand-drawn style:261 theme262 stylize rough263 base264 text265 font-family 851tegakizatsu266267Available stylize types: rough, pattern, linear-gradient, radial-gradient268269## INPUT DATA FORMAT270271The input may include tool call results. Pay attention to:272- `intermediate: true` = research/helper steps. Useful context but SECONDARY.273- `intermediate: false` (or absent) = PRIMARY/FINAL data. Focus on these.274Prioritise non-intermediate content for the main data points.275276## COMPLETE DSL EXAMPLES277278List (horizontal arrow):279infographic list-row-horizontal-icon-arrow280data281 title Feature List282 lists283 - label Fast284 icon flash fast285 - label Secure286 icon secure shield check287 - label Scalable288 icon cloud289290Sequence (steps):291infographic sequence-steps-simple292data293 title Build Process294 sequences295 - label Design296 desc Create wireframes297 - label Develop298 desc Build the MVP299 - label Launch300 desc Release to users301 order asc302303Hierarchy (tree):304infographic hierarchy-tree-curved-line-rounded-rect-node305data306 root307 label Company308 children309 - label Engineering310 children311 - label Frontend312 - label Backend313 - label Marketing314315Compare (SWOT):316infographic compare-swot317data318 compares319 - label Strengths320 children321 - label Strong brand322 - label Loyal users323 - label Weaknesses324 children325 - label High cost326 - label Opportunities327 children328 - label Emerging markets329 - label Threats330 children331 - label Competitors332333Chart (column):334infographic chart-column-simple335data336 title Monthly Revenue337 values338 - label Jan339 value 1280340 - label Feb341 value 1560342 - label Mar343 value 1890344345Relation (flow):346infographic relation-dagre-flow-tb-simple-circle-node347data348 nodes349 - id A350 label Input351 - id B352 label Process353 - id C354 label Output355 relations356 A - feeds -> B357 B - produces -> C358359## CRITICAL RULES3603611. Output ONLY DSL — no markdown, no code fences, no commentary, no explanation.3622. First line MUST be `infographic <template-name>`.3633. Two-space indentation throughout.3644. Use the CORRECT data field for the chosen template (see DATA FIELDS table).3655. Respect TEXT DENSITY LIMITS: label <= 25 chars, desc <= 50 chars, title <= 35 chars.3666. For chart-* templates: title MUST be <= 25 chars (3-4 words), desc <= 50 chars. Keep chart titles concise.3677. Respect TEMPLATE CAPACITY LIMITS. If data exceeds capacity, choose a larger template or summarise.3688. When source text is verbose, SHORTEN to core nouns. Drop articles, verbs, prepositions.3699. Do NOT fabricate data unrelated to the user's content.37010. Binary compare templates MUST have exactly 2 root items.37111. Hierarchy templates use single `root` (do not repeat `root`).37212. Preserve the user's language for labels — but shorten within that language.37313. When a specific template is requested, use it even if another might fit better.374"""375376# ---------------------------------------------------------------------------377# Template DSL examples — fetched at runtime from the static manifest.378# The manifest lives at /templates/infographic/manifest.json (served by Next.js)379# and contains { id, category, syntax } for all 237 templates.380# ---------------------------------------------------------------------------381382_MANIFEST_CACHE = None383384def _fetch_manifest():385 """Fetch the template manifest once and cache it."""386 global _MANIFEST_CACHE387 if _MANIFEST_CACHE is not None:388 return _MANIFEST_CACHE389390 url = f"{EMBLEMA_API_BASE_URL}/templates/infographic/manifest.json"391 try:392 print(f"Fetching template manifest: {url}", file=sys.stderr)393 resp = requests.get(url, timeout=15)394 resp.raise_for_status()395 manifest = resp.json()396 # Build lookup dict: id -> syntax397 _MANIFEST_CACHE = {entry["id"]: entry.get("syntax", "") for entry in manifest}398 print(f"Loaded {len(_MANIFEST_CACHE)} template examples from manifest", file=sys.stderr)399 return _MANIFEST_CACHE400 except Exception as e:401 print(f"Warning: Could not fetch template manifest: {e}", file=sys.stderr)402 _MANIFEST_CACHE = {}403 return _MANIFEST_CACHE404405406def build_template_instruction(template_name):407 """When user picks a specific template, inject its DSL example into prompt."""408 if not template_name or template_name == "auto":409 return ""410411 manifest = _fetch_manifest()412 example = manifest.get(template_name, "")413414 if example:415 return (416 "\n\nYou MUST use the template '" + template_name + "'.\n"417 "Here is the reference syntax showing the correct DSL structure:\n\n" + example + "\n\n"418 "Rules:\n"419 "- Use the EXACT template name on the first line\n"420 "- Use the SAME DSL field names (e.g. 'lists', 'sequences', 'compares', 'root', 'nodes', 'values')\n"421 "- Adapt the number of items, nesting depth, and content to fit the user's data and instructions\n"422 "- The user's additional instructions take priority when they conflict with the example's structure"423 )424425 # Final fallback - no example available426 return "\n\nYou MUST use the template '" + template_name + "'."427428429def clean_dsl_output(raw_output):430 """Clean LLM output to extract pure DSL syntax."""431 text = raw_output.strip()432433 # Remove markdown code fences if present434 if text.startswith("```"):435 lines = text.split("\n")436 # Remove first line (```yaml or ```text or ```)437 lines = lines[1:]438 # Remove last line if it's closing ```439 if lines and lines[-1].strip() == "```":440 lines = lines[:-1]441 text = "\n".join(lines).strip()442443 # Ensure it starts with 'infographic '444 lines = text.split("\n")445 start_idx = 0446 for i, line in enumerate(lines):447 if line.strip().startswith("infographic "):448 start_idx = i449 break450451 text = "\n".join(lines[start_idx:]).strip()452453 if not text.startswith("infographic "):454 raise ValueError(455 "LLM output does not contain valid infographic DSL. "456 f"Output starts with: {text[:100]}"457 )458459 return text460461462# ---------------------------------------------------------------------------463# DSL Validator — enforces capacity limits, text lengths, and canvas sizing464# Runs AFTER LLM generation, BEFORE rendering.465# ---------------------------------------------------------------------------466467import re as _re468469# (max_items, swap_target_template)470_TEMPLATE_CAPACITY = {471 'list-grid-': (6, 'list-waterfall-badge-card'),472 'list-row-': (5, 'list-column-vertical-icon-arrow'),473 'list-sector-': (6, 'list-waterfall-compact-card'),474 'list-pyramid-': (5, 'list-waterfall-badge-card'),475 'sequence-steps-': (6, 'sequence-snake-steps-compact-card'),476 'sequence-stairs-': (5, 'sequence-snake-steps-compact-card'),477 'chart-pie-': (6, None),478 'chart-bar-': (8, None),479 'chart-column-': (8, None),480}481482_MAX_LABEL = 25483_MAX_ITEM_DESC = 50484_MAX_TITLE = 40485_MAX_GLOBAL_DESC = 70486487488def _count_top_items(lines):489 """Count top-level array entries in the DSL (lines starting with '- ' at the shallowest array indent)."""490 first_indent = None491 count = 0492 for line in lines:493 stripped = line.lstrip()494 if stripped.startswith('- '):495 indent = len(line) - len(stripped)496 if first_indent is None:497 first_indent = indent498 if indent == first_indent:499 count += 1500 return count501502503def _clean_value_text(text):504 """Strip verbose prefixes from value fields, keeping just the number."""505 text = text.strip()506 # Already short and numeric-looking — keep as-is507 if len(text) <= 8:508 return text509 # N/A variants510 if 'N/A' in text or 'n/a' in text:511 return 'N/A'512 # Extract the first number (with optional decimal and %)513 m = _re.search(r'(\d+\.?\d*%?)', text)514 if m:515 return m.group(1)516 # Fallback: truncate517 return text[:8]518519520def _recommend_canvas(template, item_count):521 """Suggest minimum canvas size based on template type and data volume."""522 W, H = 1920, 1080 # Base canvas (Full HD)523524 if template.startswith(('chart-column-', 'chart-bar-')):525 return (max(W, item_count * 250), H)526527 if template.startswith('chart-'):528 return (W, H)529530 if template.startswith('list-grid-'):531 cols = 3532 rows = max(2, (item_count + cols - 1) // cols)533 return (W, max(H, rows * 300))534535 if template.startswith(('list-column-', 'list-waterfall-', 'list-zigzag-')):536 return (W, max(H, item_count * 180))537538 if template.startswith('list-row-'):539 return (max(W, item_count * 280), H)540541 if template.startswith(('sequence-roadmap-', 'sequence-snake-', 'sequence-timeline-')):542 return (W, max(H, item_count * 160))543544 if template.startswith('sequence-'):545 return (max(W, item_count * 220), H)546547 if template.startswith('hierarchy-'):548 return (W, max(H, 1200))549550 return (W, H)551552553def validate_and_fix_dsl(dsl):554 """555 Post-LLM safety net: enforce template capacity, text limits, and canvas sizing.556 Returns (fixed_dsl, recommended_width, recommended_height).557 """558 lines = dsl.split('\n')559 if not lines or not lines[0].startswith('infographic '):560 return dsl, 1920, 1080561562 template = lines[0][len('infographic '):].strip()563 item_count = _count_top_items(lines)564 print(f"DSL validator: template={template}, items={item_count}", file=sys.stderr)565566 # --- 1. Template capacity enforcement ---567 for prefix, (limit, swap) in _TEMPLATE_CAPACITY.items():568 if template.startswith(prefix) and item_count > limit:569 if swap:570 print(571 f"DSL validator: {template} has {item_count} items "572 f"(max {limit}), swapping to {swap}",573 file=sys.stderr,574 )575 lines[0] = 'infographic ' + swap576 template = swap577 else:578 print(579 f"DSL validator: {template} has {item_count} items "580 f"(max {limit}), no swap available — keeping as-is",581 file=sys.stderr,582 )583 break584585 is_chart = template.startswith('chart-')586587 # --- 2. Chart-specific: strip per-item desc (bars need only label+value) ---588 if is_chart:589 to_remove = []590 for i, line in enumerate(lines):591 stripped = line.lstrip()592 depth = len(line) - len(stripped)593 if stripped.startswith('desc ') and depth > 2:594 to_remove.append(i)595 if to_remove:596 print(f"DSL validator: removing {len(to_remove)} item-level desc lines from chart", file=sys.stderr)597 for idx in reversed(to_remove):598 lines.pop(idx)599600 # --- 3. Chart-specific: enforce title (28) and desc (52) limits ---601 _TITLE_SUFFIX_RE = _re.compile(r'\s*(?:[-–—:]\s+.+|\([^)]+\))\s*$')602 _CHART_TITLE_MAX = 28603 _CHART_DESC_MAX = 52604 if is_chart:605 for i, line in enumerate(lines):606 stripped = line.lstrip()607 depth = len(line) - len(stripped)608 if stripped.startswith('title ') and depth <= 2:609 title_text = stripped[6:]610 indent_str = line[:depth]611 # Strip trailing suffix patterns if title is too long612 suffix_match = _TITLE_SUFFIX_RE.search(title_text)613 if suffix_match and len(title_text) > _CHART_TITLE_MAX:614 title_text = title_text[:suffix_match.start()].rstrip()615 print(f"DSL validator: stripped title suffix -> '{title_text}'", file=sys.stderr)616 # Hard limit — truncate at word boundary to avoid mid-word cuts617 if len(title_text) > _CHART_TITLE_MAX:618 cut = title_text[:_CHART_TITLE_MAX].rfind(' ')619 if cut > 5:620 title_text = title_text[:cut]621 else:622 title_text = title_text[:_CHART_TITLE_MAX]623 print(f"DSL validator: truncated chart title -> '{title_text}'", file=sys.stderr)624 lines[i] = indent_str + 'title ' + title_text625 break626 # Enforce chart desc limit (global desc only)627 for i, line in enumerate(lines):628 stripped = line.lstrip()629 depth = len(line) - len(stripped)630 if stripped.startswith('desc ') and depth <= 2:631 desc_text = stripped[5:]632 if len(desc_text) > _CHART_DESC_MAX:633 indent_str = line[:depth]634 cut = desc_text[:_CHART_DESC_MAX].rfind(' ')635 if cut > 10:636 desc_text = desc_text[:cut]637 else:638 desc_text = desc_text[:_CHART_DESC_MAX]639 lines[i] = indent_str + 'desc ' + desc_text640 print(f"DSL validator: truncated chart desc -> '{desc_text}'", file=sys.stderr)641 break642643 # --- 4. Text length enforcement ---644 # Track indent depth to distinguish global vs item fields.645 # Global title/desc are at indent 2 (directly under `data`).646 # Item fields (label, desc, value) are at indent 4+.647 for i, line in enumerate(lines):648 stripped = line.lstrip()649 indent_str = line[:len(line) - len(stripped)]650 depth = len(indent_str)651652 if stripped.startswith('title ') and depth <= 2:653 text = stripped[6:]654 if len(text) > _MAX_TITLE:655 lines[i] = indent_str + 'title ' + text[:_MAX_TITLE]656657 elif stripped.startswith('desc ') and depth <= 2:658 text = stripped[5:]659 if len(text) > _MAX_GLOBAL_DESC:660 lines[i] = indent_str + 'desc ' + text[:_MAX_GLOBAL_DESC]661662 elif stripped.startswith('label '):663 text = stripped[6:]664 if len(text) > _MAX_LABEL:665 lines[i] = indent_str + 'label ' + text[:_MAX_LABEL]666667 elif stripped.startswith('desc ') and depth > 2:668 text = stripped[5:]669 if len(text) > _MAX_ITEM_DESC:670 lines[i] = indent_str + 'desc ' + text[:_MAX_ITEM_DESC]671672 elif stripped.startswith('value '):673 text = stripped[6:]674 cleaned = _clean_value_text(text)675 if cleaned != text:676 print(f"DSL validator: cleaned value '{text}' -> '{cleaned}'", file=sys.stderr)677 lines[i] = indent_str + 'value ' + cleaned678679 # --- 5. Canvas sizing ---680 rec_w, rec_h = _recommend_canvas(template, item_count)681682 fixed = '\n'.join(lines)683 print(684 f"DSL validator: recommended canvas {rec_w}x{rec_h}",685 file=sys.stderr,686 )687 return fixed, rec_w, rec_h688689690def render_infographic(syntax, fmt="png", width=800, height=600, scale=2):691 """Call the Emblema infographic rendering API."""692 url = f"{EMBLEMA_API_BASE_URL}/api/v2/helpers/infographic/render"693694 headers = {"Content-Type": "application/json"}695 if USER_TOKEN:696 headers["Authorization"] = f"Bearer {USER_TOKEN}"697698 payload = {699 "syntax": syntax,700 "format": fmt,701 "width": width,702 "height": height,703 "scale": scale,704 }705706 print(f"Calling render API: {url}", file=sys.stderr)707 response = requests.post(url, headers=headers, json=payload, timeout=60)708709 if response.status_code != 200:710 try:711 error_detail = response.json()712 error_msg = error_detail.get("message", response.text)713 except Exception:714 error_msg = response.text715 raise RuntimeError(f"Render API returned {response.status_code}: {error_msg}")716717 return response.content718719720async def generate_infographic(content, template_name, model, instruction="", temperature=0.3):721 """Generate infographic DSL using LLM."""722 system = SYSTEM_PROMPT723 system += build_template_instruction(template_name)724725 user_message = content726 if instruction and instruction.strip():727 user_message += f"\n\n## Additional Instructions\n{instruction.strip()}"728729 messages = [730 {"role": "system", "content": system},731 {"role": "user", "content": user_message},732 ]733734 result = await Gais.llm.chat_async(messages, model=model, temperature=temperature)735 raw_output = result.text736 print(f"LLM output length: {len(raw_output)} chars", file=sys.stderr)737738 dsl_syntax = clean_dsl_output(raw_output)739 print(f"Cleaned DSL length: {len(dsl_syntax)} chars", file=sys.stderr)740741 return dsl_syntax742743744def main():745 try:746 input_json = sys.stdin.read()747 execution_input = json.loads(input_json)748 inputs = execution_input.get("inputs", {})749750 content = inputs.get("content", "")751 instruction = inputs.get("instruction", "")752 template_name = inputs.get("template_name", "auto")753 model = inputs.get("llmModel", "qwen3.5-35b")754 temperature = float(inputs.get("temperature", 0.3))755 width = int(inputs.get("width", 800))756 height = int(inputs.get("height", 600))757758 if not content or not content.strip():759 raise ValueError("Content is required")760761 if len(content) > MAX_CONTENT_LENGTH:762 raise ValueError(f"Content too long ({len(content)} chars). Maximum is {MAX_CONTENT_LENGTH}.")763764 if model not in ALLOWED_MODELS:765 raise ValueError(f"Invalid model '{model}'. Allowed: {', '.join(sorted(ALLOWED_MODELS))}")766767 os.makedirs(OUTPUT_DIR, exist_ok=True)768769 # Step 1: Generate DSL via LLM770 print("Step 1: Generating infographic DSL via LLM...", file=sys.stderr)771 dsl_syntax = asyncio.run(generate_infographic(772 content, template_name, model, instruction, temperature773 ))774 print(f"Generated DSL:\n{dsl_syntax[:200]}...", file=sys.stderr)775776 # Step 2: Validate and fix DSL (enforce capacity, text limits, canvas sizing)777 print("Step 2: Validating DSL...", file=sys.stderr)778 dsl_syntax, rec_w, rec_h = validate_and_fix_dsl(dsl_syntax)779 width = max(width, rec_w)780 height = max(height, rec_h)781 print(f"Final canvas: {width}x{height}", file=sys.stderr)782783 # Step 3: Render DSL to image784 print("Step 3: Rendering infographic image...", file=sys.stderr)785 image_data = render_infographic(dsl_syntax, "png", width, height, scale=2)786787 # Save image788 out_filename = "infographic.png"789 out_path = os.path.join(OUTPUT_DIR, out_filename)790 with open(out_path, "wb") as f:791 f.write(image_data)792793 print(f"Saved: {out_filename} ({len(image_data)} bytes)", file=sys.stderr)794795 # Output both the image and the DSL syntax796 output = {797 "image": out_filename,798 "syntax": dsl_syntax,799 }800 print(json.dumps(output, indent=2))801802 except Exception as e:803 error_output = {804 "error": str(e),805 "errorType": type(e).__name__,806 "traceback": traceback.format_exc(),807 }808 print(json.dumps(error_output), file=sys.stderr)809 sys.exit(1)810811812if __name__ == "__main__":813 main()$ git log --oneline
v1.2.1
HEAD
2026-05-07v1.0.02026-04-09