ポッドキャスト編集とマスタリング
ユーザーが自分で録音したポッドキャストやナレーションを、公開できる完成品に仕上げます。ノイズ除去、前後の無音カット、音声のイコライジングとコンプレッション、ラウドネス標準化(約 -16 LUFS)、MP3 書き出しを行います。必要に応じて、オープニング/エンディングBGMやセクションごとのBGMを追加し、編集提案(どの長いポーズを短縮できるか、どの部分が言い直しか)を行い、ユーザーの確認後にカットを実行します。
以下の scripts/、references/ はすべてこのスキルディレクトリ内にあります。実行時は完全なパスに置き換えてください(例: bash <スキルディレクトリ>/scripts/edit_podcast.sh)。
一、開始前にユーザーに説明すること
- ffmpeg をインストールしておく必要があります(無料のオープンソースオーディオツール)。インストールされていない場合は、ユーザーに自分でインストールしてもらってください。macOS では
brew install ffmpeg、Linux ではシステムのパッケージマネージャ(例:apt install ffmpeg)を使用します。スクリプトは次の順序で ffmpeg を探します: 環境変数FFMPEG→ システム PATH 内のffmpeg→ 一般的なインストール場所/opt/homebrew/bin、/usr/local/bin。バージョン 6.0 と 8.1 で動作確認済みです。 - bash と Python 3.9 以降が必要です。標準ライブラリのみを使用し、追加の Python パッケージは必要ありません。
- macOS と Linux のみ対応:macOS でテスト済みです。Linux でも動作するはずです。Windows の場合は WSL が必要ですが、テストはしていません。
- 初回のノイズ除去時には、約 300KB の公開ノイズ除去モデル(RNNoise)をインターネットからダウンロードし、
~/.cache/ethan-podcast-post/に保存します。ダウンロード後、SHA-256 で検証します。ダウンロードできないか、チェックサムが一致しない場合は、自動的に ffmpeg 内蔵の afftdn ノイズ除去に切り替わります。スクリプトはどちらを使用したかを出力するので、その点をユーザーに伝えてください。モデルの出典についてはreferences/third-party.mdを参照してください。 - すべての処理はローカルで行われます:録音データがアップロードされたり、課金が発生したりすることはありません。
- 行わないこと:音声の生成やクローン、吹き替え、文字起こし(字幕が必要な場合はユーザーが SRT 字幕ファイルを提供してください)、プラットフォームへの公開、音楽の提供(BGMはユーザーが準備し、使用権限があることを確認してください)。
二、ファイルの安全性(必須)
- 元の録音は読み取り専用:スクリプトは録音ファイルを読み取るだけで、変更、移動、削除はしません。
- 出力は常に新しいディレクトリに書き出す:録音ファイルの隣に新しいディレクトリ(例:
第3回_編集後/)を作成します。録音やBGMと同じディレクトリには出力しません。スクリプトは出力ディレクトリが入力ファイルと同じディレクトリにある場合、処理を停止します。 - 無断で上書きしない:出力ディレクトリにすでに完成品、
audio_raw.wav、voice_master.wavが存在する場合、スクリプトは上書きせずに停止します。上書きする場合は、まずユーザーにどのファイルが上書きされるか、上書きすると古いファイルは復元できないことを説明し、ユーザーの同意を得た上でOVERWRITE=1を指定して再実行します。編集提案、カット、音声処理のスクリプトも同様に、同名の出力ファイルが存在する場合は停止し、ユーザーの同意を得た後に--overwriteを追加します。 - 削除前に確認する:削除(
audio_raw.wavのような大きなファイルのクリーンアップを含む)を行う際は、常にどのファイルをなぜ削除するかをユーザーに説明し、同意を得た後に削除します。スクリプトの中間ファイルは、出力ディレクトリ内の今回の実行専用の一時サブディレクトリに配置され、終了時にこのサブディレクトリのみがクリーンアップされます。 - カットの内容はユーザーが決定する:編集提案はあくまで候補リストであり、ユーザーが確認するまではカットを実行しません。
三、処理フロー
まず、環境チェックを一度実行し、結果をユーザーに伝えます。
bash <スキルディレクトリ>/scripts/edit_podcast.sh --check
これにより、見つかった ffmpeg、そのバージョン、250Hz 動的イコライザーが有効かどうか(第6節参照)、ノイズ除去モデルが準備できているかどうかが表示されます。ffmpeg が見つからない場合はインストール方法が表示されるので、インストールしてから続行します。
ステップ 1: 一次処理、音声マスターと試聴版を出力
KEEP_MASTER=1 bash <スキルディレクトリ>/scripts/edit_podcast.sh <録音ファイル> <出力ディレクトリ>/pass1 <名称>
- 録音ファイルは、wav、m4a、mp3、mp4 など ffmpeg が読み取れる形式であれば何でも構いません。2人以上の対談の場合は
INTERVIEW_MODE=1を追加します(より強力なコンプレッションと、話者間の音量差を自動的に平準化します)。 - 処理順序: 48kHz WAV に抽出 → 品質チェック → 前後の無音カット(中間のポーズには触れません)→ リップノイズ除去 → 80Hz ハイパス → ノイズ除去 → 250Hz 動的イコライジング → 歯擦音除去 → コンプレッション → 5kHz ブライトニング → リニアゲインで -16 LUFS に調整 → リミッター → MP3 書き出し → ラウドネス再測定。
- 出力:
<名称>_final.mp3: BGMなしの完成品(試聴版も兼ねます)voice_master.wav: 音声マスター(前後の無音はカット済み)。以降の文字起こし、編集提案、カットはこれを基準に行います。audio_raw.wav: 元の録音を WAV に変換したコピー(すべての処理が成功した後にのみ出力ディレクトリに書き込まれます)。後でやり直す場合に使用でき、ファイルサイズは大きくなります。
- 出力内の以下の項目を確認し、ユーザーに伝えます:
- 末尾の音量が -80dB 未満: 録音が途中で切れている可能性があります。まずユーザーに確認します。
- ノイズ除去方法: RNNoise または afftdn。
- 完成品のラウドネスは -16 LUFS ±1 以内、トゥルーピークは -1.5 dBTP 以下である必要があります。範囲を超えた場合はスクリプトが警告を出すので、そのままユーザーに伝えます。
- 録音が小さすぎるか、無音の場合は、スクリプトはエラーで停止し、空の完成品を出力しません。エラーメッセージには、録音のピーク値に基づいた推奨値が表示されます。まずユーザーに録音に確かに音声が含まれていることを確認し、次に指示に従って
INPUT_GAIN=<推奨値>(単位 dB、現在の INPUT_GAIN 設定値に加算されます)を追加して、全体を適切なレベルに上げてから再実行します。エラー発生時、出力ディレクトリにはファイルが残らないため、同じディレクトリをそのまま使用して再実行できます。また、今後録音する際は入力音量を上げるようにユーザーに提案します。
ステップ 2: 編集提案(候補をリストアップするのみで、音声は編集しません)
字幕はステップ 1 で生成された voice_master.wav から文字起こしする必要があります(または、ステップ 1 で BGM を追加していない完成品 MP3 から文字起こしします。どちらもタイムラインは同じです)。元の録音からの文字起こしは使用しないでください:元の録音は前後がトリミングされておらず、時間が合わず、見つけた位置がずれてしまいます。
短縮できる長いポーズを見つける:
python3 <スキルディレクトリ>/scripts/edit_cuts.py suggest <出力ディレクトリ>/pass1/voice_master.wav \
[--srt 字幕.srt] [--protect 15:00.0-15:03.0 ...]
- デフォルトでは、1.5 秒より長いポーズを候補としてリストアップし、1.2 秒に短縮することを提案します(ポーズの両端にそれぞれ半分を残し、部屋の暗騒音を保持します)。
- ユーザーが意図的に残したポーズ(例: 重要な発言の後の沈黙)は、
--protect 開始-終了で指定します。前後にそれぞれ 0.5 秒のバッファが確保され、カット候補から除外されます。 - 明らかに長いポーズが見つからない場合は、暗騒音が高い可能性があります。
--noiseをデフォルトの -45 から -40 に調整して再実行します。 - 40 分あたりの候補が 15 箇所以上、または合計が 60 秒を超える場合、スクリプトはパラメータが厳しすぎると警告します。
--min-pauseを大きくし、すべてをカットしないようにします。
字幕から、言い直しの合図で指定されたリテイクセクションを見つける(字幕が必要です):
python3 <スキルディレクトリ>/scripts/edit_cuts.py retakes <出力ディレクトリ>/pass1/voice_master.wav --srt 字幕.srt [--phrase あなたの合図]
- 録音時の取り决め:言い間違えた → 1 秒以上停止 → 落ち着いて言い直しの合図を言う → 1 秒以上停止 → 文頭から言い直す。
- デフォルトの合図は「言い直し」。自分の合図に変更する場合は
--phraseを使用します。文字起こしソフトが合図を同音異義語に書き換えることが多い場合は、複数の--phraseを追加して、いくつかの表記を指定します。合図は本文中に出現しない語句を選び、2回続けて言うことで誤判定を減らします。 - スクリプトは字幕内で合図を探し、言い直した文の先頭を使用して前に言い間違えた文を特定し、カット範囲と確信度(high / mid / low)を出力します。low のものは必ずユーザーに試聴確認させます。
2 つのコマンドは、それぞれ voice_master_suggested_cuts.tsv と voice_master_retake_cuts.tsv を出力します(すでに存在する場合は上書きしません。ユーザーの同意後、--overwrite を追加します)。候補を一つずつユーザーに説明し(時間、前後関係、節約できる秒数)、ユーザーがカットを承認した後、それらの行を新しい cuts.tsv にコピーします(タブ区切り: 開始、終了、備考。時間は MM:SS.s 形式)。
ステップ 3: カット適用
python3 <スキルディレクトリ>/scripts/edit_cuts.py apply <出力ディレクトリ>/pass1/voice_master.wav --cuts cuts.tsv [--srt 字幕.srt]
- サンプル精度でカットし、接合部は 15 ミリ秒の等電力クロスフェードで処理します。24-bit の
voice_master_edited.wavと、カットマップvoice_master_edited.edit_map.json(保持された各オーディオセグメントのソースと位置を記録)を出力します。 --srtが指定された場合、完全に保持された字幕はそれに従って移動します。字幕が途中でカットされた場合は、スクリプトは実行を拒否します。--srtを外して再実行し、カット後にユーザーにカット後のマスターから再度文字起こしを行ってもらいます。- カット後、ユーザーに各カットポイントの前後数秒を試聴してもらいます。クリックノイズがないこと、息継ぎが2回続いていないこと、音色が変わっていないことを確認します。
ステップ 4(オプション): 引用、回想などのセクションに異なる音響効果を適用
python3 <スキルディレクトリ>/scripts/voice_fx.py <カット後マスター.wav> --fx fx.tsv
python3 <スキルディレクトリ>/scripts/voice_fx.py --list # 5つのプリセットを表示
- プリセット: quote(手紙、他人の文章を読む)、archive(古い新聞、公文書)、telephone(電話の再現)、memory(回想、夢)、radio(古いラジオ、1エピソードにつき最大1回)。
- fx.tsv の各行: プリセット、開始、終了、フェードイン、フェードアウト、備考(タブ区切り。フェードイン/アウトに
-を指定するとデフォルト値が使用されます)。 - 処理後も長さはサンプル単位で変更されず、ラウドネスも変わりません。出力は
<元の名前>_fx.wavです。
ステップ 5(オプション): BGM とキュー表
ユーザーが BGM を提供した場合にのみ実行します。最初にキュー表を作成し、次に事前チェックを行います。キュー表は二次処理の出力ディレクトリに配置し、ファイル名は <名称>_cues.tsv とします。
キュー表: タブ区切りの 7 列。# で始まる行は無視されます。ある列に - を指定するとデフォルト値が使用されます。time は MM:SS.s 形式で、音声マスターのタイムラインを基準にします。以下の時間は例です。
# type time src dur vol fade_in fade_out
stinger 02:00.0 - 3 - - -
break 12:00.0 -@20 8 0.5 2 2
bed 18:00.0 -@40 30 0.08 - -
ending 40:00.0 -@90 - - - -
| タイプ | 効果 | dur | デフォルト音量 |
|---|---|---|---|
| stinger | 短い音楽的なアクセント。音声に重ねる。 | デフォルト 6 秒 | 0.12 |
| bed / post | 音声の下に音楽を敷く。音声が入ると自動的に音量が下がる。 | 必須 | 0.12 |
| break | その時点で音声をカットし、純粋な音楽のセクションを挿入。音楽が終わると音声に戻る。完成品の長さが dur 秒分長くなる。 | 必須 | 0.5 |
| ending | エンディング音楽。音声中は音量が下がり、音声終了後に上がり、最後にフェードアウト。 | - と記述 | 0.22 |
srcに-を指定すると、今回渡された BGM が使用されます。-@20は BGM の 20 秒目から取得を開始することを意味します。別の音楽ファイルの完全なパスを指定することもでき、同様に@秒を追加できます(追加しない場合は先頭から取得します)。- stinger の
srcが-で@がない場合、デフォルトで BGM の 60 秒目からセクションが取得されます。他のタイプで@がない場合は先頭から取得されます。 - 作成後、まず事前チェックを実行します。❌ がある場合は先に修正します。
python3 <スキルディレクトリ>/scripts/cues_lint.py <名称>_cues.tsv --master <カット後マスター.wav> --bg <BGM>
これにより、形式、範囲外、素材の有無、ウィンドウの重複、音楽の総比率(推奨 15% 以下)がチェックされ、「マスター → 完成品」タイムスタンプ変換表が出力されるので、番組のタイムライン作成時に直接使用できます。
ステップ 6: 二次処理で完成品を出力
MASTER_WAV=<カット後マスター.wav> bash <スキルディレクトリ>/scripts/edit_podcast.sh - [BGM] <出力ディレクトリ>/final <名称>
- 最初の引数は
-です。MASTER_WAVが指定されている場合、抽出、ノイズ除去、ラウドネス標準化をスキップし、直接キューを適用し、BGM をミックスして、MP3 を書き出します。 - BGM があり、かつ音声が 80 秒より長い場合、自動的にオープニング/エンディングBGMが追加されます。オープニング音楽はまず 3 秒間鳴ってから音声が入り、音声終了後は音楽が約 7 秒残ってフェードアウトします。音声が入っている間は音楽の音量は自動的に下がります。
- カットや BGM がない場合、ステップ 1 の完成品が最終的な完成品となるため、このステップを実行する必要はありません。
ステップ 7: 検収
スクリプトは最後に一度ラウドネスを再測定しています。さらに独立して測定する場合は(--check で表示されたパスの ffmpeg を使用):
"<ffmpeg パス>" -hide_banner -nostats -i <完成品.mp3> -filter_complex ebur128=peak=true -f null - 2>&1 | tail -12
- 合格基準: Integrated が -16 LUFS ±1 以内、True peak が -1.5 dBFS(dBTP)以下。
- ユーザーにヘッドホンで聴いてもらいます。音楽が音声を覆い隠していないこと、ポーズ中に音楽が突然戻ってこないこと、各カットポイントが自然であること、最後が途中で切れていないこと、意図的に残したポーズがそのまま残っていること。
- 引き渡し時にユーザーに伝えること:完成品の場所、ノイズ除去方法、ラウドネスとトゥルーピークの実測値、カットした箇所。基準を満たしていない場合はそのまま伝え、合格したとは言わないでください。
四、編集の原則
- 事故のみをカットし、呼吸はカットしない:言い間違い、原稿を見ているときの注意散漫、言い直しが必要な不要なセクションはカットします。呼吸、考えているときの「うーん」、文と文の間のためらい、意図的に残した沈黙はカットしません。迷ったら、残します。
- 0.8 秒未満のポーズは変更しません。0.8~1.5 秒の文間のポーズはデフォルトですべて残します。それより長いポーズは、間や注意散漫の証拠がある場合にのみ短縮します。
- カットポイントは自然な息継ぎの場所を選び、単語の頭や末尾の音を切らないようにします。呼吸はすべて残すか、すべて削除するかのどちらかにします。
- BGM は音声よりも音量を低くし、人の声が入っている曲や中域(1~3kHz)が豊かな曲は避けます。メロディックな音楽は、段落間(break)に配置することを優先し、音声の下に敷かないようにします。音楽の総時間は、通常、完成品の 15% を超えないようにします。
- 詳細な方法については、
references/editing-style-guide.md(スタイルとパッケージング)、references/editing-essentials.md(編集実行のポイントと検収)、用語についてはreferences/audio-glossary.mdを参照してください。
五、環境変数(すべて省略可能)
| 変数 | デフォルト | 効果 |
|---|---|---|
FFMPEG | 自動検出 | ffmpeg のパスを指定 |
DENOISE | rnnoise | rnnoise / afftdn / off |
RNNOISE_MODEL | ~/.cache/ethan-podcast-post/bd.rnnn | ノイズ除去モデルの場所。モデルがすでにある場合は直接指定可能 |
RNNOISE_URL | GitHub 上の公開モデル | ネットワークに問題がある場合、ミラーアドレスに変更可能(ダウンロード後も SHA-256 を検証) |
INPUT_GAIN | 0 | 処理前に録音全体を何 dB 上げる(または下げる)か。録音が小さすぎる場合、エラーメッセージの推奨値に設定 |
TRIM_THRESHOLD | -50 | 前後の無音をトリミングするしきい値(dB)。冒頭や末尾の小さな音が切り取られる場合は、-60 などに下げる |
INTERVIEW_MODE | 0 | 1 = 2人以上の対談 |
KEEP_MASTER | 0 | 1 = 音声マスター voice_master.wav を保持 |
MASTER_WAV | 空 | 二次処理で使用する音声マスター |
MUSIC_VOLUME | 0.12 | 背景音楽の音量比率 |
INTRO_DURATION | 40 | オープニング/エンディングBGMを適用する時間枠(秒)。音声がこの 2 倍より短い場合は追加されない |
PREROLL / TAIL | 3 / 7 | オープニング音楽が先行する秒数 / 音声終了後に音楽が残る秒数 |
OUTRO_SEEK | 空 | エンディング音楽を曲の何秒目から取得するか |
MP3_BITRATE | 160k | 完成品のビットレート |
OVERWRITE | 0 | 1 = 既存の完成品の上書きを許可(事前にユーザーの同意を得ること) |
六、よくある質問
- ffmpeg が見つからない:ユーザーにインストール(macOS:
brew install ffmpeg、Linux: パッケージマネージャ)してもらうか、FFMPEG=/完全なパス/ffmpegで指定します。 - ffmpeg のバージョン:スクリプトはバージョン番号を読み取ります。250Hz 動的イコライザーのパラメータの意味は ffmpeg 6 と 8 で変わっていますが、両方の記法を正弦波信号で校正しており、効果は同じです。検証されていない 7.x などのバージョンの場合は、この段をスキップして警告を表示し、残りの処理は通常通り行われます。前後の無音をトリミングするパラメータも新しいバージョンの動作に合わせて調整されており、6.x と 8.x で同じようにカットされます。
- エラー「前後の無音をトリミングした後、ほとんど何も残っていない」または「処理後にラウドネスが測定できない」:録音が小さすぎるか、空です。音声が含まれていることを確認した後、エラーメッセージに表示された
INPUT_GAINの推奨値を指定して再実行します(ラウドネスが -70 LUFS 未満の場合は測定できません。トリミングしきい値を調整するだけでは不十分です)。録音に本当に音声がない場合は、エラーが続き、空の完成品は出力されません。 - ノイズ除去に afftdn が使用された:モデルのダウンロードに失敗したことを意味します。
RNNOISE_URLをミラーに変更するか、RNNOISE_MODELで既存のモデルファイルを指定してから再実行できます(既存の完成品を上書きする前にユーザーに確認します)。 - 末尾の音量が -80dB 未満:録音が途中で切れている可能性が高いです。まずユーザーに確認し、編集の問題として扱わないでください。
- リテイクセクションが一つも見つからない:字幕が
voice_master.wavから文字起こしされたことを確認し、文字起こし内で合図がどのような文字に書き換えられているかを確認し、--phraseでその表記を追加します。 - 各ステップのコマンドを確認したい:
bash -x <スキルディレクトリ>/scripts/edit_podcast.sh ...を実行します。 - zsh で ffmpeg のフィルターパラメータを自分で構築する場合:変数の直後にコロンが続く場合は、
${VAR}:...と記述します。そうしないと、zsh は$VAR:aや$VAR:rを修飾子として解釈し、パラメータが静かに変更されてしまいます。
ライセンス: コードは MIT、ドキュメントは CC BY 4.0。