見出し画像

【動画編集】音声タイムスタンプのズレをPythonで解決する|WhisperによるVTTファイル作成ライブラリ【vtt-synced-voice】

1. タイムスタンプのズレは「使い方」によって致命的になる

Zoom で収録した動画には、自動生成された字幕ファイル(VTT形式)が付いてきます。ありがたい機能ですが、タイムスタンプの精度が低く、「今しゃべっている言葉」と「表示されているテキスト」が0.5秒以上ずれていることも珍しくありません。

テロップとして使うぶんには、多少のずれは許容できます。問題が起きたのは、VTTのタイムスタンプをカット編集の目印として使おうとしたときです。

Final Cut Pro(FCP)で、ZoomのVTTを基準にカット編集を行ってみました。音声波形とカット位置を見比べると、ズレの大きさが目立ちます。このズレを手動でトリム修正すると時間がかかり、疲弊します。テロップ用途ならズレても困りません。カット編集の基準にするなら、このズレは致命的になります。

Zoomのタイムスタンプは誤差がある

そこで Whisper(OpenAIの音声認識エンジン)を使って自前でVTTを生成してみました。書き起こし精度は格段に上がりましたが、タイムスタンプのズレはなぜか残りました。

この記事では、Whisperのタイムスタンプがなぜズレるのかを説明し、そのズレを±100ms程度まで補正する Python ライブラリ `vtt-synced-voice` の仕組みと使い方を紹介します。

vtt-synced-voice を使って、VTTファイル基準でカット編集すると、ズレ修正はほぼ不要になります。これがこのライブラリの最大のメリットです。

高精度のVTTなら修正は最小限で済む

まず動かしたい方は「§4. インストールと基本的な使い方」まで読み飛ばしてください。


2. なぜWhisperのタイムスタンプはズレるのか

なぜ誤差があるの?

Whisper は音声をテキストに変換するとき、内部で音声を数秒単位の「セグメント」に分割して処理します。このセグメント単位での処理は書き起こし精度には有利ですが、タイムスタンプの精度には限界があります。標準的な Whisper が返すタイムスタンプの誤差はセグメント単位で ±1000ms 程度です。

この精度をさらに上げるために使われるのが、WhisperX の Forced Alignment(強制アライメント) という技術です。wav2vec2 という音声認識モデルを使い、書き起こされたテキストを音声波形と照合することで、単語ひとつひとつの開始・終了時刻を割り出します。これにより精度は ±200ms まで向上します。

しかしそれでもまだズレが残ります。原因は、Forced Alignment が返す CTC タイムスタンプ(音素レベルのタイムスタンプ)の性質にあります。このタイムスタンプは「声が出始めた瞬間」を直接検出しているわけではなく、音素の確率分布から推定した位置を返しています。その結果、実際に声が出るより数十ミリ秒早かったり遅かったりする誤差が残ります。

`vtt-synced-voice` は CTC タイムスタンプをさらに波形レベルで補正することで、誤差を ±100ms まで縮めます。次の章でその仕組みを説明します。

3. vtt-synced-voice の仕組み

`vtt-synced-voice` は次の4ステップで音声ファイルから VTT を生成します。(精度を表すミリ秒は目安です)

ステップ1:WhisperX で単語タイムスタンプを取得する

まず WhisperX が音声をテキストに書き起こし、Forced Alignment で各単語の `start`(開始時刻)と `end`(終了時刻)を取得します。この時点での精度は ±200ms です。

ステップ2:キューを文単位にまとめる

単語間の無音ギャップが一定秒数を超えるとキューを分割し、形態素解析(日本語の場合は Janome)で文末を判定してキューを文単位にまとめます。これにより、単語ごとに細切れになったキューが自然な「ひとつの文」として出力されます。

無音ギャップで分割し、言語解析で文単位に繋げる

ステップ3:Onset検出で start を補正する

CTC タイムスタンプを出発点として、音声波形を逆方向にスキャンします。「無音フレームから有音フレームに切り替わった境界」を見つけ、それを実際の発声開始点(onset)とします。CTC が有音区間内に収まっている場合は逆スキャン、無音区間内に収まっている場合は順スキャンで境界を探します。これにより `start` の精度が ±100ms まで向上します。

逆方向のスキャンで音声波形の開始点を見つける
順方向のスキャンで音声波形の開始点を見つける

コラム|録音レベルの違いを吸収するピーク正規化
Onset検出の前に、音声波形の最大振幅が1.0になるよう正規化する処理を行います。これにより、小さな声で収録した音声でも大きな声で収録した音声でも、「無音」と「有音」の境界を同じ閾値(`silence_threshold`)で判定できます。マイクの感度や収録環境に左右されにくい設計になっています。

正規化とは1.0を基準に値を調整すること

ステップ4:タイムスタンプの終了時刻を算出する

`start` と `end` では、精度と扱い方が異なります。

`start` は波形照合と onset 補正を経た信頼性の高い値で、FCP のカット編集基準として使えます。`end` は用途によって2つの方法で算出します。

`end` は基本的に word["start"] + 0.15秒 で発声時間を推定します。後続の単語がない文末では WhisperX の word["end"] が不正確になりやすく、「短い単語ひとつ分の発声時間」の経験則から固定値 0.15秒 で代替しています。

最後に後処理クランプをかけ、次のキューの start から 0.1秒 手前を end の上限とすることで、キュー間に必ず無音区間を確保します。

まとめると、`start` は波形照合と onset 補正で信頼できる値ですが、`end` は WhisperX の推定値または固定値による近似であり、後処理クランプで上限を保証しています。

エンド時刻は経験則から「start+0.15」として近似する

4. インストールと基本的な使い方

この記事で必要なもの:

  • Python 3.10 以上がインストールされていること

  • macOS または Windows の環境

本題に入る前に、Pythonを使える状態にします。環境構築がまだの方は、先に以下の無料記事をお読みください。作業時間の目安は1時間ほどです。

  • Mac編:

  • Windows編:

準備ができたら、ターミナルで以下を実行してください。Pythonのバージョンが表示されれば、正しくインストールされています。

python --version

インストール

macOS では ffmpeg のみ手動でインストールが必要です。PyTorch を含む依存パッケージは `pip install` で自動的に入ります。

brew install ffmpeg
pip install vtt-synced-voice

Windows では ffmpeg を winget でインストールします。

winget install ffmpeg
pip install vtt-synced-voice

Windows で NVIDIA GPU を使う場合は、`pip install vtt-synced-voice` の前に PyTorch の CUDA ビルドを別途インストールする必要があります。詳細は README を参照してください。GPUを使うと、処理速度が10〜20倍程度速くなります。

フォルダ構成

ここからプログラミングを開始します。ライブラリを使う準備として、プロジェクトのフォルダ構成を作成します。

フォルダ構成の例を示します。任意の場所(ドキュメントなど)の中に `vtt-generator` フォルダを作成します。その中に3つのフォルダ `scripts` `vtt_output` `audio_input` を作成します。そして、`scripts` フォルダの中に `audio_to_vtt.py` という名前のファイルを作成してコードを記載します。

vtt-generator/
├── audio_input/      ← 処理したい音声・動画ファイルを置く
├── vtt_output/       ← 生成されたVTTファイルが出力される
└── scripts/
    └── audio_to_vtt.py   ← transcribe()を呼び出すスクリプト

最小サンプル

`audio_to_vtt.py` の中に次のコードを記載します。このライブラリは、テロップをカスタマイズできるように設定項目(パラメーター)が多いのですが、まずは最小構成で動作チェックを行なってください。

from vtt_synced_voice import transcribe

transcribe(
    audio_file="audio_input/sample.m4a",
    output_file="vtt_output/output.vtt",
    language="ja",
    model="medium",
)

`audio_input/` フォルダに音声ファイルを配置します。テストするには、2〜3分くらいの自分のナレーションの録音が良いでしょう。次のコマンドを実行すると、音声解析処理が開始します。

# 保存フォルダに移動(ドキュメントにプロジェクトを配置したときのコマンド例)
cd ~/Documents/vtt-generator

# スクリプトを実行
python scripts/audio_to_vtt.py

初回の起動は Whisper のモデルをダウンロードするため時間がかかります。処理が完了すると、出力ファイル(ファイル名.vtt)が `vtt_output/` に書き出されます。

VTTファイルは普通のテキストなので、テキストエディタで開けます。ファイルの中身を確認してみましょう。

VTTファイルは `タイムスタンプ` (00:00:03.449 --> 00:00:05.921)と `ナレーション` (皆さんテロップ作業で消耗していませんか)がブロックにまとまっており、これが一つのテロップを表します。

WEBVTT

00:00:03.449 --> 00:00:05.921
皆さんテロップ作業で消耗していませんか

00:00:07.335 --> 00:00:10.562
ファイナルカットプロでテロップを打っていると時間がどんどん溶けていきます

文字起こしの誤変換があれば、テキストエディタで開いた段階で修正します。この修正により動画に正しいテロップを配置できます。

このVTTをFCPに読み込んで自動テロップ化する方法は、別記事で詳しく解説しています。

5. パラメーター調整ガイド

`transcribe()` には以下のパラメーターがあります。まず基本設定で動かし、出力を確認しながら必要に応じて調整する、という順序で使うと効率がよいです。

基本設定

`audio_file`(必須)

入力する音声または動画ファイルのパスです。`.m4a` / `.mp4` / `.wav` など ffmpeg が対応する形式を指定できます。

`output_file`(必須)

出力する VTT ファイルのパスです。ファイル名の拡張子を `.vtt` にしてください。

`language`(デフォルト:`"ja"`)

書き起こしと文末判定に使う言語コードです。日本語は `"ja"`、英語は `"en"` を指定します。

`model`(デフォルト:`"medium"`)

WhisperX の書き起こしモデルです。精度と速度のトレードオフがあります。

`"medium"` 以上では Whisper が自動で句点(。!?)を付与するため、文末検出の精度が上がります。CPU環境では `"medium"` で十分な精度が得られます。

`device`(デフォルト:`"cpu"`)

書き起こしに使うデバイスです。NVIDIA GPU がある環境では `"cuda"` を指定すると処理速度が10〜20倍速くなります。

キュー分割の調整(最重要)

`max_gap_seconds`(デフォルト:`0.4`)

単語間の無音ギャップがこの秒数を超えると、新しいキューに分割します。VTT の分割粒度を決める最も重要なパラメーターです。

まず自分の声で2〜3分の録音を処理し、生成された VTT を確認しながら値を探してください。

  • 標準的な会話速度:`0.4`(デフォルト)

  • のんびり話す・台本を文章ごとに読み上げる:`0.5`〜`0.8`

  • 早口・マシンガントーク:`0.1`〜`0.2`

`merge_sentences=True`(後述)を使う場合、分割後に文末判定でキューをまとめ直すため、`max_gap_seconds` を小さくしても最終出力が過剰に細分化されるわけではありません。まず `max_gap_seconds` で分割の粒度を合わせ、それでも調整しきれないときに `merge_sentences` を使う、という順序が自然です。

調整時は `write_unmerged=True`(後述)でマージ前の VTT を確認しながら作業してください。

`merge_sentences`(デフォルト:`True`)

`max_gap_seconds` で分割したキューを、さらに形態素解析で「文単位」にまとめ直すオプションです。

  • 日本語(`language="ja"`):Janome 形態素解析で文末品詞を判定します。「です・ます・ました・ください」の書き言葉に加え、「よ・ね・な・けど・し・って」などの話し言葉の文末にも対応しています。

  • その他の言語:ピリオド・感嘆符・疑問符を文末として検出します。

  • 形態素解析は、あらゆる話し方に対応しているわけではありません。

`False` にすると WhisperX の単語アライメント結果をそのまま出力します。テロップを細かく表示するときや、文末判定を使いたくない場合に使います。

`min_cue_chars`(デフォルト:`50`)

マージ後にこの文字数を超えるキューを後処理でさらに分割します。句点(。!?)があれば句点で、なければ形態素解析で文末+文頭パターンを検出して分割します。`0` を指定すると後処理分割を無効にできます。

onset補正の調整

`silence_threshold`(デフォルト:`0.001`)

ピーク正規化後の音声に対する無音判定の RMS 閾値です。完全無音は約 0.0、発話は約 0.05〜1.0 の範囲になります。

  • クリーンな収録環境:`0.001`(デフォルト)

  • 環境ノイズが多い収録:`0.005`〜`0.01`

`verbose=True` で onset 補正結果を確認しながら調整してください。

`margin_before`(デフォルト:`0.066`)

onset 検出で見つけた開始時刻をさらに早める秒数です。デフォルトは 30fps × 2フレーム分です。FCP のフレームレートに合わせて微調整できますが、通常はデフォルトで問題ありません。

タイムスタンプを前後に伸ばす

`margin_after`(デフォルト:`0.0`)

キューの終了時刻を延ばす秒数です。視聴者のコスパ重視の傾向により、最近の動画編集ではセリフの間の無音をすべてカットする手法が広まっていますが、動画の内容や話し方によっては、セリフとセリフの間に少し間を置いたほうが自然に聞こえることがあります。この値を大きくすると、各キューの終了時刻が延び、FCP上でのカット位置に余白が生まれます。

出力形式

`voice_only`(デフォルト:`False`)

`True` にすると、タイムスタンプなしの `.txt` ファイルを出力します。各行末尾の句点も除去されます。カット編集済み動画にテロップを付けたい場合のワークフローで使います。

カット編集済み音声 → vtt-synced-voice (voice_only=True) → .txt → テロップツールに貼り付け

`replacements`(デフォルト:`None`)

書き起こし後に適用する置換リストです。Whisper が誤認識しやすい固有名詞や専門用語を事前に登録しておくと、生成された VTT を手動で修正する手間を大幅に減らせます。

たとえば Whisper は「Final Cut Pro」を「ファイナルカットプロ」と書き起こすことがあります。このリストに登録しておけば、以降の収録ではすべて自動で正しい表記に変換されます。

replacements=[
    ["ファイナルカットプロ", "Final Cut Pro"],
    ["ウィスパー", "Whisper"],
]

収録するたびに誤変換に気づいたらリストに追加していくと、テキスト修正の作業が回を重ねるごとに減っていきます。

注意点として、長い文字列を短い文字列より先に書いてください。リストの順番に適用されるため、`"ファイナルカットプロX"` を `"ファイナルカットプロ"` より後に書くと意図しない置換が起きます。

確認・デバッグ用

`verbose`(デフォルト:`False`)

`True` にすると、各キューの onset 補正結果をターミナルに表示します。`silence_threshold` の調整時に必ず確認してください。

[ 0] CTC=00:00:01.234  onset=00:00:01.866 (←-70ms)  --> 00:00:05.000

`CTC` が補正前、`onset` が補正後のタイムスタンプです。`(←-70ms)` は逆スキャンで70ms遡ったことを示します。

`write_unmerged`(デフォルト:`False`)

`True` にすると、マージ前のキューを `xxx_unmerged.vtt` として追加出力します。`max_gap_seconds` の調整時にマージ前後の VTT を見比べるために使います。通常使用では `False` のままにしてください。

`dry_run`(デフォルト:`False`)

`True` にすると、書き起こしと補正は行いますがファイルへの書き出しをスキップします。`verbose=True` と組み合わせて処理結果だけを確認したい場合に使います。

6. 限界と注意事項

Onset補正の探索範囲は前後300ms

Onset補正(音声波形に合わせたタイムスタンプ調整)は完全ではありません。`vtt-synced-voice` では、WhisperX が返した CTC タイムスタンプを中心に、前後 300ms の範囲で音声波形をスキャンします(ONSET_SEARCH_SEC = 0.3)。この範囲内に無音→有音の境界が見つからない場合は、CTC start をそのまま採用します。

早口では長いキューが残る場合がある

早口のナレーションでは、onset 補正や単語境界での分割が機能せず、数十秒にわたる長いキューが出力されます。`max_cue_seconds`(デフォルト15秒)で一定の長さ以上は自然な区切りで再分割しますが、タイムスタンプの精度は下がる場合があります。

カット編集前の生音声に適用すること

このライブラリはカット編集前の生音声に使うことを前提としています。

生音声 → vtt-synced-voice → VTT生成 → FCPに読み込んでカット編集

カット編集済みの音声には適用しないでください。カット編集によって文間の無音が除去されているため、ギャップによるキュー分割と onset 検出の両方が正しく動作しません。

`small` モデルでは句点が付かない

`model="small"` では Whisper が句点を付与しないため、文末判定の精度が下がります。日本語で使う場合は `"medium"` 以上を推奨します。

7. まとめ

以上で、高精度なタイムスタンプ付きVTT作成ライブラリ `vtt-synced-voice` の説明を終わります。

Premiere Pro や DaVinci Resolve はすでに自動テロップ機能を搭載しており、精度も高い。FCP はこの分野で後れを取っています。ただしそれらは「簡単に動かせるが細かく制御できない」ブラックボックスです。

各動画編集ソフトの特徴を知る

一方、vtt-synced-voice はその逆で、パラメーターによるカスタマイズ性を優先しています。セリフの間隔調整、誤変換の自動置換、モデル精度の選択——こうした細かいコントロールは既製ソフトでは提供されていません。パラメーターが多く見えるのは、その設計の裏返しです。

「高精度タイムスタンプ付きのVTT」を、私が開発した「FCP自動テロップスクリプト」に渡すと、テロップ打ち作業が75%削減されます。vtt-synced-voice はそのテロップ自動入力用として使うために開発したライブラリです。FCPユーザーの方はぜひ合わせてご覧ください。

8. 開発協力のお願い

開発に協力してください❤️

`vtt-synced-voice` は現在も継続的にアップデートしています。不具合の報告や機能の要望があれば、GitHub の Issue からお知らせください。

https://github.com/mikai-daichi/vtt-synced-voice

積極的にアップデートしていくので、使用前に最新版への更新をお願いします。

pip install --upgrade vtt-synced-voice

使ってみた感想、うまくいかなかったケース、「こういう機能があれば」という要望、どれも歓迎します。一緒にこのライブラリを育てていただけると嬉しいです。

最後まで読んでいただき、ありがとうございました。

いいなと思ったら応援しよう!