# OpenAI Whisper 本番運用ガイド：セルフホスト（large-v3-turbo）と Audio API（gpt-4o-transcribe）を使い分ける文字起こし設計

> OpenAI Whisper を本番品質で使うための実装ガイド。公式ドキュメントに忠実なモデル一覧（large-v3 / turbo）と Audio API（whisper-1 / gpt-4o-transcribe / gpt-4o-mini-transcribe）を整理し、セルフホスト vs API の選定フレームワーク、25MB制限の回避、SRT字幕生成、固有名詞のprompt誘導、幻覚（hallucination）対策、冪等・再開・可観測性まで、実コードで解説します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: Python, 音声認識, OpenAI API, アーキテクチャ設計, パフォーマンス, 可観測性
- URL: https://tomodahinata.com/blog/openai-whisper-production-guide-selfhost-vs-api

## 要点

- Whisperにはセルフホスト（OSS）とOpenAI Audio APIの2つの実体があり、最初にどちらかを確定させる
- セルフホストはturbo（large-v3-turbo）が現行のスイートスポットで、字幕重視ならAPIはwhisper-1を選ぶ
- 選定はプライバシー・コスト構造・長さ・運用体制の4軸で、社外秘音声を出せないならセルフホスト一択
- 25MB制限は無音区間（VAD）で分割し、文の途中で割らず境界の文脈を次のpromptに渡す
- 幻覚対策は前処理VAD・しきい値破棄に加え、独立した第二の根拠（OCR×ASR）の食い違いで検出する

---

「音声を文字起こししたい」——要件としては一行です。けれど本番に載せようとした瞬間、判断すべきことが一気に増えます。**セルフホストするのか API を叩くのか。どのモデルを選ぶのか。25MB を超える長尺音声はどうするのか。固有名詞の誤変換をどう抑えるのか。無音区間で湧く「幻覚（hallucination）」をどう殺すのか。**

この記事は、OpenAI Whisper を**本番品質**で運用するための実装ガイドです。題材として、私が国内大手放送事業者向けに構築した社内AIプラットフォーム（[テロップ誤字検出パイプライン](/case-studies/broadcaster-ai-content-platform)で「発話の文字起こし＝耳」として音声認識を使用）での設計判断も交えます。

> **この記事のルール**：モデル仕様・API仕様・価格は **OpenAI公式ドキュメント（2026年6月時点）** に基づきます。価格やモデル名は改定されるため、本番投入前に必ず[公式の料金ページ](https://openai.com/api/pricing/)で最新値を確認してください。コードは実運用で使える形に整えていますが、シークレットは環境変数前提です（ハードコード厳禁）。

---

## 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必須）

```bash
# Python パッケージ
pip install -U openai-whisper

# 音声デコードに ffmpeg が必須（OSごとに導入）
# macOS:        brew install ffmpeg
# Ubuntu/Debian: sudo apt update && sudo apt install ffmpeg
```

### 1.3 CLI：まず動かす

```bash
# 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行ですが、本番では**言語固定・単語タイムスタンプ・幻覚抑制**を入れます。

```python
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

```python
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）なら:

```ts
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` を使います。

```python
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` パラメータに「期待する綴り」を渡すと、モデルがそれに寄せてくれます。

```python
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` で部分結果を逐次受け取れます。

```python
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）で切る**のが定石で、文の途中でぶつ切りにしないため精度劣化を抑えられます。

```python
"""長尺音声を無音で分割し、各チャンクを文字起こしして連結する。
固定長で切ると単語が割れるため、無音境界で切るのが要点。"""
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つです。

1. **無音で切る**：固定長分割は単語を割って精度を落とす。VAD境界で切る。
2. **ビットレートを落とす**：文字起こしに高音質は不要。64kbps mp3 で十分小さくなり、25MB の心配が消える（コスト効率）。
3. **チャンク間の文脈を渡す**：前チャンク末尾を次の `prompt` に入れ、境界をまたぐ固有名詞・話題の連続性を保つ。

---

## 5. 本番運用設計：冪等・リトライ・可観測性

文字起こしは「長時間・外部API・課金あり」のジョブです。**素朴に呼ぶと、途中失敗で全部やり直し＆二重課金**になります。放送局向けプラットフォームで実際に効いた設計を、最小の形で示します。

### 5.1 チャンク単位の冪等キャッシュ

各チャンクを**内容ハッシュをキー**にキャッシュすれば、再実行で完了済みチャンクをスキップでき、リトライが冪等になります（＝部分失敗からの再開＝二重課金の回避）。

```python
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はレート制限・一時障害で必ず失敗します。**冪等な操作にだけ**リトライを掛けます。

```python
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だけの区間で「ありもしない文」を生成する**ことです。「ご視聴ありがとうございました」のような定型句が湧くのは典型例です。本番では次の多層対策で殺します。

1. **前処理でVAD（音声区間検出）**：発話のない区間を推論前に落とす。`silero-vad` などで無音を除けば、幻覚の温床自体が消える。
2. **`condition_on_previous_text=False`**：直前テキストへの引きずられ（一度湧いた幻覚の連鎖）を断つ。
3. **しきい値で捨てる**：`no_speech_threshold` を上げ、`logprob_threshold` で低確信セグメントを破棄。
4. **独立した第二の根拠で照合**：最強の対策は**クロスチェック**です。放送局のテロップ誤字検出では、「画面のテロップ（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）](https://github.com/openai/whisper) — モデル一覧・CLI・Python API・turbo の注意点
- [openai/whisper-large-v3-turbo（Hugging Face）](https://huggingface.co/openai/whisper-large-v3-turbo) — turbo のアーキテクチャ（32→4層）
- [OpenAI Speech-to-Text ガイド](https://developers.openai.com/api/docs/guides/speech-to-text) — Audio API のパラメータ・形式・ストリーミング
- [OpenAI API 料金](https://openai.com/api/pricing/) — 分課金・トークン課金の最新値（本番投入前に要確認）
