yourkln commited on
Commit
99c6658
Β·
verified Β·
1 Parent(s): ceac7f1

Upload folder using huggingface_hub

Browse files
.ipynb_checkpoints/inference_edited_chat_opt-checkpoint.py ADDED
@@ -0,0 +1,886 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Inference script for Qwen2.5-Coder-7B LoRA fine-tuned model
4
+ Input: list of prompt strings (hardcoded below)
5
+ Output: one .html file per prompt
6
+ """
7
+
8
+ import os
9
+ import re as _re
10
+ import torch
11
+ from pathlib import Path
12
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer
13
+
14
+ try:
15
+ from anthropic import Anthropic
16
+ _ANTHROPIC_AVAILABLE = True
17
+ except ImportError:
18
+ _ANTHROPIC_AVAILABLE = False
19
+
20
+ # 002 still the best with usloth
21
+ #/overfit_lora_6k_r32_8epochs_data_full/final_model and 0.02 is the best so far
22
+ # ──────────────────────────────────────────────────────────────────────────────
23
+ # Configuration
24
+ # ──────────────────────────────────────────────────────────────────────────────
25
+ MODEL_PATH = "/final_model"
26
+ OUTPUT_FOLDER = "/afdam_style15_20prompts_orig_detailed_style_2_002_less_style_more"
27
+ MAX_NEW_TOKENS = 16384
28
+ TEMPERATURE = 0.02
29
+ TOP_P = 0.9
30
+ DO_SAMPLE = True
31
+
32
+ SYSTEM_PROMPT = """You are a senior frontend architect.
33
+ Generate clean, responsive, production-ready HTML using only HTML + Tailwind CSS.
34
+
35
+ RULES:
36
+ - Output HTML only; no explanations.
37
+ - Follow the provided base HTML template.
38
+ - Adapt layouts to the target device (mobile / desktop / web).
39
+ - Use Tailwind classes exclusively.
40
+ - For brand names and wordmarks, use styled text elements (<span>, <a>) β€” NEVER generate <svg><path> elements for logos.
41
+ - For person/avatar photos, use https://i.pravatar.cc/150?img=N (vary N 1-70) β€” never storage.googleapis.com or any other URL.
42
+ - For all other image and video placeholders, use <AI-IMAGE class="..." src="short, descriptive image prompt with style" />.
43
+ - For fonts, load via Google Fonts <link rel="stylesheet"> only β€” NEVER use @font-face with remote src URLs.
44
+ - NEVER use storage.googleapis.com, uxpilot CDN, or any invented website domain URLs anywhere in the HTML.
45
+ - Use Font Awesome for icons via cdnjs.cloudflare.com.
46
+ - Use Highcharts (SVG mode) for charts when requested.
47
+ - Avoid full-height utility classes (100vh, h-screen, etc.).
48
+ - Assign unique IDs to main sections and cards.
49
+ - End output exactly at </html>.
50
+
51
+ BASE TEMPLATE:
52
+ <!DOCTYPE html>
53
+ <html>
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width,initial-scale=1">
57
+ <script src="https://cdn.tailwindcss.com"></script>
58
+ <link rel="preconnect" href="https://fonts.googleapis.com">
59
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
60
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
61
+ </head>
62
+ <body>
63
+ </body>
64
+ </html>"""
65
+
66
+ # ──────────────────────────────────────────────────────────────────────────────
67
+ # Prompt normalization (Haiku)
68
+ # ──────────────────────────────────────────────────────────────────────────────
69
+ NORMALIZE_PROMPTS = True
70
+ NORMALIZER_MODEL = "claude-sonnet-4-6"
71
+ NORMALIZER_TEMPERATURE = 0.4
72
+ NORMALIZER_MAX_TOKENS = 4096
73
+
74
+ # NORMALIZER_SYSTEM_PROMPT = """You are a prompt normalizer for a dashboard / application-UI HTML generation pipeline. A user gives you any kind of dashboard request β€” a one-liner, a partial brief, or a fully-detailed spec β€” and you output a single flowing paragraph in a precise structure that the downstream model expects.
75
+
76
+ # OUTPUT RULES (non-negotiable):
77
+ # - Output ONLY the normalized prompt. No preamble, no explanation, no headings, no markdown, no code blocks, no wrapping quotes.
78
+ # - Single flowing paragraph. No bullets, no numbered lists, no labeled sections like "Colors:" or "Typography:".
79
+ # - No mention of real brands you weren't given (Linear, Vercel, Notion, Salesforce, etc.) β€” steal patterns, never invent brand names. BUT if the user explicitly names their own product/brand (e.g., "Ardentis", "WindFarm", "Tsubaki"), preserve that exact name.
80
+ # - Length adapts to input. Short/vague input β†’ 500–900 words. Detailed input (β‰₯ 600 words) β†’ match the user's level of specificity, up to 2,000+ words. NEVER compress detailed input to fit a length budget.
81
+
82
+ # PRESERVE-DETAIL RULES (critical for detailed inputs):
83
+ # - The conversion is STRUCTURAL (labeled sections / bullets / lists β†’ flowing prose), NOT LOSSY. Every concrete detail the user provided survives the rewrite.
84
+ # - Preserve VERBATIM:
85
+ # * Exact text strings to render in the UI ("Yield Overview", "Total Collateral", "Booking Requests", any quoted copy or label)
86
+ # * Brand and product names exactly as written
87
+ # * Specific numbers (KPI values like $67659.99, counts like "ACTIVE 18", percentages, IDs like "944905011UZ")
88
+ # * All hex codes the user supplied β€” do not substitute "near-white" for their #f2f1ec
89
+ # * Distinctive widget elements (radial gauges with thresholds, kanban column counts, map overlay popups, multi-panel arrangements, status pill counts)
90
+ # - If the user describes a region in 80 words of detail, your prose version keeps every detail and is roughly the same length. Don't summarize.
91
+ # - If the user describes a nested element ("inside the sidebar, a nav stack, each nav item with icon + label + count"), preserve all nesting layers in your prose.
92
+
93
+ # AESTHETIC THINKING (when the user gave none): If the user supplied widgets and workflow but no aesthetic, imagine the dashboard deeply first as a product designer would before writing. Who is the operator? Is this a passive monitoring surface or an active work surface? Is data the hero or content the hero? What is the ONE visual move that makes this dashboard feel inevitable? Use your own taste. Avoid lazy defaults like dark navy + electric blue + Inter + 8px-corner cards.
94
+
95
+ # STRUCTURE (merge all six blocks into one continuous paragraph):
96
+
97
+ # 1. OPENING β€” "Design me a [DASHBOARD TYPE] for [DOMAIN/PRODUCT] β€” primary user is [OPERATOR ROLE], and the core workflow is [ACTION OR DECISION][, OPTIONAL MOOD FRAGMENT]."
98
+
99
+ # 2. DISTINCTIVE FLOURISH β€” one sentence describing a single standout interactive or visual behavior tied to data (live ticker on the primary KPI, pulse on a status indicator when state changes, hover-driven detail card over a chart point, smooth column-reorder on the kanban, soft glow on the active sidebar item, count-up animation on header counters). If the user didn't specify one, invent one that fits the operator's attention pattern.
100
+
101
+ # 3. STRUCTURAL WALKTHROUGH β€” walk through 8–14 regions using frame-language, not narrative-language. Describe positions ("the left side carries…", "the top edge holds…", "the main canvas is a 12-column grid where the top row contains…, the middle row splits into… and …, the bottom row carries…", "anchored to the bottom-right…","floating above the canvas in the top-right corner…"). Avoid landing-page connectives like "Start with…", "Then…", "Flow into…", "Below that…", "Anchored below…" β€” those imply a story scrolling top-to-bottom. A dashboard is a room: name what sits where in the frame.
102
+
103
+ # 4. COLORS INLINE β€” "Use [base] [hex] with [text] [hex] text, [primary accent] [hex] [role], [neutral] [hex] for [surfaces], and a state palette of [success-hex] success, [warning-hex] warning, [danger-hex] danger." Every color named inline with its hex code. Never use a "Colors:" label. Dashboards always need a state palette because status pills, gauge thresholds, and alert rows depend on it.
104
+
105
+ # 5. TYPOGRAPHY LINE β€” "[Display font] [size] [optional tracking] [weight range] for headings and large numerals, [body font] [weight range] [body size] for labels and table rows[, plus optional mono family for tabular figures]." Name real Google Fonts or common typographic families.
106
+
107
+ # 6. CLOSING RULE β€” ALWAYS end the output with this exact sentence: "Icons via Font Awesome only β€” never inline SVG." This is mandatory on every prompt. The closing rule must ALSO explicitly state, in the same final paragraph, the global rules for: (a) gradients, (b) shadows, and (c) rounded corners. Don't leave any of those three categories unspecified. Example: "Comfortable density throughout, no gradients except the subtle area-chart fills, no shadows except a soft elevated card on the active kanban column, rounded corners are 12px on cards and 999px on pills with no other radius values used. Icons via Font Awesome only β€” never inline SVG."
108
+
109
+ # NO-COIN-FLIPS CHECKLIST (apply before every output β€” these are the rules that separate prompts the model can render deterministically from prompts where it has to guess):
110
+
111
+ # 1. NO "or" CHOICES anywhere in the prompt. Pick ONE specific value. Ranges like "16–20px" are fine; binary "X or Y" between two identities is a coin-flip.
112
+
113
+ # 2. NO PER-WIDGET STAGGERED ANIMATIONS. ONE global directive only ("hold a 200ms blank state on dashboard mount, then all widgets appear simultaneously" or "no animation"). Real-time data updates are continuous, not animations β€” those are fine.
114
+
115
+ # 3. EXACTLY TWO TYPE FAMILIES, OR TWO + ONE MONO when the dashboard has heavy tabular data (table rows, ID columns, timestamps). The mono is allowed as a third family ONLY for tabular contexts; reinforce in closing rule: "two type families, plus a single mono used only for tabular figures."
116
+
117
+ # 4. ONE HEX PER COLOR ROLE. Every color used in any region must appear in the palette block, mapped to exactly one role. State palette (success/warning/danger) is mandatory for dashboards.
118
+
119
+ # 5. REGION SPECS MUST AGREE WITH CLOSING RULE. Every shadow / corner-radius / gradient mentioned in any region must be allowed by the closing rule.
120
+
121
+ # 6. DESIGN-LANGUAGE PROSE, NOT CSS-SPEC. Wrong: "box-shadow: 0 4px 12px rgba(0,0,0,0.08)", "font-weight 800", "letter-spacing -1px". Right: "soft elevated card", "extrabold tight tracking". Tailwind utility classes (py-6, gap-6, col-span-8) are FINE; pure CSS jargon is not.
122
+
123
+ # 7. CONSISTENT TAILWIND vs PROSE. Either describe spacing in Tailwind classes throughout or in design prose throughout. Don't mix.
124
+
125
+ # 8. NO AMBIGUOUS RULE EXCEPTIONS. Resolve in advance which widgets are exceptions to global rules ("no shadows except the active kanban column" instead of leaving it for the model to decide).
126
+
127
+ # SMART DEFAULTS (use when the user didn't specify):
128
+
129
+ # - COLORS: NEVER default to pure white + pure black only. Always produce 5–7 colors β€” base, text, muted copy, surface/card, border, primary accent, plus a mandatory state palette of success / warning / danger. Use a sophisticated palette that matches the dashboard's nature:
130
+ # * Trading / DeFi / financial monitoring: dark base #0A0F1A or #0D1117, off-white text, muted text, card surface #131C2E, plus a bright cool accent like cyan #22D3EE, electric blue #2563EB, or mint #34D399.
131
+ # * Operations / dispatch / logistics: dark base #0E0E10 or #131316, off-white text, muted, card surface #1C1C21, plus a status-color-driven accent rooted in green #10B981 / amber #F59E0B / red #EF4444.
132
+ # * Support / inbox / CRM: dim base #0F0F12 or #131316, off-white text, plus a warm accent like violet #A78BFA, amber #F59E0B, or coral #FB7185.
133
+ # * Analytics / BI: light base #FAFAFA, ink #0A0A0A text, muted #6B7280, surface #F4F4F4, plus a restrained accent like deep blue #1E40AF, slate #64748B, or emerald #047857.
134
+ # * Industrial / IoT / monitoring: light glass base #F5F7FA or dark control-room #0D0D0F, contextual accent in safety yellow #FACC15 or signal red #EF4444.
135
+ # * Project / workspace / kanban: dim base #0F0F12, off-white text, plus a playful accent like violet #8B5CF6, electric green #22C55E, or coral #F97316.
136
+
137
+ # - OPERATOR / WORKFLOW: infer from dashboard type. Yield / DeFi β†’ user managing positions. Fleet ops β†’ dispatcher coordinating drivers. CRM inbox β†’ support agent triaging. Analytics β†’ marketing or product lead reviewing. Industrial β†’ field operator or control-room engineer. Project β†’ PM running sprints.
138
+
139
+ # - FLOURISH: invent one that fits the workflow. Live ticker on primary KPI, pulse on state-change indicator, hover detail tooltip on chart, smooth kanban-card drag, breathing online dot, count-up animation on header counters β€” all valid patterns.
140
+
141
+ # - TYPOGRAPHY: use tasteful real-font pairings.
142
+ # * Trading / financial: Geist or Space Grotesk display + Inter body, plus Geist Mono for tabular figures.
143
+ # * Operations / industrial: Inter Tight or Space Grotesk display + Inter body, plus JetBrains Mono for sensor values and IDs.
144
+ # * CRM / inbox: Inter or Geist display + Inter body (keep simple, content is the hero).
145
+ # * Analytics / BI: Inter Display or SΓΆhne-likes + Inter body, plus IBM Plex Mono for table figures.
146
+ # * Project / workspace: Outfit or Plus Jakarta Sans display + Inter body.
147
+
148
+ # - REGIONS: always produce 8–14 distinct widget regions for a full dashboard surface, fewer for focused workspaces.
149
+
150
+ # IF THE USER'S INPUT IS ALREADY IN THIS STRUCTURE: leave it essentially as-is β€” only fix obvious structural gaps or missing fields. Do not rewrite already-good prompts.
151
+
152
+ # IF THE USER'S INPUT IS HIGHLY DETAILED (β‰₯ 600 words, has labeled regions, exact text strings, specific numbers, named widgets, unusual visual moves): treat the user's content as authoritative. Restructure into the 6-block flowing-prose format BUT keep every element the user mentioned β€” every exact label, every number, every hex code, every named widget, every nested layout layer. Your output will be longer than typical (1,500–2,200 words is fine). Do not summarize, simplify, or replace specific details with generic ones.
153
+
154
+ # Normalize the user's input now. Output only the normalized prompt."""
155
+
156
+ NORMALIZER_SYSTEM_PROMPT = """You are a prompt normalizer for a website-design HTML generation pipeline. A user gives you any kind of website-design request β€” a one-liner, a partial brief, or a fully-detailed spec β€” and you output a single flowing paragraph in a precise structure that the downstream model expects.
157
+
158
+ VERY IMPORTANT:
159
+
160
+ Always, ALWAYS specify which text color goes on which surface (so the model won't make mistakes like black text on black surface);
161
+
162
+ DO NOT USE LIGHT COLORS ON LIGHT BG, OR DARK COLORS ON DARK BG PLEASE.
163
+
164
+
165
+ OUTPUT RULES (non-negotiable):
166
+ - Output ONLY the normalized prompt. No preamble, no explanation, no headings, no markdown, no code blocks, no wrapping quotes.
167
+ - Single flowing paragraph. No bullets, no numbered lists, no labeled sections like "Colors:" or "Typography:".
168
+ - No mention of real brands you weren't given (Linear, Vercel, Apple, Stripe, Framer, Notion, etc.) β€” steal patterns, never invent brand names. BUT if the user explicitly names their own product/brand (e.g., "UX PILOT", "Nimbus", "Tsubaki"), preserve that exact name.
169
+ - Length adapts to input. Short/vague input β†’ 500–900 words. Detailed input (β‰₯ 600 words) β†’ match the user's level of specificity, up to 2,000+ words. NEVER compress detailed input to fit a length budget.
170
+
171
+ PRESERVE-DETAIL RULES (critical for detailed inputs):
172
+ - The conversion is STRUCTURAL (labeled sections / bullets / lists β†’ flowing prose), NOT LOSSY. Every concrete detail the user provided survives the rewrite.
173
+ - Preserve VERBATIM:
174
+ * Exact text strings to render in the page ("WELCOME BACK, ADAM", "Roasted Fresh, Delivered Daily", any quoted copy)
175
+ * Brand and product names exactly as written (UX PILOT, Nimbus, Nodey, Tsubaki)
176
+ * Specific numbers (counts, prices, percentages, IDs like "20881", "600,000+ USERS")
177
+ * All hex codes the user supplied β€” do not substitute "near-white" for their #f2f1ec
178
+ * Distinctive structural elements (architectural crop marks, ruler tick marks, geometric brackets, monospace annotations, nested mockups, drag-sliders, marquees with specific content, etc.)
179
+ - If the user describes a section in 80 words of detail, your prose version of that section keeps every detail and is roughly the same length. Don't summarize.
180
+ - If the user describes a nested element ("inside this container, there's a frame, inside the frame is a window mockup"), preserve all nesting layers in your prose.
181
+
182
+ AESTHETIC THINKING (when the user gave none): If the user supplied structure but no aesthetic, imagine the page deeply first as a design director would before writing, produce something good, like an award winning website, something breath taking.
183
+
184
+ STRUCTURE (merge all six blocks into one continuous paragraph):
185
+
186
+ 1. OPENING β€” "Design me a [SITE TYPE] homepage for [CONTEXT] β€” audience is [1-3 AUDIENCE TYPES], and the goal is [CONCRETE GOAL][, OPTIONAL MOOD FRAGMENT]."
187
+
188
+ 2. DISTINCTIVE FLOURISH β€” one sentence describing a single standout interactive or visual behavior (scroll-driven, cursor effect, load timing, time-of-day, typographic reveal, mouse parallax, live data tick, image-reveal animation, etc.). Example: "Replace the default cursor everywhere with a small violet dot that grows on hover." If the user didn't specify one, invent one that fits the brand tone.
189
+
190
+ 3. STRUCTURAL WALKTHROUGH β€” walk through 10–14 sections using connective phrases ("Start with… then a hero… Flow into… Follow with… Then… Close with…"). Each section gets a brief parenthetical layout hint with specific grid spans (col-span-7), pixel values (56px, min-400px), borders (1px), hex colors (#XXXXXX), spacing (py-32, mx-auto), and behaviors (hover lift, slow parallax, crossfade, image-zoom-hover). Typical sections to draw from: nav, hero, trust bar / logo strip, stats, features, how-it-works, product demo / preview, use cases, portfolio / work grid, testimonials, integrations, pricing, FAQ, final CTA, footer. Pick what fits the site type.
191
+
192
+ 4. COLORS INLINE β€” "Use [base] [hex] with [text] [hex] text, [primary accent] [hex] [role], and [neutral] [hex] for [surfaces]." Every color named inline with its hex code. Never use a "Colors:" label.
193
+
194
+ 5. TYPOGRAPHY LINE β€” "[Display font] [size] [optional tracking] [weight range] display, [body font] [weight range] [body size] body." Name real Google Fonts or common typographic families.
195
+
196
+ 6. CLOSING RULE β€” ALWAYS end the output with this exact sentence: "Icons via Font Awesome only β€” never inline SVG - never hidden body overflow." This is mandatory on every prompt. The closing rule must ALSO explicitly state, in the same final paragraph, the global rules for: (a) gradients, (b) shadows, and (c) rounded corners. Don't leave any of those three categories unspecified. Examples of valid closing rules: "Pure flat β€” no gradients, no shadows, no rounded corners except where explicitly noted on [list]. Icons via Font Awesome only β€” never inline SVG." or "Hard black offset shadows only where specified, no gradients anywhere, no rounded corners except the avatar circle and badge pill. Icons via Font Awesome only β€” never inline SVG."
197
+
198
+ NO-COIN-FLIPS CHECKLIST (apply before every output β€” these are the rules that separate prompts the model can render deterministically from prompts where it has to guess):
199
+
200
+ 1. NO "or" CHOICES anywhere in the prompt. Wrong: "approximately #1f2937 or #111827", "Cormorant or Playfair Display", "via a CSS triangle or Font Awesome icon". Right: pick ONE specific value. If the user gave a range like "16–20px thick", that's fine β€” it's a single design intent; the model can pick within. But a binary "X or Y" between two different identities is a coin-flip.
201
+
202
+ 2. NO PER-ELEMENT STAGGERED ANIMATIONS. Wrong: "every text element snaps in with a 40ms staggered clip-reveal" or "headline reveals first, then trust badge, then nav links". Right: ONE global state change. "Hold a 400ms blank state on load, then all elements snap into position simultaneously" or "scroll-driven warmth shift across the whole page" or "no animation at all". A single global directive the model can render with one CSS rule.
203
+
204
+ 3. EXACTLY TWO TYPE FAMILIES. Pick a display family + a body/UI family, OR a display family + a mono family. Never three. If the user's draft has three (e.g., Space Grotesk + Inter + JetBrains Mono), drop the middle one β€” Space Grotesk medium can do body, you don't need Inter as a third family. The closing rule should reinforce this: "exactly two type families across the entire page, no third family anywhere."
205
+
206
+ 4. ONE HEX PER COLOR ROLE. Every color used in any section must appear in the palette block, mapped to exactly one role. If the section says nav links are #2d2d2d but the palette doesn't list #2d2d2d, that's an undeclared color β€” the model treats it as noise. Every #hex used β†’ declared once in palette.
207
+
208
+ 5. SECTION SPECS MUST AGREE WITH CLOSING RULE. If the closing rule says "no shadows except the nav badge," then no other section should specify a shadow. If a section says "CTA hover has a 3px 3px 0 black shadow," the closing rule MUST allow that β€” e.g., "hard black offset shadows only where specified". Every shadow / corner-radius / gradient mentioned in any section must be allowed by the closing rule.
209
+
210
+ 6. DESIGN-LANGUAGE PROSE, NOT CSS-SPEC. Wrong: "rendered as a CSS border construction", "via a small CSS triangle", "font-weight 800", "letter-spacing -1px to -2px", "clamp(48px, 8vw, 80px)", "rgba(0,0,0,0.12)". Right: "thick lilac frame", "extrabold tight tracking", "subtle box shadow". The downstream model is trained on design-language descriptions β€” CSS-spec phrasing maps less cleanly. Tailwind utility classes (py-32, mx-auto, col-span-7) are FINE because the model is trained on Tailwind output; pure CSS jargon is not.
211
+
212
+ 7. CONSISTENT TAILWIND vs PROSE. Either describe spacing in Tailwind classes throughout (`py-80, px-8, mt-16`) or in design prose throughout ("generous vertical padding, comfortable horizontal padding"). Don't mix freely within a single prompt β€” pick one register and stick with it.
213
+
214
+ 8. NO AMBIGUOUS RULE EXCEPTIONS. If the closing rule says "no rounded corners on any interactive element" but the page has a circular user avatar, the model has to decide if the avatar is "interactive" β€” that's a coin-flip. Resolve in advance: either name the exception explicitly ("no rounded corners except the avatar circle and the badge pill") or remove the rounded element from the design.
215
+
216
+ SMART DEFAULTS (use when the user didn't specify):
217
+
218
+ - COLORS: NEVER default to pure white + pure black only. Always produce 4–5 colors β€” base, text, muted copy, surface/card, plus ONE accent. Use a sophisticated neutral palette that matches the brand tone:
219
+ * Editorial/minimal: near-white #FAFAFA base, ink #0A0A0A text, muted #6B7280 copy, surface #F4F4F4, plus a subtle accent like warm cream #F5EDE0, soft sage #8B9E84, dusty rose #D4748A, muted gold #C5A55A, olive #5C6B4F, or soft slate #64748B.
220
+ * Modern SaaS: white #FFFFFF base, near-black #0A0A0A or #111111 text, muted #6B7280 copy, card #F9FAFB, plus an accent like electric blue #2563EB, indigo #6366F1, emerald #10B981, or violet #A78BFA.
221
+ * Warm/premium: off-white #F7F5F0 base, natural #1C1C1A text, surface #EFEDE8, plus a warm accent like terracotta #C4775A, gold #B8934A, or sage #6B8F71.
222
+ * Dark/moody: charcoal #0A0A0A or #0D0D0D base, off-white #F0EBE3 text, secondary dark #1A1A1C, plus an accent like gold #C5A55A, electric green #22C55E, or electric blue #0066FF.
223
+
224
+ - AUDIENCE: infer from site type. SaaS β†’ product leads, founders, teams. DTC β†’ design-conscious consumers, ethical shoppers. Portfolio β†’ creative directors, clients. Dashboard β†’ name the professional role. Agency β†’ brand leads, marketing directors. App β†’ specific user persona.
225
+
226
+ - GOAL: infer from site type. SaaS β†’ trial signups and demo bookings. DTC β†’ first-purchase conversions. Portfolio β†’ project inquiries. App β†’ downloads and trial starts. Dashboard β†’ reduce friction on core tasks. Agency β†’ consultation requests.
227
+
228
+ - FLOURISH: invent one that fits the brand. Scroll velocity crop, time-of-day hero, cursor parallax, image-reveal on load, live data ticker, 600ms load hold, typographic reveal, background luminance pulse, kinetic captions β€” all valid patterns.
229
+
230
+ - TYPOGRAPHY: use tasteful real-font pairings.
231
+ * Editorial / serif: Playfair Display, Cormorant Garamond, Fraunces, DM Serif Display, Noto Serif Display + DM Sans or Inter body.
232
+ * Modern SaaS: Inter Tight, Geist, Space Grotesk + Inter or Geist body.
233
+ * Friendly: Outfit, Plus Jakarta Sans, Syne + Inter body.
234
+ * Dev / terminal: JetBrains Mono, IBM Plex Mono, Fira Code + Inter body.
235
+
236
+ - SECTIONS: always produce 10–14 sections even for vague inputs.
237
+
238
+ IF THE USER'S INPUT IS ALREADY IN THIS STRUCTURE: leave it essentially as-is β€” only fix obvious structural gaps or missing fields. Do not rewrite already-good prompts.
239
+
240
+ IF THE USER'S INPUT IS HIGHLY DETAILED (β‰₯ 600 words, has labeled sections, exact text strings, specific numbers, named UI components, unusual visual moves): treat the user's content as authoritative. Restructure into the 6-block flowing-prose format BUT keep every element the user mentioned β€” every exact text string, every number, every hex code, every named visual element, every nested layout layer. Your output will be longer than typical (1,500–2,200 words is fine). Do not summarize, simplify, or replace specific details with generic ones.
241
+
242
+ Normalize the user's input now. Output only the normalized prompt."""
243
+
244
+
245
+ DASHBOARD_NORMALIZER_SYSTEM_PROMPT = """You are a prompt normalizer for a dashboard / application-UI HTML generation pipeline. A user gives you any kind of dashboard request β€” a one-liner, a partial brief, or a fully-detailed spec β€” and you output a single flowing paragraph in a precise structure that the downstream model expects.
246
+
247
+ VERY IMPORTANT:
248
+
249
+ CARDS MUST BE VISIBLY DIFFERENT FROM THE BACKGROUND. Never give a card the same color as the page background β€” cards have to stand off from the surface they sit on. Always specify which text color goes on which surface; if the layout has a dark card on a light page (or any inverted surface), explicitly state the text colors for both the light and the dark surface so text never disappears into its background. Build structure through contrast, borders, and spacing together β€” not through any one alone. Make it easy to see what's a card, what's a button, what's text, and where one section ends and the next begins.
250
+
251
+
252
+ HARD LIMITS (apply before output β€” these are explicit negative rules; ignoring them produces plain or broken downstream output):
253
+
254
+
255
+ NO COORDINATION REQUIREMENTS BETWEEN REGIONS. Do not say "matching the map's height exactly" or "aligned to the table below" or "this card's width syncs with the chart above." Each region's dimensions are its own; the model handles alignment from grid structure alone.
256
+
257
+ DENSITY SELF-CHECK (before output): mentally count atomic specs in your output. If above 50, identify the bottom 20% by importance and cut them. Common safe cuts: pixel ranges that could be col-spans, secondary animation behaviors layered on the same elements, sub-elements inside cards that don't change the page's character if removed, multi-stat hero cards padded with 4-6 numbers, redundant restatements of the register (state it once at the top, don't repeat the rules in every section).
258
+
259
+ Thoroughness past a threshold becomes noise. The prompt's job is to set the register and the structural skeleton β€” not to resolve every visual decision in advance. The downstream model has its own taste at the pixel level; let it use it.
260
+
261
+ OUTPUT RULES (non-negotiable):
262
+ - Output ONLY the normalized prompt. No preamble, no explanation, no headings, no markdown, no code blocks, no wrapping quotes.
263
+ - Single flowing paragraph. No bullets, no numbered lists, no labeled sections like "Colors:" or "Typography:".
264
+ - No mention of real brands you weren't given (Linear, Vercel, Notion, Salesforce, Stripe, Mercury, Monzo, Revolut, etc.) β€” steal patterns, never invent brand names. BUT if the user explicitly names their own product/brand (e.g., "Ardentis", "WindFarm", "Tavolo"), preserve that exact name.
265
+ - Length adapts to input. Short/vague input β†’ 500–900 words. Detailed input (β‰₯ 600 words) β†’ match the user's level of specificity, up to 2,000+ words. NEVER compress detailed input to fit a length budget.
266
+ - Prefer design-direction prose over micro-spec. Set the register, not the pixel values β€” the downstream model has its own taste at the pixel level. Do NOT specify exact font-sizes, exact opacity percentages, or exact pixel paddings unless the user supplied them.
267
+
268
+ CLOSING RULE β€” ALWAYS end the output with this exact sentence: "Icons via Font Awesome only β€” never inline SVG - never hidden body overflow." This is mandatory on every prompt. The closing rule must ALSO explicitly state, in the same final paragraph, the global rules for: (a) gradients, (b) shadows, and (c) rounded corners. Don't leave any of those three categories unspecified. Examples of valid closing rules: "Pure flat β€” no gradients, no shadows, no rounded corners except where explicitly noted on [list]. Icons via Font Awesome only β€” never inline SVG." or "Hard black offset shadows only where specified, no gradients anywhere, no rounded corners except the avatar circle and badge pill. Icons via Font Awesome only β€” never inline SVG."
269
+
270
+ PRESERVE-DETAIL RULES (critical for detailed inputs):
271
+ - The conversion is STRUCTURAL (labeled sections / bullets / lists β†’ flowing prose), NOT LOSSY. Every concrete detail the user provided survives the rewrite.
272
+ - Preserve VERBATIM:
273
+ * Exact text strings to render in the UI ("Yield Overview", "Total Collateral", "Booking Requests", any quoted copy or label)
274
+ * Brand and product names exactly as written
275
+ * Specific numbers (KPI values like $67659.99, counts like "ACTIVE 18", percentages, IDs like "944905011UZ")
276
+ * All hex codes the user supplied β€” do not substitute "near-white" for their #f2f1ec
277
+ * Distinctive widget elements (radial gauges with thresholds, kanban column counts, map overlay popups, multi-panel arrangements, status pill counts)
278
+ - If the user describes a region in 80 words of detail, your prose version keeps every detail and is roughly the same length. Don't summarize.
279
+ - If the user describes a nested element ("inside the sidebar, a nav stack, each nav item with icon + label + count"), preserve all nesting layers in your prose.
280
+
281
+ VISUAL REGISTER (when the user gave none):
282
+
283
+ If the user supplied widgets and workflow but no aesthetic, commit to ONE coherent visual register before writing the structural walkthrough. Neutral is failure. Invent one that fits the operator's context, and describe it in design-direction prose β€” not in pixel values, exact opacities, or exact font-sizes. The downstream model handles those decisions from its own training; your job is to set the register and let the model's taste fill in the pixels.
284
+
285
+ Avoid the lazy fallback (dark navy + electric blue + Inter + 8px-corner cards) unless that genuinely is your imagined register β€” that's "Linear utilitarian" and only fits when the operator's context calls for it.
286
+
287
+ SMART DEFAULTS (use when the user didn't specify):
288
+
289
+ - COLORS: Always produce 5–7 colors β€” base, text, muted copy, surface/card, border, primary accent, plus mandatory state palette of success / warning / danger. Pick a palette that signals the chosen register from the library above. Avoid the lazy default (dark navy + electric blue) unless the register is genuinely Linear utilitarian.
290
+
291
+ - OPERATOR / WORKFLOW: infer from dashboard type. Yield / DeFi β†’ user managing positions. Fleet ops β†’ dispatcher coordinating drivers. CRM inbox β†’ support agent triaging. Analytics β†’ marketing or product lead reviewing. Industrial β†’ field operator or control-room engineer. Project β†’ PM running sprints.
292
+
293
+ - FLOURISH: live ticker on primary KPI, pulse on state-change indicator, hover detail tooltip on chart, smooth kanban-card drag, breathing online dot, count-up animation on header counters.
294
+
295
+ - TYPOGRAPHY: pick the pairing that matches the chosen register β€” Fraunces + Inter for soft modern fintech, Outfit + Inter for neo-banking calm, Tiempos + Inter for editorial broadsheet, Geist + Inter for glass dark aurora, JetBrains Mono alone for brutalist trading terminal, SΓΆhne-likes Condensed + Inter for swiss minimal grid, Inter + Geist Mono for Linear utilitarian, Fraunces + Inter + JetBrains Mono for architectural drafting.
296
+
297
+ - REGIONS: always produce 8–14 distinct widget regions for a full dashboard surface, fewer for focused workspaces.
298
+
299
+ IF THE USER'S INPUT IS ALREADY IN THIS STRUCTURE: leave it essentially as-is β€” only fix obvious structural gaps or missing fields.
300
+
301
+ IF THE USER'S INPUT IS HIGHLY DETAILED (β‰₯ 600 words, has labeled regions, exact text strings, specific numbers, named widgets, unusual visual moves): treat the user's content as authoritative. Restructure into the 6-block flowing-prose format BUT keep every element the user mentioned β€” every exact label, every number, every hex code, every named widget, every nested layout layer. Your output will be longer than typical (1,500–2,200 words is fine). Do not summarize.
302
+
303
+ Normalize the user's input now. Output only the normalized prompt."""
304
+
305
+ # Keywords that indicate a dashboard / app-UI prompt β€” case-insensitive substring match.
306
+ DASHBOARD_KEYWORDS = (
307
+ "dashboard",
308
+ "admin panel",
309
+ "control panel",
310
+ "control room",
311
+ "command center",
312
+ "console",
313
+ "workspace",
314
+ "monitoring",
315
+ "monitor surface",
316
+ "ops surface",
317
+ "ops console",
318
+ "kanban",
319
+ "inbox view",
320
+ "inbox workspace",
321
+ "data table",
322
+ "analytics overview",
323
+ "bi report",
324
+ "bi reporting",
325
+ "trading terminal",
326
+ "back office",
327
+ "internal tool",
328
+ )
329
+
330
+
331
+ def is_dashboard_prompt(prompt: str) -> bool:
332
+ """Return True if the user's prompt looks like a dashboard / app-UI request."""
333
+ p = prompt.lower()
334
+ return any(kw in p for kw in DASHBOARD_KEYWORDS)
335
+
336
+
337
+ # Use Highcharts (SVG mode) for charts when requested.
338
+ # 5prompts
339
+ PROMPTS = [
340
+ # 1
341
+ """Landing page for a habit-tracking mobile app called Streak. The audience is people in their 20s and 30s who have tried other habit apps and quit. The page needs: a hero section with the app name, a one-line value prop, two CTAs (App Store, Google Play), and a phone mockup placeholder showing the app; a 'why this one is different' section with three points (no streaks-anxiety, friend accountability, science-backed reminders); a section explaining how it works in four steps (pick a habit, set when, get nudged, check in); a testimonials section with three quotes from real users including their name, age, and the habit they built; a comparison table vs the two main competitors; a pricing block (free forever, $4/mo pro with extras); an FAQ with at least six questions covering data privacy, cancellation, family plans, and Apple Health integration; and a footer with newsletter signup, social links, and legal pages.""",
342
+
343
+ # 2
344
+ """Operations dashboard for a mid-size logistics company that runs ~200 trucks across the US. The primary user is a dispatcher who has this open all day. Needs a left sidebar with navigation (Overview, Shipments, Fleet, Drivers, Customers, Reports, Settings); a top bar with search, notifications bell, and a user avatar dropdown. The main area should have four KPI cards across the top (active shipments, on-time delivery rate, fleet utilization, fuel cost this week β€” each with a trend arrow and sparkline). Below that, a large interactive map placeholder showing truck positions, with a panel on the right listing the active shipments visible on the map. Below the map, a recent deliveries table (columns: shipment ID, origin, destination, driver, status, ETA, value), with status pills, sortable headers, and a filter bar. To the right of the table, a stacked alerts panel (delays, breakdowns, customer complaints), each alert clickable. The dashboard should feel dense but not cluttered.""",
345
+
346
+ # 3
347
+ """Marketing site for an AI legal assistant called Counsel.ai aimed at small law firms (2–20 attorneys). The pitch is: it drafts contracts, summarizes case law, and handles client intake conversations. Need a hero with the headline, sub-headline mentioning ABA-aligned, a CTA to book a demo and a secondary 'see it in action' that scrolls to a video. Below the hero: a logo strip of the law schools and firms already using it. Then a 'what it does' section with three big cards (contract drafting, case law research, intake automation), each with a short description and a screenshot placeholder. Then a live chat preview component showing a sample intake conversation between the AI and a potential client. Then an integrations section showing logos of Clio, MyCase, LexisNexis, Westlaw, Dropbox, and Gmail. A security and compliance section is critical β€” call out SOC2 Type II, attorney-client privilege handling, encryption at rest and in transit, and US data residency, presented as four trust badges with short explanations. End with a testimonials section featuring three managing partners by name and firm, a pricing block (Starter, Growth, Firm tiers), and two CTAs in the footer (book demo, start free trial).""",
348
+
349
+ # 4
350
+ """E-commerce product detail page for a premium pair of trail running shoes called the Ridge Pro 2. The page needs: a breadcrumb at the top (Home > Men > Running > Ridge Pro 2); a two-column layout with a vertical thumbnail strip and a main image gallery on the left (six placeholder images, with one being a 360-degree view), and the product info on the right. The right column needs the product title, a sub-line ('Built for technical descents'), a star rating with review count, the price ($185) with a 'free shipping over $75' note, a color picker with five swatches, a size picker with US sizes 7–13 including half sizes (some marked sold out), a quantity stepper, an 'Add to cart' button, and an 'Add to wishlist' icon button. Below the right column, a tabs section: Description (with a paragraph and bullet specs β€” weight, drop, stack height, lug depth, materials), Reviews (showing average rating, a 5-star bar breakdown, three sample reviews with reviewer name and verified-buyer badge), and Shipping & Returns. Below the two columns, a 'Pairs well with' carousel of four related products. On scroll, the add-to-cart bar should stick to the bottom of the viewport with the product name, selected variants, and a buy button.""",
351
+
352
+ # 5
353
+ """Onboarding flow page β€” step 3 of 5 β€” for a project management app called Mosaic. The user has just signed up and is being walked through setup. Step 3 is 'Invite your team.' The page should have a top progress indicator showing 5 steps with the third one active and the first two checked. The center of the page has a heading ('Bring your team in'), a sub-heading ('Mosaic works best when everyone is on it'), and a form area: three rows of inputs by default (each row: email field + role dropdown with options Admin/Member/Viewer), an 'Add another' link, an alternative 'Invite by link' section with a copyable URL and a 'reset link' option, and a section to bulk-paste emails. Below the form, two buttons: a primary 'Send invites and continue' and a secondary 'Skip for now.' On the right side of the page, a small contextual card: 'Why invite now? Teams that invite within the first day are 4x more likely to stick with Mosaic' with a small illustration placeholder. Footer should have a 'Need help?' link.""",
354
+
355
+ # 6
356
+ """Pricing calculator page for a cloud hosting provider called Stratus. The audience is engineering leads picking a vendor. The page should let them estimate monthly cost based on their usage. Layout: a title and short intro at the top, then a calculator card taking the center of the page with controls for compute (a slider for vCPUs from 1 to 64 and a slider for RAM from 1 to 256 GB), storage (a slider for SSD GB from 10 to 5000), bandwidth (a slider for monthly TB from 0.1 to 50), and a region selector with five options (US-East, US-West, EU-West, AP-South, AP-East) shown as pill buttons. As the user moves sliders, a price summary panel on the right of the calculator updates live: a big monthly total, a breakdown by line item, and an annual cost with a 'save 20% with annual' note. Below the calculator, a 'How we compare' section showing the same configuration's price on the three biggest competitors (AWS, GCP, DigitalOcean) β€” clearly labeled as estimates. Below that, an FAQ about how billing works, overage charges, what counts as bandwidth, and free tier. End with a CTA to start a free trial and a 'talk to sales' option for enterprise.""",
357
+
358
+ # 7
359
+ """Status page for a developer API service called Pulse. The audience is engineers who integrate Pulse and need to know if it's up. Top of the page: a big banner showing overall system status β€” green if everything is operational, with the message 'All systems operational' and a last-updated timestamp. Below the banner, a list of individual services (API, Dashboard, Webhooks, SDKs, Documentation, Authentication), each as a row with the service name on the left, a status pill on the right (operational, degraded, partial outage, major outage), and a 90-day uptime bar showing daily status as colored segments. Each row should be expandable to show recent metrics (latency p50/p95/p99, error rate). Below the service list, a 'Past incidents' section grouped by date, showing incident title, status (resolved, investigating, identified, monitoring), affected components, and a timeline of updates. At the very top right, a 'Subscribe to updates' button that opens a modal with options for email, webhook, RSS, and Slack. Header should have the company logo and links to status history, API docs, and main site.""",
360
+
361
+ # 8
362
+ """Case study page for a B2B design agency called North Field that just delivered a rebrand for a fintech client called Halcyon. The audience is potential clients evaluating the agency. The page needs: a hero with the client logo placeholder, the project title ('Rebranding Halcyon for the next stage'), a one-line summary, and three big stat numbers across the bottom (e.g., 47% lift in signups, 3 months delivery, 12 deliverables). Below the hero, a project meta strip (industry, timeline, services delivered, team size). Then a 'The challenge' section with two paragraphs and a pull quote from the client's CEO. Then a 'Our approach' section broken into three phases (discovery, design, rollout) each with a short paragraph and an image placeholder. Then a 'Results' section with three stat cards expanded into context (what the number means, how it was measured). Then a full-width testimonial quote from the CEO with their photo, name, and title. Then a deliverables gallery showing 6 placeholder images of work artifacts. End with a 'Ready to start your project?' CTA card and three thumbnails of related case studies.""",
363
+
364
+ # 9
365
+ """Settings page for a developer tools app called Forge. The user opening this is an engineering manager configuring their team's workspace. Layout: a left sub-nav listing settings sections (Profile, Workspace, API Keys, Webhooks, Team Members, Integrations, Billing, Danger Zone). The active section is API Keys. The main panel shows a heading and a short description of what API keys are. Below that, a 'Generate new key' button. Below that, a table of existing keys with columns: name, scope, last used, created, and a row actions menu (rename, revoke). Below the API keys table, the next section visible on scroll should be Webhooks β€” a list of registered webhook endpoints, each showing URL, events subscribed to, last delivery status, and a toggle to enable/disable. Below that, Team Members β€” a table with member avatars, names, emails, roles (with editable dropdowns), and a remove action. Below that, a Billing summary card showing current plan, next invoice amount, and 'manage billing' button. At the very bottom, a Danger Zone section in a clearly distinct visual treatment, with two destructive actions: Transfer Ownership and Delete Workspace. The page should feel like a serious admin surface.""",
366
+
367
+ # 10
368
+ """Inbox view for a customer support platform called Mailroom. The user is a support agent working through tickets. Three-panel layout. Left panel (narrow): conversation list with filter tabs at the top (All, Unassigned, Mine, Mentions), a search bar, and below that a list of conversations β€” each row showing customer avatar, name, subject preview, last message preview, time, and unread indicator. Middle panel (widest): the active conversation. Top of the panel: customer name, channel (email, chat, etc.), and action buttons (assign, snooze, close, more). Below that, the message thread β€” alternating customer and agent messages with timestamps, the agent's messages on the right. At the bottom of the middle panel, a reply composer with formatting toolbar, attachment, and a send button with a dropdown to send-and-close. Right panel (narrow): customer details β€” avatar, name, email, plan, signup date, lifetime value, last active. Below that, a 'past conversations' list (last 5), then 'internal notes' (a small section where teammates can leave notes about this customer), then a 'related articles' suggestions list (auto-suggested help docs based on the conversation content).""",
369
+
370
+ # 11
371
+ """Homepage for a Series A fintech startup called Stack that helps freelancers handle quarterly taxes. Audience: US-based freelancers earning $40k–$200k who currently use a CPA or do nothing. Hero: clear value prop ('Quarterly taxes, handled'), sub-line, two CTAs ('Estimate my taxes' and 'How it works'), and a hero visual of a phone showing the app's tax estimate screen. Below the hero, a logo bar with publications that have covered them (TechCrunch, NYT, Wired, etc.). Then a 'How it works' section with three steps (connect your accounts, we calculate every quarter, file with one tap), each with an icon and short description. Then a tax savings calculator widget where users input their annual freelance income and state, and it shows estimated savings and time saved vs. doing it themselves. Then a testimonials section with three quotes from named freelancers (designer, developer, copywriter) including profession and city. Then a section addressing the common objection 'why not just use a CPA?' with a side-by-side comparison. Then a security and trust section (bank-level encryption, SOC2, never sells data). Then pricing β€” flat $25/month, no upsells. End with an FAQ (six questions covering states supported, what if I'm late, integration with QuickBooks, refunds, multi-state freelancers, accuracy guarantee) and a footer with newsletter signup.""",
372
+
373
+ # 12
374
+ """Comparison page on a SaaS marketing site: 'Linear vs Jira.' The audience is a team currently on Jira considering switching. Hero: a clear headline ('A modern alternative to Jira'), a one-line sub, and two CTAs (start free, book a migration call). Below the hero, a feature-by-feature comparison table with about 20 rows grouped into sections (Speed & UX, Workflow, Integrations, Pricing, Support), with check/x marks and short clarifying notes per cell. Below the table, three side-by-side highlight cards covering the biggest differences (10x faster UI, opinionated workflow, transparent pricing). Below that, a pricing comparison block with the equivalent plan from each side by side, showing per-seat cost. Below that, a customer story section: a quote from someone who migrated, with their photo, name, title, and company, plus three stat numbers from their experience (e.g., '4 hours of meetings saved per week', '70% adoption in week 1', etc.). Below that, a 'Migration is easy' section with a three-step process (export from Jira, run our migrator, go live in a day) and a CTA to talk to the migration team. End with FAQ covering data import, custom fields, permissions parity, and pricing edge cases.""",
375
+
376
+ # 13
377
+ """Profile page on a freelance marketplace called Make. The profile belongs to a senior brand designer. The page is what a hiring client sees when they land on this designer's page. Top: a wide cover photo placeholder, with the avatar overlapping the bottom edge. Below, the name, headline ('Brand designer for early-stage SaaS'), location, hourly rate, response time, availability badge, and two prominent CTAs ('Hire' and 'Message'). Below that, a row of trust signals (top-rated badge, identity verified, total earnings, 5-year tenure on the platform). Then a section with the designer's bio (two paragraphs). Then a skills section with tag chips (brand strategy, logo design, visual identity, design systems, Figma, illustration). Then a portfolio grid of nine project thumbnails with title and category overlay on hover. Then a services offered list β€” three packaged services with title, price starting at, delivery time, and a 'select' button. Then a reviews section: average rating, total reviews, a star breakdown, and four sample reviews with client name, project title, rating, and quote. Then a 'work history' section listing past completed projects with client and date. The Hire CTA should stick to the right side of the viewport on scroll.""",
378
+
379
+ # 14
380
+ """Search results page for a job board focused on remote engineering roles, called Async. The user has just searched 'senior backend engineer.' The page should have a top filter bar with the search query and quick filters (Remote-only, Full-time, Salary > $X, Posted in last 7 days). Left sidebar: more detailed filters β€” location/timezone (with a multi-select for regions like Americas-only, Europe-only, Anywhere), salary range slider, role seniority checkboxes, company size, tech stack tags (Go, Python, Rust, TypeScript, Postgres, AWS, etc.), and equity/benefits toggles. Main area: at the top, results count and a sort dropdown (relevance, newest, highest salary). Below, a list of job result cards β€” each showing company logo, role title, company name, salary range, location/timezone, posted date, a brief role description, three tech stack tags, and a 'Save' bookmark icon. About 10 results visible, with pagination at the bottom. To the right of the result list, a sticky 'Save this search' card prompting the user to get email alerts for new matches. Below the result list, a 'Companies hiring now' section with logos linking to each company's full job list.""",
381
+
382
+ # 15
383
+ """Documentation homepage for an open-source ML library called Tensorgrove. Audience: ML engineers and researchers evaluating or starting with the library. The page should feel technical, fast, and not over-designed. Top header with library name, version selector (showing current stable version), and links (Docs, API, Tutorials, Blog, GitHub stars badge). Hero: short tagline ('Composable tensor operations for research'), a one-line description, and two prominent buttons ('Quickstart' and 'API reference'). Below the hero, a quickstart code block showing a 10-line install + first-example snippet with syntax highlighting and a copy button. Below that, a four-tile grid linking to main doc sections: Getting Started, Core Concepts, Tutorials, API Reference β€” each with an icon, short description, and arrow. Below that, a 'What you can build' section with three example projects (an image classifier, a transformer from scratch, an RL agent), each with a thumbnail placeholder, brief description, and a 'see code' link. Below that, a 'Used by' section showing logos of universities and labs. Below that, a community card linking to Discord, GitHub Discussions, and the bi-weekly office hours, with member count. End with a footer that has docs links, GitHub, license info, and a 'star us on GitHub' button.""",
384
+
385
+ # 16
386
+ """Booking flow page for a hair salon's website. This is step 2 of 4: pick your stylist and time. Top of the page: a 4-step progress indicator (Service > Stylist & Time > Your Info > Confirm), with step 2 active. Below the progress indicator, a summary strip showing what was selected in step 1 (service: 'Cut & Color', estimated duration: 2h 15m, price range: $180–$240), with a small 'edit' link. Main content split into two columns. Left column (wider): the stylist + time picker. At the top, four stylist cards in a row, each showing the stylist's photo, name, specialty tag, average rating, and price tier indicator. One can be clicked to filter the calendar below. Below the stylist cards, a date picker showing the next 14 days as a horizontal scrollable strip, with each day showing day name, date, and an availability dot. Below the date strip, a time slot grid for the selected day β€” slots in 15-minute increments from 9am to 7pm, with available slots clickable and unavailable greyed out. Right column (narrower): a sticky booking summary card showing service, selected stylist (or 'Any available'), selected date and time (or 'Pick a time'), price range, and a 'Continue to your info' button (disabled until a slot is chosen). Below the summary card, a 'Cancellation policy' note. Footer with help link and salon contact info.""",
387
+
388
+ # 17
389
+ """Help center landing page for a consumer finance app called Pocket. Audience: existing users with questions, ranging from non-technical to power users. Top: a centered hero with the help center title, a short reassurance line ('We're here to help β€” find an answer fast or talk to us'), and a large search bar with placeholder text ('Search articles, e.g., "transfer limits"'). Below the hero, a 'Popular topics' section as a 6-tile grid, each tile with an icon, a topic title (Account & login, Sending money, Cards, Limits & verification, Security, Billing & fees), and the article count in that category. Below that, a 'Top articles' list β€” 8 article links with titles and a 1-line snippet. Below that, a 'Still need help?' section with three contact options as side-by-side cards: chat with support (with current wait time), email (with response time SLA), and schedule a call (with available slots indicator). Each option should make clear what the user gets. Below that, a status indicator section linking to the public status page if there's any ongoing issue. Footer with links to community forum, security page, accessibility statement, and changelog.""",
390
+
391
+ # 18
392
+ """Changelog page for a productivity app called Nimbus. Audience: existing users who want to see what's new and developers integrating with the API. Top: page title, short intro, and three controls β€” a category filter (All, New, Improved, Fixed, Security, API), a search bar, and subscribe options (RSS, email, webhook). Below the controls, the changelog feed grouped by month, with the current month at the top. Each month is a section with a month header. Within each month, individual entries listed reverse-chronologically. Each entry has: a date, a version tag (v3.4.1 style) when applicable, a category pill, a heading, a short paragraph (2–4 sentences) describing the change, optionally an inline screenshot or short video placeholder, and a 'See in app' deep-link button when relevant. Some entries are big releases with longer write-ups including bullet sub-changes and a 'Read full post' link. Sidebar on the right: a 'jump to month' navigation, a 'biggest changes this quarter' highlight box with three pinned entries, and a card promoting the public roadmap. The page should feel like a real product log, not marketing fluff.""",
393
+
394
+ # 19
395
+ """Real estate listing detail page for a 3-bedroom house in Brooklyn. Audience: a buyer or renter browsing on desktop. Top: a breadcrumb (Home > Brooklyn > Park Slope > 312 7th Ave). Below, a full-width photo carousel placeholder showing the listing image, with arrows, image counter (1/24), and a 'view all photos' button that opens a gallery, plus tabs to switch between Photos / Floor Plan / Street View / Video Tour. Below the carousel, a two-column layout. Left column (wider): the listing details. Address, price (with 'price reduced' badge if applicable), price-per-sqft, beds, baths, square footage, and a short description paragraph. Then a 'Highlights' bullet list (renovated kitchen, private backyard, in-unit laundry, central AC, etc.). Then a 'Property details' table (year built, lot size, parking, HOA, taxes, MLS number). Then a 'Neighborhood' section with a small map placeholder, walk/transit/bike scores, and nearby points of interest (schools with ratings, parks, subway lines). Then a floor plan image with a square-footage breakdown. Then a 'Price history' table showing prior list events with date, event, and price. Right column (narrower, sticky): an agent contact card with the agent's photo, name, brokerage, phone, and a contact form (name, email, phone, message, 'I'd like to tour') with three CTA buttons ('Schedule a tour', 'Request info', 'Make an offer'). Below the agent card, a mortgage calculator widget (down payment, loan term, interest rate inputs, estimated monthly payment output). Below the two columns, a 'Similar homes nearby' carousel of 6 listing cards.""",
396
+
397
+ # 20
398
+ """Analytics overview dashboard for an e-commerce store owner running a Shopify-style storefront. Audience: a small business owner checking performance daily. Top bar: store name on the left, a global date-range picker in the center (with presets: Today, Yesterday, Last 7 days, Last 30 days, This month, Custom), and a 'Compare to' toggle (vs previous period) on the right. Below, four KPI cards across a row showing: Total revenue (with delta vs previous period), Orders, Conversion rate, Average order value β€” each with a sparkline of the chosen period. Below that, the main revenue chart β€” a large line chart with revenue over time (toggleable to gross sales, net sales, refunds), with a secondary axis option for orders. Below the revenue chart, a three-column row: a 'Top products' table (product name with thumbnail, units sold, revenue, % of total) showing the top 5 with a 'View all' link; a 'Traffic sources' donut chart with a legend (direct, organic, social, paid, email, referral) and percentages; and a 'Conversion funnel' visualization showing visitors β†’ product views β†’ add to cart β†’ checkout started β†’ purchase, with drop-off rates between each step. Below those three, a 'Recent orders' table (order number, customer name, items count, total, status, date), and to the right of that, an 'Inventory alerts' panel showing products with low stock. The whole dashboard should feel scannable in 30 seconds but rewarding when you dig in."""
399
+ ]
400
+ # ──────────────────────────────────────────────────────────────────────────────
401
+ # Prompt normalizer
402
+ # ──────────────────────────────────────────────────────────────────────────────
403
+ _anthropic_client = None
404
+
405
+
406
+ def _get_anthropic_client():
407
+ global _anthropic_client
408
+ if _anthropic_client is not None:
409
+ return _anthropic_client
410
+ if not _ANTHROPIC_AVAILABLE:
411
+ raise RuntimeError("anthropic SDK not installed. Run: pip install anthropic")
412
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
413
+ if not api_key:
414
+ raise RuntimeError("ANTHROPIC_API_KEY environment variable not set")
415
+ _anthropic_client = Anthropic(api_key=api_key)
416
+ return _anthropic_client
417
+
418
+
419
+ def normalize_prompt(raw_prompt: str) -> str:
420
+ """Normalize any user prompt into the structured format using the API.
421
+ Routes to the dashboard normalizer if the prompt looks dashboard-shaped,
422
+ otherwise uses the landing-page normalizer.
423
+ Falls back to the raw prompt if normalization fails."""
424
+ if not NORMALIZE_PROMPTS:
425
+ return raw_prompt
426
+ is_dashboard = is_dashboard_prompt(raw_prompt)
427
+ system_prompt = DASHBOARD_NORMALIZER_SYSTEM_PROMPT if is_dashboard else NORMALIZER_SYSTEM_PROMPT
428
+ print(f"[normalize_prompt] route β†’ {'DASHBOARD' if is_dashboard else 'LANDING-PAGE'} normalizer")
429
+ try:
430
+ client = _get_anthropic_client()
431
+ response = client.messages.create(
432
+ model=NORMALIZER_MODEL,
433
+ max_tokens=NORMALIZER_MAX_TOKENS,
434
+ temperature=NORMALIZER_TEMPERATURE,
435
+ system=system_prompt,
436
+ messages=[{"role": "user", "content": raw_prompt}],
437
+ )
438
+ normalized = response.content[0].text.strip()
439
+ # Strip any wrapping quotes the model might add
440
+ if normalized.startswith('"""') and normalized.endswith('"""'):
441
+ normalized = normalized[3:-3].strip()
442
+ elif normalized.startswith('"') and normalized.endswith('"'):
443
+ normalized = normalized[1:-1].strip()
444
+ elif normalized.startswith("'") and normalized.endswith("'"):
445
+ normalized = normalized[1:-1].strip()
446
+ return normalized
447
+ except Exception as e:
448
+ print(f"[normalize_prompt] WARNING: falling back to raw prompt β€” {e}")
449
+ return raw_prompt
450
+
451
+
452
+ # ──────────────────────────────────────────────────────────────────────────────
453
+ # Model loading
454
+ # ──────────────────────────────────────────────────────────────────────────────
455
+ # Set USE_UNSLOTH = True to load with Unsloth's FastLanguageModel (custom Triton
456
+ # kernels for Qwen2 inference, ~1.5–2x faster than vanilla transformers).
457
+ # Set False to fall back to plain HuggingFace AutoModelForCausalLM.
458
+ USE_UNSLOTH = True
459
+
460
+
461
+ def load_model_and_tokenizer():
462
+ print(f"Loading merged model from: {MODEL_PATH}")
463
+
464
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
465
+ model = AutoModelForCausalLM.from_pretrained(
466
+ MODEL_PATH,
467
+ torch_dtype=torch.bfloat16,
468
+ trust_remote_code=True,
469
+ low_cpu_mem_usage=True,
470
+ ).to("cuda:0")
471
+ model.eval()
472
+
473
+ print(f"Attention implementation: {getattr(model.config, '_attn_implementation', 'unknown')}")
474
+
475
+ tokenizer.eos_token = "<|im_end|>"
476
+ tokenizer.eos_token_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
477
+ tokenizer.pad_token = tokenizer.eos_token
478
+ tokenizer.pad_token_id = tokenizer.eos_token_id
479
+
480
+ print("Model loaded successfully")
481
+ return model, tokenizer
482
+
483
+
484
+ # ──────────────────────────────────────────────────────────────────────────────
485
+ # Generation
486
+ # ──────────────────────────────────────────────────────────────────────────────
487
+ IMAGE_GUARD = """IMPORTANT: Use <AI-IMAGE class="..." src="descriptive prompt" /> for every image β€” no external URLs, no src="https://...". Give every major section and card a unique id attribute."""
488
+ # with image guard is fine rank 1 for now 0.03 B
489
+ def generate_html(model, tokenizer, prompt_text):
490
+ messages = [
491
+ {"role": "system", "content": SYSTEM_PROMPT},
492
+ {"role": "user", "content": prompt_text},
493
+ ]
494
+
495
+ input_text = tokenizer.apply_chat_template(
496
+ messages,
497
+ tokenize=False,
498
+ add_generation_prompt=True,
499
+ )
500
+
501
+ inputs = tokenizer(
502
+ input_text,
503
+ return_tensors="pt",
504
+ add_special_tokens=False,
505
+ ).to("cuda")
506
+
507
+ streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
508
+
509
+ with torch.no_grad():
510
+
511
+ outputs = model.generate(
512
+ **inputs,
513
+ max_new_tokens=MAX_NEW_TOKENS,
514
+ temperature=TEMPERATURE,
515
+ top_p=TOP_P,
516
+ do_sample=DO_SAMPLE,
517
+ eos_token_id=tokenizer.eos_token_id,
518
+ pad_token_id=tokenizer.pad_token_id,
519
+ streamer=streamer,
520
+ )
521
+
522
+ generated_text = tokenizer.decode(
523
+ outputs[0][inputs.input_ids.shape[1]:],
524
+ skip_special_tokens=True,
525
+ )
526
+ return post_process(generated_text.strip())
527
+
528
+
529
+ def post_process(html: str) -> str:
530
+ """Strip known contamination patterns that slip past the SYSTEM_PROMPT guard."""
531
+ # 0. Detect degenerate SVG loop (same coordinate pair repeated >50 times)
532
+ # This consumes the entire token budget and produces a broken output.
533
+ loop_match = _re.search(r'((?:M|L|C)\d+\.?\d* \d+\.?\d*[ ,]?){50,}', html)
534
+ if loop_match:
535
+ print("[post_process] WARNING: Degenerate SVG path loop detected β€” stripping SVG element")
536
+ html = _re.sub(r'<svg[^>]*>.*?</svg>', '', html, flags=_re.DOTALL | _re.IGNORECASE)
537
+
538
+ # 1. Remove @font-face blocks that reference external (non-local) URLs
539
+ html = _re.sub(
540
+ r'@font-face\s*\{[^}]*src\s*:\s*url\(["\']?https?://[^"\')\s]+["\']?\)[^}]*\}',
541
+ '',
542
+ html,
543
+ flags=_re.DOTALL | _re.IGNORECASE,
544
+ )
545
+ # 2. Replace storage.googleapis.com avatar URLs with pravatar
546
+ def _fix_avatar(m):
547
+ import hashlib
548
+ n = int(hashlib.md5(m.group(0).encode()).hexdigest(), 16) % 70 + 1
549
+ return f'src="https://i.pravatar.cc/150?img={n}"'
550
+ html = _re.sub(
551
+ r'src="https://storage\.googleapis\.com/uxpilot-auth[^"]*"',
552
+ _fix_avatar,
553
+ html,
554
+ )
555
+ # 3. Replace invented brand domain image/video URLs with AI-IMAGE
556
+ def _fix_brand_url(m):
557
+ tag = m.group(0)
558
+ if 'pravatar' in tag or 'cdn.tailwind' in tag or 'cdnjs' in tag or 'fonts.google' in tag:
559
+ return tag
560
+ return '<AI-IMAGE src="placeholder image" />'
561
+ html = _re.sub(
562
+ r'<img[^>]+src="https?://(?!i\.pravatar\.cc)[^"]+\.(jpg|jpeg|png|gif|webp|mp4|mov)"[^>]*/?>',
563
+ _fix_brand_url,
564
+ html,
565
+ flags=_re.IGNORECASE,
566
+ )
567
+ return html
568
+
569
+
570
+ # ──────────────────────────────────────────────────────────────────────────────
571
+ # Dynamic batching inference engine
572
+ # ──────────────────────────────────────────────────────────────────────────────
573
+ # Multiple threads (or an async server) call engine.submit(prompt). A single
574
+ # worker thread collects requests into a batch (up to MAX_BATCH, with a
575
+ # WAIT_MS window for the batch to fill) and runs them in ONE model.generate()
576
+ # call. Per-request output is bit-identical to single-request inference β€”
577
+ # same model, same tokenizer, same sampling params; only difference is
578
+ # left-padding so decoder-only batched generation lines up correctly.
579
+ #
580
+ # The GPU is the serialization point, so one worker thread is correct.
581
+ # Don't add more worker threads β€” they'll just contend on CUDA and slow down.
582
+ # ──────────────────────────────────────────────────────────────────────────────
583
+ import threading
584
+ import queue as _queue
585
+ import time as _time
586
+ from dataclasses import dataclass, field
587
+ from typing import Optional, List, Tuple
588
+
589
+
590
+ @dataclass
591
+ class _BatchRequest:
592
+ prompt: str
593
+ event: threading.Event = field(default_factory=threading.Event)
594
+ result: Optional[str] = None
595
+ error: Optional[Exception] = None
596
+ submitted_at: float = field(default_factory=_time.monotonic)
597
+ started_at: Optional[float] = None
598
+ finished_at: Optional[float] = None
599
+
600
+
601
+ class BatchingInferenceEngine:
602
+ """Dynamic batching wrapper around model.generate()."""
603
+
604
+ def __init__(
605
+ self,
606
+ model,
607
+ tokenizer,
608
+ system_prompt: str,
609
+ max_batch: int = 4,
610
+ wait_ms: int = 50,
611
+ max_new_tokens: int = MAX_NEW_TOKENS,
612
+ temperature: float = TEMPERATURE,
613
+ top_p: float = TOP_P,
614
+ do_sample: bool = DO_SAMPLE,
615
+ ):
616
+ self.model = model
617
+ self.tokenizer = tokenizer
618
+ self.system_prompt = system_prompt
619
+ self.max_batch = max_batch
620
+ self.wait_s = wait_ms / 1000.0
621
+ self.max_new_tokens = max_new_tokens
622
+ self.temperature = temperature
623
+ self.top_p = top_p
624
+ self.do_sample = do_sample
625
+
626
+ # Critical: left-pad for decoder-only batched generation so every row's
627
+ # generation starts at the same index (input_len) and EOS logic works.
628
+ self.tokenizer.padding_side = "left"
629
+
630
+ self._queue: "_queue.Queue[_BatchRequest]" = _queue.Queue()
631
+ self._running = True
632
+ self._worker = threading.Thread(target=self._worker_loop, daemon=True)
633
+ self._worker.start()
634
+
635
+ def submit(self, prompt: str, timeout: Optional[float] = None) -> str:
636
+ """Blocking β€” returns the generated HTML (post-processed). Raises on failure."""
637
+ req = _BatchRequest(prompt=prompt)
638
+ self._queue.put(req)
639
+ if not req.event.wait(timeout=timeout):
640
+ raise TimeoutError("Batch inference timed out")
641
+ if req.error:
642
+ raise req.error
643
+ return req.result
644
+
645
+ def shutdown(self):
646
+ self._running = False
647
+
648
+ def _worker_loop(self):
649
+ while self._running:
650
+ batch = self._collect_batch()
651
+ if not batch:
652
+ continue
653
+ t_start = _time.monotonic()
654
+ for req in batch:
655
+ req.started_at = t_start
656
+ try:
657
+ self._run_batch(batch)
658
+ except Exception as e:
659
+ print(f"[batch_worker] ERROR: {e}")
660
+ for req in batch:
661
+ if not req.event.is_set():
662
+ req.error = e
663
+ req.event.set()
664
+
665
+ def _collect_batch(self) -> List[_BatchRequest]:
666
+ # Block for first request (heartbeat timeout to allow clean shutdown).
667
+ try:
668
+ first = self._queue.get(timeout=1.0)
669
+ except _queue.Empty:
670
+ return []
671
+ batch = [first]
672
+
673
+ # Drain additional requests inside the wait window.
674
+ deadline = _time.monotonic() + self.wait_s
675
+ while len(batch) < self.max_batch:
676
+ remaining = deadline - _time.monotonic()
677
+ if remaining <= 0:
678
+ break
679
+ try:
680
+ req = self._queue.get(timeout=remaining)
681
+ batch.append(req)
682
+ except _queue.Empty:
683
+ break
684
+ return batch
685
+
686
+ def _run_batch(self, batch: List[_BatchRequest]):
687
+ texts = []
688
+ for req in batch:
689
+ messages = [
690
+ {"role": "system", "content": self.system_prompt},
691
+ {"role": "user", "content": req.prompt},
692
+ ]
693
+ text = self.tokenizer.apply_chat_template(
694
+ messages, tokenize=False, add_generation_prompt=True,
695
+ )
696
+ texts.append(text)
697
+
698
+ inputs = self.tokenizer(
699
+ texts,
700
+ padding=True,
701
+ return_tensors="pt",
702
+ add_special_tokens=False,
703
+ ).to("cuda")
704
+
705
+ with torch.no_grad():
706
+ outputs = self.model.generate(
707
+ **inputs,
708
+ max_new_tokens=self.max_new_tokens,
709
+ temperature=self.temperature,
710
+ top_p=self.top_p,
711
+ do_sample=self.do_sample,
712
+ eos_token_id=self.tokenizer.eos_token_id,
713
+ pad_token_id=self.tokenizer.pad_token_id,
714
+ )
715
+
716
+ # Left-padded β†’ all rows share the same input_len prefix.
717
+ input_len = inputs.input_ids.shape[1]
718
+ t_end = _time.monotonic()
719
+
720
+ bsz = len(batch)
721
+ wait_times = [(r.started_at - r.submitted_at) * 1000 for r in batch]
722
+ gen_time_ms = (t_end - batch[0].started_at) * 1000
723
+ print(f"[batch] size={bsz} gen={gen_time_ms:.0f}ms "
724
+ f"wait=[{min(wait_times):.0f}-{max(wait_times):.0f}ms]")
725
+
726
+ for i, req in enumerate(batch):
727
+ generated = outputs[i][input_len:]
728
+ text = self.tokenizer.decode(generated, skip_special_tokens=True)
729
+ req.result = post_process(text.strip())
730
+ req.finished_at = t_end
731
+ req.event.set()
732
+
733
+
734
+ # ──────────────────────────────────────────────────────────────────────────────
735
+ # Parity & throughput tests
736
+ # ──────────────────────────────────────────────────────────────────────────────
737
+ def run_parity_check(model, tokenizer, test_prompt: str):
738
+ """Confirm batch-of-1 output matches single-request output exactly."""
739
+ print("\n[parity] single-request pass...")
740
+ t0 = _time.monotonic()
741
+ single = generate_html(model, tokenizer, test_prompt)
742
+ t1 = _time.monotonic()
743
+
744
+ engine = BatchingInferenceEngine(
745
+ model, tokenizer, SYSTEM_PROMPT,
746
+ max_batch=1, wait_ms=10,
747
+ )
748
+ print("[parity] batched (batch-of-1) pass...")
749
+ t2 = _time.monotonic()
750
+ batched = engine.submit(test_prompt)
751
+ t3 = _time.monotonic()
752
+ engine.shutdown()
753
+
754
+ print(f"[parity] single={t1-t0:.1f}s batched-of-1={t3-t2:.1f}s")
755
+ if single == batched:
756
+ print("[parity] OK β€” outputs are bit-identical.")
757
+ return True
758
+ else:
759
+ import difflib
760
+ diff = list(difflib.unified_diff(
761
+ single.splitlines(), batched.splitlines(),
762
+ fromfile="single", tofile="batched", lineterm="", n=2,
763
+ ))
764
+ print(f"[parity] MISMATCH β€” first 30 diff lines:")
765
+ for line in diff[:30]:
766
+ print(line)
767
+ return False
768
+
769
+
770
+ def run_throughput_test(
771
+ model, tokenizer, prompts: List[str],
772
+ max_batch: int = 4, wait_ms: int = 50,
773
+ ) -> Tuple[List[str], float]:
774
+ """Fire all prompts concurrently through one engine; measure wall time."""
775
+ engine = BatchingInferenceEngine(
776
+ model, tokenizer, SYSTEM_PROMPT,
777
+ max_batch=max_batch, wait_ms=wait_ms,
778
+ )
779
+
780
+ results: List[Optional[str]] = [None] * len(prompts)
781
+ threads: List[threading.Thread] = []
782
+
783
+ def worker(i, p):
784
+ try:
785
+ results[i] = engine.submit(p)
786
+ except Exception as e:
787
+ results[i] = f"ERROR: {e}"
788
+
789
+ t0 = _time.monotonic()
790
+ for i, p in enumerate(prompts):
791
+ th = threading.Thread(target=worker, args=(i, p))
792
+ th.start()
793
+ threads.append(th)
794
+ for th in threads:
795
+ th.join()
796
+ wall = _time.monotonic() - t0
797
+
798
+ print(f"\n[throughput] {len(prompts)} prompts in {wall:.1f}s wall "
799
+ f"β†’ {wall/len(prompts):.1f}s per prompt effective "
800
+ f"(max_batch={max_batch}, wait_ms={wait_ms})")
801
+ engine.shutdown()
802
+ return results, wall
803
+
804
+
805
+ # ──────────────────────────────────────────────────────────────────────────────
806
+ # Main
807
+ # ──────────────────────────────────────────────────────────────────────────────
808
+ def main():
809
+ model, tokenizer = load_model_and_tokenizer()
810
+
811
+ output_dir = Path(OUTPUT_FOLDER)
812
+ output_dir.mkdir(parents=True, exist_ok=True)
813
+
814
+ for i, prompt in enumerate(PROMPTS, 1):
815
+ print(f"\n{'='*80}")
816
+ print(f"Generating {i}/{len(PROMPTS)}")
817
+ print(f"Raw prompt: {prompt[:120]}..." if len(prompt) > 120 else f"Raw prompt: {prompt}")
818
+
819
+ # Normalize the prompt via Haiku before inference
820
+ normalized_prompt = normalize_prompt(prompt)
821
+ if normalized_prompt != prompt:
822
+ preview = normalized_prompt[:120] + "..." if len(normalized_prompt) > 120 else normalized_prompt
823
+ print(f"Normalized: {preview}")
824
+ print(f"{'='*80}")
825
+
826
+ # Save the normalized prompt alongside the HTML for traceability
827
+ norm_path = output_dir / f"test_prompt_{i:03d}_normalized.txt"
828
+ with open(norm_path, "w", encoding="utf-8") as f:
829
+ f.write(normalized_prompt)
830
+
831
+ try:
832
+ _t0 = _time.perf_counter()
833
+ html = generate_html(model, tokenizer, normalized_prompt)
834
+ _dt = _time.perf_counter() - _t0
835
+ print(f"[timing] prompt {i}/{len(PROMPTS)} generated in {_dt:.1f}s")
836
+
837
+ path = output_dir / f"test_prompt_{i:03d}.html"
838
+ with open(path, "w", encoding="utf-8") as f:
839
+ f.write(html)
840
+
841
+ print(f"Saved -> {path}")
842
+
843
+ except Exception as e:
844
+ print(f"Error on prompt {i}: {e}")
845
+ error_path = output_dir / f"prompt_{i:03d}_ERROR.txt"
846
+ with open(error_path, "w", encoding="utf-8") as f:
847
+ f.write(f"Error: {str(e)}\nPrompt: {prompt}")
848
+ print(f"Error saved -> {error_path}")
849
+
850
+ print(f"\nDone. All files -> {output_dir}")
851
+
852
+
853
+ if __name__ == "__main__":
854
+ import sys
855
+
856
+ mode = sys.argv[1] if len(sys.argv) > 1 else "main"
857
+
858
+ if mode == "parity":
859
+ # Verify batched output == single-request output.
860
+ # Usage: python inference_edited_chat_opt.py parity
861
+ model, tokenizer = load_model_and_tokenizer()
862
+ test_prompt = PROMPTS[0] if PROMPTS else "simple landing page for a coffee shop"
863
+ run_parity_check(model, tokenizer, test_prompt)
864
+
865
+ elif mode == "throughput":
866
+ # Measure throughput with concurrent submissions.
867
+ # Usage: python inference_edited_chat_opt.py throughput [max_batch] [wait_ms] [n_prompts]
868
+ max_batch = int(sys.argv[2]) if len(sys.argv) > 2 else 4
869
+ wait_ms = int(sys.argv[3]) if len(sys.argv) > 3 else 50
870
+ n = int(sys.argv[4]) if len(sys.argv) > 4 else min(8, len(PROMPTS))
871
+ model, tokenizer = load_model_and_tokenizer()
872
+ prompts_to_run = PROMPTS[:n] if len(PROMPTS) >= n else (PROMPTS * ((n // len(PROMPTS)) + 1))[:n]
873
+ # Normalize upfront so the test measures inference, not normalizer latency.
874
+ prompts_to_run = [normalize_prompt(p) for p in prompts_to_run]
875
+ results, wall = run_throughput_test(
876
+ model, tokenizer, prompts_to_run,
877
+ max_batch=max_batch, wait_ms=wait_ms,
878
+ )
879
+ output_dir = Path(OUTPUT_FOLDER) / f"throughput_b{max_batch}_w{wait_ms}"
880
+ output_dir.mkdir(parents=True, exist_ok=True)
881
+ for i, html in enumerate(results, 1):
882
+ (output_dir / f"batch_{i:03d}.html").write_text(html or "", encoding="utf-8")
883
+ print(f"[throughput] wrote {len(results)} files -> {output_dir}")
884
+
885
+ else:
886
+ main()
inference_edited_chat_opt.py CHANGED
@@ -179,7 +179,7 @@ PRESERVE-DETAIL RULES (critical for detailed inputs):
179
  - If the user describes a section in 80 words of detail, your prose version of that section keeps every detail and is roughly the same length. Don't summarize.
180
  - If the user describes a nested element ("inside this container, there's a frame, inside the frame is a window mockup"), preserve all nesting layers in your prose.
181
 
182
- AESTHETIC THINKING (when the user gave none): If the user supplied structure but no aesthetic, imagine the page deeply first as a design director would before writing. Who is the audience? What does this product feel like at its most honest? What is the ONE visual move that would make this page feel inevitable? Use your own taste. Avoid lazy defaults like indigo accent + Inter/DM Sans + flat design.
183
 
184
  STRUCTURE (merge all six blocks into one continuous paragraph):
185
 
 
179
  - If the user describes a section in 80 words of detail, your prose version of that section keeps every detail and is roughly the same length. Don't summarize.
180
  - If the user describes a nested element ("inside this container, there's a frame, inside the frame is a window mockup"), preserve all nesting layers in your prose.
181
 
182
+ AESTHETIC THINKING (when the user gave none): If the user supplied structure but no aesthetic, imagine the page deeply first as a design director would before writing, produce something good, like an award winning website, something breath taking.
183
 
184
  STRUCTURE (merge all six blocks into one continuous paragraph):
185
 
pod_api.py CHANGED
@@ -1,9 +1,7 @@
1
  """
2
- pod_api.py β€” RunPod-side FastAPI server that delegates generation to local
3
- trtllm-serve while keeping your existing api.py contract (job pattern,
4
- Pydantic validation, normalizer routing, auto-save, error handling).
5
 
6
- Architecture in this pod:
7
 
8
  Client ─POST /v1/jobs──▢ pod_api.py (this file, port 5000)
9
  β”‚
@@ -11,33 +9,15 @@ Architecture in this pod:
11
  β–Ό
12
  ThreadPoolExecutor
13
  β”‚
14
- β”‚ 1. normalize via Anthropic API
15
  β”‚ 2. POST to trtllm-serve
16
  β–Ό
17
- trtllm-serve (port 8000, local) ──▢ model on GPU
18
 
19
- Why this layout:
20
- - Your reliability layer (job pattern, validation, GC, auto-save) stays.
21
- - TRT-LLM does the actual generation β€” 2.85Γ— faster than transformers, and
22
- ready to add NGram speculative on top via the existing spec_config.yaml.
23
- - Anthropic-based normalizer + dashboard routing keep working unchanged
24
- because we import your existing inference_edited_chat_opt module.
25
-
26
- Setup:
27
- pip install fastapi "uvicorn[standard]" pydantic requests anthropic
28
- export ANTHROPIC_API_KEY=...
29
-
30
- # Make sure trtllm-serve is already running on :8000.
31
- # Then start this:
32
  uvicorn pod_api:app --host 0.0.0.0 --port 5000 --workers 1
33
-
34
- Endpoints (same shape as your old api.py):
35
- GET /v1/healthz
36
- GET /v1/readyz
37
- POST /v1/jobs -> 202 {"job_id": ...}
38
- GET /v1/jobs/{job_id} -> status + html when done
39
- GET /v1/jobs -> list recent jobs
40
- POST /v1/generate -> synchronous variant
41
  """
42
  from __future__ import annotations
43
 
@@ -52,7 +32,7 @@ from concurrent.futures import ThreadPoolExecutor
52
  from contextlib import asynccontextmanager
53
  from dataclasses import dataclass, field
54
  from pathlib import Path
55
- from typing import Any, Literal, Optional
56
 
57
  import requests
58
  from fastapi import FastAPI, HTTPException
@@ -60,11 +40,10 @@ from fastapi.middleware.cors import CORSMiddleware
60
  from fastapi.responses import JSONResponse
61
  from pydantic import BaseModel, Field, field_validator
62
 
63
- # Make /workspace importable so we can pull SYSTEM_PROMPT, normalizers,
64
- # is_dashboard_prompt, and post_process from your existing module.
65
  sys.path.insert(0, "/workspace")
66
  import inference_edited_chat_opt as inf
67
 
 
68
  # ──────────────────────────────────────────────────────────────────────────────
69
  # Config
70
  # ──────────────────────────────────────────────────────────────────────────────
@@ -85,8 +64,143 @@ logging.basicConfig(
85
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
86
  )
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # ──────────────────────────────────────────────────────────────────────────────
89
- # Job state (same shape as your old api.py)
90
  # ──────────────────────────────────────────────────────────────────────────────
91
  JobStatus = Literal["queued", "running", "done", "error"]
92
 
@@ -178,7 +292,6 @@ def _inflight_count() -> int:
178
  # Generation β€” call into local trtllm-serve over HTTP
179
  # ──────────────────────────────────────────────────────────────────────────────
180
  def _trtllm_generate(prompt_text: str) -> str:
181
- """Send a chat-completion request to trtllm-serve and return the HTML."""
182
  body = {
183
  "model": TRTLLM_MODEL,
184
  "messages": [
@@ -211,8 +324,6 @@ def _run_job(job: Job) -> None:
211
  logger.info("job %s started", job.id)
212
 
213
  try:
214
- # Step 1 β€” normalize via Anthropic (uses your existing normalizers,
215
- # routed by is_dashboard_prompt for landing-page vs dashboard).
216
  try:
217
  normalized = inf.normalize_prompt(job.raw_prompt)
218
  except Exception as e:
@@ -225,10 +336,8 @@ def _run_job(job: Job) -> None:
225
  normalized = job.raw_prompt
226
  job.normalized_prompt = normalized
227
 
228
- # Step 2 β€” generate via trtllm-serve (local HTTP, port 8000)
229
  raw_html = _trtllm_generate(job.normalized_prompt)
230
 
231
- # Step 3 β€” apply your existing post-processing
232
  html = inf.post_process(raw_html)
233
  if not html.strip():
234
  raise RuntimeError("post_process returned empty output")
@@ -240,7 +349,6 @@ def _run_job(job: Job) -> None:
240
  job.id, time.time() - job.started_at, len(html),
241
  )
242
 
243
- # Auto-save to disk so results survive in-memory GC.
244
  if OUTPUT_DIR is not None:
245
  try:
246
  OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
@@ -290,7 +398,6 @@ def _run_job(job: Job) -> None:
290
  async def lifespan(_: FastAPI):
291
  global _executor
292
 
293
- # Probe trtllm-serve once on startup so we fail fast if it's not running.
294
  try:
295
  r = requests.get(f"{TRTLLM_BASE_URL}/v1/models", timeout=10)
296
  r.raise_for_status()
@@ -321,7 +428,7 @@ async def lifespan(_: FastAPI):
321
  _executor.shutdown(wait=False, cancel_futures=True)
322
 
323
 
324
- app = FastAPI(title="HTML Generation API (TRT-LLM backed)", version="2.0.0", lifespan=lifespan)
325
 
326
  app.add_middleware(
327
  CORSMiddleware,
@@ -412,7 +519,6 @@ def get_job(job_id: str):
412
  job = _get_job(job_id)
413
  if job is not None:
414
  return job.to_response()
415
- # Fall back to disk if the job was GC'd from memory.
416
  if OUTPUT_DIR is not None:
417
  html_path = OUTPUT_DIR / f"{job_id}.html"
418
  meta_path = OUTPUT_DIR / f"{job_id}.json"
 
1
  """
2
+ pod_api.py β€” RunPod-side FastAPI server with structured-output normalizer.
 
 
3
 
4
+ Architecture:
5
 
6
  Client ─POST /v1/jobs──▢ pod_api.py (this file, port 5000)
7
  β”‚
 
9
  β–Ό
10
  ThreadPoolExecutor
11
  β”‚
12
+ β”‚ 1. structured-output normalize via Gemini
13
  β”‚ 2. POST to trtllm-serve
14
  β–Ό
15
+ trtllm-serve (port 8000) ──▢ model on GPU
16
 
17
+ Run:
18
+ pip install fastapi "uvicorn[standard]" pydantic requests google-genai
19
+ export GEMINI_API_KEY=...
 
 
 
 
 
 
 
 
 
 
20
  uvicorn pod_api:app --host 0.0.0.0 --port 5000 --workers 1
 
 
 
 
 
 
 
 
21
  """
22
  from __future__ import annotations
23
 
 
32
  from contextlib import asynccontextmanager
33
  from dataclasses import dataclass, field
34
  from pathlib import Path
35
+ from typing import Any, List, Literal, Optional
36
 
37
  import requests
38
  from fastapi import FastAPI, HTTPException
 
40
  from fastapi.responses import JSONResponse
41
  from pydantic import BaseModel, Field, field_validator
42
 
 
 
43
  sys.path.insert(0, "/workspace")
44
  import inference_edited_chat_opt as inf
45
 
46
+
47
  # ──────────────────────────────────────────────────────────────────────────────
48
  # Config
49
  # ──────────────────────────────────────────────────────────────────────────────
 
64
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
65
  )
66
 
67
+
68
+ # ──────────────────────────────────────────────────────────────────────────────
69
+ # Structured-output normalizer (Pydantic schema β†’ JSON β†’ assembled prompt)
70
+ # ──────────────────────────────────────────────────────────────────────────────
71
+ class _Colors(BaseModel):
72
+ base_hex: str = Field(..., description="Page background hex like #F4EFE2")
73
+ text_hex: str = Field(..., description="Primary text hex like #1A1814")
74
+ muted_hex: str = Field(..., description="Muted secondary text hex")
75
+ surface_hex: str = Field(..., description="Card/surface background hex")
76
+ border_hex: str = Field(..., description="Hairline border hex")
77
+ accent_hex: str = Field(..., description="Single primary accent hex")
78
+ accent_role: str = Field(..., description="Where accent is used")
79
+ success_hex: str = Field(..., description="Success state hex")
80
+ warning_hex: str = Field(..., description="Warning state hex")
81
+ danger_hex: str = Field(..., description="Danger state hex")
82
+
83
+
84
+ class _Typography(BaseModel):
85
+ display_family: str = Field(..., description="Real Google Font name like Fraunces, Tiempos, Geist. NEVER serif or sans-serif.")
86
+ display_weight: str = Field(..., description="Weight range like semibold-to-extrabold")
87
+ body_family: str = Field(..., description="Real Google Font name. NEVER serif or sans-serif.")
88
+ body_weight: str = Field(..., description="Weight range like regular-to-medium")
89
+ mono_family: str = Field(default="", description="Optional mono family for tabular only, or empty string")
90
+
91
+
92
+ class _ClosingRules(BaseModel):
93
+ gradients: str = Field(..., description="Gradient rule")
94
+ shadows: str = Field(..., description="Shadow rule")
95
+ corners: str = Field(..., description="Corner radius rule")
96
+
97
+
98
+ class _Section(BaseModel):
99
+ description: str = Field(..., description="One paragraph describing this section's layout, content, specific copy. Use frame-language and named hex colors.")
100
+
101
+
102
+ class _NormalizedSpec(BaseModel):
103
+ opening: str = Field(..., description="Opening clause: Design me a [type] for [context] - audience X, goal Y")
104
+ register_commitment: str = Field(..., description="One sentence committing to the visual register with hex codes, named fonts, and motifs")
105
+ distinctive_flourish: str = Field(..., description="One sentence about a single standout interactive or visual behavior")
106
+ sections: List[_Section] = Field(..., min_length=8, max_length=14, description="8-14 sections in DOM order")
107
+ colors: _Colors
108
+ typography: _Typography
109
+ closing: _ClosingRules
110
+
111
+
112
+ def _assemble(spec: _NormalizedSpec) -> str:
113
+ parts = [spec.opening.strip(), spec.register_commitment.strip(), spec.distinctive_flourish.strip()]
114
+ connectives = ["Start with", "Then", "Flow into", "Follow with", "Then", "Then", "Follow with", "Then", "Follow with", "Then", "Follow with", "Then", "Then", "Close with"]
115
+ starters = {c.split()[0].lower() for c in connectives + ["close"]}
116
+ for i, s in enumerate(spec.sections):
117
+ prefix = connectives[i] if i < len(connectives) else "Then"
118
+ desc = s.description.strip()
119
+ first = desc.split(" ", 1)[0].lower() if desc else ""
120
+ if first in starters or not desc:
121
+ parts.append(desc)
122
+ else:
123
+ parts.append(prefix + " " + (desc[0].lower() + desc[1:] if desc[0].isupper() else desc))
124
+
125
+ c = spec.colors
126
+ parts.append(
127
+ "Use " + c.base_hex + " as the base with " + c.text_hex + " primary text, " +
128
+ c.muted_hex + " muted copy, " + c.surface_hex + " for card surfaces, " +
129
+ c.border_hex + " for hairlines, and " + c.accent_hex + " as the primary accent for " + c.accent_role + ", " +
130
+ "with a state palette of " + c.success_hex + " success, " + c.warning_hex + " warning, and " + c.danger_hex + " danger."
131
+ )
132
+
133
+ t = spec.typography
134
+ typo = t.display_family + " " + t.display_weight + " for display and headings, paired with " + t.body_family + " " + t.body_weight + " for body"
135
+ if t.mono_family.strip():
136
+ typo += ", plus " + t.mono_family + " used only for tabular figures, IDs, and timestamps - two type families plus a single mono used only for tabular contexts."
137
+ else:
138
+ typo += " - exactly two type families across the entire page, no third family anywhere."
139
+ parts.append(typo)
140
+
141
+ cr = spec.closing
142
+ parts.append(
143
+ cr.gradients + ", " + cr.shadows + ", " + cr.corners + ". " +
144
+ "Icons via Font Awesome only - never inline SVG - never hidden body overflow."
145
+ )
146
+
147
+ return " ".join(parts)
148
+
149
+
150
+ SCHEMA_DIRECTIVE = (
151
+ "\n\nIMPORTANT OUTPUT FORMAT: Output as JSON matching the provided schema. "
152
+ "Every field is mandatory and non-empty. All hex codes must be valid 6-digit hex like #1A1814 - never named colors. "
153
+ "Font families must be real Google Fonts (Fraunces, Inter, Geist, Space Grotesk, Tiempos, Recoleta, Outfit, Plus Jakarta Sans, IBM Plex Mono, JetBrains Mono, etc.) - NEVER use the placeholder serif or sans-serif alone. "
154
+ "Sections array must have between 8 and 14 entries, each describing one DOM-order region with concrete layout, content, and specific copy."
155
+ )
156
+
157
+
158
+ def _normalize_via_gemini(raw_prompt: str) -> str:
159
+ if not getattr(inf, "NORMALIZE_PROMPTS", True):
160
+ return raw_prompt
161
+
162
+ is_dashboard = inf.is_dashboard_prompt(raw_prompt)
163
+ system_prompt = inf.DASHBOARD_NORMALIZER_SYSTEM_PROMPT if is_dashboard else inf.NORMALIZER_SYSTEM_PROMPT
164
+
165
+ try:
166
+ from google import genai
167
+ from google.genai import types
168
+
169
+ client = genai.Client()
170
+
171
+ r = client.models.generate_content(
172
+ model="gemini-3-flash-preview",
173
+ contents=raw_prompt,
174
+ config=types.GenerateContentConfig(
175
+ system_instruction=system_prompt + SCHEMA_DIRECTIVE,
176
+ temperature=0.6,
177
+ max_output_tokens=8192,
178
+ thinking_config=types.ThinkingConfig(thinking_level="high"),
179
+ response_mime_type="application/json",
180
+ response_schema=_NormalizedSpec,
181
+ ),
182
+ )
183
+
184
+ spec = getattr(r, "parsed", None)
185
+ if spec is None:
186
+ data = json.loads(r.text)
187
+ spec = _NormalizedSpec.model_validate(data)
188
+
189
+ assembled = _assemble(spec)
190
+ if not assembled or not assembled.strip():
191
+ raise RuntimeError("assembled normalized prompt is empty")
192
+ return assembled
193
+
194
+ except Exception as e:
195
+ logger.warning("structured normalize failed: %s - falling back to raw prompt", e)
196
+ return raw_prompt
197
+
198
+
199
+ inf.normalize_prompt = _normalize_via_gemini
200
+
201
+
202
  # ──────────────────────────────────────────────────────────────────────────────
203
+ # Job state
204
  # ──────────────────────────────────────────────────────────────────────────────
205
  JobStatus = Literal["queued", "running", "done", "error"]
206
 
 
292
  # Generation β€” call into local trtllm-serve over HTTP
293
  # ──────────────────────────────────────────────────────────────────────────────
294
  def _trtllm_generate(prompt_text: str) -> str:
 
295
  body = {
296
  "model": TRTLLM_MODEL,
297
  "messages": [
 
324
  logger.info("job %s started", job.id)
325
 
326
  try:
 
 
327
  try:
328
  normalized = inf.normalize_prompt(job.raw_prompt)
329
  except Exception as e:
 
336
  normalized = job.raw_prompt
337
  job.normalized_prompt = normalized
338
 
 
339
  raw_html = _trtllm_generate(job.normalized_prompt)
340
 
 
341
  html = inf.post_process(raw_html)
342
  if not html.strip():
343
  raise RuntimeError("post_process returned empty output")
 
349
  job.id, time.time() - job.started_at, len(html),
350
  )
351
 
 
352
  if OUTPUT_DIR is not None:
353
  try:
354
  OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 
398
  async def lifespan(_: FastAPI):
399
  global _executor
400
 
 
401
  try:
402
  r = requests.get(f"{TRTLLM_BASE_URL}/v1/models", timeout=10)
403
  r.raise_for_status()
 
428
  _executor.shutdown(wait=False, cancel_futures=True)
429
 
430
 
431
+ app = FastAPI(title="HTML Generation API (TRT-LLM backed)", version="2.1.0", lifespan=lifespan)
432
 
433
  app.add_middleware(
434
  CORSMiddleware,
 
519
  job = _get_job(job_id)
520
  if job is not None:
521
  return job.to_response()
 
522
  if OUTPUT_DIR is not None:
523
  html_path = OUTPUT_DIR / f"{job_id}.html"
524
  meta_path = OUTPUT_DIR / f"{job_id}.json"
pod_api_old.py ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pod_api.py β€” RunPod-side FastAPI server that delegates generation to local
3
+ trtllm-serve while keeping your existing api.py contract (job pattern,
4
+ Pydantic validation, normalizer routing, auto-save, error handling).
5
+
6
+ Architecture in this pod:
7
+
8
+ Client ─POST /v1/jobs──▢ pod_api.py (this file, port 5000)
9
+ β”‚
10
+ β”‚ enqueues job
11
+ β–Ό
12
+ ThreadPoolExecutor
13
+ β”‚
14
+ β”‚ 1. normalize via Anthropic API
15
+ β”‚ 2. POST to trtllm-serve
16
+ β–Ό
17
+ trtllm-serve (port 8000, local) ──▢ model on GPU
18
+
19
+ Why this layout:
20
+ - Your reliability layer (job pattern, validation, GC, auto-save) stays.
21
+ - TRT-LLM does the actual generation β€” 2.85Γ— faster than transformers, and
22
+ ready to add NGram speculative on top via the existing spec_config.yaml.
23
+ - Anthropic-based normalizer + dashboard routing keep working unchanged
24
+ because we import your existing inference_edited_chat_opt module.
25
+
26
+ Setup:
27
+ pip install fastapi "uvicorn[standard]" pydantic requests anthropic
28
+ export ANTHROPIC_API_KEY=...
29
+
30
+ # Make sure trtllm-serve is already running on :8000.
31
+ # Then start this:
32
+ uvicorn pod_api:app --host 0.0.0.0 --port 5000 --workers 1
33
+
34
+ Endpoints (same shape as your old api.py):
35
+ GET /v1/healthz
36
+ GET /v1/readyz
37
+ POST /v1/jobs -> 202 {"job_id": ...}
38
+ GET /v1/jobs/{job_id} -> status + html when done
39
+ GET /v1/jobs -> list recent jobs
40
+ POST /v1/generate -> synchronous variant
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import logging
46
+ import os
47
+ import sys
48
+ import threading
49
+ import time
50
+ import uuid
51
+ from concurrent.futures import ThreadPoolExecutor
52
+ from contextlib import asynccontextmanager
53
+ from dataclasses import dataclass, field
54
+ from pathlib import Path
55
+ from typing import Any, Literal, Optional
56
+
57
+ import requests
58
+ from fastapi import FastAPI, HTTPException
59
+ from fastapi.middleware.cors import CORSMiddleware
60
+ from fastapi.responses import JSONResponse
61
+ from pydantic import BaseModel, Field, field_validator
62
+
63
+ # Make /workspace importable so we can pull SYSTEM_PROMPT, normalizers,
64
+ # is_dashboard_prompt, and post_process from your existing module.
65
+ sys.path.insert(0, "/workspace")
66
+ import inference_edited_chat_opt as inf
67
+
68
+ # ──────────────────────────────────────────────────────────────────────────────
69
+ # Config
70
+ # ──────────────────────────────────────────────────────────────────────────────
71
+ TRTLLM_BASE_URL = os.environ.get("TRTLLM_BASE_URL", "http://localhost:8000")
72
+ TRTLLM_MODEL = os.environ.get("TRTLLM_MODEL", "final_model")
73
+ MAX_PROMPT_CHARS = 8_000
74
+ MAX_CONCURRENT_JOBS = 16
75
+ JOB_TIMEOUT_S = 60 * 25
76
+ SYNC_TIMEOUT_S = 60 * 20
77
+ JOB_RETENTION_S = 60 * 60
78
+ OUTPUT_DIR: Optional[Path] = Path(os.environ.get("API_OUTPUT_DIR", "/workspace/api_output"))
79
+ GENERATION_MAX_TOKENS = int(os.environ.get("GENERATION_MAX_TOKENS", "8192"))
80
+ GENERATION_TEMPERATURE = float(os.environ.get("GENERATION_TEMPERATURE", "0.0"))
81
+
82
+ logger = logging.getLogger("pod_api")
83
+ logging.basicConfig(
84
+ level=logging.INFO,
85
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
86
+ )
87
+
88
+ # ──────────────────────────────────────────────────────────────────────────────
89
+ # Job state (same shape as your old api.py)
90
+ # ──────────────────────────────────────────────────────────────────────────────
91
+ JobStatus = Literal["queued", "running", "done", "error"]
92
+
93
+
94
+ @dataclass
95
+ class Job:
96
+ id: str
97
+ raw_prompt: str
98
+ normalized_prompt: Optional[str] = None
99
+ status: JobStatus = "queued"
100
+ html: Optional[str] = None
101
+ error: Optional[str] = None
102
+ created_at: float = field(default_factory=time.time)
103
+ started_at: Optional[float] = None
104
+ finished_at: Optional[float] = None
105
+ done_event: threading.Event = field(default_factory=threading.Event)
106
+
107
+ def to_response(self) -> dict[str, Any]:
108
+ body: dict[str, Any] = {
109
+ "job_id": self.id,
110
+ "status": self.status,
111
+ "created_at": self.created_at,
112
+ }
113
+ if self.started_at is not None:
114
+ body["started_at"] = self.started_at
115
+ if self.finished_at is not None:
116
+ body["finished_at"] = self.finished_at
117
+ body["duration_seconds"] = round(
118
+ self.finished_at - (self.started_at or self.created_at), 2
119
+ )
120
+ if self.normalized_prompt is not None:
121
+ body["normalized_prompt"] = self.normalized_prompt
122
+ if self.status == "done":
123
+ body["html"] = self.html
124
+ elif self.status == "error":
125
+ body["error"] = self.error
126
+ return body
127
+
128
+
129
+ _jobs: dict[str, Job] = {}
130
+ _jobs_lock = threading.Lock()
131
+ _executor: Optional[ThreadPoolExecutor] = None
132
+ _inflight = 0
133
+ _inflight_lock = threading.Lock()
134
+
135
+
136
+ def _store_job(job: Job) -> None:
137
+ with _jobs_lock:
138
+ _jobs[job.id] = job
139
+
140
+
141
+ def _get_job(job_id: str) -> Optional[Job]:
142
+ with _jobs_lock:
143
+ return _jobs.get(job_id)
144
+
145
+
146
+ def _gc_jobs() -> None:
147
+ now = time.time()
148
+ with _jobs_lock:
149
+ stale = [
150
+ jid for jid, j in _jobs.items()
151
+ if j.finished_at is not None and (now - j.finished_at) > JOB_RETENTION_S
152
+ ]
153
+ for jid in stale:
154
+ _jobs.pop(jid, None)
155
+
156
+
157
+ def _try_reserve_slot() -> bool:
158
+ global _inflight
159
+ with _inflight_lock:
160
+ if _inflight >= MAX_CONCURRENT_JOBS:
161
+ return False
162
+ _inflight += 1
163
+ return True
164
+
165
+
166
+ def _release_slot() -> None:
167
+ global _inflight
168
+ with _inflight_lock:
169
+ _inflight = max(0, _inflight - 1)
170
+
171
+
172
+ def _inflight_count() -> int:
173
+ with _inflight_lock:
174
+ return _inflight
175
+
176
+
177
+ # ──────────────────────────────────────────────────────────────────────────────
178
+ # Generation β€” call into local trtllm-serve over HTTP
179
+ # ──────────────────────────────────────────────────────────────────────────────
180
+ def _trtllm_generate(prompt_text: str) -> str:
181
+ """Send a chat-completion request to trtllm-serve and return the HTML."""
182
+ body = {
183
+ "model": TRTLLM_MODEL,
184
+ "messages": [
185
+ {"role": "system", "content": inf.SYSTEM_PROMPT},
186
+ {"role": "user", "content": prompt_text},
187
+ ],
188
+ "max_tokens": GENERATION_MAX_TOKENS,
189
+ "temperature": GENERATION_TEMPERATURE,
190
+ }
191
+ resp = requests.post(
192
+ f"{TRTLLM_BASE_URL}/v1/chat/completions",
193
+ headers={"Content-Type": "application/json"},
194
+ json=body,
195
+ timeout=JOB_TIMEOUT_S,
196
+ )
197
+ resp.raise_for_status()
198
+ data = resp.json()
199
+ text = data["choices"][0]["message"]["content"]
200
+ if not isinstance(text, str) or not text.strip():
201
+ raise RuntimeError("trtllm-serve returned empty content")
202
+ return text
203
+
204
+
205
+ # ──────────────────────────────────────────────────────────────────────────────
206
+ # Job runner
207
+ # ──────────────────────────────────────────────────────────────────────────────
208
+ def _run_job(job: Job) -> None:
209
+ job.started_at = time.time()
210
+ job.status = "running"
211
+ logger.info("job %s started", job.id)
212
+
213
+ try:
214
+ # Step 1 β€” normalize via Anthropic (uses your existing normalizers,
215
+ # routed by is_dashboard_prompt for landing-page vs dashboard).
216
+ try:
217
+ normalized = inf.normalize_prompt(job.raw_prompt)
218
+ except Exception as e:
219
+ logger.warning(
220
+ "normalize failed for job %s: %s β€” falling back to raw prompt",
221
+ job.id, e,
222
+ )
223
+ normalized = job.raw_prompt
224
+ if not isinstance(normalized, str) or not normalized.strip():
225
+ normalized = job.raw_prompt
226
+ job.normalized_prompt = normalized
227
+
228
+ # Step 2 β€” generate via trtllm-serve (local HTTP, port 8000)
229
+ raw_html = _trtllm_generate(job.normalized_prompt)
230
+
231
+ # Step 3 β€” apply your existing post-processing
232
+ html = inf.post_process(raw_html)
233
+ if not html.strip():
234
+ raise RuntimeError("post_process returned empty output")
235
+
236
+ job.html = html
237
+ job.status = "done"
238
+ logger.info(
239
+ "job %s done in %.1fs (%d chars)",
240
+ job.id, time.time() - job.started_at, len(html),
241
+ )
242
+
243
+ # Auto-save to disk so results survive in-memory GC.
244
+ if OUTPUT_DIR is not None:
245
+ try:
246
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
247
+ (OUTPUT_DIR / f"{job.id}.html").write_text(html, encoding="utf-8")
248
+ (OUTPUT_DIR / f"{job.id}.json").write_text(
249
+ json.dumps({
250
+ "job_id": job.id,
251
+ "raw_prompt": job.raw_prompt,
252
+ "normalized_prompt": job.normalized_prompt,
253
+ "created_at": job.created_at,
254
+ "started_at": job.started_at,
255
+ "finished_at": time.time(),
256
+ "duration_seconds": round(time.time() - job.started_at, 2),
257
+ }, indent=2),
258
+ encoding="utf-8",
259
+ )
260
+ logger.info("job %s saved to %s", job.id, OUTPUT_DIR)
261
+ except Exception as e:
262
+ logger.warning("failed to persist job %s: %s", job.id, e)
263
+
264
+ except requests.HTTPError as e:
265
+ job.error = f"trtllm-serve returned {e.response.status_code}: {e.response.text[:500]}"
266
+ job.status = "error"
267
+ logger.exception("job %s β€” trtllm-serve HTTP error", job.id)
268
+
269
+ except requests.RequestException as e:
270
+ job.error = f"trtllm-serve unreachable: {e}"
271
+ job.status = "error"
272
+ logger.exception("job %s β€” trtllm-serve unreachable", job.id)
273
+
274
+ except Exception as e:
275
+ job.error = f"{type(e).__name__}: {e}"
276
+ job.status = "error"
277
+ logger.exception("job %s failed", job.id)
278
+
279
+ finally:
280
+ job.finished_at = time.time()
281
+ job.done_event.set()
282
+ _release_slot()
283
+ _gc_jobs()
284
+
285
+
286
+ # ──────────────────────────────────────────────────────────────────────────────
287
+ # FastAPI app + lifespan
288
+ # ──────────────────────────────────────────────────────────────────────────────
289
+ @asynccontextmanager
290
+ async def lifespan(_: FastAPI):
291
+ global _executor
292
+
293
+ # Probe trtllm-serve once on startup so we fail fast if it's not running.
294
+ try:
295
+ r = requests.get(f"{TRTLLM_BASE_URL}/v1/models", timeout=10)
296
+ r.raise_for_status()
297
+ logger.info(
298
+ "trtllm-serve OK at %s (%d models loaded)",
299
+ TRTLLM_BASE_URL, len(r.json().get("data", [])),
300
+ )
301
+ except Exception as e:
302
+ logger.error(
303
+ "trtllm-serve not reachable at %s β€” %s. "
304
+ "Start it before this API: trtllm-serve /workspace/final_model --host 0.0.0.0 --port 8000",
305
+ TRTLLM_BASE_URL, e,
306
+ )
307
+
308
+ _executor = ThreadPoolExecutor(
309
+ max_workers=MAX_CONCURRENT_JOBS,
310
+ thread_name_prefix="job-runner",
311
+ )
312
+ logger.info(
313
+ "executor started (max_workers=%d), output_dir=%s",
314
+ MAX_CONCURRENT_JOBS, OUTPUT_DIR,
315
+ )
316
+
317
+ try:
318
+ yield
319
+ finally:
320
+ if _executor is not None:
321
+ _executor.shutdown(wait=False, cancel_futures=True)
322
+
323
+
324
+ app = FastAPI(title="HTML Generation API (TRT-LLM backed)", version="2.0.0", lifespan=lifespan)
325
+
326
+ app.add_middleware(
327
+ CORSMiddleware,
328
+ allow_origins=["*"],
329
+ allow_methods=["*"],
330
+ allow_headers=["*"],
331
+ )
332
+
333
+
334
+ class GenerateRequest(BaseModel):
335
+ prompt: str = Field(..., min_length=1, max_length=MAX_PROMPT_CHARS)
336
+
337
+ @field_validator("prompt")
338
+ @classmethod
339
+ def _strip(cls, v: str) -> str:
340
+ v = v.strip()
341
+ if not v:
342
+ raise ValueError("prompt is empty after stripping whitespace")
343
+ return v
344
+
345
+
346
+ @app.exception_handler(Exception)
347
+ async def _unhandled(request, exc):
348
+ logger.exception("unhandled exception in request: %s", exc)
349
+ return JSONResponse(
350
+ status_code=500,
351
+ content={"error": "internal_server_error", "detail": str(exc)},
352
+ )
353
+
354
+
355
+ # ──────────────────────────────────────────────────────────────────────────────
356
+ # Endpoints
357
+ # ──────────────────────────────────────────────────────────────────────────────
358
+ @app.get("/v1/healthz")
359
+ def healthz():
360
+ return {"status": "ok"}
361
+
362
+
363
+ @app.get("/v1/readyz")
364
+ def readyz():
365
+ if _executor is None:
366
+ return JSONResponse(status_code=503, content={"status": "executor_not_ready"})
367
+ try:
368
+ r = requests.get(f"{TRTLLM_BASE_URL}/v1/models", timeout=5)
369
+ if r.status_code != 200:
370
+ return JSONResponse(
371
+ status_code=503,
372
+ content={"status": "trtllm_unhealthy", "trtllm_status": r.status_code},
373
+ )
374
+ except Exception as e:
375
+ return JSONResponse(
376
+ status_code=503,
377
+ content={"status": "trtllm_unreachable", "detail": str(e)},
378
+ )
379
+ return {
380
+ "status": "ready",
381
+ "in_flight": _inflight_count(),
382
+ "max_concurrent_jobs": MAX_CONCURRENT_JOBS,
383
+ "trtllm_url": TRTLLM_BASE_URL,
384
+ }
385
+
386
+
387
+ @app.post("/v1/jobs", status_code=202)
388
+ def create_job(req: GenerateRequest):
389
+ if _executor is None:
390
+ raise HTTPException(status_code=503, detail="server still warming up")
391
+ if not _try_reserve_slot():
392
+ raise HTTPException(
393
+ status_code=503,
394
+ detail=f"server at capacity ({MAX_CONCURRENT_JOBS} in-flight) β€” try again shortly",
395
+ )
396
+ job = Job(id=uuid.uuid4().hex, raw_prompt=req.prompt)
397
+ _store_job(job)
398
+ _executor.submit(_run_job, job)
399
+ logger.info(
400
+ "job %s queued (in_flight=%d, prompt_chars=%d)",
401
+ job.id, _inflight_count(), len(req.prompt),
402
+ )
403
+ return {
404
+ "job_id": job.id,
405
+ "status": "queued",
406
+ "in_flight": _inflight_count(),
407
+ }
408
+
409
+
410
+ @app.get("/v1/jobs/{job_id}")
411
+ def get_job(job_id: str):
412
+ job = _get_job(job_id)
413
+ if job is not None:
414
+ return job.to_response()
415
+ # Fall back to disk if the job was GC'd from memory.
416
+ if OUTPUT_DIR is not None:
417
+ html_path = OUTPUT_DIR / f"{job_id}.html"
418
+ meta_path = OUTPUT_DIR / f"{job_id}.json"
419
+ if html_path.exists():
420
+ try:
421
+ meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
422
+ return {
423
+ "job_id": job_id,
424
+ "status": "done",
425
+ "html": html_path.read_text(encoding="utf-8"),
426
+ "source": "disk",
427
+ **meta,
428
+ }
429
+ except Exception as e:
430
+ logger.warning("failed to read persisted job %s: %s", job_id, e)
431
+ raise HTTPException(
432
+ status_code=404,
433
+ detail="job not found (not in memory and not persisted to disk)",
434
+ )
435
+
436
+
437
+ @app.get("/v1/jobs")
438
+ def list_jobs(limit: int = 50):
439
+ if limit < 1 or limit > 500:
440
+ raise HTTPException(status_code=400, detail="limit must be between 1 and 500")
441
+ with _jobs_lock:
442
+ items = sorted(_jobs.values(), key=lambda j: j.created_at, reverse=True)[:limit]
443
+ return {
444
+ "count": len(items),
445
+ "jobs": [
446
+ {"job_id": j.id, "status": j.status, "created_at": j.created_at}
447
+ for j in items
448
+ ],
449
+ }
450
+
451
+
452
+ @app.post("/v1/generate")
453
+ def generate_sync(req: GenerateRequest):
454
+ if _executor is None:
455
+ raise HTTPException(status_code=503, detail="server still warming up")
456
+ if not _try_reserve_slot():
457
+ raise HTTPException(
458
+ status_code=503,
459
+ detail=f"server at capacity ({MAX_CONCURRENT_JOBS} in-flight) β€” try again shortly",
460
+ )
461
+ job = Job(id=uuid.uuid4().hex, raw_prompt=req.prompt)
462
+ _store_job(job)
463
+ _executor.submit(_run_job, job)
464
+ finished = job.done_event.wait(timeout=SYNC_TIMEOUT_S)
465
+ if not finished:
466
+ raise HTTPException(
467
+ status_code=504,
468
+ detail={
469
+ "job_id": job.id,
470
+ "error": "generation timed out β€” use GET /v1/jobs/{id} to retrieve",
471
+ },
472
+ )
473
+ if job.status == "done":
474
+ return {
475
+ "job_id": job.id,
476
+ "html": job.html,
477
+ "normalized_prompt": job.normalized_prompt,
478
+ "duration_seconds": round(
479
+ (job.finished_at or 0) - (job.started_at or 0), 2
480
+ ),
481
+ }
482
+ raise HTTPException(
483
+ status_code=500,
484
+ detail={"job_id": job.id, "error": job.error or "unknown error"},
485
+ )