2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReazonSpeechってなんだ?〜国産最高峰・35,000時間の日本語音声認識を完全攻略〜

2
Posted at

この記事の対象読者

  • Pythonの基本文法(関数、クラス、pip)を理解している方
  • 音声認識(Speech-to-Text)に興味がある方
  • Whisperは使ったことあるけど、日本語特化モデルを試したい方
  • ローカル環境で音声認識を動かしたい方

この記事で得られること

  • ReazonSpeechの概念と、なぜ日本語認識で最強なのかの理解
  • 3種類のモデル(NeMo / K2 / ESPnet)の違いと選び方
  • コピペで即動く音声認識コード
  • 35,000時間のコーパスへのアクセス方法

この記事で扱わないこと

  • Pythonの環境構築方法(pyenvやvenvの使い方)
  • 深層学習の基礎理論(Transformer、Attention機構など)
  • 音声認識モデルの学習・ファインチューニング

1. ReazonSpeechとの出会い

「日本語の音声認識、なんでこんなに認識精度悪いんだ...」

Whisperを使って議事録の文字起こしをしていたとき、そう感じたことはありませんか?

私は何度もありました。英語だと驚くほど正確なのに、日本語になると突然「空耳アワー」が始まる。固有名詞は壊滅的。句読点もめちゃくちゃ。

そんな中で見つけたのが ReazonSpeech です。

初めて動かしたとき、正直びっくりしました。「え、句読点ちゃんと入るじゃん」「固有名詞もかなり正確...」と。しかもWhisper Large-v2並みの精度で、Whisper Tinyより速い。

ReazonSpeechは、世界最大のオープン日本語音声コーパスを構築するプロジェクトです。株式会社Reazon Holdingsのヒューマンインタラクション研究所が開発・公開しています。

人間で例えるなら、Whisperが「53カ国語を話せる通訳」だとすれば、ReazonSpeechは「日本語だけに人生を捧げた国語の先生」。専門特化の強みが光ります。

ここまでで、ReazonSpeechがどんなプロジェクトか、なんとなくイメージできたでしょうか。次は、この技術を支える基盤について見ていきましょう。


2. 前提知識の確認

本題に入る前に、この記事で使う用語を整理しておきます。

2.1 ASR(Automatic Speech Recognition)とは

ASRは「自動音声認識」の略称です。音声をテキストに変換する技術のことで、「Speech-to-Text」とも呼ばれます。SiriやGoogle音声入力の裏側で動いている技術ですね。

2.2 CER(Character Error Rate)とは

音声認識の精度を測る指標です。認識結果と正解テキストを比較して、どれだけ文字単位で間違えたかを表します。値が低いほど高精度。日本語のように単語区切りが曖昧な言語では、WER(Word Error Rate)よりCERがよく使われます。

2.3 RTF(Real Time Factor)とは

処理速度を表す指標です。「1秒の音声を処理するのに何秒かかるか」を示します。RTFが0.5なら、1秒の音声を0.5秒で処理できる(リアルタイムより速い)ということ。RTFが1.0未満ならリアルタイム処理が可能です。

2.4 コーパス(Corpus)とは

機械学習モデルを訓練するための大規模データセットのことです。音声認識の場合、「音声ファイル + その書き起こしテキスト」のペアが大量に必要になります。ReazonSpeechは35,000時間分のコーパスを公開しています。

これらの用語が押さえられたら、次に進みましょう。


3. ReazonSpeechが生まれた背景

3.1 日本語音声認識の課題

日本語の音声認識は、英語と比べて難しいとされています。その理由は以下の通りです。

課題 説明
文字種の多さ ひらがな、カタカナ、漢字、アルファベットが混在
単語境界の曖昧さ 英語のようにスペースで区切られない
同音異義語の多さ 「橋」「端」「箸」など、文脈判断が必要
学習データの不足 英語に比べて大規模コーパスが少なかった

特に最後の「学習データ不足」は深刻でした。高品質な日本語音声コーパスは、CSJ(日本語話し言葉コーパス)の約600時間程度が主流。英語圏の数万時間規模と比べると圧倒的に少なかったのです。

3.2 ReazonSpeechの登場

2023年1月、Reazon Holdingsが19,000時間の日本語音声コーパスを公開しました。これは当時、オープンな日本語音声コーパスとして世界最大規模でした。

さらに2024年2月のv2.0で35,000時間に拡大。英語圏の大規模データセット(LibriSpeechの約1,000時間、Common Voiceの数千時間)と比較しても遜色のないスケールに達しています。

背景がわかったところで、抽象的な概念から順に、具体的な仕組みを見ていきましょう。


4. ReazonSpeechの基本概念

4.1 3つのモデルバリエーション

ReazonSpeechは、用途に応じて3種類のモデルを提供しています。

モデル パラメータ数 特徴 依存ライブラリ
NeMo版 619M 最高精度、長時間音声対応 NVIDIA NeMo
K2版 159M 高速・軽量、ONNX形式 sherpa-onnx
ESPnet版 120M 研究用途向け ESPnet

初心者には K2版 がおすすめです。軽量で高速、しかもONNX形式なのでGPUがなくてもCPUで動きます。

4.2 アーキテクチャの秘密

v2.0以降のReazonSpeechが高速なのは、Fast ConformerZipformer という最新アーキテクチャを採用しているからです。

従来のConformerは、Transformerの注意機構(Attention)が入力長の2乗に比例する計算量を要しました。これが長時間音声で遅くなる原因でした。

Fast Conformerは、Longformerスタイルの線形注意機構を採用。計算量を線形に抑えることで、長時間音声もシングルパスで高速処理できます。

4.3 Whisperとの比較

公式ベンチマークによると、ReazonSpeech v2.0の性能は以下の通りです。

比較軸 ReazonSpeech v2.0 (NeMo) Whisper Large-v2
精度 (JSUT-BASIC5000) 同等 同等
速度 Whisper Tiny並み 遅い
パラメータ数 619M 1.55B
日本語特化 Yes No (多言語)

同じ精度なら2.5倍速い。これがReazonSpeechの強みです。

基本概念が理解できたところで、これらの抽象的な概念を具体的なコードで実装していきましょう。


5. 実際に使ってみよう

5.1 環境構築

まず、Python仮想環境を作成します。

# 仮想環境の作成
python3 -m venv reazonspeech-env
source reazonspeech-env/bin/activate  # Windowsは: reazonspeech-env\Scripts\activate

# pipのアップグレード
pip install --upgrade pip wheel

次に、使用するモデルに応じてパッケージをインストールします。

# K2版(推奨・軽量で高速)
git clone https://github.com/reazon-research/ReazonSpeech
pip install ReazonSpeech/pkg/k2-asr

# NeMo版(最高精度・要GPU)
pip install ReazonSpeech/pkg/nemo-asr

# ESPnet版(研究用途)
pip install ReazonSpeech/pkg/espnet-asr

5.2 設定ファイルの準備

以下の3種類の設定ファイルを用意しています。用途に応じて選択してください。

開発環境用(config.yaml)

# config.yaml - 開発環境用(このままコピーして使える)
# ReazonSpeech音声認識設定

model:
  # K2版を使用(軽量・高速)
  type: "k2"
  device: "cpu"  # GPU使用時は "cuda"
  
audio:
  # 入力音声の設定
  sample_rate: 16000  # ReazonSpeechは16kHz固定
  max_duration: 30    # K2版の上限は約30秒
  
output:
  # 出力設定
  format: "text"      # "text" or "json"
  timestamps: false   # タイムスタンプ出力
  save_dir: "./results"

logging:
  level: "DEBUG"
  file: "./logs/dev.log"

本番環境用(config.production.yaml)

# config.production.yaml - 本番環境用(このままコピーして使える)
# ReazonSpeech音声認識設定 - Production

model:
  # NeMo版を使用(最高精度)
  type: "nemo"
  device: "cuda"
  
audio:
  sample_rate: 16000
  max_duration: 3600  # NeMo版は長時間対応
  
output:
  format: "json"
  timestamps: true
  save_dir: "/var/data/transcripts"

logging:
  level: "INFO"
  file: "/var/log/reazonspeech/app.log"
  
performance:
  batch_size: 8
  num_workers: 4

テスト環境用(config.test.yaml)

# config.test.yaml - テスト/CI用(このままコピーして使える)
# ReazonSpeech音声認識設定 - Test

model:
  type: "k2"
  device: "cpu"  # CI環境はCPU前提
  
audio:
  sample_rate: 16000
  max_duration: 10   # テスト用に短い音声
  
output:
  format: "json"
  timestamps: false
  save_dir: "./test_results"

logging:
  level: "WARNING"
  file: null  # テストではログファイル不要

test:
  sample_audio: "./fixtures/test_audio.wav"
  expected_text: "これはテスト用の音声です"

5.3 基本的な使い方(K2版)

"""
ReazonSpeech K2版 音声認識サンプル
使い方: python transcribe_k2.py audio.wav
必要なパッケージ: pip install ReazonSpeech/pkg/k2-asr
"""
import sys
from reazonspeech.k2.asr import load_model, transcribe, audio_from_path


def main():
    """メイン処理"""
    # 引数チェック
    if len(sys.argv) < 2:
        print("使い方: python transcribe_k2.py <音声ファイル>")
        print("例: python transcribe_k2.py speech.wav")
        sys.exit(1)
    
    audio_path = sys.argv[1]
    print(f"音声ファイル: {audio_path}")
    
    # モデルのロード(初回はダウンロードに時間がかかる)
    print("モデルをロード中...")
    model = load_model()
    print("モデルロード完了")
    
    # 音声ファイルの読み込み
    print("音声を読み込み中...")
    audio = audio_from_path(audio_path)
    
    # 音声認識の実行
    print("認識中...")
    result = transcribe(model, audio)
    
    # 結果の表示
    print("\n" + "=" * 50)
    print("認識結果:")
    print("=" * 50)
    print(result.text)
    print("=" * 50)
    
    return result.text


if __name__ == "__main__":
    main()

5.4 タイムスタンプ付き認識

"""
ReazonSpeech K2版 タイムスタンプ付き音声認識
字幕ファイル生成などに便利
"""
from reazonspeech.k2.asr import load_model, transcribe, audio_from_path


def transcribe_with_timestamps(audio_path: str) -> list:
    """
    タイムスタンプ付きで音声認識を実行
    
    Args:
        audio_path: 音声ファイルのパス
        
    Returns:
        list: [(開始秒, トークン), ...] のリスト
    """
    model = load_model()
    audio = audio_from_path(audio_path)
    result = transcribe(model, audio)
    
    timestamps = []
    for subword in result.subwords:
        timestamps.append({
            "start": subword.seconds,
            "token": subword.token
        })
    
    return timestamps


def format_as_srt(timestamps: list) -> str:
    """SRT字幕形式に変換"""
    srt_lines = []
    
    # 文字をチャンク化(10文字ごと)
    chunk_size = 10
    current_chunk = []
    chunk_start = 0.0
    
    for i, item in enumerate(timestamps):
        if not current_chunk:
            chunk_start = item["start"]
        current_chunk.append(item["token"])
        
        if len(current_chunk) >= chunk_size or i == len(timestamps) - 1:
            chunk_end = item["start"] + 0.5
            text = "".join(current_chunk)
            
            # SRT形式で出力
            idx = len(srt_lines) // 4 + 1
            start_str = format_time(chunk_start)
            end_str = format_time(chunk_end)
            
            srt_lines.append(str(idx))
            srt_lines.append(f"{start_str} --> {end_str}")
            srt_lines.append(text)
            srt_lines.append("")
            
            current_chunk = []
    
    return "\n".join(srt_lines)


def format_time(seconds: float) -> str:
    """秒数をSRT形式の時刻に変換"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)
    millis = int((seconds % 1) * 1000)
    return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"


if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 2:
        print("使い方: python transcribe_timestamps.py <音声ファイル>")
        sys.exit(1)
    
    timestamps = transcribe_with_timestamps(sys.argv[1])
    
    print("=== タイムスタンプ一覧 ===")
    for item in timestamps[:20]:  # 最初の20件
        print(f"{item['start']:.2f}秒: {item['token']}")
    
    print("\n=== SRT形式 ===")
    srt = format_as_srt(timestamps)
    print(srt)

5.5 実行結果

上記のコードを実行すると、以下のような出力が得られます。

$ python transcribe_k2.py sample.wav
音声ファイル: sample.wav
モデルをロード中...
モデルロード完了
音声を読み込み中...
認識中...

==================================================
認識結果:
==================================================
気象庁は、雪や路面の凍結による交通への影響、暴風雪や高波に警戒するとともに、雪崩や屋根からの落雪にも十分注意するよう呼びかけています。
==================================================

タイムスタンプ付き版の出力:

$ python transcribe_timestamps.py sample.wav
=== タイムスタンプ一覧 ===
0.00秒: 気
1.04秒: 象
1.20秒: 庁
1.44秒: は
1.96秒: 雪
2.16秒: や
...

=== SRT形式 ===
1
00:00:00,000 --> 00:01:44,500
気象庁は、雪や路

2
00:01:96,000 --> 00:03:44,500
面の凍結による交通
...

5.6 よくあるエラーと対処法

エラー 原因 対処法
ModuleNotFoundError: No module named 'sherpa_onnx' K2版の依存ライブラリ未インストール pip install sherpa-onnx を実行
RuntimeError: Audio too long K2版の30秒制限超過 音声を分割するか、NeMo版を使用
FileNotFoundError: [Errno 2] No such file 音声ファイルパスが間違い パスを確認、絶対パスで指定
ValueError: Sample rate must be 16000 サンプルレートが16kHzでない ffmpegで変換: ffmpeg -i input.mp3 -ar 16000 output.wav
CUDA out of memory GPUメモリ不足(NeMo版) バッチサイズを小さくするか、CPU使用に切り替え

基本的な使い方をマスターしたので、次は応用例を見ていきましょう。


6. ユースケース別ガイド

6.1 ユースケース1: 議事録の自動作成

  • 想定読者: 会議の文字起こしを自動化したいビジネスパーソン
  • 推奨構成: NeMo版(長時間音声対応)+ 話者分離
  • サンプルコード:
"""
議事録自動作成スクリプト
長時間の会議音声を文字起こしし、テキストファイルに保存
"""
import os
from datetime import datetime
from reazonspeech.nemo.asr import load_model, transcribe, audio_from_path


def create_meeting_minutes(audio_path: str, output_dir: str = "./minutes") -> str:
    """
    会議音声から議事録を作成
    
    Args:
        audio_path: 会議音声ファイルのパス
        output_dir: 出力ディレクトリ
        
    Returns:
        str: 保存したファイルのパス
    """
    # 出力ディレクトリの作成
    os.makedirs(output_dir, exist_ok=True)
    
    # モデルのロード
    print("モデルをロード中...")
    model = load_model()
    
    # 音声の読み込みと認識
    print(f"音声ファイルを処理中: {audio_path}")
    audio = audio_from_path(audio_path)
    result = transcribe(model, audio)
    
    # 議事録ファイルの作成
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_path = os.path.join(output_dir, f"minutes_{timestamp}.txt")
    
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(f"議事録\n")
        f.write(f"作成日時: {datetime.now().strftime('%Y年%m月%d日 %H:%M')}\n")
        f.write(f"元ファイル: {os.path.basename(audio_path)}\n")
        f.write("=" * 50 + "\n\n")
        f.write(result.text)
    
    print(f"議事録を保存しました: {output_path}")
    return output_path


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("使い方: python meeting_minutes.py <会議音声ファイル>")
        sys.exit(1)
    
    create_meeting_minutes(sys.argv[1])

6.2 ユースケース2: リアルタイム字幕生成

  • 想定読者: 配信者、動画制作者
  • 推奨構成: K2版(高速処理)+ VAD(音声区間検出)
  • サンプルコード:
"""
リアルタイム風字幕生成
短い音声チャンクを連続処理して字幕を生成
"""
import wave
import numpy as np
from reazonspeech.k2.asr import load_model, transcribe


def generate_subtitles(audio_path: str, chunk_seconds: float = 5.0) -> list:
    """
    音声を分割して字幕を生成
    
    Args:
        audio_path: 音声ファイルのパス
        chunk_seconds: チャンクの長さ(秒)
        
    Returns:
        list: 字幕データのリスト
    """
    model = load_model()
    subtitles = []
    
    # WAVファイルを読み込み
    with wave.open(audio_path, 'rb') as wf:
        sample_rate = wf.getframerate()
        n_channels = wf.getnchannels()
        total_frames = wf.getnframes()
        
        chunk_frames = int(chunk_seconds * sample_rate)
        current_time = 0.0
        
        while True:
            frames = wf.readframes(chunk_frames)
            if not frames:
                break
            
            # numpy配列に変換
            audio_data = np.frombuffer(frames, dtype=np.int16)
            if n_channels == 2:
                audio_data = audio_data[::2]  # ステレオ→モノラル
            
            # float32に正規化
            audio_float = audio_data.astype(np.float32) / 32768.0
            
            # 認識実行
            result = transcribe(model, audio_float)
            
            if result.text.strip():
                subtitles.append({
                    "start": current_time,
                    "end": current_time + chunk_seconds,
                    "text": result.text
                })
            
            current_time += chunk_seconds
    
    return subtitles


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("使い方: python realtime_subtitle.py <音声ファイル>")
        sys.exit(1)
    
    subtitles = generate_subtitles(sys.argv[1])
    
    for sub in subtitles:
        print(f"[{sub['start']:.1f}s - {sub['end']:.1f}s] {sub['text']}")

6.3 ユースケース3: 音声検索システム

  • 想定読者: 大量の音声アーカイブを検索可能にしたい開発者
  • 推奨構成: K2版 + 全文検索エンジン(Elasticsearch等)
  • サンプルコード:
"""
音声ファイルのインデックス作成
認識結果をJSONで保存し、検索可能に
"""
import os
import json
import hashlib
from pathlib import Path
from reazonspeech.k2.asr import load_model, transcribe, audio_from_path


def index_audio_files(audio_dir: str, index_path: str = "audio_index.json") -> dict:
    """
    ディレクトリ内の音声ファイルをインデックス化
    
    Args:
        audio_dir: 音声ファイルが格納されたディレクトリ
        index_path: インデックスの保存先
        
    Returns:
        dict: インデックスデータ
    """
    model = load_model()
    index = {"files": [], "version": "1.0"}
    
    audio_extensions = {".wav", ".mp3", ".flac", ".m4a"}
    audio_files = [
        f for f in Path(audio_dir).rglob("*")
        if f.suffix.lower() in audio_extensions
    ]
    
    print(f"処理対象: {len(audio_files)} ファイル")
    
    for i, audio_file in enumerate(audio_files, 1):
        print(f"[{i}/{len(audio_files)}] {audio_file.name}")
        
        try:
            audio = audio_from_path(str(audio_file))
            result = transcribe(model, audio)
            
            # ファイルのハッシュ値を計算(重複検出用)
            file_hash = hashlib.md5(
                audio_file.read_bytes()
            ).hexdigest()
            
            index["files"].append({
                "path": str(audio_file),
                "hash": file_hash,
                "transcript": result.text,
                "duration": len(audio) / 16000  # 秒数
            })
            
        except Exception as e:
            print(f"  エラー: {e}")
            continue
    
    # インデックスを保存
    with open(index_path, "w", encoding="utf-8") as f:
        json.dump(index, f, ensure_ascii=False, indent=2)
    
    print(f"\nインデックスを保存: {index_path}")
    return index


def search_audio(query: str, index_path: str = "audio_index.json") -> list:
    """
    インデックスからキーワード検索
    
    Args:
        query: 検索クエリ
        index_path: インデックスファイルのパス
        
    Returns:
        list: マッチしたファイルのリスト
    """
    with open(index_path, "r", encoding="utf-8") as f:
        index = json.load(f)
    
    results = []
    for file_info in index["files"]:
        if query in file_info["transcript"]:
            results.append({
                "path": file_info["path"],
                "transcript": file_info["transcript"],
                "match_context": extract_context(
                    file_info["transcript"], query
                )
            })
    
    return results


def extract_context(text: str, query: str, context_len: int = 30) -> str:
    """検索語の周辺テキストを抽出"""
    idx = text.find(query)
    if idx == -1:
        return ""
    
    start = max(0, idx - context_len)
    end = min(len(text), idx + len(query) + context_len)
    
    return f"...{text[start:end]}..."


if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 2:
        print("使い方:")
        print("  インデックス作成: python audio_search.py index <音声ディレクトリ>")
        print("  検索: python audio_search.py search <検索語>")
        sys.exit(1)
    
    command = sys.argv[1]
    
    if command == "index" and len(sys.argv) >= 3:
        index_audio_files(sys.argv[2])
    
    elif command == "search" and len(sys.argv) >= 3:
        query = sys.argv[2]
        results = search_audio(query)
        
        print(f"検索結果: {len(results)}")
        for r in results:
            print(f"\n📁 {r['path']}")
            print(f"   {r['match_context']}")
    
    else:
        print("引数が不正です")

ユースケースが把握できたところで、この記事を読んだ後の学習パスを確認しましょう。


7. 学習ロードマップ

この記事を読んだ後、次のステップとして以下をおすすめします。

初級者向け(まずはここから)

  1. 公式クイックスタートを試す

  2. Google Colabで動かす

  3. サンプル音声でベンチマーク

    • 自分の録音した音声で精度を確認
    • Whisperと比較してみる

中級者向け(実践に進む)

  1. HowToガイドを読む

  2. VAD(音声区間検出)と組み合わせる

    • Sileroなどの VADで無音区間をスキップ
    • 長時間音声の効率的な処理
  3. APIサーバー化する

    • FastAPIやFlaskでREST API化
    • Dockerでコンテナ化

上級者向け(さらに深く)

  1. コーパスでファインチューニング

  2. カスタムモデルの学習

    • k2-fsa/icefallの学習レシピを参照
    • 特定ドメイン向けにチューニング
  3. ストリーミングASRの実装

    • リアルタイム音声認識の実現
    • WebSocket + 逐次認識

8. まとめ

この記事では、ReazonSpeechについて以下を解説しました。

  1. ReazonSpeechとは: 世界最大のオープン日本語音声コーパスを構築するプロジェクト
  2. なぜ強いのか: 35,000時間のコーパス + 最新アーキテクチャ(Fast Conformer / Zipformer)
  3. 3つのモデル: NeMo版(最高精度)、K2版(高速・軽量)、ESPnet版(研究向け)
  4. 実践コード: 基本的な文字起こしからタイムスタンプ、議事録作成まで

私の所感

正直なところ、ReazonSpeechを使い始めてから「日本語の音声認識ってこんなに進化してたのか」と驚きました。

Whisperが多言語対応の汎用性で勝負しているのに対し、ReazonSpeechは日本語一点集中の職人技。どちらが良いかは用途次第ですが、日本語だけを扱うなら、ReazonSpeechを第一選択肢として検討する価値は十分あります。

特にK2版の「軽量なのに高精度」は、ローカル環境で手軽に試せるという点で大きなアドバンテージ。GPU必須だったWhisper Largeと同等の精度が、CPUで動くというのは革命的です。

Apache 2.0ライセンスで商用利用も可能。ぜひ皆さんも試してみてください。


参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?