Communitygithub.com

视频工作室 (video-studio)

用代码从零生成中文视频(科普片、纪录片、宣传片、动画短片):写剧本→edge-tts旁白→实测时长排时间轴→PIL或Canvas逐帧渲染→ffmpeg编码→numpy采样级混音→自动质检。适用于需要制作视频、生成宣传片、将报告转为视频、制作动画、配音配字幕、出成片、修复音画不同步或视频画质模糊等场景。

Was ist 视频工作室 (video-studio)?

**Video Studio (video-studio)** 是一个完全由代码驱动的视频生成技能,专为需要从零创建中文视频(如科普片、纪录片、宣传片、动画短片)的开发者或AI代理设计。它的核心哲学是“音频先做,画面后做”,确保时间轴由TTS(edge-tts)实测时长反推,从而避免字幕漂移。该技能支持PIL(Python Imaging Library)和Canvas(通过无头浏览器)两种渲染引擎,并通过numpy进行采样级混音,保证音频质量。它内建了严格的提案流程(`propose.py`),要求在任何新项目开始时自动生成多个设计方案供用户选择,避免“千片一面”。此外,技能包含完整的质检(QA)门禁系统,自动检查帧同步、音频响度和画质,确保交付物专业可靠。

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/znlgis/dsh-video-studio-skill/tree/HEAD/skills/video-studio

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

视频工作室(video-studio)

一句话:视频 = 剧本 + 音频实测时长 + 确定性逐帧函数 + ffmpeg 编码 + 自动质检。 画面与声音全部由代码生成,零素材、可复现、可断点续跑、可脚本化验收。

0. 铁律(违反任意一条,成片一定会出问题)

  1. 音频先做,画面后做。时间轴必须由 TTS 实测时长反推,绝不按字数估算。按字数排的时间轴会在第 20 句开始明显漂移。
  2. 每帧是纯函数:draw(t, frame) -> Image。禁 random/time(),要随机用 filmkit.hash2/hash3/vnoise。同一帧号必须永远画出同一张图。
  3. 字幕与大字必须落在安全区:正文 y ∈ [58, 974],字幕带 y ∈ [872, 1030],永不超过 1030。
  4. 原始帧直通编码:PIL 帧 → rawvideo 管道 → libx264。不要用 cv2.VideoWriter 做中间文件(慢 2 倍且二次有损)。
  5. 混音用 numpy 采样级叠加,不要用一条 ffmpeg 命令喂上百个输入(Windows 上会非确定性静音)。
  6. 交付前必须跑质检脚本 + 亲眼看 3 张静帧(用 read_image)。"能出图"不等于"没问题"。
  7. 声音跟着画面走:该响一下的地方在场景代码里登记提示点(K.CueBus / scenes.py 的 cues() 钩子), 不要事后手工对时间。画面改了(换停留窗口、调节奏),提示点自动跟着改。
  8. 没看清就加停留,别整片放慢:film.json 的 hold(全局)或 lines[].hold(单句) 在语音结束后加停留;配乐用 bpm 对齐小节。

0.5 【铁律】先提案,再出片(不论用户是否要求)

用户没要求,也必须先给方案。 这是本技能最重要的一条产品行为,不是建议。 理由:外观一旦由程序替用户选,用户就从头到尾没参与决策,结果必然是"每个视频都差不多"。 所以流程固定为:现场采样出几个方案 → 用户挑一个 → 冻结成设计 → 再出片。

固定动作(任何新成片任务的第一件事):

python propose.py --brief "<题材/受众/时长/语气>" --script film.json --n 3

它会产出 N 个候选方案(每个 = 一份设计 + 4 张静帧 + 成本 + 与其它候选的设计距离), 落盘 work/proposal.json,并打印一句"确认其中一个 / 都不满意就 --revise"。全程离线、秒级。

用户反应你要做的
"就这个" / 选了某个python propose.py --confirm --candidate B,然后才允许渲染
"太冷了/太花哨/换个声音/节奏快点"python propose.py --revise "<原话>" —— 新 rev,旧 rev 保留可 diff
"都不行,重来"同上,把要求写具体(--revise 的文本会存进 proposal.feedback)
明确说"你定就行"仍然先出方案,挑一个你推荐的并说明理由,让用户点头

硬约束:

  1. propose.py --confirm 之前,render.py 的 frames/all 会被提案闸拦下(默认警告,--require-proposal 失败)。
  2. 候选必须两两在 ≥2 个设计维度上不同(配色族/母题/转场/缓动/编号/配乐/声线)。只换颜色不算多方案,diversity.vs[].n < 2 会被标出来。
  3. 每个候选必须有一句中文立意(pitch),说清"这个方案想干成什么样"——不是"方案 A/方案 B"。
  4. 没有内置模板可套:design.json 的所有字段都开放(35 字段主题 + 五组视觉语言签名 + 声音/配乐/节奏),配色是连续色相空间里的任意一点,不是从若干套现成外观里挑。 每次提案都是现场生成——同一个 brief 连拍两次会得到不同设计(要复现就传 --seed)。
  5. 片间去重台账(默认开启):--confirm 会把冻结的设计记进 ~/.dsh/video-studio/design-ledger.json(最多 500 条),下次提案主动避开最近交付过的外观—— 候选与台账里任一部片的设计距离 < 2 维时自动换 nonce 重采并告警。 这是"不同片不重复"从"靠随机"变成"有记忆"的落地机制。--show-ledger 查台账、 --no-ledger 本次忽略、环境变量 VIDEO_STUDIO_LEDGER 改路径或设 off 关闭。 台账读写失败一律当空表,永不阻塞出片。

只有一种情况可以跳过提案:纯修复 / 纯重渲(不改内容、不改设计,只修音画不同步或重出片)。 这种情况下用 --require-proposal 之外的方式跑,并在回复里说明"本次未提案,因为…"。

0.5.1 外观从哪来:只有两条路

没有"内置风格"可选。 film.json 里写任何 style 值(auto / legacy / 某个 id)都会 被明确拒绝并给两步指引——剧本不决定长相,长相由 work/design.json 决定。 同理,技能里没有 THEMES/STYLES/VISUAL_LANGUAGES 表,也没有 presets/; 配乐由 musicgen.py 按参数现场编排,scenes.draw_frame 也没有 legacy 分支。

要特定外观只有两条路:

# ① 自然语言提要求(认得 must_have/avoid 里的关键词)
python propose.py --brief "科技发布会,深蓝,要遮幅" --script film.json

# ② 直接手写 design.json(字段全开放,配色是任意 RGB)
python render.py --script film.json --stage all     # 前提:work/design.json 已存在

详细契约见 references/proposal.md 与 schemas/design.schema.json。


1. 五步上手(零拷贝:不拷脚本,不拷模板)

这条路径没有任何"拷贝"动作。 技能脚本留在技能目录里直接执行; 你的工程目录里只需要一个 film.json(外加运行产生的 work/ 与 out/)。

<SKILL_DIR> 指本次加载技能时 skill 工具回报的 resourceBase.path。装成插件后它就是 本插件包内的 skills/video-studio/(profile 的 node_modules/@dsh-gis/dsh-video-studio-skill 是链接)。 找不到时按名字定位:find ~ -path '*/skills/video-studio/scripts/filmkit.py'。

<REPO> 指包根(仓库根,或装好后 profile 里的 node_modules/@dsh-gis/dsh-video-studio-skill/), 即 <SKILL_DIR> 往上两级。tools/ 与 schemas/ 在 <REPO> 下,不在 <SKILL_DIR> 下。

# 1) 只写一个 film.json(工程目录里唯一需要你手写的文件)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
  "title": "我的片子",
  "lines": [
    {"id": "l01", "kind": "hook",  "chapter": "01 · 开场",
     "text": "这是用代码从零生成的画面与声音。", "sub": "零素材,全代码生成", "big": "零素材,全代码生成"},
    {"id": "l02", "kind": "body",  "chapter": "02 · 正文", "kicker": "PIPELINE",
     "text": "整条管线只有五步:写剧本、生成旁白、排时间轴、逐帧渲染、混音合成。",
     "sub": "五步成片,每步落盘", "big": "五步成片",
     "items": [{"text": "剧本 film.json"}, {"text": "旁白"}, {"text": "时间轴"}, {"text": "逐帧渲染"}, {"text": "混音"}]},
    {"id": "l03", "kind": "outro", "chapter": "99 · 收尾",
     "text": "先定脚本,再定声音,最后才是画面。", "sub": "顺序比努力重要", "big": "顺序比努力重要"}
  ]
}
JSON
#    lines[].text = 朗读文本(自然口语);sub = 上屏字幕(短);kind = hook/body/quote/outro/emphasis/calm
#    画风/配乐/声线/节奏全部由 design 决定,film.json 里**没有** style 字段可写。
#    契约校验(可选,几秒):
python <SKILL_DIR>/scripts/filmcheck.py film.json

# 2) 先自检一次环境(可选;69 条回归护栏,约 10–20 秒,不联网;缺可选模块会标 SKIP)
python <SKILL_DIR>/scripts/selfcheck.py

# 3) 【铁律】先出方案,再出片——同一个 brief 每次都不一样,所以必须让用户看过
python <SKILL_DIR>/scripts/propose.py \
       --brief "AI 大模型的三年,30 秒科普,深蓝" \
       --script film.json --work work
#    产出 3 个候选:各自一份 design + 静帧联系表(work/proposals/rev1/_compare.png)+ 设计距离,
#    2 秒级出结果、全程离线。看并排图,挑一个。

# 4) 确认候选,冻结设计(work/design.json)——此后渲染只认这份设计
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
#    不满意就换一个:--revise "再冷一点,少装饰,换女声"(新 rev,旧 rev 留着眼 diff)
#    想复现某一次:首次提案时加 --seed 42(不传 seed = 每次新 nonce,这是"每片不同"的来源)

# 5) 先看静帧,再出全片;--scenes 不写就用技能内置渲染器
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:用 read_image 亲眼看,别跳过

python <SKILL_DIR>/scripts/render.py --script film.json --work work \
       --out out/film.mp4 --stage all
#    TTS→排版→配乐→音效→混音→逐帧→编码→质检 一条龙;中间产物全在 work/,可断点续跑。

关于 --scenes(零拷贝的关键):render.py 不写 --scenes 时的定位顺序是 ① 剧本同目录的 scenes.py → ② 技能内置 assets/scenes.py。所以默认情况下你什么都不用写、 也不用拷,内置渲染器直接出片;只有想改画面时才在剧本同目录放一个自己的 scenes.py (实现下面的 draw_frame 与可选的 cues()),它会被自动发现——也可以显式 --scenes my_scenes.py。 内置渲染器源码就是最好的范例:<SKILL_DIR>/assets/scenes.py。

# 真实签名(六个参数,少一个都会崩):`scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
    # scene    : 这一段在 timeline 里的字典(id / kind / scene_type / sub / big / items / stats ...)
    # t_local  : 段内时间(秒,0..dur)——入场/持续动效都按它在段内推进
    # dur      : 这一段的总时长(秒)
    # t_global : 全片时间(秒)——跨段的连续动效(镜头缓推、呼吸)用它
    # script   : 整份剧本/计划字典(plan、主题色、words 等;有缓存,别原地改)
    # frame    : 绝对帧号(整数)——需要确定性噪声/相位时用它,别用 time()
    return img   # PIL.Image,必须 1920x1080 RGB(用 K.new_frame() 起手最稳)

写自定义 scenes.py 时先抄 <SKILL_DIR>/assets/scenes.py 的一个绘制器(_draw_hero 之类), 它已经把纯函数、缓存排版、预烘焙底版这些坑都绕过一遍了。

为什么先提案:没有"内置风格"可选,外观由 design.json 决定。 同一个 brief 连拍两次会得到不同设计(配色/母题/转场/配乐/声线都可能不同), 所以流程必须是"生成方案 → 人确认 → 冻结 → 出片";不确认就渲染,render.py 会打提案警告 (加 --require-proposal 则直接失败)。设计契约见 references/proposal.md。

分阶段跑(每个 stage 只做自己的事,落盘后可单独重跑):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style   # 解析已确认的 design,打印主题/视觉语言/版式轮换/字幕样式
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts      # 只合成旁白 + 实测时长(带输入校验的缓存)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout   # 只排时间轴 + 选构图,落 timeline.json / sub.srt / storyboard.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues     # 只出提示点 -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score    # 只出配乐 -> work/music.wav(musicgen 现场编排)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx      # 只出音效轨(sfxkit,按 cues.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio    # 只混音(旁白+配乐+音效,旁白优先)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames   # 只渲染帧(--engine pil|web,--jobs N 分块并行)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux      # 只合成(视频流 copy,两遍 loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa       # 只质检(只读输入,只写 work/qa/)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills   # 只渲母版静帧 + 联系表
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills   # 人工签核一道闸(写 work/signoff.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain  # 只报"哪些阶段过时、为什么、该跑什么",不跑任何 stage

--stage all 的完整顺序:style → tts → layout → cues → score → sfx → audio → frames → mux → qa。 任何一步出问题都会在该步停下,不会带着坏输入继续往下跑。

开渲前先算成本(可选;estimate.py 是仓库工具,按 <REPO> 调用):

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

它给帧数/各阶段耗时/磁盘/分块建议(--json 可机读),是区间上界不是承诺; 有 work/timeline.json 时用真实时长,否则按字数粗估。要省掉全片 VMAF 就在 film.json 写 "qa": {"vmaf": {"mode": "off"}}(estimate 与 render 都读这个字段,G11 记 skipped)。

看交付凭据再签核:

cat out/QC-REPORT.md     # gate matrix 分 pass / fail / skipped 三档;skipped = 本次没执行,不等于通过
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release

签核绑内容指纹:改了剧本 / scenes.py / 设计,旧签核自动失效,要重新看、重新签。 交付前想强制(skipped 也算失败):加 --strict-gates。

长片渲染(分块并行 + 断点续跑):

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

分块落在 work/segments/,按 seg_<起始帧>_<结束帧>.mp4 命名;拼接前会显式检测缺口, 缺块直接报错,不允许"少一段也能出片"。默认 chunk 按 总帧数 / jobs / 4 向整秒取整(jobs=4、 900 帧时约 8 秒一段),可用 film.json 的 chunk_frames 覆盖。

改台词 / 改 scenes.py / 改设计 / 改编码参数之后,--resume 会拒绝复用旧分块(work/segments/_plan.json 里存着计划指纹),并提示你 rm -rf work/segments——这是刻意的:旧分块配新时间轴会拼出 "画面和旁白对不上"的片子,而且时长门禁查不出来。

2. 标准八阶段(P0–P7)

阶段产出关键动作常见错误
P0.5 定设计work/design.json + work/style.jsonpropose.py 出方案 → --confirm 冻结设计;--stage style 只解析已确认的设计并打印主题/视觉语言/版式轮换/字幕跳过 → render.py 报错(没有 design 就没有外观);不确认就渲染会打提案警告
P0 定基准CONVENTIONS.md分辨率/帧率/时长/BPM→帧换算一次算死,全项目引用同一份各文件自己写 150 这种魔数 → 幕边界对不齐
P1 写剧本film.json每句拆 text(朗读)与 sub(字幕);每段一个 kind;可选 lines[].type 锁定构图一个字段两头用 → 字幕太长或读起来生硬;别写 style(不再影响画面,外观由 work/design.json 决定)
P2 事实核查FACTS.md上屏的每个数字/年份有出处编号;建"不能上屏"黑名单跳过这步 → 画面和旁白数字打架,评论区第一条就是你错了
P3 TTSwork/tts/*.mp3 + tts.jsonedge-tts 逐句合成,ffmpeg -f f32le 实测时长,磁盘缓存按字数估时长 → 字幕漂移
P4 排时间轴work/timeline.json + sub.srtdur = lead + 实测语音 + tail,句读必须在本段内段太短 → 旁白被下一段截断
P5 混音work/audio.wavnumpy 总线:旁白 + 配乐 + 音效按采样点叠加ffmpeg 多输入混流 → 非确定性静音
P6 母版3 张静帧先渲 3 帧看构图/配色/字重,定了再渲全片直接渲全片 → 36000 帧后发现字体丑
P7 成片out/film.mp4rawvideo→libx264 CRF18 + 两遍 loudnorm 到 -16 LUFS码率过低/响度 -25 LUFS → "像草稿"

注:filmkit / render.py 的默认是 crf=18; assets/film.json 只是测试夹具(跑通全链与金帧/门禁回归用,不是给你抄的模板),它写的是 17。 自己写剧本时不写 crf 就是 18;追求更锐利的文字可以写 17(体积约 +30%)。

3. 渲染引擎怎么选

场景选型实测速度适合
图文/数据/科普/UI 风PIL(numpy+cv2 辅助) — filmkit 默认40–80 fps @1080p30(示例片 75)绝大多数中文科普片、报告转视频
需要流畅矢量/粒子/3D/纸纹质感无头浏览器 Canvas2D/WebGL + CDP 截图 → image2pipe(webengine.py)80–130 ms/帧(核显,PNG 截图)纪录片级长片、需要复杂视觉语言
纯数据动画manim(若已安装)视场景而定数学/公式推导

上表速度是单机空载实测口径(换机、重负载画面都会变;详见 §12)。别把区间当保证。

设计只覆盖 PIL 引擎(engine: pil,默认):work/design.json 的配色/字体/版式/母题 由 assets/scenes.py 读取。--engine web 的页面由你自己写(assets/web/ 只是能跑通的示例页, 不是出片模板),画面不跟随设计;但配乐与旁白照样跟随 design.json(它们不依赖渲染引擎)。

默认走 PIL:依赖少(numpy+Pillow 就够)、启动快、确定性强、调试成本低。 需要"一镜到底的粒子形变 / 实时着色器质感"才上浏览器方案:

cp -r <SKILL_DIR>/assets/web ./web                     # 页面模板(原生 Canvas2D,零三方库)
pip install playwright && python -m playwright install chromium   # 只在走 web 引擎时需要
python render.py ... --engine web --stage cues        # 从页面导出提示点
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html            # 文字重叠/出画/一闪而过/从没露面

两条路线的取舍(先想清楚再选,换引擎要重做画面):

PIL(engine: pil)浏览器(engine: web)
依赖numpy + Pillow(+cv2 做锐度比)+ playwright + Chromium/Edge
速度40–80 帧/秒8–12 帧/秒(逐帧截图+编码,慢一个数量级)
画面语言位图合成、渐变、几何、文字Canvas2D/WebGL:粒子、形变、着色器、矢量动画
确定性天然(纯函数画图)靠"把时间轴精确 seek 到 f/FPS"保证(window.renderFrame)
调试改一帧跑一帧浏览器里 index.html?dev=1&t=秒 直接看,出了 bug 好查
适合图文/数据/科普/宣传片纪录片级长片、需要一镜到底的视觉语言

浏览器引擎的三条硬约束(webengine.py 已实现,自己写页面时也要守):

  1. 所有补间放在一条暂停的主时间轴上,渲染第 f 帧 = 把它 seek 到 f / FPS 秒;不许依赖播放。
  2. 循环/粒子/波浪这类程序化内容用"按时间直算"的更新器(update(fn)),不许用 setInterval 累加。
  3. 页面必须暴露 window.ready / DURATION / renderFrame(f) / getCues();不连 CDN、不请求网络。

要跨浏览器会话的字节级可复现(崩溃重开、多次渲染做 diff、质检要比对两张图)时开确定性模式: --deterministic 或环境变量 WEBENGINE_DETERMINISTIC=1(关 GPU 光栅,实测 8/8 会话一致, 代价约 +22%/帧)。默认不开:同一会话内本来就一致,跨会话只有 ≤4 像素、±1 级的抗锯齿差异。

4. 画质清单(这几项决定"像不像专业片")

  • 亮度与层次:暗底 (#0a1420) + 顶部亮 accent;不要纯黑纯白,用户屏幕会吃掉渐变。
  • 字重与字号:大字 84–96px、副标题 34–38px、字幕 46px;中文用 msyhbd.ttc(微软雅黑粗体)。
  • 字幕:≤ 2 行、单行约 24 个中文字、带半透明背板、y ≥ 872。 实际折行看的是像素宽度(fit_sub_lines 按 max_w=1560 px 自动折),24 字是"大致上限"并非硬校验 ——SUB_MAX_CHARS=24 这个常量目前没有被任何代码引用,别指望它拦你。
  • 节奏:每 3–6 秒要有视觉变化(新卡片 / 位移 / 数字滚动);静止超过 6 秒观众就走。
  • 镜头:只做 3–15% 缓推,不要大幅拉远拉近。
  • 调色:整片一个主色 + 一个强调色,别超过 4 个色相。
  • 音频:母带 -16 LUFS / 真峰 ≤ -1 dBTP;旁白比间隙响 12 dB 以上;AAC 前留 1.4 dB 余量。

5. 声音:配乐 + 音效 + 提示点

这一节的思路来自一部真实成片(ai-history-film)的复盘:声音不好不是"混得不够响",而是"声音不认识画面"。 解决顺序永远是:先让声音知道画面上发生了什么(提示点),再谈音色,最后才是母带。

5.1 三层声音

层谁做落盘说明
旁白filmkit.tts(edge-tts)work/tts/*.mp3唯一的时长权威:时间轴由它反推
配乐musicgen.py(参数化编排)work/music.wav按设计里的 bpm/key/scale/mood/seed + 编排参数现场生成;没有现成曲风可挑
音效sfxkit.py(物理建模)work/sfx.wav按 cues.json 里登记的提示点逐条合成

三条总线在 --stage audio 里合成:音效响起时配乐自动让位(duck_db 默认 4 dB, cinema/epic 5.5 dB——3 dB 的让位量在段落过门时配乐几乎与音效等高,听感会糊成一团), 最后交给 ffmpeg 两遍 loudnorm 到 -16 LUFS / 真峰 ≤ -1 dBTP。不要自己在 numpy 里做最终响度标准化—— 你有 loudnorm,它比手写归一化准得多,而且真峰才是门禁。

片内分离:配乐在段落交界本来就有过门(编排器写的 swell / riser / sub_boom), sfx 轨不在同一个边界重复放 whoosh/riser——--stage sfx 会对落在 [边界 - 2.2 s, 边界] 内的转场类 cue 自动 -3 dB(sfx.duck_transitions=true,默认)。 电影级长片建议提成 duck_transitions=4.5 以避免过门堆积。

5.2 提示点(让声音"看见"画面)

# 在自定义 scenes.py 的 cues() 里登记
from filmkit import CueBus

def cues():
    bus = CueBus()
    bus.add(t=2.5, kind="sfx:transition", meta={"type": "whoosh"})
    bus.add(t=8.0, kind="sfx:impact", meta={"intensity": 0.7})
    bus.add(t=15.0, kind="sfx:ambient", meta={"loop": True, "src": "wind"})
    # 提示点自带时间戳(相对于段起点),`--stage layout` 会转成全局时间
    return bus.cues

提示点不会自动生成——你得告诉音效师(sfxkit.py)什么地方该响。 CueBus 生成的 cues.json 会被 --stage sfx 读取。

提示点类型:

kind说明音效素材
sfx:transition段落转场whoosh / swoosh / riser
sfx:impact强调 / 重音鼓 / 金属 / 合成器 hit
sfx:ambient环境背景风 / 城市 / 太空低频
sfx:stinger章节开场 / 重要时刻短促标志音
music:beat配乐对齐点(节拍器)无(仅元数据)

5.3 配乐编排参数

musicgen.py 通过 work/design.json 读取以下编排参数:

{
  "music": {
    "bpm": 90,
    "key": "C",
    "scale": "minor",
    "mood": "cinematic",
    "instruments": ["synth_pad", "orchestral_hits", "sub_bass"],
    "structure": "intro-verse-chorus-verse-outro",
    "transition_riser": true,
    "density": 0.6
  }
}

没有"预置曲风"——musicgen.py 的参数决定了配乐的性格。 参数组合是无限的,不要问"有没有欢快的",要问"bpm 多快、什么调、什么 mood"。 如果 musicgen.py 因为缺少依赖(如 spleeter)回退到无配乐, --stage score 会输出静音文件并告警,但不阻断流程。

6. 质检门禁(G1–G12)

--stage qa 自动跑以下 12 道闸。每道闸输出 pass / fail / skipped。 --stage accept --gate release 要求所有 gate 为 pass,否则拒绝签核。

编号闸名检查内容失败时
G1tts_duration每句 TTS 时长 > 0,全片旁白总长 < 片长 × 1.15提示重新运行 --stage tts
G2timeline_fit每段时长 >= 该段 TTS 时长 + 0.5s提示手动调整 hold
G3sub_length字幕最多 2 行,每行 < 1560 px自动缩减或告警
G4sub_safearea字幕 y >= 872--stage layout 重排
G5big_safearea大字 y ∈ [58, 974]--stage layout 重排
G6frame_numbers渲染帧数 = 预期帧数(typ: 900 帧)提示重跑 --stage frames
G7frame_continuity相邻帧 PSNR < 99%(过静检测)提示检查 scenes.py 动效
G8audio_loudness母带 -16 ±1 LUFS,真峰 ≤ -1 dBTP提示重新 loudnorm
G9audio_phase旁白与字幕时间戳偏移 < 100 ms提示重跑 --stage audio
G10video_bitrate码率 >= 8 Mbps @1080p提示降低 CRF
G11vmafVMAF >= 85 @ 源帧(可选,qa.vmaf.mode 默认 auto)提示重渲或调 CRF
G12duration_gate全片时长 ±3% 预期提示检查 timeline

G11(VMAF)默认只在 --stage all 时运行;--stage qa 时可加 --run-vmaf 强制。 要关闭 VMAF,在 film.json 写 "qa": {"vmaf": {"mode": "off"}}。

7. 故障排除

7.1 常见问题

问题原因解决
字幕漂移时间轴按字数估算rm -rf work/tts work/timeline.json 重跑 --stage tts --stage layout
画面和旁白对不上--resume 用了旧分块rm -rf work/segments 重跑 --stage frames
音效没声音提示点没登记 / cues.json 为空检查 scenes.py 的 cues() 是否返回非空列表
配乐太响/太轻duck_db 不合适在 design.json 的 music 段调 duck_db
字体显示为方块字体文件没装fc-list :lang=zh 确认,或 pip install msyhbd.ttc(需手动)
浏览器引擎很慢逐帧截图天然慢考虑换 PIL 引擎,或减少帧数 / 调高 --deterministic(关 GPU)
lmxl 缺少扩展系统 libxml2 版本过低sudo apt install libxml2-dev 或 sudo ln -sf /usr/lib/x86_64-linux-gnu/libxml2.so.2 /usr/lib/x86_64-linux-gnu/libxml2.so

7.2 日志与调试

# 每个 stage 在 work/ 下有自己的日志文件
ls work/logs/
# work/logs/tts.log
# work/logs/layout.log
# work/logs/frames.log

# 全局调试开关
export VIDEO_STUDIO_DEBUG=1
python render.py ... --stage all   # 控制台输出更多细节

# 想看某一帧渲染了什么
export VIDEO_STUDIO_FRAME_DEBUG=1
python render.py ... --stage frames   # 每帧输出渲染时间

7.3 参考文件

文件路径说明
设计契约references/proposal.md设计提案/确认/revise 的详细协议
设计 Schemaschemas/design.schema.jsonwork/design.json 的完整 JSON Schema
剧本 Schemaschemas/script.schema.jsonfilm.json 的完整 JSON Schema
内置场景渲染assets/scenes.py默认渲染器,也是写自定义 scenes.py 的模板
Web 示例页assets/web/浏览器引擎的示例页面(原生 Canvas2D,零三方库)
金帧测试tests/golden/回归测试用的已知正确帧
门禁测试tests/gates/每道闸的单元测试

附录 A:环境变量速查

变量默认说明
VIDEO_STUDIO_LEDGER~/.dsh/video-studio/design-ledger.json片间去重台账路径;设 off 关闭
WEBENGINE_DETERMINISTIC0浏览器引擎确定性模式(关 GPU 光栅)
VIDEO_STUDIO_DEBUG0全局调试输出
VIDEO_STUDIO_FRAME_DEBUG0逐帧渲染时间输出
VIDEO_STUDIO_NO_LOCK0不创建文件锁(默认锁定 work/ 防并发)
VIDEO_STUDIO_TTS_CACHE~/.cache/video-studio/ttsTTS 文件缓存目录
VIDEO_STUDIO_MUSIC_CACHE~/.cache/video-studio/music配乐缓存目录

附录 B:关于 film.json 的完整字段

建议用 filmmkit.FilmScript 构造,或参考完整 Schema schemas/script.schema.json。 以下只列常用字段:

字段类型必填说明
titlestring是片名(用于日志/文件名/元数据)
linesarray[Line]是台词数组;每句一条
lines[].idstring是唯一 ID(如 l01)
lines[].kindenum是hook / body / quote / outro / emphasis / calm
lines[].chapterstring否章节标题(如 "01 · 开场")
lines[].textstring是朗读文本(自然口语)
lines[].substring否上屏字幕(短句)
lines[].bigstring否大字标题
lines[].kickerstring否段首小标签(如 "PIPELINE")
lines[].itemsarray[Item]否要点列表
lines[].typeenum否强制构图类型(hero / split / center / list)
lines[].holdfloat否该句语音结束后额外停留秒数
holdfloat否全局每句后额外停留秒数(会被 lines[].hold 覆盖)
qaobject否质检配置(如 {"vmaf": {"mode": "off"}})
chunk_framesint否并行渲染时的分块帧数(覆盖自动计算)

Verwandte Skills