Visual Prompt Forge
SkillMediaGenerate model-specific prompts from shots.json. Outputs copy-paste-ready prompts for stills (Midjourney, Flux, Ideogram, GPT Image, Nano Banana, Seedream) and motion video (Kling, Veo, Seedance, Hailuo). Also runs a revision mode that reads a critique.json and re-emits prompts for only the failed shots, closing the QA loop. Use when the user asks for image or video prompts, mentions any of those generators, wants AI-generated frames for a storyboard, or hands over shots.json. The prompt half of the pipeline. Composes with storyboard-architect upstream, visual-asset-critic downstream.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Visual Prompt Forge skill
What this skill tells your AI
The instructions your AI receives, as published by whystrohm/shotkit in skills/visual-prompt-forge/SKILL.md and read by ahel’s review.
You are turning structured shot data into prompts that work in production. Each image generator rewards a different prompting style, short and high-signal for Midjourney, natural-language for Flux, paragraph-form for GPT Image, text-aware for Ideogram. A prompt that crushes in one will produce slop in another.
This skill adapts. Same shot, different syntax.
When to use
Trigger when the user:
- Hands over a
shots.json(or any structured shot list) and asks for prompts - Names a specific generator (Midjourney, Flux, Ideogram, GPT Image, Nano Banana, Seedream, Kling, Veo, Seedance, Hailuo)
- Asks for "image prompts," "Midjourney prompts," "AI prompts," "generation prompts" for a storyboard
- Wants the same shot adapted to multiple generators
If the user wants to build a storyboard from scratch (no shots.json yet), use storyboard-architect first, then chain into this skill.
What you produce
For a given shots.json and a list of target generators, produce one file per generator,
inside a directory named for the round:
output/prompts/round-1/
├── midjourney.txt # If targeted
├── flux.txt
├── ideogram.txt
├── gpt-image.txt
├── nano-banana.txt
├── seedream.txt
├── kling.txt # Motion-aware video, default
├── veo.txt # Motion, dialogue/lipsync + native audio
├── seedance.txt # Motion, multi-shot sequences
└── hailuo.txt # Motion, budget iteration
Round 1 is the first pass. Revision mode writes output/prompts/round-2/, and so on.
The round in the path is not decoration: prompt files used to be written to one fixed
path per generator, so round 2 destroyed round 1 and the prompt that actually produced
most of the surviving frames was gone.
Each file is plain text, one prompt per shot, separated by a blank line and a # shot_NN comment. Designed for copy-paste workflows, drop into the generator's UI or pipe into an API.
The five-layer prompt anatomy
Every prompt is composed from these layers. Read references/prompt-anatomy.md for the full theory. Quick version:
- Brand Lock, palette, type, mood, "never" list (constant across project)
- Series Lock, character/environment/lighting anchors (constant across storyboard)
- Shot Spec, framing, angle, motion, subject (per shot)
- Text Layer, never in the prompt, composited separately
- Generator Adapter, model-specific syntax wrapper
The first four come from shots.json and the brand-lock. The fifth is what this skill applies.
Workflow
Step 1. Read inputs
You need:
shots.json(required), the structured shot listbrand-lock.snapshot.md(required), referenced from shots.json- Target generators (required), ask if not specified
Validate before composing:
python tools/validate_shots.py output/
If the brand-lock is missing or shots.json does not validate, stop and tell the user.
Don't try to forge prompts from incomplete data.
If tools/ is not on hand (a Claude.ai upload, or a single-skill install), read the
schema from ../storyboard-architect/templates/shots.schema.json and check by hand. That
relative path only resolves when the skills sit side by side; when they don't, ask the
user for the schema rather than composing from memory of it.
Step 2. Pick the adapters
For each target generator, read the matching adapter file:
adapters/midjourney.mdadapters/flux.mdadapters/ideogram.mdadapters/gpt-image.mdadapters/nano-banana.mdadapters/seedream.mdadapters/kling.md(motion video, default)adapters/veo.md(motion video, dialogue/lipsync + native audio)adapters/seedance.md(motion video, multi-shot sequences)adapters/hailuo.md(motion video, budget iteration)
Each adapter file documents the prompting style, parameter syntax, and known pitfalls for that generator. You must read the adapter before writing prompts for it. Don't guess from training data, image-gen syntax has churned multiple times.
adapters/_capabilities.json is the single source of truth for per-generator limits (max_prompt_words, supports_text_render, supports_motion, aspect_param, and so on). Read it once at the start and respect those values when composing, and do not target motion on a stills-only generator.
max_prompt_words is a ceiling. The range in an adapter .md is the recommended target
and always sits inside that ceiling, so a .md saying "40 to 70 words" under a ceiling of
120 is guidance, not a conflict. Where a fact in a .md and a fact in the JSON genuinely
disagree, the JSON wins.
That rule is now enforced rather than trusted. tools/validate_capabilities.py fails the
build when an adapter advertises more words than its ceiling, or when an adapter never
documents the aspect_param the JSON tells you to send. The second check exists because
nano-banana's matrix entry said aspect_ratio while its adapter said the API expects
aspectRatio; the precedence rule meant the wrong one won, silently, on every prompt.
Step 3. Compose per shot
For each shot in shots.json, for each target generator:
- Pull brand-lock palette, mood, "never" list
- Pull series_lock character/environment/lighting
- Pull shot framing/angle/motion/subject
- Strip any on_screen_text reference, text never goes in the prompt
- Apply the generator adapter's syntax wrapper
- Append generator-specific parameters (aspect ratio, style flags, seed if applicable)
Step 4. Write output files
One file per generator, into output/prompts/round-{N}/. Format:
# Storyboard: {project title}
# Generator: midjourney
# Model: {model_version from _capabilities.json}
# Aspect: 9:16
# Brand-lock: brand-lock.snapshot.md
# Run: {run_id from run.json}
# Round: 1
# shot_01, hook, 0.0-2.0s, MCU eye-level static
{the prompt}
# shot_02, pain, 2.0-6.0s, MS eye-level push
{the prompt}
...
The # lines are comments; the user copies just the prompt body. tools/copy-prompt.py
parses this format, treating a comment line that names a shot as a block header and any
other comment inside a block as an annotation it will not copy.
Record the run and round in the header, not a wall-clock "Generated" line. A timestamp in the header made every file differ between two otherwise identical runs, which is a strange thing to put in an artifact whose selling point is determinism.
Step 4b. Append the round to run.json
After writing the files, append an entry to run.json's rounds array: the round number,
started_at, a reason, and for each file its generator, path, SHA-256, and the shot ids
it covers.
shasum -a 256 output/prompts/round-1/*.txt
This is the only writeable part of run.json. Everything else was fixed when the
architect wrote it.
Step 5. Hand off
Tell the user where the files are. Offer the next step:
"Want me to QA the generated images against the storyboard? Use
visual-asset-criticonce you have the renders."
For paste-into-generator workflows, the user can pipe individual shots to the clipboard with the bundled helper:
python tools/copy-prompt.py output/prompts/round-1/midjourney.txt
python tools/copy-prompt.py output/prompts/round-2/revised-midjourney.txt --shot shot_03
This is optional. The .txt files are also directly readable, and the user can copy any block by hand. The helper exists for the case where the operator is bouncing between the terminal and a generator UI repeatedly.
Revision mode (closing the QA loop)
This is what visual-asset-critic's structured output is for. When the user hands you shots.json plus one or more critique.json files (the machine-readable verdict the critic writes), don't re-forge the whole storyboard, re-emit prompts for only the shots that failed, with the fix already applied.
Trigger
The user says "apply the critique", "revise the failed shots", "re-roll what didn't pass", or hands over an output tree containing critiques/.
Workflow
- Read every critique under
output/critiques/round-{N}/, where N is the highest round present. Each file is one shot's verdict (shot_id,verdict,issues[]). - Skip any with
verdict: ACCEPT. Those are done. - Stop on
REJECT. A REJECT means the critic found a blocking issue, or three or more major ones: a failure with no clear fix path. Re-emitting a prompt for it pretends otherwise. List the rejected shots, say what the critic said about them, and ask the user how to proceed. Common answers are a change to the shot spec, a change to the brand-lock, or a different generator, and all three are decisions above this skill's pay grade. - For every
REVISEshot, walk itsissues[]and branch onfix_type:prompt-level, recompose that shot's prompt with the change infixapplied (e.g. add the missing series_lock anchor). Re-emit it.re-roll, keep the prompt identical; the generation was just a bad sample. Re-emit it with a# fix [Technical, re-roll]annotation saying to take 2-3 samples and pick the cleanest.post-level, do not re-emit. The fix happens in compositing, not a new generation.
- A shot whose issues are all
post-levelneeds no new prompt. Leave it out of the revised file and add its id topost_only_shotson the new round entry inrun.json. Saying it in chat is not recording it: that obligation has to exist on disk or the compositing step is a memory. - Re-apply the five-layer anatomy and the same adapter as the original run.
Output
Write output/prompts/round-{N+1}/revised-{generator}.txt containing only the revised
shots, then append the round to run.json. Annotate each shot with what changed and why,
citing the issue:
# shot_03, reframe, 11.0-16.0s, MCU eye-level push, revision (was REVISE)
# fix [Series Lock, major]: added 'salt-and-pepper hair' to the character anchor (was missing)
# fix [Shot Spec, minor]: medium shot -> medium close-up
{the revised prompt}
The block header leads with the shot id, same as a full pass. tools/copy-prompt.py
identifies a block by the shot id near the start of the line, so a header that led with
"Revision of" produced a file the paste helper could not read at all, which is
inconvenient in the one file the operator is about to paste from repeatedly.
Tell the user which shots were revised, which need only post work, which were rejected,
and which were already ACCEPT. Then they generate the revised shots and run
visual-asset-critic again.
Determinism, and its limits
Given the same shots.json, the same brand-lock, and the same critique, this should
produce the same revised prompt. Nothing in the file format fights that any more: no
wall-clock stamps, no random ordering.
What the format cannot guarantee is the judgement in between. Applying a fix field means
reading a sentence of English and editing prose, so hold yourself to the narrowest edit
that satisfies the fix and leave every other layer byte-identical. If you find yourself
rewriting a prompt the critique did not ask you to touch, stop: that is drift, and it will
read as a mystery six shots later.
Hard rules
These are non-negotiable. Violating them produces broken output even if the prompt looks fine.
Rule 1. Text is never in the prompt
If the shot has on_screen_text: "text_03", the prompt does NOT contain the text content. Text is composited separately. The only exception: Ideogram, where text-in-image is the reason you'd choose it, but even then, treat it as an explicit override flagged in rationale.
Rule 2. Brand colors never in shot subject prose
Colors come from the series_lock color_grade and the brand_lock palette. They get rendered into the prompt by the adapter. Don't write "deep navy blazer" in the shot subject if "deep navy" is already in the palette, that's a duplicated description and produces oversaturation.
Rule 3. Series_lock anchors are verbatim
The series_lock environment, lighting, and color_grade strings flow into every
prompt verbatim. This is what produces visual consistency across shots. If you
paraphrase or vary, shots stop matching each other.
Verbatim means the whole string, unedited. Not "the same idea in better prose". The
failure mode is specific and easy to walk into: you are writing fluent descriptive
English, environment is "minimalist home office, white walls, oak desk, single
houseplant", and it reads more naturally as "a minimal home office with white walls and
an oak desk". That is drift. The frames stop matching, and it stays invisible until you
line up six shots and see six different rooms.
Capitalising the first letter to start a sentence is fine, because the check is case-insensitive. "Minimalist home office, white walls, oak desk, single houseplant." satisfies the rule and reads like a sentence. Nothing else may change: no reordering, no dropped clause, no synonym, no inserted adjective.
character is a warning rather than an error, because a shot with no person in it can
legitimately leave it out. When the shot has a person, it is verbatim too.
This rule is enforced now, not trusted:
python tools/validate_prompts.py output/
It exists because a careful authoring pass over a seven-shot storyboard drifted on these anchors in all seven shots while every other validator stayed green.
Rule 4. Adapters are the source of truth on syntax
If your training data says Midjourney uses --style 4a and the adapter file says --style raw, the adapter wins. Image-gen syntax changes monthly. The adapter is current; your training is not.
Rule 5. Prompts must be reproducible
Every prompt is composed from the same inputs the same way. If two consecutive runs produce different prompts for the same shot, the skill is broken. Determinism is the whole point.
Reference files
references/prompt-anatomy.md, the five-layer model in depthreferences/consistency-locks.md, how series_lock prevents shot driftreferences/failure-modes.md, common image-gen failures and their prompt-side fixes
Adapters
One file per generator. Read these on demand, only for the generators being targeted.
| File | Generator | Strength |
|---|---|---|
adapters/midjourney.md | Midjourney v7+ | Aesthetic, cinematic |
adapters/flux.md | Flux 2 / Flux 1.1 Pro | Photorealism |
adapters/ideogram.md | Ideogram v3 | Text in image (override only) |
adapters/gpt-image.md | GPT Image 1.5 / 2 | Prompt accuracy, spatial reasoning |
adapters/nano-banana.md | Gemini 2.5 Flash Image | Edit fidelity, inpainting |
adapters/seedream.md | Seedream 4.5 | High-volume, cost-efficient |
adapters/kling.md | Kling 3.0 | Motion video, default (best camera motion per dollar) |
adapters/veo.md | Veo 3 | Motion video, dialogue/lipsync + native audio |
adapters/seedance.md | Seedance 2.0 | Motion video, multi-shot sequences |
adapters/hailuo.md | Hailuo 02 Pro | Motion video, budget iteration |
Quality bar
Run the validator. Do not eyeball this list.
python tools/validate_prompts.py output/
python tools/copy-prompt.py output/prompts/round-1/flux.txt --list
validate_prompts.py checks the mechanical half, which is everything that used to be a
checkbox here:
- the header names the storyboard, generator, aspect, brand-lock, run, and round
- the generator is a real id in
_capabilities.json - the header aspect matches
project.aspect - no prompt exceeds that generator's
max_prompt_wordsceiling - a full pass covers every shot; a revision file covers a subset of real shots
- no shot block is duplicated, and none has a header with no body
- Rule 1: no on-screen text copy appears in any prompt
- Rule 3:
environment,lighting, andcolor_gradeappear verbatim, with the character anchor as a warning
What it cannot check, and you still have to:
- One output file per requested generator
- On a revision pass, the shots you left out are accounted for as ACCEPT, post-only, or rejected, and you said which is which
- Generator-specific parameters are right for the surface the user will paste into
- The round is appended to
run.jsonwith a hash per prompt file - The prompt actually describes the shot, which is the part no validator will ever do
Examples
examples/one-shot-all-adapters/ contains a single shot rendered across seven adapters
side-by-side: the six stills generators plus Kling. Use it to calibrate output quality.
The three remaining motion adapters (Veo, Seedance, Hailuo) have no worked example yet;
their .md files carry a worked prompt each in the meantime.
For a complete two-round output tree, prompts and frames and critiques together, see
../visual-asset-critic/examples/worked-run/.
Signals
- GitHub stars
- 20
- Forks
- 5
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
visual-prompt-forge- Source
- github.com/whystrohm/shotkit