---
name: video-production-pipeline
description: "End-to-end video production pipeline (orchestration layer): aspect-ratio lock → script and storyboard → character consistency → keyframes → image-to-video → performance and lived-in feel → voiceover, music and sound effects → code-driven compositing → QC and delivery, supporting landscape 16:9 and portrait 9:16, with digital-human likeness compliance and false-positive appeals plus on-screen common-sense verification. Use when the user asks to make a video / short drama / concept film / commercial, to turn a piece of text into a video, to get a landscape or portrait final cut, for a video production process, or to lock the aspect ratio/proportion."
---

> Original skill · commissioned by the user · English edition, 2026-11-12

# Video production master pipeline (orchestration layer)

This is the **orchestration layer**: it only decides "what gets done when, how the parallel work is scheduled, and what condition counts as passing". The actual craft is delegated to existing skills and to this skill's scripts.

## 0. Six kickoff questions (lock the parameters first, act after)

> ⚠️ **Question 1 is a hard prerequisite: the aspect ratio must be locked immediately after the script is finalized, and before any single image is generated.**
> **The aspect ratio is a rule, not an option at generation time. No locked aspect ratio → no anchor image, no keyframes.**
> A verified failure: images were generated without passing a ratio → 3:2 and 16:9 were mixed inside the same film → the final cut was 752×560 (**claiming to be portrait, actually landscape**).

0. **Read memory first** (added after a past review, **ahead of all other questions**): run
   `python <SKILL_LIBRARY>/memory-compounding/scripts/memory_check.py --dir <MEMORY_DIR> --brief`
   —— read out the recent lessons and the items awaiting cleanup, and **check whether any old pitfall matches this task**.
   ⚠️ Basis: in a past run the `memory-compounding` skill sat unused for 11 hours after it was built, because it was only a "read this first at kickoff" rule with no anchor point;
   **binding the read to this question that always gets asked is the only way to stop depending on memory**.
1. **Budget and approved amount** (**ahead of the aspect ratio — the amount decides aspect ratio / duration / shot count, not the other way round**):
   How much money did the user approve? **Without an approved amount you do not enter planning.** Use
   `python scripts/budget_plan.py --budget <amount> --max-shots --duration-each 4` to work out "at most how many shots and how many seconds fit the amount".
   → Gate 13
2. **Aspect ratio**: landscape 16:9 or portrait 9:16? **Write it down together with the pixel values** (9:16 = 1584×2816 | 16:9 = 2816×1584).
   It determines the keyframe ratio, composition, subtitle position and delivery spec. **Never leave it blank, never rely on model defaults.**
3. **Duration and language**: how long? one language or two? Do you want a voiceover, do you want burned-in subtitles?
4. **Distribution platform**: Bilibili/YouTube (landscape) or Douyin/WeChat Channels/Reels (portrait)? (determines safe areas and bitrate)
5. **Capability prerequisites** (**a route you cannot answer for does not enter the option comparison**): is there a usable **payment method**?
   Is the **compute** enough (no discrete GPU → local lip-sync and heavy models are ruled out immediately)? How much **quota** is left this month
   (TTS characters / API)? → Basis: a GPU and Replicate option was once researched, and only afterwards did it turn out that "the payment-method binding is a blocker we cannot pass".

**Immediately after locking, write it into S0 table four (aspect-ratio lock table) and run the check**:
```powershell
python scripts/check_aspect.py --locked 9:16 --images out/kf/*.png --videos out/clips/*.mp4
```

**The three places must share one source**: image generation `--size 9:16` | video generation `--ratio 9:16` (follows the first frame) | final-cut resolution.
Details, the ratio↔pixel lookup table and the decision order are in `references/01-parameters-and-specs.md` §0.

**Trigger phrases**: make a video / short drama / concept film / commercial; turn this piece of text into a video; deliver a landscape or portrait final cut; video production process.
**Upstream writing layer**: script and dialogue go to **`short-drama-script`** (premise → beats → verbatim dialogue → de-AI-flavor check),
and this skill takes over from the **S0 four tables**. **Do not improvise lines in the production layer** (that is exactly the root cause of out-of-sync audio and a robotic tone).

**This skill's files**: `SKILL.md` (orchestration and gates) · `references/01–13` (craft details) ·
`scripts/` (executable gates) · `tests/` (regression cases, to prevent rollback) · **`LESSONS.md` (cross-project lessons ledger, see §10)**

**Call existing local skills on demand**: `short-drama-script` (**script and dialogue, S0 upstream**),
`video-editing` (editing workflow), `clip-skills` (editing rules), `media-use` (music/SFX sourcing),
`agent-caption` (precise subtitles), `voice-generation` (neural TTS), `remotion-*` (code-driven rendering),
`local-audio` (**offline fallback**: local TTS / transcription / music; delivery still goes through this skill's volc and make_bgm).
**External capabilities** (Volcano Ark / Doubao / HeyGen / Runway) go through pluggable interfaces.

**Credentials (all via environment variables, the scripts read them automatically)**:

| Purpose | Environment variable | Notes |
|---|---|---|
| Text-to-image / image-to-video | any variable whose value looks like `ark-xxxx` | `ark_gen.py`/`ark_video.py` scan the registry and discover it automatically; the variable name is not restricted |
| Doubao speech synthesis (voiceover) | `<API_KEY_ENV>` | Single key from the new console; **it must be created in the Default project** |
| BGM (audio generation) | `mediakit-audio-gen` | The AI MediaKit key, **a different key from the speech one** |

⚠️ **Ark error-code reference** (do not misdiagnose a non-key problem as a key problem):

| Response | Meaning | Action |
|---|---|---|
| `401` | The key itself is invalid/suspended | Switch keys |
| `404 ModelNotOpen` | The model is not enabled | Enable it in the Ark console under "Enablement management". ⚠️ **Enabling has a 1–2 minute propagation delay**: the API may still return this error after the console shows "enabled", so wait a minute or two before retrying, do not rush to call it a failure |
| `429 SetLimitExceeded` | The **"safe trial mode" quota is used up** and the service was suspended automatically | Adjust that model's quota (**raise it rather than turning it off** — turning it off means unlimited pay-as-you-go) |
| `400 InvalidParameter` | Illegal parameter | Read the `--dry-run` output and check the request body |
| `400 InputImageSensitiveContentDetected` | **The input image was judged as "possibly containing a real person"** | **No charge**. To reduce realism you need **painterly texture over the whole frame**; local skin smoothing does not work. Retrying the same image gives the same result, so you must swap the image. See `references/06` §6 |
| `400 InvalidParameter ... duration` | **The duration is outside the range the model supports** | seedance-2.0-mini only supports **4–15 seconds**; write storyboards at ≥4s directly. See `references/04` appendix |
| `400` content moderation | The prompt/reference image was rejected | **Usually not billed**; change the composition or the prompt |

The video pipeline needs one enabled `doubao-seedance-*` (image-to-video) and one enabled `doubao-seedream-*` (text-to-image).

## 0.5 Three-platform delivery spec (confirmed by the user in a past review; overrides this skill's defaults)

- **Douyin portrait cuts must be natively laid out**: re-render 1080×1920 with the same `timing.json / meta.json / audio_meta.json`; **never crop a landscape version narrow** (the source already carries large burned-in text, so cropping doubles the text and cuts through content).
- **Landscape 2–5 minute pieces go only to Xigua Video / Toutiao**, logged in with the Douyin account or via "sync to Xigua Video", with no separate registration; the main Douyin account only posts 60–90 second portrait pieces.
- The same piece of content is not posted to Douyin on the same day as both a landscape and a portrait version.
- Portrait safe areas: 64 px on the left and right, the 200 px right-side icon column, the bottom 320 px must not carry body text; subtitles/large text sit at 1420–1540.
- Tools: `<TOOLS_DIR>/video/portrait_frames.py` (native portrait frame output), `layout_spec.py` (layout spec sheet); `vertical_cut.py` is only for subtitle-free masters.
- Full spec: `<WORKSPACE>/ops/three-platform-ops-spec.md`.

## 1. The eight-stage main flow

| Stage | Input | Key actions | Output | Gate | Parallelism |
|---|---|---|---|---|---|
| **S0 upfront design** | Idea/outline (the script is produced by `short-drama-script`) | **① aspect-ratio lock (do this first) → ② script revision → ③ character card (look ↔ voice match) → ④ sound design table (verbatim lines / vocal onset and offset / lip-sync plan / ambience / foley / BGM mood)** | **Four tables (frozen)** | **Four tables frozen**: aspect ratio and pixels, verbatim lines, look↔voice match rationale, measured vocal onsets | Serial (**the only input to the whole pipeline**) |
| S1 script and storyboard | Idea/outline | Set the emotional curve, the beat rhythm, narration and dialogue; **annotate shot type and performance notes for every shot** | Script + storyboard table | Measured line density + **shot-length rhythm** (median ≥3.0s, range ≥1.5s, performance shots ≥1/4) | Serial (the only input downstream) |
| S2 visual and sound spec | S1 | Palette, subtitle placement, sound anchors, character timbre (**the aspect ratio was locked in S0; this stage only inherits it**) | Spec document + parameter JSON | Spec frozen, no further changes | Serial |
| S3 character/scene anchor images | S2 | Generate 1 anchor image per entity first | Anchor image set | Anchor images pass (consistency baseline) | **Parallel across entities**, serial within an entity |
| S4 keyframes | S3 | Anchor image + reference image + consistency prefix | Storyboard keyframes | Size/ratio/no text | **5 concurrent** |
| S5 video clips | S4 | Image-to-video / first-and-last-frame chaining; **write motion prompts as "physiological reaction + action process + resistance"** | Clip set | Single model, continuous motion; **every shot must contain a perceivable human action (camera moves do not count)**; **performance shots use the main tier model**; **prompt lint passes** | External platforms in parallel, do other work locally |
| S6 sound | S2 | Line-by-line voiceover (timbre card) / BGM / SFX / ambience; **build the `audio/provenance.csv` source ledger** | Split audio tracks | Aligned to the timeline, level standards; **voice and BGM meet spec**; **no placeholder synthetic audio left** (see Gate 8) | Fully parallel with S5 |
| S7 compositing and color grading | S4+S5+S6 | Code-driven timeline (Remotion) → **one unified color baseline for the whole film + film-emulation LUT + light grain/vignette** | Final cut master | Duration error 0, subtitle placement compliant; **color tags bt709/tv**; grading is not optional | CPU parallel concurrency=6~8 |
| S8 QC and delivery | S7 | Automated verification + manual spot check | Master + platform versions | **All hard gates pass** | Dual-aspect rendering in parallel |

## 2. Hard gates (no passing, no moving to the next step)
1. **10-second technical test film**: contains ① a front-facing speaking shot (verifies lip-sync) ② one moving shot (verifies continuity) ③ one edit beat. No passing, no production.
2. **Lip-sync plan decided**: front-facing dialogue must have a lip-sync plan; if not, switch to "off-screen voice + reaction shot" or profile/back/hand close-ups (see `references/04`).
3. **Single video model + first/last-frame chaining**: mixing models is forbidden; within a scene chain first→last→first.
   **Anchor point (added after a past review)**: this rule **must be written verbatim into the external task brief**, and after every shot is generated you verify "last frame of the previous shot = first frame of this shot".
   ⚠️ **Sitting in SKILL.md alone = it will not be executed**. Verified on a past project (<SAMPLE_TITLE>): all 11 shots were generated independently, the costume changed 4 times, and the same bench turned into 4 different places. The user's own words: "This isn't making a video, this is making a PowerPoint."
4. **Subtitle/timeline auto-verification passes**: `scripts/check_timeline.py`, `scripts/check_subtitle_layout.py` all green.
5. **Master spec**: duration error 0, loudness -14 LUFS, true peak ≤ -1.0 dBTP, color bt709/tv, all audio tracks present.
6. **Digital-human likeness compliance gate**: a realistic digital-human shot must first have the "not a real person" evidence pack in place (prompt / model and version / seed / call ID [async task_id, or the X-Request-Id response header for synchronous interfaces] / watermark-free original), and the explicit AI label must be applied; when a platform rejects it as portrait-privacy, file a **false-positive appeal** — not a request for a "real-person asset allowlist". See `references/06-digital-human-compliance-and-false-positive-appeals.md`.
7. **Performance gate (anti-commercial feel)**: cinematic-cliché prompts, an entire film of uniform 2-second shots, zero performance shots — any one of the three means rework.
   The storyboard must give "shot type" and "performance notes" (physiological reaction) for every shot; performance shots last ≥3.5 s and use the main tier model.
   Automated check `scripts/lint_performance.py --prompts <motion> --csv <storyboard>`.
   Methodology in `references/07-performance-and-lived-in-feel.md`.
8. **Sound delivery gate** (three items, none optional):
   - **Native audio: decide by target tier, do not blindly treat it as "top priority"**: the model's `generate_audio` / `enable_audio`
     is on by default, and the output is generated **in sync with the picture** (dialogue + ambience + BGM in one pass), so audio and picture are naturally aligned.
     ⚠️ **But it cannot be controlled separately, it likes to add drama on its own, and a quiet scene may randomly sprout voices** (especially once a character timbre is bound) —
     when you want "TV-series grade" controllable delivery you should **build picture and sound as separate tracks** (6 audio layers, see `references/10` §10.6).
     **Routing rule (decided per shot, A preferred)**:
     ① Non-front-facing shots (empty shots/back/profile/hands) → go **A** (the prompt states only action and environment, no dialogue)
     ② Front-facing speaking shots → try **A** first (2.0+ model + `generate_audio=true` + the dialogue written into the prompt);
        **if rejected with `400 InputImageSensitiveContentDetected` → switch to B immediately**
        (that shot moves to the 1.0 series + post-recorded voiceover + a lip-sync layer)
     ③ **A rejected attempt costs nothing** (interception before generation is not billed) → so A can be tried first at no cost
     **④ Hard rule for phrasing**: the prompt **states only "scene + ambient sound + verbatim dialogue" and never describes the character**
     (no age/gender/front-facing/wardrobe) — the character is decided by the reference image. Describing the character triggers real-person detection;
     remove the description and the same image passes. → `references/06`
     ⚠️ **Mixing A/B within one film is normal, not the exception**; **every shot must record which route it took**.
     → `references/10` §10.6
     **Whichever route you take**: cropping and transcoding **must not use `-an`**; even when you are certain the audio is unused, **extract it as a sidecar first, then mute**.
     → `references/10` §1, §10
   - **Source ledger**: `audio/provenance.csv` must exist and register every audio source's origin/license/commercial usability;
     the delivery version **must not** contain synthesizer-made BGM, free navigation-voice TTS (SAPI / Baidu gettts), or synthesized noise SFX (placeholders are allowed only before S6, and must be replaced by then).
   - **BGM spec**: at most 2 segments, covering ≤40% of the film length, the in-point/abrupt-stop point within ≤0.1 s of the on-screen beat, a 0.2 s hard cut to silence at the end;
     sources limited to "licensed music library / AI generated (human-selected) / own original work", registering title · author · license number. → `references/08-bgm-specs-and-music-selection.md`
   - **Vocal performance spec**: one timbre per character (never changes across shots or episodes), pitch separation between characters ≥15%,
     dialogue 4.0–5.5 characters/second (>6.5 = newsreader tone), inter-sentence pause 0.25–0.5 s (median <0.20 s = reading off a script),
     room reverb is mandatory (wet/dry 15–30%; the dry signal is a newsreader tone), restrained noise reduction, generate sentence by sentence rather than the whole block at once.
     → `references/09-voice-and-voiceover-performance-specs.md`
   - **Subtitle timing must match the voiceover**: windows must be re-laid out against the measured natural speaking rate of the voiceover (converge in two rounds), not force-compressed;
     keep the line rate within 3.5–5.5 characters/second, and rewrite the lines if it falls outside. → `references/10` §5
   - **Landing scripts** (already implemented in this skill, call them directly, do not assemble ad hoc):
     - Voice: `python scripts/make_vo_tracks.py --srt subs/zh.srt --cast cast.json --out audio/vo_zh.wav`
       `--backend volc` (**default**, Doubao speech synthesis 2.0). In cast.json fill in
       `speaker` / `emotion` / `emotion_scale` / `pitch` / `tempo` / `reverb` for each character.
       ⚠️ The baidu/sapi backends are placeholder channels, **forbidden in delivery**.
     - BGM: `python scripts/make_bgm.py --prompt "..." --out audio/bgm.wav --total <film-length> --start <in-point> --end <out-point> --loop`
       automatically does "generate → normalize → seamless loop → lay out at the in-point"; add
       `--mix-with audio/vo_zh.wav --mix-out audio/final.wav` to get a ducking mix directly.
     - Room reverb is already built into the voice track (`--reverb`, default 0.24); for the BGM channel see `references/10` §8.
9. **Cost gate (no spending without reason)**: any generative call must **compute the money first, watch the money, and report for approval when over threshold**.
   **What counts as money (added after a past review)**: **platform quota the user paid for also counts** — Doubao/Jimeng membership quota, free quota,
   third-party generation on your behalf, all get an entry in `cost_ledger.py` (unit can just be "quota").
   Verified on a past project (<SAMPLE_TITLE>): **0 ledger files** across the whole job, so the quota the user spent was unverifiable afterwards.

   > ⚠️ **This gate applies to any project that generates cost** (TTS quota / images / video / API / cloud services),
   > **not only to projects inside the video pipeline**. The tool accepts any path:
   > `python scripts/cost_ledger.py init --ledger <project>/cost_ledger.json --budget <value>`.
   > Basis: in a past audit **almost no project had a cost ledger**, so their spend was completely untraceable.
   - **① Before starting you must produce a written "spend plan" and get the user's confirmation** (not a mental estimate):
     `python scripts/cost_preflight.py --csv storyboard.csv --plan-out spend-plan.md`
     The plan contains: per-item budget + **rework reserve (20~30% of the main item, not optional)** + **tier lock** + approval threshold.
     → Rules and template in `references/11-cost-control-and-roi.md` §8
   - **Write the ledger ceiling immediately after confirmation**: `python scripts/cost_ledger.py init --budget <confirmed-value>`
   - **Book every expense immediately**: `python scripts/cost_ledger.py add --item "clip_01" --cost 2.92`
     —— **planning without booking the actual spend is the easiest place for this rule to fail** (which is how a 71% overspend happened in a past run).
     Any estimate **more than 10% over plan** → stop and re-request approval.
   - **The tier must be locked**: write the model ID into the plan; **switching tiers mid-way = changing the plan = re-requesting approval**.
   - **Experiment with images, not video**: composition / consistency / style / lighting are all verified with images (about 0.2 CNY each);
     **until the images are final, do not touch the video endpoint**. One 5-second 1080P video costs about the same as **94 images**.
   - **Use the cheapest tier for technical validation** (480P / mini tier, around 0.1 CNY per second);
     **never validate the chain on the 1080P main tier** — the tier price difference can reach **37×**.
   - **`--dry-run` before every call**: the script prints the **estimated cost** and labels the price source
     (tier recognized / "tier not recognized, conservatively estimated at the fast tier"). **For any tier labelled "not recognized",
     check the official price manually before running**; never treat the default price as authoritative.
   - **Cost ceiling**: `ark_video.py` / `ark_gen.py` support `--max-cost <CNY>`, which aborts when exceeded.
     When unsure, set a small value and try.
   - **Batch threshold approval**: a single batch estimated at **> 50 CNY**, or the whole film estimated at **> 200 CNY** →
     **first report to the user "how much it will cost and what it produces", and only execute after confirmation**. Acting first and reporting later is forbidden.
   - **Tier grading**: transition shots / empty shots use the value tier; **only performance shots switch to the main tier** (together with Gate 7).
   - ⚠️ **Billing traps**: `enable_retry` **bills each retry separately**; Seedance 2.5 sample mode is a
     **two-step task with each step billed independently**; image sets are **billed by actual image count**; layer separation is billed by pixel tier.
   - **Failure is cheap, success is expensive**: failures caused by moderation/parameter errors are usually not billed → **you can experiment boldly**;
     what really burns money is "batch success".
   - **Run a pre-check before batching**: `python scripts/cost_preflight.py --csv storyboard.csv`
     —— reads the storyboard and totals it as "performance shots → main tier / everything else → value tier", automatically prompting for approval
     above 50 CNY (single batch) / 200 CNY (whole film); add `--budget N` to cap your own budget. Purely local, zero cost, no network.
   - Full rules, price magnitudes, the pre-check script and the pre-kickoff checklist → `references/11-cost-control-and-roi.md`.
10. **Color grading and delivery gate**:
    - **Grading is not optional**: unify temperature / contrast / black level / saturation across the whole film (**one baseline**),
      then apply a film-emulation LUT (Kodak 2383 / Fuji 3510) + **light** grain + **light** vignette.
      Skip it and every shot drifts in color, so it looks like "a pile of clips" instead of a film; **this is the most cost-effective "cinematic" lever**.
      Tool: DaVinci Resolve — **the free version is enough**. → `references/12-color-grading-and-delivery.md`
    - **Delivery must carry color tags** `bt709` / `tv`: `qc_video.py` checks them, and **ffmpeg does not add them by default, so you must set them explicitly**
      (this is the most common delivery accident).
    - **Deliver subtitles as a separate track, not burned into the picture**; when burned-in subtitles are needed, render a second version and keep both.
    - **AIGC labeling**: explicit (corner badge) + implicit (metadata watermark), **which cannot be turned off manually**; AI content needs separate filing.
    - **Complete audio layering** (`references/10` §10): the ambience bed has a spatial source, foley is bound to visible contact,
      the mix follows the priority chain (dialogue > key SFX > breathing foley > ambience bed > music), and cross-shot sound continuity is recorded.
11. **Delivery-completeness gate (all-green technical QC ≠ a complete film)**: `qc_video.py` passing all 8 checks **still does not mean it is deliverable**.
    **Executable check (must run, never rely on someone remembering)**:
    `python <project-retro>/scripts/retro_scan.py --project <project> --json review/evidence.json`
    → read `missing_shots`: **non-empty = the delivery is incomplete, not allowed out**.
    Basis: verified on a past web-series project (<SAMPLE_TITLE>) — **1 shot (S16) was genuinely missing and it was still delivered**; Gate 11 had existed for ages but was never executed.
    You must manually check the list in `references/13` §5; **any item failing means you may not mark it "done"**:
    - **Does every character the storyboard requires appear in the final cut?** (a past run failed here: the male lead never had a usable shot, the film was missing him, and it was still delivered as "done")
    - Does every storyboard shot have a corresponding clip? Missing ones must be **re-shot or the storyboard changed and re-approved**; silently skipping is not allowed
    - Does every line have a corresponding speaking image, or is it explicitly marked as off-screen voice?
    - **Do the lip movements match the lines?** Front-facing dialogue shots must pass, otherwise switch to off-screen voice/reaction shot
    - **Does the BGM mood match the scene's emotion?** (the question is not "is there music" but "does it fit")
    - **Do the voices match the characters' looks?** (against the character card's match rationale)
    - **Does the aspect ratio match the S0 locked value?** (three places: image generation / video generation / final cut; `scripts/check_aspect.py` gates it)
    - **Do the objects on screen obey common sense?** (the back of a phone is a back panel not a screen, five fingers, a round bowl is a round bowl,
      objects are supported, food is steaming…) → `references/02` §object common-sense spec

   **Two items added to the completeness list after a past review (learned the hard way)**:
   - **Cross-shot consistency**: stitch the first frame of every shot into one image and check whether **wardrobe / scene / lighting / characters** are the same one.
     This is exactly the item a past project (<SAMPLE_TITLE>) omitted — aspect, duration, lines and loudness were all green, yet to the naked eye the actress changed 4 costumes.
   - **Action within a shot**: look at **each shot's first and last frame side by side**; they must be visibly different.
     ⚠️ **Camera movement will fool any automated check** (adding a push-in guarantees the pixels differ), **only a side-by-side look counts**.

12. **Aspect-ratio lock gate (the first gate at kickoff, earlier than every generation action)**:
    - **The first thing after the script is finalized is locking the aspect ratio**, written into S0 table four (orientation / ratio / image-generation pixels / video-end ratio / final-cut resolution).
    - **No locked aspect ratio → calling `ark_gen.py` to generate any image is forbidden** (including anchor images "just to try it out").
      This was the failure point of a past run: no ratio passed → the model supplied a default → **3:2 and 16:9** mixed in the same film →
      the final cut was **752×560 (claimed portrait, actually landscape)** and every keyframe had to be redone.
    - **The ratio is an active choice, not a default.** Seeing `3:2 (2496×1664)` is basically proof that "`--size` was forgotten".
    - **Three places, one source**: image generation `--size` = video generation `--ratio` = final-cut resolution; **`ark_video.py`'s `--ratio`
      defaults to `adaptive` (follows the first frame)**, so **the first frame's ratio is the line between life and death**.
    - **Portrait must generate portrait keyframes natively**; cropping a landscape version is forbidden.
    - After locking, run `python scripts/check_aspect.py --locked <ratio> --images <images> --videos <clips>`;
      if anything mismatches, **stop immediately**. → Details `references/01` §0, `references/13` table four

13. **Budget gate (five-step closed loop)**: **make a budget → report the budget → plan within the approved amount → book the spend → compare**.
    **In execution order it comes before Gate 12** — the amount decides the aspect ratio / duration / shot count, not the other way round.

    | Step | Hard requirement |
    |---|---|
    | ① **Make a budget** | `scripts/budget_plan.py --budget <amount> --durations <seconds per shot> --plan-out <project>/spend-plan.md`; it must contain the **rework reserve** (default 25%) and the **tier lock** |
    | ② **Report the budget** | Hand the plan to the user and get an **explicitly approved amount**. An empty "approved amount" field in the plan = **not approved = no work may start** |
    | ③ **Plan within the amount** | Scope / shot count / duration / tier are **back-derived from the amount**; if it does not fit, **cut scope** (lower the tier → fewer shots → shorter duration), **never exceed the budget** |
    | ④ **Book the spend** | Book every expense immediately (**including rejections at 0**). Planning without booking actuals = the easiest place to fail (a past run overspent by 71%) |
    | ⑤ **Compare** | More than **10%** over plan → stop and re-request approval; **switching tiers = changing the plan = re-requesting approval** |

    **Division of labour between the three tools (the price table has a single source of truth, reused from `cost_preflight.py`)**:
    `budget_plan.py` amount→plan | `cost_preflight.py` storyboard→money | `cost_ledger.py` actuals→ledger.
    Exceeding any of them exits with code 1, which can be used directly as a process gate.

    ⚠️ **Verified counter-example**: on a 6 CNY amount, **1 main-tier shot (4 seconds = 14.96 CNY) alone overspends by 2.5×** —
    not counting the money before planning inevitably burns rework. **Forbidden "cuts"**: compressing the speaking rate to cram lines in (turns into newsreader tone),
    dropping keyframe verification (which postpones the rework cost to the final cut).

## 2.5 External collaboration: four items every task brief must carry (**when you delegate generation to an external platform or another person**)

> Basis: a past project (<SAMPLE_TITLE>) verified in a past run (all 11 shots reworked). **However detailed the task brief is, missing any one of the items below
> and the platform will work in "generate every shot independently" mode, so consistency is bound to collapse. These four matter more than the prompts.**

| # | Must be written into the task brief | Matching rule | Consequence of omitting it (verified) |
|---|---|---|---|
| 1 | **First/last-frame chaining**: S01 uses the costume-test image; S02 uses S01's last frame; S03 uses S02's last frame… every shot grows out of the previous one | Gate 3 | The actress's costume changed 4 times; the same bench turned into 4 different places |
| 2 | **The three motion elements**: physiological reaction + **action process** + resistance; one action runs all the way through without stopping halfway | S5 | A 4-second shot whose first and last frames are almost identical → the user called it "like a slideshow" |
| 3 | **Consistency lock**: write one **verbatim-reused** constant paragraph each for wardrobe and scene, and paste it into every shot's prompt | Gate 3 | The AI sometimes follows the reference image and sometimes the prompt; the two sources of truth fight each other |
| 4 | **Acceptance criteria**: the first and last frames must differ; the same scene viewed side by side must be the same place | Gate 11 | All technical metrics green, and the final cut is a PowerPoint |

**Plus three extra hard requirements**:

- **One source of truth**: reference images are responsible only for "the face"; **wardrobe and scene are always decided by the text** — image and text as two sources of truth will inevitably fight.
- **Validate with 2 shots first**: look at the first frames of S01 and S02 side by side; once wardrobe/scene/lighting/face line up, continue.
  **Never do the whole film in one go** (a past project did all 11 shots in one go and lost all of them).
- **A/B and per-shot gain**: for multiple assets generated in parallel, **align loudness before joining**.
  In a past run the raw assets differed by nearly 20 dB (-25.2 → -44.8); without per-shot gain, the sound jumps at every cut.

## 2.6 Required-reading hooks for the references (**added after a past review, learned the hard way**)

> Verified on a past project (<SAMPLE_TITLE>): the rules in the references **were written out in full**, but **not one of them was read** while writing prompts.
> Result: all 11 shots violated the "motion phrasing" rule (micro-expressions were written → slideshow), "first/last-frame chaining" was not executed (wardrobe/scene drifted entirely),
> the "lived-in feel checklist" was missed (the picture was as clean as a commercial), and **a push-in was added to every shot** — while `07` §2 states plainly that
> **"the more camera-language words in the prompt, the more the final cut looks like a commercial"**.
>
> **Picking references by "what feels relevant right now" = inevitably missing the one you most needed to read.** Hence the hooks:

| Stage | Required reading (relative to this skill directory) | Consequence of not reading (verified) |
|---|---|---|
| S1 storyboard | `references/02-storyboard-and-prompts.md` (four-part structure / motion phrasing / consistency lock) | Action written as micro-expression → the final cut looks like a PPT |
| S5 generation | `references/02` §motion phrasing + §first/last-frame chaining; `references/07-performance-and-lived-in-feel.md` (six root causes / three-piece kit / lived-in checklist) | Consistency drift + commercial feel |
| **Stuck / retrospective** | `references/14-historical-incident-cases.md` (10 cases with the original error text; find similar ones by "symptom") | The same pitfall stepped on again and again (list ≠ usable, fake green light, silent loss) |

**How to enforce it (never rely on self-discipline)**: when delivering a storyboard or task brief, state in the notes **which references were read**.
**Not written = not read = that stage is not complete.**


> 📋 **For a 10-second slice-of-life short, fill in the start sheet directly**: `templates/short-slice-of-life-start-sheet.md`
> (5 irreversible cells + execution order + five-part fill-in + first-frame self-check + common failures; verified baseline: 10 seconds / 2.40 CNY / 45 minutes, passed first time)

## 2.7 Three steps for connecting an external platform (**added after a past review, each one missed once within the same hour**)

> Verified friction (all unrelated to creativity, purely environmental):
> ① env var names do not follow the convention — neither a `<prefix>-<date>`-style name nor a localized `<API_KEY_ENV>`-style name contains `API_KEY`,
>    so three rounds of regex search found nothing and it was **briefly misreported as "no credentials"**;
> ② `ARK_MODEL` was not set → it errored out and stopped;
> ③ `doubao-seedream-3-0-t2i` appears in `--list-models` but **a real call returns 404 no-permission**;
> ④ the price list has `seedance-1-0-mini` while the account actually only has `1-0-lite` (and **the 1.0 series does not support audio**);
> ⑤ the user's image was 3:4 and the final cut had to be 16:9 → a landscape first frame had to be generated first.

| Step | What to do | Why |
|---|---|---|
| ① **List every credential variable** | List **all** environment-variable names in the User scope and read through them by eye — **do not use regex matching** | Names may be in Chinese, contain hyphens, or carry a date suffix |
| ② **Probe the account's capabilities and save JSON** | Take the list from `--list-models` → **probe each one with a real call** (404 is free, only success is billed) → save `account_capabilities.json`: usable models / measured unit price / audio support | **A listing ≠ permission; a price list ≠ usable on this account** |
| ③ **Budget at measured prices** | Use the unit prices from ②, not the built-in list price; without a measured price, run `--dry-run` first | The list price conservatively estimates `lite` as `fast` (8 CNY estimated vs 1 CNY actual for 5 seconds) |

**In one sentence**: the leverage for efficiency is not "writing better prompts" but **turning "things you have to look up every time" into "things written into a file"**.

## 3. Parallel orchestration (the core of efficiency)
- **Parallel across queues, serial within a queue**: split into queues by "entity" (one queue per character/scene), serial inside a queue to preserve consistency.
- **Safe concurrency**: image generation 5 | rendering 6~8 | TTS sequential + throttling + retries + fallback channel.
- **Treat external platforms as parallel compute**: push long generations outside while the local machine does sound and compositing.
- **Import every batch into the asset library + spot check**: `scripts/import_assets.ps1` normalizes + records in the ledger.
> Three-window division of labour, the dependency graph and measured speed-up data are in `README.md`.

## 4. Aspect-ratio branches (the 7 items that must differ between landscape/portrait)
Resolution, keyframe ratio, subtitle bottom margin, left/right safe margins, font size, line width, platform UI occlusion zones — the lookup table is in `references/01`.

**Image-generation pixels (Seedream 2K tier, copy directly)**: `9:16 → 1584×2816` | `16:9 → 2816×1584`
(`--size` already accepts ratio forms like `9:16` and the script converts them automatically; see `references/01` §0)

**Hard rules**:
1. **The aspect ratio is locked in S0, not decided here** — this section only covers "once locked, how landscape and portrait differ".
2. **Portrait must generate portrait keyframes anew; cropping a landscape version and calling it portrait is forbidden.**
3. **The ratio must share one source across image generation, video generation and the final cut**; after locking, gate it once with `scripts/check_aspect.py`.

## 5. Lip-sync and consistency (pluggable interfaces)
Interface definitions and degradation strategy are in `references/04-interface-lipsync-and-consistency.md`:
- **Lip-sync driving**: external by default (Ark/Doubao or HeyGen/Runway); with no GPU, no local lip-sync;
- **Consistency lock**: anchor image + reference image + consistency prefix (`scripts/ark_gen.py --ref`);
- **Aspect adaptation**: prefer native portrait generation; when conversion is really needed, use Runway outpainting / Adobe reflow.

## 6. QC and moderation pre-check
- Delivery QC (④): `scripts/qc_video.py` (resolution/frame rate/duration/audio tracks/color tags/black-frame sampling/loudness)
- Platform moderation pre-check (⑤): `scripts/moderation_preflight.py` (realistic-portrait risk, sensitive words, rule list) + a "probe run" before generating
- Manual spot-check list in `references/05-qc-and-moderation-precheck.md`
- **Performance and sound gates (⑦⑧)**: `scripts/lint_performance.py` (cinematic-cliché prompts / shot-length rhythm / BGM segment count and share / source ledger),
  output `review/performance_lint.md`; with any blocking item (missing ledger, unreplaced placeholder sources) it must not proceed to S8.
- **Digital portrait rejected (false positive)**: evidence pack generation `scripts/face_evidence_manifest.py` → `review/evidence/appeal-statement.md`; three-step handling in `references/06`
- **Aspect consistency**: `scripts/check_aspect.py --locked 9:16 --images out/kf/*.png --videos out/clips/*.mp4`
  —— reads the width/height ratio of every file and compares it with the locked value, **blocking any mismatch**; purely local, zero cost, no network.

## 7. Delivery spec (default)
| Version | Spec |
|---|---|
| Master | 1920×1080 or 1080×1920; 24 fps; H.264 CRF16; yuv420p(tv, bt709); AAC 320k/48kHz; -14 LUFS; TP ≤ -1.0 |
| Platform version | Same size H.264 8–12 Mbps; `+faststart` |
| 4K master (optional) | 3840×2160 / 2160×3840, same color and loudness standards |

## 8. Risks and boundaries (hard constraints)
- **Do not modify the Codex configuration**, do not install dependencies, do not register plugins, do not go online automatically; scripts default to dry-run.
- **Credentials are read from environment variables only**, and must never be written into any file, log, subtitle or final cut; run a credential scan before publishing.
- **Probe generative calls with 1 image/1 clip first**, then batch; failed calls are usually not billed, so you can experiment boldly.
- Do not generate identifiable real faces; delivery must add an AI-generated label per local regulation (explicit label + implicit metadata label).
- **Do not throw away the model's native audio**: one `-an` drops a layer of real ambience/SFX synchronized with the picture —
  this is the most expensive and most common waste (hit in a past run).
- **The wrong SFX is worse than a synthetic one**: general SFX libraries often lack specific sounds like a heartbeat, high heels or a metallic clink;
  if the nearest neighbour a search returns has the wrong timbre, keep the synthetic version and mark it `self-synthesized (placeholder)` in the ledger.
- **The picture can be skimped on, the sound cannot**: synthesizer-made BGM and free TTS voiceover may only be **placeholders**, and must be replaced before S6 ends with
  neural TTS / a licensed music library / a sample library, otherwise the final cut will be judged "draft/commercial".
- **High resolution ≠ not a commercial**: the root cause of commercial feel is "a prompt that only describes camera moves, every shot exactly 2 seconds, uniform pacing throughout", not resolution.
  Image quality can be bought, performance must be designed — see `references/07`.
- A realistic digital human being judged "a real person" is a common false positive: the burden of proof is on the **generation chain** (proving it is not a real person), not portrait authorization; the handling flow is in `references/06`.

## 9. Directory conventions
```
project/  01_script · 02_storyboard-and-audiovisual · 03_voiceover-and-subtitles · 04_production-process
          05_assets-and-outputs/{keyframes,clips,audio,exports,review,tools}
          06_composite-project(Remotion) · inbox/{clips,audio,images}
```
> Full field descriptions are in `README.md`.

## 10. Lessons feedback protocol (**mandatory after every project delivery, 5 minutes**)

> **This section covers "what to record"; "how to do it" is in the `project-retro` skill** (evidence collection → three-way attribution → rule audit → change table → approval → consolidation).

This skill **accumulates experience through projects**. Everything accumulates in `LESSONS.md`, using the same mechanism as the writing skill (`short-drama-script`).

**When it triggers**: after a project is delivered (or when the user says "retrospective" / "summarize").

**Follow the three retrospective questions, do not freestyle**:

1. **Where did this project have rework?** (rework = a missing rule, or a rule that existed but did not take effect)
2. **What was the cost?** (money / number of reworks / manual labour) — **something with a cost can be escalated after a single occurrence**
3. **Which existing rule failed to trigger?** If the rule existed but did not stop it, the **anchor point was wrong**:
   something that should have gone into a script was written as prose (this skill has already turned 6 mechanically decidable items into `scripts/`).

**Then**: append one line to `LESSONS.md` (`observing`), **do not rush to change the rules**.

| State transition | Threshold |
|---|---|
| `observing` → `escalated` | **reproduced ≥2 times**, or **only once but with a real cost** (rework / wasted generation spend / delivery rejected) |
| `escalated` → `retired` | not reproduced across 3 consecutive projects (to stop gates from piling up) |

**Where to escalate to**:

| Nature of the lesson | Landing spot |
|---|---|
| Ordering / timing | `SKILL.md` body or a hard gate |
| Craft phrasing and revisions | The matching `references/` |
| **Mechanically decidable** (ratio/resolution/duration/level/share) | Add a check in `scripts/` — **prefer this one** |
| Interfaces / error codes | `SKILL.md` §0 lookup table |
| Just "bad luck this time" | Keep it in `LESSONS.md`, **do not escalate** |

> ⚠️ **Three hard constraints**
> 1. **A lesson must not exist only in the conversation.** A past round paid for this — several findings were scattered across the dialogue,
>    and only came in because the user pushed ("add this round's findings into the skill"). **A lesson not written into `LESSONS.md` = not accumulated.**
> 2. **A cost lesson must carry the ledger**: writing "we overspent" is useless; writing "planned 4.2 CNY / paid 7.0 CNY / 71% over, because we planned without booking actuals" can become a gate.
> 3. **Before changing anything, read `LESSONS.md` §5 "the skill's own correction history"** — some changes were only made after a failure
>    (for example the 1% tolerance in `check_aspect.py`); reverting them will immediately bring the false positive back.

**Self-check when modifying this skill (hard requirement)**: after touching `scripts/` or any threshold you **must run**

```powershell
python tests/run_tests.py      # all green is the only definition of done
```

It freezes the historical false positives and misses from `LESSONS.md` §5 into assertions (including "a 3:2 first frame must be aborted" and "560×752 must not be reported as a false positive").
**A "don't change it back" note that only lives in the documentation does not work** — a counter-example must be re-runnable with one command.

---

14. **Time-ledger gate (no "slow and still produces nothing")**: after every delivery, record in `scripts/project_ledger.py` and look at three ratios —
    **counting only money and not time hides waste like "5.5 hours for 45 seconds, then judged unqualified"**.

    | Ratio | How it is computed | Red line |
    |---|---|---|
    | **Speed** | total elapsed ÷ final-cut seconds | > 8 minutes/second → too slow, first check whether you are reworking |
    | **Rework share** | rework/discarded time ÷ total time | > 40% → the direction is probably wrong |
    | **First-pass rate** | output that passed first time ÷ total output | < 50% → upfront validation was skipped |

    **Verified baseline (a past run)**:
    - One project (<SAMPLE_TITLE>): **5.5 hours / 45 seconds / 0 useful output** (judged unqualified), rework share **44%** (over the red line) — all three red
    - The 10-second slice-of-life short: 45 minutes / 10 seconds, passed first time, speed **4.5 minutes/second** — all three green

    **Key**: the three ratios must be **archived by project type**, so the next project of the same type can be compared. **Without comparison you cannot see progress**.

    **Rework must be split by stage (verified in a past run: two projects both had a 44% rework share, yet opposite outcomes)**:
    - **Configuration/parameter rework → compounds**: how to find credentials, how to probe models, how to fill parameters — once paid for, it is saved next time;
      the 10-second short's 44% was entirely here, and it **dropped to zero on the next project of the same type**;
    - **Direction/generation rework → pure loss**: whole batches redone because the direction was wrong; neither the money nor the time comes back —
      that is where the other project's 44% went.
    **So when the rework share looks high, first ask: can this rework be saved next time?**

## Working discipline (three hard rules, **read before making changes**)

**① Research first** — before creating or heavily changing a skill/process, you must first check **existing local skills** and **online sources (GitHub / plugin marketplaces)**
for an existing solution, and give the three-part conclusion: **what was found → adopt / do not adopt → why**.

- The reason for not adopting must be concrete (license restriction / conflicts with the current spec / maintenance cost / context burden); "not suitable" is not allowed
- When a reusable solution is found, **prefer reusing it or borrowing only the method**; do not rewrite
- Basis: three recurrences (the audio round's "what open-source options are there", the script skill's "check whether something exists before building", the retrospective skill's "online and offline")

**② Do not expand scope unprompted** — scope expansions the user did not ask for are **proposed only, not executed**;
a proposal must **explicitly label the cost** (skill table +N items, +X context per round, how many new files), and the user decides.

- Exception: when not expanding scope would cause a **wrong conclusion or a safety risk**, stop and explain first; do not continue on your own initiative
- Basis: the "borrow a body-level lint" → "revert" round trip

**③ Budget first** — **make a budget → report the budget → plan within the approved amount → book the spend → compare**, a five-step closed loop.
**① and ② unfinished means no work may start; ③ failing means no paid interface may be called.**

| Step | Action | Hard requirement |
|---|---|---|
| ① **Make a budget** | `python <pipeline>/scripts/budget_plan.py --budget <amount> --durations <seconds per shot> --plan-out <project>/spend-plan.md` | Must contain the **rework reserve** (default 25%, not optional) and the **tier lock** |
| ② **Report the budget** | Hand the plan to the user and get an **explicitly approved amount** | An empty "approved amount" field = **not approved = no work may start** |
| ③ **Plan within the amount** | Scope / shot count / duration / tier are **back-derived from the amount** | Not enough amount → **cut scope** (lower tier→fewer shots→shorter duration), **never exceed the budget** |
| ④ **Book the spend** | Book every expense immediately, **including rejections at 0** | Planning without booking actuals = the easiest place to fail (a past run overspent by 71% because of it) |
| ⑤ **Compare** | Settle: plan vs paid | More than **10%** over plan → stop and re-request approval; **switching tiers = changing the plan = re-requesting approval** |

```powershell
# 1) Make a budget (amount → plan; the price table is reused from cost_preflight.py, single source of truth)
python <pipeline>/scripts/budget_plan.py --budget 6 --durations 4,4,4 --anchors 1 --plan-out spend-plan.md
# 2) When you only know the amount, first work out the maximum number of shots
python <pipeline>/scripts/budget_plan.py --budget 6 --max-shots --duration-each 4
# 3) Book / check / gate (exit code 1 when over budget)
python <pipeline>/scripts/cost_ledger.py add --ledger <project>/cost_ledger.json --item "clip_01" --cost 1.20
python <pipeline>/scripts/cost_ledger.py check --ledger <project>/cost_ledger.json
```

- Verified: on a 6 CNY amount, **1 main-tier shot (4 seconds = 14.96 CNY) alone overspends by 2.5×** — not counting the money before planning inevitably leads to rework
- Division of labour: `budget_plan.py` amount→plan | `cost_preflight.py` storyboard→money | `cost_ledger.py` actuals→ledger
- Basis: **almost no project had a ledger**; one round paid 71% over plan
