LESSON 30分

ストーリー

田中VPoE
Step 2で画像AIを学んだ。次は音声と動画の処理だ。まずは音声認識(STT: Speech-to-Text)から始めよう
あなた
会議の議事録作成や、コールセンターの通話分析に使えそうですね
田中VPoE
その通り。Whisperの登場で音声認識の精度は飛躍的に向上した。APIとローカル実行の両方を押さえておけば、コストと精度を最適化できる
あなた
まずはWhisperから学びたいです

音声認識(STT)の概要

主要なSTTモデル・サービス

モデル/サービス提供元特徴コスト目安
Whisper APIOpenAI高精度、多言語対応、日本語に強い$0.006/分
Whisper(OSS)OpenAIローカル実行可能、GPU推奨無料(GPU費用のみ)
Google Speech-to-TextGoogleリアルタイム対応、ストリーミング$0.006/15秒
Amazon TranscribeAWSAWS統合、カスタム語彙$0.024/分
Azure SpeechMicrosoftリアルタイム対応、カスタムモデル$1/音声1時間

Whisperのアーキテクチャ

音声入力(WAV/MP3/M4A)


[メルスペクトログラム変換]  ← 音声を周波数×時間の2D表現に


[Encoder(Transformer)]   ← 音声特徴量を抽出


[Decoder(Transformer)]   ← テキストを自己回帰的に生成


テキスト出力(タイムスタンプ付き)

Whisperモデルのサイズ比較

モデルパラメータ数必要VRAM処理速度精度
tiny39M~1GB最速
base74M~1GB速い中低
small244M~2GB
medium769M~5GBやや遅い中高
large-v31.55B~10GB遅い最高

Whisper APIでの音声認識

基本実装

from openai import OpenAI

client = OpenAI()

def transcribe_audio(audio_path: str, language: str = "ja") -> dict:
    """Whisper APIで音声をテキストに変換"""
    with open(audio_path, "rb") as audio_file:
        transcript = client.audio.transcriptions.create(
            model="whisper-1",
            file=audio_file,
            language=language,
            response_format="verbose_json",
            timestamp_granularities=["segment"]
        )
    return {
        "text": transcript.text,
        "segments": [
            {
                "start": seg.start,
                "end": seg.end,
                "text": seg.text
            }
            for seg in transcript.segments
        ]
    }

# 使用例
result = transcribe_audio("meeting_recording.mp3")
print(result["text"])
for seg in result["segments"]:
    print(f"[{seg['start']:.1f}s - {seg['end']:.1f}s] {seg['text']}")

ローカルWhisperの実行

import whisper
import torch

def transcribe_local(
    audio_path: str,
    model_size: str = "large-v3",
    language: str = "ja"
) -> dict:
    """ローカルWhisperで音声認識"""
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model = whisper.load_model(model_size, device=device)

    result = model.transcribe(
        audio_path,
        language=language,
        task="transcribe",
        verbose=False,
        beam_size=5,
        best_of=5,
        temperature=0.0
    )

    return {
        "text": result["text"],
        "segments": [
            {
                "start": seg["start"],
                "end": seg["end"],
                "text": seg["text"]
            }
            for seg in result["segments"]
        ],
        "language": result["language"]
    }

音声の前処理

なぜ前処理が重要か

問題影響対処法
ノイズ混入認識精度の低下ノイズリダクション
音量が小さい認識漏れ音量正規化
サンプルレート不一致モデル入力エラー16kHzにリサンプリング
長時間音声メモリ不足チャンク分割
複数話者話者の区別不可話者分離(ダイアライゼーション)

前処理パイプライン

from pydub import AudioSegment

def preprocess_audio(
    input_path: str,
    output_path: str,
    target_sample_rate: int = 16000,
    normalize_dbfs: float = -20.0
) -> str:
    """音声ファイルの前処理"""
    # 読み込み
    audio = AudioSegment.from_file(input_path)

    # モノラルに変換
    audio = audio.set_channels(1)

    # サンプルレート変換
    audio = audio.set_frame_rate(target_sample_rate)

    # 音量正規化
    change_in_dbfs = normalize_dbfs - audio.dBFS
    audio = audio.apply_gain(change_in_dbfs)

    # 無音区間の除去(先頭・末尾)
    from pydub.silence import detect_leading_silence
    start_trim = detect_leading_silence(audio, silence_threshold=-40)
    end_trim = detect_leading_silence(audio.reverse(), silence_threshold=-40)
    audio = audio[start_trim:len(audio) - end_trim]

    # 出力
    audio.export(output_path, format="wav")
    return output_path


def split_audio_chunks(
    audio_path: str,
    chunk_duration_ms: int = 30 * 60 * 1000,  # 30分
    overlap_ms: int = 5000  # 5秒のオーバーラップ
) -> list[str]:
    """長時間音声をチャンクに分割"""
    audio = AudioSegment.from_file(audio_path)
    chunks = []

    for i in range(0, len(audio), chunk_duration_ms - overlap_ms):
        chunk = audio[i:i + chunk_duration_ms]
        chunk_path = f"chunk_{i // chunk_duration_ms}.wav"
        chunk.export(chunk_path, format="wav")
        chunks.append(chunk_path)

    return chunks

話者分離(ダイアライゼーション)

概要

会議録音のように複数人が話す音声で、「誰がいつ話したか」を特定する技術です。

音声ストリーム


[話者分離モデル] → Speaker A: 0:00-0:15, 0:45-1:00
(pyannote.audio)   Speaker B: 0:15-0:45, 1:00-1:20


[STT] → 各話者の発言をテキスト化


話者ラベル付きテキスト

実装例

from pyannote.audio import Pipeline

def diarize_audio(audio_path: str, num_speakers: int = None) -> list[dict]:
    """話者分離を実行"""
    pipeline = Pipeline.from_pretrained(
        "pyannote/speaker-diarization-3.1",
        use_auth_token="YOUR_HF_TOKEN"
    )

    if num_speakers:
        diarization = pipeline(audio_path, num_speakers=num_speakers)
    else:
        diarization = pipeline(audio_path)

    segments = []
    for turn, _, speaker in diarization.itertracks(yield_label=True):
        segments.append({
            "start": turn.start,
            "end": turn.end,
            "speaker": speaker
        })

    return segments


def transcribe_with_speakers(
    audio_path: str,
    num_speakers: int = None
) -> list[dict]:
    """話者分離 + 音声認識"""
    # 1. 話者分離
    speaker_segments = diarize_audio(audio_path, num_speakers)

    # 2. 音声認識
    transcript = transcribe_local(audio_path)

    # 3. 話者ラベルとテキストを統合
    result = []
    for t_seg in transcript["segments"]:
        best_speaker = "Unknown"
        best_overlap = 0
        for s_seg in speaker_segments:
            overlap = min(t_seg["end"], s_seg["end"]) - max(t_seg["start"], s_seg["start"])
            if overlap > best_overlap:
                best_overlap = overlap
                best_speaker = s_seg["speaker"]

        result.append({
            "start": t_seg["start"],
            "end": t_seg["end"],
            "speaker": best_speaker,
            "text": t_seg["text"]
        })

    return result

まとめ

技術概要推奨ツール
STT基本音声をテキストに変換Whisper API / ローカルWhisper
前処理ノイズ除去、正規化、チャンク分割pydub, ffmpeg
話者分離誰がいつ話したかを特定pyannote.audio
統合処理STT + 話者分離の結果を統合カスタムパイプライン

チェックリスト

  • Whisper APIとローカルWhisperの違いを理解した
  • 音声の前処理(正規化、リサンプリング、チャンク分割)を実装できる
  • 話者分離の仕組みとSTTとの統合方法を把握した
  • 用途に応じたモデル・サービスの選定ができる

次のステップへ

次は音声合成(TTS)を学び、テキストから音声を生成する技術を習得します。


推定読了時間: 30分