Auto-Cut - 計測に基づく無音 & 死重トリミング
原則
- カットはシーン/キャプション作成の前に実施すること。 カット後にタイムスタンプ(キャプション、ズーム、効果音)が全てずれるため。このステップの出力:
assets/<source>.cut.mp4+assets/transcript.cut.json- 以降の全てのステップはカット版を使用する。 - brief で autoCut が有効な場合、カットは必須(推奨ではない)。 カットできない場合は、具体的な理由(元動画がすでにタイトだった)をレポートに明記すること。
- 閾値はサーバー側が管理し、作業者が決めるものではない。 自分で
silencedetectを実行したり、感覚で dB 閾値を選んではならない。以下の 2 つの測定事実がこのワークフローを無効化した:- 適切な閾値はファイルごとに異なり、定数ではない。実際のファイルでは、-40dB で無音 0、-30dB で 13、-25dB で 21 と結果が変わった。固定値は無意味か、音声を削りすぎる。
- ラウドネスだけでは「間」と「小声の発話」を区別できない。実際のファイルで
silencedetectを -30dB で実行すると 48.6 秒の「無音」が検出されたが、実際に単語間の本当のギャップにあったのは 18.2 秒のみ。残りの 30.4 秒は単語内部(音節区切り、無声子音 c/t/p/ch の末尾)にあった。音量だけでカットすると音声を損失する。 サーバーが計測(autoTrim.ts)と候補生成(deadWeight.ts)を行う。作業者はレビューを行う。 この分割が本スキルの要点である。
- カット対象は 2 種類ある:無音(機械計測、トランスクリプトで保護)と内容上の死重(フィラー、言い直し、言い間違い - サーバーが提案、作業者が承認;さらに繰り返しポイント - これは作業者しか見つけられない - ステップ 3 参照)。
ステップ 0 - 前提条件:ワードタイムスタンプ付きトランスクリプト
全体の保護機能はこれに依存する。サーバーは assets/transcript.raw.json、次に assets/transcript.json を探す。見つからない場合、分析は実行されるが guarded: false となり、死重リストは空になり、数値は両方向で推測値になる(悪いカットを通したり、良いカットを失敗させたりする)。まずトランスクリプトを作成すること - ベトナム語のトーキングヘッドを保護なしでトリミングしてはならない。
ステップ 1 - 分析(無料、繰り返し可能、エンコードなし)
POST http://localhost:6869/api/projects/<id>/auto-trim/analyze
body: {} # または { "source": "assets/face.mp4", "level": "tight" }
level は natural | default | tight で、デフォルトは brief.autoCutLevel。応答:
silence-durationSec、選択されたthresholdDb+thresholdNote(その閾値が選ばれた理由)、計測されたnoiseFloorDb、カットされるsilences、keepRanges、removedSec、完全なsweepテーブル、wordGuard(トランスクリプトが拒否した秒数)。deadWeight-candidates[](kind:filler | stutter | repeat-take | hesitation)、start/end、text、confidence、reason、context;さらにtotalSecとbyKind。guarded- トランスクリプトが見つかった場合のみtrue。falseの場合は、カット前に修正すること。
エンコードは行われないため、何度でも呼び出せる(217 秒のソースが約 1 秒で分析される)。
ステップ 2 - 全ての候補をレビュー(機械ではなく、あなたの仕事)
リストは決定論的である:同じトランスクリプト、同じ候補、同じ信頼度。機械にできないのは意味の理解である。一つ一つ確認し、自分が責任を持てるものだけを残せ:
- 信頼度が低い = 「おそらく問題ない」ではなく「まず文脈を読め」。 ベトナム語のフィラーはほぼ常に実在の単語と衝突する。
đó、ấy、thế、mà、làはフィラー助詞であると同時に、実際の指示詞・接続詞でもある。 - 接続句(
hoặc là、tức là、bởi vì là、với lại là)はトラップである。 実際のトランスクリプトで測定した例:「…có thể là ok ứng dụng nó / Hoặc là / Tham khảo để tìm cách…」は本物の代替案であり、これをカットすると文の分岐の一つが失われる。しかし「Còn trường hợp mà mọi người có thể nghe / Hoặc là / Còn trường hợp mà người AI không ứng dụng được…」は放棄された文であり、除去すべきである。表層形式は同一であり、意味だけが異なる。まさにこの理由から、これらは低い基本信頼度と「文脈を読め」という理由で返される。 repeat-take候補:承認前に、後のテイクが本当により完全であることを確認せよ。hesitation候補は単語間の純粋な無音である - 通常は安全だが、オチの前の意図的な間を削除していないか確認せよ。
自由に却下せよ。フィラーを残しても 0.4 秒の損失で済むが、実在の単語をカットすると文が無意味になり、視聴者はすぐに違和感を覚える。
ステップ 3 - 繰り返しポイント(あなただけが見つけられる - 必須)
どの検出器もこれを捕捉できず、これがユーザーが最も不満に思う死重である。トランスクリプトを発話として読み、意味的に分析せよ:
- 同じ主張を述べる文をグループ化する - 文言は一致する必要はなく、内容のみが重要。話者はしばしば同じ主張を 2-3 回言い直す:最初は短く/つっかえながら、後により完全に。隣接しない文も考慮せよ(主張 A を述べ、脱線し、より詳細な主張 A に戻る)。
- グループごとに最も完全なバージョンを 1 つだけ残す:
- 最初の短いバージョンと後のより完全な言い直し → 後者を残し、前者をカット。
- 後の文が単なる短いエコー(「Đúng vậy, như tôi nói…」)の場合 → 前者を残し、エコーをカット。
- 同等の 2 バージョン → よりスムーズな配信(つっかえが少なく、フィラーがない方)を残す。
- カット後の流れを確認する:残したテキストを最初から最後まで読み直す。残した文がカットした文を参照している場合(「như vừa nói」)、両方を残すか、参照のないバージョンを選べ。
- 各判断を
{start, end}範囲に変換し、ステップ 4 で承認済み候補と共に送信せよ。 - カット前に表を作成せよ(レポートに含める):
タイムスタンプ | カットした文 | 理由 | 残した文- ユーザーが全ての判断を確認できるように。
ステップ 4 - 適用(レンダーキューを経由)
POST http://localhost:6869/api/projects/<id>/auto-trim/apply
body: { "cutCandidates": [{ "start": 51.81, "end": 52.25 }, …] } # 承認したもののみ
-> 202 { job }
何も承認しなかった場合は空リストを送信せよ - 計測された無音はそれでもカットされる。その後 GET /api/jobs/<jobId> をポーリングしてジョブ完了を待つ。ジョブの内容:
- トランスクリプトガード付きで再分析し、無音範囲と承認範囲をマージ(重複は統合、二重計上なし)。
- 単語の中間点を飲み込む承認範囲をすべて削除 - レビューに対する最後の安全網。削除された範囲がログに記録されるので、その箇所を再確認せよ。
- 1 回の ffmpeg パス(
trim/atrim + setpts/asetpts + concat)でカットし、assets/<stem>.cut.mp4を書き出す。 - 全てのタイムスタンプをカットタイムラインに再マッピング →
assets/transcript.cut.json。 - 再マッピングされた単語で出力を検証し、
assets/auto-trim-report.jsonを書き出す。
カットする価値がない場合、ジョブは意図的に .cut.mp4 を生成しない(再エンコードしても同一コピーは品質が低下するのみ) - レポートの output は null となり、ソースをそのまま使う。
ステップ 5 - レポートを読む(それを完了して初めてジョブ完了)
assets/auto-trim-report.json には以下が含まれる:カット前後の時間、removed.silenceSec と removed.approvedSec、選択された閾値とその理由、却下された候補、verification。
verdict: "pass"- 結果がそのレベルのプロファイルを満たしている。verdict: "fail"- ジョブは完了しファイルは使用可能だが、プロファイルを満たしていない。ログに明示的に記録される。ステップ 2/3 でさらに死重を承認して再実行するか、最終レポートでなぜ受け入れるのかを明記せよ。fail を無視してカット完了を報告してはならない。
合格基準は古い「無音 > 0.8 秒なし」ルールではない - 測定により、そのルールは大幅に緩すぎることが判明した。実際のファイルでは 0.45-0.67 秒の残存無音が 13 個もあり、合計 6.7 秒(実行時間の 4.2%)で明らかに間延びして聞こえる。現在のプロファイルは、最長単一無音と総比率の両方に上限を設定している(default:0.5 秒 / 3%)。
ステップ 6 - カット版をすべての場所で使用する
以降、キャプション、キーレイアウト、効果音のタイミング、ズームは assets/transcript.cut.json と .cut.mp4 を読み込む。QC チェック dead-air は、組み立てられた動画をトランスクリプトと照合して再測定し、brief.autoCut がオンでプロファイルを超えるデッドエアがある場合に FAIL する - これはバックストップであり、計画ではない。
最終レポートには以下を含めること:ステップ 3 のカット表、除去秒数(無音と承認された死重に分割)、検証結果。数値は auto-trim-report.json から取得せよ。
既知の問題
- カット後に古いトランスクリプトを使用すると、キャプションが後半に向けてずれる。常に
transcript.cut.jsonに切り替えること。 - 無音だけをカットし、繰り返しポイントを無視する - ユーザーが「死重」と呼ぶもののほとんどはステップ 3 である。
- ソースではなくレンダリング済みドラフトをカットすると、2 回目のエンコードで品質が低下する。常にソースからカットすること(サーバーはソースの上書きを拒否し、デフォルト選択時に
*.cut.*のファイルをスキップする)。 - 唯一のトランスクリプトが raw である状態で
.cut.mp4を分析すると、ガードのタイムラインがずれる。ソースファイルとそれを記述するトランスクリプトを一致させること。 - 独自の先頭無音を持つ効果音/ナレーションファイル →
data-media-startを使用して HyperFrames 内でトリミングすること(noti-tiktok-vn スキルの「SFX loudness and lead silence」を参照)、先頭無音 0.3 秒のためにオーディオファイルを再エンコードしないこと。 - カットセグメントをつなぐときクロスフェードなし → すべての結合点でクリック音が発生。
autoTrim.tsで修正済み;ルールは以下にあり、オーディオのカットや連結を行う新しいコードすべてに適用される。
オーディオ:カットエッジごとに 30ms フェード(厳格ルール)
カットは波形の途中で発生する。あるセグメントの最後のサンプルと次のセグメントの最初のサンプルは無関係であるため、結合は垂直の段差となり、段差はクリック音となる。原因はエンコーダーでもソースでもなく、結合そのものであるため、後段の再エンコードでも消えない。
重要度はカットがどこに当たるかによって大きく異なる。 実際の 278 秒のトーキングヘッドでロスレス測定した結果、同じカットをフェードあり/なしで比較(結合点での最大サンプル間段差;約 0.02 以上で可聴):
| 結合の種類 | フェード前 | フェード後 |
|---|---|---|
無音トリミング、40 結合点(balanced) | 中央値 0.0040、最悪 0.0113 - 全て非可聴 | 中央値 0.0000 |
| 発話途中、4 結合点(死重 / 繰り返しポイント) | 0.1195、0.0744、0.0232、0.0064 - 4 中 3 が可聴 | 全て ≤ 0.0008 |
参考までに、それらのファイルの通常の音声で最大の段差は 0.06-0.13 であった。したがって、フェードなしの発話途中の結合は、コンテンツ自体が生成するどの段差よりも大きく、一方無音の結合は可聴域に全く達しない。
最初の行の結果に基づいてフェードを省略してはならない。通常の無音トリミングはたまたま影響がないケースであるが、ステップ 3(繰り返しポイント)やクリップ抽出は発話の途中でカットするため、それらの結合はクリック音を発生させる。
修正方法:すべてのセグメントの両エッジに 30ms のフェードを適用する。段差をなくすのに十分な長さであり、フェードとして聞こえるにはあまりに短い。apps/server/src/util.ts の audioCutFade() を使用せよ;フィルタを手組みしてはならない。
[0:a]atrim=start=S:end=E,asetpts=PTS-STARTPTS,afade=t=in:st=0:d=0.030,afade=t=out:st=D-0.030:d=0.030[aN]
これを何もせずに沈黙させる 3 つのポイント:
- 順序。
afadeはasetpts=PTS-STARTPTSの後に配置すること。afadeはストリーム自身のクロックからstを読み取るため、元のタイムスタンプではst=0は過去であり、フェードインは決して発火しない。 - 長さ。
Dはセグメントの長さであるため、フェードアウト開始はD - 0.030となる。短いセグメントではフェードをD/2に制限しないと、2 つのフェードが重なってセグメントを飲み込む。 - クリップ抽出(結合ではない)。 長い動画からクリップを切り出す場合、結合は行われなくとも同じ 2 つの生のエッジが存在する -
reframe.tsは-afでそれらをフェードする。
自分で測定する際の注意:2 つの AAC エンコードを比較してはならない。再エンコードはサンプル全体を乱し、40 の実際の結合点が 294 のスプリアス差分領域に埋もれ、2 つのファイル間に 54ms のオフセットが生じる。代わりに両方を pcm_s16le に直接レンダリングせよ - そうすれば 2 つはサンプル単位で一致し、結合点は累積セグメント長の位置に正確に存在する。