ScribeとOfox APIで録音を文字起こしし、SRT字幕にする

Ofox経由のScribeで音声を書き起こし、単語の時刻からSRT字幕を作成してMP4に追加します。実際の音声、応答、変換スクリプト付き。

青みがかったグレーの背景にタイプライターの線画。黄色い円とScribe: Audio to Subtitlesの文字。

Ofoxの /v1/audio/transcriptions にWAVまたはMP3を送り、verbose_json の語タイムスタンプを確認してから、ローカルでSRTを作成します。ここで検証したゲートウェイはJSON出力を使うため、上流サービスのSRT指定がそのまま通るとは扱いません。

2026年10月10日、ElevenLabsで実際に生成した10.00秒の音声をScribeへ送りました。句読点の違いを除き原稿の語が返り、語の範囲は0.14~9.78秒でした。3つの字幕キューへ変換し、MP4の選択可能な英語字幕トラックに格納しました。短い合成音声での一連の処理例であり、雑音のある会議の認識精度テストではありません。

ファイルの役割を分ける

キットには音声、未編集JSON、変換器、SRT、字幕トラック付きMP4が含まれます。どの時間情報から字幕を作ったか確認できるよう、一緒に保管してください。

ElevenLabsの生成音声

ファイル役割証明しないこと
elevenlabs.mp3実際の入力音声雑音下の人声への性能
narration-transcript.jsonScribeの元応答人が校正済みの最終原稿
narration.srt語をまとめた字幕すべての再生環境で同じ表示
subtitled-selectable.mp4映像・音声・字幕の格納文字が映像へ焼き付けられたこと

本文の原稿は独自に用意し、音声は合成しています。顧客の録音ではありません。実際のインタビューや授業を使う場合は、選択したクラウドサービスへ送れる権限を確認してください。録音を聞けることと、外部へアップロードできることは別です。

1. 元のタイムラインを残して音声を用意する

検証した経路はWAVとMP3を受け付けます。動画から取り出す場合は原本を残し、変換コマンドも記録します。後で字幕のずれを調べるとき、無音の削除や編集があったか分からない状態を避けます。

ffmpeg -i original-video.mp4 -vn -c:a pcm_s16le recording.wav
ffprobe -v error -show_entries format=duration \
  -of default=noprint_wrappers=1:nokey=1 recording.wav

非圧縮WAVは大きくなりますが、元の声の品質が改善するわけではありません。対応形式と現在のアップロード制限を調べ、長い録音も必ず1回で送れるとは考えないでください。

最初の転写前に不要な編集をしない方が切り分けやすくなります。先頭2秒を切れば、元動画に対する以後の時刻は2秒ずれます。意図的に一部分だけ処理するときは、その開始位置を保存し、全体に戻す際にオフセットを加えます。

複数音声トラックのある動画では対象を明示します。解説、翻訳、無音トラックを誤って選ばないよう、ストリームを調べ、抽出したファイルを聞いてから転写します。

2. multipartで正しいモデルへ送る

OFOX_API_KEY を設定し、Pythonの requests、FFmpeg、ffprobe を準備します。

python3 audio_api.py transcribe \
  --input elevenlabs.mp3 \
  --output my-transcript.json

クライアントは elevenlabs/scribe_v2 と verbose_json を送信します。ファイルをバイナリで開き、multipartの境界はライブラリに任せます。応答と入力の長さを保存し、失敗や結果不明のPOSTを自動反復しません。

同等のcURL例はこちらです。

curl --fail-with-body --silent --show-error \
  https://api.ofox.io/v1/audio/transcriptions \
  -H "Authorization: Bearer $OFOX_API_KEY" \
  -F 'model=elevenlabs/scribe_v2' \
  -F 'response_format=verbose_json' \
  -F 'file=@elevenlabs.mp3;type=audio/mpeg' \
  --output my-transcript.json

どちらか一方を使います。両方実行すれば再度課金対象の呼び出しになります。境界のない Content-Type: multipart/form-data を手動設定しないでください。認証ヘッダーとフォームのモデル・ファイル指定は役割が異なります。

現在のOfoxモデルページを確認します。ElevenLabsのネイティブ資料は参考になりますが、そこで説明される機能をアダプターがすべて転送するとは限りません。

3. 変換器を書く前に応答の形を見る

今回のJSONには text、language、duration、usage、logprobs、words があります。各語は word、start、end を持ちます。別のAPI向けコードが segments や語単位の text を期待していても、実際の応答の代わりにはなりません。

最初の文は“A clear product video starts with a clear brief.”です。最後の語は9.78秒で終わり、コンテナは10.00秒です。末尾無音や符号化の余白があるため、発話終端とファイル終端は同じとは限りません。

保存済み応答の検査には新しいAPI呼び出しは不要です。

import json
from pathlib import Path
result = json.loads(Path('my-transcript.json').read_text())
print(result['text'])
print(result.get('language'))
print(result.get('usage'))
for item in result.get('words', [])[:5]:
    print(item['start'], item['end'], item['word'])

表記を直す前に元JSONを残します。words がなければ、テキストだけから正確な字幕タイミングは復元できません。タイムスタンプ付き応答か別のアラインメントを用意します。長さを単語数で等分しても実測の代わりにはなりません。

人名が発話に登場しても、話者の本人確認にはなりません。本例は語時刻を示すもので、検証された話者IDではありません。会議からアクションを抽出する手順でもこの区別を保ちます。

4. 語を読みやすいキューへまとめる

SRTには連番、開始・終了、本文が必要です。make_subtitles.py は文字数と時間で語をまとめ、各グループの最初と最後の語の時刻を使います。新たな位置合わせを推測しているわけではありません。

python3 make_subtitles.py my-transcript.json my-subtitles.srt

今回の先頭キューです。

1
00:00:00,140 --> 00:00:02,980
A clear product video starts with a clear brief.

残りは3.56~7.36秒、7.42~9.78秒です。第1文の後の間を保ち、タイムラインを埋めるためだけに字幕を延長しません。

58文字・5秒という分割目安は、この短い英語用の実装判断です。放送やアクセシビリティの共通基準ではありません。日本語や韓国語では語間隔や分割方法が違うため、携帯画面で読める速度や改行を含めて調整します。

境界を一つずつ確認します。サンプル第2キューは“and”で終わり、形式上は有効でも編集上は改善の余地があります。隣の語を移すならその語の実時刻を使い、修正版を別保存します。元転写を上書きして変更を見えなくしないでください。

変換器は欠落、負値、非有限値、逆行する開始時刻を拒否し、修正版では語の重なりも拒否します。それでも表示が短すぎる、文の切れ方が悪いなどの問題は残り得ます。構造検査と読みやすさの確認は別です。

5. 校正の根拠を残す

音声と承認済み資料を比較し、句読点の正規化と意味の誤りを分けます。本例では語は一致しましたが末尾の句読点が省かれました。ブランド名の誤認識や否定語の欠落と同じ扱いにはしません。

実際の録音では氏名、数値、日付、単位、否定を優先します。スライドと違う発言を、説明なくスライド側へ変更しないでください。不確かな箇所は保留し、権限のある確認者へ回します。

校正表には元の表現、修正案、音声区間、理由、確認状態を残します。元音声、JSON、編集したSRTを分ければ、なぜ修正したかを具体的な区間で説明できます。

6. 字幕をMP4に格納して確認する

次の例は映像と音声を変更せず、選択可能な字幕トラックを追加します。

ffmpeg -i narration-video.mp4 -i my-subtitles.srt \
  -map 0:v:0 -map 0:a:0 -map 1:0 \
  -c:v copy -c:a copy -c:s mov_text \
  -metadata:s:s:0 language=eng \
  -disposition:s:0 default subtitled-selectable.mp4

字幕に合う言語コードを指定します。これは焼き付けではありません。デスクトップで表示されてもWebプレーヤーが無視する場合があり、default指定も全環境での表示保証ではありません。

付属の10秒MP4はH.264映像、AAC音声、mov_text 字幕を含みます。以前の教材動画の一部に今回のナレーションを組み合わせてMP4に格納した例で、APIが映像を生成した証拠ではありません。

ストリームを確認し、字幕を取り出して照合できます。

ffprobe -v error -show_entries stream=codec_type,codec_name \
  -of json subtitled-selectable.mp4
ffmpeg -i subtitled-selectable.mp4 -map 0:s:0 \
  recovered-subtitles.srt

焼き付けには文字描画に対応した環境と再エンコードが必要です。今回のFFmpegには試した字幕フィルターがなく、納品例は選択式にしました。焼き付け済み、あるいは画面上の再生検収済みとは説明しません。投稿先では実際のアップロード後に表示を確認してください。

7. 失敗と同期ずれを切り分ける

形式エラーではゲートウェイが受け付ける入出力を確認します。それをもってScribeのネイティブ機能が存在しないとは判断しません。コピーを対応形式へ直してから再試行します。

割当を示す401ではエラー全文とリクエストIDを残します。このプロジェクトは上流経路の復旧後に成功しましたが、ウォレット残高だけでは呼び出し先のアカウントは分かりません。

一定のずれなら先頭カットや部分録音の開始位置を、徐々に広がるずれなら編集差や速度差を調べます。同じタイムラインか確認する前に各キューを勘で動かさないでください。

長い録音を分割する場合はオフセットと重複区間の照合を設計します。境界で語が重複したり文脈が失われたりします。この短い変換器は長尺の整列を自動解決するものではありません。

8. 用量と最終検収を混同しない

転写の用量は返った単語数と同義ではありません。本例のコンテナは10.00秒ですが、APIは usage.seconds=10.083265306122449 を報告しています。丸めて差を消さず、両方保存してください。最後の語時刻も請求量の代わりにはならず、用量だけで確定請求額を示すこともできません。

検収では、正しい許可済み録音、成功JSON、実語時刻、校正済み本文、対象プレーヤーの表示を確認します。キットはAPI・変換・コンテナを検証していますが、視聴者向けの表示は利用先で別途確認します。

よくある質問

Ofoxに直接SRTを要求できますか?
検証した経路はJSONまたはverbose JSONを使います。この記事では実際の語時刻をローカル変換し、ネイティブのSRT指定が転送されるとは仮定しません。
テキストだけでwordsがありません。
転写は保管し、時刻を作りません。タイムスタンプ応答または別の音文整列を用意します。
MP4の字幕は焼き付けですか?
いいえ。選択可能な mov_text トラックです。焼き付けでは文字を映像フレームへ描画します。
会議での精度を証明していますか?
いいえ。短い合成音声の例です。実会議の重複発話、雑音、人名、話者の曖昧さは別に評価します。