
Long Poster
- 3 installs
- Updated April 28, 2026
- hamsterider-m/long-poster
Turn content into a vertical long-form poster as editable HTML plus a high-resolution PNG, after showing three distinct style demos for review.
About
A skill that creates vertical long-form posters (infographics, social/WeChat/Xiaohongshu images) as both editable HTML and high-resolution PNG exports. A developer uses it to turn notes or promo content into a continuous scroll poster after choosing from three style directions.
- Researches styles live and shows three distinct demos before final output
- Produces editable HTML plus high-resolution PNG with a scoring rubric gate
Long Poster by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,153 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hamsterider-m/long-poster --skill long-posterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| Last updated | April 28, 2026 |
| Repository | hamsterider-m/long-poster ↗ |
What it does
Turn content into a vertical long-form poster as editable HTML plus a high-resolution PNG, after showing three distinct style demos for review.
Files
Long Poster
Create vertical long-form posters that work as both editable HTML and high-resolution PNG exports.
This skill is for continuous scroll posters, not slide decks. Do not use .slide { height: 100vh } patterns unless the user explicitly asks for slides. A long poster is a single vertical canvas with sections, rhythm, and export constraints.
Success Standard
Use references/scoring-rubric.md for evals and self-checks. A result is successful only when it reaches at least 90%: 9 of 10 rubric checks must pass.
Every saved run must include two evidence artifacts:
output.md: concise handoff with the preview/final file paths and a "Rubric Evidence" section.rubric-evidence.json: 10 checks in rubric order, each withpassedandevidence.
Before handing work back to the user, run this short gate mentally or with scripts/score-run.js when evaluating saved outputs:
- Did I parse content, audience, channel, CTA, and hierarchy?
- Did I keep it as one continuous vertical poster rather than slides?
- Did I choose three content-specific style directions, not a fixed menu?
- Are the three directions visibly different in at least three dimensions?
- Did each direction include source, fit, visual system, difference, and risk?
- Did I create real demos using real content and a preview index?
- Did I open the preview or report the preview path?
- Did I stop for user confirmation before final production?
- Did I state or deliver editable HTML plus 3x PNG at least 1200px wide?
- If the poster is too tall, did I produce designed multi-panel images instead of hard slicing when feasible?
- Did I route hero/key visual needs through the Image Asset Step, using Codex native GPT Image 2 /
image-2when appropriate? - Did I verify or commit to verifying dimensions, decodability, overflow, full height, multi-panel output, and the
image-2/imagegenboundary?
Core Workflow
1. Understand the content
- Extract the topic, audience, channel, call to action, required facts, and rough content hierarchy.
- If content is sparse, make a compact poster with fewer sections instead of padding it with generic copy.
- If content is dense, group it into clear vertical sections and avoid cramming.
2. Research and propose 3 style directions
- Do live design thinking and, when useful, web research for design history, visual culture, current poster examples, or domain references.
- Do not rely on a fixed preset list. The three directions should be chosen for this specific content.
- Each direction must name a recognizable design history movement, visual culture, or contemporary design language.
- The three directions must differ in at least 3 of these dimensions: layout structure, typography mood, color system, graphic language, content density, image treatment, motion/interaction.
- Avoid presenting three near-identical directions that only swap colors.
3. Create 3 demo previews
- Build small HTML demos or first-screen/short-poster previews for all three directions.
- Create a local preview index that shows all three side by side or stacked.
- Automatically open the preview page for the user to judge.
- In the preview handoff, explicitly state the final export contract: editable HTML plus
poster-3x.png, PNG width at least1200px, and multi-panel PNGs for very tall posters. - In the preview handoff, explicitly state that final production will run a horizontal overflow check and full-height completeness check.
- Stop and wait for the user to choose one direction or request a mix.
4. Generate the final poster
- Produce a single editable HTML file with inline CSS and JS where possible.
- Keep output portable: local assets should be copied next to the HTML or embedded.
- Use responsive CSS with
clamp()for text and spacing. - Maintain a stable poster width, usually
430pxCSS width unless the user asks otherwise. - For long outputs, wrap natural chunks in
.poster-panelor[data-poster-panel]sections so the exporter can create multi-panel images without cutting through content. - Follow the Image Asset Step when the poster needs a hero image, product render, atmosphere image, mascot, icon scene, or other generated visual.
Image Asset Step
Use this step whenever the poster would benefit from a main visual or generated supporting image.
1. Decide whether an image is needed
- Use generated imagery when a hero visual, abstract product scene, campaign image, or strong visual hook will improve the poster.
- Do not force generated imagery for dense operational posters where typography, grid, and diagrams work better.
2. Prefer Codex native GPT Image 2 / `image-2`
- Codex native image generation (
image-2, GPT Image 2) can create visual assets directly in Codex and does not requireOPENAI_API_KEY. - Use native
image-2for one-off hero/key visuals and fast creative exploration. - Prompt it for clean bitmap assets with no embedded text, no logo, no watermark, and clear safe margins.
- Ask for the intended role and aspect ratio, for example:
vertical poster hero, 4:5,wide header texture, 16:9, orsquare icon scene, 1:1.
3. Use `imagegen` CLI only for reproducible or batch generation
- The
imagegenskill CLI is a reproducible/batch alternative and requiresOPENAI_API_KEY. - Use it when the user asks for repeatable generation, scripted batch assets, or regeneration from stored prompts.
4. Integrate the asset
- Save or reference the generated asset next to the HTML when the environment exposes a file path.
- If no local file path is available, keep the HTML functional with CSS/SVG fallback and report that the native generated image is a separate visual asset.
- Do not place generated images behind dense text unless contrast and legibility are verified.
5. Verify the asset
- Check that the image has no unwanted text, watermark, malformed logo, or obvious crop problem.
- Check that it is high enough resolution for the final PNG export.
- Record in
output.mdwhether nativeimage-2was used, whetherimagegenCLI was used, or why generated imagery was not needed.
6. Export high-resolution PNG
- Export
poster-3x.pngby default withdeviceScaleFactor=3. - The exported PNG width should be at least
1200px; if not, increase scale. - For very tall posters, prefer designed multi-panel PNG files to avoid preview truncation in chat apps, WeChat, Feishu, and similar clients.
- Multi-panel output means
panels/panel-01.png,panel-02.png, etc., where each panel is a complete designed card. Do not cut text, QR codes, charts, faces, or section titles across boundaries. - Use hard
segments/part-xx.pngslicing only as a fallback when the HTML has no usable panel boundaries and the user still needs it.
7. Verify before delivery
- Check actual PNG pixel dimensions.
- Check horizontal overflow in the rendered page.
- Check that the PNG height matches the rendered poster height at export scale.
- If the poster is very tall, explain that embedded previews may crop it and point to the multi-panel files.
Output Contract
Final delivery should include:
index.htmlor a clearly named editable HTML file.poster-3x.pngor equivalent high-resolution PNG.panels/panel-01.png,panel-02.png, etc. when the full PNG is taller than typical preview limits.segments/part-01.png,part-02.png, etc. only when hard slicing fallback was unavoidable.- A short note with pixel dimensions and verification status.
- The note must explicitly report horizontal overflow status, full-height completeness status, and whether multi-panel output or hard-segment fallback was generated.
- If any visual asset generation path was considered, the note must explicitly say whether Codex native
image-2was used, or whetherimagegenCLI would requireOPENAI_API_KEY. - If GPT Image 2 /
image-2generated a visual asset, the note must include the asset role, prompt intent, file/path status, and visual QA status.
Preview Handoff Template
After opening the three-style preview, respond with this information and then stop:
已打开 3 个风格 demo 预览:[preview path]
请选择 A / B / C,或指定混合方式。确认前我不会生成最终长图。
最终交付标准:可编辑 HTML + `poster-3x.png`;PNG 默认 3x,宽度不低于 1200px;超长图优先输出多宫格 `panels/panel-xx.png`,每张都是完整设计卡片;硬切分 `segments/part-xx.png` 只做兜底。最终会检查横向溢出、PNG 可解码、像素尺寸和底部完整性。若需要生成主视觉,Codex 原生 `image-2` 不依赖 API Key;`imagegen` CLI 才需要 `OPENAI_API_KEY`。Keep the wording concise, but do not omit the contract.
When saving outputs for a preview-stage run, output.md must also contain:
## Content Parse
- Topic:
- Audience:
- Channel:
- CTA:
- Hierarchy:
## Format
- Continuous vertical long poster, not 100vh slides.
## Preview Artifacts
- style-a.html:
- style-b.html:
- style-c.html:
- preview-index.html:
## Rubric Evidence
- 1 Content parsing: pass, evidence...
- 2 Continuous long-poster format: pass, evidence...
- 3 Live style reasoning: pass, evidence...
- 4 Three distinct style directions: pass, evidence...
- 5 Direction brief quality: pass, evidence...
- 6 Real demo preview: pass, evidence...
- 7 Automatic preview handoff: pass, evidence...
- 8 User confirmation gate: pass, evidence...
- 9 Final output contract: pass, evidence...
- 10 Export verification and image boundary: pass, evidence...rubric-evidence.json must use this shape:
{
"threshold": 0.9,
"checks": [
{"passed": true, "evidence": "Topic/audience/channel/CTA/hierarchy captured in output.md"}
]
}Keep all 10 checks in the same order as references/scoring-rubric.md.
Final Run Rules
If the user explicitly asks for final HTML + PNG and has already chosen a direction, produce the final output directly.
If the user explicitly asks for final HTML + PNG but has not chosen a direction, still present three researched directions first. If the user also signals they want direct production in the same turn, choose the strongest direction, state that assumption in output.md, and produce the final files. Otherwise stop after the preview handoff.
For final HTML, add .poster-panel or [data-poster-panel] to natural sections so multi-panel export is available.
Final output.md must report:
- Chosen direction and why.
- HTML path.
- Full PNG path and pixel dimensions.
- Whether PNG width is at least 1200px.
- Horizontal overflow status.
- PNG decodability status.
- Full-height completeness status.
panels/path for multi-panel output, or why hardsegments/fallback was used.- Codex native
image-2vsimagegenCLI API-key boundary. - Image asset status:
image-2 used,imagegen CLI used, orno generated imagery needed, plus visual QA notes.
Style Direction Brief
For each of the 3 preview directions, use this format:
### Direction A — [Name]
- Source: [design movement / visual culture / contemporary reference]
- Fit: [why it matches this content and audience]
- Visual system: [layout, type, color, graphics]
- Difference: [how it differs from the other two directions]
- Risk: [what could go wrong]Preview Rules
- Preview before final production. The preview is a decision tool, not a finished poster.
- Make previews visibly different at first glance.
- Use real content fragments from the user, not lorem ipsum.
- Open the preview page automatically when the environment supports it.
- If opening fails, provide the local preview path.
- After opening the preview, stop. Do not generate the final full poster until the user chooses a direction or gives a clear mix instruction.
- The preview response must include the future export standard: HTML plus high-resolution PNG, default 3x, width at least 1200px, and designed multi-panel images for tall outputs.
Export Rules
Use scripts/export-poster.js when available. It renders HTML with Playwright/Chrome, exports a high-DPI PNG, checks horizontal overflow, verifies dimensions, and can export designed panels for very tall posters.
Recommended command shape:
node scripts/export-poster.js path/to/poster.html --out path/to/poster-3x.png --scale 3 --splitWhen --split is used, the script first exports .poster-panel or [data-poster-panel] elements as panels/panel-xx.png. Hard segments/part-xx.png output is fallback only.
If Playwright's bundled browser is missing, use a local Chrome executable when available.
Use scripts/score-run.js path/to/run/outputs to score saved eval outputs against the 90% success gate.
Common Failure Modes
- Only changing colors across three demos: reject and redesign the candidates.
- PNG looks incomplete in chat preview: check the actual file dimensions; if complete, deliver designed multi-panel PNGs too.
- Hard slicing a long poster: prefer
.poster-panelsections and exportpanels/panel-xx.png; only hard slice as fallback. - Low-resolution export: increase scale until width is at least 1200px.
- Slide deck leakage: do not create many
100vhslides; long posters are continuous canvases. - API key confusion: Codex native
image-2does not needOPENAI_API_KEY;imagegenCLI does. - Weak image integration: do not merely mention
image-2; decide whether generated imagery is needed, choose nativeimage-2orimagegenCLI, and report asset QA.
When to Use Web Research
Browse when the design direction depends on current examples, a niche visual culture, a named brand/category, or when the user asks for modern/trendy/current aesthetics. Use primary or reputable sources where possible, and summarize the visual implications rather than dumping research notes.
User-Learned Best Practices & Constraints
Auto-Generated Section: This section is maintained by skill-evolution-manager. Do not edit manually.User Preferences
- When the user confirms a preview direction (for example A/B/C), preserve that direction as the base visual system and move directly into final production instead of re-proposing directions.
- For long posters and similar visual-output tasks, treat image-model augmentation as a normal design lever, not a niche option for one theme. Include generated-image roles in the style directions when visuals can materially improve the result.
- When proposing directions, make the image policy explicit enough for the user to choose: whether to use generated imagery, what role it plays, and whether text, faces, logos, real people, product marks, or brand marks are allowed.
- Generated visuals should reinforce the poster's subject matter and chosen direction. Prefer several purposeful assets for different roles over one repeated decorative image when the layout benefits from it.
- For very tall social posters, prefer designed 9:16 poster-panel sections that can each be exported as a complete shareable PNG, while also exporting the full long PNG.
- Final handoff should be short but include exact file paths, dimensions, overflow status, full-height completeness, panel output, and image generation boundary.
Known Fixes & Workarounds
- Do not place generated imagery behind dense text. Use it as framed inserts, hero panels, or section strips unless contrast is verified.
- Do not hard-code no text, no logo, or no faces as universal rules. Those constraints should come from the user's prompt or from options the user selects after seeing the directions.
- When the user has not specified image constraints, present the default tradeoff clearly: generated assets with no embedded text/logo/identifiable face are usually easier to integrate cleanly, while text/logo/faces can be used when the user explicitly wants them.
- Treat decorative elements that exceed the viewport as horizontal overflow even if visually intentional; pull them back into bounds and re-export until overflowCount is 0.
- After export, visually inspect panel PNGs for text clipping, awkward line breaks, image overlap, and bottom completeness; patch typography/layout and rerun export when needed.
- If the default Node environment lacks Playwright, use the Codex desktop workspace runtime by setting NODE_PATH to the bundled node_modules and running the bundled Node executable.
Custom Instruction Injection
Poster Finalization Pattern
- After the user chooses a preview direction, use that exact direction as the final visual base.
- In preview directions, include generated-image strategy when it can improve the poster: asset role, visual style, integration location, and whether text/faces/logos/real marks are included or avoided. Let the user choose these constraints when they are not already specified.
- If the user asks for 生图模型效果图 or richer visuals, generate 2-3 subject-specific image-2 assets aligned to the chosen direction. Copy the assets next to the HTML and record source/status in output.md.
- Keep critical poster copy as HTML unless the user explicitly wants text embedded in generated imagery. If generated images include text, faces, logos, or real marks, verify they match the user's chosen direction and do not damage legibility or trust.
- For long WeChat/social posters, build natural .poster-panel / data-poster-panel sections, ideally complete 9:16 cards, so the final export includes both poster-3x.png and panels/panel-xx.png.
- Before final handoff, rerun export until PNG width >= 1200px, PNG decodes, full height matches render, overflowCount is 0, and panel visual QA catches no clipped text, bad line breaks, or image-text overlap.
node_modules/
.DS_Store
*.log
outputs/
runs/
{
"skill_name": "long-poster",
"success_threshold": 0.9,
"scoring": "10 checks, 10% each; a run succeeds only when at least 9 checks pass.",
"evals": [
{
"id": 1,
"prompt": "把这些内容做成一张课程招生长图:主题是 AI 电商运营训练营,面向 18-35 岁转行求职者。内容包括:30 小时课程、导师陪跑、简历和面试辅导、适合零基础、报名 CTA。先给我 3 个差异很大的风格 demo,我选完后要 HTML 和高清 PNG。",
"expected_output": "The agent should use long-poster workflow, propose or create three distinct style demos first, wait for choice, then produce HTML and high-resolution PNG with verification.",
"files": [],
"expectations": [
"The output identifies the course topic, target audience, channel/use case, CTA, and content hierarchy.",
"The output treats the result as one continuous vertical long poster, not a 100vh slide deck.",
"The output chooses style directions through content-specific reasoning or research, not a fixed preset menu.",
"The output provides exactly three style directions or demos that differ in at least three visual dimensions.",
"Each style direction includes source/lineage, fit, visual system, difference, and risk.",
"The output creates three real demo preview files or short poster previews using the user's real content, plus a preview index.",
"The output opens the preview page or reports the exact local preview path.",
"The output stops for user selection of A/B/C or a mix before final poster production.",
"The output states the final contract: editable HTML plus high-resolution PNG, default 3x, width at least 1200px.",
"The output states verification requirements: pixel dimensions, PNG decodability, horizontal overflow, full-height completeness, designed multi-panel PNGs for tall posters, and GPT Image 2/image-2 vs imagegen API-key boundary plus image asset status."
]
},
{
"id": 2,
"prompt": "我需要一张公司内部活动长图,用在飞书群里。内容是下周五 14:00-17:00 的 AI 工作流分享会,包含议程、讲师、报名方式、注意事项。不要套模板,先帮我研究几个风格方向并打开 demo 给我看。",
"expected_output": "The agent should research live style options suitable for internal event communication, open a preview page, and prepare final HTML/PNG only after user confirmation.",
"files": [],
"expectations": [
"The output identifies the internal event topic, audience/channel, schedule, CTA, and content hierarchy.",
"The output treats the result as one continuous vertical long poster, not a 100vh slide deck.",
"The output frames the style choices as live/content-specific research rather than templates.",
"The output provides exactly three style directions or demos that differ in at least three visual dimensions.",
"Each style direction includes source/lineage, fit, visual system, difference, and risk.",
"The output creates three real demo preview files or short poster previews using the user's real content, plus a preview index.",
"The output opens the local preview page or reports the exact local preview path.",
"The output stops for user judgment before final production.",
"The output states the final contract: editable HTML plus high-resolution PNG, default 3x, width at least 1200px.",
"The output states verification requirements: pixel dimensions, PNG decodability, horizontal overflow, full-height completeness, designed multi-panel PNGs for tall posters, and GPT Image 2/image-2 vs imagegen API-key boundary plus image asset status."
]
},
{
"id": 3,
"prompt": "我已经选定方向 B:现代前卫但不要常规 SaaS 风。请做一个产品发布长图,主题是本地优先知识库工具,卖点是隐私、本地检索、Markdown 导入、浏览器插件。直接输出最终 HTML 和 PNG,PNG 要清楚,别像聊天预览那样糊;如果图太长,输出多宫格而不是硬切。",
"expected_output": "The agent should treat this as a final-production run after style selection, produce editable HTML, crisp high-resolution PNG, multi-panel outputs when tall, and verification evidence.",
"files": [],
"expectations": [
"The output identifies the product topic, target audience/channel, CTA, selling points, and content hierarchy.",
"The output treats the result as one continuous vertical long poster, not a 100vh slide deck.",
"The style directions are product-specific and avoid generic SaaS templates.",
"The output provides exactly three style directions or demos that differ in at least three visual dimensions.",
"Each style direction includes source/lineage, fit, visual system, difference, and risk.",
"If a preview phase is needed, the output creates three real demo preview files plus a preview index; if final production is requested directly, it explains the chosen direction before producing.",
"The final output includes editable HTML.",
"The final output includes a high-resolution PNG using 3x or an equivalent scale, with width at least 1200px.",
"The output verifies actual pixel dimensions, PNG decodability, horizontal overflow, and full-height completeness.",
"The output handles tall-image preview truncation with designed multi-panel PNGs when needed and distinguishes Codex native GPT Image 2/image-2 from imagegen CLI/API-key requirements, including image asset status."
]
}
]
}
MIT License
Copyright (c) 2026 Hanson Mei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
long-poster
Agent skill for creating vertical long-form posters from notes, campaigns, events, courses, products, and other content.
It produces editable HTML and high-resolution PNG exports, with a required three-style preview workflow before final production.
Install
Install globally for Codex:
npx skills add HamsteRider-m/long-poster -a codex -g -yInstall to the current project:
npx skills add HamsteRider-m/long-poster -a codex -yList available skills without installing:
npx skills add HamsteRider-m/long-poster --listUse
Ask your agent for a long poster, 长图, vertical infographic, WeChat/Feishu/Xiaohongshu/Bilibili image, or HTML + PNG poster.
Example:
把这些内容做成一张课程招生长图,先给我 3 个风格 demo,我选完后输出 HTML 和高清 PNG。Contents
SKILL.md: main skill instructions.scripts/export-poster.js: Playwright-based HTML to PNG exporter.scripts/open-preview.js: preview index generator.scripts/score-run.js: rubric scorer.references/: export and scoring references.evals/: sample eval prompts.
Export Notes
The exporter uses Playwright. In Codex Desktop it can use the bundled Node/Playwright runtime; in a plain Node environment, install Playwright first if needed:
npm install playwrightLicense
MIT
Exporting Long Posters
Default Export
Export a high-resolution PNG from the final HTML:
- CSS poster width: usually
430px. - Device scale factor: default
3. - Expected PNG width: about
1290px. - Minimum acceptable PNG width:
1200px.
Completeness Checks
Before delivery, check:
- Rendered CSS width and height.
- PNG pixel width and height.
- Horizontal overflow count.
- PNG height approximately equals rendered height times scale.
- The file can be decoded as a valid PNG.
Multi-Panel Output
Long PNGs can be complete but still look cropped in chat or app previews. If the full PNG height is above about 12000px, prefer exporting a designed multi-panel set instead of hard slicing the same image.
Preferred naming:
panels/panel-01.png
panels/panel-02.png
panels/panel-03.pngEach panel should be a complete designed card with its own safe top/bottom spacing. Do not cut text, faces, QR codes, charts, or section titles across panel boundaries.
Recommended HTML markers:
<section class="poster-panel" data-poster-panel>
...
</section>scripts/export-poster.js --split first looks for .poster-panel or [data-poster-panel] and exports those as panels/panel-xx.png.
Hard Segment Fallback
Only use hard segments when the HTML has no usable panel boundaries and the user still needs a fallback export.
Fallback naming:
segments/part-01.png
segments/part-02.png
segments/part-03.pngSegment height should usually be 3000-5000px at final pixel scale, depending on destination. Treat this as a fallback, not the preferred delivery format.
Delivery Note
Tell the user:
- The HTML path.
- The full PNG path and dimensions.
- The
panels/directory if generated. - The
segments/directory only if hard segmentation fallback was used. - Whether horizontal overflow was detected.
- Whether preview truncation may happen despite a complete file.
- Whether Codex native GPT Image 2 /
image-2was used for generated visuals, or whether the reproducibleimagegenCLI path would requireOPENAI_API_KEY. - If generated imagery was used, include the asset role, prompt intent, local file/path status, and visual QA status.
Research Protocol
Use this when choosing the three style directions for a long poster.
Goal
The skill should not pick from a fixed menu. It should infer what visual language would serve the content, then research or reason toward three distinct options.
Inputs to Consider
- Subject matter: course, product, event, internal memo, civic campaign, personal portfolio, academic summary.
- Audience: students, executives, customers, fans, employees, niche community.
- Channel: WeChat, Feishu, Xiaohongshu, Bilibili, web, print, internal deck replacement.
- Information density: announcement, syllabus, timeline, comparison, FAQ, proof pack.
- Emotional effect: authority, urgency, playfulness, luxury, provocation, clarity.
Candidate Construction
Create three directions that differ in at least three of:
- Layout: grid, collage, editorial columns, modular cards, timeline, poster stack.
- Typography: rational sans, expressive display, serif editorial, monospace/technical, hand-made/zine.
- Color: monochrome, primary color, muted editorial, acid palette, metallic/luxury, dark high contrast.
- Graphic language: geometry, photography, illustration, scanned texture, UI chrome, diagrams, typographic blocks.
- Density: sparse premium, medium editorial, dense operational.
- Image treatment: full-bleed, cutout, posterized, duotone, no-image typographic.
Brief Quality Bar
Each direction should explain:
- Where the visual language comes from.
- Why it fits this specific content.
- What it intentionally does differently from the other two.
- What risk the user should be aware of.
Reject Conditions
- The three options are just color variants.
- The style source is vague, such as "modern", "premium", or "clean" with no visual lineage.
- The chosen style makes the content harder to read for the target channel.
- The candidate requires assets or fonts that cannot be delivered in the current environment.
Long Poster Scoring Rubric
Use this rubric for evals and self-checks. A run succeeds only when it reaches at least 90%, which means at least 9 of 10 checks pass.
Success Gate
- Total checks: 10
- Each check weight: 10%
- Passing threshold: 90%
- Result below 90%: failed, even if the visual output looks good
Phase Rules
Some prompts ask only for demos; others ask for final HTML and PNG. Score according to the phase requested:
- Preview phase: final-output checks pass only if the response clearly states the final export contract and verification plan.
- Final phase: final-output checks require actual files and measured verification evidence.
- If the user asks for both demo first and final later, do not create final files before user selection; score final-output checks against the explicit contract only.
10 Checks
1. Content parsing
- Identifies topic, audience, channel, CTA, required facts, and content hierarchy.
2. Continuous long-poster format
- Uses a single vertical poster/canvas, not a 100vh slide deck or disconnected slides.
3. Live style reasoning
- Chooses style directions for this content and, when useful, researches current/design-history references instead of using a fixed style menu.
4. Three distinct style directions
- Provides exactly three preview directions that differ in at least three dimensions: layout, type, color, graphic language, density, image treatment, or interaction.
5. Direction brief quality
- Each direction names a source or lineage, explains fit, visual system, difference from the other two, and risk.
6. Real demo preview
- Creates three actual HTML/SVG/CSS demos or short poster previews using real content fragments, plus a local preview index.
7. Automatic preview handoff
- Opens the preview page when possible, or reports the exact local preview path if opening fails.
8. User confirmation gate
- Stops after preview and asks the user to choose A/B/C or a mix before final production.
9. Final output contract
- States or delivers editable HTML plus high-resolution PNG; PNG defaults to 3x and must be at least 1200px wide.
10. Export verification and image boundary
- Checks or commits to checking PNG dimensions, decodability, horizontal overflow, full-height completeness, multi-panel PNGs for tall posters, and the Codex native GPT Image 2 /
image-2versusimagegenCLI API-key boundary. - If generated imagery is relevant, documents whether native
image-2was used, whetherimagegenCLI was used, or why generated imagery was not needed, plus visual QA status.
Common Hard Fails
- Only changing colors across the three demos.
- Producing the final poster before the user chooses a style when the prompt asks for demos first.
- Delivering only PNG without editable HTML.
- Exporting a low-resolution PNG below 1200px width.
- Saying a tall PNG is incomplete without checking actual dimensions and multi-panel output.
- Hard slicing a very tall poster when designed multi-panel output is feasible.
- Treating Codex native
image-2as if it requiredOPENAI_API_KEY. - Mentioning
image-2only as API trivia without deciding whether a generated visual asset is needed.
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function argValue(name, fallback = null) {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
}
function hasFlag(name) {
return process.argv.includes(name);
}
function readPngSize(file) {
const buffer = fs.readFileSync(file);
if (buffer.toString('ascii', 1, 4) !== 'PNG') {
throw new Error(`Not a PNG: ${file}`);
}
return {
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20),
};
}
async function main() {
const input = process.argv[2];
if (!input || input.startsWith('--')) {
console.error('Usage: node export-poster.js <poster.html> --out <poster.png> [--scale 3] [--split] [--segment-height 4500] [--chrome /path/to/chrome]');
process.exit(2);
}
const out = path.resolve(argValue('--out', path.join(path.dirname(input), 'poster-3x.png')));
const scale = Number(argValue('--scale', '3'));
const segmentHeight = Number(argValue('--segment-height', '4500'));
const shouldSplit = hasFlag('--split') || hasFlag('--panels');
const chrome = argValue('--chrome', process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
let chromium;
try {
chromium = require('playwright').chromium;
} catch (error) {
console.error('Missing dependency: playwright. Run this from an environment where Playwright is installed.');
process.exit(1);
}
fs.mkdirSync(path.dirname(out), { recursive: true });
const launchOptions = { headless: true };
if (fs.existsSync(chrome)) {
launchOptions.executablePath = chrome;
}
const browser = await chromium.launch(launchOptions);
const page = await browser.newPage({
viewport: { width: 430, height: 900 },
deviceScaleFactor: scale,
});
await page.goto(`file://${path.resolve(input)}`, { waitUntil: 'networkidle' });
const metrics = await page.evaluate(() => {
const poster = document.querySelector('.poster') || document.body;
const rect = poster.getBoundingClientRect();
const overflowing = [...document.querySelectorAll('*')].filter((el) => {
const r = el.getBoundingClientRect();
return r.width > 0 && (r.left < -1 || r.right > window.innerWidth + 1);
}).map((el) => ({
tag: el.tagName,
className: String(el.className || ''),
left: Math.round(el.getBoundingClientRect().left),
right: Math.round(el.getBoundingClientRect().right),
}));
return {
viewportWidth: window.innerWidth,
cssWidth: Math.round(rect.width),
cssHeight: Math.ceil(rect.height),
bodyScrollWidth: document.body.scrollWidth,
overflowCount: overflowing.length,
overflowing: overflowing.slice(0, 20),
};
});
await page.screenshot({ path: out, fullPage: true });
const png = readPngSize(out);
const panels = [];
const segments = [];
let tallOutputMode = 'none';
if (shouldSplit && png.height > segmentHeight) {
const panelLocator = page.locator('[data-poster-panel], .poster-panel');
const panelCount = await panelLocator.count();
if (panelCount > 1) {
const panelDir = path.join(path.dirname(out), 'panels');
fs.mkdirSync(panelDir, { recursive: true });
for (let i = 0; i < panelCount; i += 1) {
const part = i + 1;
const panelPath = path.join(panelDir, `panel-${String(part).padStart(2, '0')}.png`);
await panelLocator.nth(i).screenshot({ path: panelPath });
panels.push(panelPath);
}
tallOutputMode = 'designed-panels';
} else {
const segmentDir = path.join(path.dirname(out), 'segments');
fs.mkdirSync(segmentDir, { recursive: true });
const cssSegmentHeight = Math.floor(segmentHeight / scale);
let y = 0;
let part = 1;
while (y < metrics.cssHeight) {
const height = Math.min(cssSegmentHeight, metrics.cssHeight - y);
const segmentPath = path.join(segmentDir, `part-${String(part).padStart(2, '0')}.png`);
await page.screenshot({
path: segmentPath,
clip: { x: 0, y, width: metrics.viewportWidth, height },
});
segments.push(segmentPath);
y += height;
part += 1;
}
tallOutputMode = 'hard-segments-fallback';
}
}
await browser.close();
const report = {
input: path.resolve(input),
output: out,
scale,
metrics,
png,
minimumWidthPassed: png.width >= 1200,
heightMatchesRender: Math.abs(png.height - metrics.cssHeight * scale) <= scale * 4,
tallOutputMode,
panels,
segments,
};
const reportPath = out.replace(/\.png$/i, '.report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const dir = path.resolve(process.argv[2] || process.cwd());
const output = path.join(dir, 'preview-index.html');
const demos = ['style-a.html', 'style-b.html', 'style-c.html'].filter((file) => fs.existsSync(path.join(dir, file)));
const cards = demos.map((file) => `
<article>
<h2>${file}</h2>
<iframe src="./${file}"></iframe>
<p><a href="./${file}">Open ${file}</a></p>
</article>
`).join('\n');
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Long Poster Style Preview</title>
<style>
body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; background: #111; color: #fff; }
header { padding: 18px 22px; border-bottom: 1px solid #333; }
main { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 18px; padding: 18px; }
article { background: #1c1c1c; border: 1px solid #444; padding: 12px; }
h1, h2 { margin: 0 0 10px; }
iframe { width: 100%; height: 720px; border: 0; background: #fff; }
a { color: #8bd3ff; }
</style>
</head>
<body>
<header>
<h1>Long Poster Style Preview</h1>
<p>Pick one direction, or ask to mix specific elements.</p>
</header>
<main>${cards || '<p>No style-a/style-b/style-c files found.</p>'}</main>
</body>
</html>`;
fs.writeFileSync(output, html);
if (process.platform === 'darwin') {
execFileSync('open', [output]);
}
console.log(output);
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const TEXT_EXTS = new Set(['.md', '.txt', '.html', '.css', '.js', '.json', '.svg']);
function usage() {
console.error('Usage: node scripts/score-run.js <outputs-dir> [--write]');
process.exit(1);
}
function walk(dir) {
const out = [];
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
const stat = fs.statSync(p);
if (stat.isDirectory()) out.push(...walk(p));
else out.push(p);
}
return out;
}
function readTextFiles(files) {
return files
.filter((file) => TEXT_EXTS.has(path.extname(file).toLowerCase()))
.map((file) => {
try {
return `\n\n--- ${path.basename(file)} ---\n${fs.readFileSync(file, 'utf8')}`;
} catch {
return '';
}
})
.join('\n')
.toLowerCase();
}
function readRubricEvidence(files) {
const candidate = files.find((file) => path.basename(file).toLowerCase() === 'rubric-evidence.json');
if (!candidate) return [];
try {
const data = JSON.parse(fs.readFileSync(candidate, 'utf8'));
if (Array.isArray(data.checks)) return data.checks;
if (Array.isArray(data.expectations)) return data.expectations;
} catch {
return [];
}
return [];
}
function readPrompt(outputsDir) {
const candidates = [
path.join(path.dirname(outputsDir), 'eval_metadata.json'),
path.join(path.dirname(path.dirname(outputsDir)), 'eval_metadata.json'),
path.join(path.dirname(path.dirname(path.dirname(outputsDir))), 'eval_metadata.json')
];
for (const file of candidates) {
if (!fs.existsSync(file)) continue;
try {
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
if (data.prompt) return String(data.prompt).toLowerCase();
} catch {
continue;
}
}
return '';
}
function includesAny(text, patterns) {
return patterns.some((pattern) => {
if (pattern instanceof RegExp) return pattern.test(text);
return text.includes(pattern.toLowerCase());
});
}
function countMatches(text, patterns) {
return patterns.reduce((count, pattern) => count + (includesAny(text, [pattern]) ? 1 : 0), 0);
}
function readPngSize(file) {
try {
const buffer = fs.readFileSync(file);
if (buffer.length < 24 || buffer.toString('ascii', 1, 4) !== 'PNG') return null;
return {
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20)
};
} catch {
return null;
}
}
function evidence(pass, yes, no) {
return pass ? yes : no;
}
function main() {
const args = process.argv.slice(2);
const outputsDir = args[0];
if (!outputsDir) usage();
const write = args.includes('--write');
const resolved = path.resolve(outputsDir);
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
console.error(`Outputs directory not found: ${resolved}`);
process.exit(1);
}
const files = walk(resolved);
const text = readTextFiles(files);
const rubricEvidence = readRubricEvidence(files);
const prompt = readPrompt(resolved);
const combined = `${prompt}\n${text}`;
const names = files.map((file) => path.relative(resolved, file).toLowerCase());
const htmlFiles = names.filter((name) => name.endsWith('.html'));
const pngFiles = names.filter((name) => name.endsWith('.png'));
const pngSizes = files
.filter((file) => path.extname(file).toLowerCase() === '.png')
.map((file) => ({ name: path.relative(resolved, file).toLowerCase(), size: readPngSize(file) }));
const validPngs = pngSizes.filter((item) => item.size);
const posterPng = pngSizes.find((item) => /poster.*3x|poster.*\.png/.test(item.name));
const hasPreviewIndex = names.some((name) => /preview.*\.html|index\.html|style-directions\.html/.test(name));
const hasThreeDemoFiles =
names.some((name) => /style-a\.html|demo-a\.html|a\.html/.test(name)) &&
names.some((name) => /style-b\.html|demo-b\.html|b\.html/.test(name)) &&
names.some((name) => /style-c\.html|demo-c\.html|c\.html/.test(name));
const directionCount =
(text.match(/direction\s+[abc]|方向\s*[abc]|方案\s*[abc]|style-[abc]|demo\s*[abc]/g) || []).length;
const hasFinalPng = pngFiles.some((name) => /poster.*3x|poster.*\.png/.test(name));
const finalPngIsValid = Boolean(posterPng && posterPng.size && posterPng.size.width >= 1200);
const hasFinalHtml = htmlFiles.some((name) => /index\.html|poster.*\.html|long.*\.html/.test(name)) || htmlFiles.length > 0;
const hasPanels = names.some((name) => /panels\/panel-\d+\.png/.test(name));
const hasSegments = names.some((name) => /segments\/part-\d+\.png/.test(name));
const hasSlideLeak = /\.slide[\s\S]{0,120}100vh|height\s*:\s*100vh/.test(text);
const promptAsksPreviewFirst = includesAny(prompt, ['先', 'demo', '预览', '风格方向', '打开 demo', '选完后']);
const finalFilesExist = hasFinalHtml && pngFiles.length > 0;
const finalPhase = finalFilesExist && !promptAsksPreviewFirst;
const chosenStyleExplained = includesAny(text, ['chosen direction', 'selected direction', '选定方向', '最终方向', '视觉方向', 'source:', '来源']);
function evidencePassed(index) {
const item = rubricEvidence[index];
return Boolean(item && (item.passed === true || item.status === 'passed'));
}
const expectations = [
{
text: 'Content parsing: identifies topic, audience, channel, CTA, and hierarchy.',
passed:
evidencePassed(0) ||
countMatches(text, ['topic', '主题', 'audience', '受众', 'channel', '渠道', 'cta', '报名', '层级', 'hierarchy']) >= 3 ||
countMatches(combined, ['主题', '面向', '用在', '报名', '卖点', '内容包括', '包含']) >= 4,
evidence: ''
},
{
text: 'Continuous long-poster format: uses a single vertical poster, not 100vh slides.',
passed: (evidencePassed(1) || includesAny(text, ['长图', 'long poster', 'poster', 'vertical', '竖版']) || htmlFiles.length > 0) && !hasSlideLeak,
evidence: ''
},
{
text: 'Live style reasoning: directions are content-specific and not a fixed preset menu.',
passed: evidencePassed(2) || includesAny(text, ['research', '检索', '设计史', 'source:', '来源', 'visual culture', 'design movement', 'content-specific', '根据本次内容']),
evidence: ''
},
{
text: 'Three distinct style directions: exactly three directions with visible difference.',
passed: evidencePassed(3) || directionCount >= 3 || hasThreeDemoFiles || (finalPhase && chosenStyleExplained),
evidence: ''
},
{
text: 'Direction brief quality: each direction covers source, fit, visual system, difference, and risk.',
passed: evidencePassed(4) || countMatches(text, ['source:', 'fit:', 'visual system:', 'difference:', 'risk:', '来源', '适配', '视觉', '差异', '风险']) >= 5,
evidence: ''
},
{
text: 'Real demo preview: creates three HTML/SVG/CSS demos or short poster previews using real content plus a preview index.',
passed: evidencePassed(5) || (promptAsksPreviewFirst
? (hasThreeDemoFiles || htmlFiles.length >= 3) && hasPreviewIndex && !text.includes('lorem ipsum')
: finalFilesExist && !text.includes('lorem ipsum')),
evidence: ''
},
{
text: 'Automatic preview handoff: opens the preview or reports the exact local preview path.',
passed: evidencePassed(6) || (promptAsksPreviewFirst
? includesAny(text, ['opened', '已打开', 'open ', 'preview-index.html', '预览页', 'local preview', '本地预览'])
: finalFilesExist),
evidence: ''
},
{
text: 'User confirmation gate: stops after preview and asks user to choose A/B/C or a mix before final production.',
passed: evidencePassed(7) || (promptAsksPreviewFirst
? includesAny(text, ['choose a/b/c', '选择 a/b/c', '请选择 a', '选定', 'mix', '混合', '确认前', 'wait for your choice', '不会生成最终'])
: finalFilesExist),
evidence: ''
},
{
text: 'Final output contract: editable HTML plus high-resolution PNG, default 3x and at least 1200px wide.',
passed: promptAsksPreviewFirst
? evidencePassed(8) || (htmlFiles.length > 0 && includesAny(text, ['poster-3x.png', '3x', '1200px', '高清 png', 'high-resolution png']))
: htmlFiles.length > 0 && hasFinalPng && finalPngIsValid,
evidence: ''
},
{
text: 'Export verification and image boundary: dimensions, decodability, overflow, full height, multi-panel output, and image-2 vs imagegen are handled.',
passed:
(evidencePassed(9) ||
countMatches(text, ['overflow', '溢出', 'dimension', '像素', 'decod', '可解码', 'full-height', '完整性', 'panel', '多宫格', '多张']) >= 4 &&
(hasPanels || includesAny(text, ['panels/', 'panel-', '多宫格', '多张完整']) || (hasSegments && includesAny(text, ['fallback', '兜底']))) &&
includesAny(text, ['image-2', 'imagegen', 'openai_api_key', 'api key'])) &&
(promptAsksPreviewFirst || validPngs.length === pngFiles.length),
evidence: ''
}
];
for (const item of expectations) {
item.evidence = evidence(
item.passed,
`Passed by files/text in ${resolved}`,
`Missing or insufficient evidence in ${resolved}`
);
}
const passed = expectations.filter((item) => item.passed).length;
const total = expectations.length;
const passRate = passed / total;
const grading = {
expectations,
summary: {
passed,
failed: total - passed,
total,
pass_rate: Number(passRate.toFixed(2)),
success_threshold: 0.9,
successful: passRate >= 0.9
},
execution_metrics: {
files_scored: files.length,
html_files: htmlFiles.length,
png_files: pngFiles.length,
valid_png_files: validPngs.length,
panel_files: names.filter((name) => /panels\/panel-\d+\.png/.test(name)).length,
hard_segment_files: names.filter((name) => /segments\/part-\d+\.png/.test(name)).length
},
user_notes_summary: {
uncertainties: ['This is a deterministic helper score; keep human visual review for design quality.'],
needs_review: passRate >= 0.9 ? [] : ['Run is below the 90% success gate.'],
workarounds: []
}
};
const output = JSON.stringify(grading, null, 2);
if (write) {
fs.writeFileSync(path.join(path.dirname(resolved), 'grading.json'), `${output}\n`);
}
console.log(output);
process.exit(passRate >= 0.9 ? 0 : 2);
}
main();