会議の録音、顧客との通話、セミナー動画——「あとで文字にしたい音声」は現場に溢れています。コールセンターの事例研究で書いたとおり、音声認識(ASR)は「使える閾値」を越え、業務システムの部品になりました。
このハンズオンでは、その部品を自分の手で動かします。題材はWhisper[1]——正確には、その高速再実装であるfaster-whisper[3]を使い、日本語音声の書き起こしパイプラインをローカルで組みます。API課金なし・音声を外部に送らない構成なので、社内音声の検証にも使いやすいはずです。
本記事のコードは faster-whisper 1.2.1 / macOS 26 / CPU(int8量子化)で動作確認済みです。実行結果もその実測値を掲載しています。
なぜ faster-whisper か
Whisperの公式実装[2]はPyTorchベースで、そのまま使うとCPUでは重めです。faster-whisperは推論エンジンCTranslate2による再実装で、同じモデル・ほぼ同じAPIのまま、数倍速く・省メモリに動きます。GPUがない環境でも、int8量子化でsmallモデルが実用速度で回ります。ローカル書き起こしの現在の定番です。
セットアップと最小コード
python -m venv .venv && source .venv/bin/activate
pip install faster-whisper
書き起こしの最小コードはこれだけです。
from faster_whisper import WhisperModel
# GPUがあれば device="cuda"。CPUなら int8 量子化が実用的
model = WhisperModel("small", device="cpu", compute_type="int8")
segments, info = model.transcribe(
"sample.wav",
language="ja", # 言語が分かっているなら指定した方が安定する
vad_filter=True, # 無音区間をスキップ(長い録音・通話で効く)
)
print(f"言語: {info.language}(確度 {info.language_probability:.2f})")
for seg in segments:
print(f"[{seg.start:7.2f}s -> {seg.end:7.2f}s] {seg.text}")
初回実行時はモデル(smallで約500MB)が自動ダウンロードされます。segmentsはジェネレータで、イテレートした時点で初めて推論が走る点だけ注意してください(リストにしたければ list(segments))。
実測 — 動かすと何が起きるか
手元で検証用の音声(合成音声で「お電話ありがとうございます。本日はどのようなご用件でしょうか。先日購入した製品の返品についてお伺いしたいです」と読み上げたWAV)を作り、モデルサイズを変えて書き起こした実測結果です。
| モデル | 出力(実測) |
|---|---|
| tiny | 「HASILA、SFG。」(崩壊。日本語として成立せず) |
| small | 「お電話ありがとうございます。本日はどのようなご要件でしょうか。先日購入した製品の変品についてお伺いしたいです。」 |
この実測から、実務に直結する教訓が2つ読み取れます。
- 日本語はsmall以上を使う。tinyやbaseは英語なら動きますが、日本語では実用になりません。品質重視なら large-v3、速度と品質のバランスなら small〜medium が目安です
- 誤りは「同音異義語」に集中する。smallの結果は文構造こそ完璧ですが、「用件→要件」「返品→変品」と、音が同じ単語の変換を外しています。ASRの誤りはランダムではなく、ドメイン用語と同音異義語に偏る——この性質が、次の補正テクニックにつながります
ドメイン用語の補正 — initial_prompt と後処理
固有名詞・専門用語の誤変換への対策は2段構えです。
segments, info = model.transcribe(
"sample.wav",
language="ja",
vad_filter=True,
# 冒頭の「文脈」として用語を与えると、表記が引っ張られる
initial_prompt="通話記録。返品、用件、注文番号、配送状況。",
)
initial_prompt: モデルに「直前の文脈」として渡すテキストで、ドメイン用語や表記(漢字/カタカナ)をここに並べると出力がその表記に寄ります。万能ではありませんが、コストゼロで試せる第一手です- 後処理の置換辞書: それでも残る定番の誤変換(社名・製品名)は、正規表現の置換テーブルで補正します。コールセンター事例で「実務の大半は用語整備」と書いたのは、まさにこの層のことです
フォルダ一括処理パイプライン
実務では「録音フォルダを毎晩まとめてテキスト化」が基本形です。
from pathlib import Path
from faster_whisper import WhisperModel
AUDIO_DIR = Path("recordings")
OUT_DIR = Path("transcripts")
OUT_DIR.mkdir(exist_ok=True)
model = WhisperModel("small", device="cpu", compute_type="int8")
for audio in sorted(AUDIO_DIR.glob("*.[wm][ap][v3]*")): # wav / mp3 / m4a
out = OUT_DIR / f"{audio.stem}.txt"
if out.exists():
continue # 処理済みはスキップ(再実行に強くする)
segments, _ = model.transcribe(str(audio), language="ja", vad_filter=True)
text = "\n".join(seg.text.strip() for seg in segments)
out.write_text(text, encoding="utf-8")
print(f"done: {audio.name}")
「処理済みスキップ」を入れておくと、失敗しても再実行するだけで続きから走ります。夜間バッチはこの冪等性が生命線です。
おまけ: 字幕(SRT)を出す
タイムスタンプがあるので、字幕ファイルも数行で作れます。
def to_srt(segments) -> str:
def fmt(t: float) -> str:
h, rest = divmod(t, 3600)
m, s = divmod(rest, 60)
return f"{int(h):02}:{int(m):02}:{int(s):02},{int((s % 1) * 1000):03}"
lines = []
for i, seg in enumerate(segments, 1):
lines += [str(i), f"{fmt(seg.start)} --> {fmt(seg.end)}", seg.text.strip(), ""]
return "\n".join(lines)
動画に字幕を焼きたい場合は、この出力をffmpegに渡すだけです。
運用に載せる前の注意
- 音声は要配慮の塊: 通話・会議録音には個人情報が含まれます。ローカル処理はその点で有利ですが、書き起こしテキストの保存先・アクセス権・保持期間の設計はデータ管理の問題として残ります
- 話者分離は別技術: 「誰が話したか」(ダイアライゼーション)はWhisperの守備範囲外で、pyannote系など別のモデルを組み合わせます。まず「全文が文字になる」価値から始めるのが定石です
- リアルタイムは別設計: 本記事はバッチ処理です。通話中のリアルタイム書き起こしはストリーミング対応の構成が別途必要で、難易度が一段上がります
次のステップ — 書き起こしは素材にすぎない
テキスト化した瞬間、音声はLLMの入力になります。要約して対応記録の下書きにする、構造化出力で用件・約束事項を抽出する、全文を検索対象にする——コールセンターの4層スタックの残り3層は、今日作ったこのパイプラインの上に載ります。
まとめ
- faster-whisperなら、GPUなし・API課金なしで日本語書き起こしがローカルで動く
- 日本語はsmall以上。tinyは実測で崩壊した。品質重視ならlarge-v3
- ASRの誤りは同音異義語・ドメイン用語に偏る。
initial_promptと置換辞書の2段で補正する - 実務の基本形は「フォルダ一括+処理済みスキップ」の冪等なバッチ
- 話者分離・リアルタイムは別技術。まず「文字になる」価値から始める
30分あれば、手元の録音が検索できるテキストに変わります。まずは自分の会議録音1本から試してみてください。
参考文献・一次情報
- [1]PAPERRobust Speech Recognition via Large-Scale Weak Supervision (Whisper)
Whisperの原論文。68万時間の弱教師あり学習による頑健なASR
- [2]
- [3]