0
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?

ローカル音声認識を自分の音声で測る:Moonshine のストリーミング認識と、応答速度・CER の測り方(Python)

0
Posted at

音声波形を挟んで向き合う 2 台のロボット

日本語の音声認識を、自分の音声で測る方法をまとめます。題材は、ローカルで動く音声認識の Moonshine Voice(日本語の現行モデル Small Streaming)と、比較用の faster-whisper です。

測った結果と考察は、本家のブログにまとめました。

この記事は、その測定に使ったコードと、つまずいた点の実装編です。コードは、そのままコピーして動かせるように、最小の形にしています。

準備

python3 -m venv venv
./venv/bin/pip install moonshine-voice faster-whisper numpy

測る音声は、16 kHz・モノラル・16 bit の WAV にします。手元の音声は、ffmpeg で変換できます。

ffmpeg -y -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le sample.wav

動作確認に使ったバージョンは、moonshine-voice 0.1.5 と faster-whisper 1.2.1 です。

Moonshine でストリーミング認識して、応答速度を測る

「応答速度」を、次の時間として測ります。

最後の行が確定した時刻 − その行の発話が終わった時刻

Moonshine は、音声を少しずつ受け取りながら認識します。そこで、音声を実時間の速さで流し込み、行が確定した時刻を記録します。

速度の測り方の違い(Moonshine は流しながら、Whisper は全体を受け取ってから)

import json, sys, time, wave
import numpy as np
from moonshine_voice import Transcriber, TranscriptEventListener
from moonshine_voice.download import get_model_for_language


def load_wav(path):
    """16 kHz・モノラル・16bit の WAV を float32 の配列にする"""
    with wave.open(path) as w:
        assert w.getframerate() == 16000 and w.getnchannels() == 1
        raw = w.readframes(w.getnframes())
    return np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768


def transcribe_stream(path):
    audio = load_wav(path)
    model_path, arch = get_model_for_language("ja")  # 初回にモデルをダウンロード
    moon = Transcriber(model_path, arch, options={"max_tokens_per_second": 13})

    done = []  # (確定した時刻, 行の開始, 行の長さ, 文字)
    t0 = 0.0

    class Listener(TranscriptEventListener):
        def on_line_completed(self, e):
            done.append((time.perf_counter() - t0, e.line.start_time, e.line.duration, e.line.text))

    stream = moon.create_stream(update_interval=0.2)
    stream.add_listener(Listener())
    stream.start()
    t0 = time.perf_counter()
    for i in range(0, len(audio), 1600):  # 100 ms ずつ、実時間の速さで流し込む
        stream.add_audio(audio[i:i + 1600].tolist(), 16000)
        wait = t0 + (i + 1600) / 16000 - time.perf_counter()
        if wait > 0:
            time.sleep(wait)
    stream.stop()

    text = "".join(d[3] for d in done)
    # 応答速度 = 最後の行が確定した時刻 − その行の発話が終わった時刻
    latency = max(0.0, done[-1][0] - (done[-1][1] + done[-1][2])) if done else None
    return text, latency


if __name__ == "__main__":
    text, latency = transcribe_stream(sys.argv[1])
    print(json.dumps({"text": text, "latency": latency}, ensure_ascii=False))

実行例です。

./venv/bin/python measure.py sample.wav
{"text": "医師である田中先生の意思決定はいつも迅速で的確です", "latency": 0.24}

日本語は max_tokens_per_second を 13 にする

開発元のドキュメントでは、max_tokens_per_second の既定値は 6.5 です。日本語のように、1 秒あたりのトークン数が多くなる言語では、約 13 にするよう推奨されています。上のコードでは、options で 13 を指定しています。

筆者の測定(合成音声 16 文、公開データセット FLEURS 20 件、自分の声 8 文)では、6.5 と 13 で、文字誤り率(CER)は小数第 3 位まで同じでした。今回の音声では差が出ませんでしたが、ドキュメントの推奨に従っておく方が安全です。

Whisper(faster-whisper)の時間を測る

Whisper は、クリップ全体を渡して、結果が出るまでの時間を測ります。transcribe() はジェネレーターを返すので、文字を取り出すまで処理が進みません。時間は、join で取り出した後に測ります。

import sys, time
from faster_whisper import WhisperModel
from measure import load_wav

model = WhisperModel("small", device="cpu", compute_type="int8")
model.transcribe(load_wav(sys.argv[1]), language="ja")  # ウォームアップ(結果は捨てる)

audio = load_wav(sys.argv[1])
t0 = time.perf_counter()
segments, _ = model.transcribe(audio, language="ja", beam_size=5)
text = "".join(s.text for s in segments)  # ここで実際の処理が終わる
print(text, round(time.perf_counter() - t0, 2))

初回はモデルの読み込みで遅くなるので、1 回ウォームアップしてから測ります。

CER(文字誤り率)を計算する

CER は、正解文との編集距離を、正解文の文字数で割った値です。句読点と空白は除き、全角と半角は揃えます。

import re, unicodedata

def normalize(t):
    t = unicodedata.normalize("NFKC", t)
    return re.sub(r"[、。,.\s!!??「」『』()()【】\-—・::;;]", "", t)

def levenshtein(a, b):
    prev = list(range(len(b) + 1))
    for i, ca in enumerate(a, 1):
        cur = [i]
        for j, cb in enumerate(b, 1):
            cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb)))
        prev = cur
    return prev[-1]

def cer(ref, hyp):
    ref, hyp = normalize(ref), normalize(hyp)
    return levenshtein(ref, hyp) / len(ref)

print(round(cer("医師である田中先生の意思決定はいつも迅速で的確です。",
                "医師である田中先生の医師決定はいつの迅速で的確です"), 3))  # 0.12

注意点が 1 つあります。「三百二十八万五千円」を「328万5000円」と書かれると、意味は合っているのに、CER は大きくなります。数字の表記を揃える後処理を入れてから測らないと、この種の誤りが、比較を歪めます。

つまずいた点

同じプロセスで連続して処理すると、空の結果が返ることがあった

20 件の音声を、1 つの Python プロセスで続けて処理したところ、3 件で、Moonshine の結果が空になりました。同じ音声を単独で処理すると、正しく認識されました。

原因は特定していません。クリップごとに別のプロセスで処理すると、空の結果は出なくなりました。

for f in clips/*.wav; do ./venv/bin/python measure.py "$f"; done

長時間動かすアプリに組み込むときは、連続処理で問題が出ないかを確認してください。

日本語は、短い行に分かれる

Moonshine は、息継ぎや間で行を区切ります。日本語の文章をそのまま読み上げると、短い行が並びます。文単位で扱いたいときは、後処理が要ります。上のコードは、行を単純に連結しています。

ライブラリの表示とライセンス

非英語のモデルを読み込むと、「非商用のライセンスのモデルを使っています」という表示が出ます。公式リポジトリの LICENSE では、現行のストリーミングモデルは、日本語を含めて MIT で、非商用のライセンスが残っているのは旧型の一部だけです。表示と LICENSE が食い違っているので、業務で使う前に、公式に確認してください。

測定結果の目安

Linux の CPU(16 コア)で、筆者が測った値です。詳しい条件と考察は、本家の記事にあります。

音声 Moonshine の CER Whisper small の CER Moonshine の応答速度 Whisper small の応答速度
合成音声(8 文) 0.092 0.104 0.18 秒 1.31 秒
FLEURS の読み上げ(20 件) 0.110 0.155 0.30 秒 1.55 秒
筆者の声(8 文) 0.104 0.106 0.21 秒 2.25 秒

サンプル数が少ないので、傾向の目安として読んでください。また、Moonshine は音声を実時間で流した値、Whisper はクリップ全体の処理時間で、測り方が違います。

元記事

0
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
0
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?