ラペルシンク は何をしますか?
カメラのクリップと別録りのレコーダートラックを入力として、クリップごとに1つのラペル音声スライスと、同期済みのResolveタイムラインを出力します。初回テストはライブトークで実施:59分のレコーダートラックに対して5つのカメラクリップ。すべてのクリップが20ミリ秒以内に着地し、1フレームを大幅に下回りました。
なぜ重要か: ライブイベントではカメラのマイクは部屋の反対側にあります。その音声は使い物にならず、ラペルだけがクリーンな音声です。このスキル実行後、<work>/<clip>.wav と <clip>.json がラペル音声になるため、ペーパー編集やビデオリールは何も変更があったことを知らずにクリーンな音声と文字起こしを読み取れます。
セットアップ:以下を入力
| プレースホルダー | 説明 | 例 |
|---|---|---|
<FOOTAGE_DRIVE> | 生映像がある場所。ビデオごとに1フォルダ、レコーダーファイルは audio/ サブフォルダ内 | /Volumes/MyWorkDrive |
<SYNC_OUT> | フル品質のラペルスライスの出力先 | ~/Movies/<video>-sync |
<LUT_PATH> | オプションのLUT、ResolveのLUTフォルダからの相対パス | MyLuts/SLog3-to-709.cube |
WHISPER_MODEL (環境変数) | whisper.cppのモデルファイル | デフォルト ~/.cache/whisper-models/ggml-small.en.bin |
要件:ffmpeg、ffprobe、whisper-cli (whisper.cpp)、Python 3(numpy と scipy は自動で ~/.cache/lapel-sync-venv にインストール)、execute_resolve_code を公開するResolve MCPサーバーを伴うDaVinci Resolve、および paper-edit スキル(ensure_resolve.sh 用)。
実行方法
作業ディレクトリ:セッションのスクラッチパッド、例:<scratchpad>/<video>。スキルディレクトリは以下 .claude/skills/lapel-sync。フル品質のスライスは <SYNC_OUT> に出力。
zshの注意点: インラインシェルでは $ss:end_sample は :e を修飾子として解析し、テキストを食べてしまいます。常に ${ss} と記述してください。
1. 映像と音声を見つける
<FOOTAGE_DRIVE> 上にビデオごとに1フォルダ。レコーダーファイルは audio/ サブフォルダにあります:
find "<FOOTAGE_DRIVE>" -maxdepth 4 -iname "*<keyword>*" -not -path "*/.*"
ls "<folder>" "<folder>/audio"
例えばTascam DR-10Lは、000_YYMMDD.wav、001_... のように15分ごとに分割し、さらに _D ツインファイルを書き出します。_D ファイルはデュアルレコードのセーフティトラックで、約20 dB低いです。メインファイルを使用し、_D はクリッピング部分の修正用に保持します。
2. レコーダーファイルを連結する
bash <skill>/scripts/stitch.sh "<folder>/audio" "<SYNC_OUT>/lav-master.wav"
各ファイルのBWF time_reference(真夜中からのサンプル数)を読み取り、すべてのファイルが直前のファイルの終了位置から正確に始まっていることを確認します。サンプル連続している場合はロスレスのストリームコピー。ギャップがある場合はその分の無音を挿入し、マスターが壁掛け時刻に一致するようにし、そのサイズを警告します。
3. 両方の音声を文字起こしする
bash <skill>/scripts/transcribe_sources.sh "<folder>" "<SYNC_OUT>/lav-master.wav" "<work>"
すべての出力は <work>/sync/ に格納され、<work> 自体には置かれないため、ペーパー編集の sentences.py が読み取ることはありません。カメラ音声は最初に強く増幅されます(highpass=f=120,volume=40dB,alimiter、16kモノラル)。これは多くの場合 -65 dB程度であるためです。既に完了した処理はスキップするため、自由に再実行できます。
ラペルは音声1時間あたり約8分かかります。バックグラウンドで実行してください。Whisperは単語のタイミングを <work>/sync/lav.log にストリーミングし、align.py は lav.json がまだ書き込まれていない場合にそのログを読み取るため、ラペルの文字起こしが完了する前にアライメントを開始できます。文字起こしが完了していないクリップは「N単語のみ一致」と報告するだけなので、完了後に再実行してください。
4. アライメント
python3 <skill>/scripts/align.py "<work>"
<work>/sync/sync.json を書き込みます。初回実行時は ~/.cache/lapel-sync-venv にnumpyとscipyをインストールします(システムのpython3にはこれらのパッケージがないことが多い)。以降はその仮想環境内で自身を再実行するだけです。
仕組み(出力を信頼・検証できるよう説明):
- まず単語。 両方の文字起こしに現れる、ユニークな4単語のラン。オフセット = ラペル時間 - カメラ時間。クリップあたり数百の一致。最も密なクラスターを0.5秒以内に絞り込み、その中央値を採用。約0.1秒の精度。
- 波形の精密調整。 カメラ音声の20秒チャンク(40秒未満のクリップは6秒チャンク)を300-3000 Hzのバンドパスフィルターで8 kHzに処理し、単語オフセットの±0.6秒の範囲でラペル音声と相互相関。中央値の6倍を超えるピークをカウント。20ミリ秒以内のチャンクは一致とみなす。5分を超えるクリップでは直線フィットによりオフセットとドリフトを算出。
出力されるすべての行:オフセット、一致したチャンク数、ばらつき、ドリフト。「CHECK BY EAR」とマークされたクリップ(一致チャンクが3未満)は最大0.1秒ずれている可能性があるため、Resolveで確認してください。クリップ全体で1フレームを超えるドリフトもフラグが立てられます。
5. クリップごとにラペルをスライスする
python3 <skill>/scripts/slice.py "<work>" "<SYNC_OUT>/lav-master.wav" "<SYNC_OUT>"
クリップごとに以下を書き込みます:
<SYNC_OUT>/<clip>_lav.wav:24ビット、ソースレート、クリップの長さにサンプル単位で一致(ラペル時間 = クリップ時間)<work>/<clip>.wav(16kモノラル)と<work>/<clip>.json(スライスのWhisper文字起こし):ペーパー編集とビデオリールが読み取るファイル<work>/clips.ndjsonに"audio"フィールドが追加され、スライスを指す
レコーダーの開始が遅れた、またはクリップに対して早く停止した場合、その部分は無音で埋められ、警告が表示されます。スライスに対するWhisperの処理には数分かかります。その後でペーパー編集の transcribe.sh を実行しないでください(jsonが存在するためスキップされますが)。
6. クリッピングをチェックする
slice.py はスライスごとにフルスケールの0.99以上のサンプルをカウントし、最長の連続部分を出力します。数百の短いラン(数十サンプル)は、通常レコーダーのリミッターがピークをキャッチしたものであり、歪みではありません。1ミリ秒以上のランがリストされる場合、またはユーザーが音が歪んでいると言った場合のみ、_D ツインファイルから該当部分をパッチします(同じタイミング、約20 dB上げる)。
7. Resolveでタイムラインを構築する
bash .claude/skills/paper-edit/scripts/ensure_resolve.sh
<work>/sync/params.json を書き込みます:
{"project":"<Video> - YYYY-MM-DD","work":"<work>","name":"Synced - Full Talk",
"lut":"<LUT_PATH>"}
lut はオプション。使用しない場合は省略。その後、execute_resolve_code を1回呼び出します:
PARAMS = "<work>/sync/params.json"
exec(open("<skill>/scripts/resolve_sync_timeline.py").read())
プロジェクトを作成(同名が存在すれば開く)、カメラクリップとラペルスライスをインポートし、Synced - Full Talk というタイムラインを構築:各クリップはV1にピクチャーのみ(mediaType: 1)、そのスライスはA1にサウンドのみ(mediaType: 2)、同じイン・アウトフレーム。recordFrame = タイムライン開始 + round((オフセット - 最小オフセット) * 24000/1001)。これによりクリップ間の実際のギャップが保持されます。LUTはノード1に適用。カメラのスクラッチ音声はオフに。タイムラインの解像度は、クリップが縦向きの場合は1080x1920、それ以外は1920x1080(width/height で上書き可能)。タイムライン名が既に存在する場合は、既存のものを変更せずに v2 を追加します。
ユーザーに各クリップで明確な子音(「p」や「t」)の部分をいくつかスクラブさせて確認してもらいます。これが本当のチェックです。
8. 引き継ぎ
ユーザーにタイムラインが存在すること、ステップ4のクリップごとの数値(各行に1クリップ)、フラグが立ったもの、クリッピングの判定結果を伝えます。その後、次のステップに進みます:ペーパー編集(文字起こしステップはスキップ。<work> は既にセットアップ済み)またはビデオリール。
知っておくべき事実
- 波形相関だけでは失敗しました。 残響のある部屋でカメラマイクの平均が約-65 dBだった場合、広いウィンドウでのクリップ全体および20秒チャンクの相関は一貫性のないゴミを生成しました。単語を先にマッチさせ、その後で狭い範囲の波形を検索する方法が機能します。
- カメラの時計はしばしば間違っています。 テストでは、カメラはレコーダーより約149秒遅れていました。一部のカメラ(例:Sony)は
creation_timeをUTC(末尾がZ)で書き込みますが、レコーダーのBWFtime_referenceはローカルタイムです。creation_timeは大まかなヒントとしてのみ扱ってください。単語マッチングには必要ありません。 - ドリフトは無視できる テストした機材では:約0.5 ms/分、21分のクリップで11 ms。タイムストレッチは不要。短いクリップでは、数秒のフィットはノイズに過ぎないため0と報告されます。
- スクリプトの再実行により、手作業で作成したスライスと6ミリ秒以内の差で再現されました。
- 縦向き映像: 例えばSonyで縦向きに撮影した場合、4K 23.976、回転=90の表示マトリックスで記録されます。Resolveは2160x3840として読み取り、回転修正は不要です。
- レコーダーの例、Tascam DR-10L: 48k 24ビットモノラルBWF、15分ごとに新規ファイル、
_Dセーフティツインは約20 dB低い。 - paper-edit の
resolve_build.pyはまだカメラファイルから音声を取得します。 ラペル撮影の場合、"audio"フィールドを読み取るようになるまで、そのタイムラインはカメラのスクラッチ音声を保持します。それまではユーザーに伝えるか、A1を_lav.wavスライスに再リンクしてください。スライス時間 = クリップ時間であるため、カットポイントは同じです。 - Whisperは
-oj -ml 1 -sowオプションで単語タイミングを取得します。
関連
paper-edit:ラフカット。この後に実行。video-reels:clips.ndjsonを読み取り。"audio"フィールドがスライスから音声を取得するよう指示します(インストールされている場合)。