「音声を文字起こししたい」——要件としては一行です。けれど本番に載せようとした瞬間、判断すべきことが一気に増えます。セルフホストするのか API を叩くのか。どのモデルを選ぶのか。25MB を超える長尺音声はどうするのか。固有名詞の誤変換をどう抑えるのか。無音区間で湧く「幻覚(hallucination)」をどう殺すのか。
この記事は、OpenAI Whisper を本番品質で運用するための実装ガイドです。題材として、私が国内大手放送事業者向けに構築した社内AIプラットフォーム(テロップ誤字検出パイプラインで「発話の文字起こし=耳」として音声認識を使用)での設計判断も交えます。
この記事のルール:モデル仕様・API仕様・価格は OpenAI公式ドキュメント(2026年6月時点) に基づきます。価格やモデル名は改定されるため、本番投入前に必ず公式の料金ページで最新値を確認してください。コードは実運用で使える形に整えていますが、シークレットは環境変数前提です(ハードコード厳禁)。
0. 最初の分岐:「Whisper」という言葉には2つの実体がある
ここを混同したまま設計を始めると、後で必ず詰まります。Whisper には性質の異なる2つの提供形態があります。
| ① セルフホスト(OSS) | ② OpenAI Audio API | |
|---|---|---|
| 実体 | openai-whisper(PyPI)/ Hugging Face の重み | api.openai.com のホスト型エンドポイント |
| 代表モデル | large-v3 / turbo(large-v3-turbo) | whisper-1 / gpt-4o-transcribe / gpt-4o-mini-transcribe |
| 実行場所 | 自前の GPU / CPU(音声が外に出ない) | OpenAI のサーバー(音声を送信する) |
| 課金 | 計算リソース(GPU時間) | 分課金 or トークン課金 |
| ファイル上限 | なし(メモリ次第) | 25MB / 1リクエスト |
| 主な強み | プライバシー・固定費・無制限長 | 運用ゼロ・高精度・即日導入 |
「Whisper を使う」と言ったとき、この①と②のどちらを指しているのかを最初に確定させてください。本記事は両方を扱い、選定基準(第3章)まで示します。
1. セルフホスト版:最新モデルと正しい使い方
1.1 モデル一覧(公式の数値)
公式 README のモデル表は次のとおりです。Relative speed は large を 1x とした相対値です。
| Size | Parameters | 必要VRAM(目安) | 相対速度 | English-only (.en) |
|---|---|---|---|---|
| tiny | 39M | ~1GB | ~10x | あり |
| base | 74M | ~1GB | ~7x | あり |
| small | 244M | ~2GB | ~4x | あり |
| medium | 769M | ~5GB | ~2x | あり |
| large | 1550M | ~10GB | 1x | なし(多言語のみ) |
| turbo | 809M | ~6GB | ~8x | なし |
turbo(= large-v3-turbo)が現行のスイートスポットです。large-v3 のデコーダ層を 32層 → 4層 に削った蒸留・剪定モデルで、精度の劣化を最小限に抑えつつ約8倍速を実現しています。日本語を含む多言語の「文字起こし」用途なら、まず turbo を試すのが定石です。
公式が明記する turbo の落とし穴:turbo は翻訳データを除いて学習されているため、
--task translateを指定しても元言語のまま返ることがあります。「日本語音声 → 英語テキスト」の翻訳が欲しいときはmediumかlargeを使ってください。英語のみのアプリでは.enモデルの方が精度が出る、とも公式は述べています。
1.2 インストール(ffmpeg必須)
# Python パッケージ
pip install -U openai-whisper
# 音声デコードに ffmpeg が必須(OSごとに導入)
# macOS: brew install ffmpeg
# Ubuntu/Debian: sudo apt update && sudo apt install ffmpeg
1.3 CLI:まず動かす
# turbo で文字起こし(複数ファイルもまとめて渡せる)
whisper audio.mp3 --model turbo --language Japanese --output_format srt
# 日本語音声を「英語に翻訳」したい場合は medium/large + translate
whisper meeting.wav --model large --language Japanese --task translate
--output_format に srt / vtt / txt / json を指定でき、字幕ファイルがそのまま出力されます。字幕生成が目的なら、自前でタイムスタンプを組み立てる前に CLI の出力形式で足りないかを先に確認してください(YAGNI)。
1.4 Python API:本番で使う形
最小例は3行ですが、本番では言語固定・単語タイムスタンプ・幻覚抑制を入れます。
import whisper
# モデルは一度だけロードして使い回す(プロセス内で再利用 = コールドスタート回避)
model = whisper.load_model("turbo")
result = model.transcribe(
"audio.mp3",
language="ja", # 言語を固定 → 言語自動検出のブレと初動コストを排除
word_timestamps=True, # 単語単位の時刻(字幕・検索・ハイライトに必須)
# --- 幻覚(hallucination)抑制の三点セット(詳細は第6章) ---
condition_on_previous_text=False, # 直前文への引きずられを断つ
no_speech_threshold=0.6, # 無音判定を厳しめに
temperature=0, # 決定的に(再現性とテスト容易性)
)
print(result["text"])
for seg in result["segments"]:
print(f"[{seg['start']:6.2f}–{seg['end']:6.2f}] {seg['text'].strip()}")
word_timestamps=True を付けると、各 segment に words(word / start / end / probability)が入ります。低 probability の単語=モデルが自信のない箇所なので、校正UIで黄色くハイライトする、といった「人間の確認ゲート」を設計に組み込めます。
設計のコツ:
modelはリクエストごとにロードしないこと。large系は読み込みだけで数秒〜十数秒かかります。Web サーバーならプロセス起動時に一度ロードし、ワーカーで共有します(SRP:ロードと推論を分離)。
2. OpenAI Audio API 版:運用ゼロで高精度を取りに行く
GPU を持ちたくない・即日で精度の高い文字起こしが欲しい——その場合はホスト型 API が最短です。
2.1 モデルとエンドポイント(公式の対応表)
| モデル | 用途 | 対応 response_format | ストリーミング |
|---|---|---|---|
gpt-4o-transcribe | 最高精度の文字起こし | json / text | ✅ (stream=True) |
gpt-4o-mini-transcribe | 低コスト・高速 | json / text | ✅ |
gpt-4o-transcribe-diarize | 話者分離つき | diarized_json ほか | — |
whisper-1 | 旧来の万能型・字幕形式が豊富 | json / text / srt / verbose_json / vtt | — |
gpt-4o-transcribe 系は GPT-4o を基盤に WER(単語誤り率)を改善した新世代で、認識精度と言語判定が向上しています。一方で SRT/VTT などの字幕形式や verbose_json の細粒度タイムスタンプが必要なら whisper-1 を選びます。「精度重視=gpt-4o系、字幕の機械可読フォーマット重視=whisper-1」と覚えると迷いません。
- Transcriptions(文字起こし):音声 →「元言語の」テキスト。
- Translations(翻訳):音声 → 英語テキスト。
whisper-1専用です。
2.2 基本:Python と TypeScript
from openai import OpenAI
client = OpenAI() # APIキーは環境変数 OPENAI_API_KEY から(ハードコード禁止)
with open("speech.mp3", "rb") as audio_file:
transcription = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=audio_file,
language="ja", # 言語を渡すと精度とレイテンシが安定する
)
print(transcription.text)
Next.js / Node(このサイトと同じ TypeScript)なら:
import OpenAI from "openai";
import { createReadStream } from "node:fs";
const openai = new OpenAI(); // process.env.OPENAI_API_KEY を自動参照
const transcription = await openai.audio.transcriptions.create({
file: createReadStream("audio.mp3"),
model: "gpt-4o-transcribe",
language: "ja",
});
console.log(transcription.text);
2.3 字幕(SRT)と単語タイムスタンプ:whisper-1
機械可読な字幕や単語単位の時刻が要るときは whisper-1 + verbose_json を使います。
with open("lecture.mp3", "rb") as audio_file:
result = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="ja",
response_format="verbose_json",
timestamp_granularities=["segment", "word"], # verbose_json 必須
)
# セグメント単位(字幕のかたまり)
for seg in result.segments:
print(f"[{seg.start:.2f}-{seg.end:.2f}] {seg.text}")
# 単語単位(検索インデックス・ハイライトに)
for w in result.words:
print(w.word, w.start, w.end)
SRT ファイルそのものが欲しいだけなら response_format="srt" を指定すれば、変換コードを書かずに字幕テキストが返ります(DRY:自前のSRTシリアライザを書かない)。
2.4 固有名詞・専門用語を正しく綴らせる:prompt
文字起こしで最も多いクレームは固有名詞・社名・専門用語の誤変換です。prompt パラメータに「期待する綴り」を渡すと、モデルがそれに寄せてくれます。
GLOSSARY = "登場する固有名詞: 友田陽大, ハコキット, Next.js, Supabase, RLS, gpt-4o-transcribe"
with open("podcast.mp3", "rb") as audio_file:
transcription = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=audio_file,
language="ja",
prompt=GLOSSARY, # ドメイン語彙を事前に教える
)
これは安価で効果が大きいチューニング手段です。番組制作の文字起こしでは「人名・番組名・商品名」の表記ゆれが事故に直結するため、案件ごとに用語集を prompt に流し込む運用が効きます。
2.5 ストリーミング:話しながら出す(gpt-4o系)
会議の議事録やライブ字幕のように「待たせたくない」UXでは、stream=True で部分結果を逐次受け取れます。
stream = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=open("speech.mp3", "rb"),
response_format="text",
stream=True, # gpt-4o-transcribe / mini で利用可
)
for event in stream:
# delta: 増分テキスト / done: 確定
if event.type == "transcript.text.delta":
print(event.delta, end="", flush=True)
elif event.type == "transcript.text.done":
print("\n--- 確定 ---")
マイク入力をリアルタイムに文字化する用途では、ファイルストリーミングではなく Realtime API(WebSocket / WebRTC)を使うのが本筋です。「録音済みファイルの逐次返却」と「ライブの逐次認識」は別物なので、要件を取り違えないでください。
3. 選定フレームワーク:セルフホスト vs API をどう決めるか
「どっちが正解」ではなく、4つの軸で要件に合う方を選びます。
| 判断軸 | セルフホスト(turbo/large)が有利 | Audio API が有利 |
|---|---|---|
| プライバシー | 医療・行政・社外秘で音声を外に出せない | 外部送信が許容される |
| コスト構造 | 大量・常時稼働で固定費にしたい(GPU償却) | 少量・スパイク的で変動費が合理的 |
| 長さ | 数時間の長尺を1本で回したい(上限なし) | 25MB に収まる/分割前提でよい |
| 運用体制 | GPU・モデル更新・スケールを運用できる | 運用したくない/即日で精度が欲しい |
コストの直感
公式の分課金(2026年6月時点・要確認)はおおむね次の水準です。
whisper-1/gpt-4o-transcribe:$0.006/分(≒ $0.36/時)gpt-4o-mini-transcribe:$0.003/分(≒ $0.18/時)
月に数十時間程度ならAPIが圧倒的に安い(GPUを1枚借りるより安く、運用ゼロ)。逆に、毎日数百時間を常時処理するようなワークロードでは、turbo をGPUインスタンスで回した方が単価が下がる損益分岐点が現れます。「まずAPIで価値検証 → 量が読めてからセルフホスト移行を検討」が、多くの案件で正しい順序です(YAGNI / コスト効率)。
4. 25MB の壁を越える:長尺音声のチャンク分割
API のファイル上限は 25MB。1時間の会議録音は普通に超えます。無音区間(VAD)で切るのが定石で、文の途中でぶつ切りにしないため精度劣化を抑えられます。
"""長尺音声を無音で分割し、各チャンクを文字起こしして連結する。
固定長で切ると単語が割れるため、無音境界で切るのが要点。"""
from openai import OpenAI
from pydub import AudioSegment, silence
client = OpenAI()
def split_on_silence_bounded(path: str, max_ms: int = 15 * 60_000) -> list[AudioSegment]:
"""無音を境界に、各チャンクを max_ms 以下に収める(25MB制限の安全側)。"""
audio = AudioSegment.from_file(path)
silences = silence.detect_silence(audio, min_silence_len=700, silence_thresh=-40)
cut_points = [s[0] for s in silences] + [len(audio)]
chunks: list[AudioSegment] = []
start = 0
for point in cut_points:
if point - start >= max_ms:
chunks.append(audio[start:point])
start = point
if start < len(audio):
chunks.append(audio[start:])
return chunks
def transcribe_long(path: str, language: str = "ja") -> str:
parts: list[str] = []
context = "" # 直前チャンク末尾を次の prompt に渡し、境界の文脈を維持
for i, chunk in enumerate(split_on_silence_bounded(path)):
buf = chunk.export(format="mp3", bitrate="64k") # 軽量化で25MBに余裕
res = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=("chunk.mp3", buf, "audio/mpeg"),
language=language,
prompt=context[-200:], # 境界をまたぐ固有名詞・文脈のヒント
)
parts.append(res.text)
context = res.text
return "\n".join(parts)
ポイントは3つです。
- 無音で切る:固定長分割は単語を割って精度を落とす。VAD境界で切る。
- ビットレートを落とす:文字起こしに高音質は不要。64kbps mp3 で十分小さくなり、25MB の心配が消える(コスト効率)。
- チャンク間の文脈を渡す:前チャンク末尾を次の
promptに入れ、境界をまたぐ固有名詞・話題の連続性を保つ。
5. 本番運用設計:冪等・リトライ・可観測性
文字起こしは「長時間・外部API・課金あり」のジョブです。素朴に呼ぶと、途中失敗で全部やり直し&二重課金になります。放送局向けプラットフォームで実際に効いた設計を、最小の形で示します。
5.1 チャンク単位の冪等キャッシュ
各チャンクを内容ハッシュをキーにキャッシュすれば、再実行で完了済みチャンクをスキップでき、リトライが冪等になります(=部分失敗からの再開=二重課金の回避)。
import hashlib
from pathlib import Path
def chunk_key(data: bytes, model: str, language: str) -> str:
"""音声内容+パラメータで決まる決定的キー。同入力 → 同キー → キャッシュヒット。"""
h = hashlib.sha256()
h.update(data)
h.update(f"{model}:{language}".encode())
return h.hexdigest()
def transcribe_chunk_idempotent(data: bytes, model: str, language: str, cache_dir: Path) -> str:
key = chunk_key(data, model, language)
cached = cache_dir / f"{key}.txt"
if cached.exists():
return cached.read_text(encoding="utf-8") # 再実行はAPIを叩かない(冪等・節約)
res = client.audio.transcriptions.create(
model=model, file=("c.mp3", data, "audio/mpeg"), language=language,
)
cached.write_text(res.text, encoding="utf-8")
return res.text
5.2 指数バックオフ付きリトライ
外部APIはレート制限・一時障害で必ず失敗します。冪等な操作にだけリトライを掛けます。
import time
from openai import APIConnectionError, RateLimitError, APIStatusError
def with_retry(fn, *, max_attempts: int = 4, base: float = 1.0):
"""指数バックオフ。リトライ対象を限定し、4xx(=入力不正)は即時失敗させる。"""
for attempt in range(1, max_attempts + 1):
try:
return fn()
except (RateLimitError, APIConnectionError) as e:
if attempt == max_attempts:
raise
time.sleep(base * (2 ** (attempt - 1))) # 1s, 2s, 4s, ...
except APIStatusError as e:
if 400 <= e.status_code < 500:
raise # 入力不正はリトライしても無駄 → 即失敗(fail fast)
if attempt == max_attempts:
raise
time.sleep(base * (2 ** (attempt - 1)))
5.3 可観測性:何を必ず記録するか
文字起こしジョブでは、本文(PII)ではなくメタデータを記録します(プライバシーと可観測性の両立)。
- ジョブID / チャンク番号 / 入力ハッシュ / モデル / 言語
- 処理時間(音声長に対するリアルタイム比 = RTF)
- 課金量(分 or トークン)と推定コスト
- 失敗種別(レート制限 / タイムアウト / 入力不正)とリトライ回数
これらを構造化ログ(OpenTelemetry 等)で出すと、「どのチャンクで詰まったか」「コストが妥当か」が一目で追えます。本文ログにPII(個人名・連絡先)を残さないのは、内部統制案件の絶対条件です。
6. 幻覚(Hallucination)と無音問題:精度の最後の1割
Whisper の既知の弱点は、無音・雑音・BGMだけの区間で「ありもしない文」を生成することです。「ご視聴ありがとうございました」のような定型句が湧くのは典型例です。本番では次の多層対策で殺します。
- 前処理でVAD(音声区間検出):発話のない区間を推論前に落とす。
silero-vadなどで無音を除けば、幻覚の温床自体が消える。 condition_on_previous_text=False:直前テキストへの引きずられ(一度湧いた幻覚の連鎖)を断つ。- しきい値で捨てる:
no_speech_thresholdを上げ、logprob_thresholdで低確信セグメントを破棄。 - 独立した第二の根拠で照合:最強の対策はクロスチェックです。放送局のテロップ誤字検出では、「画面のテロップ(OCR=目)」と「発話の文字起こし(ASR=耳)」という独立した2系統を突き合わせ、食い違いを検出しました。単一情報源の確信度ではなく、2つの情報源の不一致を手がかりにすると、幻覚も誤変換も浮かび上がります。
1情報源の「自信」は当てになりません。**独立した2つの経路の「食い違い」**こそが信頼できる検出シグナルになる——これは音声認識に限らず、AIを本番に載せるときの一般原則です。
7. セキュリティとプライバシー:どこで線を引くか
- 音声は個人情報になり得る:声・氏名・連絡先・病歴が含まれ得ます。社外秘・要配慮個人情報を含む音声はAPIに送れないことが多い。その場合はセルフホスト(turbo/large)一択です。
- APIキーはサーバー側に:ブラウザから直接 OpenAI を叩かない(キー漏洩)。Next.js なら Route Handler / Server Action 経由で、キーは環境変数に。
- 入力の検証:ファイル形式(
mp3/mp4/mpeg/mpga/m4a/wav/webm)・サイズ(25MB)・長さを境界でバリデーションしてから API に渡す。ユーザー由来の入力を素通しにしない。 - 保存方針の明示:文字起こし結果を保存するなら、保持期間・暗号化・削除フローを最初に決める。PIIは AES-256-GCM 等で暗号化し、検索は復号せずトークン化(HMAC)で部分一致させる、といった設計が内部統制では標準です。
8. まとめ:選定チートシート
最後に、迷ったときの早見表です。
- とりあえず精度高く・即日で:
gpt-4o-transcribe(API)。言語を渡し、固有名詞はpromptで誘導。 - コストを最優先:
gpt-4o-mini-transcribe(API、$0.003/分)。 - SRT/VTT字幕・単語タイムスタンプが要る:
whisper-1+verbose_json。 - 音声を外に出せない/長尺を大量に回す:セルフホスト
turbo(多言語)/large(翻訳)。 - ライブ字幕:Realtime API。録音ファイルの逐次返却なら
stream=True。 - 本番化の共通装備:無音分割・冪等キャッシュ・指数バックオフ・PIIを出さない可観測性・幻覚の多層対策。
文字起こしは「一行の要件」に見えて、コスト・精度・プライバシー・信頼性のトレードオフを設計する仕事です。私は放送事業者向けの社内AIプラットフォームで、音声認識を「テロップ誤字検出の第二の目」として本番運用し、冪等・再開・可観測性を担保した長時間ジョブとして組み上げました。
「自社の音声をどう文字起こしし、どう業務に組み込むか」——その設計から実装・運用まで、一気通貫で伴走できます。 要件の整理段階からでも、お気軽にご相談ください。
参考(公式ドキュメント)
- OpenAI Whisper(GitHub・OSS) — モデル一覧・CLI・Python API・turbo の注意点
- openai/whisper-large-v3-turbo(Hugging Face) — turbo のアーキテクチャ(32→4層)
- OpenAI Speech-to-Text ガイド — Audio API のパラメータ・形式・ストリーミング
- OpenAI API 料金 — 分課金・トークン課金の最新値(本番投入前に要確認)