こんにちは。
以前faster-whisperとpyannoteを使った文字起こしについて記事を書きました。
この記事ではlarge-v3を使用していましたが、今回はlarge-v3-turbo(以下turbo)というモデルをおすすめする、アップデート記事となります。
今回は前回のlarge-v3との比較に加えて、最近主流になっているGeminiなどのLLM(API)による文字起こしとの比較も行いました。
検証した結果、LLMではなくfaster-whisperのturboモデルを使ってみるのは、今後の選択肢のひとつとしてアリなのではないかと思いましたので、気になる方は是非参考にしていただければと思います。
それではよろしくお願いします。
検証ソースコード
前回のソースコードから大幅に追加・修正しました。
またDocker環境だけでなく、uvでの動作もできるよう整備しました。
uv syncで、すぐにお試しできます。
本記事では、このリポジトリをベースに話を進めます。
検証のポイント
今回の検証のポイントは2点です。
- どのぐらい速くなったのか?
- どのぐらい精度が良いのか?
比較対象は以前使用したlarge-v3と、Geminiは最新のgemini-3-flashモデルを使います。
faster-whisperはpyannote(話者分離)+ whisper(文字起こし)の2段階処理となっていますが、Geminiは一括で話者分離と文字起こしした出力ができるよう、プロンプトとして指示します。
これもfaster-whisperと比較ができるよう、出力形式をそろえるようにします。(詳細は後述)
testデータとして、以下の動画の開始30secの音声データを使用します。
最近のイラン情勢は色々心配が広がりますね。。
開始30secで男性二人、女性一人の声を識別できるのか(これはPyannote vs Geminiとなりますが)にも注目です。
今回の検証では、CERやWERなどの一般的な評価指標を用いた細かい検証はしません。
簡易的に差異を確認しつつ、処理時間などの部分を定量的に検証するにとどめ、後は使う皆様の参考になればと考えます。
faster-whisper-large-v3-turbo
turboモデルは、本家としてはOpenAIのWhisperで2024年10月にリリースされています。
デコーダー層を32から4に削減し、知識蒸留を使うことで精度を担保しながら軽量化を行ったようです。
実際に上記リンク先を見ると、日本語でのエラー率は、large-v3やv2と比べても遜色ない結果となっていることが確認できます。
faster-whisperの実装は、前回同様こちらのライブラリを使用します。
pip installが可能なので、以下のコマンドでインストールします。
pip install faster-whisper==1.2.1uvなら以下になります。
uv add faster-whisper==1.2.1前回のver1.0.2だとturboモデルはまだ導入されていないため、ver1.2.1を使用します。
コードの書き方も簡単で、引数を「large-v3」から「turbo」に変えるだけです。
model = WhisperModel("turbo", device="cuda", compute_type="int8")このコードで実行すると、初回はモデルのダウンロードが発生します。
インターネットが使えないオフライン環境で動かすための方法も過去に紹介しました。
もしオフラインモデルを作りたい場合は以下のモデルをダウンロードしましょう。
モデルサイズもlarge-v3が3GBに対して1.5GBと約半分となっていることがわかります。
これはより低コストな実装化が期待できます。
手動でのダウンロードを紹介しましたが、以下のスクリプトで一括ダウンロードもできます。
from huggingface_hub import snapshot_download
# ローカルの指定したフォルダに全ファイルをダウンロード
snapshot_download(
repo_id="deepdml/faster-whisper-large-v3-turbo-ct2",
local_dir="./turbo_offline",
)引数にturboモデルを保存しているフォルダのパスを渡せばOKです。
model = WhisperModel("./turbo_offline", device="cuda", compute_type="int8")本記事ではこちらのオフラインコードで検証を進めていきます。
turboモデルについての解説は以上です。
Geminiの検証コードについて
faster-whisperのコードについては冒頭の記事で紹介した内容とほとんど変わらないため省略しますが、今回比較用にGeminiでの文字起こし・話者分離を行うスクリプトを作成したのでそれについて説明します。
import time
from pathlib import Path
from dotenv import load_dotenv
from google import genai
import os
# src から実行してもプロジェクト直下の .env を読む
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
load_dotenv()
gemini_api_key = os.environ["GEMINI_API_KEY"]
# transcript.py の test.json と同一スキーマ(id, start, end, text, speaker)になるよう指示する
TRANSCRIPT_OUTPUT_PROMPT = """\
あなたは音声の話者分離(ダイアライゼーション)と文字起こしを行います。
【出力】
説明文・前置き・マークダウンのコードフェンス(```)は一切付けず、JSON 配列だけを出力してください。先頭が `[`、末尾が `]` となるようにし、パース可能な JSON のみとします。
各要素は次のキーを必ず持つオブジェクトにしてください(transcript.py が書き出す test.json と同じ形):
- "id": 整数。先頭の発話セグメントから 1 始まりの連番。
- "start": 文字列。開始時刻を "HH:MM:SS.mmm"(24時間制、ミリ秒は3桁ゼロ埋め)。例: "00:01:23.456"
- "end": 文字列。終了時刻。形式は start と同じ。
- "text": 文字列。その区間の発話テキスト(前後の空白は除く)。
- "speaker": 文字列。話者IDは "SPEAKER_00", "SPEAKER_01", ... のように2桁ゼロ埋め。同一話者には常に同じIDを付与する。
【ルール】
- 音声の先頭を 00:00:00.000 とし、start/end は実際の経過時間に合わせる。
- 聞き取れる発話を意味のまとまりでセグメント化する。過度な碎片化は避ける。
- 標準的な JSON の文字列エスケープに従う(改行や引用符を text に含める場合はエスケープ)。
上記のみを満たす JSON 配列を出力し、それ以外は書かないでください。
"""
MODEL = "gemini-3-flash-preview"
# Gemini 3 Flash プレビュー(標準・有料階層)USD / 100 万トークン
# https://ai.google.dev/gemini-api/docs/pricing?hl=ja#gemini-3-flash-preview
PRICE_INPUT_TEXT_IMAGE_VIDEO_PER_1M_USD = 0.50
PRICE_INPUT_AUDIO_PER_1M_USD = 1.00
PRICE_OUTPUT_INCL_THOUGHTS_PER_1M_USD = 3.00
def estimate_cost_usd_gemini_3_flash_preview(
prompt_token_count: int,
candidates_token_count: int,
thoughts_token_count: int | None,
text_only_prompt_tokens: int | None,
) -> tuple[float, dict[str, float | int]]:
"""usage_metadata とプロンプト単体の count からおおよその USD を推定する。
入力単価はテキスト/画像/動画と音声で異なるため、プロンプト文字列のトークン数を
count_tokens で取得し、prompt の残りを音声側として按分する(近似)。
"""
thoughts = int(thoughts_token_count or 0)
out_tok = int(candidates_token_count or 0) + thoughts
if text_only_prompt_tokens is not None and text_only_prompt_tokens >= 0:
text_part = min(text_only_prompt_tokens, prompt_token_count)
audio_part = max(0, prompt_token_count - text_part)
else:
text_part = 0
audio_part = prompt_token_count
input_usd = (
text_part * PRICE_INPUT_TEXT_IMAGE_VIDEO_PER_1M_USD / 1_000_000
+ audio_part * PRICE_INPUT_AUDIO_PER_1M_USD / 1_000_000
)
output_usd = out_tok * PRICE_OUTPUT_INCL_THOUGHTS_PER_1M_USD / 1_000_000
total = input_usd + output_usd
breakdown: dict[str, float | int] = {
"input_text_tokens_est": text_part,
"input_audio_tokens_est": audio_part,
"output_tokens_incl_thoughts": out_tok,
"input_usd": round(input_usd, 6),
"output_usd": round(output_usd, 6),
"total_usd": round(total, 6),
}
return total, breakdown
client = genai.Client(api_key=gemini_api_key)
myfile = client.files.upload(file="output.wav")
text_only_token_result = client.models.count_tokens(
model=MODEL, contents=TRANSCRIPT_OUTPUT_PROMPT
)
text_only_tokens = getattr(text_only_token_result, "total_tokens", None)
if text_only_tokens is not None:
print(f"入力トークン(count_tokens・プロンプト文字列のみ): {text_only_tokens}")
# 入力のみのトークン数(generate 前の見積)。音声は概ね 32 トークン/秒など:
# https://ai.google.dev/gemini-api/docs/tokens?hl=ja#video-audio
input_token_result = client.models.count_tokens(
model=MODEL, contents=[TRANSCRIPT_OUTPUT_PROMPT, myfile]
)
input_total = getattr(input_token_result, "total_tokens", None)
if input_total is not None:
print(f"入力トークン(count_tokens): {input_total}")
else:
print(f"入力トークン(count_tokens): {input_token_result}")
transcription_start = time.perf_counter()
response = client.models.generate_content(
model=MODEL, contents=[TRANSCRIPT_OUTPUT_PROMPT, myfile]
)
transcription_sec = time.perf_counter() - transcription_start
print(response.text)
um = response.usage_metadata
if um is not None:
prompt_n = getattr(um, "prompt_token_count", None)
out_n = getattr(um, "candidates_token_count", None)
total_n = getattr(um, "total_token_count", None)
print(
"usage_metadata: "
f"prompt_token_count={prompt_n}, "
f"candidates_token_count={out_n}, "
f"total_token_count={total_n}"
)
thoughts = getattr(um, "thoughts_token_count", None)
cached = getattr(um, "cached_content_token_count", None)
if thoughts is not None:
print(f" thoughts_token_count={thoughts}")
if cached is not None:
print(f" cached_content_token_count={cached}")
if prompt_n is not None:
_, est = estimate_cost_usd_gemini_3_flash_preview(
prompt_token_count=prompt_n,
candidates_token_count=out_n or 0,
thoughts_token_count=thoughts,
text_only_prompt_tokens=text_only_tokens,
)
print(
"推定料金(gemini-3-flash-preview・標準・有料階層想定・USD・概算): "
f"${est['total_usd']:.6f}"
)
print(
f" 入力: テキスト相当 {est['input_text_tokens_est']} tok @ "
f"${PRICE_INPUT_TEXT_IMAGE_VIDEO_PER_1M_USD}/1M + "
f"音声相当 {est['input_audio_tokens_est']} tok @ "
f"${PRICE_INPUT_AUDIO_PER_1M_USD}/1M → ${est['input_usd']:.6f}"
)
print(
f" 出力(候補+思考): {est['output_tokens_incl_thoughts']} tok @ "
f"${PRICE_OUTPUT_INCL_THOUGHTS_PER_1M_USD}/1M → ${est['output_usd']:.6f}"
)
print(
" ※ 無料枠・バッチ・キャッシュ・為替・請求の丸めは未反映。"
" 入力のテキスト/音声按分は count_tokens(プロンプトのみ)による近似です。"
)
else:
print("usage_metadata: なし(API が返さない場合があります)")
print(f"文字起こし: {transcription_sec:.2f}秒")
実際の処理部分は過去にGeminiを使った記事で紹介したものと大体似たようなものです。
ポイントはプロンプトです。
faster-whisper側の文字起こし・話者分離の出力結果の構造は以下のようなJSON形式になっているため、それに合わせるよう指示をしています。
[
{
"id": 1,
"start": "00:00:00.000",
"end": "00:00:10.339",
"text": "アメリカイスラエルによるイランへの攻撃開始から今日で2週間です 情報が錯綜している面もありますので今日は一つ一つ情報を整理し",
"speaker": "SPEAKER_02"
},
{
"id": 2,
"start": "00:00:10.339",
"end": "00:00:18.920",
"text": "戦闘の集結をめぐる課題について最新情報を交えながら考えます いやー頃さんね今この状況を見ているとすんなり",
"speaker": "SPEAKER_00"
},
]あとは文字起こしに何秒かかったのかとか、トークンでどのぐらい消費したのか(=一回あたり料金としていくらかかる見積りなのか)の部分が大半となっています。
トークンや料金計算に関してはこちらを参考にしました。
概算程度の見積もり価格なので、実際とはズレがあるかもしれませんが、そこはご了承ください。。
では結果をみていきましょう。
比較結果
こちらが音声です。聞きながら確認してみてください。
Gemini 3 Flash
Geminiの出力結果は以下です。
[
{
"id": 1,
"start": "00:00:01.000",
"end": "00:00:04.000",
"text": "アメリカ・イスラエルによるイランへの攻撃開始から今日で2週間です。",
"speaker": "SPEAKER_00"
},
{
"id": 2,
"start": "00:00:05.000",
"end": "00:00:14.000",
"text": "情報が錯綜している面もありますので、今日は一つ一つ情報を整理し、戦闘の終結をめぐる課題について、最新情報を交えながら考えます。",
"speaker": "SPEAKER_01"
},
{
"id": 3,
"start": "00:00:15.000",
"end": "00:00:20.000",
"text": "うーん、矢後郎さん。これね、今この状況を見ているとすんなりこう終結するとは思えないんですけど。",
"speaker": "SPEAKER_02"
},
{
"id": 4,
"start": "00:00:21.000",
"end": "00:00:29.000",
"text": "まあ、戦争というのはいつも始めるのはね、えー、安くてこう終わらせるのは非常に難しいと。今回もトランプさんの言ってることがコロコロ変わるでしょう?",
"speaker": "SPEAKER_03"
}
]ほぼパーフェクトな文字起こしですね!
わずかに誤字として「いやー五郎さん」を「矢後郎さん」というのがあったり、わずかに助詞が違うなどはありますが、問題ないレベルですね。
id=0と3のSPEAKERは同じなので、そこは同じと認識してほしかったところです。
処理時間:話者分離と文字起こしで合計約20秒程度です。
推定料金:$0.017805
1ドル159円と考えると、大体約2.8円程度ですね。(※2026年3月時点のレート)
large-v3
続いてlarge-v3の出力結果は以下です。
[
{
"id": 1,
"start": "00:00:00.000",
"end": "00:00:10.339",
"text": "アメリカイスラエルによるイランへの攻撃開始から今日で2週間です 情報が錯綜している面もありますので今日は一つ一つ情報を整理し",
"speaker": "SPEAKER_02"
},
{
"id": 2,
"start": "00:00:10.339",
"end": "00:00:18.920",
"text": "戦闘の集結をめぐる課題について最新情報を交えながら考えます いやー頃さんね今この状況を見ているとすんなり",
"speaker": "SPEAKER_00"
},
{
"id": 3,
"start": "00:00:18.920",
"end": "00:00:25.980",
"text": "宿を集結するとは思えないんですよ戦争といつも始めるのはね 安く終わらせるのは非常に難しい",
"speaker": "SPEAKER_01"
},
{
"id": 4,
"start": "00:00:25.980",
"end": "00:00:31.219",
"text": "はい今回もトランプさんの言っていることはクロクロ変わるでしょ",
"speaker": "SPEAKER_01"
}
]それなりにあってますが、結構誤字が目立ちますね。
また人物ごとの区切り方も微妙な感じです。
さてfaster-whisper側はCPUで動かした時とGPUで動かした時の処理時間を出していきます。
精度については大きな違いはなかったため、全ての出力結果を表示することは割愛します。
ちなみに私のCPU,GPU情報を出しておくと
CPU:i7-13700H
GPU:GeForce RTX 4050 Laptop(4GB)
となっております。
GPU:話者分離18.40秒、文字起こし8.75秒
CPU:話者分離22.74秒、文字起こし89.49秒
Geminiよりも遅いですね。。
まぁGPUのスペックがGemini APIの方がきっとハイスペックだと思うので、対等ではないかもしれません。
ですが一般的なノートパソコンのGPUと仮定すれば、APIを叩く方が速そうです。
APIを使えない制限下だったり、コストを抑えたいのであれば、まぁ許容レベルといったところでしょうか。
pyannote側はGPU使ってもあまり高速化は期待できなさそうですね。
turbo
では本命のturboモデルです。
[
{
"id": 1,
"start": "00:00:00.000",
"end": "00:00:04.900",
"text": "アメリカ・イスラエルによるイランへの攻撃開始から 今日で2週間です",
"speaker": "SPEAKER_02"
},
{
"id": 2,
"start": "00:00:04.900",
"end": "00:00:10.320",
"text": "情報が錯綜している面もありますので 今日は一つ一つ情報を整理し",
"speaker": "SPEAKER_00"
},
{
"id": 3,
"start": "00:00:10.320",
"end": "00:00:15.400",
"text": "戦闘の終結をめぐる課題について 最新情報を交えながら考えます",
"speaker": "SPEAKER_00"
},
{
"id": 4,
"start": "00:00:15.400",
"end": "00:00:21.199",
"text": "いやー 五郎さん 今この状況を見ていると すんなり終結するとは思えないんですけど",
"speaker": "SPEAKER_02"
},
{
"id": 5,
"start": "00:00:21.199",
"end": "00:00:25.980",
"text": "戦争というのをいつも始めるのは 安く終わらせるのは非常に難しい",
"speaker": "SPEAKER_01"
},
{
"id": 6,
"start": "00:00:25.980",
"end": "00:00:29.300",
"text": "今回もトランプさんの言っていることは コロコロ変わるでしょう",
"speaker": "SPEAKER_01"
},
{
"id": 7,
"start": "00:00:29.300",
"end": "00:00:30.300",
"text": "うん",
"speaker": "SPEAKER_01"
}
]large-v3と同等のクオリティを想像していましたが、large-v3よりも精度が良いように見えます!
区切り方もlarge-v3に比べて綺麗に分けられていることがわかります!
Geminiが間違っていた「矢後郎さん」も正しく文字起こしできてますね!
話者分離も正しく、pyannoteも優秀ですね!
では処理時間はどうでしょうか?
GPU:話者分離17.23秒、文字起こし3.13秒
CPU:話者分離21.73秒、文字起こし32.78秒
ほぼGemini並みの速さですね!
faster-whisperではGPUを使えばCPUの約10倍速くなるようです。
インターネット不要で料金も電気代だけと考えれば、Geminiよりもこちらの方を使うのが良いかもしれませんね!
もちろん音声データが長くなれば、精度も処理時間も変わるかもしれませんが、LLMも長いデータは得意ではありませんし、料金も増加してしまいます。
この精度と速度なら、一度faster-whisperのturboモデルを採用してみるというのもアリなのではないでしょうか。
まとめ
結果をまとめると以下のようになります。
| モデル・構成 | 精度・特徴 | 処理時間(話者分離+文字起こし) | 料金・コスト |
| Gemini (API) | ほぼ完璧・微細な誤字(「矢後郎さん」等)や助詞のズレあり・同一話者(id=0と3)の統合に課題 | 合計:約20秒 | 約2.8円($0.017805) |
| large-v3(faster-whisper + pyannote) | 実用レベルだが課題あり・誤字が目立つ・人物ごとの区切り方が不自然 | 【GPU】合計:27.15秒(話者18.40秒+文字8.75秒)【CPU】合計:112.23秒(話者22.74秒+文字89.49秒) | 無料(電気代のみ・オフライン可) |
| turbo(faster-whisper + pyannote) | 非常に優秀(本命)・区切りが綺麗でlarge-v3より大幅改善・Geminiが間違えた単語も正確に認識 | 【GPU】合計:20.36秒(話者17.23秒+文字3.13秒)【CPU】合計:54.51秒(話者21.73秒+文字32.78秒) | 無料(電気代のみ・オフライン可) |
GeminiなどのLLMを使って日常業務のAI化をおこなっているかもしれません。
最近では低コスト化やセキュリティの観点からローカル化、最適化がトレンドになっています。
文字起こしなどの音声解析業務などで最適化をご検討中であれば、faster-whisperを試してみてはいかがでしょうか?
本記事がそんなときの参考になれば幸いです。
ここまでご覧いただきありがとうございました!




