---
name: video-editing
description: "Xiaohongshu/RED-tuned short-form video workflow for voice-over, talking-head, tutorials, explainer and science-popularisation videos assembled from external footage (official product demos, screen recordings) as well as self-shot material, interviews, podcasts, screen recordings, chroma key, B-roll, captions, and generated assets. Covers source ingest and conformance, hard and ping-pong loops, edit routing and runtime profiles, transcription and semantic review, multi-take and multicam sync/speaker switching, stabilization/dead-air cleanup, highlights/shorts, story/source/authorization gates, B-roll and generated-asset planning/review, storyboard animatics and shot handoffs, audio design/mix/loudness, captions, color/speed/J-L cuts, local upscale/interpolation review, reversible revisions, render/QA, multi-platform and size exports, publish packages, NLE handoff, and Remotion."
metadata: { "openclaw": { "emoji": "🎬", "os": ["darwin", "linux", "win32"], "requires": { "bins": ["ffmpeg", "python3"] }, "install": [{ "id": "ffmpeg-brew", "kind": "brew", "formula": "ffmpeg", "bins": ["ffmpeg"], "label": "Install FFmpeg (brew)" }] } }
---

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

# Video Editing Skill (V3)

Tuned for the three mainstream platforms **Xiaohongshu (RED) / Douyin / WeChat Channels**. An end-to-end pipeline **source ingest → voiceover → story restructure → platform gates → auto enrichment → render → three-platform export → title and body copy**, with parameters tuned to each platform's algorithm, aspect ratio, duration and review rules — not just an editing tool.

## V3 full pipeline (at a glance)

```
voiceover audio + silent footage
   │
   ├─→ project_bootstrap.py     raw footage dir → source inventory / project.md
   ├─→ edit_brief_plan.py       one-line user request → local script runbook / gates
   ├─→ runtime_preflight.py     workflow profile → FFmpeg/Node commands, encoders, filters / live gate
   ├─→ interlace_conform.py     interlace/telecine sampling → progressive working copy / full-length A/B gate
   ├─→ frame_rate_conform.py    phone/screen-recording VFR → full PTS detection / CFR working copy / live gate
   ├─→ black_edge_trim.py       head/tail black frames + silence coverage → CFR working copy / source-boundary proof + live gate
   ├─→ loop_fill.py             short clip → count/fixed-duration hard repeat / real-seam proof + full review gate
   ├─→ ping_pong_loop.py        short action → end-frame-deduped forward/reverse / turnaround + loop dual proof / live gate
   ├─→ clip_assembly.py         multi-source clips → single-pass canvas/CFR/SAR/audio normalisation / all-seam proof + live gate
   ├─→ edit_style_profile.py    personal/brand creative direction, rhythm and render/copy defaults → portable profile
   ├─→ production_authorization.py
   │                            exact footage/action/provider/rights basis → explicit authorisation + live gate
   ├─→ transcribe.py            transcription + word-level timestamps + mispronunciation flags
   ├─→ semantic_transcript_review.py
   │                            full-context review pack / minimal-patch verification / human choices gate
   ├─→ transcript_review.py     local media-synced HTML proofreading / CPS hints / review.txt write-back
   ├─→ takes_pack.py            multi-take / Scribe transcript → phrase-level reading view
   │                            speaker / audio_event editing beats
   ├─→ script_alignment.py      reviewed target script → multi-take verbatim candidates / choices / render_config
   │                            word/segment boundaries / transparency score / ambiguity and missing-footage gate
   ├─→ audio_sync.py            external audio track auto-alignment / audio replacement plan
   ├─→ multicam_sync.py         multicam → reference timeline / clock-drift evidence / alignment preview gate
   ├─→ multicam_switch.py       synced angles → normalised speaker audio / automatic director's rough cut / full review gate
   ├─→ scene_boundaries.py      fixed/adaptive visual cut points + per-cut evidence
   ├─→ visual_dedupe.py         multi-source scenes → perceptual-hash duplicate groups / keep suggestions / review gate
   ├─→ video_understanding.py   sampled frames + optional YOLO detection / tracks / scene_tags
   ├─→ video_stabilization.py   handheld footage → source-bound backend plan / working copy / full-length A/B gate
   ├─→ chroma_key.py            green/blue screen → composite + matte representative frames / human review / full render gate
   ├─→ highlight_picker.py      long-video highlight candidates / brief-query targeted segment search
   ├─→ audio_boundary_snap.py   selected segments → word/sentence-end/silence boundary correction
   ├─→ shorts_batch.py          highlight candidates → per-short render_config / render + QA job sheet
   ├─→ rough_cut.py             ASR rough cut: strip pure filler phrases / adjacent repeated sentences
   ├─→ hook_variants.py         first-three-second hook angle batch / recommended ranking / risk check
   ├─→ rewrite_script.py        LLM restructure into 5 parts (hook/pain/turn/value/cta)
   ├─→ content_guard.py         80+ platform landmine lint rules
   ├─→ source_receipts.py       factual claim → URL/screenshot source deck + publish gate
   ├─→ beat_sync.py             BGM beat-grid → auditable edit skeleton, or snap to existing cut points
   ├─→ speed_ramp.py            impact ranges → source-bound local speed-ramp plan / verification / apply
   ├─→ freeze_punch.py          impact frame → freeze replacement window / punch crop / unchanged audio timeline gate
   ├─→ auto_enrich.py           B-roll / chapter cards / stickers / emphasis points / BGM cuts / imagegen prompts
   │       └─→ Codex imagegen   automatic gpt-image-2 generation (abstract-concept illustration)
   ├─→ audio_cue_sheet.py       BGM / SFX audio design sheet / generation approval gate
   ├─→ audio_cue_mix.py         cue sheet + final narration → local/procedural SFX single track / 1× review gate
   ├─→ storyboard_plan.py       storyboard shot cards / generation routing / continuity anchors
   ├─→ storyboard_animatic.py   per-shot stills + optional narration → timed MP4 / 1× review / live gate
   ├─→ sequence_handoff.py      adjacent-shot receive/handoff / edit type / axis direction / live gate
   ├─→ reference_story_formula.py
   │                            reference timecode emotional mechanism → target storyboard content anchors / live gate
   ├─→ provider_capability.py   provider/surface/model capability contract / verification date / live gate
   ├─→ video_prompt_pack.py     Dreamina/Veo/LTX/Wan/Sora prompt pack / approval + capability gate
   ├─→ reference_frame_preflight.py
   │                            first-frame/style key size/orientation/transparent background/aspect ratio gate
   ├─→ generation_task_log.py   async generation task ledger / submit_id / download gate
   ├─→ generated_clip_review.py generated clip contact sheet / common-sense physics / continuity / regeneration gate
   ├─→ generation_chain_handoff.py
   │                            reviewed previous last frame → next shot's exact first frame / sequential generation live gate
   ├─→ generated_motion_window.py
   │                            short generated clip 0.25s freeze → active intervals / manual trim / live gate
   ├─→ scoped_video_edit_review.py
   │                            original vs local AI edit result / change-only + preserve invariants / A-B gate
   ├─→ generated_sequence_review.py
   │                            reviewed clips' adjacent last/first frames/preview / cross-shot continuity gate
   ├─→ final_audio_storyboard.py
   │                            locked visual EDL + original storyboard → final sound storyboard / voice ledger / live gate
   ├─→ narration_loudness_qa.py final isolated narration + exact phrase ranges → LUFS/spread/dBTP/LRA live gate
   ├─→ caption_speech_qa.py     subtitle_pack + isolated voice track → orphaned/misplaced/out-of-range subtitles live gate
   ├─→ subtitle_glyph_qa.py     subtitle_pack + explicit fonts → per-character cmap coverage / missing glyphs live gate
   ├─→ generation_lessons.py    reviewed clips → provider/model scoped prompt lessons library
   ├─→ storyboard_assets.py     asset task list / ready preflight / paid-quota reminder
   │                            optional media_library.py recommend ranks B-roll candidates
   ├─→ stock_material_plan.py   remote stock search planning
   │                            Pexels / Pixabay / Coverr query plan + asset logging hints
   ├─→ screen_focus.py          screen-recording clicks/hotspots → auto focus plan
   ├─→ pip_overlay.py           screen recording + facecam → PIP inset plan
   ├─→ color_grade.py           bounded colour grade plan / single-encode hook into render_final
   ├─→ jump_cut.py              adaptive pause removal + 20% removal budget + auditable cut list + 30 ms anti-pop fade
   ├─→ multimodal_dead_air.py   silence AND still frame → source-bound conservative removal / single encode / live gate
   ├─→ audio_transition.py      explicit J-cut/L-cut → source handle / hash / 1× listen gate
   ├─→ edit_revision.py         text edit artifact → source-bound approval / grouped apply / undo / redo
   ├─→ edit_recipe.py           reviewed render_config → typed-slot portable recipe / replay bound to new footage
   ├─→ edit_preflight.py        render_config/enrich_plan/cut list pre-render preflight gate
   ├─→ platform_safe_area_qa.py subtitles/PIP/CTA/markers → platform UI safe-area gate + SVG guide
   ├─→ subtitle_style_preview.py real source frame → final ASS preset comparison JPEGs / selection / live gate
   ├─→ render_final.py          single-encode render (optional voiceover denoise + enrich_plan/focus_events/pip_overlays + Heavy subtitles + loudness normalisation + BGM ducking)
   │                            optional --versioned-output prevents overwriting an older final cut
   ├─→ subtitle_render_review.py final MP4 + subtitle_pack → high-risk subtitle 1× clips / full-size frames / full review live gate
   ├─→ render_qa.py             post-render black frame/still frame/silence/size QA + review packet
   ├─→ stream_coverage_qa.py    final MP4 → decoded A/V head and tail PTS / frame count / container coverage live gate
   ├─→ encode_quality_qa.py     same timeline master vs re-encode → SSIM/PSNR / worst frame / live gate
   ├─→ audio_channel_qa.py      final cut channel activity/onset/balance/phase/mono fold-down live gate
   ├─→ audio_dropout_qa.py      final voice/mix short near-silence candidates / 1× WAV listen-through live gate
   ├─→ flash_safety_qa.py       final cut luminance/saturated-red flash → 1s/5s risk windows / live gate
   ├─→ temporal_artifact_qa.py  single-frame/few-frame return-to-state spikes → three-frame evidence / human audit / live gate
   ├─→ shot_color_qa.py         final cut shot luminance/contrast/chroma/saturation/broadcast-range + cut-point jump gate
   ├─→ retention_rhythm_qa.py   final cut hook activity / long shots / attention gaps / rhythm gate
├─→ hook_bridge_check.py       hook↔body three bridges (visual/audio/semantic) + opening phrasing lint / publish gate
   ├─→ reference_edit_rhythm.py reference film vs final cut hard-cut structure / contact sheets / live gate
   ├─→ speech_continuity_qa.py  second-pass ASR of final cut → cut-point repeats / near-duplicate takes / intra-sentence stutter gate
   ├─→ lip_sync_review.py       final master → 1×/0.25× lip evidence / human audit / live gate
   ├─→ review_proxy.py          low-bitrate full review MP4 / visible timecode / faststart
   ├─→ timeline_view.py         removed source segments / final cut points filmstrip + waveform retrospective image
   ├─→ edit_compare.py          original continuous clock vs final pixels / removed segments blacked out / mapping verification
   ├─→ subtitle_pack.py         SRT/VTT/ASS/JSON subtitle delivery pack (speed/offset aligned)
   ├─→ soft_subtitles.py        reviewed SRT → MP4 toggleable subtitle track / lossless audio-video copy / live receipt
   ├─→ subtitle_readability_qa.py
   │                            final subtitle CPS / duration / line length / overlap / media overflow gate
   ├─→ import_capcut_subtitles.py
   │                            CapCut auto-captions → transcript / gap cut list
   ├─→ srt_edit_plan.py         SRT + keep/drop edit instructions → render_config / cut list
   ├─→ project_resume.py        resume context pack / agent handoff / optional CLAUDE.md
   ├─→ review_dashboard.py      static HTML/JSON manual review panel / gate review queue
   ├─→ export_edl.py            render_config / cut list → EDL + manifest
   ├─→ export_fcpxml.py         render_config / cut list → FCPXML + manifest
   ├─→ export_otio.py           render_config / cut list → OTIO + manifest
   ├─→ framing_preview.py       master → per-platform cover/contain/blur real frames / selection / live gate
   ├─→ multi_export.py          reviewed framing strategy → Xiaohongshu 3:4 / Douyin 9:16 / Channels ≤60s
   ├─→ hdr_sdr.py               PQ/HLG HDR → source-bound Rec.709 SDR / full decode gate
   ├─→ video_enhancement.py     Lanczos upscale + optional motion interpolation / full-length A-B + with-audio review gate
   ├─→ delivery_encode.py       source-bound two-pass H.264/AAC / hard size cap / full decode gate
   ├─→ gif_preview.py           short video excerpt → palette GIF / source-bound receipt / full decode
   ├─→ generate_caption.py      title + 200-500 character body + 3-6 tags + posting window
   ├─→ cover_variants.py        2-4 cover sets / feed-size preview / final selection gate
   ├─→ approval_receipt.py      reviewed deliverable → SHA-256 receipt / stale approval gate
   └─→ publish_package.py       platform video/cover/subtitle/chapter/copy upload pack + gate status
```

**Complete prompt template for shipping one short video a day**: [docs/prompts/15-xhs-daily-tech-video.md](docs/prompts/15-xhs-daily-tech-video.md) (recommended entry point).

For image generation, prefer Codex's built-in `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).

## V3 new scripts at a glance (in call order)

| Script | Responsibility | Key CLI |
|---|---|---|
| `project_bootstrap.py` | raw footage dir → project structure / source inventory / project.md | `--source raw_dir` `--project-dir work/project-a` `--mode copy|hardlink` `--strict` |
| `runtime_preflight.py` | workflow profile → local FFmpeg/Node commands, encoders, filters, environment-drift live gate | `list-profiles` / `analyze --profile ... --output --strict` / `verify --report --strict` |
| `edit_brief_plan.py` | natural-language edit request → local script runbook / commands / manifest gate | `--brief` `--brief-file` `--source-media` `--platform` `--markdown` `--strict` |
| `interlace_conform.py` | interlace/telecine multi-segment detection → bwdif/yadif progressive working copy, full-length A/B human confirmation and live gate | `analyze` / `plan --mode frame|field --parity` / `apply` / `confirm` / `verify --strict` |
| `frame_rate_conform.py` | phone/screen-recording VFR → decoded PTS cadence, exact CFR working copy, frame count, A/V start and end live gate | `plan <source> --fps 30 --delivery` / `apply <plan>` / `verify <plan> --strict` |
| `black_edge_trim.py` | head/tail blackdetect + silence coverage → source-bound CFR working copy, source-boundary proof and human live gate | `plan --delivery --edge-proof` / `apply` / `confirm` / `verify --strict` |
| `loop_fill.py` | progressive CFR short footage → repeat by count/target duration, first real seam 1× proof, full review and live gate | `plan --times|--duration --delivery --seam-proof` / `apply` / `confirm` / `verify --strict` |
| `ping_pong_loop.py` | progressive CFR short action → end-frame-deduped forward/reverse cycle, turnaround/loop dual proof, cache cap and live gate | `plan --start --end --cycles|--duration --delivery --turnaround-proof --loop-seam-proof` / `apply` / `confirm` / `verify --strict` |
| `clip_assembly.py` | multi-source video → single-pass canvas/CFR/SAR/timestamp/audio normalisation, all-seam 1× proof and live gate | `plan <clips...> --delivery --boundary-proof` / `apply` / `confirm` / `verify --strict` |
| `podcast_audiogram.py` | audio-only excerpt + still cover + SRT relative to the excerpt → vertical MP4 with helper waveform and source-bound receipt | `render <audio> <cover> <captions.srt> --start --duration --output --receipt` / `verify <receipt>` |
| `gif_preview.py` | short video excerpt → silent, palette-optimised GIF and source-bound receipt | `render <video> --start --duration --output --receipt` / `verify <receipt>` |
| `edit_style_profile.py` | personal/brand creative direction, edit rhythm, render/copy defaults → path-free portable profile / digest verification / defaults-only merge | `template` / `create --spec` / `verify --profile --strict` / `apply --config --receipt` |
| `production_authorization.py` | external upload, invasive editing, paid generation, voice cloning, real people/IP and publishing → source-bound authorisation gate | `prepare --scope --response-template` / `audit --request --response --strict` / `verify --report --strict` |
| `transcript_review.py` | transcript → text or locally media-synced HTML proofreading → reviewed transcript | `export` / `html --video --max-cps` / `apply --review --output` |
| `semantic_transcript_review.py` | transcript → context review pack / minimal-patch audit / human choices / reviewed transcript | `prepare` / `audit --strict` / `apply --choices` |
| `_internal_text_guard.py` | blocks internal tokens from reaching the frame | internal module, called automatically by render_final |
| `content_guard.py` | platform landmine lint | `--script` `--title` `--caption` `--strict` |
| `source_receipts.py` | factual claim → URL/screenshot proof deck, Markdown/HTML and publish gate | `--claims source_claims.json` `--html` `--require-primary-source` `--strict` |
| `takes_pack.py` | multi-take / top-level Scribe words → phrase-level Markdown/JSON, preserving speaker/audio events | `--transcript take1=...` `--transcripts-dir` `--json` `--break-gap` |
| `script_alignment.py` | reviewed target script → multi-take word/segment boundary candidates, human choices, render_config and gate | `--target-script` `--transcript label=...` `--media label=...` `--choices` `--render-config` `--strict` |
| `audio_sync.py` | scratch audio + external audio track → offset / audio replacement commands / gate | `--reference-media` `--external-audio` `--replace-output` `--apply` `--strict` |
| `multicam_sync.py` | multicam → offset/coverage/audio-track selection/pairwise/optional clock drift/alignment preview gate | `--reference-media` `--angle` `--manual-offset` `--measure-clock-drift` `--preview-output` `--apply-preview` `--strict` |
| `multicam_switch.py` | ready sync plan + speaker-mapped angle audio → source-bound automatic director's rough cut, full decode and human live gate | `plan --sync-plan --speaker --program-audio --delivery` / `apply` / `confirm` / `verify --strict` |
| `rough_cut.py` | transcript rough cut: strip filler phrases/repeated sentences | `--transcript` `--cut-list` / `--input` `--output` |
| `scene_boundaries.py` | FFmpeg fixed/adaptive scene score → scene boundaries + cut evidence | `--method adaptive` `--adaptive-threshold` `--min-scene-score` `--min-scene-duration` |
| `visual_dedupe.py` | three-point perceptual hash across multi-source scenes → duplicate groups / keep suggestions / review gate | `--manifest` / `<videos...>` `--hamming-threshold` `--include-same-source` `--strict` |
| `video_understanding.py` | sampled frames + optional YOLO object detection + lightweight tracklets | `--detector yolo` `--scene-boundaries` `--external-detections` `--strict` |
| `video_stabilization.py` | source video → exact FFmpeg backend / source hash / stabilised working copy / full-length A/B review gate | `doctor` / `plan --decision` / `apply --comparison` / `confirm` / `verify --strict` |
| `chroma_key.py` | green/blue-screen foreground + image/video background → composite/matte preview, four-point human review, source-bound full render gate | `prepare --foreground --background --output-video` / `review` / `apply` / `verify --strict` |
| `highlight_picker.py` | long-video highlight candidates + brief/query targeted segment search | `--transcript` `--brief`/`--query` `--scene-boundaries` `--render-config` |
| `audio_boundary_snap.py` | selected highlights → word-level, sentence-end and silence boundary correction + blocker | `--candidates` `--transcript` `--media` `--markdown` `--strict` |
| `shorts_batch.py` | highlight_candidates → multiple short-video render_config / render + QA job sheet | `--highlights` `--video` `--render-config-dir` `--output-dir` `--strict` |
| `hook_variants.py` | transcript/clean_script → 8 first-three-second hook angles, risk check and recommended ranking | `--transcript` `--topic` `--platform` `--output` `--markdown` `--strict` |
| `rewrite_script.py` | LLM 5-part restructure + verification | `--transcript` `--structure` `--hook-template` `--emit-prompt` / `--llm-output` |
| `auto_broll.py` | B-roll scheduling | `--transcript` `--assets` `--max-single-shot` |
| `media_library.py` | local asset library index + B-roll candidate recommendation + asset provenance logging | `init` `scan` `search` `recommend --category broll --json` `import` `annotate` |
| `stock_material_plan.py` | topic/script → Pexels/Pixabay/Coverr stock query plan | `--subject` `--script` `--provider` `--media-library` `--output` `--markdown` |
| `auto_chapter_cards.py` | chapter card PNGs | `--script` `--audio` `--style` `--output-dir` |
| `auto_stickers.py` | emotion → stickers | `--transcript` `--min-interval` |
| `auto_emphasis.py` | questions/numbers/turns/conclusions → badge + subtle push-in cues | `--transcript` `--output` `--markdown` |
| `beat_sync.py` | BGM → beat edit slots / Markdown review, or snap to existing cut points | `--bgm --generate-plan --beats-per-cut 4 --output ... --markdown ...` / `--cuts --window` |
| `speed_ramp.py` | impact ranges → snap/ease/s-curve/hold plan, digest verification and local transactional apply | `plan <video> --ramp --hold --interpolate-fps` / `verify --strict` / `apply --output --receipt` |
| `freeze_punch.py` | impact frame → source-bound freeze emphasis, slight anchor crop, unchanged duration/audio and final-cut live gate | `plan <video> --freeze --delivery` / `apply <plan>` / `verify --strict` |
| `auto_enrich.py` | orchestrates B-roll / stickers / emphasis points / chapter cards / imagegen cues | `--transcript` `--clean-script` `--bgm` `--output` |
| `imagegen_hint.py` | detects abstract concepts → produces gpt-image-2 prompts | `--transcript` `--clean-script` `--codex-md` |
| `audio_cue_sheet.py` | transcript → BGM/SFX cues, generation approval and audio gates | `--transcript` `--asset-root` `--require-local-music` `--require-local-sfx` `--strict` |
| `audio_cue_mix.py` | audio cue sheet + final narration → local/procedural SFX, 48 kHz stereo single track, full listen-through live gate | `plan --cue-sheet --voice --delivery [--synthesize-missing]` / `apply` / `confirm` / `verify --strict` |
| `final_audio_storyboard.py` | locked visual EDL + storyboard → final-timeline sound storyboard, voice ledger, omitted-story decisions and live gate | `prepare --edl --storyboard` `audit --request --response` `verify --report --strict` |
| `storyboard_plan.py` | transcript/clean_script → storyboard shot cards + generation routing | `--transcript` `--clean-script` `--output` `--markdown` |
| `reference_story_formula.py` | reference video + timecoded transcript + target storyboard → emotional/narrative mechanism, per-shot content anchors, copy-exclusion policy and source-bound live gate | `prepare --reference-video --reference-transcript --target-storyboard --response-template` / `audit --request --response` / `verify --report --strict` |
| `storyboard_animatic.py` | storyboard + per-shot stills + optional narration → source-bound timed MP4, full 1× review and live gate | `plan --panel SHOT=PATH --delivery` / `apply` / `confirm` / `verify --strict` |
| `sequence_handoff.py` | storyboard → adjacent-shot relay, edit boundaries, 180° axis/direction, audio bridges, edit handles and source-bound live gate | `prepare --storyboard --response-template` `audit --request --response` `verify --report --strict` |
| `provider_capability.py` | exact provider/surface/model → dated mode/aspect ratio/duration/resolution/reference-limit capability contract live gate | `verify --bundle --max-age-days --output --markdown --strict` |
| `video_prompt_pack.py` | storyboard_plan → multi-provider video generation prompt pack + reviewed story formula/boundaries/real last-frame handoff + character/brand/style lock + paid approval/capability gate | `--storyboard-plan` `--project-dir` `--reference-story-formula` `--sequence-handoff` `--generation-chain-handoff` `--capability-profile` `--approved` `--strict` |
| `reference_frame_preflight.py` | video_prompt_pack → first-frame/style key existence, decode, size, orientation, aspect ratio, transparent background gate | `--prompt-pack` `--require-style-reference` `--reference shot_id=...` `--strict` |
| `generation_reference_preflight.py` | prompt pack + image/video/audio reference manifest + provider profile → type/count/duration/mode/@tag role source-bound gate | `template --prompt-pack` / `analyze --references --capability-profile --strict` / `verify --report --strict` |
| `generation_task_log.py` | async generation task ledger: submit_id/task id, polling, download, local persist gate | `add` `update` `import-provider-decision` `report --strict` |
| `generated_clip_review.py` | source-bound visual review of a generated clip: contact sheet, scoring, crop range, regeneration suggestions | `prepare --clip/--asset-manifest` `audit --request --response` `verify --report --strict` |
| `generation_chain_handoff.py` | reviewed generated clip + reviewed adjacent boundary → exact last-frame PNG, human confirmation, next-shot exact first-frame live gate | `prepare --clip-review --sequence-handoff --boundary` / `confirm --decision use_exact_start_frame` / `verify --strict` |
| `generated_motion_window.py` | all-frame freeze of a short generated clip → active intervals, human trim/keep/reject, frame-accurate working copy and live gate | `analyze <clip>` `confirm --decision` `apply --output` `verify --strict` |
| `scoped_video_edit_review.py` | original + local AI edit result → same-timestamp A/B evidence, single change target, item-by-item preservation review and live gate | `prepare --source --edited --change --preserve` `audit --request --response` `verify --report --strict` |
| `generated_sequence_review.py` | cross-shot continuity review of reviewed generated clips: real last/first frames, side-by-side grids, silent boundary preview and live gate | `prepare --clip-review [--storyboard-plan]` `audit --request --response` `verify --report --strict` |
| `generation_lessons.py` | extracts explicitly approved provider/model/category lessons from the canonical generated-clip review, and offers them to the next prompt pack | `add --review --clip-id --lesson --approved-by [--supersedes]` `verify --strict` `select --provider [--model]` |
| `storyboard_assets.py` | storyboard_plan → asset list + ready/paid preflight | `--storyboard-plan` `--asset-root` `--output` `--strict` |
| `screen_focus.py` | screen-recording clicks/hotspots → focus zoom enrich plan | `--events` `--event` `--screen-width` `--output` |
| `pip_overlay.py` | screen recording + facecam → PIP camera inset enrich plan | `--camera` `--segment` `--sync-offset` `--output` |
| `color_grade.py` | bounded colour grade plan + FFmpeg filter + optional re-version of an existing master | `--preset` `--output` `--markdown` `--render-output` `--strict` |
| `jump_cut.py` | adaptive silence detection → pause-removal plan / removal budget gate / final cut + audio fade at cut points | `<input.mp4>` `--dry-run` `--cut-list cuts.json` `--strict` / `--output jumpcut.mp4` `--max-removal-ratio 0.20` `--allow-over-budget` |
| `multimodal_dead_air.py` | silence + still-frame intersection → source-bound dead-air plan / live verify / single-encode working copy | `plan --delivery --output --markdown --strict` / `verify --strict` / `apply --markdown` |
| `audio_transition.py` | render_config → explicit J-cut/L-cut source handles / hash / single-encode apply / receipt | `plan --transition AFTER_CLIP,TYPE,DURATION` / `apply --output` / `verify --receipt --strict` |
| `edit_revision.py` | text artifacts such as render_config/enrich_plan → source-bound proposal / separate approval / grouped apply / undo / redo | `prepare --artifact --depends-on` / `audit --strict` / `apply --approval` / `status|undo|redo` |
| `edit_recipe.py` | reviewed render_config → typed-slot path-free recipe / digest verification / replay bound to new footage | `export --config --name` / `verify --recipe` / `replay --bind SLOT=PATH --receipt --strict` |
| `edit_preflight.py` | render_config/enrich_plan/cut list pre-render preflight gate | `--config render_config.json` `--enrich-plan enrich_plan.json` `--output edit_preflight.json` `--strict` |
| `platform_safe_area_qa.py` | subtitles, badges, PIP, CTA, chapter cards, markers → platform UI occlusion gate + SVG guide | `--config` `--enrich-plan` `--elements` `--platform xhs|douyin|wxch` `--guide` `--strict` |
| `subtitle_style_preview.py` | real source frame → `render_final.py` final ASS style comparison JPEGs, human selection and source/font/style live gate | `create --video --platform --preview-dir --require-selection` / `select --report --style` / `verify --strict` |
| `render_final.py` | single-encode render + optional personal style defaults / voiceover denoise / J-cut/L-cut / enrich_plan / narration-driven BGM ducking | `--config render_config.json` `--style-profile work/edit_style_profile.json` `--audio-transition-plan audio_transition_plan.json` `--speech-denoise light` `--enrich-plan enrich_plan.json` `--bgm-ducking` `--output final.mp4` |
| `subtitle_render_review.py` | final MP4 + subtitle_pack → high-risk subtitle context clips / midpoint full-size frames, full 1× human review and source-bound live gate | `prepare --video --subtitle-pack --proof-dir` / `audit --request --response --strict` / `verify --report --strict` |
| `render_qa.py` | post-render QA: size/audio/black frames/still frames/silence + review packet | `<video.mp4>` `--platform douyin` `--json qa.json` `--review-dir verify/qa` |
| `stream_coverage_qa.py` | final MP4 → full decode, head/tail PTS, A/V and container coverage, video frame count source-bound live gate | `analyze <video> --output --markdown [--expected-duration --expected-video-frames] --strict` / `verify --report --strict` |
| `encode_quality_qa.py` | same-timeline reference master vs re-encode → full-length SSIM/PSNR, P05, worst-frame timecode and dual-input live gate | `analyze <reference> <candidate> --output --markdown --strict` / `verify --report --strict` |
| `video_enhancement.py` | existing video → local Lanczos upscale, optional `minterpolate` frame interpolation, full-length A/B and human review with audio live gate | `plan <video> --target-short-edge|--scale [--fps] --enhanced --comparison` / `apply` / `confirm` / `verify --strict` |
| `flash_safety_qa.py` | final/platform video → large-area luminance/saturated-red flash, rolling 1s/5s risk windows and source-bound live gate | `analyze <video> --output --markdown --strict` / `verify --report --strict` |
| `temporal_artifact_qa.py` | final/generated video → single-frame/few-frame local spike screening, before/suspect/after evidence and human decision live gate | `analyze <video> --evidence-dir --response-template --strict` / `audit --report --response` / `verify --strict` |
| `shot_color_qa.py` | rendered master → per-shot luminance/contrast/chroma/saturation/broadcast-range and cut-point jump gate | `<video.mp4>` `--scene-boundaries` `--output shot_color_qa.json` `--markdown` `--strict` |
| `retention_rhythm_qa.py` | final cut hook activity, long shots, attention gaps, even-spacing/fast-cut and subtitle rhythm risk | `<video.mp4>` `--timed-text subtitles.json` `--output retention_rhythm_qa.json` `--strict` |
| `hook_bridge_check.py` | hook-to-body coherence: visual bridge (perceptual hash), audio bridge (cut-point level jump/fade-out), semantic bridge (human declaration), plus opening begging/meta-talk lint | `<video.mp4>` `--hook-end` `--body-start` `--first-line` `--claim` `--out` `--markdown` `--strict` |
| `reference_edit_rhythm.py` | reference film vs final cut hard-cut density, shot duration, ending hold, normalised cut points and contact-sheet source-bound comparison | `analyze --reference --candidate --evidence-dir [--require-match]` / `verify --report --strict` |
| `speech_continuity_qa.py` | second-pass transcript of the final cut → repeated lines / near-duplicate takes / intra-sentence stutter gate | `<final_transcript.json>` `--output speech_continuity_qa.json` `--markdown` `--strict` |
| `lip_sync_review.py` | final master → complete-phrase 1× with audio / 0.25× silent proof, human lip-sync audit and source-bound live gate | `prepare --video --segment --anchor --proof-dir` / `audit --request --response --strict` / `verify --report --strict` |
| `review_proxy.py` | master/platform MP4 → low-bitrate timecoded review video + JSON/Markdown | `<video.mp4>` `--output verify/review_proxy.mp4` `--dry-run` `--no-timecode` |
| `audio_master_report.py` | final-cut loudness report: LUFS / true peak / LRA / long-silence gate | `<video.mp4>` `--output audio_master_report.json` `--markdown audio_master_report.md` `--strict` |
| `audio_channel_qa.py` | final master → channel activity, onset misalignment, L/R balance, phase correlation and mono fold-down source-bound live gate | `analyze <media> --output --markdown --strict` / `verify --report --strict` |
| `audio_dropout_qa.py` | final voice/mix → 20 ms short near-silence candidates, 1× WAV evidence, human listen-through and source-bound live gate | `analyze <media> --evidence-dir --response-template --strict` / `audit --response` / `verify --strict` |
| `narration_loudness_qa.py` | per-phrase consistency of the final isolated narration: target LUFS / segment spread / true peak / LRA source-bound live gate | `analyze <media> --segments <json> --output --markdown --strict` / `verify --report --strict` |
| `caption_speech_qa.py` | subtitle_pack cues → isolated voice activity coverage, head/tail silence, internal pauses and timeline overflow source-bound live gate | `analyze <isolated-speech> --subtitle-pack <json> --output --markdown --strict` / `verify --report --strict` |
| `subtitle_glyph_qa.py` | subtitle_pack + explicit project fonts → per-character Unicode cmap coverage, missing-glyph/fallback list and source-bound live gate | `analyze --subtitle-pack --font [--fallback-font] --output --strict` / `verify --report --strict` |
| `timeline_view.py` | visual retrospective of removed source segments / final cut points | `<video.mp4>` `--at 42.5` `--output view.png` / `--rendered-cut-list cuts.json` `--output-dir verify/` |
| `edit_compare.py` | original continuous clock vs final pixels two-column video; removed segments blacked out and mapping verified | `<source.mp4> <final.mp4>` `--cut-list` `--output-speed` `--output-offset` `--output` |
| `subtitle_pack.py` | transcript/render_config → SRT/VTT/ASS/JSON subtitle pack | `--transcript work/transcript.json --output-dir output/subtitles` / `--config render_config.json --speed 1.25 --offset 2.0` |
| `soft_subtitles.py` | reviewed SRT aligned to the final timeline → toggleable `mov_text` MP4 subtitle track; copies the original video/audio streams and verifies content and timecodes | `mux final.mp4 final.srt --language zho --output final-soft.mp4 --receipt soft-subtitles.json` / `verify soft-subtitles.json` |
| `subtitle_readability_qa.py` | output-aligned subtitles → CPS, duration, line length, overlap and media overflow gate | `<subtitle_pack.json>` `--media final.mp4` `--output subtitle_readability_qa.json` `--strict` |
| `import_capcut_subtitles.py` | CapCut auto-captions or SRT → transcript + gap cut list | `--draft <draft_dir>` / `--srt captions.srt` `--transcript work/capcut_transcript.json` `--cut-list work/capcut_gap_cut.json` |
| `srt_edit_plan.py` | SRT + human/agent keep/drop instructions → edit plan / render_config / cut list | `--srt captions.srt --guide edit_guide.md --source-media origin/talking.mp4 --render-config work/render_config.json --strict` |
| `project_resume.py` | local artifacts → resume context pack / agent handoff | `--project-dir work/project-a --markdown work/project-a/project_resume.md --agent-note work/project-a/CLAUDE.md` |
| `review_dashboard.py` | local artifacts → static HTML/JSON review queue + gate snapshot | `--project-dir work/project-a --html work/project-a/review_dashboard.html --strict` |
| `export_edl.py` | NLE handoff: export EDL + manifest | `--config render_config.json --output edit.edl` / `--cut-list rough_cut.json --output rough.edl` |
| `export_fcpxml.py` | NLE handoff: export FCPXML + manifest | `--config render_config.json --output edit.fcpxml` / `--cut-list rough_cut.json --output rough.fcpxml` |
| `export_otio.py` | NLE handoff: export OpenTimelineIO `.otio` + manifest | `--config render_config.json --output edit.otio` / `--cut-list rough_cut.json --output rough.otio` |
| `framing_preview.py` | master → per-platform cover/contain/blur real-frame previews, human selection and source/filter/preview live gate | `create --video --platforms --require-selection` / `select --platform --strategy` / `verify --strict` |
| `multi_export.py` | three-platform export, can consume a reviewed framing strategy | `<input.mp4>` `--platforms xhs douyin wxch` `--framing-preview work/framing_preview.json` |
| `hdr_sdr.py` | PQ/HLG master → source-bound Hable tone-map / BT.709 limited tags / full decode gate | `plan --delivery` / `apply` / `verify --strict` |
| `delivery_encode.py` | master → target-size H.264/AAC MP4 / source hash / two-pass bitrate / full decode gate | `plan --delivery --max-size-mib` / `apply` / `verify --strict` |
| `generate_caption.py` | title/body/tags | `--script` `--profile` `--output` |
| `cover_variants.py` | multiple cover A/B options, personal style defaults, feed-size preview, title synergy and final selection | `<video>` `--title` `--style-profile` `--caption` `--platform` `--render` `--select cover-c` `--strict` |
| `approval_receipt.py` | reviewed video/cover/copy/subtitles/QA → SHA-256 receipt and stale-approval gate | `create --artifact ... --approved-by` / `verify --strict` |
| `publish_package.py` | publish upload pack: platform video, cover, subtitles, chapters, copy and gate status | `--project-dir` `--platforms` `--video xhs=...` `--strict` |
| `profiles/__init__.py` | audience profile loading | `load_profile("tech_pro")` |

## V3 new render_final.py flags

| Flag | Default | Notes |
|---|---|---|
| `--profile tech_pro` | off | Loads the rhythm/subtitle/BGM defaults from [scripts/profiles/tech_pro.yaml](scripts/profiles/tech_pro.yaml) |
| `--style-profile work/edit_style_profile.json` | off | Validates the personal/brand edit style profile and only fills controlled fields that are missing or `null` in the config |
| `--primary-speed 1.25` | 1.0 | Primary output speed. `--speed` can still add extra variants |
| `--no-loudnorm` | omitted = loudness normalisation on | Turns off `dynaudnorm + acompressor + loudnorm` |
| `--speech-denoise light|medium|strong` | off | 80 Hz high-pass + 6/9/12 dB `afftdn`, cleaning the steady noise floor before speed change/loudness/BGM ducking; `--no-speech-denoise` overrides the config |
| `--no-content-guard` | omitted = lint on | Turns off platform rule checking (not recommended) |
| `--subtitle-style karaoke` | normal | Word-by-word karaoke subtitles |
| `--enrich-plan work/enrich_plan.json` | off | May be passed repeatedly; automatically wires in B-roll / chapter cards / stickers / generated images / focus_events / pip_overlays |
| `--color-grade work/color_grade.json` | off | Wires in `color_grade.py` output or a preset, placed before subtitles/HUD |
| `--bgm-ducking` | off | Uses the final narration track to trigger the FFmpeg sidechain and dynamically lower the BGM; `--no-bgm-ducking` overrides the config |
| `--versioned-output` | off | Writes to the next `<name>_V<N>.mp4`, so the previous final cut is never overwritten |

## V3 past production lessons (now encoded as default behaviour)

| Lesson | How V3 solves it |
|---|---|
| An internal token such as `1.25x` leaked into the top of the frame | `_internal_text_guard` rejects it automatically; the rule lives in [scripts/_internal_text_guard.py](scripts/_internal_text_guard.py) |
| Subtitle font Hiragino W3 was too thin | `find_chinese_font()` default order: Source Han Sans Heavy > Smiley Sans > STHeiti Medium > PingFang Semibold |
| The middle section became unintelligible after the speed-up | render_final now defaults to `dynaudnorm=f=250:g=15 + acompressor=threshold=-18dB:ratio=3 + loudnorm=I=-16:TP=-1.5:LRA=11` |
| Wanted 1.25× as the primary output but `--speed` still left a 1.0× | Added `--primary-speed` as a first-class flag |
| Wrong Whisper words inside subtitles (ChatGPTT and similar) | `rewrite_script.py` prefers the cleaned script; the raw Whisper words are used only for timestamps |
| Platform-banned words were only discovered after reaching a rate limit | `content_guard.py` lints automatically before rendering |

---

# Legacy (V2) reference material

What follows is the workflow documentation from the V2 era. It is still valid, but for daily use start with docs/prompts/15.

## Prerequisites

Before doing anything, run the environment check:

```bash
python3 scripts/utils.py
```

This auto-detects the platform (macOS/Linux/WSL/Windows), the GPU type, available encoders and the Whisper engine, and prints a diagnostic report.

## Prerequisites

Before doing anything, run the environment check:

```bash
python3 scripts/utils.py
```

This auto-detects the platform (macOS/Linux/WSL/Windows), the GPU type, available encoders and the Whisper engine, and prints a diagnostic report.

If dependencies are missing, tell the user to install them:
- **ffmpeg**: `brew install ffmpeg` (macOS) or `apt install ffmpeg` (Linux/WSL) or download the Windows build
- **whisper**:
  - **Apple Silicon (M1/M2/M3/M4)**: `pip install mlx-whisper` (recommended, fastest with Metal acceleration)
  - **NVIDIA / CPU**: `pip install faster-whisper` (recommended, about 4x faster) or `pip install openai-whisper`
- **Users in China**, faster install (Apple Silicon): `pip install mlx-whisper -i https://pypi.tuna.tsinghua.edu.cn/simple`
  Other platforms: `pip install faster-whisper -i https://pypi.tuna.tsinghua.edu.cn/simple`

If the project root has a `.venv` virtual environment, activate it before running Python scripts:
```bash
source .venv/bin/activate  # macOS/Linux/WSL
# Windows: .venv\Scripts\activate
```

### Platform notes

- **macOS (Apple Silicon)**: uses VideoToolbox hardware encoding automatically; the Whisper engine picks `mlx-whisper` (when installed) and the large-v3-turbo model is recommended (via `mlx-community/whisper-large-v3-turbo`)
- **Subtitle fonts (short video)**: prefer Chinese fonts at Heavy / Medium weight: `Source Han Sans SC Heavy` / `Smiley Sans` from the user's font library > system `STHeiti Medium` > `PingFang SC Semibold`. If none are present it downloads the Heavy weight from [adobe-fonts/source-han-sans](https://github.com/adobe-fonts/source-han-sans) and caches it under `<CACHE_DIR>/video-editing/fonts/`. Never default to a thin face such as Hiragino W3 again
- **macOS (Intel)**: uses VideoToolbox encoding; Whisper runs in CPU mode
- **Linux**: auto-detects NVIDIA GPU (NVENC), Intel QSV and AMD AMF
- **WSL**: supported; auto-detects the Windows font path (`/mnt/c/Windows/Fonts/`)
- **Windows**: running inside WSL2 is recommended; QSV/AMF hardware encoding is supported
- **No discrete GPU (integrated graphics)**: Intel iGPU uses QSV encoding, AMD iGPU uses AMF encoding; for Whisper prefer the `medium` model rather than `large`
- **Users in China**: auto-detects the China region and uses the Tsinghua PyPI mirror and the HuggingFace mirror to download models; `--mirror` forces it on

### Linux GPU setup guide (NVIDIA / Intel Arc)

Using GPU acceleration for Whisper speech recognition on Linux needs a different setup per graphics card. Running `python3 scripts/utils.py` auto-detects the card model and prints a recommendation, but if you hit problems, use the options below.

#### Option A: NVIDIA 40-series cards (RTX 4060 / 4070 / 4080 / 4090)

The 40 series (Ada Lovelace architecture, Compute Capability 8.9) has the most mature faster-whisper support and works out of the box.

**Install steps:**
```bash
# 1. Install the NVIDIA driver (535+) and CUDA Toolkit 12.4+
sudo apt install nvidia-driver-535 nvidia-cuda-toolkit
# or install the latest driver from the NVIDIA site: https://www.nvidia.com/drivers

# 2. Verify CUDA
nvidia-smi  # should show the driver version and CUDA version

# 3. Install faster-whisper (installs a matching CTranslate2 automatically)
pip install faster-whisper>=1.1.0
```

**Configuration notes:**
- CUDA Toolkit: 12.4+ (12.6 recommended)
- CTranslate2: >= 4.5.0 (installed automatically with faster-whisper)
- Compute precision: `float16` (default), `int8_float16` and `int8` all work
- Whisper model: `large-v3` recommended (VRAM >= 6GB)
- No special configuration needed; `python3 scripts/transcribe.py` auto-detects and uses CUDA

#### Option B: NVIDIA 50-series cards (RTX 5060 / 5060 Ti / 5070 / 5080 / 5090)

The 50 series (Blackwell architecture, Compute Capability 12.0, sm_120) needs extra care with the CUDA version and the compute precision setting.

**Known issue:**
On Blackwell, CTranslate2 with INT8 precision fails with `cuBLAS failed with status CUBLAS_STATUS_NOT_SUPPORTED`, because Blackwell's INT8 Tensor Cores require matrix dimensions aligned to a multiple of 16. CTranslate2 >= 4.7.1 fixes this, but to be safe this tool automatically uses `float16` precision when it detects a 50-series card.

**Install steps:**
```bash
# 1. Install the NVIDIA driver (565+, must support Blackwell)
#    Download the latest driver from the NVIDIA site: https://www.nvidia.com/drivers
#    or install version 565 or newer from the package manager
sudo apt install nvidia-driver-565

# 2. Install CUDA Toolkit 12.8+ (the Blackwell minimum)
#    Recommended from the NVIDIA site: https://developer.nvidia.com/cuda-downloads
#    Choose Linux > x86_64 > Ubuntu > deb (network)

# 3. Verify CUDA
nvidia-smi  # should show CUDA 12.8+

# 4. Install faster-whisper and the latest CTranslate2
pip install faster-whisper>=1.1.0
pip install --upgrade ctranslate2>=4.7.1  # make sure the Blackwell fix is included

# 5. If it still fails, force float16 precision (this tool already does it automatically)
#    Manual test:
python3 -c "
from faster_whisper import WhisperModel
model = WhisperModel('tiny', device='cuda', compute_type='float16')
print('CUDA float16 OK')
"
```

**Configuration notes:**
- CUDA Toolkit: >= 12.8 (13.0+ recommended, 13.2 is the latest)
- NVIDIA driver: >= 565
- CTranslate2: >= 4.7.1 (includes the INT8 padding fix)
- Compute precision: `float16` recommended (most stable); `int8_float16` may work on CTranslate2 >= 4.7.1
- If `int8` still fails, the tool automatically falls back to `float16`
- Whisper model: `large-v3` recommended (VRAM >= 6GB)
- `utils.py` auto-detects 50-series cards (by looking for "RTX 50" in the GPU name reported by `nvidia-smi`) and picks the safe `float16` precision

**Troubleshooting:**
If you get a `CUBLAS_STATUS_NOT_SUPPORTED` error:
1. Confirm the CTranslate2 version is >= 4.7.1: `python3 -c "import ctranslate2; print(ctranslate2.__version__)"`
2. Confirm the CUDA version is >= 12.8: `nvidia-smi` or `nvcc --version`
3. Try forcing `--compute-type float16` manually (when calling transcribe.py directly)
4. Confirm the driver version is >= 565: check Driver Version in `nvidia-smi`

#### Option C: Intel Arc discrete cards (A770 / A750 / B580)

Intel Arc cards **do not support CUDA**, so faster-whisper (which depends on CTranslate2/CUDA) cannot use GPU acceleration directly on Intel Arc.
An alternative is required.

**Recommended: OpenVINO + Whisper (easiest)**
```bash
# 1. Install OpenVINO
pip install openvino openvino-genai

# 2. Use the OpenVINO GenAI WhisperPipeline
python3 -c "
import openvino_genai as ov_genai
pipe = ov_genai.WhisperPipeline('OpenVINO/whisper-large-v3-fp16-ov', device='GPU')
result = pipe.generate('audio.wav', language='<|zh|>')
print(result.texts[0])
"

# 3. Or use a pre-converted model from Hugging Face
pip install optimum[openvino]
# Download an OpenVINO-format Whisper model from HuggingFace
# https://huggingface.co/OpenVINO/whisper-medium-int8-ov
```

**Alternative: whisper.cpp + SYCL (better performance, more complex setup)**
```bash
# 1. Install the Intel oneAPI Base Toolkit
#    https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit-download.html
wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
  | gpg --dearmor | sudo tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] \
  https://apt.repos.intel.com/oneapi all main" \
  | sudo tee /etc/apt/sources.list.d/oneAPI.list
sudo apt update && sudo apt install intel-oneapi-base-toolkit

# 2. Build whisper.cpp (with the SYCL backend enabled)
source /opt/intel/oneapi/setvars.sh
git clone https://github.com/ggml-org/whisper.cpp.git
cd whisper.cpp
cmake -B build -DWHISPER_SYCL=ON
cmake --build build --config Release

# 3. Download a Whisper model and run it
./build/bin/whisper-cli -m models/ggml-large-v3.bin -f audio.wav -l zh
```

**Configuration notes:**
- Intel Arc does not support CUDA, so faster-whisper can only run in CPU mode on Intel Arc
- The OpenVINO route is the simplest and supports Arc A770/A750/B580 as well as integrated graphics
- whisper.cpp + SYCL performs better (close to a mid-range NVIDIA card on an A770) but needs the oneAPI environment
- SYCL support for the B580 (Battlemage architecture) is still being optimised; the A770 is currently more stable
- Recommended Whisper model: `medium` (A770 with 12GB VRAM) or `small` (A750 with 8GB VRAM)
- If the user has an Intel Arc card, the script auto-detects it and runs faster-whisper in CPU mode (as a fallback)

#### GPU configuration cheat sheet

| Card series | Architecture | CUDA Toolkit | Driver version | CTranslate2 | Compute precision | Whisper engine |
|---------|------|-------------|---------|-------------|---------|-------------|
| RTX 40xx | Ada Lovelace (sm_89) | >= 12.4 | >= 535 | >= 4.5.0 | float16 / int8 both work | faster-whisper |
| RTX 50xx | Blackwell (sm_120) | >= 12.8 | >= 565 | >= 4.7.1 | **float16** (recommended) | faster-whisper |
| Intel Arc | Xe HPG / Battlemage | N/A | i915 | N/A | N/A | OpenVINO or whisper.cpp+SYCL |
| Intel iGPU | integrated graphics | N/A | i915 | N/A | int8 (CPU) | faster-whisper (CPU mode) |
| No discrete GPU | CPU | N/A | N/A | any version | int8 (CPU) | faster-whisper (CPU mode) |

## Workflow

### Phase 0a: Project Bootstrap (Project Startup / Raw Asset Import)

When the user hands over a raw asset folder, a downloads directory, or a pile of unorganised footage, first build a stable project structure with [project_bootstrap.py](./scripts/project_bootstrap.py) instead of editing straight on the external raw path:

```bash
python3 scripts/project_bootstrap.py \
  --source /path/to/raw-shoot \
  --project-dir work/day61 \
  --title "Day61 launch edit" \
  --output work/day61/work/source_inventory.json \
  --markdown work/day61/work/source_inventory.md \
  --project-note work/day61/project.md \
  --strict
```

Output:
- `origin/raw|broll|audio|bgm|images|assets|sidecars/`: in-project working copies.
- `work/source_inventory.json`: `project_bootstrap.v1`, recording the external `source_path`, the in-project `project_path`, the classification, the action and the next actions.
- `work/source_inventory.md`: the asset inventory for human review.
- `project.md`: cross-session project memory.
- `next_steps.md`: the list of next commands.

The default is `--mode copy`; duplicate names get a suffix automatically and are never overwritten. For large assets on the same disk, `--mode hardlink` is available; on failure it falls back to copy and writes a warning. This script does not transcode, render, upload, call an LLM, or submit any generation job. To make asset import part of the analysis gate, run:

```bash
python3 scripts/pipeline_manifest.py \
  --project-dir work/day61 \
  --target-stage analysis \
  --require source_inventory \
  --strict
```

### Phase 0aa: Runtime Preflight (Verify the Workstation's Capabilities per Task)

Before opening media or starting a render, run [runtime_preflight.py](./scripts/runtime_preflight.py). Use `media_io` for transcription, probe or stream extraction only; use `core_edit` for tasks that will encode the final picture, then add `soft_subtitles / gif_preview / podcast_audiogram / ping_pong_loop / multicam_switch / video_enhancement / storyboard_animatic / audio_cue_mix / generation_chain_handoff / captions / qa / edge_black_trim / hdr_sdr / stabilization / interlace / remotion` as needed:

```bash
python3 scripts/runtime_preflight.py analyze \
  --profile core_edit \
  --profile captions \
  --profile qa \
  --output work/runtime_preflight.json \
  --markdown work/runtime_preflight.md \
  --strict
python3 scripts/runtime_preflight.py verify \
  --report work/runtime_preflight.json --strict
python3 scripts/pipeline_manifest.py . \
  --require runtime_preflight --strict
```

`missing` and `unknown` must be treated separately: the first means the component is definitively absent from the resolvable inventory, the second means the version command or the FFmpeg listing failed, timed out or could not be parsed; both stop the current workflow. Fix the environment following the action in the Markdown, then run `analyze` again; do not hand-edit the JSON. The report binds the selected profiles, the relevant command versions, the FFmpeg component states and a `report_id`; `pipeline_manifest.py` re-verifies on the spot, so old results are invalidated as soon as the environment or the report drifts.

This step is introspection only; it runs no real encoding, GPU, font or provider job. After it passes you still have to run the project input preflight, the real media probe, a full decode and the final audio-visual QA. See [Runtime Preflight](./docs/prompts/115-runtime-preflight.md).

### Phase 0ab: Interlace Conform (Legacy Broadcast / DV / DVD Conditional Gate)

When the footage shows combing, `TFF/BFF`, `1080i/576i/480i` or telecine traces, run `interlace_conform.py analyze` first. It runs FFmpeg `idet` on the head, middle and tail, and classifies the material as progressive, true TFF/BFF interlace, telecine candidate, or mixed/uncertain. Telecine must stop and go through IVTC; progressive stays as it is; uncertain material may only use a classification override backed by frame-by-frame evidence.

Once true interlace is confirmed, bind the decision with `plan --reviewed-by --note --delivery work/<name>-progressive.mp4 --comparison verify/<name>-interlace-compare.mp4`. bwdif is preferred by default, yadif is a fallback that carries a warning; `frame` keeps the frame rate, `field` keeps the motion of every field and exactly doubles the rate. apply promotes only after the progressive field flag, live idet, size/frame rate/duration, audio and video start and end, and a full decode have passed; then watch the A/B at full 1× and confirm only after combing, motion, line detail, field order and audio sync all `pass`. See [Interlace Conform](./docs/prompts/116-interlace-conform.md).

### Phase 0ac: Frame-rate Conform (Optional for Phone / Screen-recorded VFR)

When the source is known to be VFR, when `r_frame_rate` and `avg_frame_rate` look suspicious, or when cumulative A/V drift appears after multi-segment cuts, first create an in-project CFR working copy:

```bash
python3 scripts/frame_rate_conform.py plan origin/phone.mp4 \
  --fps 30 \
  --delivery work/phone-cfr.mp4 \
  --project-dir . \
  --output work/frame_rate_conform_plan.json \
  --markdown work/frame_rate_conform_plan.md
python3 scripts/frame_rate_conform.py apply work/frame_rate_conform_plan.json
python3 scripts/frame_rate_conform.py verify work/frame_rate_conform_plan.json --strict
```

The script reads the PTS intervals of every decoded video frame and records variable/non-monotonic intervals, the exact target rational frame rate, the source/output SHA-256, the media contract and the full-decode receipt. apply promotes atomically only when the output frame intervals are constant, `frame_count ≈ duration × fps`, A/V start and end are within tolerance, and the display size/orientation is correct; after that, all transcription, cutting, subtitles, sync and rendering use `work/phone-cfr.mp4`. Choose 30/60 fps against the real motion and the platform; increasing the frame rate only duplicates frames, lowering it loses motion samples. HDR/BT.2020/>8-bit, a pronounced pre-existing A/V offset, or independent device clock drift are outside this tool's automatic repair scope. See [Frame-rate Conform](./docs/prompts/112-frame-rate-conform.md).

### Phase 0ad: Edge-black Trim (Optional for Head / Tail Black)

When a capture-card, camera-prerecorded or transcoded file has black at the head or tail, run FFmpeg `blackdetect` with `black_edge_trim.py plan`; by default it also uses `silencedetect` to check that the exact removal range is at least 95% silence, considers only intervals touching the head or tail of the timeline, and leaves mid-timeline black untouched.

```bash
python3 scripts/black_edge_trim.py plan origin/capture.mp4 \
  --delivery work/capture-edge-trimmed.mp4 \
  --edge-proof verify/capture-edge-proof.mp4 \
  --audio-policy silent_only \
  --project-dir . \
  --output work/black_edge_trim_plan.json \
  --markdown work/black_edge_trim_plan.md
python3 scripts/black_edge_trim.py apply work/black_edge_trim_plan.json
```

The script keeps the default 0.08 s visual padding, applies the same source-time trim to audio and video, and outputs H.264/yuv420p CFR with optional 48 kHz AAC; it promotes atomically only after the media contract and a full decode pass. The edge proof comes from the original and contains the proposed removal range plus the immediately adjacent content. Watch the proof and the delivery at full 1×, confirm the first and last frames, the content coverage and the audio continuity, then run `confirm`, then `verify --strict`. When you know the sound inside the black picture should also be removed, you may explicitly use `--audio-policy allow_audible`; that override keeps a warning. See [Edge-black Trim](./docs/prompts/119-black-edge-trim.md).

### Phase 0ae: Loop Fill (Fill a Fixed Duration with a Short Background / Ambient Clip)

When a short clip must be repeated a specified number of times or fill a fixed slot, use `loop_fill.py plan` to bind the progressive CFR source, the target, the delivery and the proof of the first real seam. It keeps or explicitly drops the source audio and advances audio and video together on each repeat; it does not add a crossfade automatically, and it does not package arbitrary head and tail material as a seamless loop.

```bash
python3 scripts/loop_fill.py plan origin/ambient.mp4 \
  --duration 00:30 \
  --audio-mode drop \
  --delivery work/ambient-30s.mp4 \
  --seam-proof verify/ambient-loop-seam.mp4 \
  --project-dir . \
  --output work/loop_fill_plan.json \
  --markdown work/loop_fill_plan.md
python3 scripts/loop_fill.py apply work/loop_fill_plan.json
```

Watch the seam proof and the full delivery at normal speed; after confirming the picture seam, motion continuity, repeated flicker frames, audio seam and slot coverage, run `confirm`, then release with `verify --strict` or `pipeline_manifest.py --require loop_fill_plan --strict`. For VFR run `frame_rate_conform.py` first; for HDR/BT.2020/>8-bit settle the `hdr_sdr.py` colour path first. See [Loop Fill](./docs/prompts/117-loop-fill.md).

### Phase 0af: Ping-pong Loop (Short Action Played Forward Then Backward)

When the head and tail of a gesture, a product rotation or a generated video action do not close, and you need a back-and-forth loop, use `ping_pong_loop.py`. First restrict the action range to a few seconds; FFmpeg `reverse` caches every selected frame, so the planner records a conservative memory estimate and stops early when `--max-working-set-mib` is exceeded.

```bash
python3 scripts/ping_pong_loop.py plan origin/gesture.mp4 \
  --start 1.2 --end 2.05 --cycles 4 \
  --delivery work/gesture-ping-pong.mp4 \
  --turnaround-proof verify/gesture-turnaround.mp4 \
  --loop-seam-proof verify/gesture-loop-seam.mp4 \
  --project-dir . \
  --output work/ping_pong_loop_plan.json \
  --markdown work/ping_pong_loop_plan.md
python3 scripts/ping_pong_loop.py apply work/ping_pong_loop_plan.json
```

The cycle uses `0…N-1,N-2…1`, deliberately excluding the turnaround point and the duplicate end frame at the end of the cycle. The source audio is always dropped; connect BGM/SFX only after the picture is approved. Watch the turnaround proof, the loop-seam proof and the full delivery at normal speed, and after everything about the turnaround motion, the loop motion, repeated still frames, framing and creative intent `pass`, run `confirm`, then release with `verify --strict` or `pipeline_manifest.py --require ping_pong_loop_plan --strict`. See [Ping-pong Loop](./docs/prompts/128-ping-pong-loop.md).

### Phase 0ag: Clip Assembly (Safe Stitching of Multiple Sources)

When the intro, main body, generated clips and outro come from different canvases, orientations, frame rates, SARs, timestamps, sample rates or channel counts, use `clip_assembly.py` instead of a concat copy. Pass every unique source clip in final playback order; reuse a clip with `loop_fill.py`.

```bash
python3 scripts/clip_assembly.py plan \
  origin/intro.mp4 origin/main.mov origin/outro.mp4 \
  --width 1080 --height 1920 --fps 30 \
  --fit contain --audio-mode fill-silence \
  --delivery output/assembled.mp4 \
  --boundary-proof verify/clip-assembly-boundaries.mp4 \
  --project-dir . \
  --output work/clip_assembly_plan.json \
  --markdown work/clip_assembly_plan.md
python3 scripts/clip_assembly.py apply work/clip_assembly_plan.json
```

Inside one FFmpeg filter graph the script unifies PTS, canvas, rotation, CFR, SAR 1:1, `yuv420p` and 48 kHz stereo, then hard-cuts to H.264/AAC. When at least one segment has audio, segments without an audio track get an equal-length silence so A/V coverage holds; when no sound is needed at all use `--audio-mode drop`. An existing audio track that does not match the video start and end, or HDR/BT.2020/>8-bit, stops the plan stage.

apply promotes atomically only after the total duration, size, frame rate, decoded cadence, SAR, A/V start and end, and a full decode pass, and generates a normal-speed proof from the exact delivery covering every seam. After playing the delivery and the proof in full, reach an observable conclusion on clip order, visual seams, frame continuity, audio seams and complete coverage, then `confirm` and `verify --strict`. This tool does not invent transitions or semantic continuity; when you need a J-cut/L-cut, go through a separate audio transition plan. See [Clip Assembly](./docs/prompts/118-clip-assembly.md).

### Phase 0b: Production Authorization (Authorize by Exact Scope)

When the flow involves handing assets to an external service, changing the original narrative order or trim range, consuming generation quota, cloning a real person's voice, using a real person / a minor / a public figure / a brand / IP, or publishing directly, write a `production_authorization_scope.v1` before the action happens. The scope must name the in-project assets, the concrete action, the purpose, the exact provider/surface, the possible cost/quota, and the applicable rights subject; full schema in [Production Authorization](./docs/prompts/97-production-authorization.md).

```bash
python3 scripts/production_authorization.py prepare \
  --project-dir . \
  --scope work/production_authorization_scope.json \
  --output work/production_authorization_request.json \
  --markdown work/production_authorization_request.md \
  --response-template work/production_authorization_response.json \
  --strict

# After filling in approve/reject, note, rights basis and evidence_note item by item:
python3 scripts/production_authorization.py audit \
  --project-dir . \
  --request work/production_authorization_request.json \
  --response work/production_authorization_response.json \
  --output work/production_authorization.json \
  --markdown work/production_authorization.md \
  --strict

python3 scripts/production_authorization.py verify \
  --project-dir . \
  --report work/production_authorization.json \
  --strict
```

`prepare` binds the scope to each source asset's relative path, size and SHA-256; `audit` requires a complete per-item decision on every action and rights item; `verify` re-reads the scope, the assets, the request and the response on the spot and recomputes the report. Any change to a provider, a purpose, a cost statement, a rights object, asset bytes or a decision invalidates the old report. This artifact only records a local, self-reported review scope: it does not upload/cut/generate/publish, and it does not replace identity verification, digital signatures, real authorization documents or legal advice.

### Phase 0: Media Library Setup

On first use, help the user create the asset directory structure:

```bash
python3 scripts/media_library.py init [project_dir]
```

This creates the following directory structure:
```
media/
├── raw/      — raw footage (video straight out of a camera/phone)
├── broll/    — B-roll assets (city streets, product close-ups, etc.)
├── bgm/      — background music (MP3/WAV/M4A)
├── assets/   — overlay assets (watermark PNG, logo, etc.)
└── output/   — output directory
```

**Ask where the assets come from**:
1. Ask the user where the video files are (local path, external device or cloud)
2. Suggest copying/moving the raw footage into `media/raw/`
3. Ask whether there are B-roll, BGM or other supporting assets
4. If the user's videos are scattered across several directories, suggest gathering them into `media/raw/` first

**Scan and build the index**:
```bash
python3 scripts/media_library.py scan [project_dir]
```

The index system automatically:
- scans all video/audio/image files
- extracts metadata such as duration, resolution and frame rate
- links existing transcript files
- uses a JSON index for small projects (< 200 files) (`media_index.json`)
- automatically upgrades to a SQLite index for large projects (`media_index.db`)
- manual upgrade: `python3 scripts/media_library.py upgrade`

**Check the asset library status**:
```bash
python3 scripts/media_library.py status
```

**Search assets**:
```bash
python3 scripts/media_library.py search "keyword"
```

**Recommend B-roll candidates**:
```bash
python3 scripts/media_library.py recommend "AI workflow dashboard" \
  --project-dir . \
  --category broll \
  --target-duration 3 \
  --target-aspect 9:16 \
  --json
```

The recommendations contain `score`, `reasons` and `absolute_path`, so a human or an agent can confirm first and then write them into `render_config` or `enrich_plan`. Indexed files that no longer exist are filtered out by default; add `--include-missing` when you need to clean a stale index.

**Plan stock queries when local assets are insufficient**:
```bash
python3 scripts/stock_material_plan.py \
  --subject "AI workflow automation" \
  --script work/transcript.json \
  --provider pexels \
  --provider pixabay \
  --provider coverr \
  --media-library . \
  --output work/stock_material_plan.json \
  --markdown work/stock_material_plan.md
```

`stock_material_plan.py` only produces `stock_material_plan.v1` and a Markdown review; it does not go online, download, or consume quota. It borrows MoneyPrinterTurbo's `video_terms` / Pexels / Pixabay / Coverr / `video_count` way of planning assets, but keeps this skill's artifact-first style.

**Register downloaded or client-supplied assets once authorization is confirmed**:
```bash
python3 scripts/media_library.py import /path/to/downloaded.mp4 \
  --project-dir . \
  --category broll \
  --copy \
  --provider pexels \
  --source-url "https://www.pexels.com/video/demo-123/" \
  --creator "Demo Creator" \
  --license "Pexels License" \
  --tag "workflow,dashboard"

python3 scripts/media_library.py annotate media/broll/downloaded.mp4 \
  --project-dir . \
  --source-url "https://example.com/source" \
  --license "owned" \
  --tag "client-approved"
```

The registered `provider`, `source_url`, `creator` and `license` go into `media_index.json/db`, and `asset_provenance.py` later uses them as the publish gate.

### Phase 0.25: Creator-owned Edit Style Profile (Personal / Brand Editing Style, Optional)

When the user wants several projects to keep one creative direction, editing rhythm, subtitle/cover/colour-grade/BGM habits and title spelling, generate and hand-edit the spec first, then create a portable profile:

```bash
python3 scripts/edit_style_profile.py template \
  --output work/edit_style_profile_spec.json

# After editing the real preferences and the approval basis in the spec:
python3 scripts/edit_style_profile.py create \
  --spec work/edit_style_profile_spec.json \
  --output work/edit_style_profile.json \
  --markdown work/edit_style_profile.md \
  --strict

python3 scripts/edit_style_profile.py verify \
  --profile work/edit_style_profile.json \
  --strict
```

At render time add `render_final.py --style-profile work/edit_style_profile.json`; when generating title copy and covers, pass the same argument to `generate_caption.py` and `cover_variants.py` respectively. The profile only fills controlled fields that are missing or `null` in the config; the project config and explicit CLI (including the cover `--style`) always win. It describes "how editing is usually done", whereas `edit_recipe.py` stores "how this one timeline is reproduced"; neither replaces the other. A profile digest can only detect file drift; it is not a signature, identity verification or proof of rights. Full usage in [docs/prompts/98-edit-style-profile.md](docs/prompts/98-edit-style-profile.md).

### Phase 0.5: Source Receipts (Fact-source Proof Deck, Optional but Recommended)

If the video contains news, data, product facts, health/financial/legal judgements, source-page screenshots or "official statements", turn the claims and the evidence into source receipts before moving on to storyboard/publish:

```bash
python3 scripts/source_receipts.py \
  --claims work/source_claims.json \
  --project-dir . \
  --output work/source_receipts.json \
  --markdown work/source_receipts.md \
  --html work/source_receipts.html \
  --require-primary-source \
  --strict
```

`source_receipts.py` only verifies the URLs and local screenshot/evidence files you supply; it does not crawl, screenshot or upload. The `screenshot` / `source_file` entries in `source_claims.json` are resolved relative to the directory holding the claims JSON. High-risk claims (news, data, finance, health, law) must have a `source_url`; add `--require-screenshot` when you need visual proof cards. The publish gate can enforce it with `pipeline_manifest.py --require source_receipts --strict`; a `summary.blocking > 0` blocks.

### Phase 1: Audio Extraction

For every input video file, extract audio with [extract_audio.py](./scripts/extract_audio.py):

```bash
python3 scripts/extract_audio.py "<video_path>"
```

Output: a `<video_name>_audio.wav` file in the same directory as the video.

### Phase 2: Speech Recognition

Use [transcribe.py](./scripts/transcribe.py) to run speech recognition on the audio and produce timestamped sentence-level text:

```bash
python3 scripts/transcribe.py "<audio_path>" --model auto --language zh --detect-fillers
```

- `--model auto`: pick the best model for the hardware (NVIDIA GPU → large-v3, Apple Silicon → large-v3-turbo, integrated graphics → medium, CPU only → small)
- You can also name one explicitly: `tiny`, `base`, `small`, `medium`, `large-v3`, `large-v3-turbo`
- `--engine auto`: automatically detect faster-whisper (recommended) or openai-whisper
- `--mirror`: download models from a mirror source (for users in mainland China)
- `--language`: `zh` (Chinese), `en` (English), `ja` (Japanese) and so on; you can also omit it and let Whisper detect the language
- `--silence-threshold 1.0`: silence detection threshold in seconds, default 1.0. Set it to 0 to disable
- `--word-timestamps`: enable per-word timestamps (required for karaoke subtitles)
- `--detect-fillers`: detect filler words (Chinese: um / uh / like / you know; English: um/uh/like/you know), and mark segments that are nothing but fillers as suggested skips

Output: a `<video_name>_transcript.json` file in the same directory as the audio, in this format:

```json
{
  "segments": [
    {"id": 1, "start": 0.0, "end": 2.5, "text": "hello everyone"},
    {"id": 2, "start": 2.5, "end": 5.1, "text": "today let's talk about one topic"}
  ],
  "silences": [
    {"start": 15.2, "end": 18.5, "duration": 3.3, "before_segment": 5, "after_segment": 6}
  ],
  "filler_words": [
    {"segment_id": 3, "text": "um, you know", "fillers_found": ["um", "you know"], "is_filler_only": true},
    {"segment_id": 7, "text": "like, I think this plan", "fillers_found": ["like"], "is_filler_only": false}
  ]
}
```

**Silence detection**: transcribe.py automatically analyses the gaps between adjacent speech segments. Gaps beyond the threshold (1 second by default) are marked as silence and written to the `silences` field. These silences are usually the speaker's pauses, stumbles or slips of the tongue; avoid those regions when selecting segments to build render_config.json.

### Phase 2a: Video Keyframe Extraction

For talking-head videos (especially footage shot while walking outdoors, with ambient sound), audio transcription alone cannot tell you the visual content. Use [extract_keyframes.py](./scripts/extract_keyframes.py) to extract keyframes and build a storyboard so the video content is understood as a whole:

```bash
python3 scripts/extract_keyframes.py "<video_path>"
```

Parameters:
- `--max-frames 16`: maximum number of keyframes (default 16)
- `--threshold 0.4`: scene-change detection sensitivity (0.0-1.0, lower extracts more keyframes, default 0.4)
- `--cols 4`: number of columns in the storyboard grid (default 4)
- `--thumb-width 320`: thumbnail width (default 320px)
- `--output-dir`: keyframe output directory (default `<video_name>_keyframes/`)
- `--no-storyboard`: only extract keyframes, do not compose the storyboard

Output:
- `<video_name>_keyframes/` — one PNG per keyframe (named with its timestamp)
- `<video_name>_storyboard.png` — the composed storyboard (grid layout + timestamp labels)
- `<video_name>_keyframes.json` — keyframe metadata (timestamp, frame number, file path)

**Why keyframe extraction matters**:
- Talking-head videos are often shot while walking, and the scene changes in frame (street → park → coffee shop) are important narrative cues
- The storyboard lets the AI see the audio content (transcript) and the visual content (keyframes) at the same time, so it chooses segments better
- It surfaces information that audio-only analysis cannot capture: body language, facial changes, environment switches, product demonstrations, etc.

**Typical workflow**:
```bash
# 1. Extract audio and transcribe
python3 scripts/extract_audio.py video.mp4
python3 scripts/transcribe.py video_audio.wav --model auto --language zh --detect-fillers

# 2. Extract keyframes and build the storyboard
python3 scripts/extract_keyframes.py video.mp4

# 3. The AI analyses transcript + storyboard together
# → open video_storyboard.png to see what the video shows
# → read video_transcript.json for what is said
# → decide which segments are worth keeping
```

**Key points for the AI agent's combined analysis**:
- Put the keyframe storyboard next to the transcript and annotate each time range with "what was said + what is on screen"
- Find the high-quality stretches where picture and speech match best (e.g. the frame is on the shop exactly when the narrator says "this shop")
- Identify stretches that are blurry, occluded or poorly lit and suggest skipping them
- For outdoor footage, note the stretches with heavy camera shake and lower their priority when choosing segments

### Phase 2a: Adaptive Scene Boundaries

Before splitting a long video into clips, sampling it for understanding, or analysing the rhythm of a finished cut, generate adaptive scene boundaries for moving-camera footage first:

```bash
python3 scripts/scene_boundaries.py video.mp4 \
  --method adaptive \
  --adaptive-threshold 3.0 \
  --min-scene-score 0.15 \
  --min-scene-duration 1.0 \
  --output work/scene_boundaries.json \
  --markdown work/scene_boundaries.md
```

`adaptive` compares each frame's FFmpeg scene score with the mean of its surrounding neighbourhood, which reduces the dense false cuts caused by continuous panning, motion or flicker; `boundary_evidence[]` keeps the score, the adaptive ratio and the neighbourhood mean, and you must read the Markdown before handing it to `highlight_picker.py --scene-boundaries`. To reproduce the old flow, or for locked-off camera footage, use `--method fixed --threshold 0.35`.

### Phase 2b: Visual Dedupe (Cross-asset Duplicate Shot Review)

Before multicam, multi-take, repeatedly transcoded or B-roll candidates enter the timeline, use `visual_dedupe.py` to run a three-point perceptual-hash review on the scenes of several sources:

```bash
python3 scripts/visual_dedupe.py \
  --manifest work/visual_dedupe_sources.json \
  --output work/visual_dedupe.json \
  --markdown work/visual_dedupe.md \
  --strict
```

The manifest's `sources[]` gives each source an `id`, a `video`, and optional `scene_boundaries` and `quality_score`; relative paths resolve against the manifest directory. By default the script only compares different sources, requires at least two of the 10%/50%/90% sample points to match, and uses `quality_score`, resolution and file size for the keep recommendation. It only outputs a review artifact and never deletes or moves source assets. When duplicate groups appear, look at the source ranges by hand first, then exclude the confirmed duplicates from the downstream edit plan.

### Phase 2c: Video Understanding (Sampled Frames + Optional YOLO)

When the people, phones, computer screens, products, vehicles or other moving objects in the footage affect cropping, privacy masking or B-roll selection, use [video_understanding.py](./scripts/video_understanding.py) to produce a structured visual-understanding artifact. No detector needs to be installed by default; to run YOLO, install the optional dependency `ultralytics` first.

```bash
# No detector: sample frames only and produce a review shell
python3 scripts/video_understanding.py video.mp4 \
  --output work/video_understanding.json \
  --markdown work/video_understanding.md

# Optional YOLO: detect objects and produce detections/tracks/scene_tags
pip install ultralytics
python3 scripts/video_understanding.py video.mp4 \
  --scene-boundaries work/scene_boundaries.json \
  --detector yolo \
  --model yolo11n.pt \
  --output work/video_understanding.json \
  --markdown work/video_understanding.md \
  --strict
```

Output:
- `video_understanding.json` — `video_understanding.v1`, containing `frames[]`, `detections[]`, `tracks[]`, `scene_tags[]`, `warnings[]`
- `video_understanding.md` — a human review table listing sampled frames, detection counts, tracks, tags and warnings

Typical downstream:
```bash
python3 scripts/smart_reframe.py video.mp4 \
  --detections work/video_understanding.json \
  --platform douyin \
  --output work/reframe_douyin.json \
  --markdown work/reframe_douyin.md

python3 scripts/privacy_redact.py \
  --video video.mp4 \
  --detections work/video_understanding.json \
  --output work/privacy_redaction.json \
  --markdown work/privacy_redaction.md
```

Note: the built-in tracklets are lightweight associations made for talking-head editing and are not equivalent to frame-by-frame multi-object tracking. For highly dynamic footage such as sports, traffic or crowded occlusion, you can use Ultralytics `model.track(..., tracker="bytetrack.yaml")`, BoT-SORT or Norfair to produce denser detection/track results and convert them into the same `detections[]` / `tracks[]` JSON.

### Phase 2.5: Transcript Review

After transcription, you **must** review the text in every transcript.json line by line and fix the following two classes of problem:

**1. Speech recognition errors (ASR errors)**:
Common Whisper error types:
- **Proper nouns / product names**: e.g. "opencloud" → "OpenClaw", "cloudcode" → "Claude Code", "cloud ops" → "Claude Opus"
- **Homophone characters**: e.g. a homophone of the platform name typed as the wrong characters, "Jianying" (CapCut) mistranscribed as a homophone, "break in the posting cadence" heard as "stop farming", "too lazy to say it" heard as "too lazy to cut it"
- **English spelling**: e.g. "scale" → "skill", a misheard word in place of "video"
- **Tail hallucination**: Whisper sometimes emits meaningless repeated text at the end of quiet stretches; delete it directly

**2. Speaker errors**:
- **Repetition / stumbles**: the speaker repeats the same sentence or stalls and starts again; mark as a suggested skip
- **Garbled segments**: stretches recognised as meaningless text because the speech was unclear (e.g. a run of single-character fragments); mark as a suggested skip

When there is a lot of specialist terminology, personal names, homophones or Chinese-English code-switching, run `semantic_transcript_review.py prepare` first and have the current agent/model fill in a provider-neutral response; then run `audit`. Do not treat model confidence as approval: copy the choices template from the audit Markdown, which is bound to `source_sha256 + review_id`, and run `apply` only after deciding `approve` / `reject` item by item. `audit` derives complete coverage from the source transcript and rejects whole-sentence rewrites, non-minimal character patches, number/punctuation changes, out-of-range/overlapping patches and stale transcript hashes. After a successful apply, hand `transcript_semantic_reviewed.json` to the synchronised-media HTML below for listening review; see [docs/prompts/79-semantic-transcript-review.md](docs/prompts/79-semantic-transcript-review.md).

```bash
python3 scripts/semantic_transcript_review.py prepare \
  --transcript work/transcript.json \
  --output work/semantic_review_request.json \
  --markdown work/semantic_review_request.md

python3 scripts/semantic_transcript_review.py audit \
  --transcript work/transcript.json \
  --response work/semantic_review_response.json \
  --output work/transcript_semantic_review.json \
  --markdown work/transcript_semantic_review.md \
  --strict

python3 scripts/semantic_transcript_review.py apply \
  --transcript work/transcript.json \
  --audit work/transcript_semantic_review.json \
  --choices work/semantic_review_choices.json \
  --output work/transcript_semantic_reviewed.json
```

**Review flow**:
1. Use `transcript_review.py html` to generate a local page with no external dependencies; in a terminal-only environment use `export` to generate text.
2. On the page, click a timecode to proofread against the media; the current segment highlights during playback. Inline editing supports local autosave, find-and-replace and CPS highlighting.
3. Save `transcript_review.txt` explicitly; do not let the page or the agent overwrite the original transcript directly.
4. Use `apply` to produce `transcript_reviewed.json`; clean-up, the rough cut, the storyboard and subtitles all use the reviewed file from then on.
5. For stumble/garbled segments, mark them as suggested skips when the segment list is shown (Phase 3).

```bash
python3 scripts/transcript_review.py html \
  --transcript work/transcript.json \
  --video origin/talking.mp4 \
  --corrections work/corrections.json \
  --output work/transcript_review.html \
  --max-cps 20

python3 scripts/transcript_review.py apply \
  --transcript work/transcript.json \
  --review work/transcript_review.txt \
  --output work/transcript_reviewed.json
```

The HTML and the media only open locally; nothing is uploaded and no LLM is called. When the browser cannot write files directly it downloads `transcript_review.txt`; put it back under `work/` and then apply. CPS is a pre-render hint; `subtitle_readability_qa.py --strict` still has to run at the end.

**Note**: this step must be completed before Phase 5 (render), because subtitle text comes from transcript.json. Re-render after the corrections so the subtitle text in the final video is correct.

### Phase 2.5a: Target Script Alignment (Assemble the Verbatim Words of a Confirmed Script, Optional)

If the client, the director or the user has already confirmed the script for the final cut, and the same line was recorded in several takes or is spread across different assets, first lay the target script out as "one complete spoken unit per line", then match it against the reviewed transcripts:

```bash
python3 scripts/script_alignment.py \
  --target-script work/target_script.md \
  --transcript take-a=work/take-a_transcript_reviewed.json \
  --transcript take-b=work/take-b_transcript_reviewed.json \
  --media take-a=origin/take-a.mp4 \
  --media take-b=origin/take-b.mp4 \
  --output work/script_alignment.json \
  --markdown work/script_alignment.md \
  --render-config work/render_config.json \
  --clean-script work/clean_script.md \
  --strict
```

The script does local word-level matching only: it does not call an LLM, does not modify source files, and does not judge expression, shot choice or performance quality. It outputs a stable candidate id, a source time, the verbatim words and sequence/coverage/ngram/length evidence for each sentence; when word timestamps exist it tightens to word boundaries first, and with segment timestamps only it conservatively keeps the whole segment. Low scores, two near-tied top candidates, no candidate, missing assets or duplicated source-time occupancy write `summary.blocking` and make `--strict` return 2.

When the same line across takes produces `ambiguous_match`, you must watch/listen to the candidates in the Markdown, write the confirmed result as `{"choices":{"target-001":"<candidate-id>"}}`, and re-run with `--choices work/script_alignment_choices.json`. A manual choice can resolve word-level low scores/multiple solutions only; it cannot bypass missing files or overlapping times. Only after `summary.blocking=0` move on to `edit_preflight.py` / `render_final.py`; detailed usage in [docs/prompts/78-script-alignment.md](docs/prompts/78-script-alignment.md).

### Phase 2.6: ASR Rough Cut (Filler-word / Repeated-sentence Rough Cut, Optional)

If the transcript contains many pure filler words, stumbles and restarts, or adjacent repeated sentences, first use [rough_cut.py](./scripts/rough_cut.py) to produce an auditable rough-cut plan:

```bash
python3 scripts/rough_cut.py --transcript work/transcript.json --cut-list work/rough_cut.json
```

Once the plan is confirmed you can render the rough cut directly:

```bash
python3 scripts/rough_cut.py \
  --transcript work/transcript.json \
  --input origin/talking.mp4 \
  --output output/talking.roughcut.mp4 \
  --cut-list work/rough_cut.json
```

`rough_cut.py` outputs `decisions` / `removed_segments` / `keep_segments` / `speedup_ratio`. It does not call an LLM and submits no paid task; it only uses the filler metadata from `transcribe.py --detect-fillers` plus adjacent-text similarity for a conservative rough cut. Before rendering, use `timeline_view.py --cut-list work/rough_cut.json` to look at the removed ranges in the source; after rendering, use `--rendered-cut-list work/rough_cut.json` to look at the actual splice points in the final cut.

### Phase 2.6b: Multimodal Dead-Air (Conservative Silence + Still-frame Dead-air Removal, Optional)

If audio-only `jump_cut.py` might delete pauses that still carry expression, gestures, a product demonstration or an on-screen operation, switch to an AND gate of audio silence and still picture:

```bash
python3 scripts/multimodal_dead_air.py plan origin/talking.mp4 \
  --delivery work/talking-dead-air-tight.mp4 \
  --output work/multimodal_dead_air_plan.json \
  --markdown work/multimodal_dead_air_plan.md \
  --strict
python3 scripts/multimodal_dead_air.py verify work/multimodal_dead_air_plan.json --strict
DEAD_AIR_CUT_COUNT="$(python3 -c 'import json; print(len(json.load(open("work/multimodal_dead_air_plan.json"))["removed_segments"]))')"
python3 scripts/timeline_view.py origin/talking.mp4 \
  --cut-list work/multimodal_dead_air_plan.json \
  --output-dir verify/dead-air-cuts \
  --limit "$DEAD_AIR_CUT_COUNT"
python3 scripts/multimodal_dead_air.py apply work/multimodal_dead_air_plan.json \
  --markdown work/multimodal_dead_air_plan.md
```

By default a segment is only selected when the still frames cover at least 60% of the silence, and only the intersection of the two is removed; 80ms padding, a 30ms audio fade and a 20% removal budget still apply. The plan binds the source SHA-256 and the media contract; apply uses a temporary MP4 and promotes atomically only after H.264/AAC, `yuv420p`, size, frame rate, sample rate, channels, duration and a full decode all pass. `timeline_view.py` only looks at 20 cut points by default, so you must pass this plan's actual removed-segment count to `--limit` as shown above; if the count is 0, stop and keep the original. It does not understand performance, semantics or intentional pauses; every source cut point must still be viewed first, and the output must still be reviewed in full at 1× with sound. See [docs/prompts/88-multimodal-dead-air.md](docs/prompts/88-multimodal-dead-air.md).

### Phase 3: User Interaction

**Show the segment list to the user** in the following format:

```
Video segment list:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  #   | Time range        | Content
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  1   | 00:00.0 - 00:02.5 | Hi everyone
  2   | 00:02.5 - 00:05.1 | Today we are going to talk about a topic
  3   | 00:05.1 - 00:08.3 | This topic is very interesting
  ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Choose the segments to assemble (examples):
  - Contiguous range: 1-10
  - Several segments: 1,3,5,7
  - Mixed selection: 1-4,6,8-10
```

If there are several video files, show the segment list for each video separately and let the user select across videos.

#### AI-assisted shot selection

While showing the segment list, the AI agent should give each segment a recommendation score (1-5 stars) based on the following dimensions:

**Attractiveness scoring dimensions**:
1. **Hook strength** (first 3 seconds): whether the opening is compelling (a question, a counter-intuitive claim, an emotional trigger)
2. **Information density**: how much useful information is delivered per second (avoid repetition and filler)
3. **Emotional variation**: whether there is an emotional arc (humour → serious → surprise)
4. **Completeness**: whether the segment forms a complete narrative unit (opening, development, close)

**Automatic skip suggestions**:
- Segments marked `is_filler_only: true` in the transcript (pure filler words)
- Adjacent segments separated by a silence gap > 2 seconds (a stall followed by a re-take)
- Segments whose transcript text highly repeats the previous segment (a slip of the tongue retold)

**Automatic splitting of long videos into shorts** (when the video is > 3 minutes):
The AI agent should analyse the transcript to identify topic switch points (semantic breaks, transition words such as "next", "also"), group the segments by topic into standalone short videos (30-90 seconds each), and compute an overall attractiveness score for each group:

```
Recommended short-video split proposal:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Plan | Segment range | Duration | Topic             | Recommendation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  A    | #1-#8         | 45s      | Pain point intro  | ★★★★★
  B    | #9-#18        | 62s      | Core method       | ★★★★☆
  C    | #19-#25       | 38s      | Live walkthrough  | ★★★☆☆
  D    | #1-#25        | 2m25s    | Full version      | ★★★★☆
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

Wait for the user's reply with the selection, then proceed to Phase 4.

After the selection and before batch rendering, it is advisable to snap the cut points with word-level timestamps so a candidate range does not land mid-word or at the end of half a sentence:

```bash
python3 scripts/audio_boundary_snap.py \
  --candidates work/highlight_candidates.json \
  --transcript work/transcript.json \
  --media origin/long-talk.mp4 \
  --output work/audio_boundary_plan.json \
  --markdown work/audio_boundary_plan.md \
  --strict
```

The `selected[]` of the `audio_boundary_plan.v1` output can be passed directly to `shorts_batch.py --highlights`. The script only reads local candidates, the transcript and optional media; it does not render, upload, or call a provider. `--strict` returns 2 when word-level timestamps are missing, a time range is invalid, the source media is missing, or a safe boundary exceeds the platform duration limit.

### Phase 4: Render Config

Based on the user's selection, generate a `render_config.json` config file.

**Important note: the render config is a project-level file**

The render_config.json format shown in the SKILL is a **template/reference only**; do not directly modify any file in the skill repository. Every video project should create its own render config under the **user working directory**:

```bash
# Create a script/ folder in the user's video project working directory
mkdir -p script/

# Create a separate render config file for each video project under script/
# e.g. script/render_config.json
```

**Directory structure example**:
```
<WORKSPACE>/my-project/        ← user working directory
├── script/                    ← render config directory (one per project)
│   └── render_config.json     ← render config for this project
├── media/
│   ├── raw/                   ← raw footage
│   ├── broll/
│   └── output/                ← final output
├── *_audio.wav                ← extracted audio
└── *_transcript.json          ← transcript file
```

**Render config file format** (`script/render_config.json`):

```json
{
  "clips": [
    {"video": "path/to/video1.MOV", "segment_id": 4, "transcript": "path/to/transcript1.json"},
    {"video": "path/to/video1.MOV", "segment_id": 5, "transcript": "path/to/transcript1.json"},
    {"video": "path/to/video2.MOV", "segment_id": 1, "transcript": "path/to/transcript2.json",
     "broll": "path/to/cityscape.mp4", "broll_start": 5.0}
  ],
  "title": "cover title text",
  "subtitle": "subtitle / emotional hook (optional)",
  "cover_style": "news",
  "cover_duration": 2.0,
  "cover_image": "path/to/custom_cover.png",
  "cover_use_frame": false,
  "video_overlay": "path/to/overlay.png",
  "rec_blink": {
    "dot_image": "path/to/dot.png",
    "x": 55, "y": 66,
    "period": 1.0
  },
  "end_cards": [
    {"text": "Thanks for watching\nMore coming soon", "duration": 3.5}
  ],
  "bgm": "path/to/background_music.mp3",
  "bgm_volume": 0.15,
  "bgm_fade_out": 3.0,
  "bgm_ducking": true,
  "bgm_ducking_threshold": 0.03,
  "bgm_ducking_ratio": 8,
  "bgm_ducking_attack_ms": 20,
  "bgm_ducking_release_ms": 500,
  "subtitle_style": "karaoke",
  "subtitle_highlight_color": "#FFFF00",
  "chapters": [
    {"title": "chapter name", "start": 0.0, "end": 30.0}
  ],
  "text_badges": [
    {"text": "key takeaway", "start": 12.0, "end": 13.5}
  ],
  "broll_overlays": [
    {"video": "path/to/cityscape.mp4", "start": 8.0, "end": 10.0, "source_start": 2.0}
  ],
  "image_overlays": [
    {"image": "path/to/generated.png", "start": 18.0, "end": 21.0, "fit": "cover"}
  ],
  "pip_overlays": [
    {"video": "path/to/facecam.mp4", "start": 0.0, "end": 42.0, "position": "bottom_right", "sync_offset": 0.18}
  ]
}
```

**Using the render config**:
```bash
# Point render at the config file under script/
python3 scripts/render_final.py --config script/render_config.json --output media/output/final.mp4
```

**B-roll replacement** (`broll` field):
- Adding `"broll": "path/to/video.mp4"` to a clip replaces that clip's picture while keeping the original audio
- `broll_start` sets the time point in the B-roll video from which to start cutting (default 0.0)
- The B-roll video is automatically scaled/cropped to match the main video resolution
- Use cases: screen-recorded voiceover paired with city street footage, audio voiceover paired with picture, etc.

**Auto-Enrich automatic hookup** (recommended):
- First run `auto_enrich.py --output work/enrich_plan.json`
- Add `--enrich-plan work/enrich_plan.json` when rendering
- `broll[].suggested_asset` is turned into a timed B-roll video overlay; `chapter_cards[]`, `stickers[]` and `emphasis_cues[]` are turned into ASS badges; `emphasis_cues[]` can additionally produce a slight center push-in with no red frame; `chapter_cards[].png` / `imagegen[].image_path` / `imagegen[].generated_path` are turned into timed image overlays
- `pip_overlays[]` is turned into a timed facecam / camera inset; camera audio is ignored by default and the main audio still comes from the render config
- An imagegen cue with no actually generated file is only emitted as a hint and does not block rendering
- The merged visible text still goes through `_internal_text_guard` and `content_guard.py`

**Beat Edit Plan beat-cut skeleton** (optional for music videos / beat-synced montages):
- Run `beat_sync.py --bgm origin/bgm.mp3 --generate-plan --duration 30 --beats-per-cut 4 --min-segment 0.75 --max-segment 3 --output work/beat_edit_plan.json --markdown work/beat_edit_plan.md`
- Outputs `beat_edit_plan.v1`: `cut_times[]`, program-time `segments[]`, per-cut `boundary_evidence[]`, the detection method and `summary`
- One cut point is proposed every 4 beats by default; the min/max shot guards prefer re-selecting a nearby beat, and only insert a `duration_guard` when no suitable beat exists
- If `librosa` is unavailable or fails to read, only a fixed BPM grid is generated and the status is marked `review`; you must listen to the music and review it, and must not treat the fallback as real beats
- The plan only defines time slots; it does not select assets, render, or modify source files. After confirmation, map the shots into `render_config` / EDL / OTIO
- When cut times already exist, use the original command `beat_sync.py --bgm origin/bgm.mp3 --cuts work/cut_times.json --window 0.2`

**Speed Ramp local slow motion / velocity edit** (optional for action, product reveals, games, montages):
- First locate the impact frame frame by frame, then run `speed_ramp.py plan origin/action.mp4 --ramp 4.6,5.0,1,0.25,s_curve --hold 5.0,5.8,0.25 --ramp 5.8,6.2,0.25,1,ease --interpolate-fps 120 --output work/speed_ramp_plan.json --markdown work/speed_ramp_plan.md`
- `verify` recomputes the plan id, source file SHA-256, source/output coverage and per-segment `duration / speed`; any drift in the source or the pieces blocks immediately
- `apply` renders to a temp file in the same directory and only replaces the target on success; by default it does not overwrite, does not follow the output symlink, and cannot overwrite the source
- `--interpolate-fps` is FFmpeg motion interpolation and may produce limb / edge artefacts; extremely slowed audio must be auditioned, and add `--mute-audio` at the plan stage if necessary
- You must finally play it through completely at 1× with sound; after the speed change regenerate subtitles / timecoded artifacts, run render QA, and redo the final approval. See `docs/prompts/83-speed-ramp.md` for details

**Freeze-Punch keyframe freeze emphasis** (optional for expression peaks, action landings, product reveals):
- First determine the impact frame by frame, then run `freeze_punch.py plan origin/reaction.mp4 --freeze 4.8,0.8,1.08,0.5,0.42 --delivery work/reaction-freeze-punched.mp4 --output work/freeze_punch_plan.json --markdown work/freeze_punch_plan.md`
- It replaces the short picture window after the impact frame with that frame; audio and total duration stay unchanged, so this operation does not drift subtitle, chapter or cue timecodes
- A pending plan blocks; read the Markdown, then run `apply`, then `verify --strict`. apply only atomically promotes the output and writes the hash back after the H.264/AAC, yuv420p, size/fps/duration/audio-track contracts and full-length decoding pass
- You must check at 1× with audio: the freeze entry/exit, a visible speaker's frozen mouth, action jumps, subject cropping and upscale softening. Drift in source/plan/final cut invalidates the live gate; old QA, approval receipts and publish packages must be redone. See `docs/prompts/101-freeze-punch.md` for details

**J-cut / L-cut audio-leading / audio-carrying transition** (optional for interviews, narration, scene changes):
- Run only at boundaries the human explicitly selects: `audio_transition.py plan work/render_config.json --transition 1,j_cut,0.4 --transition 3,l_cut,0.5 --output work/audio_transition_plan.json --markdown work/audio_transition_plan.md`
- A J-cut uses the real source audio handle before the next clip's in-point; an L-cut uses the handle after the previous clip's out-point and skips an equal length of the next clip's opening audio to restore sync. An insufficient handle blocks outright; never fake it with silence
- The plan binds the render config, transcript, source/B-roll SHA-256 and canonical plan id; `verify` recompiles from live inputs, so a hand-edited digest cannot hide drift
- Use `audio_transition.py apply ... --output output/master.mp4 --receipt work/audio_transition_apply.json`, or add `--audio-transition-plan` to `render_final.py`; subtitles, overlays, BGM and the offset main audio are still completed in a single FFmpeg encode
- You must audition each changed boundary at 1× on headphones and on a phone, and confirm no swallowed words, repeated lines, doubled voices, clicks, pumping or background-noise jumps. A machine can only verify timing/hash, not whether overlapping dialogue is appropriate. See [docs/prompts/86-audio-transition.md](docs/prompts/86-audio-transition.md)

**Audio Cue Mix single-track SFX cue mix** (run after `audio_cue_sheet.v1` is reviewed):
- Run with the final standalone narration: `audio_cue_mix.py plan --project-dir . --cue-sheet work/audio_cue_sheet.json --voice work/final_narration.wav --delivery work/audio_cue_mix.wav --output work/audio_cue_mix.json --markdown work/audio_cue_mix.md`; local sound effects must live inside the project and are bound by SHA-256/media contract
- When `transition_whoosh / emphasis_ping / success_chime / warning_tick` are missing, you may explicitly add `--synthesize-missing` to use deterministic FFmpeg recipes; unknown categories, explicitly missing assets, out-of-range cues or illegal gain stay blocking
- `apply` normalises to 48 kHz stereo, trim/pad, short fade, millisecond-level `adelay` and restricted gain; `amix normalize=0` preserves the narration level, the limiter only does peak protection, and the output is atomically promoted only after full decoding passes
- After listening through once at 1×, run `confirm` and confirm each of `speech_intelligibility / cue_timing / sfx_level / creative_fit / clicks_or_clipping`, then `verify --strict`. Drift in the cue sheet, narration, SFX, output or review binding re-blocks
- This output contains no BGM; music still goes through `render_final.py --bgm-ducking`, and after mixing the music you must listen through again and run the final audio QA. See [docs/prompts/123-audio-cue-mix.md](docs/prompts/123-audio-cue-mix.md)

**Video Stabilization source-bound handheld stabilisation** (only for unwanted shake):
- Run `video_stabilization.py doctor` first. `plan --backend auto` prefers two-pass `vidstab`; when that filter is missing it explicitly writes single-pass `deshake` into the plan and keeps a degradation warning, and it will not silently swap the backend at apply time
- After watching the original, record the source SHA-256, the profile and the human decision with `plan origin/handheld.mp4 --decision stabilize --reviewed-by editor --output work/video_stabilization_plan.json --markdown work/video_stabilization_plan.md`; intentional handheld / pan should use `--decision keep` instead
- `apply ... --output work/handheld-stabilized.mp4 --comparison verify/handheld-stabilization-compare.mp4` only writes a new working copy and a full-length left/right A/B; afterwards you must watch the whole comparison at 1×, then run `confirm --reviewed-by ... --note ...`
- `pipeline_manifest.py` live-checks the hashes and review state of the source, the stabilised version and the comparison. Stabilisation cannot fix rolling shutter, motion blur or missed focus; keep using the working copy afterwards and never overwrite `origin/`. See `docs/prompts/84-video-stabilization.md` for details

**Video Enhancement local upscale / frame interpolation delivery** (run when an existing final cut needs a higher resolution or frame rate):
- First run `runtime_preflight.py analyze --profile video_enhancement ... --strict`, then `video_enhancement.py plan output/master.mp4 --target-short-edge 1080 --fps 60 --enhanced output/master-enhanced.mp4 --comparison verify/video-enhancement-ab.mp4 --output work/video_enhancement_plan.json --markdown work/video_enhancement_plan.md`
- `--target-short-edge` works for both landscape and portrait, and `--scale 2` can be used instead; omit the size parameters when only interpolating frames. The script only accepts upscaling and targets not below the source cadence; source hash, display size, target settings, filters, output and review all enter the canonical plan
- `apply` uses Lanczos and optional FFmpeg motion-compensated `minterpolate` and outputs H.264/AAC; the output is atomically promoted only after size, frame rate, duration, audio-track presence and full decoding of both MP4s pass
- Watch the full source-left/enhanced-right A/B at normal speed, then watch the complete enhanced file separately with sound; `confirm` must give `pass|fail` for each of `detail / edges / motion_cadence / audio_sync`. Any fail keeps blocking
- Lanczos is only a deterministic resize and cannot recover detail that does not exist in the source; interpolation can produce ghost/warp at cuts, occlusions, hands, faces and fast motion. When ML VSR or generative repair is needed, go through an authorised tool and redo the review on the real output. See [docs/prompts/126-video-enhancement.md](docs/prompts/126-video-enhancement.md)

**Chroma Key green / blue screen keying and background replacement** (only for an explicit solid-colour backdrop):
- First run `chroma_key.py prepare --project-dir . --foreground origin/presenter-green.mp4 --background origin/studio.png --output-video output/presenter-studio.mp4 --preview-dir verify/chroma_key --report work/chroma_key.json --markdown work/chroma_key.md`
- `prepare` generates a composite and a black-and-white matte at the default 15%/50%/85% time points; check hair/hand/clothing edges, subject holes, green/blue spill and background perspective/light colour, and if needed change only one of `--similarity / --blend / --despill` and preview again
- After all four items are confirmed, run `review --edge-quality pass --subject-integrity pass --spill-control pass --background-fit pass --reviewer ... --note ...`, then `apply --strict` and `verify --strict`
- A still-image background is kept; a video background loops from its first frame and its audio track is ignored; the final sound uses only the foreground. Any drift in source/background/preview/filter/output invalidates the live gate
- This is not blind AI matting or per-frame roto; complex transparent objects, reflections, backdrop wrinkles or a subject in the same colour should be reshot or handed to a professional keyer. Full notes in `docs/prompts/99-chroma-key.md`

**PIP Overlay screen-recording camera inset** (optional for screen-recorded tutorials):
- Run `pip_overlay.py --camera origin/facecam.mp4 --segment "0,18,bottom_right" --segment "18,42,top_right" --sync-offset 0.18 --output work/pip_overlay_plan.json --markdown work/pip_overlay_plan.md`
- Add `--enrich-plan work/pip_overlay_plan.json` when rendering; it can be passed repeatedly together with `screen_focus_plan.json` and `enrich_plan.json`
- Outputs `pip_overlay_plan.v1` containing `pip_overlays[]`, each segment's position, source_start, sync offset, size ratio, opacity and fades
- `render_final.py` compresses the camera inset timeline together with `--primary-speed` / `--speed`, so the PIP and the main picture do not drift apart in a speed-changed output

**Audio Sync automatic alignment of external audio** (recommended with in-camera audio + an external mic):
- Run `audio_sync.py --reference-media origin/camera.mp4 --external-audio origin/lav.wav --output work/audio_sync_plan.json --markdown work/audio_sync_plan.md --replace-output output/camera_lav_synced.mp4 --strict`
- Outputs `audio_sync_plan.v1` with `alignment.offset_seconds`, `confidence`, `replace_audio.command`, a Markdown review table and `summary.blocking`
- A positive offset means the external audio track is delayed; a negative offset means the head of the external audio track is trimmed
- After confirming the plan, add `--apply` to run the FFmpeg audio replacement; the script copies the original video stream and encodes the synced external track as AAC
- If the automatic estimate has low confidence, you can skip estimation with a manual offset such as `--offset 0.18`; `pipeline_manifest.py` blocks audio sync artifacts with low confidence or missing files

**Multicam Sync reversible multicam sync plan** (recommended when two or more devices record the same event):
- Run `multicam_sync.py --reference-media origin/cam-a.mp4 --angle origin/cam-b.mp4 --angle origin/cam-c.mp4 --measure-clock-drift --output work/multicam_sync_plan.json --markdown work/multicam_sync_plan.md --preview-output output/verify/multicam_sync_preview.mp4 --apply-preview --strict`
- Outputs `multicam_sync_plan.v1`: per-angle offset/confidence, the automatic loudest track, reference/source coverage, common overlap, pairwise consistency, optional drift probes/linear fit, and the preview command
- Originals are not modified or re-encoded; only `--apply-preview` produces a short grid preview. A positive offset means that camera's `t=0` sits later on the reference timeline
- A multi-track camera can select the active track with `--audio-stream "origin/cam-b.mp4=2"`; with no audio track or for a human decision use `--manual-offset "origin/cam-c.mp4=1.24"`
- Drift measurement is off by default; when enabled it takes 5 windows of 20 seconds by default and only fits `offset(R)=intercept+slope*R` with at least 4 consensus inliers, reporting ppm with an explicit sign, measurement resolution, accumulated drift, residuals and the unapplied `atempo/setpts` advisory factors
- Drift evidence only covers the selected reference/source audio tracks; it cannot be used to assert automatically that the video PTS or other audio tracks share the same clock
- Drift beyond the threshold or an unreliable fit enters the review gate; the script does not correct automatically, and audio and picture must use the same affine mapping. When disabled, head/middle/tail must still be re-checked for anything 30 minutes or longer
- Detailed usage and boundaries in [docs/prompts/76-multicam-sync.md](docs/prompts/76-multicam-sync.md)

**Audio-guided Multicam Switch director draft generated from speaker audio** (optional after sync passes):
- Run `multicam_switch.py plan --sync-plan work/multicam_sync_plan.json --speaker angle_00_cam-a=host --speaker angle_01_cam-b=guest --program-audio angle_00_cam-a --delivery output/multicam_switch_draft.mp4 --output work/multicam_switch_plan.json --markdown work/multicam_switch_plan.md --strict`
- Each track is normalised separately to its P10 noise floor / P95 peak; the camera switches only when both the activity score and the lead margin pass, ambiguous windows keep the previous camera, and a flash cut shorter than `--min-shot` folds back to the adjacent shot
- Highly correlated audio is blocked by default as a shared mix / strong crosstalk; `--allow-correlated-audio` only keeps an explicit override warning and cannot prove that speaker detection is reliable. Do not map a wide shot, a programme master or a camera without an isolated speaker track as a speaker candidate
- `--program-audio` designates continuous programme sound that does not follow the picture when mics switch. apply unifies canvas/FPS/SAR and then hard-cut renders, promoting the output only after the media contract and full decoding pass
- After a complete 1× playback with sound, confirm must record `speaker_selection / cut_timing / sync / audio_continuity` item by item; any drift in the source, sync plan, settings, output or review invalidates the manifest gate
- Detailed usage and boundaries in [docs/prompts/127-multicam-switch.md](docs/prompts/127-multicam-switch.md)

**Storyboard Plan storyboard and generation routing** (recommended before generating assets):
- Run `storyboard_plan.py --transcript work/transcript.json --clean-script work/clean_script.md --output work/storyboard_plan.json --markdown work/storyboard_plan.md`
- Outputs each shot's timecode, narration, first/motion/last-frame descriptions, the `codex_imagegen` / `dreamina_video` / `remotion_hyperframes` / `media_library_broll` route, fallback and continuity anchors
- Before using a generative video provider, run `provider_capability.py verify --bundle work/provider_capabilities.json --max-age-days 30 --strict` to check the exact UI/API surface, model, mode, aspect ratio, duration, resolution, reference limits and evidence date
- Optionally run `video_prompt_pack.py --storyboard-plan work/storyboard_plan.json --asset-root work --style-reference work/imagegen/style-key.png --capability-profile work/provider_capabilities.json --require-capability-profile --resolution 720p --output work/video_prompt_pack.json --markdown work/video_prompt_pack.md --strict` to turn the storyboard into Dreamina/Jimeng Seedance, Veo, LTX, Wan and Sora prompt packs, a shared style lock, approval and the capability gate
- Before submitting image-to-video, run `reference_frame_preflight.py --prompt-pack work/video_prompt_pack.json --output work/reference_frame_preflight.json --markdown work/reference_frame_preflight.md --require-style-reference --strict` to block missing / corrupt / direction-conflicting / severe aspect-conflicting reference frames
- Then run `storyboard_assets.py --storyboard-plan work/storyboard_plan.json --asset-root work --media-library . --output work/storyboard_assets.json --markdown work/storyboard_assets.md --strict` and confirm the assets are `ready` before rendering
- `dreamina_video` only means suitable for video generation; it does not submit tasks automatically. Confirm before submitting to Dreamina/Jimeng because it may consume credits
- For image generation prefer the built-in Codex `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).

**Reference Story Formula transfer of a reference film's story formula** (run when the user asks to borrow the emotional mechanism of a reference film):
- Put the reference video and its timecoded transcript into the project, then run `reference_story_formula.py prepare --project-dir . --reference-video origin/reference.mp4 --reference-transcript origin/reference_transcript.json --target-storyboard work/storyboard_plan.json --output work/reference_story_formula_request.json --markdown work/reference_story_formula_request.md --response-template work/reference_story_formula_response.json --strict`
- Watch the reference film in full and fill in each beat's `mechanism / viewer_state_before / viewer_state_after / trigger / camera_function / transferable_rule / do_not_copy`; every reference transcript segment must belong to exactly one ordered beat
- For every shot of the target storyboard fill in `beat_id / content_anchor / viewer_shift / surface_change / visual_action`; the order must not go backwards, and you must also explicitly exclude the reference film's pixels, audio, wording, branding and specific plot
- Run `audit --request ... --response ... --output work/reference_story_formula.json --markdown work/reference_story_formula.md --strict`, then `verify --report work/reference_story_formula.json --strict`. Drift in the inputs, the response or the derived report fails closed; long verbatim overlap produces a visible warning
- Add `--reference-story-formula work/reference_story_formula.json` to `video_prompt_pack.py`. The script live-verifies the byte binding with the current storyboard and injects the corresponding structure, target content anchor and no-copy contract into every prompt. It does not prove copyright clearance, originality, retention or generation quality. See [docs/prompts/125-reference-story-formula.md](docs/prompts/125-reference-story-formula.md)

**Storyboard Animatic storyboard timing preview** (recommended before paid generation or the formal edit):
- Prepare one reviewed still frame per storyboard shot, then run `storyboard_animatic.py plan --project-dir . --storyboard work/storyboard_plan.json --panel shot_001=work/storyboard/shot_001.png --panel shot_002=work/storyboard/shot_002.png --delivery verify/storyboard_animatic.mp4 --output work/storyboard_animatic.json --markdown work/storyboard_animatic.md`
- `--panel SHOT_ID=PATH` must cover every shot exactly; optionally add `--audio work/narration.wav`. The tool binds the SHA-256 and media metadata of the storyboard, panels and audio, and produces a CFR H.264 MP4 with shot IDs/timecodes at the shot starts
- After `apply`, play it through once at 1× and check `shot_order / timing_rhythm / panel_legibility / visual_continuity / audio_sync`, then run `confirm`; with no audio `audio_sync=not_applicable`
- `verify --strict` and `pipeline_manifest.py --require storyboard_animatic --strict` re-read all inputs live, fully decode the deliverable and check the review/output binding. Animatic only validates still-frame order and rhythm; the generated clips and the final cut still need their own review. See [docs/prompts/121-storyboard-animatic.md](docs/prompts/121-storyboard-animatic.md)

**Sequence Handoff shot handoff and edit boundaries before generation** (recommended with two or more generated shots):
- Run `sequence_handoff.py prepare --project-dir . --storyboard work/storyboard_plan.json --output work/sequence_handoff_request.json --markdown work/sequence_handoff_request.md --response-template work/sequence_handoff_response.json --strict`
- For each adjacent shot pair review `carrier_type / offer_from / receive_in / edit_type / match_requirement`, and declare the 180° axis, screen motion direction, audio bridge, head/tail edit handles, risks, fallback and a specific review note; automatic suggestions only read the storyboard text and cannot replace a human's spatial/action judgement
- Generate the report with `audit --request work/sequence_handoff_request.json --response work/sequence_handoff_response.json --output work/sequence_handoff.json --markdown work/sequence_handoff.md --strict`, then recompute live with `verify --report work/sequence_handoff.json --strict`; `revise`, missing fields, illegal axis/direction flips, and drift in the source storyboard or the decisions all fail closed
- Add `--project-dir . --sequence-handoff work/sequence_handoff.json` to the final prompt pack; the tool live-verifies first, then writes the first shot's handoff-out, the last shot's receive-in and the two-way contract of the middle shots into the corresponding provider prompt
- A text contract does not prove final pixel continuity; after generation still run `generated_clip_review.py` and `generated_sequence_review.py`, and review all boundaries at normal speed after assembly. See [docs/prompts/120-sequence-handoff.md](docs/prompts/120-sequence-handoff.md)

**Video Prompt Pack + Reference Frame Preflight generation prompts and reference-frame gate** (recommended before generating video):
- First build `provider_capabilities.json` per [docs/prompts/96-provider-capability.md](docs/prompts/96-provider-capability.md), then run `provider_capability.py verify --bundle work/provider_capabilities.json --max-age-days 30 --strict`; the profile binds the exact provider/surface/model and the source, and by default blocks anything older than 30 days
- Run `video_prompt_pack.py --project-dir . --storyboard-plan work/storyboard_plan.json --sequence-handoff work/sequence_handoff.json --asset-root work --character "same host" --brand-anchor "palette=charcoal,white,signal yellow" --style-reference work/imagegen/style-key.png --capability-profile work/provider_capabilities.json --require-capability-profile --resolution 720p --output work/video_prompt_pack.json --markdown work/video_prompt_pack.md --strict`
- Outputs `global.character_sheet_prompt`, `global.style_reference`, `global.sequence_handoff`, `items[].prompt`, `items[].sequence_handoff`, `items[].negative_prompt`, `items[].surface/model/resolution`, `items[].capability_profile`, `items[].capability_issues`, `items[].approval_status` and `summary.blocking`
- `--style-reference` binds the same style key to every shot and adds the same `STYLE LOCK` to every provider prompt
- Once the reference images are written to disk, run `reference_frame_preflight.py --prompt-pack work/video_prompt_pack.json --output work/reference_frame_preflight.json --markdown work/reference_frame_preflight.md --require-style-reference --strict`
- `reference_frame_preflight.v1` checks whether the first frame/style key exists, is decodable, matches direction/aspect, is not too low-resolution and has no transparent background; blocking items feed into `pipeline_manifest.py`
- `--provider veo|ltx|wan|sora|dreamina_seedance` can rewrite the same storyboard into the specified model's prompts; `--animate-stills` turns the `codex_imagegen` reference-image route into image-to-video prompts
- `--strict` returns 2 when the generated-video provider has not been `--approved`, or the capability profile is missing/expired/out of bounds; the script does not submit provider tasks or consume credits
- For image generation prefer the built-in Codex `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).

**Generation Reference Preflight image / video / audio generation reference gate** (must run when using multimodal references):
- The provider profile's `reference_media` must record whether frame/reference are mutually exclusive, whether audio-only is supported, the total file count, and the extension, byte size, per-item duration and total duration limits for image/video/audio; unverified limits must not be filled in by guessing
- First build the manifest with `generation_reference_preflight.py template --project-dir . --prompt-pack work/video_prompt_pack.json --output work/generation_references.json`; for every actually submitted asset fill in `kind / path / role / exclude`, and the order is the `@ImageN / @VideoN / @AudioN` order for each type
- Run `generation_reference_preflight.py analyze --project-dir . --prompt-pack work/video_prompt_pack.json --references work/generation_references.json --capability-profile work/provider_capabilities.json --output work/generation_reference_preflight.json --markdown work/generation_reference_preflight.md --strict`
- The report fully decodes the local references, checks type, extension, size, per-item/total duration, count, audio-only, frame/reference mode exclusivity and the capability-profile binding, and outputs `shots[].provider_prompt` with explicit roles and exclusions
- At submission use the ordered asset list and `provider_prompt` from the ready report; afterwards re-check live with `verify --report ... --strict` or `pipeline_manifest.py --require generation_reference_preflight --strict`. See [docs/prompts/122-generation-reference-preflight.md](docs/prompts/122-generation-reference-preflight.md)

**Generation Task Log async generation task ledger** (recommended after submitting generation tasks):
- Generate the pending-approval ledger from the provider decision: `generation_task_log.py import-provider-decision --provider-decision work/provider_decision.json --log work/generation_tasks.json --markdown work/generation_tasks.md --strict`
- After submitting to Dreamina/Jimeng you must save the `submit_id`: `generation_task_log.py add --log work/generation_tasks.json --provider dreamina --task-id <submit_id> --shot-id shot_002 --expected-path work/generated_video/shot_002.mp4 --status submitted`
- After the download completes, update the local file: `generation_task_log.py update --log work/generation_tasks.json --provider dreamina --task-id <submit_id> --status downloaded --asset-path work/generated_video/shot_002.mp4 --markdown work/generation_tasks.md`
- Outputs `generation_task_log.v1` with `poll_command`, `download_command`, `readiness` and `summary.blocking`
- `completed` but with no local `asset_path` still blocks; `pipeline_manifest.py` recognises `generation_tasks.json` automatically and lists uncleared tasks as a gate

**Generated Clip Review review of generated video clips** (must run after downloading generated results and before assembly):
- Refresh `storyboard_assets.json` first, then run `generated_clip_review.py prepare --project-dir . --asset-manifest work/storyboard_assets.json --contact-sheet-dir verify/generated_clips --output work/generated_clip_review_request.json --markdown work/generated_clip_review_request.md --response-template work/generated_clip_review_response.json`
- The reviewer must complete all four passes: 1× with sound, 0.25×, muted for picture, and audio-only; the contact sheet is only sampling navigation and cannot replace playing the complete clip
- The response records for each clip six 1–5 scores for identity/wardrobe, action/end state, motion/anatomy/physics, camera, frame integrity, look consistency, plus hard-fail codes, `keep_ranges` / `remove_ranges`, whether to regenerate, and the specific `prompt_fix`
- `pass` requires a weighted score ≥80 with no removed range; `pass_with_edits` requires ≥65 and keep/remove ranges that exactly cover the whole clip; a commonsense/physics, identity, prop, text watermark, continuity or audio-picture hard fail forces `fail` regardless of the total score
- After filling in the response, run `generated_clip_review.py audit --request work/generated_clip_review_request.json --response work/generated_clip_review_response.json --output work/generated_clip_review.json --markdown work/generated_clip_review.md --strict`; any clip/contact-sheet drift, missed review, illegal range or regeneration requirement blocks
- Before assembly/publishing, run `generated_clip_review.py verify --report work/generated_clip_review.json --strict`, or use `pipeline_manifest.py --require generated_clip_review --strict`. The reviewer label is not identity authentication or a digital signature

**Generation Chain Handoff reviewed-clip last-frame handoff** (run for continuous generation of the same scene):
- Use it only when `sequence_handoff.v1` has approved that adjacent boundary and the previous shot's `generated_clip_review.v1` is `pass` or `pass_with_edits`; do not force a handoff across scene changes, time jumps, deliberate jump cuts or ordinary edit matches
- Run `generation_chain_handoff.py prepare --project-dir . --clip-review work/generated_clip_review.json --sequence-handoff work/sequence_handoff.json --boundary boundary_001 --frame-output work/generation_chain/boundary_001_tail.png --output work/generation_chain_handoff.plan.json --markdown work/generation_chain_handoff.plan.md`; the script enumerates decoded frames, takes the exact last frame from the full clip or from the last approved keep range, and requires the source frame/PNG RGB24 pixel digest to match
- After watching the previous shot and the PNG, run `confirm --decision use_exact_start_frame --reviewed-by ... --same-scene-confirmed --tail-frame-accepted --original-anchors-preserved --notes ... --output work/generation_chain_handoff.json --strict`; if the tail frame is unsuitable, use `reject` and return to the storyboard, generation or an ordinary edit boundary
- Before submitting the next shot, run `verify --plan work/generation_chain_handoff.json --strict`, then add `--generation-chain-handoff work/generation_chain_handoff.json` to `video_prompt_pack.py`. The target shot switches to `image_to_video` and the reviewed tail frame becomes the exact first frame; the prompt keeps the original character/product/style anchors and is limited to one primary action and one camera behaviour
- Drift in upstream reports, clips, approved ranges, frame index/PTS, PNG bytes/pixels, the human decision or the canonical artifact ID blocks; every new generated clip still needs its own review, and the cross-shot review runs after all are done. See [docs/prompts/124-generation-chain-handoff.md](docs/prompts/124-generation-chain-handoff.md)

**Generated Motion Window usable motion window of generated video** (run after the per-clip visual review and before cross-shot assembly):
- A contact sheet may miss a 0.25–1 second frozen opening or brief motion; for every generated clip that passed visual review run `generated_motion_window.py analyze work/generated_video/shot_001.mp4 --project-dir . --output work/generated_motion_window/shot_001.json --markdown work/generated_motion_window/shot_001.md`
- The script uses local FFmpeg `freezedetect` to save `freezes[]` and its complement `active_intervals[]`, and binds source SHA-256, media contract, parameters and canonical plan id; a fresh analyze blocks by default, and detector suggestions must not be treated as approval
- After playing the source through once at 1×, run `confirm ... --decision trim|keep|reject --reviewed-by ... --note ...`. `trim` uses the suggested interval by default, or explicit `--start/--end`; both boundaries must fall inside an active interval
- `trim` keeps blocking until apply; `apply ... --output work/generated_motion_window/shot_001-active.mp4` only writes a new H.264/AAC, `yuv420p` working copy, and atomically promotes it only after verifying duration/size/fps/audio track and fully decoding. Drift in the source, the plan, the detection evidence or the output fails closed
- The new working copy must be played through in full again, and generated-clip review, sequence/final QA and downstream approval must be re-run; `pipeline_manifest.py --require generated_motion_window --strict` live-verifies each `work/generated_motion_window/*.json`. FFmpeg freeze only measures full-frame similarity; it cannot judge action meaning, performance, identity, physics or product quality. See [docs/prompts/102-generated-motion-window.md](docs/prompts/102-generated-motion-window.md)

**Scoped Video Edit Review scope review of localised AI video edits** (must run after any edit mode such as wardrobe/background/packaging/local VFX changes):
- Declare exactly one change per provider call; run `scoped_video_edit_review.py prepare --project-dir . --source origin/source.mp4 --edited work/scoped_video_edit.mp4 --change-category wardrobe --change "change only the jacket from blue to red" --preserve subject_identity --preserve performance_motion --preserve camera_motion --preserve framing_composition --preserve source_audio --evidence-dir verify/scoped_video_edit --output work/scoped_video_edit_review_request.json --markdown work/scoped_video_edit_review_request.md --response-template work/scoped_video_edit_review_response.json`
- `prepare` binds the source/edited SHA-256 and media contract and generates side-by-side JPEGs at 15%/50%/85% of the in-range time points, plus a full side-by-side H.264 preview; when both inputs have audio, the preview keeps source/edited as separate tracks
- The reviewer must watch the source and the edited result in full at 1× with sound separately, then watch the complete A/B scope preview; the target change and each `--preserve` may only be `pass|fail|not_observable` — never guess pass when it is not clearly visible
- `audit --strict` is ready only when the target change is genuinely done, every preserve item passes, all three playbacks are confirmed and duration/size/frame rate have not drifted; a failure must write a specific `repair_action`
- Before assembly/publishing run `verify --report work/scoped_video_edit_review.json --strict`, or `pipeline_manifest.py --require scoped_video_edit_review --strict`. The script does not call the provider, upload assets or consume credits; the suggested prompt is a provider-neutral contract and cannot replace capability verification of the current surface. See `docs/prompts/100-scoped-video-edit-review.md`

**Generated Sequence Review cross-shot continuity review** (must run once two or more generated clips pass individually and before assembly):
- Run `generated_sequence_review.py prepare --project-dir . --clip-review work/generated_clip_review.json --storyboard-plan work/storyboard_plan.json --evidence-dir verify/generated_sequence --output work/generated_sequence_review_request.json --markdown work/generated_sequence_review_request.md --response-template work/generated_sequence_review_response.json`
- In storyboard order, the script extracts for each adjacent boundary the previous shot's tail frame within the approved range, the next shot's first frame, a side-by-side image and a silent 1× "tail + head" preview; if the upstream is `pass_with_edits`, only the approved first/last keep range is used
- After viewing the previews and comparisons in full, fill in `match|intentional_change|mismatch|not_applicable` for identity/wardrobe, prop state, spatial orientation, action end state, camera framing, lighting/palette. At least two items must be genuinely assessed; `mismatch` must be `fail`, with a failure code and a specific `repair_action`
- Run `generated_sequence_review.py audit --request work/generated_sequence_review_request.json --response work/generated_sequence_review_response.json --output work/generated_sequence_review.json --markdown work/generated_sequence_review.md --strict`, and before assembly/publishing `verify --report work/generated_sequence_review.json --strict`; drift in any clip, upstream review, storyboard or evidence bytes blocks
- `pipeline_manifest.py --require generated_clip_review --require generated_sequence_review --strict` can gate both the per-clip and the cross-shot layer. The preview is silent and cannot replace the full sound-and-picture review of the final master; reviewer labels and SHA-256 are not identity authentication or signatures

**Generation Lessons generation-video lessons feedback loop** (optional; only capture lessons that generalise and are explicitly approved by a human):
- Pick a clip from a completed `generated_clip_review.json` and run `generation_lessons.py add --library work/generation_lessons.json --review work/generated_clip_review.json --clip-id shot_002 --category hand_contact --lesson "For hand-to-prop contact, isolate one interaction and keep the hand visible through release." --approved-by "<reviewer-label>" --model seedance-2.0`
- `add` recomputes the canonical review live and allows the expected blocker "the clip genuinely needs regeneration", but rejects source/contact-sheet drift, missed review, illegal responses and tampered stored summary/report-id; the entry binds the report/request/clip/contact-sheet SHA-256. `approved_by` is only an audit label, not identity authentication or a signature
- Before reuse run `generation_lessons.py verify --library work/generation_lessons.json --strict`; preview available lessons by provider/model/category with `generation_lessons.py select --library work/generation_lessons.json --provider dreamina_seedance --model seedance-2.0 --limit 3 --output work/selected_generation_lessons.json --markdown work/selected_generation_lessons.md`
- When new evidence conflicts with an old rule, do not delete the old entry; create a new lesson with `--supersedes <old-lesson-id>`. The old evidence stays in the library, but `select` excludes a superseded rule as long as a new entry also matches the current provider/model/category
- The next generation prompt pack explicitly adds `video_prompt_pack.py ... --lesson-library work/generation_lessons.json --lesson-model seedance-2.0 --lesson-limit 3`. Provider-specific rules take priority, and without `--lesson-model` model-specific lessons are not misapplied; the script only appends human-approved `lesson` items, never treats a clip-specific `prompt_fix` as a general rule, and never regenerates or consumes credits automatically
- For image generation prefer the built-in Codex `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).
- Detailed notes in [docs/prompts/89-generated-clip-review.md](docs/prompts/89-generated-clip-review.md)

**Storyboard Assets asset manifest and preflight** (recommended after the storyboard and before rendering):
- Run `storyboard_assets.py --storyboard-plan work/storyboard_plan.json --asset-root work --media-library . --output work/storyboard_assets.json --markdown work/storyboard_assets.md`
- Outputs each shot's asset status: `ready` / `candidate_found` / `needs_generation` / `needs_approval` / `needs_render` / `search_needed`
- If `--media-library` is passed, `media_library_broll` shots get `candidate_paths` + `candidate_scores` generated from the asset index, transparently ranked by tag, filename, metadata, duration and aspect ratio
- With `--strict`, any asset not ready returns exit code 2, which suits the step right before the final render
- `needs_approval` means a task such as Dreamina/Jimeng that may consume credits and must be confirmed before submission; for image generation prefer the built-in Codex `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).

**Custom cover** (`cover_image`):
- Provide a path to a custom cover PNG; it takes priority over the auto-generated cover
- The size should match the video resolution (e.g. 1080x1920)

**Persistent overlay** (`video_overlay`):
- Provide a transparent PNG path; it is overlaid continuously over the whole video after the cover
- Use cases: brand watermark, series marks (e.g. DAY labels, data metrics), etc.
- The PNG must contain an alpha channel (RGBA); transparent areas do not obscure the video

**Blinking dot** (`rec_blink`):
- Overlay a small periodically blinking dot PNG (e.g. a camcorder REC mark)
- `dot_image`: path to the dot PNG (12-16px recommended, RGBA)
- `x`, `y`: pixel coordinates of the dot in the video frame
- `period`: blink period in seconds, default 1.0 (0.5s on + 0.5s off)

**End cards** (`end_cards`):
- Append black-screen text cards at the end of the video; each card fades in and out over 300ms
- `text`: card text content, use `\n` for line breaks
- `duration`: display duration per card in seconds, 3.0-4.0 recommended
- Text is centred, at 1.4× the size of the body subtitles

**Talking-head steady noise cleanup** (`speech_denoise`):
- Defaults to `off`; only enable `"light"` / `"medium"` / `"strong"` when air conditioning, fans, mains hum or light room noise are relatively steady
- On the CLI use `--speech-denoise light`; when the config has it on, `--no-speech-denoise` disables it temporarily
- All three levels use a fixed 80 Hz, 2-pole high-pass with FFT reduction of 6/9/12 dB; `strong` never exceeds 12 dB
- The order is fixed: `highpass → afftdn → atempo → dynaudnorm → acompressor → loudnorm → cover delay → BGM ducking/mix`
- Tracks that already went through a VAD/noise gate, Adobe Podcast, Descript, RX or strong in-camera noise reduction usually stay off; digital silence, sudden noise changes, multiple mics, transient knocks/coughs and reverb must be handled separately
- First render a 10–20 second off/light/medium A/B and audition consonants, tail sounds and pauses on headphones at normal speed; after rendering keep running `audio_master_report.py --strict`

**Background music** (`bgm`):
- Provide a background music file path (MP3/M4A/WAV and other formats FFmpeg supports)
- `bgm_volume`: BGM volume (0.0-1.0), default 0.15 (voice first, BGM second)
- `bgm_fade_out`: end fade-out duration in seconds, default 3.0
- `bgm_ducking: true` or CLI `--bgm-ducking`: use the final narration track as the sidechain to automatically lower the BGM while narration plays; over the cover, pauses and the tail without narration the music recovers naturally
- The default ducking parameters are threshold `0.03`, ratio `8`, attack `20ms`, release `500ms`; adjust only when auditioning or `audio_master_report.py` evidence shows it is needed
- The ducking branch uses `amix normalize=0` to preserve voice loudness and adds `alimiter=0.95` at the end of the mix to prevent peak overs; old configs still default to off so existing output stays compatible
- The BGM loops automatically until the video ends; there is no need to pre-trim its length
- Recommended free sources for commercial use: Pixabay Music, Mixkit, YouTube Audio Library
- Track selection advice: for talking-head/tutorial content use gentle instrumental music (no vocals) with a soft rhythm so it does not fight the voice

**Subtitle style presets** (`subtitle_style`):
| Style | Effect | Use case |
|------|------|---------|
| `normal` | White text with black outline (default) | Suitable for all content |
| `karaoke` | Word-by-word highlight | Music / rhythmic content |
| `bold_pop` | Heavy outline, high contrast | MrBeast/Hormozi style |
| `neon` | Neon cyan-purple | Tech/trend content |
| `minimal` | Minimal, no outline | Artistic/quiet content |
| `yellow_pop` | Yellow text with black outline | High visibility, outdoor/noisy footage |

**Karaoke subtitles / word-by-word highlight** (`subtitle_style: "karaoke"`):
- Set `"subtitle_style": "karaoke"` in the config to enable word-by-word highlighting
- You must first run speech recognition with `--word-timestamps` to obtain word-level timestamps
- `subtitle_highlight_color`: colour of the current word, default `"#FFFF00"` (yellow)
- `subtitle_base_color`: base colour of words not yet spoken, default `"#FFFFFF"` (white)
- `subtitle_base_alpha`: base colour opacity hex, default `"80"` (semi-transparent)
- If the transcript has no word-level timestamps it falls back to an even per-character distribution (slightly worse)
- The CLI argument `--subtitle-style karaoke` can also override the config
- Typical workflow:
  ```bash
  # 1. Enable word-level timestamps during transcription
  python3 scripts/transcribe.py audio.wav --model auto --language zh --word-timestamps
  # 2. Choose the karaoke subtitle style when rendering
  python3 scripts/render_final.py --config render_config.json --output final.mp4 --subtitle-style karaoke
  ```

**Audio source substitution (M4A/standalone audio)**:
- If you have a separately recorded audio file (M4A etc.), first convert it with ffmpeg to an MP4 with a black video track:
  ```bash
  ffmpeg -f lavfi -i "color=c=black:s=1080x1920:r=30:d=60" -i audio.m4a -c:v libx264 -c:a aac -shortest audio.mp4
  ```
- Then use `"video": "audio.mp4"` in the clip to supply the audio source and `"broll"` to supply the picture
- This lets you pair a re-recorded voiceover with any picture

**Cover title**:
1. If the user provided a title, use it directly.
2. If the user has no specific requirement, **from the viewer's perspective** summarise an attractive title (6-15 characters).
3. `subtitle` is optional; it adds an emotional hook or key information.
4. **Mobile first**: on a phone list page the cover is only a thumbnail, so the title must guarantee readability first and picture detail second.

**Cover text layout rules**:
- Design the title for **at most about 8 CJK characters per line**; wrap automatically beyond that, and do not cram a single line full.
- Estimate English letters/digits at **about half a CJK character width**; e.g. `AI` or `GPT-5` should not blow out the whole line width.
- `subtitle` defaults to **about 50%** of the title size; it only carries supplementary information and does not compete with the main title for visual focus.
- If the title exceeds two lines, prefer **shortening the copy** rather than shrinking the font to force it in.
- For tutorial/tool covers keep the main title to **4-8 characters** and the subtitle to **6-12 characters**.

**Cover style** (`cover_style`) — choose the most suitable style for the video content:
| Style | Effect | Use case |
|------|---------|---------|
| `bold` | Tutorials, explainers, technical | Black background + large bold white type, simple and strong |
| `news` | Breaking news, opinion, controversy | Dark gradient background + white title + yellow subtitle, high impact |
| `frame` | Vlog, live footage, scenes | First video frame as background + dark overlay + outlined white type |
| `gradient` | Life, emotion, art | Purple-pink gradient background + glowing white type, soft and elegant |
| `minimal` | Thinking, culture, depth | Pure black background + thin white type, minimal and restrained |
| `white` | Tutorials, products, branded content | Pure white background + dark type, a more modern feel |
| `techcard` | Screen recordings, software tutorials, AI tool demos | Large title on the left + picture card on the right, balancing information and readability |

The AI agent should choose automatically based on the video subject and content tone:
- Tech/tool → `bold` or `news`
- Controversy/opinion → `news` (white title + yellow subtitle is the most eye-catching)
- Vlog/live footage → `frame`
- Emotion/life → `gradient`
- Depth/culture → `minimal`
- Pure desktop screen recording / software tutorial → prefer `techcard`; if the picture is too busy, fall back to `bold` / `white`

**Background frame selection rules**:
- Do not mechanically use the first frame. For screen-recorded tutorials, prefer a frame with **higher information density** and a more complete interface as the background or card image.
- If the background picture would hurt title legibility, prefer a plain-background style such as `bold` / `white` / `minimal`.
- To preview a cover on its own, use `scripts/generate_cover_image.py --frame-timestamp 00:10:00` to specify the frame time.

**Multiple cover A/B variants and the final choice**:
```bash
python3 scripts/cover_variants.py output/final_xhs.mp4 \
  --title "shoot a video in 20 minutes" \
  --subtitle "the full AI editing workflow" \
  --caption output/caption.json \
  --platform xhs \
  --frame-timestamp 12.5 \
  --output-dir output/covers \
  --render \
  --output work/cover_variants.json \
  --markdown work/cover_variants.md
```

By default it produces 3 variants and small `*_preview.png` images. Compare them at feed size first, then re-run with `--select cover-c --require-selection --strict` to record the final cover. The title carries the subject/keywords, the cover carries the result/emotion/contrast/evidence; add `--require-distinct-cover-text` when distinct cover text must be enforced. `pipeline_manifest.py --require cover_variants` can make the selection a publishing gate. If an AI-generated or AI-edited cover base image is needed, prefer the built-in Codex `image_gen` tool, i.e. OpenAI GPT Image 2 (`gpt-image-2`).

**Cover duration** (`cover_duration`):
- Default 2.0 seconds; the first frame is frozen and the cover overlaid
- Can also be overridden with the `--cover-duration` command-line argument

**Chapter division**:
- Divide chapters by the video's content logic; **no more than 4 chapters** recommended
- Chapter names must be **short** (2-4 characters), e.g. pain point, cause, solution, tools
- Chapter times must be computed precisely from the cumulative durations of the selected segments

### Phase 4.8: Edit Revision (reversible revision of edit artifacts, optional)

When the user asks to change `render_config.json`, `enrich_plan.json`, a caption or a subtitle sidecar, and also needs a full review pass, dependency drift protection and undo/redo, prepare a proposal first:

```bash
python3 scripts/edit_revision.py prepare \
  --project-dir . \
  --artifact work/render_config.json \
  --artifact work/enrich_plan.json \
  --depends-on work/transcript_reviewed.json \
  --title "Tighten the opening and adjust B-roll" \
  --reason "Timecoded review is done; adopting the second version of the opening." \
  --output work/edit_revision_proposal.json
```

Change only `artifacts[].proposed_content` in the proposal, then run `audit`. A legal audit returns 2 for `pending_approval` under `--strict`; that is the expected manual gate. You must save a separate approval JSON bound to the same `review_id` with `decision: approve` and a non-empty `approved_by_label`, and only then does `apply` write the multiple files together as one revision and create `work/edit_revision_history.json`. A runtime write error attempts to restore the old bytes, but cross-file OS-level atomic commit is not promised; after a process crash run `status --strict` first. status checks artifacts, applied dependencies and content-addressed before/after blobs live; external hand edits, dependency drift, symlinks or corrupted blobs make it refuse undo/redo. Raw media, code, rendered output and `verify/` are out of scope. Full flow: [docs/prompts/80-edit-revision.md](docs/prompts/80-edit-revision.md).

### Phase 4.85: Portable Edit Recipe (portable edit recipe, optional)

When the user wants to save a reviewed `render_config.json` as a template, or reproduce the same timeline/subtitle/BGM/overlay structure on a new batch of footage, export a path-free recipe first:

```bash
python3 scripts/edit_recipe.py export \
  --config work/render_config.json \
  --name fast-tech-explainer \
  --description "Fast-paced tech talking head" \
  --output work/recipes/fast-tech-explainer_edit_recipe.json \
  --markdown work/recipes/fast-tech-explainer_edit_recipe.md
```

The export must pass source preflight first; local file references become typed slots, the same path occupies one slot only, and the recipe stores no original paths. In the new project, open the Markdown slot table, pass `--bind SLOT=PATH` once per slot when running `replay`, and write `--receipt` and `--markdown` at the same time; the script validates the portable digest, the slot set, file type and existence, records the new binding hashes, and re-runs preflight on the output config. It does not overwrite by default; add `--force` only when the target is definitely correct. The recipe hash is not a signature or a manual approval; after a replay you still must render and watch the full final cut. Full flow: [docs/prompts/82-edit-recipe.md](docs/prompts/82-edit-recipe.md).

### Phase 4.9: Platform Safe Area QA (platform UI safe-area gate)

Before rendering or multi-platform export, check per target platform whether subtitles, emphasis badges, the PIP presenter, CTA, chapter cards and tap markers fall into the top status bar, the bottom caption area or the right-side interaction button rail:

```bash
python3 scripts/platform_safe_area_qa.py \
  --config work/render_config.json \
  --enrich-plan work/enrich_plan.json \
  --platform xhs \
  --output verify/xhs_platform_safe_area_qa.json \
  --markdown verify/xhs_platform_safe_area_qa.md \
  --guide verify/xhs_platform_safe_area_guide.svg \
  --strict
```

For Douyin and WeChat Channels derivatives use `--platform douyin` / `--platform wxch` respectively. The script estimates bounding boxes from the default ASS, PIP and focus-marker geometry rules of `render_final.py`; CTA/logo elements outside the renderer can be supplied as pixel or normalized bounding boxes through `--elements custom_elements.json`. When a platform app UI changes, override the measured margins with `--safe-left` / `--safe-top` / `--safe-right` / `--safe-bottom`.

This is a local, auditable layout gate: it uploads nothing, calls no LLM, does no OCR, and never infers where the subject or text sits inside a generated image or a cover. Existing full-image chapter cards are marked `uncheckable` and need a manual check in the SVG guide or a rendered frame. `summary.blocking > 0` makes `--strict` return 2; to make it mandatory on the production line, use `pipeline_manifest.py --require platform_safe_area_qa --strict`.

### Phase 4.95: Subtitle Style Preview (subtitle style preview on real footage)

Before the final encode, render the real ASS presets onto representative early/mid/late frames of the source clip:

```bash
python3 scripts/subtitle_style_preview.py create \
  --project-dir . \
  --video origin/talking.mp4 \
  --platform xhs \
  --text "This subtitle line covers English, Chinese and the digits 2026" \
  --preview-dir verify/subtitle_styles \
  --output work/subtitle_style_preview.json \
  --markdown work/subtitle_style_preview.md \
  --require-selection

python3 scripts/subtitle_style_preview.py select \
  --report work/subtitle_style_preview.json \
  --style normal

python3 scripts/subtitle_style_preview.py verify \
  --report work/subtitle_style_preview.json \
  --strict
```

It compares `normal` / `minimal` / `bold_pop` by default and uses the same ASS builder, fonts, font sizes and target-aspect center crop as `render_final.py`. Look at both phone size and full size; once confirmed, pass the chosen value to `render_final.py --subtitle-style`. A change in the source clip, fonts, style definitions or JPEG bytes invalidates the old selection. A `karaoke` preview only proves the visual style; real word-by-word timing still depends on word-level timestamps. You still have to run safe-area QA, readability QA and a 1x review of the final cut. See [docs/prompts/94-subtitle-style-preview.md](docs/prompts/94-subtitle-style-preview.md).

### Phase 5: Single-Pass Render (single render pass)

Use [render_final.py](./scripts/render_final.py) to produce the final video from the original footage in **one encode**:

```bash
# Remove --style-profile if no profile was generated in Phase 0.25.
python3 scripts/render_final.py --config render_config.json \
  --style-profile work/edit_style_profile.json \
  --enrich-plan work/enrich_plan.json \
  --output final.mp4 --speed 1.25 1.5
```

**Core principle**:
- **Single video** (the most common case): uses `select/aselect` + `between()` expressions to filter every kept segment in one pass; FFmpeg decodes the whole source but encodes only the selected frames, and with `-crf 18 -preset medium` it is encoded exactly once
- **Multi-video montage**: `trim/atrim` cuts + `concat` joins (automatic fallback)
- The cover freezes the first frame with `tpad` and adds silence with `adelay`, subtitle timings shift automatically, all inside **one encode**
- The chapter timeline is not burned into the video; after rendering it is emitted as text for the user to paste manually into Xiaohongshu and similar platforms

Parameters:
- `--config`: path to the render config JSON
- `--style-profile`: optional; after validation it only fills in personal/brand style defaults that are missing from the config
- `--enrich-plan`: optional; reads the JSON written by `auto_enrich.py` and wires B-roll / chapter cards / stickers / emphasis marks / generated-image cues back into the render
- `--output`: output file path
- `--speed 1.25 1.5`: also emit speed variants (each variant is encoded directly from the original footage, never recompressed from an already encoded video)
- `--cover-duration 2.0`: cover freeze duration in seconds, overriding `cover_duration` in the config
- `--font-path`: custom font file
- `--font-size`: subtitle font size (default 120, i.e. portrait width 1080 ÷ 9 = 120, each Chinese character taking 1/9 of the screen width, auto-scaled from the real resolution)
- `--no-subtitles`, `--no-cover`: skip the corresponding feature

**Output**:
- `final.mp4` (normal speed)
- `final_1_25x.mp4` (1.25x speed)
- `final_1_5x.mp4` (1.5x speed)
- After rendering, the terminal prints the chapter timeline text, ready to copy into Xiaohongshu

**Multi-platform format export**:
```bash
python3 scripts/render_final.py --config render_config.json --output final.mp4 \
  --formats vertical square horizontal
```

It also outputs:
- `final_vertical.mp4` (9:16 Douyin/Xiaohongshu/TikTok)
- `final_square.mp4` (1:1 Instagram)
- `final_horizontal.mp4` (16:9 YouTube/Bilibili)

The crop strategy is center-crop, keeping the frame's subject unchanged.

**Automatic features**:
- Subtitles auto-detect the language, auto-wrap lines, and get portrait-optimized positioning
- The cover automatically overlays the title text (with stroke and shadow), freezing the first frame for 1-2 seconds
- Subtitle timings scale automatically in the speed variants
- B-roll is auto-scaled and cropped to match the main video resolution
- Timed B-roll / image overlays from `--enrich-plan` are auto-scaled and cropped to match the main video resolution
- The end card automatically appends black frames + silence + fade-in/fade-out subtitles
- Persistent overlays and blinking dots are enabled automatically after the cover
- Video/audio durations are auto-aligned (`-shortest`), avoiding duration mismatches on platform upload

### Phase 5b: Export to JianYing / CapCut (export a JianYing project)

If the user wants to keep editing in JianYing (adding effects, grading, fine-tuning transitions and so on), export the edit plan **straight to a JianYing project file** with no ffmpeg render:

```bash
python3 scripts/export_capcut.py --config render_config.json --output ./my_draft
```

**What is exported**:
- **Video track**: all selected segments in order on the main video track, including the opening cover (frozen first frame or a custom cover image)
- **Subtitle track**: the transcribed text of each segment as a separate subtitle item under the frame
- **Text track**: cover title, subtitle and end-card text (kept separate from subtitles so they stay independently editable)
- **Audio track**: background music (BGM), with the volume set automatically
- **Transitions**: fade-in/fade-out transitions (300 ms) added automatically between segments
- **B-roll**: B-roll can replace the picture (the broll path is used as the video source)

Parameters:
- `--config`: path to the render config JSON (the same render_config.json as render_final.py)
- `--output`: output folder path (creates draft_content.json and draft_meta_info.json)
- `--name`: project name (defaults to the title in the config)

**How to use it**:
1. Copy the exported folder into the JianYing drafts directory:
   - macOS: `~/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/`
   - Windows: `%APPDATA%/JianyingPro/User Data/Projects/com.lveditor.draft/`
2. Open JianYing; the project appears in the "Drafts" list
3. In JianYing you can keep adding effects, adjusting transitions, grading, exporting, and so on

**Notes**:
- The exported project references video/audio assets by **absolute path**, so do not move the asset files
- JianyingPro is supported; drafts created this way open in JianYing 6+
- This feature and Phase 5 (ffmpeg render) are **mutually exclusive**: if the user only needs the final video, use render_final.py; if further editing in JianYing is needed, use export_capcut.py

If the subtitles were already generated with Auto Captions in JianYing/CapCut and manually proofread, you can import them back as this project's transcript:

```bash
python3 scripts/import_capcut_subtitles.py \
  --draft ~/Movies/JianyingPro/User\ Data/Projects/com.lveditor.draft/my_draft \
  --transcript work/capcut_transcript.json \
  --cut-list work/capcut_gap_cut.json \
  --markdown work/capcut_subtitles.md \
  --source-media origin/talking.mp4
```

You can also import an SRT exported from JianYing:

```bash
python3 scripts/import_capcut_subtitles.py \
  --srt exports/capcut_auto_captions.srt \
  --transcript work/capcut_transcript.json \
  --srt-output output/subtitles/capcut_clean.srt
```

`--cut-list` builds `keep_segments` from subtitle gaps, which suits reviewing first with `timeline_view.py --cut-list` and then handing over to `export_edl.py` or a later rough-cut flow. By default it reads subtitle material only; if the draft stores auto captions as a plain text track, add `--include-overlays` as well.

If you already have an SRT and manual/agent-written numbered subtitle edit notes, use `srt_edit_plan.py` to generate a reviewable edit plan directly:

Example `work/edit_guide.md`:

```md
title: Launch event highlights
platform: xhs
keep 3-5: Start with the product launch and the audience reaction
drop 1-2: The setup is too slow
keep 8: Add one line with the core conclusion
```

```bash
python3 scripts/srt_edit_plan.py \
  --srt exports/capcut_auto_captions.srt \
  --guide work/edit_guide.md \
  --source-media origin/talking.mp4 \
  --output work/srt_edit_plan.json \
  --render-config work/render_config.json \
  --cut-list work/srt_edit_cut.json \
  --markdown work/srt_edit_plan.md \
  --strict
```

`keep/include/use/select` lines produce the final output order in the order they appear; `drop/skip/exclude/remove` lines go into review. The `--cut-list` output is sorted by original footage time, which suits `timeline_view.py --cut-list` for inspecting cut points; the real reordered output comes from `--render-config`. To force every subtitle number to be reviewed, add `--require-all-reviewed --strict`.

### Phase 5c: NLE Handoff EDL / FCPXML / OTIO (hand off to Premiere / FCP / Resolve)

If the user does not want a JianYing draft but wants to hand the automatic edit plan to professional editing software for grading, mixing, fine cutting or collaborative review, export a single-track EDL, a single-spine FCPXML or OpenTimelineIO `.otio`:

```bash
python3 scripts/export_edl.py --config render_config.json --output work/edit.edl --fps 30
python3 scripts/export_edl.py --cut-list work/rough_cut.json --output work/rough_cut.edl --fps 30
python3 scripts/export_fcpxml.py --config render_config.json --output work/edit.fcpxml --fps 30 --width 1080 --height 1920
python3 scripts/export_fcpxml.py --cut-list work/rough_cut.json --output work/rough_cut.fcpxml --fps 30
python3 scripts/export_otio.py --config render_config.json --output work/edit.otio --fps 30
python3 scripts/export_otio.py --cut-list work/rough_cut.json --output work/rough_cut.otio --fps 30
```

`export_edl.py` / `export_fcpxml.py` / `export_otio.py` all also write a `<output>.json` manifest that keeps the absolute source paths, exact seconds, record/source timecode and the event list. `export_otio.py` writes V1 + A1 tracks by default; use `--no-audio-track` to hand off video only. Complex subtitles, overlays, chapter cards and B-roll still go through `render_final.py` / `export_capcut.py`; NLE handoff only carries a lightweight selection timeline.

### Phase 5d: Final Audio Storyboard (rebuild audio after the visual EDL is locked)

For multi-segment generated video or continuous short films, lock the visual EDL first, then rebuild narration, dialogue, ambience, Foley and music against the final record timeline; do not apply the whole pre-cut audio plan unchanged to a shortened picture. Each EDL event's label should correspond to a shot id in `storyboard_plan.json`:

```bash
python3 scripts/export_edl.py \
  --config work/render_config.json \
  --output work/locked_visual.edl \
  --manifest work/locked_visual.edl.json \
  --fps 30

python3 scripts/final_audio_storyboard.py prepare \
  --project-dir . \
  --edl work/locked_visual.edl.json \
  --storyboard work/storyboard_plan.json \
  --output work/final_audio_storyboard_request.json \
  --markdown work/final_audio_storyboard_request.md \
  --response-template work/final_audio_storyboard_response.json \
  --strict

# After filling in the response:
python3 scripts/final_audio_storyboard.py audit \
  --project-dir . \
  --request work/final_audio_storyboard_request.json \
  --response work/final_audio_storyboard_response.json \
  --output work/final_audio_storyboard.json \
  --markdown work/final_audio_storyboard.md \
  --strict
python3 scripts/final_audio_storyboard.py verify \
  --project-dir . \
  --report work/final_audio_storyboard.json \
  --strict
```

Each final section must state `single_track / sectioned_tracks / stems`, shared tone, voice, sound, music, stems and the reason for keeping native sound; each fully deleted story beat must choose `remove / rewrite_into_adjacent / offscreen_bridge`. Duplicate voiced lines, missed reviews, time-mapping or input/response/report drift fail closed. The script generates no audio, submits nothing to a provider and burns no credits; the approval JSON still has to be rewritten into a timed cue sheet for the actual audio tool, and paid generation needs separate confirmation. See [docs/prompts/95-final-audio-storyboard.md](docs/prompts/95-final-audio-storyboard.md).

If the final narration is assembled from multiple TTS runs, segmented recordings or sentence-by-sentence post-processing, measure the **final standalone narration track** phrase by phrase before mixing in BGM/SFX:

```bash
python3 scripts/narration_loudness_qa.py analyze work/final_narration.wav \
  --segments work/final_narration_segments.json \
  --project-dir . \
  --output verify/narration_loudness_qa.json \
  --markdown verify/narration_loudness_qa.md \
  --strict
python3 scripts/narration_loudness_qa.py verify \
  --report verify/narration_loudness_qa.json \
  --project-dir . \
  --strict
```

Defaults are a per-segment target of `-18 LUFS ±2 LU`, spread `≤1 LU` across non-exception segments, true peak `≤-2 dBTP` and LRA `≤5 LU`. Creative exceptions must carry a per-segment `reason + reviewer` and can never bypass the peak ceiling; at least two non-exception phrases must remain before spread can be judged. The report binds the narration, the segment manifest, the media contract, the algorithm and the live re-measurement. It does not judge timbre, pronunciation, breath, seams or performance, and it does not replace the whole-film `-16 LUFS` check of `audio_master_report.py` after mixing. See [docs/prompts/105-narration-loudness-qa.md](docs/prompts/105-narration-loudness-qa.md).

### Phase 6: Post-render Validation (validation after rendering)

After rendering, run the validation flow on the final video:

**6a. Machine QA (run first)**:
```bash
python3 scripts/render_qa.py final.mp4 \
  --platform douyin \
  --json final_qa.json \
  --review-dir verify/final_qa \
  --review-clips
```

`render_qa.py` checks container metadata, platform dimensions, the video/audio streams, black frames, long still stretches and long silences. For Xiaohongshu derivative files use `--platform xhs`; for Douyin/WeChat Channels use `--platform douyin` or `--platform wxch`. `--review-dir` writes `render_qa_review.json` / `.md`, and `--review-clips` extracts short MP4 clips for suspicious ranges; if you only want a quick metadata check, add `--no-filters`.

**6a00. Final audio/video track coverage (mandatory for the master and every important platform version)**:
```bash
python3 scripts/stream_coverage_qa.py analyze final.mp4 \
  --project-dir . \
  --output verify/stream_coverage_qa.json \
  --markdown verify/stream_coverage_qa.md \
  --strict
python3 scripts/stream_coverage_qa.py verify \
  --report verify/stream_coverage_qa.json \
  --project-dir . --strict
```

This gate fully decodes the final cut and, for the first video stream and the first audio stream separately, records the decoded frame count, the first-frame PTS, the last-frame end time and the timeline digest; by default it blocks audio/video head-or-tail offset or any stream not covering the container timeline. When the render plan has an exact duration/frame count, add `--expected-duration` and `--expected-video-frames`. AAC priming/discard does not use `nb_frames` as a hard gate, while the video frame count is still checked. After the report passes, play the exact delivered file in full at 1x. See [docs/prompts/114-stream-coverage-qa.md](docs/prompts/114-stream-coverage-qa.md).

**6a0. Same-timeline re-encode quality (mandatory for compressed / transcoded derivatives)**:
```bash
python3 scripts/encode_quality_qa.py analyze output/master.mp4 output/final_delivery.mp4 \
  --project-dir . \
  --output verify/encode_quality_qa.json \
  --markdown verify/encode_quality_qa.md \
  --strict
python3 scripts/encode_quality_qa.py verify \
  --report verify/encode_quality_qa.json --project-dir . --strict
```

Use it only when reference/candidate keep the same timeline and the same framing; an fps difference above `0.01`, a duration difference above 1 frame or a different display aspect ratio is rejected outright. Defaults are mean SSIM `≥0.95`, P05 SSIM `≥0.88` and mean PSNR over finite frames `≥35 dB`; when the candidate resolution differs but the aspect ratio matches, it normalizes with Lanczos and WARNs. View A/B at the worst-frame timecodes in the Markdown report and play the whole thing at 1x; metrics are never a manual quality approval. It does not measure audio, and does not apply to crop/grade/HDR/retime/interpolation; this implementation does not call `libvmaf` and makes no VMAF claim. `pipeline_manifest.py --require encode_quality_qa --strict` can make it a publish gate. See [docs/prompts/107-encode-quality-qa.md](docs/prompts/107-encode-quality-qa.md).

**6a1. Audio channel integrity (mandatory for the final master and important platform versions)**:
```bash
python3 scripts/audio_channel_qa.py analyze final.mp4 \
  --project-dir . \
  --output verify/audio_channel_qa.json \
  --markdown verify/audio_channel_qa.md \
  --strict

python3 scripts/audio_channel_qa.py verify \
  --report verify/audio_channel_qa.json \
  --project-dir . \
  --strict
```

`audio_channel_qa.py` uses local FFmpeg with fixed windows to measure left/right channel activity, onset offset, energy balance, phase correlation and mono fold-down energy loss. Mono passes the channel-specific part directly; stereo is measured live; multichannel requires a clearly defined downmix first. It does not change the audio, and it does not replace a full 1x stereo/mono listening pass, the loudness/peak check of `audio_master_report.py` or a sync review. See [docs/prompts/106-audio-channel-qa.md](docs/prompts/106-audio-channel-qa.md).

**6a2. Short audio dropouts (final standalone voice track first)**:
```bash
python3 scripts/audio_dropout_qa.py analyze work/final_narration.wav \
  --project-dir . \
  --evidence-dir verify/audio_dropout_clips \
  --output verify/audio_dropout_qa_scan.json \
  --markdown verify/audio_dropout_qa_scan.md \
  --response-template verify/audio_dropout_qa_response.json \
  --strict
```

By default it locates `40–400 ms` near-silences between active audio using a 20 ms window. When candidates appear, play the input and every WAV in full at 1x, fill in `dropout | intentional_pause | uncertain`, then run `audit` and `verify`. Confirmed dropouts and uncertain items keep blocking; intentional pauses keep a warning. Prefer the final standalone dialogue/narration stem, since BGM/SFX can mask dropouts. It does not recognize speech, does not repair audio, and does not replace `audio_master_report.py`. See [docs/prompts/113-audio-dropout-qa.md](docs/prompts/113-audio-dropout-qa.md).

### Caption / Speech QA (aligning subtitles with independent voice activity)

```bash
python3 scripts/caption_speech_qa.py analyze work/final_narration.wav \
  --subtitle-pack output/subtitles/final.json \
  --project-dir . \
  --output verify/caption_speech_qa.json \
  --markdown verify/caption_speech_qa.md \
  --strict
python3 scripts/caption_speech_qa.py verify \
  --report verify/caption_speech_qa.json \
  --project-dir . --strict
```

Hand this script only the final independent dialogue/narration track, or a speech-dominant track that explicitly has no BGM/SFX. It uses local FFmpeg `silencedetect` to invert silence intervals into audio-activity intervals, then checks every `subtitle_pack.v1` cue for being entirely inactive, for a valid-activity ratio that is too low, for excessive head/tail silence, for excessive internal pauses, or for falling outside the audio track timeline. The report binds the voice track, the subtitle JSON, the media contract, the parameters, the full measurements and a canonical id; `pipeline_manifest.py --require caption_speech_qa --strict` can be set as a publish gate. Amplitude activity is not speech recognition / forced alignment; BGM/SFX will cause false passes, and ready still cannot replace a full 1× subtitle review on the final mix. See [docs/prompts/109-caption-speech-qa.md](docs/prompts/109-caption-speech-qa.md).

### Subtitle Glyph QA (recommended for rare characters / emoji / multilingual subtitles)

```bash
python3 scripts/subtitle_glyph_qa.py analyze \
  --subtitle-pack output/subtitles/final.json \
  --font 'fonts/NotoSansSC[wght].ttf' \
  --project-dir . \
  --output verify/subtitle_glyph_qa.json \
  --markdown verify/subtitle_glyph_qa.md \
  --strict
python3 scripts/subtitle_glyph_qa.py verify \
  --report verify/subtitle_glyph_qa.json \
  --project-dir . --strict
```

The script reads the Unicode `cmap` of TTF/OTF/TTC/OTC directly, covers every visible character in `subtitle_pack.v1`, and binds the subtitles, the primary font, the explicit fallbacks, the coverage assignment and a canonical report id together. Implicit system font substitution does not count as evidence; missing glyphs block, explicit fallbacks warn by default, and `--require-primary` can forbid fallback. It does not prove shaping, color emoji, ASS layout or visual readability, so the final sign-off still requires a full 1× look at the subtitles plus full-resolution frame pulls. See [docs/prompts/110-subtitle-glyph-qa.md](docs/prompts/110-subtitle-glyph-qa.md).

### Subtitle Render Review (mandatory for any final cut that has burned-in subtitles)

```bash
python3 scripts/subtitle_render_review.py prepare \
  --project-dir . \
  --video output/final.mp4 \
  --subtitle-pack output/subtitles/final.json \
  --proof-dir verify/subtitle_render \
  --output work/subtitle_render_review_request.json \
  --markdown work/subtitle_render_review_request.md \
  --response-template work/subtitle_render_review_response.json

# After watching final completely at 1× and filling in the response cue by cue:
python3 scripts/subtitle_render_review.py audit \
  --request work/subtitle_render_review_request.json \
  --response work/subtitle_render_review_response.json \
  --output work/subtitle_render_review.json \
  --markdown work/subtitle_render_review.md --strict
python3 scripts/subtitle_render_review.py verify \
  --report work/subtitle_render_review.json --project-dir . --strict
```

The script picks the first and last, longest, highest-CPS, shortest, mid-point and evenly distributed cues from the exact final, and exports 1× context clips with audio plus full-size JPEGs of the subtitle midpoint. Passing requires playing the whole film completely, and every sample must be `visible / matches / readable / clear / repair_action=none`; missing, stale copy, unreadable, cropped, occluded or unobservable all fail closed. The report binds the final, the subtitle pack, the proof bytes, the media contract, the sampling settings, the human response and a canonical id; redo it after any re-render or evidence drift. It does no OCR and no automatic readability scoring, and the default sampling does not replace a cue-by-cue full review. See [docs/prompts/111-subtitle-render-review.md](docs/prompts/111-subtitle-render-review.md).

**6a2. Flicker / photosensitivity risk heuristic pre-check (mandatory for the final master and every important platform version)**:
```bash
python3 scripts/flash_safety_qa.py analyze final.mp4 \
  --project-dir . \
  --output verify/flash_safety_qa.json \
  --markdown verify/flash_safety_qa.md \
  --strict

python3 scripts/flash_safety_qa.py verify \
  --report verify/flash_safety_qa.json \
  --project-dir . \
  --strict
```

`flash_safety_qa.py` samples frames at low resolution with local FFmpeg, separately records luminance and saturated red-opposite transitions covering at least 25% of the frame, and pairs them into flashes; more than 3 flashes in a rolling 1 second, or at least 10 in a rolling 5 seconds, blocks. On a hit, play the Markdown time range at normal speed, prefer removing repeated white/red flashes, reducing contrast/red saturation, shrinking the flashing area or lowering the frequency, then re-render from the timeline. The report binds the source SHA-256, the media contract, the parameters, the algorithm contract and on-site recomputation evidence; `pipeline_manifest.py --require flash_safety_qa --strict` can be set as a publish gate. It is only a heuristic screen: it does not detect harmful spatial patterns, and it is not medical advice, legal compliance evidence, or WCAG/Harding/broadcast certification; high-risk or regulated deliveries must escalate to a recognized professional photosensitivity analyzer. See [docs/prompts/104-flash-safety-qa.md](docs/prompts/104-flash-safety-qa.md).

**6a3. Single-frame / few-frame transient artifacts (recommended for the final master and generated assets)**:
```bash
python3 scripts/temporal_artifact_qa.py analyze final.mp4 \
  --project-dir . \
  --evidence-dir verify/temporal_artifact_frames \
  --output verify/temporal_artifact_qa.json \
  --markdown verify/temporal_artifact_qa.md \
  --response-template work/temporal_artifact_response.json \
  --strict
```

The script looks for local spikes where 1–3 sampled frames briefly deviate and then return to approximately the original picture; an ordinary hard cut should not satisfy the two-sided abrupt-change-plus-recovery similarity contract. On a candidate hit you must play the full 1×, then describe each of `before / suspect / after`, fill in `intentional_edit|artifact|uncertain` and run `audit`. The latter two decisions stay blocking; drift in the source, algorithm, analysis, JPEGs, response or derived state invalidates the live gate. It does not recognize faces, identity, physics or semantics, and it can miss sustained drift and small-area problems. See [docs/prompts/108-temporal-artifact-qa.md](docs/prompts/108-temporal-artifact-qa.md).

**6b. Shot color / exposure QA (recommended for multicam, B-roll, generated assets, or after grading)**:
```bash
python3 scripts/shot_color_qa.py final.mp4 \
  --output verify/shot_color_qa.json \
  --markdown verify/shot_color_qa.md \
  --strict
```

`shot_color_qa.py` uses FFmpeg `signalstats` on the final encoded file to aggregate luminance, contrast, U/V, saturation and broadcast-range metrics per shot, and compares the luminance/chroma difference between adjacent shots. Visual jumps only WARN by default and must be checked against the Markdown timecodes and the master in `timeline_view.py`; sustained out-of-range values on non-full-range output, or a shot with no samples, block by default. You can reuse already-reviewed scenes with `--scene-boundaries work/scene_boundaries.json`, or let the script detect them automatically. It is not calibrated scopes, HDR proof, white-balance/skin-tone judgement or an aesthetic score. `pipeline_manifest.py --require shot_color_qa --strict` can make the report a publish requirement.

**6c. Retention rhythm risk audit (recommended for short-video masters / platform exports)**:
```bash
python3 scripts/subtitle_pack.py \
  --config render_config.json \
  --output-dir output/subtitles \
  --basename final_master \
  --speed 1.25 \
  --offset 2.0

python3 scripts/retention_rhythm_qa.py final.mp4 \
  --timed-text output/subtitles/final_master.json \
  --output verify/retention_rhythm_qa.json \
  --markdown verify/retention_rhythm_qa.md \
  --strict
```

`retention_rhythm_qa.py` reuses FFmpeg scene scores on the **already rendered final cut** and can merge the output-aligned subtitle JSON to check first-three-seconds scene/subtitle activity, long visual holds, scene + subtitle attention gaps, mechanically even pacing, over-dense fast cuts and subtitles that go unrenewed for too long. Without timed text, an inactive hook only warns, to avoid mistaking continuous camera movement or slight motion for a hard failure; high-confidence severe long holds / attention gaps are written into `summary.blocking`. This is observable rhythm risk, not a platform retention rate or a viral-hit prediction. On a hit, first check the Markdown time ranges and the master, then go back to the source timeline and re-render. `pipeline_manifest.py --require retention_rhythm_qa --strict` can make the report a publish requirement.

**6c.3. Hook-body bridge check (recommended for short-video masters; "abrupt opening" is an observed rejection cause)**:

```bash
python3 scripts/hook_bridge_check.py output/final.mp4 \
  --hook-end 3.5 --body-start 8.2 \
  --first-line "the first spoken line of the body" \
  --claim work/hook_claim.json \
  --out verify/hook_bridge.json --markdown verify/hook_bridge.md --strict
```

At least one of the three bridges must pass, otherwise the hook and the body are not connected:
- **Visual bridge**: perceptual hash distance between the hook's last frame and the body's first frame ≤ 12/64. **Do not use color histograms as the gate** — measured once, a sunset beach and an indoor interview had a histogram correlation of 0.998 (judged as the same picture), while the perceptual hash distance of the same pair was 46/64.
- **Audio bridge**: level jump at the cut point ≤ 5dB or a clear fade-out present; > 8dB with no fade-out is a fail.
- **Semantic bridge**: the human states "what the hook promises / which body line delivers it" (`--claim`); the script cannot test this, but an undeclared claim blocks.

Also check two high-frequency mistakes: **audio cut point separated from the visual cut point by > 1 second** (viewers feel the opening is truncated; a 4.7-second gap was measured once); and opening phrasing that hits begging style ("don't scroll away yet") / meta-talk ("next I'll tell you") / empty promises ("this is mind-blowing") blocks immediately.

It only reports observable continuity and does not judge creative quality — `retention_rhythm_qa.py` measures rhythm but cannot measure "whether the picture relates to the content" (it passes as long as the hook is moving); run both together. See [docs/prompts/129-hook-bridge.md](docs/prompts/129-hook-bridge.md).

**Delivering the on-screen visual for every spoken line (mandatory for explainer / tutorial videos)**: for concrete nouns, numbers, error messages and interfaces that appear in the talking-head voiceover, the picture must **actually display them**, pushed in front of the viewer with a continuous push-in — **"a visual was added" is not the same as "the content was delivered"**. Companion criteria and craft: the footage resolution floor (the long side of the face being driven ≥ 1000px, the face area ≥ 400px), the two platform face-compliance routes (real people go through authorization / a virtual face may only use trusted **text-to-image** output), rebuild the timeline from **real segment durations**, dedupe the audio track by **audio segment**, **do not scale static frames** to prevent jitter (if scaling is required, supersample 2× first), and **do not burn subtitles into cards or zoomed shots**. See [docs/prompts/130-keyword-to-screen.md](docs/prompts/130-keyword-to-screen.md).

**Shot language and sound (explainer videos must not be just "hard cuts + push-ins")**: first fix the **shot size** (which layer of information must the viewer see clearly in this sentence) → then decide **whether to move** (if the information density is sufficient, don't move) → if you do move, choose the **movement** (push = focus / pull = reveal / pan = compare / follow = follow), one shot does one thing; sound has three layers (voice -16 LUFS / BGM 12–18dB lower / SFX land on the cut frame). The checklist is in [docs/prompts/131-camera-language.md](docs/prompts/131-camera-language.md).

**6c.1. Reference-video edit-rhythm quantification (run when the user explicitly provides a reference cut)**:
```bash
python3 scripts/reference_edit_rhythm.py analyze \
  --project-dir . \
  --reference origin/reference-ad.mp4 \
  --candidate final.mp4 \
  --evidence-dir verify/reference_edit_rhythm \
  --output work/reference_edit_rhythm.json \
  --markdown work/reference_edit_rhythm.md \
  --strict

python3 scripts/reference_edit_rhythm.py verify \
  --report work/reference_edit_rhythm.json \
  --strict
```

`reference_edit_rhythm.py` runs the same hard scene detection on the reference and the candidate, binds both videos and both contact sheets by SHA-256/media contract, and compares cuts/minute, median shot, final-hold ratio, normalized cut positions and opening/middle/closing cut shares. Deviations only WARN by default; add `--require-match` only when structural matching is an explicit acceptance condition. Watch both videos completely and borrow structure only — do not copy the reference's picture, audio, brand or story. Scene scores miss some dissolves and in-shot movement, and a contact sheet does not replace 1× playback. See [docs/prompts/92-reference-edit-rhythm.md](docs/prompts/92-reference-edit-rhythm.md).

**6c.2. Final-cut lip-sync review (digital humans / generated talking heads / lip-sync close-ups)**:
```bash
python3 scripts/lip_sync_review.py prepare \
  --project-dir . \
  --video final.mp4 \
  --segment hook=2.40:6.10 \
  --anchor hook="state the brand selling point clearly" \
  --proof-dir verify/lip_sync \
  --output work/lip_sync_review_request.json \
  --markdown work/lip_sync_review_request.md \
  --response-template work/lip_sync_review_response.json

# After watching the 1× clips with sound and the 0.25× silent proofs, and filling in the response:
python3 scripts/lip_sync_review.py audit \
  --request work/lip_sync_review_request.json \
  --response work/lip_sync_review_response.json \
  --output work/lip_sync_review.json \
  --markdown work/lip_sync_review.md \
  --strict
python3 scripts/lip_sync_review.py verify --report work/lip_sync_review.json --strict
```

The phrase must come from the final master and be fully visible, 1–10 seconds long, and preferably contain p/b/m-type closed-lip anchors. `pass` requires all of plosive closed lips, vowel timing, mouth movement while speaking, a visible speaker and audio quality to pass; `not_observable` fails closed. The script only binds the human-reviewed master/proof bytes and does no face or automatic phoneme recognition. Any cut, speed change, audio swap, re-encode or evidence drift means running `prepare → audit` again. See [docs/prompts/93-lip-sync-review.md](docs/prompts/93-lip-sync-review.md).

**6d. Full review proxy (run when you need to share the whole video or give precise timecode feedback)**:
```bash
python3 scripts/review_proxy.py final.mp4 \
  --output verify/final_review_proxy.mp4 \
  --manifest verify/final_review_proxy.json \
  --markdown verify/final_review_proxy.md
```

`review_proxy.py` does not modify the master; it outputs a lightweight MP4 of at most 720p, 24fps, H.264/AAC, `+faststart`, and burns `REVIEW PROXY` plus an elapsed timecode into the top-left corner. Review notes should cite the visible timecode, and final QA and publishing still use the master / platform export. `--dry-run` writes only the reproducible command and artifacts, and `--no-timecode` turns the timecode off.

**6e. Visual retrospective (run when QA has WARN/FAIL or when spot-checking cut points)**:
```bash
python3 scripts/timeline_view.py final.mp4 --at 42.5 --radius 1.5 --output verify/42_5s.png
python3 scripts/timeline_view.py origin/talking.mp4 --cut-list work/jumpcut.json --output-dir verify/cuts --limit 12
python3 scripts/timeline_view.py output/rough_cut.mp4 --rendered-cut-list work/rough_cut.json --output-speed 1.25 --output-offset 1.0 --output-dir verify/rendered_cuts --json verify/rendered_cuts.json
```

`timeline_view.py` generates filmstrip + waveform PNGs; the upper half shows picture continuity, the lower half shows voice/silence boundaries. `--cut-list` inspects the source footage timeline, while `--rendered-cut-list` maps the accumulated `keep_segments` durations to the actual splice points in the final cut; when there is a global speed change or an opening title card, pass `--output-speed` / `--output-offset` as well. The JSON keeps the output cut points and the source ranges before and after them; a video without audio automatically produces a filmstrip only.

**6f. Source-time vs final-cut comparison (run when reviewing a structural edit)**:
```bash
python3 scripts/edit_compare.py \
  origin/talking.mp4 final.mp4 \
  --cut-list work/rough_cut.json \
  --output-speed 1.25 \
  --output-offset 2.0 \
  --output verify/source_vs_final.mp4 \
  --report verify/edit_compare.json \
  --markdown verify/edit_compare.md
```

The left column plays the source continuously; the right column projects the final delivery pixels back onto the same source clock, showing black where ranges were deleted. The script checks duration, the size of both columns, the source-clock audio track, black frames for deleted segments and representative kept-segment pixels. V1 only supports a single source, time-ascending, non-overlapping `keep_segments` plus a global speed/offset; reordering, multiple sources or nonlinear speed changes must go back to an NLE/OTIO timeline review. See [docs/prompts/74-edit-compare.md](docs/prompts/74-edit-compare.md) for details.

**6g. Subtitle sidecar delivery (run when the platform needs SRT/VTT)**:
```bash
python3 scripts/subtitle_pack.py \
  --config render_config.json \
  --output-dir output/subtitles \
  --basename final_master \
  --speed 1.25 \
  --offset 2.0 \
  --formats srt vtt ass json
```

`subtitle_pack.py` can generate SRT/VTT/ASS/JSON from `transcript.json` or `render_config.json`. `--config` concatenates the subtitle timeline in final clip order; `--speed` aligns with `render_final.py --primary-speed`; `--offset` aligns with the seconds of the opening title card. The JSON manifest keeps each cue's source segment and proofreading warnings such as `over_max_chars`.

When the player needs toggleable subtitles, run `soft_subtitles.py mux final.mp4 output/subtitles/final_master.srt --language zho --output output/final-soft.mp4 --receipt verify/soft_subtitles.json` with the reviewed SRT that is aligned to the final MP4, then run `soft_subtitles.py verify verify/soft_subtitles.json`. The script accepts only a source with a single video, at most one audio track and no other tracks; it copies the audio and video streams, adds one `mov_text` subtitle track, and checks the SRT timings, the extracted subtitle content, the source/final stream hashes, full decoding and file drift. Even after `ready_for_human_review` you must still watch it at 1× with sound in the target player and confirm subtitle toggling, Chinese rendering and audio-visual sync. This command does not remove subtitles already burned into the picture, so confirm before delivery whether double subtitles will appear.

After the JSON is generated, run the read-only publish gate on the final subtitles:

```bash
python3 scripts/subtitle_readability_qa.py \
  output/subtitles/final_master.json \
  --media final.mp4 \
  --output verify/subtitle_readability_qa.json \
  --markdown verify/subtitle_readability_qa.md \
  --strict
```

`subtitle_readability_qa.py` checks output-timeline invalid timings, ordering, overlaps, extremely short flashes, CPS, duration, line count/line length, and can use FFprobe to verify that no cue extends past the end of the final cut. Ordinary CPS/typesetting risk only WARNs and must be checked against the master at normal speed; deterministic timing accidents and extreme reading speeds are written into `summary.blocking`. It does no OCR and does not replace human review of font, outline, position or occlusion. `pipeline_manifest.py --require subtitle_readability_qa --strict` can make the report a publish requirement.

**6g.1 Platform framing preview (run when the master's aspect ratio differs from the target, or the picture contains protected information)**:

```bash
python3 scripts/framing_preview.py create \
  --project-dir . --video output/master.mp4 \
  --platforms xhs douyin wxch \
  --preview-dir verify/framing \
  --output work/framing_preview.json \
  --markdown work/framing_preview.md \
  --require-selection

python3 scripts/framing_preview.py select \
  --report work/framing_preview.json --platform xhs --strategy contain
python3 scripts/multi_export.py output/master.mp4 \
  --output-dir output --platforms xhs douyin wxch \
  --framing-preview work/framing_preview.json
```

Look at the real early/middle/late JPEGs for each platform. `cover` must not crop off faces, hands, multi-person relationships, readable UI, logos, product edges or document boundaries; when there is risk, choose `contain` or `blur`. Source/preview bytes, display orientation, platform canvas, filter digest or selection drift all block before encoding. Sampled frames do not replace a full 1× review of each platform export; see [docs/prompts/103-framing-preview.md](docs/prompts/103-framing-preview.md).

**6h. Delivery upload package (run before the final upload)**:
```bash
python3 scripts/publish_package.py \
  --project-dir work/day58 \
  --platforms xhs douyin wxch \
  --output work/day58/publish_package.json \
  --markdown work/day58/publish_package.md \
  --strict
```

`publish_package.py` does not upload and does not call platform APIs; it only combines the platform MP4s from `multi_export.py`, the title/body/tags from `generate_caption.py`, the cover, the SRT/VTT, the chapter text and the `pipeline_manifest` gate status into `publish_package.v1`. If `cover_variants.json` exists and the `selected_cover` file is valid, the reviewed cover is preferred; an explicit `--cover` overrides it. If a platform video is missing, a caption is empty, or the pipeline manifest is already blocked, `--strict` returns 2. When handing off to an external publishing connector, use this JSON as the handoff; for manual uploads, follow the Markdown checklist.

**6i. Final approval receipt (run after client/human confirmation)**:
```bash
python3 scripts/approval_receipt.py create \
  --project-dir work/day58 \
  --artifact output/day58_xhs.mp4 \
  --artifact output/day58_douyin.mp4 \
  --artifact output/day58_wxch.mp4 \
  --artifact output/cover.png \
  --artifact output/day58_caption.json \
  --artifact verify/render_qa.json \
  --approved-by "reviewer label" \
  --output verify/approval_receipt.json \
  --markdown verify/approval_receipt.md

python3 scripts/publish_package.py \
  --project-dir work/day58 \
  --require-approval-receipt \
  --strict
```

`approval_receipt.py` binds only the stable delivery artifacts you list explicitly; do not put the pipeline manifest, the publish package or the dashboard — anything rewritten on every run — into the receipt. `verify --strict` and `pipeline_manifest.py --require approval_receipt --strict` re-read the current files and compare SHA-256; any re-render, cover swap, copy change, subtitle removal or path turning into a symlink expires the old approval. `approved_by` is only a local label, not authentication or a digital signature. See [docs/prompts/77-approval-receipt.md](docs/prompts/77-approval-receipt.md) for details.

**6j. Resume context package (run when wrapping up across sessions or in automation)**:
```bash
python3 scripts/project_resume.py \
  --project-dir work/day58 \
  --target-stage publish_ready \
  --output work/day58/project_resume.json \
  --markdown work/day58/project_resume.md \
  --agent-note work/day58/CLAUDE.md \
  --strict
```

`project_resume.py` reuses the local gates of `pipeline_manifest.py`; it does not render, upload or submit generation jobs. It outputs `project_resume.v1` containing `phase`, `recommended_first_action`, `next_actions[]`, the most recent artifacts, a key gate snapshot and a one-sentence `suggested_prompt` that can be handed straight to the next agent. When a long pipeline gets its context compacted, when automation ends, or when you hand over to another agent, prefer the Markdown/agent note as the entry point.

**6k. Human review dashboard (run before final confirmation / handoff)**:
```bash
python3 scripts/review_dashboard.py \
  --project-dir work/day58 \
  --target-stage publish_ready \
  --output work/day58/review_dashboard.json \
  --html work/day58/review_dashboard.html \
  --strict
```

`review_dashboard.py` reuses the local gates of `pipeline_manifest.py` and outputs `review_dashboard.v1` plus an HTML file that opens directly in a browser. It puts blocking/missing/warning gates into `review_items[]`, and also lists `next_actions[]`, the latest artifacts and the full gate snapshot. It suits the user's final confirmation and also serves as the handoff to the next agent when automation ends.

**6l. Final-cut re-speak / stutter gate**:
```bash
python3 scripts/extract_audio.py output/final.mp4
python3 scripts/transcribe.py output/final_audio.wav --model auto --language zh --word-timestamps
python3 scripts/speech_continuity_qa.py output/final_transcript.json \
  --output verify/speech_continuity_qa.json \
  --markdown verify/speech_continuity_qa.md \
  --strict
```

You must transcribe the **rendered master** again; do not reuse the source footage transcript. `speech_continuity_qa.py` checks exact re-speak at the end/start of adjacent segments, adjacent near-duplicate takes and mid-sentence immediate stutters; different speakers are not compared against each other by default. On a hit, audition the master at the timecode first, then adjust the source `render_config` / cut list, re-render and re-run. `pipeline_manifest.py --require speech_continuity_qa --strict` can make this report a publish requirement.

**6m. PQ/HLG HDR → Rec.709 SDR delivery (run when an HDR master targets ordinary social media / the web)**:
```bash
python3 scripts/hdr_sdr.py plan output/final_hdr.mp4 \
  --delivery output/final_sdr.mp4 \
  --output work/hdr_sdr_plan.json \
  --markdown work/hdr_sdr_plan.md
python3 scripts/hdr_sdr.py apply work/hdr_sdr_plan.json
python3 scripts/hdr_sdr.py verify work/hdr_sdr_plan.json
```

`hdr_sdr.py` accepts only `smpte2084` PQ or `arib-std-b67` HLG with explicit metadata, and requires BT.2020 primaries/matrix; unknown or contradictory color tags fail closed. A correct conversion must have both FFmpeg `zscale` + `tonemap`; a bare tonemap or a degenerate fallback that only drops to `yuv420p` is not allowed. apply uses linear-light float + Hable to produce an H.264/AAC, `yuv420p`, BT.709 limited-range MP4, explicitly validates the four color tags, duration/size/fps/audio track, hash and full-length decode, and only then promotes it atomically. Dolby Vision/HDR10+ dynamic metadata is not preserved; you must fully review skin tones, highlights, shadows, gradients and saturated colors on a trusted SDR display, then re-run QA and approval on the SDR file. See [docs/prompts/87-hdr-sdr.md](docs/prompts/87-hdr-sdr.md).

**6n. Target-size delivery encode (run when there is a hard MB cap)**:
```bash
python3 scripts/delivery_encode.py plan output/final.mp4 \
  --delivery output/final_delivery.mp4 \
  --max-size-mib 18 \
  --output work/delivery_encode_plan.json \
  --markdown work/delivery_encode_plan.md
python3 scripts/delivery_encode.py apply work/delivery_encode_plan.json \
  --markdown work/delivery_encode_plan.md
python3 scripts/delivery_encode.py verify work/delivery_encode_plan.json
```

`delivery_encode.py` binds the master SHA-256 and computes the H.264/AAC bitrate with two-pass `libx264` within a 6% container margin; it blocks if the output exceeds the hard cap, if container/stream/size/fps/duration/audio do not match, or if the full FFmpeg decode fails. apply first writes only a temporary MP4 in the same directory and promotes it atomically only after technical validation, never overwriting the source file. A complete decode is not a picture-quality approval; if composition/timeline are unchanged, run `encode_quality_qa.py` right after, then watch the delivery version at normal speed, run `render_qa.py` and put the final SHA-256 into `approval_receipt.py`. See [docs/prompts/85-delivery-encode.md](docs/prompts/85-delivery-encode.md) and [docs/prompts/107-encode-quality-qa.md](docs/prompts/107-encode-quality-qa.md).

**6o. Final subtitle text check**:
1. Read the text of every transcript segment used by the final video.
2. In the final video's segment order, check each one for the following problems:
   - **Residual speech-recognition errors**: homophone and proper-noun mistakes that Phase 2.5 may have missed
   - **Uncleaned slips of the tongue**: whether the speaker's slips (saying the opposite, repeating) are still in the final video
   - **Contextual continuity**: whether semantic breaks or logical jumps appear between adjacent segments after stitching across videos
   - **Subtitle consistency**: whether the same word/name is spelled the same way in different segments
3. If problems are found, list them and show the list to the user:
   ```
   Subtitle check result:
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     #  | Issue type        | Original → Suggested fix
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      3  | ASR error         | "Primiere" → "Premiere"
      7  | Slip of tongue    | "said the result first" → suggest deleting this segment
     12  | Name inconsistency| "opencloud" → unify as "OpenClaw"
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   ```
4. After the user confirms the fixes, edit the corresponding transcript.json and then re-run the Phase 5 render.

## Important Notes

### Video Quality and Encoding Rules (Most Important)

1. **Single-encode rule**: from the raw video to the final output, **only one encode is allowed**. Repeated re-encoding is strictly forbidden (for example split and encode, then burn subtitles and encode, then add the cover and encode) — every re-encode accumulates quality loss. Use the `render_final.py` `select/aselect` + `between()` approach (single video) or the `trim/atrim` + `concat` approach (multiple videos) to do all of the trimming, concatenation, subtitling and cover work in one ffmpeg command.
2. **Speed-changed versions are also encoded directly from the raw video**: the `--speed 1.25 1.5` versions integrate `setpts` + `atempo` in `filter_complex` and go straight from the raw video in one step — do **not** compress the already-encoded 1x video a second time.
3. **Encoding parameters**: use a fixed bitrate (for example `-b:v 12M`) rather than a quality parameter (for example `-q:v`). A fixed bitrate gives precise control over file size and quality and avoids `-q:v` behaving inconsistently across encoders. Set it by reference to the source video's bitrate (usually 8-15 Mbps).
4. **Legacy pipeline scripts are for preview only**: `split_video.py`, `burn_subtitles.py`, `merge_clips.py`, `generate_cover.py` and `add_chapter_bar.py` can still be used on their own, but **the final output must use `render_final.py`**. The legacy scripts are fine for quickly previewing the look of a single segment.

### Rotation Detection

5. **Rotation detection**: portrait video recorded on devices such as an iPhone may be encoded at 1920x1080 with rotation=-90 metadata. All scripts detect rotation through `utils.get_video_info()` and swap width and height automatically, so the correct display size is obtained. Rotation must be checked first whenever video information is read.

### Chapter Timeline

6. **Number of chapters**: no more than 4 chapters is recommended, with chapter names of 2-4 characters.
7. **The timeline is not burned into the video**: after rendering, output the chapter timeline as text (including the cover offset) so the user can paste it manually into the video description on Xiaohongshu and similar platforms.

### Other

8. **Handling multiple videos**: if the user provides several videos, run Phase 1-2.5 independently for each one, then in Phase 3 show the segment list for all videos together and support mixing segments across videos.
9. **Transcription model choice**: for Chinese-language video use the `large` model; `base`/`small` have low Chinese recognition accuracy. The `large` model needs about 2.9GB of download space.
10. **Working directory**: all intermediate files (audio, transcripts) are stored in the directory containing the video file, which keeps them easy to manage. Temporary files (the ASS subtitle file, the filter_complex script) should be cleaned up after rendering.
11. **Error handling**: if a step fails, report the specific error message to the user and suggest possible solutions.
12. **Subtitle font**: the default is LXGW WenKai Medium, downloaded and cached automatically. ffmpeg must be compiled with `libass` and `libfreetype`. On macOS this is available via `brew install ffmpeg`.
13. **Portrait fit**: subtitle position and font size are already optimised for 9:16 portrait video (Xiaohongshu, Douyin and similar). The default font size is 120 (= screen width 1080 ÷ 9), so each Chinese character takes about 1/9 of the portrait width. Landscape video is supported too, and the font size scales automatically with the actual resolution.

## Remotion Voiceover Workflow

When the user has only a voice track (or an audio file) and no visuals, use Remotion to generate visuals that go with the voice.

See [REMOTION_VOICEOVER.md](./REMOTION_VOICEOVER.md) for the detailed Remotion API reference, template styles and component structure.

### When to Use

- **Audio-only voiceover** — you have a recording or TTS voice and need matching visuals
- **Audio + still images** — you have both voice and image assets and need them combined into a moving video
- **Podcast visualisation** — turn podcast or conversation audio into a video with subtitles and visual effects
- **Explainer video** — a combination of voiceover, text animation and background images

### Remotion Workflow

1. **Audio preparation** — extract audio with `extract_audio.py`, or use the audio file the user supplies directly
2. **Transcription** — use `transcribe.py` to produce transcript.json (per-sentence timestamps)
3. **Timeline generation** — the AI agent analyses the semantics of the transcript and generates `timeline.json`:
   - Group multiple segments into scenes by meaning
   - Choose a type for each scene (title/content/kinetic/quote and so on)
   - Choose the background visuals, text animations and transitions
   - Configure the subtitle style (TikTok word-by-word highlight / bottom subtitles / full-screen text)
4. **Asset preparation** — collect or generate the images each scene needs
5. **Remotion render** — render the final video from timeline.json with `npx remotion render`
6. **Post-processing (optional)** — merge with other video segments using `render_final.py`

### Video Template Style Selection

| Style | Use case | Visual effect |
|------|---------|---------|
| `tiktok` | Short voiceover video, knowledge sharing | Gradient background + keyword cards + TikTok-style word-by-word subtitles + progress bar |
| `tutorial` | Tutorials, popular science, product intros | Split image/text columns + Ken Burns images + bullet points fading in line by line + bottom subtitles |
| `kinetic` | Emotional, motivational, copy-led pieces | Full-screen large type springing in line by line + elastic motion effects |
| `podcast` | Podcasts, interviews, conversations | Avatar + audio waveform visualisation + quote text |
| `news` | News briefing, industry updates | Top banner + lower third info bar + rolling subtitles |
| `slideshow` | Product intros, travel, photo albums | Multi-image Ken Burns + transitions + caption text |

### Remotion Environment Requirements

```bash
# Requires Node.js 18+ and npm
node --version  # >= 18.0.0
npm --version

# Install the dependencies of the Remotion standup project
cd remotion-standup
npm install
```

### Standup Comedy / Audio-Only Video Generation

When the user has a standup comedy audio track (or any audio-only content) and no visuals, you can generate a video with text animation:

**Step 1: Transcription**
```bash
python3 scripts/transcribe.py audio.wav --model auto --language zh
```

**Step 2: Generate the timeline**
```bash
# Default style
python3 scripts/generate_standup_timeline.py transcript.json \
    --audio audio/standup.wav \
    --output remotion-standup/public/timeline.json

# Energetic style (more exaggerated motion effects)
python3 scripts/generate_standup_timeline.py transcript.json \
    --audio audio/standup.wav \
    --style energetic \
    --output remotion-standup/public/timeline.json

# Select specific segments + custom font
python3 scripts/generate_standup_timeline.py transcript.json \
    --audio audio/standup.wav \
    --segments 1-10,12,15-20 \
    --font "LXGW WenKai, sans-serif" \
    --output remotion-standup/public/timeline.json
```

**Step 3: Preview and render**
```bash
cd remotion-standup

# Copy the audio file into the public directory
cp /path/to/audio.wav public/audio/standup.wav

# Development preview
npx remotion studio

# Render the final video
npx remotion render StandupVideo out/standup.mp4 --codec=h264 --crf=18
```

**The script automatically:**
- Detects punchlines / short sentences (exclamation marks, keywords, a short sentence contrasted with a long one) → uses exaggerated motion such as slam/shake/bounce
- Enlarges the font size of short sentences (emphasis 1.3-1.6x) and shrinks long sentences to avoid overflow
- Uses a radial gradient plus loud colours (red/orange/gold/green) for punchlines
- Uses a dark gradient background with a different animation loop for ordinary sentences
- Inserts line breaks in the middle of long Chinese sentences automatically

**12 text animation effects:**

| Animation | Effect | Best for |
|------|------|---------|
| `fadeIn` | Fade in | Steady narration |
| `springIn` | Spring in | Normal conversation |
| `scaleUp` | Scale up from small | Emphasis |
| `scaleDown` | Scale down from large | Impact |
| `typewriter` | Typewriter | Quotes/conversation |
| `bounce` | Bounce | Lively/funny |
| `shake` | Shake | Punchlines/roasting |
| `slam` | Fast slam-in | Heavy punchline |
| `wave` | Per-character wave | Opening/ending |
| `glitch` | Glitch flicker | Surprise/twist |
| `rotateIn` | Rotate in | Topic switch |
| `splitReveal` | Split open from the middle | Reveal/reveal-out |

**3 style presets:**

| Style | Description |
|------|------|
| `default` | Balanced mix of all animations, suits most content |
| `calm` | Only gentle animations (fadeIn/springIn/typewriter), suits storytelling/deep content |
| `energetic` | Leans on exaggerated animations (slam/shake/bounce/glitch), suits standup/funny content |

### timeline.json Configuration Format

```json
{
  "fps": 30,
  "width": 1080,
  "height": 1920,
  "audioSrc": "public/audio/voiceover.wav",
  "style": "tiktok",
  "font": {
    "family": "Noto Sans SC",
    "titleWeight": "700",
    "bodyWeight": "400",
    "titleSize": 72,
    "bodySize": 48
  },
  "colors": {
    "background": "#1a1a2e",
    "text": "#ffffff",
    "highlight": "#FFD700",
    "accent": "#e94560"
  },
  "scenes": [
    {
      "id": 1,
      "startMs": 0,
      "endMs": 5000,
      "type": "title",
      "title": "Let's talk about a topic today",
      "background": { "type": "gradient", "colors": ["#667eea", "#764ba2"] },
      "transition": { "type": "fade", "durationMs": 500 }
    }
  ],
  "captions": {
    "enabled": true,
    "style": "tiktok",
    "fontSize": 56,
    "highlightColor": "#FFD700"
  }
}
```

## Font Catalog

The project ships with a downloadable catalog of free fonts covering the fonts commonly used in Chinese and English video production.

### Font Management

```bash
# Show the environment report (includes the list of cached fonts)
python3 scripts/utils.py

# Use the font API from Python
python3 -c "
from scripts.utils import list_available_fonts, download_font

# List every available font
for f in list_available_fonts():
    status = '✓' if f['cached'] else '○'
    print(f\"{status} {f['id']:25s} {f['name']:25s} {f['use_case']:8s} {f['description']}\")

# Download a specific font
download_font('lxgw-wenkai')
download_font('inter')
"
```

### Chinese Fonts

| Font ID | Name | Style | Use | License |
|---------|------|------|------|--------|
| `noto-sans-sc` | Noto Sans SC (Source Han Sans) | Universal sans | All scenarios | SIL OFL |
| `noto-serif-sc` | Noto Serif SC (Source Han Serif) | Formal serif | Culture/classical/in-depth | SIL OFL |
| `lxgw-wenkai` | LXGW WenKai | Handwritten script | Literary/cultural/emotional (~24MB) | SIL OFL |
| `lxgw-wenkai-lite` | LXGW WenKai Lite (light build) | Handwritten script | Same as above, smaller size (~13MB) | SIL OFL |
| `lxgw-wenkai-bold` | LXGW WenKai Medium | Bold handwritten script | Titles | SIL OFL |
| `zcool-kuaile` | ZCOOL KuaiLe | Rounded and playful | Light/entertainment titles | SIL OFL |
| `zcool-qingke-huangyou` | ZCOOL QingKe HuangYou | Trendy hand lettering | Fashionable/youthful titles | SIL OFL |
| `zcool-xiaowei` | ZCOOL XiaoWei | Clean and upright | Body copy/subtitles | SIL OFL |

### English Fonts

| Font ID | Name | Style | Use | License |
|---------|------|------|------|--------|
| `inter` | Inter | Modern sans-serif | All scenarios | SIL OFL |
| `montserrat` | Montserrat | Geometric sans-serif | Titles | SIL OFL |
| `poppins` | Poppins | Rounded geometric | Titles | SIL OFL |
| `roboto` | Roboto | Neutral modern | All scenarios | Apache 2.0 |
| `oswald` | Oswald | Condensed sans-serif | News/headline titles | SIL OFL |
| `playfair-display` | Playfair Display | Elegant serif | Literary/high-end titles | SIL OFL |

### Font Selection Guidelines

**Default font: LXGW WenKai Medium** — a handwritten script style that combines good looks with readability, and is the best choice for voiceover/Vlog videos. It is downloaded automatically if not cached.

**Default font size: 120** (portrait 1080px width ÷ 9 = 120), meaning each Chinese character takes about 1/9 of the screen width, which is clear and easy to read on a phone in portrait.

| Video type | Recommended Chinese font | Recommended English font |
|---------|------------|------------|
| **Voiceover/Vlog (default)** | **LXGW WenKai Medium** | Inter / Roboto |
| Tech/tutorials | Noto Sans SC | Inter / Roboto |
| News/updates | Noto Sans SC | Oswald / Montserrat |
| Culture/in-depth | LXGW WenKai / Noto Serif SC | Playfair Display |
| Entertainment/light | ZCOOL KuaiLe | Poppins |
| Fashion/trendy | ZCOOL QingKe HuangYou | Montserrat |
| Formal/business | Noto Sans SC | Inter |

### Using Fonts in FFmpeg

```bash
# After downloading the font, point to it with the --font-path argument
python3 scripts/render_final.py --config render_config.json --output final.mp4 \
  --font-path fonts/LXGWWenKai-Regular.ttf
```

### Using Fonts in Remotion

```tsx
// Approach 1: @remotion/google-fonts (recommended for English fonts)
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();

// Approach 2: @remotion/fonts (recommended for local/CJK fonts)
import { loadFont } from "@remotion/fonts";
loadFont({
  family: "LXGW WenKai",
  url: staticFile("fonts/LXGWWenKai-Regular.ttf"),
  format: "truetype",
});

// Approach 3: load CJK through @remotion/google-fonts
import { loadFont } from "@remotion/google-fonts/NotoSansSC";
const { fontFamily } = loadFont("normal", {
  weights: ["400", "700"],
  subsets: ["chinese-simplified"],  // Important: specify subsets to reduce size
});
```

### Notes

- **CJK fonts are large** (10-20MB); when using `@remotion/google-fonts` always specify `subsets: ["chinese-simplified"]` to reduce size
- **Font cache**: downloaded fonts are stored in the project's `fonts/` directory, which `.gitignore` already excludes
- **China-based users**: font downloads automatically use the jsDelivr CDN for acceleration
- **Commercial safety**: every font in the catalog is under SIL OFL or Apache 2.0, so all are free for commercial use

## FAQ / Troubleshooting

When you hit an error, run the environment diagnostics first:
```bash
python3 scripts/utils.py
```

### Q1: `No such filter: 'drawtext'` or `No such filter: 'ass'`

**Cause**: ffmpeg was built without `libfreetype` (needed by drawtext) or `libass` (needed by subtitles).

**Diagnosis**:
```bash
ffmpeg -hide_banner -filters 2>/dev/null | grep -E "drawtext|ass|subtitles"
```
No output means the corresponding filter is missing.

**Fix**:
- **macOS**: the standard `brew install ffmpeg` may not include these libraries. Install the full build from a third-party tap:
  ```bash
  brew tap homebrew-ffmpeg/ffmpeg
  brew install homebrew-ffmpeg/ffmpeg/ffmpeg --with-fdk-aac
  ```
  That tap enables `--enable-libfreetype --enable-libass --enable-libfontconfig` by default.
- **Linux/WSL**: `apt install ffmpeg` usually includes them. If not, install the development dependencies and build from source:
  ```bash
  sudo apt install libfreetype6-dev libfontconfig1-dev libass-dev
  ```
- **Impact**: without drawtext, subtitle burn-in and cover text either fail or degrade automatically.

### Q2: `Undefined constant or missing '(' in 'iw*0.5-tw/2'`

**Cause**: the `x` expression of ffmpeg drawtext uses `tw` (text width), but in some ffmpeg versions `tw` is not available in the context of the `x` parameter.

**Fix**: the scripts already fix this (using the pixel value `{pixel_x}-text_w/2` instead of `iw*{frac}-tw/2`). If you modified a script and hit this error, use `text_w` rather than `tw`, and make sure the `x` expression contains no dynamic `iw*` computation.

### Q3: `Invalid alpha value specifier '%{eif:...}'` (drawtext fontcolor)

**Cause**: an attempt to embed a `%{eif}` expression in the `fontcolor` parameter to animate opacity, but ffmpeg does not support that syntax inside a colour value.

**Fix**: use drawtext's `alpha` parameter (separate from fontcolor) instead of embedding an expression in `fontcolor=white@'%{eif:...}'`. Correct form:
```
drawtext=text='hello':fontcolor=white:alpha='if(lt(t,1),t,1)'
```
Wrong form (errors out):
```
drawtext=text='hello':fontcolor=white@'%{eif:if(lt(t,1),t,1):d:2}'
```

### Q4: ffmpeg hardware encoder fails (`h264_videotoolbox` / `h264_nvenc` / `h264_qsv` errors)

**Cause**: the detected hardware encoder does not support the current video parameters (an unusual resolution or colour space, for example), or the driver version is incompatible.

**Diagnosis**:
```bash
ffmpeg -encoders 2>/dev/null | grep -E "nvenc|videotoolbox|qsv|amf"
```

**Fix**: temporarily modify `get_ffmpeg_encoder()` in `scripts/utils.py` so it returns `("libx264", ["-preset", "fast", "-crf", "18"])` directly.

### Q5: Chinese subtitles render as boxes (tofu)

**Cause**: no usable Chinese font file on the system.

**Diagnosis**:
```bash
python3 -c "from scripts.utils import find_chinese_font; print(find_chinese_font())"
```
If it returns `(None, ...)`, no Chinese font was found.

**Fix**:
- **macOS**: PingFang SC ships with the system, so this problem generally does not occur.
- **Linux/WSL**: install a Chinese font package:
  ```bash
  sudo apt install fonts-noto-cjk
  ```
- **WSL fallback**: the script automatically tries the Windows font file `msyh.ttc` (Microsoft YaHei), provided that font is installed on Windows.
- **Specify manually**: pass the `--font-path /path/to/your/font.ttf` argument.
- **Automatic download**: on first run the script tries to download Noto Sans SC from Google Fonts (China-based users go through the jsDelivr CDN) and caches it in the `fonts/` directory.

### Q6: Whisper model download fails / times out

**Cause**: network problems — China-based users in particular cannot reach HuggingFace.

**Fix**:
- Use the `--mirror` argument: `python3 scripts/transcribe.py audio.wav --mirror --model auto`
- Or set the environment variable by hand:
  ```bash
  export HF_ENDPOINT=https://hf-mirror.com
  ```
- With faster-whisper the model is downloaded from HuggingFace; once `HF_ENDPOINT` is set the mirror is used automatically.
- With openai-whisper the model is downloaded from GitHub, which China-based users may need a proxy for. Switching to faster-whisper is recommended.

### Q7: `pip install faster-whisper` fails / times out

**Fix**: China-based users should use the Tsinghua mirror:
```bash
pip install faster-whisper -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn
```

### Q8: ffmpeg not found or too old under WSL

**Diagnosis**:
```bash
which ffmpeg && ffmpeg -version | head -1
```

**Fix**:
```bash
sudo apt update && sudo apt install ffmpeg
```
If the ffmpeg in the distribution repositories is too old (< 4.0), use a PPA:
```bash
sudo add-apt-repository ppa:savoury1/ffmpeg4
sudo apt update && sudo apt install ffmpeg
```

### Q9: Poor video quality / blurry output

**Cause**: the video was re-encoded several times, and every encode loses quality.

**Fix**: you must use `render_final.py` for single-pass encoding. Check whether the pipeline used the legacy chain of `split_video.py` + `burn_subtitles.py` + `merge_clips.py` + `generate_cover.py` + `add_chapter_bar.py` (which causes 4-5 re-encodes). Switch to `render_final.py --config` and do it in one step.

## Known Issues & Solutions

The following issues were hit and their solutions verified in past real video production runs.

### K1: Mixed frame-rate source footage freezes when concatenated with concat copy

**Symptom**: after joining several B-roll clips with `ffmpeg -f concat -c copy`, the picture freezes at about the 1-minute mark.

**Root cause**: DJI footage is 30/1 fps while some DJI files and all iPhone MOV files are 30000/1001 (29.97fps). `-c copy` concatenation does not re-encode, so differing frame rates leave discontinuous timestamps and the player stalls at the switch point.

**Fix**: use source-bound clip assembly to unify canvas, CFR, SAR, timestamps, pixel format and audio layout for every segment inside one output encode:
```bash
python3 scripts/clip_assembly.py plan \
  origin/clip-a.MOV origin/clip-b.mp4 \
  --width 1080 --height 1920 --fps 30 \
  --delivery output/assembled.mp4 \
  --boundary-proof verify/clip-assembly-boundaries.mp4 \
  --project-dir . \
  --output work/clip_assembly_plan.json
python3 scripts/clip_assembly.py apply work/clip_assembly_plan.json
```
Play the delivery file and every seam proof in full and confirm before running `confirm` / `verify --strict`. **Never use `-c copy` concat across differing frame rates, canvases, SAR, time bases or audio layouts.**

**How to detect it**: `clip_assembly.py plan` reads each source clip's full decoded `best_effort_timestamp_time`, its media contract and its source hash; looking only at `r_frame_rate` or the FFmpeg exit code cannot prove the joined result is continuous.

---

### K2: Audio truncated inside a complex filter_complex

**Symptom**: the audio suddenly disappears partway through the video (at 1:55, for example) while the picture keeps playing normally.

**Root cause**: combining `amix` (mixing in `anullsrc` as silence padding) + `atempo` (speed change) + video processing in a single `filter_complex` — `amix`'s `duration=longest` is unreliable together with a synthesised null audio source, so the audio stream ends early.

**Fix**: split audio processing into separate steps; never do audio padding + speed change + video processing in one filter_complex:
1. `adelay` to add the silent cover segment
2. `atempo=1.25` for the speed change
3. `apad=whole_dur=X` to pad out to the target duration
4. Export to WAV, then merge with the video using `-c:v copy -c:a aac`

**Principle**: audio padding + speed change + video processing — do not put all three in the same filter_complex.

---

### K3: render_final.py select filter runs out of memory with many clips

**Symptom**: rendering 100+ clips with render_final.py (a voiceover track with B-roll scenes, for example) reports "Cannot allocate memory".

**Historical root cause**: the early `_clips_in_temporal_order()` only checked whether all clips came from the same video and were in increasing time order; it did not check for a broll field. When every clip referenced the same voiceover video (each with a different B-roll visual), the select filter path was still taken, producing a giant select filter containing 100+ `between()` expressions and exhausting memory.

**Fix**:
- A clip-level `broll` now forces the trim/concat path and no longer goes through the select filter.
- For B-roll cues produced by `auto_enrich.py`, prefer `render_final.py --enrich-plan work/enrich_plan.json`: it attaches them as timed video overlays and does not need the voiceover split into many small clips.
- If you still build 100+ clips by hand, use a manual ffmpeg pipeline instead:
  1. Normalise every B-roll clip to the same frame rate, then concat them
  2. Process the audio separately (extract, trim, speed change, pad)
  3. Merge video and audio with `-c:v copy -c:a aac`
- If you must use render_final.py's many-clip mode, keep the clip count within a reasonable range (under 50 clips is recommended)

---

### K4: English words truncated inside Chinese subtitles

**Symptom**: when subtitles wrap, English words (such as "OpenClaw" or "Claude Code") are split down the middle.

**Root cause**: the Chinese mode of `wrap_subtitle_text()` computes line width by character count and treats English letters as single characters of the same width as a Chinese character. When wrapping at a mid-string position it is unaware of ASCII word boundaries and cuts straight through.

**Fix**: the wrap function needs to:
1. Compute by display width: a CJK character = 1 unit, an ASCII character about 0.5 unit
2. Detect ASCII word boundaries when searching for the wrap point (a run of letters/digits counts as one word) and never break inside an English word or number
3. Prefer wrapping at punctuation, spaces and CJK/ASCII boundaries

---

### K5: Cover/end card does not appear

**Symptom**: end_cards is configured in the render config, but the end card is not visible in the final video.

**Root cause**: the B-roll, once concatenated, was far longer than needed (215 seconds required but 431 seconds of B-roll joined, for example). After concatenating cover + B-roll + end card, limiting the total duration with the `-t` parameter cut the end card off beyond the duration limit.

**Fix**: before concatenating the cover and the end card, the B-roll must first be trimmed precisely to match the audio duration:
```bash
# 1. Get the audio duration
audio_dur=$(ffprobe -v 0 -show_entries format=duration -of csv=p=0 audio.wav)
# 2. Trim the B-roll to the audio duration
ffmpeg -i broll_concat.mp4 -t $audio_dur -c copy broll_trimmed.mp4
# 3. Then concatenate cover + trimmed B-roll + end card
```
**Principle**: before concatenating, make sure the duration of every part is precisely known; do not rely on `-t` to cut things off after the fact.

## Audio-Only Podcast Vertical Audiogram

When you have only audio plus a cover image and need to publish a short video, pick a short excerpt that carries complete meaning and prepare an SRT aligned to that excerpt starting at 0 seconds. If native video exists, cut the real talking footage first. When a new cover is needed, prefer Codex's built-in `image_gen` (GPT Image 2); use an existing cover as it is.

Run `runtime_preflight.py analyze --profile podcast_audiogram --output work/runtime_preflight.json --strict`, then run `podcast_audiogram.py render origin/episode.wav origin/cover.png work/excerpt.srt --start 32 --duration 28 --output output/audiogram.mp4 --receipt verify/podcast_audiogram.json`. The script binds the audio, cover, subtitles, settings and final cut together, and outputs 720×1280 H.264/AAC with an auxiliary waveform and burned-in subtitles; a single clip may be at most 600 seconds. `podcast_audiogram.py verify verify/podcast_audiogram.json` re-checks hashes, media specifications and a full decode. `ready_for_human_review` only means the technical checks passed; before delivery you must still watch it end to end at 1× with sound, checking subtitles against the speech, cover rights and legibility, waveform position and end trimming. Re-render and re-review after changing any input.

## Short Motion GIF Preview

When a short action from an already-reviewed video needs to be shown in a chat or a document, run `runtime_preflight.py analyze --profile gif_preview --output work/runtime_preflight.json --strict`, then run `gif_preview.py render output/master.mp4 --start 5 --duration 3 --width 480 --fps 12 --output output/preview.gif --receipt verify/gif_preview.json`. The excerpt is limited to 10 seconds, an even pixel width of 64-640, 5-20 fps and 20 MiB; FFmpeg generates the palette for the whole clip in the same task and exports a silent GIF. `gif_preview.py verify verify/gif_preview.json` re-checks the source/GIF hashes, frame count, dimensions, duration and a full decode. Before delivery, watch the full loop at the real display size and confirm the start and end of the action, subtitle legibility and colour banding; the GIF carries no sound, so the reviewed MP4 is still what gets published.
