Communitygithub.com

znlgis/video-studio

用程式碼從零生成中文影片(科普片/紀錄片/宣傳片/動畫短片):寫劇本→edge-tts 旁白→實測時長排時間軸→PIL 或 Canvas 逐幀渲染→ffmpeg 編碼→numpy 取樣級混音→自動品管。當使用者要「做一個影片/生成宣傳片/把報告變成影片/做動畫/配音配字幕/出成片/修影片音畫不同步/影片畫質糊」,或提到 ffmpeg、edge-tts、逐幀渲染、字幕、分鏡、配樂、混音時使用。 | Code-generated video production (Chinese narration, deterministic frame rendering, ffmpeg, edge-tts, QA gates). Use when asked to make a video, turn a report into a video, animate a story, fix A/V sync or blurry text in a generated video.

video-studio 是什麼?

Video Studio (video-studio) 是一個程式碼驅動的中文影片生成工具,專為開發者與 AI 代理設計。它能讓您透過編寫一個簡單的 `film.json` 腳本,自動化完成影片製作的全部流程:從劇本撰寫、使用 edge-tts 生成旁白並實測音訊長度以建立精準時間軸,到使用 PIL 或 Canvas 進行確定性的逐幀渲染,最終透過 ffmpeg 編碼輸出。它的核心設計原則是「音訊先於畫面」與「每一幀都是純函數」,確保輸出的影片完全可複現、可腳本化驗收,並能自動進行品質檢查。此工具支援多種影片類型,如科普片、紀錄片、宣傳片及動畫短片。 此技能的獨特之處在於其「先提案,再生產」的工作流程。每次新任務啟動時,它會強制透過 `propose.py` 腳本現場生成多個視覺與聽覺風格截然不同的候選方案,讓使用者挑選或提供反饋,避免了每次產出影片都大同小異的問題。產品外觀並非來自預設模板,而是從連續的色相空間與參數化設計中即時產生,並配備了「片間去重台帳」機制,確保不同影片之間不會重複使用相似的設計。這個工作流程確保了使用者在創作過程中的主導權,產出的影片具有獨特性和高品質。 Video Studio 的核心功能包括:透過 `propose.py` 快速生成並比較多種設計方案、使用 `render.py` 進行包含自動 TTS、排版、配樂、混音、逐幀渲染與編碼的全自動生產管線。它支援「斷點續跑」,中間產物會保留在 `work/` 目錄中,方便除錯與迭代。對於需要修改畫面的使用者,只需在專案目錄中放置一個自訂的 `scenes.py` 檔案並實作 `draw_frame` 函數,即可完全控制每一幀的視覺細節,而不需要複製或修改技能內部的任何腳本。最後,它還內建了嚴格的品質閘門,包括鐵律規範與自動化檢測腳本,確保最終影片的穩定性和可靠性。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/znlgis/dsh-video-studio-skill/tree/HEAD/skills/video-studio

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Video Studio (video-studio)

In one sentence: Video = Script + Audio-measured timing + Deterministic frame functions + ffmpeg encoding + Automated QC. All visuals and audio are code-generated: zero stock footage, reproducible, pause-and-resume capable, scriptable verification.

0. Hard Rules (Breaking any one will definitely ruin the final video)

  1. Audio first, visuals second. The timeline must be derived from actual TTS durations. Never estimate by word count. A timeline based on word counts will drift noticeably starting around sentence 20.
  2. Each frame is a pure function: draw(t, frame) -> Image. Forbid random/time(), use filmkit.hash2/hash3/vnoise for randomness. The same frame number must always produce the exact same image.
  3. Subtitles and large text must be in the safe zone: Body text y ∈ [58, 974], subtitles y ∈ [872, 1030], never exceed 1030.
  4. Direct raw frame encoding: PIL frames → rawvideo pipe → libx264. Do not use cv2.VideoWriter as an intermediate file (2x slower and adds a second compression loss).
  5. Audio mixing uses numpy sample-level summation. Do not feed a single ffmpeg command with hundreds of inputs (causes non-deterministic silence on Windows).
  6. Must run QC script + visually inspect 3 still frames (using read_image) before delivery. "Can render frames" ≠ "no issues."
  7. Audio follows visuals: Register cue points for sound effects in the scene code (via K.CueBus / scenes.py's cues() hook). Do not manually align audio after the fact. When visuals change (different hold duration, pace adjustment), cue points update automatically.
  8. If unsure, add hold, don't slow down the entire piece: Use hold in film.json (global) or lines[].hold (per sentence) to add pauses after speech ends; align music with bpm for bar matching.

0.5 [Hard Rule] Propose First, Then Produce (Regardless of User Request)

The user must see a proposal even if they didn't ask for it. This is the most important product behavior of this skill, not a suggestion. Reason: If the appearance is chosen by the program on behalf of the user, the user has no decision-making input from start to finish, and the result will inevitably be "every video looks the same." Therefore, the workflow is fixed: Generate several design options on the spot → User picks one → Freeze the design → Then produce the video.

Fixed Action (the very first thing for any new video production task):

python propose.py --brief "<Topic/Audience/Length/Tone>" --script film.json --n 3

This produces N candidate proposals (each = one design spec + 4 still frames + cost + design distance from other candidates), saved to work/proposal.json, and prints a message like "Confirm one / or use --revise if none satisfy." Fully offline, takes seconds.

User ReactionWhat You Do
"This one" / picks onepython propose.py --confirm --candidate B, then proceed to render
"Too cold/too fancy/change voice/faster pace"python propose.py --revise "<original feedback>" — new revision, old revision retained for diff
"None work, start over"Same as above, make the request specific (the --revise text is saved in proposal.feedback)
Explicitly says "You decide"Still produce a proposal first, pick one you recommend and explain why, then get user approval

Hard Constraints:

  1. Before propose.py --confirm, render.py's frames/all stages are blocked by a proposal gate (default warning, --require-proposal causes failure).
  2. Candidates must differ in ≥2 design dimensions (color palette/motif/transition/easing/numbering/music/voice). Changing only colors doesn't count as multiple proposals; diversity.vs[].n < 2 will be flagged.
  3. Each candidate must have a one-line Chinese pitch (pitch), clearly stating "what this proposal aims to achieve" — not just "Proposal A/Proposal B."
  4. No built-in templates to choose from: All fields in design.json are open (35 themed fields + five visual language signatures + sound/music/rhythm), colors are any point in continuous hue space, not picked from a few pre-built looks. Each proposal is generated on the spot — running the same brief twice yields different designs (use --seed for reproducibility).
  5. Cross-video deduplication ledger (enabled by default): --confirm records the frozen design into ~/.dsh/video-studio/design-ledger.json (max 500 entries). Next proposal will actively avoid looks recently delivered — candidates whose design distance to any video in the ledger is < 2 dimensions will be automatically re-sampled with a different nonce and a warning. This turns "different videos look different" from "relying on randomness" to "having a memory." Use --show-ledger to inspect, --no-ledger to ignore for this run, environment variable VIDEO_STUDIO_LEDGER to change path or set to off to disable. Ledger read/write failures always treat it as empty, never block production.

There is only one exception to skip the proposal: Pure fixes / pure re-renders (no content change, no design change, only fixing A/V sync or re-outputting the video). In this case, run without --require-proposal and explain in the response "Proposal skipped this time because…"

0.5.1 Where the Look Comes From: Only Two Paths

There are no "built-in styles" to choose from. Any style value written in film.json (auto / legacy / some id) will be clearly rejected with two-step guidance — the script does not determine the look; the look is determined by work/design.json. Similarly, this skill has no THEMES/STYLES/VISUAL_LANGUAGES tables, no presets/ directory; music is generated on the fly by musicgen.py based on parameters, and scenes.draw_frame has no legacy branches.

To get a specific look, there are only two paths:

# ① Describe requirements in natural language (recognizes keywords in must_have/avoid)
python propose.py --brief "Tech keynote, deep blue, letterboxed" --script film.json

# ② Write the design.json directly (all fields open, colors are any RGB)
python render.py --script film.json --stage all     # Prerequisite: work/design.json already exists

For detailed contract, see references/proposal.md and schemas/design.schema.json.


1. Five Steps to Get Started (Zero Copy: No copying scripts or templates)

This path involves zero "copy" actions. Skill scripts are executed directly from the skill directory; your project directory only needs a film.json (plus the work/ and out/ directories generated during execution).

<SKILL_DIR> refers to the resourceBase.path reported by the skill tool when this skill is loaded. Installed as a plugin, it is the skills/video-studio/ directory within this plugin package (the profile's node_modules/@dsh-gis/dsh-video-studio-skill is a symlink). If not found, locate by name: find ~ -path '*/skills/video-studio/scripts/filmkit.py'.

<REPO> refers to the package root (repository root, or after installation, node_modules/@dsh-gis/dsh-video-studio-skill/ in the profile), which is two levels above <SKILL_DIR>. tools/ and schemas/ are under <REPO>, not under <SKILL_DIR>.

# 1) Write only a film.json (the only file you need to manually create in your project directory)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
  "title": "My Video",
  "lines": [
    {"id": "l01", "kind": "hook",  "chapter": "01 · Opening",
     "text": "This is visuals and audio generated entirely from code.", "sub": "Zero assets, all code-generated", "big": "Zero assets, all code-generated"},
    {"id": "l02", "kind": "body",  "chapter": "02 · Body", "kicker": "PIPELINE",
     "text": "The entire pipeline has only five steps: write script, generate narration, layout timeline, render frame by frame, mix and encode.",
     "sub": "Five steps to a finished video, each persisted", "big": "Five steps to a finished video",
     "items": [{"text": "Script film.json"}, {"text": "Narration"}, {"text": "Timeline"}, {"text": "Frame-by-frame rendering"}, {"text": "Mixing"}]},
    {"id": "l03", "kind": "outro", "chapter": "99 · Closing",
     "text": "First define the script, then the audio, and only then the visuals.", "sub": "Order matters more than effort", "big": "Order matters more than effort"}
  ]
}
JSON
#    lines[].text = text to be read aloud (natural spoken language); sub = on-screen subtitle (short); kind = hook/body/quote/outro/emphasis/calm
#    Visual style/music/voice/rhythm are all determined by the design, there is **no** `style` field to write in film.json.
#    Contract validation (optional, takes a few seconds):
python <SKILL_DIR>/scripts/filmcheck.py film.json

# 2) Run a self-check first (optional; 69 regression guardrails, about 10–20 seconds, no network; missing optional modules will be flagged SKIP)
python <SKILL_DIR>/scripts/selfcheck.py

# 3) [Hard Rule] Propose first, then produce — the same brief gives different results each time, so the user must see the options
python <SKILL_DIR>/scripts/propose.py \
       --brief "Three years of AI LLMs, 30-second explainer, deep blue" \
       --script film.json --work work
#    Produces 3 candidates: each with a design + contact sheet of stills (work/proposals/rev1/_compare.png) + design distances,
#    results in seconds, fully offline. Look at the side-by-side comparison, pick one.

# 4) Confirm a candidate, freeze the design (work/design.json) — subsequent rendering accepts this design only
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
#    Not satisfied? Pick another or revise: --revise "Make it colder, less decoration, change to female voice" (new revision, old one kept for diff)
#    To reproduce a specific run: add --seed 42 on the first proposal (no seed = new nonce each time, this is the source of "every video is different")

# 5) View stills first, then render the full video; if --scenes is omitted, the built-in renderer in the skill is used
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
       --stills "l01:3,l02:5,l03:2.5"
#    → work/stills/_sheet.png: Visual inspect using read_image, don't skip this step

python <SKILL_DIR>/scripts/render.py --script film.json --work work \
       --out out/film.mp4 --stage all
#    TTS→Layout→Music→SFX→Mix→Frames→Encode→QC — all-in-one; intermediate artifacts all in work/, can be resumed from breakpoints.

About --scenes (the key to zero copy): Without --scenes, render.py looks for the scene renderer in this order: ① scenes.py in the same directory as the script → ② Built-in assets/scenes.py in the skill. So by default you don't need to write anything or copy anything; the built-in renderer produces the video directly. Only when you want to customize the visuals should you place your own scenes.py (implementing draw_frame and optionally cues()) in the same directory as the script — it will be auto-discovered. You can also use --scenes my_scenes.py explicitly. The built-in renderer source code is the best example: <SKILL_DIR>/assets/scenes.py.

# Actual signature (six parameters, missing one will crash): `scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
    # scene    : Dictionary for this segment from the timeline (id / kind / scene_type / sub / big / items / stats ...)
    # t_local  : Time within the segment (seconds, 0..dur) — entrance/continuous animations advance within it
    # dur      : Total duration of this segment (seconds)
    # t_global : Global time in the video (seconds) — cross-segment continuous effects (slow push, breathing) use this
    # script   : The entire script/plan dictionary (plan, theme colors, words, etc.; cached, don't modify in place)
    # frame    : Absolute frame number (integer) — use for deterministic noise/phase, not time()
    return img   # PIL.Image, must be 1920x1080 RGB (start with K.new_frame() for safety)

When writing a custom scenes.py, start by copying one renderer (like _draw_hero) from <SKILL_DIR>/assets/scenes.py. It has already solved pitfalls like pure functions, cached typography, and pre-baked base layers.

Why propose first: There are no "built-in styles" to choose; the look is determined by design.json. The same brief run twice yields different designs (palette/motif/transitions/music/voice can all differ), so the workflow must be "generate proposals → human confirms → freeze → produce"; rendering without confirmation will trigger a proposal warning from render.py (adding --require-proposal causes it to fail outright). For the design contract, see references/proposal.md.

Staged execution (each stage does only its own task, persisted and individually restartable):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style   # Parse confirmed design, print theme/visual language/layout rotation/subtitle style
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts      # Synthesize narration only + measure actual duration (cached with input validation)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout   # Build timeline only + select compositions, output timeline.json / sub.srt / storyboard.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues     # Generate cue points only -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score    # Generate music only -> work/music.wav (musicgen on-the-fly orchestration)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx      # Generate sound effects track only (sfxkit, based on cues.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio    # Mix audio only (narration + music + sfx, narration prioritized)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames   # Render frames only (--engine pil|web, --jobs N chunked parallel)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux      # Encode only (video stream copy, two-pass loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa       # Run QC only (read-only input, writes only to work/qa/)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills   # Render master stills + contact sheet only
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills   # Human sign-off on one gate (writes work/signoff.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain  # Report "which stages are stale, why, and what to run" only, doesn't run any stage

The full --stage all order: style → tts → layout → cues → score → sfx → audio → frames → mux → qa. Any step that fails will stop the process and not continue running with bad inputs.

Estimate cost before rendering (optional; estimate.py is a repository tool, called via <REPO>):

python <REPO>/tools/estimate.py --script film.json --jobs 4

It provides frame count / per-stage time / disk usage / chunking suggestions (--json for machine-readable output). It gives an upper bound, not a guarantee; if work/timeline.json exists, it uses actual durations; otherwise, rough estimate by word count. To skip full-video VMAF, write "qa": {"vmaf": {"mode": "off"}} in film.json (both estimate and render read this field, G11 logs it as skipped).

Inspect delivery credentials before sign-off:

cat out/QC-REPORT.md     # Gate matrix with pass / fail / skipped tiers; skipped = not executed this run, not equal to pass
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release

Sign-off is bound to content fingerprints: changing the script / scenes.py / design invalidates old sign-offs, requiring re-inspection and re-signing. To force sign-off with skipped gates (treated as failures): add --strict-gates.

Long-form video rendering (chunked parallel + resume from breakpoints):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume

Chunks are stored in work/segments/, named as seg_<start_frame>_<end_frame>.mp4; before concatenation, an explicit gap detection is performed, missing chunks cause an immediate error — a missing segment does not produce a partial video. Default chunk size is total_frames / jobs / 4 rounded to whole seconds (jobs=4, 900 frames gives ~8 second chunks). Override with chunk_frames in film.json.

After changing dialog / scenes.py / design / encoding parameters, --resume will refuse to reuse old chunks (the plan fingerprint is stored in work/segments/_plan.json) and prompt you to rm -rf work/segments — this is intentional: old chunks with a new timeline would produce a video where "visuals and narration don't match," and the duration gate wouldn't catch it.

2. Standard Eight Stages (P0–P7)

StageOutputKey ActionCommon Mistake
P0.5 Define Designwork/design.json + work/style.jsonpropose.py generates proposals → --confirm freezes design; --stage style only parses confirmed design and prints theme/visual language/layout rotation/subtitlesSkipping → render.py errors (no design = no look); rendering without confirmation triggers proposal warning
P0 Set BaselineCONVENTIONS.mdResolution/framerate/duration/BPM→frame conversion calculated once, shared across entire projectEach file writing magic numbers like 150 → scene boundaries misaligned
P1 Write Scriptfilm.jsonSplit each sentence into text (spoken) and sub (subtitle); each segment has a kind; optional lines[].type to lock compositionUsing one field for two purposes → subtitle too long or reads stiffly; Don't write style (it no longer affects visuals; look is determined by work/design.json)
P2 Fact CheckFACTS.mdEvery number/year on screen has a source number; maintain a "never show" blacklistSkipping this step → visual and narration numbers conflict, first comment will point out your error
P3 TTSwork/tts/*.mp3 + tts.jsonedge-tts synthesizes sentence by sentence, ffmpeg -f f32le measures actual duration, disk cachedEstimating duration by word count → subtitle drift
P4 Layout Timelinework/timeline.json + sub.srtdur = lead + measured speech + tail, punctuation must be within its own segmentSegment too short → narration cut off by next segment
P5 Mix Audiowork/audio.wavnumpy bus: narration + music + sfx summed sample by sampleffmpeg multi-input mixing → non-deterministic silence
P6 Master Stills3 still framesRender 3 frames first to check composition/colors/font weight, then render full videoRendering the full video directly → 36000 frames later discovering ugly font
P7 Final Videoout/film.mp4rawvideo→libx264 CRF18 + two-pass loudnorm to -16 LUFSBitrate too low/loudness -25 LUFS → "looks like a draft"

Note: The default crf in filmkit / render.py is 18; assets/film.json is just a test fixture (used to verify the full pipeline and golden frame/gate regression, not a template for you to copy). It uses 17. If you don't write crf in your own script, it defaults to 18; for sharper text, use 17 (approx +30% file size).

3. Choosing a Rendering Engine

ScenarioChoiceMeasured SpeedSuitable For
Graphics/Data/Explainer/UI-stylePIL (numpy+cv2 assisted) — filmkit default40–80 fps @1080p30 (example video: 75)Most Chinese explainer videos, report-to-video
Needs smooth vectors/particles/3D/paper texturesHeadless browser Canvas2D/WebGL + CDP screenshot → image2pipe (webengine.py)80–130 ms/frame (integrated GPU, PNG screenshot)Documentary-level pieces, complex visual language
Pure data animationmanim (if installed)Depends on sceneMathematics/formula derivation

The speeds in the table above are measured on a single machine under no load (will vary with different hardware and more complex visuals; see §12). Don't treat the ranges as guarantees.

Design only covers the PIL engine (engine: pil, default): The colors/fonts/layouts/motifs in work/design.json are read by assets/scenes.py. The --engine web page is written by you (assets/web/ is just a working example page, not a production template); its visuals don't follow the design. However, music and narration do follow design.json (they don't depend on the rendering engine).

Default to PIL: Fewer dependencies (numpy+Pillow is sufficient), faster startup, strong determinism, lower debugging cost. Use the browser engine only when you need "continuous particle deformation / real-time shader textures":

cp -r <SKILL_DIR>/assets/web ./web                     # Page template (native Canvas2D, zero third-party libraries)
pip install playwright && python -m playwright install chromium   # Only needed for the web engine
python render.py ... --engine web --stage cues        # Export cue points from the page
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html            # Check for text overlaps/out of frame/flashes/never-appeared

Trade-offs between the two paths (decide carefully before choosing; switching engines requires redoing the visuals):

PIL (engine: pil)Browser (engine: web)
Dependenciesnumpy + Pillow (+cv2 for sharpness comparison)+ playwright + Chromium/Edge
Speed40–80 frames/sec8–12 frames/sec (per-frame screenshot + encoding, an order of magnitude slower)
Visual LanguageBitmap compositing, gradients, geometry, textCanvas2D/WebGL: particles, deformation, shaders, vector animations
DeterminismNatural (pure functions for drawing)Guaranteed by precisely seeking the timeline to f/FPS (via window.renderFrame)
DebuggingModify a frame and run itDebug in the browser with index.html?dev=1&t=seconds
Suitable ForGraphics/Data/Explainer/Promotional VideosDocumentary-level pieces, continuous visual language sequences

Three hard constraints for the browser engine (implemented in webengine.py, must also be followed when writing your own page):

  1. All tweens must be on a single paused main timeline; rendering the f-th frame = seeking it to f / FPS seconds; must not rely on playback.
  2. Procedural content like loops/particles/waves must use a "calculate by time" updater (update(fn)), not cumulative setInterval.
  3. The page must expose window.ready / DURATION / renderFrame(f) / getCues(); no CDN, no network requests.

For cross-session bit-exact reproducibility (after crash restart, multiple renders for diffing, QC comparing two images), enable deterministic mode: --deterministic or environment variable WEBENGINE_DETERMINISTIC=1 (disables GPU rasterization, empirically consistent across 8/8 sessions, cost ~+22%/frame). Disabled by default: within the same session, it's inherently consistent; cross-session differences are limited to ≤4 pixels, ±1 level of anti-aliasing.

4. Visual Quality Checklist (These Items Determine "Does It Look Professional")

  • Brightness & Depth: Dark background (#0a1420) + bright top accent; avoid pure black or white — user screens will eat the gradients.
  • Font Weight & Size: Large text 84–96px, subtitles 34–38px, subtitles 46px; use msyhbd.ttc (Microsoft YaHei Bold) for Chinese.
  • Subtitles: ≤ 2 lines, approx 24 Chinese characters per line, with semi-transparent background, y ≥ 872. Actual line breaking is based on pixel width (fit_sub_lines auto-wraps at max_w=1560 px), the "24 characters" is a rough upper bound, not a hard validation — the constant SUB_MAX_CHARS=24 is currently not referenced by any code, don't rely on it to stop you.
  • Rhythm: Visual change every 3–6 seconds (new card / movement / number counter); if static for more than 6 seconds, the viewer leaves.
  • Camera: Only use 3–15% slow push; avoid large zooming in/out.
  • Color Grading: One primary color + one accent color for the entire video, no more than 4 hues.
  • Audio: Master at -16 LUFS / True Peak ≤ -1 dBTP; narration 12 dB above ambient gaps; leave 1.4 dB headroom before AAC.

5. Sound: Music + SFX + Cue Points

The philosophy in this section comes from a retrospective on a real finished video (ai-history-film): Bad sound isn't "not loud enough," it's "sound doesn't know what's happening on screen." The order of resolution is always: first make the sound aware of what's happening on screen (cue points), then discuss timbre, and finally mastering.

5.1 Three Audio Layers

LayerWho Does ItPersisted ToDescription
Narrationfilmkit.tts (edge-tts)work/tts/*.mp3The sole authority for duration: the timeline is derived from it
Musicmusicgen.py (parametric orchestration)work/music.wavGenerated on the fly based on design's bpm/key/scale/mood/seed + orchestration parameters; no pre-existing musical styles to choose from
SFXsfxkit.py (physical modeling)work/sfx.wavSynthesized per cue point registered in cues.json

All three buses are mixed in --stage audio: When SFX plays, music automatically ducks (duck_db defaults to 4 dB, cinema/epic 5.5 dB — with a 3 dB duck, music is almost at the same level as SFX during a transition, causing a muddy sound), finally handed to ffmpeg's two-pass loudnorm for -16 LUFS / True Peak ≤ -1 dBTP. Do not do final loudness normalization yourself in numpy — you have loudnorm, which is far more accurate than manual normalization, and True Peak is the gate.

Intra-piece separation: Music already has transitions at segment boundaries (swells/risers/sub_boom from the orchestrator); the SFX track doesn't double up whooshes/risers at the same boundary — --stage sfx automatically reduces transition-like cues that fall within [boundary - 2.2 s, boundary] by -3 dB (sfx.duck_transitions).

[continues...]

6. QC Gates (G0–G11)

All gates are idempotent — run any number of times, they always produce the same result. Never modify inputs.

GateCheckFails IfOutlier Handling
G0 Planfilm.json passes schema...Schema error → stop with code hint
G1 AssetsAudio files exist + have correct lengthMissing or corrupted files--stage tts to regenerate; stale file > tokens → warn "stale TTS, rerun tts"
G2 TimelineSentences non-overlapping + total length = expected ±0.5sOut-of-bounds / over 0.5s driftTarget frame count difference → warn "N frames mismatch after mux, length gate will catch"
G3 CuesAll cue registrants have a rendererMissing / duplicate / 0-duration cuesPrint list of unresolved cues
G4 MusicDuration matches timeline ±0.5s (no gap/overshoot)MismatchGap → interpolate or refill; overshoot → stop
G5 SFXAll sfx.mp3 durations align with cuesUnmatched / timed poorlyLeftovers → warn "orphan sfx"
G6 Audio3 tracks mix to -16 LUFS ±0.5 / True Peak ≤ -0.5 dBTPOut of spec or clippingTrue Peak > -0.5 dBTP → increase headroom; mix > 0.25s deviation → wait/rerun
G7 FramesAll frames output + no batch failuresBatch error / zero outputCheck chunk finish log; --resume reruns empty chunks
G8 EncodeDuration ±0.03s / VMAF ≥ 88(CRF18 reference) / no freeze frames ≥ 5GOP error / black frames / out-of-syncRe-encode; if VMAF < 88, try --crf 17
G9 TextQANo text overlaps / no uncommanded blanks / no flash-textWrong subtitle timing (visual > audio + 0.33s)webengine.py textqa compares rendered frames + subtitle SRT
G10 Stills3 stills compared with master (SSIM ≥ 0.92)Sign-off expired (script/design/scenes changed)Re-inspect stills, keep screen open; --stage accept --gate stills
G11 ReleaseAll gates passed + last sign-off fingerprint matches current inputStale sign-off / manual --skipPrint diff: "N files changed: A, B, C…" → user must re-sign

G7 & G8 output detours: Floats do not affect the video of the current render, only delay correction to the next render. Detours are not pass.

G8 VMAF reference: CRF 18 encodes average ~92.5±2.5 (source 1080p text). Below 88 causes visual quality traffic light (yellow: ≥ 86, can pass but will warn; red: < 86, forced failure). Different engines have different baselines: PIL average 92, browser 89 — in film.json, adjust with "qa.vmaf.engine_offsets": {"web": 3}. Not recommended unless the engine is the limiting factor (using WebGL with complex shaders).

G8 freeze frame detection: Preceded by 0 still frames ≥ 0.3s; detects "repeated frames" by comparing pixel hash. Manual judgment needed: intentional still picture vs machine glitch. Use --mux no-detect to skip (GCAT times out, GCAT must watch side-by-side). Never output invisible black side bars; use letterbox widening (letterboxing can cause VMAF drops).

G10 stills master: Even if design/scenes haven't changed, rendering with a different engine version (Pillow 10→11) may cause differences. --stage stills outputs work/stills/_master/ with commit hash → when rendering is repeated, compare diff → re-inspect stills.

G11 release gate: The only gate that needs human confirmation. Even if all automated G0–G10 pass, G11 fail will block out/ output. During --stage accept --gate release, read the QC report aloud: "G1 / G2 / … all pass. Check three stills?"

7. Environment Check & Common Integration Problems

7.1 Main Dependency Tree

filmkit
├── numpy                     # Core compute (audio mixing, pixel ops)
├── Pillow                    # Frame rendering (PIL engine)
├── edge-tts                  # Narration synthesis (Microsoft edge TTS)
├── ffmpeg (binary)           # Encode + loudnorm
├── (optional) opencv-python  # VMAF comparison, format conversion
└── (optional) playwright     # Web engine

Key hard constraints:

ProblemSymptomSolution
ffmpeg < 5.0loudnorm returns inconsistent LUFSffmpeg -version, upgrade
edge-tts no moduleModuleNotFoundError: No module named 'edge_tts'pip install edge-tts
Network unavailable + cache miss—stage tts stuck / errorPre-download or fallback offline TTS; —tls-bypass skips TLS check for intranet
Windows ffmpeg pathFileNotFoundError: ffmpegAdd to PATH or $env:FFMPEG_PATH="C:\\path\\to\\ffmpeg.exe"

8. References

ReferenceLocationPurpose
Design Contractschemas/design.schema.jsonDefines all fields and constraints of design.json
Proposal Protocolreferences/proposal.mdDescribes proposal generation, revision, comparison, and confirmation workflow
Built-in Scene Rendererassets/scenes.pyReference implementation of draw_frame, including all built-in scene types
film.json Schemaschemas/film.schema.jsonDefines script structure, sentence types, chapter layout
Pipeline Stage Mapreferences/stages.mdData flow, dependency graph, and cross-stage cache invalidation rules for all stages
QC Gate Mapreferences/gates.mdChecks, failure modes, and outlier handling for all 12 gates
Timeline Formatschemas/timeline.schema.jsonDefines timeline output format, composing sentence start, end, duration, and composition reference
Browser Engine Protocolreferences/webengine.mdHow to write a browser page that the engine can drive renderFrame / getCues
Audio Bus Contractreferences/audiobus.mdTrack levels, ducking rules, final limiter parameters for each audio layer
VMAF Baseline Dataresources/vmaf-baseline.csvBaseline VMAF scores for CRF 18/17 and different engine configurations

相關技能