会話動画用字幕
概要
キャプションは独立したレイヤーです。話されたすべての単語を担い、フレーム内の描画に依存しません。 フレーム内の物体のラベル(部品名、賛否、矢印など)は別のレイヤーであり、異なる視覚言語(オブジェクトの横に配置されるコールアウト)を使用します。「画面上に既に表示されているから」という理由でキャプションの単語を削ってはいけません。
パイプライン: words.json + forbid.json(キャプションが覆ってはいけない領域) + config.json + プロファイル
→ plan → manifest.json(正規データ) → burn(焼き込み済みプレビュー=視覚的な真実)/ export srt|ass|vtt → validate。
各フォーマットの役割(混同しないこと):
| フォーマット | 担うもの | 担わないもの |
|---|---|---|
manifest.json | 単語、タイミング、レイアウト、placement、許容された重なり、リフトとその理由、レンダリングチェック:データの信頼できる唯一の情報源 | ピクセル |
.srt | テキスト + タイミング | ウェイト、色、アウトライン、アクティブワード、位置。SRTではスタイルを約束してはいけません |
.ass | ファミリー + Bold/\b700、libassメトリクスに変換されたサイズ、アウトライン、バッキングボックス、タイムスライスごとのアクティブワード、位置 | フェード;フォントが必要: subtitles=x.ass:fontsdir=fonts |
.vtt | ::cue CSS(ファミリー、ウェイト、text-shadowによるアウトライン、バッキング)+ タイムスライスごとのクラス .active_<spk> / .em_<spk> / .dim / .u | 正確な位置;ページは@font-faceでフォントをロードする必要あり |
焼き込み済み preview.mp4 | すべて:唯一の視覚的な真実の情報源 | — |
使用するタイミング
- 会話スキット、インタビュー、ナレーター付きの解説動画:縦または正方形、最大2行。
- 単語タイミングがある(または音声がある:環境変数キーによるオプションのクラウドSTT)場合、顔/手/主要プロップの位置(ボックスで指定;3Dシーンの場合はフラットカラーのマットをレンダリングしてボックス化する)が分かっている場合。
- 歌詞動画には使用しない(バウンス、スケーリング単語は意図的にサポートされていません)。
アルゴリズム
セグメンテーション(プロファイル内のchunk):pause_s(0.35秒)より長いポーズ、文の終わり、話者交代でハードブレーク;チャンクが2語以上になった場合のカンマでソフトブレーク;max_words、ピクセル幅(セーフゾーンのmax_width_frac)、max_dur_s(約1.2秒の音声が読みやすい)で制限。チャンクが機能語(не、в、а、and、the…)で終わってはいけません:次のチャンクに移動します。孤立した単語は、同じ文の隣接単語と結合できる場合は結合します。単語の継続時間は中央値の2.5倍にクランプされるため、単語がポーズにまで残ることはありません。タイミング:単位は最初の単語の50ミリ秒前までに表示され、直前の単語を決して切らず、最後の単語の後にhold_s秒間留まり、少なくともmin_show_s秒間表示されます。
配置 — placement: {mode: fixed_bottom_center, collision_lift: false}(対話のデフォルト)
- 動画全体で1つのベースライン:
baseline = safe.bottom - baseline_margin_px(または明示的に設定)。すべての単位はその上に下端揃えされ(y + h = baseline)、フレームの中央に配置されます(x + w/2 = W/2)。何も移動しません:リフトなし、横ずれなし、話者ごとのx移動なし。行幅は、中央揃えされた行がセーフゾーン内に収まるように制限されます。 - 禁止領域とベースライン上で交差する行はそのまま配置されます:単位には
overlap_accepted: [クラス]が記録され、QAシートには「重複許容(固定ベースライン)」として黄色い枠が描画されます。情報であり、失敗ではありません。プラットフォームUIとの交差は依然としてエラーです(フォントを縮小するか、フレーズを分割してください)。 - オプション
collision_lift: true(モードbaseline_lift):話者の下にxを配置(align: speaker)、または中央揃え+左右フォールバック;すべてのxで衝突する場合は、lift_step_pxステップずつ持ち上げて空き領域が見つかるまで繰り返します。単位にはlift = {px, reason: [ベースラインで衝突したクラス], t0, t1}が記録され、マニフェストにはすべてのリフトがcollision_liftsにリストされます。理由なし → リフトなし。顔を絶対に隠さないようにするよう指示された場合にのみ使用します。 - 単語を決して削除しない。
見た目。アウトライン(outline_px)+ ソフトシャドウ + 背景の背後にある最も暗い色相が4.5:1(WCAG)以上を維持するために必要な分だけアルファが引き上げられた明るいバッキング。話者はアクティブワードの色で区別され、ハイブリッド/カードモードでは話者色の4pxのバーが追加されます。吹き出しや重いネームプレートはありません。落ち着いた動き:アルファフェードのみ、スケールポップなし、垂直スライドなし。
QA(scripts/validate.py、demoでも実行):
- スキーマ:
templates/manifest.schema.jsonに対するマニフェストの検証; - 行数 ≤ 2;
- 配置:
fixed_bottom_centerではすべての単位のy-ベースラインとx-センターのばらつきが0(許容差0.5px)であり、リフトがないこと;baseline_liftではすべてのリフトがグリッド上にあり、理由が存在し、ベースラインで実際に衝突していること; - すべてのフレームでプラットフォームUIとの重複がゼロ;禁止領域:
baseline_liftでは0、fixed_bottom_centerではoverlap_accepted(情報)として報告; - すべての単語が正確に1回、順序通り、話されている間に表示される;
- コントラスト ≥ 4.5、バウンスなし(両方とも
burnでフレームごとに測定); - フォントが解決される:ファイルが存在し、Pillowのファミリーとスタイル、OS/2ウェイト、
fc-scan、libassのfontselectが宣言されたファイルとウェイトを選択すること; - ピクセル:焼き込みフレーム上とASSのlibassレンダリングの両方で、アクティブカラーが存在し、グリフの下端がレイアウトから3px以内、ステム幅が宣言されたウェイトの参照ラスターから1px以内、アウトラインが
outline_pxから1px以内であること。
プロファイル (profiles/*.json)
| プロファイル | 説明 | 最適な用途 |
|---|---|---|
karaoke | 一度に2〜4語、話し中の単語を話者色で表示、残りは暗く | 最も安定;固定レーン、説明動画 |
reveal | 単語が音声に合わせてフェードイン;レイアウトは事前に固定;≤ 2行 | 落ち着いたナレーション |
cards | カードあたり1つの思考(≤ 8語、2行)、密集したパネル、キーワードに色付け | ゆっくりとした、引用に適した行;音声より先に読む |
kinetic | 一度に1トークン、大きめのタイプ | 決め台詞のみ;文脈はほとんどなし |
hybrid(デフォルト) | ≤ 3語、単語がフェードイン、話し中の単語に下線、強調色、話者バー | 対話 |
R&Dシステムからのオプション:テンポ = chunk.max_dur_s: 1.2 + timing.min_show_s: 0.83(hybridではオン);ストレス = emphasis: "energy"(--audioからの話者ごとの単語ラウドネスzスコア)、またはkeywordsリスト付きの"keyword"。
コマンド
python -m subkit demo examples/fixture # 5プロファイル + hybrid_lift -> プレビュー、srt/ass/vtt、validation_report.{md,json}
python -m subkit plan --words W --forbid F --config C [--profile hybrid] [--audio A] -o manifest.json
python -m subkit burn manifest.json --forbid F --video in.mp4 [--audio A] -o preview.mp4 --qa qa.jpg
python -m subkit export manifest.json --format ass -o captions.ass # srt | ass | vtt
python scripts/check_clean.py # コミット前: media/keys/絶対パスがないことを確認
ELEVENLABS_API_KEY=... python -m subkit stt audio.wav -o words.json # オプション;または --provider groq で GROQ_API_KEY
落とし穴
- libass ≠ Pillowのフォントサイズ。 libassはOS/2 winAscent+winDescentでサイズを決定し、ベースラインを行頭からwinAscentの位置に配置します;エクスポートは両方を変換します。変換しないと、ASSのテキストは約35%小さく、細く、低く表示されます。
- ASS内のバリアブルフォントはデフォルトインスタンス(通常はRegular)にフォールバックします。 使用するウェイトのスタティックインスタンス(ここでは
Onest-Bold.ttf、700)を同梱し、fontsdirを渡してください。 ffmpeg -ssを-iの前に置くとタイムスタンプがリセットされます:サブタイトルフィルタはt=0をレンダリングします。ASSを焼き込むときは-iの後にシークしてください。\k/\kfカラオケはセンチ秒単位で、話されたすべての単語を永続的に色付けします。「現在の単語のみ」の場合は、タイムスライスを出力してください(エクスポートが行う方法)。- SRTにはスタイルがありません:納品物にウェイトやハイライトが必要な場合は、ASS/VTTまたは焼き込み動画を提供してください。
- 浮遊する行(話者ごとのx、ヒステリシス、「見た目が良い」リフト)は対話ではジッターと解釈されます。デフォルトの固定モードを維持してください;
baseline_liftでは、バリデータが各リフトが実際に衝突していることを再チェックします。 - コントラストは、背景の平均ではなく、背景の最も明るい10%のピクセルに対する色相で判断されます:火、雪、ハイライトは平均で失敗します。
- 画面上に既に描画された単語を削除しないでください。代わりに、描画されたラベルを注釈レイヤーに移動してください。
templates/safe_zones.jsonのセーフゾーンは近似値です;プラットフォームUIは変更されるため、公開前に再確認してください。
チェックリスト
- words.json が音声全体をカバーしている(クラウドSTTまたは強制アライナー);話者がラベル付けされている
- forbid.json:ショットごとの顔、手、火/プロップ(
align: speaker用のspeakersボックスも含む) - config:サイズ、fps、
safe_zone、話者色(異なる色相、両方ともバッキング上で ≥ 4.5:1) -
plan→ マニフェスト;overlap_accepted(固定モード)またはcollision_lifts(リフトモード、それぞれに実際の理由がある)を確認 -
burn→ QAシート(シアン=ベースライン、黄色=重複許容、オレンジ=理由付きのリフト)と実際のフレームを確認 -
validateがすべてPASS;焼き込み動画を提供(+ プレイヤーがレンダリングする場合はASS/VTT;SRTはプレーンテキストとしてのみ)