$ cat node-template.py
Slide Generator
// Generates a single Reveal.js slide from text content and an instruction. Output is an HTML <section> element.
Process
Document
template.py
1import os2import sys3import json4import re5import base646import mimetypes7import traceback8import httpx910LITELLM_BASE_URL = os.getenv("OPENAI_BASE_URL")11LITELLM_API_KEY = os.getenv("LITELLM_API_KEY")1213if not LITELLM_BASE_URL:14 raise ValueError("OPENAI_BASE_URL environment variable not set")15if not LITELLM_API_KEY:16 raise ValueError("LITELLM_API_KEY environment variable not set")1718SYSTEM_PROMPT = '''You are a presentation slide designer. Your task is to generate exactly ONE Reveal.js slide as an HTML <section> element.1920RULES:211. Output ONLY a single <section>...</section> element. No <!DOCTYPE>, no <html>, no <head>, no wrapper.222. Use <h2> for the slide title (not <h1>).233. Use semantic HTML: <ul>/<ol> for lists, <p> for text, <table> for data, <blockquote> for quotes.244. Use class="fragment" on elements that should appear one-by-one during the presentation.255. Use <aside class="notes">...</aside> for speaker notes (always include brief notes).266. Do NOT set data-background-color on <section>. The presentation theme controls all colors — never override it.277. Keep content concise — slides should be scannable, not walls of text.288. Do NOT include any CSS <style> blocks or <script> tags.299. Do NOT set color, background-color, or any color-related inline styles. Let the Reveal.js theme handle all colors.3010. Only use inline styles for LAYOUT purposes (display:flex, display:grid, gap, font-size). Never for colors or backgrounds.3111. Use plain HTML that Reveal.js themes will style automatically.3233IMAGE PLACEHOLDERS:34When a slide layout requires images, use picsum.photos for royalty-free placeholder images:35- Landscape: <img src="https://picsum.photos/seed/{topic}/600/400" alt="description">36- Portrait: <img src="https://picsum.photos/seed/{topic}/400/600" alt="description">37- Square: <img src="https://picsum.photos/seed/{topic}/400/400" alt="description">38Replace {topic} with a short keyword related to the slide content (e.g. "analytics", "team", "growth").39Always include a descriptive alt attribute. Set width via style if needed for layout.4041LAYOUT PATTERNS you can use:42- Title slide: <h2> + <p> subtitle43- Bullet points: <h2> + <ul> with <li class="fragment">44- Two columns: <div style="display:flex;gap:2em"> with two <div> children45- Data table: <h2> + <table>46- Quote: <blockquote> + attribution47- Image + text: <h2> + <div style="display:flex;gap:2em"> with <img> and text side by side4849Always output valid HTML. The content should be based ONLY on the provided source material — do not invent facts or data.'''5051# Layout instructions for each slide template52TEMPLATE_INSTRUCTIONS = {53 # Title54 "title-slide": "Create a TITLE SLIDE: centered <h2> with large bold title + <p> subtitle below. Keep it simple and impactful.",55 "title-subtitle": "Create a TITLE & SUBTITLE slide: <h2> title + multiple <p> elements for extended subtitle/description text.",56 "section-divider": "Create a SECTION DIVIDER: a simple <section> with centered <h2> in bold and a <p> sub-label. Do NOT set any background color — the theme handles it.",57 "closing-thankyou": "Create a CLOSING slide: centered <h2> 'Thank You' or similar + <p> with contact info or call-to-action below.",58 # Content59 "bullet-points": "Create a BULLET POINTS slide: <h2> title + <ul> with 4-6 <li class='fragment'> bullet items. Keep each bullet concise.",60 "numbered-list": "Create a NUMBERED LIST slide: <h2> title + <ol> with numbered <li class='fragment'> items for steps or sequences.",61 "key-takeaway": "Create a KEY TAKEAWAY slide: <h2> small label + <p style='font-size:1.4em'> with one prominent statement, then <p> for supporting context.",62 "text-block": "Create a TEXT BLOCK slide: <h2> title + several <p> paragraphs of flowing text content. Keep paragraphs concise.",63 # Layout64 "two-column": "Create a TWO-COLUMN slide: <h2> title + <div style='display:flex;gap:2em'> with two equal <div> children containing content.",65 "two-column-header": "Create a TWO-COLUMN WITH HEADER slide: <h2> title + <p> subtitle description + <div style='display:flex;gap:2em'> with two <div> columns.",66 "image-left": "Create an IMAGE LEFT slide. Use this EXACT structure:\n"67 "<section>\n"68 " <div style='display:flex;gap:2em;align-items:center'>\n"69 " <div style='flex:1;min-width:0'><img src='{{IMAGE}}' alt='description' style='width:100%;border-radius:8px'></div>\n"70 " <div style='flex:1;min-width:0'>\n"71 " <h2>Title</h2>\n"72 " (text content, bullet points, etc.)\n"73 " </div>\n"74 " </div>\n"75 " <aside class='notes'>Speaker notes.</aside>\n"76 "</section>\n"77 "CRITICAL: Both children MUST have style='flex:1;min-width:0'. The image MUST be wrapped in a <div> — never put <img> as a direct flex child. "78 "Use {{IMAGE}} as the exact src value — it will be replaced with the actual image.",79 "image-right": "Create an IMAGE RIGHT slide. Use this EXACT structure:\n"80 "<section>\n"81 " <div style='display:flex;gap:2em;align-items:center'>\n"82 " <div style='flex:1;min-width:0'>\n"83 " <h2>Title</h2>\n"84 " (text content, bullet points, etc.)\n"85 " </div>\n"86 " <div style='flex:1;min-width:0'><img src='{{IMAGE}}' alt='description' style='width:100%;border-radius:8px'></div>\n"87 " </div>\n"88 " <aside class='notes'>Speaker notes.</aside>\n"89 "</section>\n"90 "CRITICAL: Both children MUST have style='flex:1;min-width:0'. The image MUST be wrapped in a <div> — never put <img> as a direct flex child. "91 "Use {{IMAGE}} as the exact src value — it will be replaced with the actual image.",92 # Data93 "stats-metrics": "Create a STATS/METRICS slide: <h2> title + flex/grid container with 3-5 metric cards, each having a large number (<strong style='font-size:2em'>) and label below.",94 "data-table": "Create a DATA TABLE slide: <h2> title + <table> with <thead>/<tbody>, proper headers, and data rows. Keep the table clean and readable.",95 "timeline": "Create a TIMELINE slide: <h2> title + ordered list or custom layout showing chronological events with dates and descriptions.",96 "progress-steps": "Create a PROGRESS STEPS slide: <h2> title + horizontal flex container with numbered step circles/badges and labels below each step.",97 # Compare98 "side-by-side": "Create a SIDE-BY-SIDE comparison: <h2> title + <div style='display:flex;gap:2em'> with two <div> sections, each with its own <h3> heading and feature list.",99 "pros-cons": "Create a PROS & CONS slide: <h2> title + two columns — left column with green-themed 'Pros' heading and list, right with red-themed 'Cons' heading and list.",100 "before-after": "Create a BEFORE & AFTER slide: <h2> title + two columns with 'Before' label on left and 'After' label on right, showing the transformation.",101 "swot-matrix": "Create a SWOT MATRIX: <h2> title + 2x2 CSS grid (<div style='display:grid;grid-template-columns:1fr 1fr;gap:1em'>). Four cells: Strengths, Weaknesses, Opportunities, Threats, each with colored header and bullet items.",102 # Special103 "quote-blockquote": "Create a QUOTE slide: <blockquote><p> with the quoted text </p></blockquote> + <p> for attribution (e.g. '— Author Name'). Make the quote prominent.",104 "team-profile-grid": "Create a TEAM GRID slide: <h2> title + flex/grid container with team member cards, each having a name in <strong>, role in <em>, and brief description.",105 "code-showcase": "Create a CODE SHOWCASE slide: <h2> title + <pre><code> block with formatted code sample, followed by <p> explanation of what the code does.",106 "big-number": "Create a BIG NUMBER slide: <h2> small context label + <p style='font-size:3em;font-weight:bold'> with one large number/stat + <p> description below explaining the significance.",107 # Branded — Polizia Postale (layout from original PPTX)108 "polpostale-intro": "Create a POLIZIA POSTALE INTRO/COVER slide. Use this EXACT structure:\n"109 "<section class='pp-cover'>\n"110 " <div class='pp-intro-layout'>\n"111 " <img class='pp-emblem' src='__PP_STEMMA__' alt='Stemma'>\n"112 " <div class='pp-intro-text'>\n"113 " <h2>TITLE</h2>\n"114 " <p class='pp-subtitle'>Subtitle line 1</p>\n"115 " <p class='pp-subtitle'>Subtitle line 2</p>\n"116 " </div>\n"117 " </div>\n"118 " <aside class='notes'>Cover slide.</aside>\n"119 "</section>\n"120 "Replace the title and subtitle with content from the source material. "121 "The emblem goes on the LEFT, title text on the RIGHT — matching the original PPTX layout. "122 "The h2 title should be in UPPERCASE. The pp-subtitle paragraphs are for secondary info. "123 "The h2 title should be in UPPERCASE. The pp-subtitle paragraphs are for secondary info. Keep all class names exactly.",124 "polpostale-content": "Create a POLIZIA POSTALE CONTENT slide. Use this EXACT structure:\n"125 "<section>\n"126 " <div class='pp-header-title'>SLIDE TITLE HERE</div>\n"127 " <h2>Section heading</h2>\n"128 " (content: bullet points, text, tables, etc.)\n"129 " <aside class='notes'>Speaker notes.</aside>\n"130 "</section>\n"131 "The pp-header-title is white bold text positioned INSIDE the blue header bar at the top — this is the slide title. "132 "The <h2> below is for the main content heading in navy color. "133 "The theme background image already includes the blue header bar, Polizia Postale badge, and footer — do NOT add any badge images. "134 "Use standard HTML for content (lists, tables, text).",135 "polpostale-image-left": "Create a POLIZIA POSTALE IMAGE LEFT slide. Use this EXACT structure:\n"136 "<section>\n"137 " <div class='pp-header-title'>SLIDE TITLE HERE</div>\n"138 " <div style='display:flex;gap:2em;align-items:center;padding:0.5em 1em'>\n"139 " <div style='flex:1'><img src='{{IMAGE}}' alt='description' style='max-width:100%;border-radius:8px'></div>\n"140 " <div style='flex:1'>\n"141 " <h2>Section heading</h2>\n"142 " (text content, bullet points, etc.)\n"143 " </div>\n"144 " </div>\n"145 " <aside class='notes'>Speaker notes.</aside>\n"146 "</section>\n"147 "The pp-header-title is white bold text positioned INSIDE the blue header bar at the top. "148 "Use {{IMAGE}} as the exact src value — it will be replaced with the actual image. "149 "The theme background image already includes the blue header bar, badge, and footer — do NOT add any badge images.",150 "polpostale-image-right": "Create a POLIZIA POSTALE IMAGE RIGHT slide. Use this EXACT structure:\n"151 "<section>\n"152 " <div class='pp-header-title'>SLIDE TITLE HERE</div>\n"153 " <div style='display:flex;gap:2em;align-items:center;padding:0.5em 1em'>\n"154 " <div style='flex:1'>\n"155 " <h2>Section heading</h2>\n"156 " (text content, bullet points, etc.)\n"157 " </div>\n"158 " <div style='flex:1'><img src='{{IMAGE}}' alt='description' style='max-width:100%;border-radius:8px'></div>\n"159 " </div>\n"160 " <aside class='notes'>Speaker notes.</aside>\n"161 "</section>\n"162 "The pp-header-title is white bold text positioned INSIDE the blue header bar at the top. "163 "Use {{IMAGE}} as the exact src value — it will be replaced with the actual image. "164 "The theme background image already includes the blue header bar, badge, and footer — do NOT add any badge images.",165 "polpostale-outro": "Create a POLIZIA POSTALE CLOSING slide. Use this EXACT structure:\n"166 "<section class='pp-closing'>\n"167 " <img class='pp-emblem' src='__PP_STEMMA__' alt='Stemma'>\n"168 " <p class='pp-closing-text'>GRAZIE</p>\n"169 " <aside class='notes'>Closing slide.</aside>\n"170 "</section>\n"171 "The closing slide shows the coat of arms next to 'GRAZIE' in large navy bold text (40pt equivalent). "172 "Replace 'GRAZIE' with appropriate closing text from the source material if relevant. "173 "The pp-closing class centers the layout horizontally. Keep all class names exactly.",174}175176177def build_layout_instruction(template_name):178 """Build additional system prompt instruction for the selected layout template."""179 if not template_name or template_name == "auto":180 return ""181 instruction = TEMPLATE_INSTRUCTIONS.get(template_name, "")182 if instruction:183 return f"\n\nIMPORTANT LAYOUT INSTRUCTION: The user selected the '{template_name}' layout. {instruction}\nFollow this layout pattern precisely while adapting the content."184 return f"\n\nThe user selected the '{template_name}' layout. Use this layout pattern."185186187def call_llm(messages: list, model: str) -> str:188 # Call LiteLLM proxy using OpenAI-compatible API.189 headers = {190 "Authorization": f"Bearer {LITELLM_API_KEY}",191 "Content-Type": "application/json",192 }193 payload = {194 "model": model,195 "messages": messages,196 "temperature": 0.3,197 }198 url = f"{LITELLM_BASE_URL}/v1/chat/completions"199200 with httpx.Client(timeout=300) as client:201 response = client.post(url, headers=headers, json=payload)202 response.raise_for_status()203 result = response.json()204 return result["choices"][0]["message"]["content"].strip()205206207def extract_section(html: str) -> str:208 # Extract the <section>...</section> from LLM output, handling markdown fences.209210 # Strip markdown code fences if present211 html = re.sub(r"^```(?:html)?\s*", "", html, flags=re.MULTILINE)212 html = re.sub(r"```\s*$", "", html, flags=re.MULTILINE)213 html = html.strip()214215 # Extract <section>...</section> if wrapped in other content216 match = re.search(r"(<section[\s\S]*</section>)", html, re.IGNORECASE)217 if match:218 return match.group(1)219220 # If no <section> found, wrap the content in one221 return f"<section>\n{html}\n</section>"222223224def main():225 try:226 execution_input = json.loads(sys.stdin.read())227 inputs = execution_input.get("inputs", {})228229 content = inputs.get("content")230 instruction = inputs.get("instruction")231 image_filename = inputs.get("image", "")232 template_name = inputs.get("template_name", "auto")233 llm_model = inputs.get("llmModel")234235 if not content:236 raise ValueError("Required input 'content' not provided")237 if not instruction:238 raise ValueError("Required input 'instruction' not provided")239 if not llm_model:240 raise ValueError("Required input 'llmModel' not provided")241242 # Read image and convert to base64 data URI if provided243 image_data_uri = ""244 if image_filename:245 image_path = f"/data/input/{image_filename}"246 if os.path.isfile(image_path):247 mime = mimetypes.guess_type(image_filename)[0] or "image/png"248 with open(image_path, "rb") as f:249 b64 = base64.b64encode(f.read()).decode("ascii")250 image_data_uri = f"data:{mime};base64,{b64}"251 print(f"Image loaded: {image_filename} ({len(b64)} base64 chars)", file=sys.stderr)252253 print(f"Generating slide with model: {llm_model}", file=sys.stderr)254 print(f"Content length: {len(content)} chars", file=sys.stderr)255 print(f"Instruction: {instruction[:100]}...", file=sys.stderr)256 print(f"Layout template: {template_name}", file=sys.stderr)257258 # Build system prompt with optional layout instruction259 system_prompt = SYSTEM_PROMPT + build_layout_instruction(template_name)260261 # If image provided, add instruction to use {{IMAGE}} placeholder262 if image_data_uri:263 system_prompt += (264 "\n\nIMAGE INPUT: The user has provided an image. "265 "Use {{IMAGE}} as the exact src attribute value for the <img> tag. "266 "CRITICAL: Always wrap the <img> inside a <div style='flex:1;min-width:0'> when using flex layout. "267 "Never place <img> as a direct flex child — it will overflow. "268 "Example: <div style='flex:1;min-width:0'><img src='{{IMAGE}}' alt='description' style='width:100%;border-radius:8px'></div>. "269 "Do NOT use picsum.photos — use {{IMAGE}} instead."270 )271272 user_message = (273 f"<instruction>{instruction}</instruction>\n\n"274 f"<source_content>\n{content}\n</source_content>"275 )276277 messages = [278 {"role": "system", "content": system_prompt},279 {"role": "user", "content": user_message},280 ]281282 raw_output = call_llm(messages, llm_model)283 slide_html = extract_section(raw_output)284285 # Post-process: ensure flex children have min-width:0 to prevent overflow.286 # LLMs output flex:1 without min-width:0, causing text to expand and hide images.287 def _fix_flex_styles(html):288 result = []289 i = 0290 tag = 'style="'291 while i < len(html):292 pos = html.find(tag, i)293 if pos == -1:294 # Also try single quotes295 pos2 = html.find("style='", i)296 if pos2 == -1:297 result.append(html[i:])298 break299 pos = pos2300 tag = "style='"301 q = "'"302 else:303 q = '"'304 result.append(html[i:pos])305 # Find end of style attribute306 start = pos + len('style=') + 1307 end = html.find(q, start)308 if end == -1:309 result.append(html[pos:])310 break311 style_val = html[start:end]312 # Fix: add min-width:0 if has flex:1 but not min-width313 if 'flex:' in style_val and '1' in style_val.split('flex:')[-1][:3] and 'min-width' not in style_val:314 style_val = style_val.rstrip(';') + ';min-width:0;overflow:hidden'315 result.append(f'style={q}{style_val}{q}')316 i = end + 1317 tag = 'style="'318 return ''.join(result)319320 slide_html = _fix_flex_styles(slide_html)321322 # Replace {{IMAGE}} placeholder with actual base64 data URI323 if image_data_uri and "{{IMAGE}}" in slide_html:324 slide_html = slide_html.replace("{{IMAGE}}", image_data_uri)325 print("Replaced {{IMAGE}} placeholder with base64 data URI", file=sys.stderr)326327 print(f"Generated slide: {len(slide_html)} chars", file=sys.stderr)328329 output = {"slide_html": slide_html}330 print(json.dumps(output))331332 except Exception as e:333 error_output = {334 "error": str(e),335 "errorType": type(e).__name__,336 "traceback": traceback.format_exc(),337 }338 print(json.dumps(error_output), file=sys.stderr)339 sys.exit(1)340341342if __name__ == "__main__":343 main()