ビデオスタジオ (video-studio)
一言で:動画 = 脚本 + 音声実測時間 + 決定論的フレーム関数 + ffmpeg エンコード + 自動品質検査。 画像と音声のすべてをコードで生成。素材ゼロ、再現可能、途中再開可能、スクリプト検収可能。
0. 絶対ルール(一つでも破ると完成品は必ず問題を起こす)
- 音声を先に、画像は後で。タイムラインはTTS実測時間から逆算し、決して文字数で推定しない。文字数で組んだタイムラインは20文目から明らかにずれ始める。
- 各フレームは純粋関数:
draw(t, frame) -> Image。random/time()は禁止。乱数が必要な場合はfilmkit.hash2/hash3/vnoiseを使用。同一フレーム番号は常に同じ画像を描画する。 - 字幕・大文字はセーフゾーン内に収める:本文
y ∈ [58, 974]、字幕はy ∈ [872, 1030]、1030を超えてはならない。 - 生フレームを直接エンコード:PILフレーム →
rawvideoパイプ →libx264。cv2.VideoWriterで中間ファイルを作らない(2倍遅く、2回目の劣化が生じる)。 - ミキシングはnumpyのサンプルレベル加算、ffmpegコマンドに数百の入力を与えない(Windowsでは非決定論的な無音になる)。
- 納品前に必ず品質検査スクリプトを実行し、3枚の静止画を実際に見る(
read_image使用)。「画像が出る」は「問題ない」と同義ではない。 - 音は映像に従う:効果音が必要な場所はシーンコードにキューを登録(
K.CueBus/scenes.pyのcues()フック)。後から手動で時間を合わせない。映像を変更(停留ウィンドウの変更、リズム調整)すると、キューは自動的に追随する。 - はっきり見たいなら停留を追加し、動画全体をスローにしない:
film.jsonのhold(グローバル)またはlines[].hold(一文単位)で音声終了後に停留を追加。BGMは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は保持され差分確認可能 |
| 「どれもダメ、やり直し」 | 同上。要件を具体的に記述(--revise のテキストは proposal.feedback に保存) |
| 「あなたが決めて」と明確に言われた | それでもまず企画案を提示、あなたが推奨する案を選び理由を説明し、ユーザーの承認を得る |
ハード制約:
propose.py --confirmの前に、render.pyのframes/allは企画ゲートでブロックされる(デフォルト警告、--require-proposalの場合は失敗)。- 候補は互いに ≥2 つのデザイン次元で異なる(色相ファミリー/モチーフ/トランジション/イージング/ナンバリング/BGM/声質)。色だけの変更は複数案と見なさず、
diversity.vs[].n < 2の場合は警告表示される。 - 各候補には日本語のコンセプト(
pitch)が必要。「案A/案B」ではなく、「この案でどのような表現を目指すか」を明確に説明する。 - 組み込みテンプレートは存在しない:
design.jsonの全フィールドが開放されている(35フィールドの主題 + 5組のビジュアル言語シグネチャ + 音声/BGM/リズム)、配色は連続色相空間内の任意の点であり、数組の既成ルックから選ぶのではない。 毎回の提案はその場で生成される。同じbriefを2回連続で実行すると異なるデザインが得られる(再現するには--seedを指定)。 - 作品間重複排除台帳(デフォルト有効):
--confirmにより凍結されたデザインは~/.dsh/video-studio/design-ledger.jsonに記録される(最大500件)。次回の提案では直近に納品されたルックを積極的に回避する。 候補と台帳内のいずれかの作品とのデザイン距離 < 2次元の場合、自動的にnonceを変更して再サンプリングし警告を表示する。 これは「作品ごとに異なる見た目」を「ランダム頼み」から「記憶あり」に変える仕組み。--show-ledgerで台帳を確認、--no-ledgerで今回のみ無視。環境変数VIDEO_STUDIO_LEDGERでパス変更またはoffで無効化。 台帳の読み書き失敗は常に空のテーブルとして扱い、決して動画出力をブロックしない。
企画をスキップできる唯一のケース:純粋な修正 / 純粋な再レンダリング(内容もデザインも変更せず、音声同期ずれの修正や再出力のみ)。
この場合、--require-proposal 以外の方法で実行し、返信で「今回は企画をスキップしました。理由は…」と説明する。
0.5.1 ルックの出所:2つの道のみ
「組み込みスタイル」は選択できません。 film.json に style 値(auto / legacy / 特定のID)を書くと、
明確に拒否され、2ステップのガイダンスが表示される。脚本はルックを決定せず、ルックは work/design.json が決定する。
同様に、スキル内に THEMES/STYLES/VISUAL_LANGUAGES テーブルや presets/ は存在しない。
BGMは musicgen.py がパラメータに基づいてその場で編成し、scenes.draw_frame にレガシーブランチもない。
特定のルックを得るには2つの道のみ:
# ① 自然言語で要求(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. 5ステップで開始(コピーゼロ:スクリプトもテンプレートもコピーしない)
このパスには「コピー」動作が一切ありません。 スキルスクリプトはスキルディレクトリに残したまま直接実行します。 プロジェクトディレクトリには
film.jsonと(実行によって生成されるwork/とout/)のみが必要です。
<SKILL_DIR>は、今回スキルをロードした際にskillツールが返すresourceBase.pathを指します。プラグインとしてインストールすると、 プラグインパッケージ内のskills/video-studio/になります(プロファイルのnode_modules/@dsh-gis/dsh-video-studio-skillはリンク)。 見つからない場合は名前で特定:find ~ -path '*/skills/video-studio/scripts/filmkit.py'。
<REPO>はパッケージルート(リポジトリルート、またはインストール後のプロファイル内のnode_modules/@dsh-gis/dsh-video-studio-skill/)を指し、 つまり<SKILL_DIR>の2階層上です。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": "パイプライン",
"text": "パイプライン全体はたったの5ステップ:脚本作成、ナレーション生成、タイムライン配置、フレームレンダリング、ミキシング合成。",
"sub": "5ステップで完成、各ステップで保存", "big": "5ステップで完成",
"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
# 画風/BGM/声質/リズムはすべて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大規模言語モデルの3年間、30秒解説、ダークブルー" \
--script film.json --work work
# 3つの候補を生成:各々デザイン + 静止画コンタクトシート(work/proposals/rev1/_compare.png)+ デザイン距離、
# 2秒で結果出力、完全オフライン。比較画像を見て、一つを選択。
# 4) 候補を確認、デザインを凍結(work/design.json)——以降のレンダリングはこのデザインのみ認識
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
# 不満なら別の候補に:--revise "もっと寒色系に、装飾は少なく、女性の声に"(新しいrev、古いrevは差分確認用に保持)
# 特定の結果を再現したい場合:初回提案時に --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→レイアウト→BGM→効果音→ミキシング→フレーム→エンコード→品質検査 の一貫処理。中間成果物はすべて work/ に保存、途中再開可能。
--scenes について(コピーゼロの鍵):render.py で --scenes を指定しない場合の検索順序は
① 脚本と同じディレクトリの scenes.py → ② スキル内蔵 assets/scenes.py。よってデフォルトでは何も書かず、
コピーも不要。内蔵レンダラーがそのまま動画出力する。映像を変更したい場合のみ、脚本と同じディレクトリに独自の scenes.py
(後述の draw_frame とオプションの cues() を実装)を配置すれば、自動的に発見される。明示的に --scenes my_scenes.py と指定することも可能。
内蔵レンダラーのソースコードが最良のサンプル:<SKILL_DIR>/assets/scenes.py。
# 実際のシグネチャ(6引数、一つ欠けてもクラッシュ):`scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
# scene : タイムライン上のこの区間の辞書(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を2回連続実行すると異なるデザインが得られる(配色/モチーフ/トランジション/BGM/声質が異なる可能性あり)。
そのためフローは「企画生成 → 人間が確認 → 凍結 → 動画出力」でなければならない。確認せずにレンダリングすると、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 # BGMを出力 -> 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 # ミキシング(ナレーション+BGM+効果音、ナレーション優先)
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 # 合成(ビデオストリームコピー、2パス 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 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 # ゲートマトリクスは pass / fail / skipped の3段階;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の命名。結合前に明示的に欠損を検出し、 欠落ブロックがあれば直接エラー。デフォルトのチャンクは総フレーム数 / jobs / 4を整数秒に丸めた値(jobs=4、 900フレームで約8秒)。film.jsonのchunk_framesで上書き可能。台詞 / scenes.py / デザイン / エンコードパラメータを変更した後、
--resumeは古い分割ブロックの再利用を拒否する(work/segments/_plan.jsonに保存された計画フィンガープリントを確認)。rm -rf work/segmentsを促す。これは意図的な動作:古い分割ブロックと新しいタイムラインを組み合わせると 「映像とナレーションが合わない」作品になり、時間ゲートでも検出できない。
2. 標準8ステージ(P0–P7)
| ステージ | 成果物 | 主要アクション | よくあるエラー |
|---|---|---|---|
| P0.5 デザイン決定 | work/design.json + work/style.json | propose.py で企画生成 → --confirm でデザイン凍結;--stage style は確認済みデザインのみを解析しテーマ/ビジュアル言語/レイアウトローテーション/字幕を出力 | スキップ → render.py がエラー(designなしでは外観がない);確認せずにレンダリングすると企画警告が出力 |
| P0 基準設定 | CONVENTIONS.md | 解像度/フレームレート/時間/BPM→フレーム換算を一度に確定、全プロジェクトで同一のものを参照 | 各ファイルで独自にマジックナンバーを記述 → 幕境界が揃わない |
| P1 脚本作成 | film.json | 各行を text(読み上げ)と sub(字幕)に分割;各段落に kind;オプションで lines[].type で構図を固定 | 一つのフィールドを2つの用途に使う → 字幕が長すぎるか読みにくい;style を書かない(外観に影響しなくなり、ルックは work/design.json が決定) |
| P2 事実確認 | FACTS.md | 画面に表示するすべての数字・年号に出典番号を付記;「表示不可」ブラックリストを作成 | このステップをスキップ → 映像とナレーションで数字が矛盾し、コメントで最初に指摘される |
| P3 TTS | work/tts/*.mp3 + tts.json | edge-tts で一文ずつ合成、ffmpeg -f f32le で実測時間、ディスクキャッシュ | 文字数で時間を推定 → 字幕がずれる |
| P4 タイムライン配置 | work/timeline.json + sub.srt | dur = lead + 実測音声 + tail、句読点は当該段落内に収める | 段落が短すぎる → ナレーションが次の段落に途切れる |
| P5 ミキシング | work/audio.wav | numpyバス:ナレーション + BGM + 効果音をサンプル単位で加算 | ffmpeg複数入力ミキシング → 非決定論的な無音 |
| P6 マスター | 3枚の静止画 | まず3フレームをレンダリングして構図/配色/フォントウェイトを確認、確定後に全フレームをレンダリング | 直接全フレームをレンダリング → 36000フレーム後にフォントが汚いと判明 |
| P7 完成品 | out/film.mp4 | rawvideo→libx264 CRF18 + 2パス 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/フレーム(内蔵GPU、PNGスクリーンショット) | ドキュメンタリー級長編、複雑なビジュアル言語が必要 |
| 純データアニメーション | manim(インストール済みの場合) | シナリオによる | 数学/数式導出 |
上記の速度は単一マシン、無負荷での実測値(マシンや重い描画内容で変化。詳細は§12を参照)。区間を保証と見なさないこと。
デザインはPILエンジンのみをカバー(
engine: pil、デフォルト):work/design.jsonの配色/フォント/レイアウト/モチーフ はassets/scenes.pyが読み取る。--engine webのページはあなた自身が記述する(assets/web/は動作するサンプルページであり、 動画出力用テンプレートではない)。画面はデザインに従わないが、BGMとナレーションは同様に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 # 文字重なり/画面からのはみ出し/一瞬で消える/一度も表示されない
2つのルートのトレードオフ(よく考えてから選択、エンジン変更は画面の作り直しが必要):
PIL(engine: pil) | ブラウザ(engine: web) | |
|---|---|---|
| 依存関係 | numpy + Pillow(+cv2でシャープネス比較) | + playwright + Chromium/Edge |
| 速度 | 40–80 フレーム/秒 | 8–12 フレーム/秒(フレームごとにスクリーンショット+エンコード、一桁遅い) |
| 画面言語 | ビットマップ合成、グラデーション、幾何図形、テキスト | Canvas2D/WebGL:パーティクル、変形、シェーダー、ベクターアニメーション |
| 決定性 | 天然(純粋関数で描画) | window.renderFrame でタイムラインを正確に f/FPS にシークすることで保証 |
| デバッグ | 1フレーム書き換えて実行 | ブラウザで index.html?dev=1&t=秒 を直接確認、バグ発見が容易 |
| 適性 | 図版/データ/解説/プロモーション動画 | ドキュメンタリー級長編、ワンカットのビジュアル言語が必要 |
ブラウザエンジンの3つのハード制約(webengine.py は実装済み、自身でページを記述する場合も遵守):
- すべてのトゥイーンは一時停止されたメインタイムライン上に配置。第fフレームのレンダリング = それを
f / FPS秒の位置にシーク。再生に依存してはならない。 - ループ/パーティクル/ウェーブのようなプログラム的コンテンツは「時間から直接計算」するアップデータ(
update(fn))を使用し、setIntervalでの累積は禁止。 - ページは
window.ready / DURATION / renderFrame(f) / getCues()を公開しなければならない。CDNに接続せず、ネットワーク要求も行わない。
ブラウザセッションを超えたバイトレベルの再現性(クラッシュ再開、複数回のレンダリングでdiffを取る、品質検査で2枚の画像を比較する)が必要な場合は、決定性モードを有効化:
--deterministic または環境変数 WEBENGINE_DETERMINISTIC=1(GPUラスタライズを無効化、実測で8/8セッション一致、
代償は約+22%/フレーム)。デフォルトはオフ:同一セッション内では元々一致し、セッションを超えると最大4ピクセル、±1レベルのアンチエイリアス差のみ。
4. 画質チェックリスト(これらが「プロっぽいか」を決定づける)
- 輝度と階調:暗めの背景 (
#0a1420) + 上部に明るいアクセント。純黒/純白は避ける。ユーザーの画面でグラデーションが潰れる。 - フォントウェイトとサイズ:大文字84–96px、サブタイトル34–38px、字幕46px。日本語は
msyhbd.ttc(Microsoft YaHei Bold)。 - 字幕:≤ 2行、1行約24字、半透明背景付き、
y ≥ 872。 実際の折り返しはピクセル幅(fit_sub_linesはmax_w=1560 pxで自動折り返し)に基づく。24字は「おおよその上限」であり、ハードチェックではない。SUB_MAX_CHARS=24という定数は現在どのコードからも参照されていないので、これに頼ってはいけない。 - リズム:3–6秒ごとに視覚的変化が必要(新しいカード / 移動 / 数字スクロール)。6秒以上静止すると視聴者は離れる。
- カメラ:3–15%のスロープッシュのみ。大幅なズームイン/アウトはしない。
- カラーグレーディング:動画全体で1つのメインカラー + 1つのアクセントカラー。色相は4つまで。
- オーディオ:マスター -16 LUFS / トゥルーピーク ≤ -1 dBTP。ナレーションは隙間より12 dB以上大きい。AACエンコード前に1.4 dBのヘッドルームを確保。
5. 音声:BGM + 効果音 + キュー
このセクションの考え方は、実際の完成作品(
ai-history-film)の振り返りに基づく:音が悪いのは「ミックスが足りないから」ではなく、「音が映像を認識していないから」。 解決の順序は常に:まず音に画面上で何が起きているかを知らせる(キュー)、次に音色、最後にマスタリング。
5.1 3層の音声
| 層 | 担当 | 保存先 | 説明 |
|---|---|---|---|
| ナレーション | filmkit.tts(edge-tts) | work/tts/*.mp3 | 唯一の時間基準:タイムラインはこれから逆算 |
| BGM | musicgen.py(パラメータ化編成) | work/music.wav | デザインのbpm/key/scale/mood/seed + 編成パラメータに基づきその場で生成。既成の曲調は選択不可 |
| 効果音 | sfxkit.py(物理モデリング) | work/sfx.wav | cues.json に登録されたキューに従い、1つずつ合成 |
3つのバスは --stage audio で合成:効果音が鳴るときはBGMが自動的に後退(duck_db デフォルト 4 dB、
cinema/epic 5.5 dB——3 dBの後退量では、段落の変わり目でBGMが効果音とほぼ同じ音量になり、聴感上ごちゃつく)。
最終的にffmpegの2パス loudnorm で -16 LUFS / トゥルーピーク ≤ -1 dBTP に調整。numpyで最終ラウドネス正規化を自分で行わないこと。
loudnorm は手動正規化よりはるかに正確であり、トゥルーピークがゲート条件となるため。
作品内分離:BGMは段落の変わり目に元々トランジションがある(オーケストレーターが記述する swell / riser / sub_boom)。 sfxトラックは同じ境界で whoosh/riser を重複して再生しない。
--stage sfxは [境界 - 2.2 s, 境界] 内に位置するトランジション系のcueに対して自動的に -3 dB する(sfx.duck_transitions=true)。
5.2 キューBus(K.CueBus)
キューはシーンコードから登録する。各 scenes.py は K.CueBus.cue() を呼び出し、cues.json に構造化されたcueを書き込める。
--stage sfx は cues.json から効果音トラックを生成する。sfx/null/空のキューは無音を意味しない。
# scenes.py で cue を出す例
from filmkit import K
# scene 辞書の上で、キューを登録
K.CueBus.cue(t_global, # キューが発生する絶対時間(秒)
kind="whoosh", # 効果音の種類(sfxkit.py が定義するマッピングに従う)
layer="transition", # レイヤー(transition / highlight / ambient)
intensity=0.7) # 強度 0-1
正しいキューの書き込みは --stage sfx 時の sfxkit にとって必須。キューが不正確または不足していると効果音が合わない。
間違ったキューや不要なノイズを生成しないため(sfxkitのバリデーションが無効なキューを拒否)。
6. 品質検査ゲート(QC)
--stage qa は自動検査を実行し、work/qa/qa-report.json(機械可読)と work/qa/qa-report.md(人間可読)を生成する。
検査する項目:
| ゲート | チェック内容 | 失敗条件 |
|---|---|---|
| VMAF | libvmaf でマルチスケール構造類似性をスコアリング | スコア < 80(設定可能、film.json の qa.vmaf.min_score) |
| PSNR | ピーク信号対雑音比 | < 35 dB(film.json の qa.psnr.min_db、デフォルト35) |
| ラウドネス | 統合ラウドネス -16 LUFS ± 1 LU | 範囲外 |
| トゥルーピーク | トゥルーピーク ≤ -1 dBTP | 超過 |
| 字幕範囲 | 字幕 y 座標すべてが [872, 1030] 内 | 範囲外字幕発見 |
| フレーム完結性 | 合計フレーム数 = 期待値 | フレーム数不一致 |
| 音声同期 | ナレーション波形とタイムラインのオフセット | オフセット > 1フレーム |
| コンテンツフィンガープリント | outputファイルのSHA256が期待値と一致 | 改変検出 |
ゲート結果は QC-REPORT.md に集約。すべてのゲートがpass/fail/skipped状態で表示。
--strict-gates を使用すると skipped は失敗と見なされる。
7. トラブルシューティングとよくある問題
7.1 レンダリングが遅い、またはフレームレートが低い
--engine pilを使用しているか確認。--jobs Nで並列レンダリング。デフォルトはCPUコア数。film.jsonのchunk_framesでチャンクサイズを調整。大きすぎるとメモリ不足、小さすぎるとオーバーヘッド増加。--stage framesのみを実行し、他のステージをスキップ。- 複雑な
scenes.py描画関数:各フレームで重い計算を避ける。
7.2 タイムラインがずれる、または字幕とナレーションが合わない
常に --stage tts から始め、実測時間を確認。
--stage layout の出力 work/timeline.json を確認。各行の dur が正しいか。
--resume 時のブロック再利用は、work/segments/_plan.json のフィンガープリントが一致する場合のみ許可。
7.3 ffmpegエラー
ffmpegがパスにあり、実行可能か確認。rawvideoパイプの設定を確認:-f rawvideo -pix_fmt rgb24 -s 1920x1080。- ffmpegのバージョンと利用可能なコーデックを確認。
8. スクリプトリファレンス
<SKILL_DIR>/scripts/ のスクリプト:
| スクリプト | 機能 |
|---|---|
propose.py | デザイン企画を生成 |
render.py | 全レンダリングパイプライン |
filmcheck.py | film.json のスキーマ検証 |
selfcheck.py | 環境依存関係のチェック |
filmkit.py | コアライブラリ(決定論的描画、TTS、タイムライン) |
9. ライセンスと帰属
このスキルはオープンソースです。MIT ライセンスの下で提供されます。
10. 変更履歴
v1.0.0
- 初版リリース