18 KiB
name, description
| name | description |
|---|---|
| h3-video-production | Render approved shot plans on the private MiniMax H3 ComfyUI fleet with `uv run piecesai h3`, then assemble, verify and derive the delivery. Load it at two points: early, to preflight a brief's duration, aspect ratio, spoken language and frame rate before a shot plan is built on them, and again once an official creative Skill has produced an approved prompt and the user authorises generation. Not for replacing the creative workflow, skipping its confirmation gates, or diagnosing a broken worker. |
H3 Video Production
Repository-local execution companion to the untouched official MiniMax H3 Skills. The official Skill owns the brief, shot plan and prompt. This Skill owns submission, delivery post-processing, and the evidence trail.
Everything runs through uv run piecesai h3. Never call a ComfyUI endpoint
directly and never hard-code a provider URL — the fleet addresses live in
MINIMAX_H3_BASE_URLS and the CLI owns worker selection.
Fleet
Four ComfyUI workers: one RTX 5090 (remote) and three RTX 4090s (local).
uv run piecesai h3 workers reports each one's status, device and queue depth.
- Mode: Ref2VA only. Pass
--h3-mode ref2vaexplicitly.autois not equivalent:infer_h3_moderesolves it by reference count (0 →t2va, 1 →i2va, 2 →fl2va, 3+ →ref2va), and theu06workflow profile this Skill always renders on only supportsref2va. With--h3-mode autoand one or two--referenceflags, the render fails withLightX2V profile u06 does not support i2va(orfl2va).autoonly lands onref2vaby itself once you supply three or more references. - References: at least one required.
--referencetakes a local path and uploads it; a public URL is not needed. Because--h3-mode ref2vais passed explicitly (see above), one or two references work fine —auto's three-reference floor does not apply when the mode is explicit. - Cost knob:
--megapixels(default1.03, range0.1–1.03). The cap was lowered from1.5; values above1.03are rejected at validation.--resolutiononly selects which aspect the canvas derives from —--resolution 256pat default megapixels still renders full cost. - Super-resolution: only the 5090 carries the node. Off by default.
- Audio: H3 renders native audio. Keep it; standardise only at delivery.
Render shots in parallel
One render call occupies one worker for the length of that shot, so a
sequential loop over N shots leaves the rest of the fleet idle.
Use batch rather than hand-rolling concurrency:
uv run piecesai h3 batch --jobs jobs.json --project-id <proj_id> [--dry-run]
jobs.json is a list of clip jobs:
[{"clip": "P03_S01_C1", "group": "P03_S01",
"prompt_file": "/abs/prompts/video/P03_S01_C1.txt",
"refs": ["/abs/plate.png", "/abs/anchor.png"],
"duration": 5.0, "megapixels": 1.03}]
Batch pins one worker per concurrent task, benches a worker that fails twice in
a row and requeues its work, skips clips whose request fingerprint is unchanged,
and joins each group once its clips are complete and agree on frame size.
--dry-run prints the plan without touching a worker.
The fingerprint reads the contents of the prompt file and the references,
not their paths, so a rewritten prompt or an edited anchor is different work.
It hashed filenames until 2026-08-20, when six clips were rewritten to fix a
product-lock violation and every one came back skip: unchanged. After editing
prompts, --dry-run and confirm the clips you changed say would render.
megapixels decides the canvas — 1.03 renders 768x1344 — so every clip
in one group must use the same value. Mixing them is refused at assembly
rather than silently rescaled.
Loudness normalisation and cover extraction stay outside batch: they depend on
platform safe areas and delivery targets, which belong to the campaign rather
than the runtime. scripts/finalize_audio.py is the loudness pass.
Captions ship no tool at all. post has verify, assemble, frames and
derive and nothing else; there is no subtitle burn-in anywhere in this
repository. When a brief needs burned-in subtitles, that is hand-written ffmpeg
work — say so at the brief instead of treating it as a step that already exists.
Comparing settings: change the seed
A render whose request fingerprint matches a completed run resumes from that run's receipt and returns the old file in seconds. Correct for resuming a batch, wrong for an A/B: two configurations at the same seed can come back byte-identical while the timings suggest the settings were free.
Give each comparison round its own --seed, then confirm the outputs actually
differ before reading anything into them:
ffmpeg -v error -i a.png -f rawvideo -pix_fmt rgb24 - | cmp -s - <(...)
A mean channel difference near zero means the comparison never happened. This
has already cost two rounds — references/render-settings-evidence.md.
Steps and the turbo LoRA
The default is the v4 step600 turbo LoRA at strength 1.0, 8 sampling steps.
Do not turn it off and do not raise steps without a reason from the shot in
front of you: --no-turbo-lora --sampling-steps 14 costs about 30% more
wall-clock per clip and was measured as no better.
Strength is tuned for 1.0. Raise toward 1.2 against ghosting and smear, lower toward 0.8 against over-sharp grain. Stay inside 0.0-2.0.
# only when a specific shot argues for it
uv run piecesai h3 render ... --lora-strength 1.2 # ghosting on fast motion
uv run piecesai h3 render ... --no-turbo-lora --sampling-steps 14 # slower, not better
Do not judge motion with a sharpness metric. Motion coherence needs eyes: render the variants, show them, let the owner pick.
The four-round selection, the measured timings and why the sharpness proxy was
rejected are in references/render-settings-evidence.md. Its numbers answer to
the comment block above U06_V4_SAMPLING_STEPS in
src/piecesai/generators/minimax_h3.py, which is the authority when they
disagree.
Preflight the brief — before the shot plan, not before the render
The official creative Skills settle duration, aspect ratio, language and beat timing in their first step, then build a shot plan on top. Several of those answers are constrained by the renderer, and each is cheap to change at the brief and expensive to change afterwards. Check them the moment they are given:
uv run piecesai h3 preflight \
--aspect-ratio 9:16 --duration 30 --language English \
[--clip-duration 5] [--planning-fps 30] [--separate-audio narration] [--json]
Exit 2 means a FAIL that no render will survive. WARN findings are soft gates:
put the trade-off to the user, do not silently resolve it.
The command owns the values; this section owns the reasons. Do not restate its lists here — a stale copy of a supported-language list or a megapixel ceiling is exactly how the LoRA section above went three commits out of date. Run it and read what it prints.
The traps it exists to catch:
- Aspect. H3 renders two aspects. The official Skills offer five in their
intake, and the other three are delivery aspects only — reachable through
post derive, which refuses a crop that would discard the composition. Plan the shots for a render aspect; treat anything else as a derived cut, never as a promise. - Duration. One clip is 5-15 s, so a 30 s promo is several clips. Preflight splits the total evenly instead of taking clips off the front and leaving a short tail, because that tail is rejected at render after the other clips have been paid for.
- Frame rate. H3 renders at 24 fps (`frame_count = max(5, round(duration
- 24))`). Several official Skills plan beats at 30 fps and count transition overlaps in frames. Those boundaries land on the wrong grid — re-express beats in seconds, or replan on a 24 fps grid.
- Spoken language. H3 performs the words in
<d>…</d>itself and is stable in a fixed set of languages; outside it the vendor says only "supported to varying degrees", which is an untested render rather than a refusal. The limit covers spoken and sung lines only — the prompt body stays English either way, and on-screen text is a glyph problem, not a speech one.references/spoken-language-support.mdholds the list, the<d>tag values, the failure modes to listen for and the probe command. - Assets with no local equivalent. No TTS, no standalone music generation, no caption burn-in. A separately editable narration track cannot be delivered; spoken lines and score are performed by H3 inside the clip. Say so at the brief rather than promising a track that has no tool behind it.
None of this replaces the calling Skill's own confirmation gate. It supplies the constraints that gate should be confirming against.
Write the prompt first — mandatory gate
Do not call render with a prompt you wrote freehand. Load
h3-prompt-writing and write the prompt in H3's own Ref2VA structure first.
That skill is not optional styling; its structure is what keeps a render
faithful to its references.
Ref2VA prompts have six sections, in this order:
subject_definitions · summary · retention_analysis ·
detailed_description · overall_soundscape · non_diegetic_music
Read h3-prompt-writing/references/ref-en.txt for the label rules and a
complete example. Two parts of that structure do real work and are the
reason this gate exists:
subject_definitionsforces you to name what each reference actually contributes — an identity, an environment, a composition anchor. A product photo cited as<Subject 1>behaves differently from the same file cited as<Picture 1>first frame.retention_analysisforces one line per reference stating whether it isfully_preserved,partially_preserved,attribute_transferorweak_reference, and in which shots. Writing that line is what surfaces a contradiction before you spend a render on it.
<Picture N> entries carry no sources, and no gate should compare their
source set. A picture's source is itself, so sources: <Picture 1> is a
tautology — and the one thing a picture definition must do is say who is in that
frame, which forces it to mention that the same person also appears in
<Picture 2>. The set becomes [1,2] and the entry is rejected for a
contradiction that the format invented. sources belongs to <Subject N>;
h3-prompt-writing/references/ref-en.txt never gives pictures one.
Getting this wrong is expensive and not obviously the instruction's fault: two
different model tiers burned five attempts on it in one session. The same
error from two model tiers means the instruction is ambiguous, not that the
model is too weak — read the failing constraint and ask whether it carries any
information at all. Full account in references/failure-modes.md. The only
cross-image identity that genuinely needs strict checking is <Subject j>
narrowing: a character losing identity across shots.
Freehand prose skips both checks, and this repository has the re-renders to show for it in the same file.
Keep the written prompt next to the render — it is the record of what was asked for, and the starting point when a shot has to be done again.
Never write "no X" — it draws X
When a render puts an unwanted object in frame, the instinct is to name the
object and forbid it. That reliably makes it worse: the text conditioning does
not parse negation, so no jug and jug enter cross-attention as the same
token. Negating an attribute (no lip) is safe; negating a whole object noun
is what backfires.
To exclude an object, do not mention it. Occupy the space it would fill:
-
Separate the confusable classes into their own Subjects and distinguish them by form, not by count — "the only vessel in this video that has a pouring lip" beats "no second pitcher".
-
State counts positively:
the whole table holds five glass objects in total,exactly one stream of liquid is visible at any moment. -
Make the causal chain checkable — keep source and target in the same frame so the render can be verified rather than argued about.
-
Give a countable set a container sized to exactly that count. Four cups that must stay four go on a board defined as four cups long and one cup deep, filled end to end. A physical boundary outperforms every counting phrase tried here, because it leaves nowhere for an extra one to stand.
It holds only while the whole container stays in frame. Any camera move that crops an end re-opens the space beyond it and the count drifts again — the shots that kept their board ends inside the frame rendered the right number on the first try, and the one that cropped them needed three attempts and a fixed wide frame before it agreed. When a shot has to assert a count, hold one frame that contains the whole container with bare ground visible beyond both ends, and let something else in the film carry the camera movement.
This applies to the boilerplate too. A retention_analysis line reading
no spoon, ladle, chopstick or peeler merges with it carries the same risk and
should be rewritten the same way.
The render that poured a pitcher into another pitcher is in
references/failure-modes.md.
Render
uv run piecesai h3 init --name "<project name>"
uv run piecesai h3 render \
--project-id <proj_id> --title "<shot title>" --skill <calling-skill-name> \
--prompt-file <path> \
--reference <character.png> --reference <scene.png> \
--aspect-ratio 16:9 --duration 5.0 --resolution 768p \
--h3-mode ref2va --megapixels 1.03
Each render writes generations/<run_id>/ under the project, holding
manifest.json, references/, prompts/, parts/, receipts/, qa/ and
finals/. The finished master is finals/final_master_<slug>.mp4. That
directory is the record — keep it.
Validate the riskiest shot first at a low --megapixels before committing
the whole film at full cost. What "riskiest" means comes from the calling
Skill's own shot table.
A cheap probe validates motion, structure and composition — not how many
objects appear. Object count is a function of canvas size: at a low
megapixel tier the frame is small and the model fills it with what you asked
for, and at 1.03 the same prompt has more room and puts more things in it.
A shot that must contain exactly N of something is only proven at the
megapixel tier it will ship at. Probe the motion cheaply, then probe the
count at full cost on that one shot before committing the batch.
Delivery
uv run piecesai h3 post assemble --shot s01.mp4 --shot s02.mp4 --out film.mp4
uv run piecesai h3 post verify --input film.mp4 --expect-aspect 16:9 --expect-duration 20
uv run piecesai h3 post frames --input film.mp4 --at 2.6,11.4,17.0 --out-dir review/
uv run piecesai h3 post derive --input film.mp4 --aspect 9:16 --out vertical.mp4
assemblenormalises each shot before concatenating, so mismatched time bases cannot drop audio or fail the join. Shot order is the order of--shot.verifyexits2on any FAIL. Read the failing line before re-rendering.deriverefuses a crop that would discard the composition and tells you to re-render at the target aspect instead. A refusal is a correct answer, not a tool failure.
For the standard delivery audio pass (AAC 256k / 48 kHz, 2× gain, peak limit):
uv run python skills/h3-video-production/scripts/finalize_audio.py \
film.mp4 film_audio_boost2x.mp4 --receipt film_audio_boost2x.receipt.json
Keep both files. Do not enable denoising unless inspection proves persistent noise.
When one clip's audio collapses
H3 drops a clip's audio at some rate: the sound cuts out partway and the tail turns into low-frequency rumble louder than anything else in the clip. It is a dice roll, not a prompt fault. Re-render the same prompt and it is usually clean.
The tell is the tail out-energising the whole clip, plus a harmonic band in the spectrogram that stops partway and is replaced by strong energy near DC. A clean take keeps its harmonic lines to the end. Check it without listening:
ffmpeg -v error -i clip.mp4 -af volumedetect -f null - # per-segment energy
ffmpeg -v error -i clip.mp4 -lavfi showspectrumpic=s=800x400 spec.png
Re-roll before theorising. One collapsed sample is never grounds for
changing the prompt scaffold that every other clip depends on. The trigger for
that is the same prompt collapsing twice in a row — that is the difference
between a bad die and a bad prompt. Measurements in
references/failure-modes.md.
Failures
Read the error before acting. Out of memory, a missing checkpoint or a rejected workflow is deterministic — change the request, do not retry it. A transient network error is already retried by the client.
When a render succeeds but the content is wrong, the fault is usually the prompt, not the fleet:
- Quote the shot's reference anchors verbatim into the prompt and re-render.
- Still wrong — split the shot into two shorter ones, update the calling Skill's shot table, and re-render.
- Still wrong — stop and take it to the user with what you observed. There is no second model to fall back to here.
If renders fail across every shot rather than one, the problem is the fleet, not
the prompt. Load h3-fleet-ops — do not diagnose workers from this Skill.
swads MCP migration (not yet live)
An approved migration moves execution to the SW Ads queue via video_h3_submit /
video_h3_status / video_h3_workers
(docs/superpowers/specs/2026-08-15-h3-execution-moves-to-swads-design.md).
P1 changed the routing; P2 is not live and those tools do not exist yet.
Check by whether video_h3_submit is available in the session. While it is not,
uv run piecesai h3 is the execution path — the migration design keeps it
deliberately for exactly this window. When P2 lands, this section and the CLI
instructions above retire together.