nyosegawa

Activity

Stars and mentions · Last 30 days, Oct 5 – Nov 3

GitHub stars202
Mentions26
016324864Oct 5Oct 19Nov 3
UTC days · repositories nyosegawa owns1 repository

Top repositories

nyosegawa’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside nyosegawa’s account where nyosegawa opened or reviewed pull requests in the past 365 days.

Who noticed nyosegawa

Everyone who starred or forked nyosegawa’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

27posts
Nov 41 post
  1. CER 6.1% の日本語音声認識 Haas-ja-v1.0 で、認識0.3〜0.4秒・メモリ約710MBの Mac ローカル音声入力を作る 2026年10月、日本語音声認識モデル Haas-ja-v1.0 が公開されました。NVIDIA の Parakeet-TDT をベースに、予測ネットワークに Kimi Delta Attention(KDA)を採用したモデルです。開発元の asr-ja-bench v1.1 では、CER strict が6.12%で、開発元が評価したオープンモデルの中で最も低い値です。 公式実装は Linux と NVIDIA GPU を前提としていますが、10月31日に公開された C++ の推論ランタイム speech.cpp v0.9.3 が Haas に対応し、Apple Silicon の Mac でも動かせるようになりました。続いて、認識の高速化やカスタム語彙への対応も加わっています。 この記事では、speech.cpp と音声入力クライアントの OpenWhispr を組み合わせ、Haas を使ったローカル音声入力環境を Mac に作ります。手元の Apple M2 では、3〜8秒の発話を8回試したときの認識サーバーの処理時間は0.29〜0.41秒でした。技術用語を指定した表記で出すための辞書設定と、実際の使い心地も紹介します。 全体の構成は次の3段です。 検証環境は macOS 26、Apple M2(メモリ 16GB)、speech.cpp 0.10.0、OpenWhispr 1.11.0(2026年11月2日時点)です。メモリは footprint コマンドで測った speech serve の phys_footprint で、待機時に712MB、測定時点までの最大は750MBでした(中継スクリプトは約20MB)。 手元の録音2本で4モデルを試した結果 Haas-ja-v1.0 の公式 Python 実装は NVIDIA GPU(CUDA)専用ですが、speech.cpp が Apple Silicon の Metal バックエンドと Q8_0 量子化モデル(0.66GB)に対応しています。自分の普段の話し方でどのくらい認識できるかを見るため、手元の録音2本(12.9秒の1文と、48.0秒の6文)を speech.cpp で動く日本語対応の4モデルに入力して比べました(各1回の実測値です)。処理時間には CLI と常駐サーバーでの測定が混在するため、モデル間の厳密な速度比較ではありません。 モデル(すべて Q8_0) 48秒音声の処理時間(実時間比) 句読点 48秒、6文の認識結果 固有名詞の補正手段 Haas-ja-v1.0(指定なし) 0.68秒(約70倍速) 自然に入る 6文すべて出力。ただし「Haas→ハーズ」「OpenWhispr→オープンインスパー」「keywords.txt→キーワードテキスト」 なし Haas-ja-v1.0(keywords[] 指定) 0.68秒(約70倍速) 自然に入る 6文すべて出力。Haas、OpenWhispr、keywords.txt は指定した表記で出力。「精度→制度」「カタカナ→カカナ」などの誤りは残る keywords[](表記=読み) parakeet-tdt_ctc-0.6b-ja 1.01秒(約48倍速) 末尾の句点1つのみ 6文すべて認識するが、48秒間が読点なしの1文につながる なし reazonspeech-v2 1.22秒(約39倍速) なし 前半5文が消失し、最後の1文だけ出力された(VAD による無音分割が必須) なし qwen3-asr-0.6b(--prompt 指定) 3.92秒(約12倍速) 入る(文の途中で句点が入る箇所あり) 6文認識するが、プロンプトで渡した OpenWhispr が OpenWhisker に化け、「経由」が「議員」になる --prompt テスト用に読み上げた次の1文(12.9秒)に対する各モデルの出力は次のとおりです。 テスト発話:「OpenWhispr を一度終了し、t3 のプロセスにならないよう、Finder、LaunchServices 経由で独立したアプリとして起動し直しました」 Haas-ja-v1.0(キーワード登録前、0.40秒):オーブンミスパーを一度終了し、t3のプロセスにならないよう、ファインダー、各ランチサービス、経由で独立したアプリとして起動し直しました。 Haas-ja-v1.0(キーワード登録後、0.19秒):OpenWhisprを一度終了し、t3のプロセスにならないよう、ファインダー、LaunchServices、経由で独立したアプリとして起動し直しました。 parakeet-tdt_ctc-0.6b-ja(0.27秒):オープンウィスパーを一度終了しt3のプロセスにならないようファインダー各クランチサービス経由で独立したアプリとして起動し直しました。 reazonspeech-v2(0.56秒):国民スパを一度収容しt3のプロセスになる内容ファインダー各コランチサービス経由で独立したアプリとして激動し直しました。 qwen3-asr-0.6b(プロンプト指定あり、0.81秒):OpenWhispr を一度終了し、T3 のプロセスにならないよう Finder、各 LaunchServices 議員で独立したアプリとして起動し直しました。 素の状態ではどのモデルも OpenWhispr や Haas のような新しい固有名詞を外しました。違いが出たのは語彙を補正する方法です。4モデルのうち Haas-ja-v1.0 だけが、表記=読み1,読み2(例:OpenWhispr=オープンウィスパー,オープンインスパー,オーブンミスパー)の形で、デコード時に語彙を優先させられます。今回の2本では、語彙を指定しても処理時間は増えず、句読点も残りました。ただし語彙の指定は似た音の別の語を指定した表記に置き換えることもあり得ます。表記を直せることと句読点が入ることから、音声入力用のエンジンとして Haas-ja-v1.0 を選びました。 カーソル位置に文字を入力するクライアントは、自前の OpenAI 互換サーバーに接続できる OSS の中から OpenWhispr(MIT ライセンス、無料)を選びました。VoiceInk でも同じ構成で動作を確認できましたが、配布バイナリは7日間の試用後に25ドルの買い切りライセンスが必要です(Xcode でソースから自前ビルドすれば無料で全機能を使えます)。Handy は外部の文字起こしサーバーへの接続機能を持っていません。 中継スクリプトで吸収した仕様差 speech serve は OpenAI 互換の /v1/audio/transcriptions を提供しており、OpenWhispr も OpenAI 互換のセルフホストサーバーに対応しています。ただし直接つなぐと、次の2点が問題になりました。あわせて、長い音声の扱いも中継スクリプトで決めています。 1. 辞書パラメータ prompt が 400 で拒否される OpenWhispr はカスタム辞書を prompt フィールドに入れて送信します。一方 speech.cpp は、モデルが受け付けないフィールドを受け取ると、無視せずに 400 Bad Request を返します。speech serve --help にも、モデルが受け付けないオプションは拒否すると書かれており、意図された動作です。 {"error":{"message":"Haas-ja-v1.0 does not take the option prompt; leave it out","type":"invalid_request_error","param":"prompt","code":"unsupported_parameter"}} VoiceInk からつないだ場合も、毎回送る temperature=0 が同じ理由で 400 になります。またモデル名は、大文字で始まる Haas-ja-v1.0 以外は 404 になります。中継スクリプトでは file、language、response_format 以外のフィールドを除いてから転送しています。 2. テキスト補正が既定でオンのため /v1/chat/completions も呼ばれる OpenWhispr には、文字起こしのあとに LLM で文を整える「テキスト補正」があり、v1.11.0 では既定でオンです(src/stores/settingsStore.ts で useCleanupModel の既定値が true)。私の環境では、補正の送り先も文字起こしと同じセルフホストのサーバーになっていて、文字起こしのたびに /v1/chat/completions へリクエストが届きました。 speech serve は音声専用のサーバーで /v1/chat/completions を持たないため 404 を返し、発話のたびに「クリーンアップに失敗しました」という通知が表示されます。 対策は「言語モデル → ディクテーションのクリーンアップ」で「テキスト補正を有効にする」をオフにすることです。 設定がオンに戻っても通知が出ないよう、中継スクリプトでも /v1/chat/completions を受けるようにしました。OpenWhispr が送ってくる user メッセージは次のような形なので、<transcript> と </transcript> の内側にある認識テキストだけを抜き出してそのまま返しています。 <system-reminder>...(アカウント等のメタ情報)...</system-reminder> <transcript> こんにちは、こちら晴天です。 </transcript> Output only the cleaned transcript. 3. 長い音声だけ VAD で分割する(しきい値90秒) 長い音声を1回で渡すと文が抜けるモデルがあります(上の表の reazonspeech-v2 では前半5文が消えました)。そのため当初は30秒を超える音声に chunking_strategy=auto(silero-vad で無音区間ごとに分けて認識する指定)を付けていました。 しかし Haas-ja-v1.0 に48秒で6文の録音を渡して比べると、分割したほうが遅く、句読点の一部も消えました。 chunking_strategy=auto あり(0.85秒):認識の精度について、今回の音声は、プロキシのログがプロキシになっていましたカタカナの技術用語はHaasが苦手な場合がありますよく使う後はkeywords.txtに... 分割なし(丸ごと認識、0.68秒):認識の制度について、今回の音声はプロキシのログがプロキシになっていました。カカナの技術用語は、Haasが苦手な場合があります。よく使う語はkeywords.txtに... 手元の48秒の録音では分割せずに6文すべて出力されたため、しきい値は90秒に設定しました(環境変数 HAAS_CHUNK_SECONDS で調整できます)。 セットアップ手順 中継スクリプト(proxy.py)、カスタム語彙ファイルのテンプレート(keywords.txt)、launchd 登録スクリプト(setup.sh)は akitana-airtanker/haas-mac-dictation にまとめてあります。setup.sh は以下の手順1〜4と同じことをまとめて行います。setup.sh を使う場合は手順1〜4を飛ばして手順5へ、手作業で入れる場合は手順1から進めてください。どちらも Homebrew と Xcode Command Line Tools(git と /usr/bin/python3 に必要。xcode-select --install で入ります)が前提です。 brew install ffmpeg brew install --cask openwhispr git clone akitana-airtanker/haas-mac-dictation cd haas-mac-dictation ./setup.sh setup.sh は、~/.local/share/haas-dictation/proxy.py と ~/Library/LaunchAgents/ の2つの plist を上書きし、2つのサービスを登録し直します。keywords.txt は既にあれば上書きしません。 1. speech.cpp とモデルの取得 公式のインストールスクリプトで ~/.local/bin/speech にバイナリが入り、モデルも名前指定で取得できます。ここでは検証したバージョンの 0.10.0 を指定しています。このスクリプトは ~/.local/share/speech.cpp/ にある他のバージョンを削除し、~/.local/bin を PATH に加える行を ~/.zshrc などに書き足します。 curl -fsSL raw.githubusercontent.com | sh -s -- --version 0.10.0 export PATH="$HOME/.local/bin:$PATH" speech pull haas-ja-v1.0 speech pull silero-vad 確認:両方のモデルで checked its SHA-256 が表示されれば完了です。 2. speech serve 単体での動作確認 ポート 8080 でサーバーを起動し、Web UI または curl で認識できるか確かめます。 speech serve haas-ja-v1.0 silero-vad --host 127.0.0.1 --port 8080 ブラウザで 127.0.0.1 を開くか、別ターミナルから WAV ファイルを POST すると認識結果が返ります。確認したら Ctrl + C で止めます(手順4で常駐させます)。 3. 中継スクリプト(proxy.py)と語彙ファイルの配置 ~/.local/share/haas-dictation/ を作り、proxy.py と keywords.txt を置きます。OpenWhispr の再試行機能などは WebM/Opus 形式で音声を送るため、WAV 以外の音声が来たときは ffmpeg(brew install ffmpeg)で 16kHz モノラル WAV に変換してから speech serve に渡しています。 mkdir -p ~/.local/share/haas-dictation ~/.local/share/haas-dictation/keywords.txt には固有名詞の表記と読みを書きます。 1行に1語。「書き方=読み1,読み2」 Claude Code=クロードコード OpenWhispr=オープンウィスパー,オープンインスパー,オーブンミスパー Haas=ハース,ハウズ GitHub=ギットハブ,リットハグ LaunchServices=ランチサービス,各ランチサービス keywords.txt=キーワードテキスト ~/.local/share/haas-dictation/proxy.py は Python 標準ライブラリだけで動作します(GitHub リポジトリの proxy.py と同じ内容です)。認識結果は既定ではログに残しません。確認のために残したいときだけ、環境変数 HAAS_LOG_TEXT=1 を指定します。 proxy.py の全文#!/usr/bin/env python3 """OpenAI 互換の文字起こしクライアント (VoiceInk など) と speech.cpp の間の中継。 - speech.cpp が拒否する項目 (temperature, model, stream など) を落とす - keywords.txt の語彙を keywords[] として足す (Haas のカスタム語彙) - 長い音声 (CHUNK_SECONDS 超) だけ chunking_strategy=auto を足し、silero-vad の区間ごとに認識させる (短い音声は丸ごとの方が句読点が正確。長い一括は文が抜けることがある) 標準ライブラリだけで動く。 """ import json import os import subprocess import sys import tempfile import time import uuid import urllib.error import urllib.request from email.parser import BytesParser from email.policy import HTTP from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer HERE = os.path.dirname(os.path.abspath(file)) UPSTREAM = os.environ.get("HAAS_UPSTREAM", "127.0.0.1") PORT = int(os.environ.get("HAAS_PROXY_PORT", "8081")) KEYWORDS_FILE = os.environ.get("HAAS_KEYWORDS", os.path.join(HERE, "keywords.txt")) FFMPEG = "/opt/homebrew/bin/ffmpeg" if os.path.exists("/opt/homebrew/bin/ffmpeg") else "ffmpeg" PASS = {"file", "language", "response_format"} CHUNK_SECONDS = float(os.environ.get("HAAS_CHUNK_SECONDS", "90")) 認識結果は機密を含み得るため、既定ではログに残さない。HAAS_LOG_TEXT=1 のときだけ記録する。 LOG_TEXT = os.environ.get("HAAS_LOG_TEXT") == "1" def ensure_wav(data, filename): """RIFF WAVE 以外で、中身が空でなければ ffmpeg で 16kHz mono PCM WAV に変換する。""" if data[:4] == b"RIFF" and data[8:12] == b"WAVE": return data, filename or "audio.wav", "audio/wav", False if not data or all(b == 0 for b in data[:64]): return data, filename or "audio.wav", "audio/wav", False try: with tempfile.NamedTemporaryFile(suffix=".wav") as tmp: p = subprocess.run( [FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", "pipe:0", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", tmp.name], input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30 ) if p.returncode == 0: wav = tmp.read() if wav[:4] == b"RIFF": return wav, "audio.wav", "audio/wav", True except Exception: pass return data, filename or "audio.wav", "audio/wav", False def wav_seconds(data): """RIFF の fmt と data チャンクから長さを求める。読めなければ None。""" if data[:4] != b"RIFF" or data[8:12] != b"WAVE": return None i, rate_bytes = 12, None while i + 8 <= len(data): cid, size = data[i:i + 4], int.from_bytes(data[i + 4:i + 8], "little") if cid == b"fmt ": rate_bytes = int.from_bytes(data[i + 16:i + 20], "little") # byte rate elif cid == b"data" and rate_bytes: return min(size, len(data) - i - 8) / rate_bytes i += 8 + size + (size & 1) return None def parse_keywords(text): lines = (l.strip() for l in text.splitlines()) return [l for l in lines if l and not l.startswith("#")] def keywords(): try: with open(KEYWORDS_FILE, encoding="utf-8") as f: return parse_keywords(f.read()) except FileNotFoundError: return [] def extract_transcript(body): text = "" try: req = json.loads(body.decode("utf-8")) for m in reversed(req.get("messages", [])): if m.get("role") == "user": c = m.get("content", "") text = c if isinstance(c, str) else "".join(p.get("text", "") for p in c if isinstance(p, dict)) break except Exception: pass s, e = text.rfind("<transcript>"), text.rfind("</transcript>") if s != -1 and e > s: text = text[s + len("<transcript>"):e] return text.strip() def parse(ctype, body): msg = BytesParser(policy=HTTP).parsebytes( b"Content-Type: " + ctype.encode() + b"\r\n\r\n" + body) for part in msg.iter_parts(): yield part.get_param("name", header="content-disposition"), part def rebuild(ctype, body, kws=None, to_wav=None): kws = keywords() if kws is None else kws to_wav = to_wav or ensure_wav boundary = "haas-" + uuid.uuid4().hex out = bytearray() def add(name, data, filename=None, ptype=None): disp = f'form-data; name="{name}"' + (f'; filename="{filename}"' if filename else "") out.extend(f"--{boundary}\r\nContent-Disposition: {disp}\r\n".encode()) if ptype: out.extend(f"Content-Type: {ptype}\r\n".encode()) out.extend(b"\r\n" + data + b"\r\n") dropped, seconds = [], None for name, part in parse(ctype, body): if name not in PASS: dropped.append(name) continue data = part.get_payload(decode=True) or b"" if name == "language" and data.strip() not in (b"ja", b"auto"): dropped.append("language") # Haas は ja だけ continue if name == "file": data, fname, ptype, transcoded = to_wav(data, part.get_filename()) if transcoded: dropped.append("(ffmpeg->wav)") seconds = wav_seconds(data) add(name, data, fname, ptype) continue add(name, data, part.get_filename(), part.get_content_type() if part.get_filename() else None) for kw in kws: add("keywords[]", kw.encode()) if seconds and seconds > CHUNK_SECONDS: add("chunking_strategy", b"auto") dropped.append(f"(chunked {seconds:.0f}s)") out.extend(f"--{boundary}--\r\n".encode()) return f"multipart/form-data; boundary={boundary}", bytes(out), dropped class Handler(BaseHTTPRequestHandler): def _forward(self, method, path, body=None, ctype=None): req = urllib.request.Request(UPSTREAM + path, data=body, method=method) if ctype: req.add_header("Content-Type", ctype) try: with urllib.request.urlopen(req, timeout=300) as r: status, headers, data = r.status, r.headers, r.read() except urllib.error.HTTPError as e: status, headers, data = e.code, e.headers, e.read() except urllib.error.URLError as e: status, headers = 502, {} data = ('{"error":{"message":"speech serve に接続できません: %s"}}' % e.reason).encode() self.send_response(status) self.send_header("Content-Type", headers.get("Content-Type", "application/json")) self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) return status, data def do_GET(self): self._forward("GET", self.path) def do_POST(self): body = self.rfile.read(int(self.headers.get("Content-Length", 0))) ctype = self.headers.get("Content-Type", "") if self.path.rstrip("/").endswith("/audio/transcriptions") and ctype.startswith("multipart/"): ctype, body, dropped = rebuild(ctype, body) status, data = self._forward("POST", "/v1/audio/transcriptions", body, ctype) detail = data[:200].decode(errors="replace") if LOG_TEXT else f"({len(data)} bytes)" sys.stderr.write(f" dropped={dropped} -> {status} {detail}\n") elif self.path.rstrip("/").endswith("/chat/completions"): OpenWhispr のテキスト補正がこのサーバーに向いた場合は、<transcript> の中身をそのまま返す text = extract_transcript(body) resp = json.dumps({ "id": "chatcmpl-passthrough", "object": "chat.completion", "created": int(time.time()), "model": "passthrough", "choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}], }, ensure_ascii=False).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(resp))) self.end_headers() self.wfile.write(resp) sys.stderr.write(f" chat/completions passthrough ({len(text)} chars)\n") else: self._forward("POST", self.path, body, ctype) if name == "main": print(f"haas proxy on http://127.0.0.1:{PORT} -> {UPSTREAM}", file=sys.stderr) ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever() 4. launchd による常駐化 ログイン時に自動起動し、異常終了時に再起動するよう ~/Library/LaunchAgents/ に2つの plist を置きます。 D=$HOME/.local/share/haas-dictation LA=$HOME/Library/LaunchAgents mkdir -p "$LA" cat > "$LA/local.haas.speech-serve.plist" <<EOF <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "apple.com"> <plist version="1.0"><dict> <key>Label</key><string>local.haas.speech-serve</string> <key>ProgramArguments</key><array> <string>$HOME/.local/bin/speech</string><string>serve</string> <string>haas-ja-v1.0</string><string>silero-vad</string> <string>--host</string><string>127.0.0.1</string><string>--port</string><string>8080</string> </array> <key>RunAtLoad</key><true/><key>KeepAlive</key><true/> <key>StandardErrorPath</key><string>$D/serve.log</string> <key>StandardOutPath</key><string>$D/serve.log</string> </dict></plist> EOF cat > "$LA/local.haas.proxy.plist" <<EOF <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "apple.com"> <plist version="1.0"><dict> <key>Label</key><string>local.haas.proxy</string> <key>ProgramArguments</key><array> <string>/usr/bin/python3</string><string>$D/proxy.py</string> </array> <key>RunAtLoad</key><true/><key>KeepAlive</key><true/> <key>StandardErrorPath</key><string>$D/proxy.log</string> <key>StandardOutPath</key><string>$D/proxy.log</string> </dict></plist> EOF launchctl bootstrap gui/$(id -u) "$LA/local.haas.speech-serve.plist" launchctl bootstrap gui/$(id -u) "$LA/local.haas.proxy.plist" 確認:launchctl list | grep haas で2行表示され、curl -s 127.0.0.1 に Haas-ja-v1.0 が含まれていれば稼働しています。 5. OpenWhispr のインストールと設定 冒頭でインストールしていなければ Homebrew でインストールし、Finder または Dock から起動します。 brew install --cask openwhispr ! 私の環境では、T3 Code(ターミナルを内蔵したアプリ)から OpenWhispr のバイナリを直接起動したとき、録音しても音が入りませんでした。macOS の tccd のログには次の行が出ていました。 service: kTCCServiceMicrophone requires entitlement com.apple.security.device.audio-input but it is missing for responsible={identifier=t3, ...}, accessing={identifier=com.gizmolabs.openwhispr.helper}; access to kTCCServiceMicrophone denied macOS はマイクの許可を、OpenWhispr ではなく起動元のアプリ(ここでは t3)について判定し、起動元に com.apple.security.device.audio-input の entitlement がないため拒否していました。Finder から起動し直すと録音できました。 設定画面の「音声認識」で「セルフホスト」を選び、サーバー URL に 127.0.0.1 Haas-ja-v1.0 を入力します。 「ホットキー」でディクテーション用のキー(図では Ctrl + S)を設定します。 初回録音時に macOS のマイク権限とアクセシビリティ権限を求められたら許可します。 確認:任意の入力欄で設定したホットキー(Ctrl + S など)を押して話し、~/.local/share/haas-dictation/proxy.log に POST /v1/audio/transcriptions HTTP/1.1" 200 が記録されてテキストが入力されれば完成です。認識が崩れた固有名詞は keywords.txt に追記して保存するだけで、プロセス再起動なしに次の発話から反映されます。 実際に動かして確認したこと 日常的に使ううえで気になった点も手元で確かめました。 音声データの通信先:OpenWhispr で8回話すあいだ、プロセスの通信先を lsof で記録しました。音声データは 127.0.0.1:8081 の中継スクリプトから 127.0.0.1:8080 の speech serve へ渡されます(これとは別に、OpenWhispr 本体から Vercel のアドレスへの接続が2本あり、通信量は数KBでした)。 ログの扱い:speech serve のログ(serve.log)には認識本文は記録されず、起動情報や音声の秒数、テキストのバイト数、処理時間などが記録されます。中継スクリプト(proxy.log)も既定では認識テキストを出力しないようにしてあります(デバッグ時のみ HAAS_LOG_TEXT=1 で先頭200バイトを記録できます)。 メモリ使用量:speech serve が待機時に約710MB、中継スクリプトが約20MB、OpenWhispr 本体が約99MBです(OpenWhispr の別機能でローカル LLM を有効にしている場合は、追加で llama-server のメモリが乗ります)。 処理時間のばらつき:3〜8秒の発話8回では speech serve の処理時間は0.29〜0.41秒でした。モデル読み込み直後の初回や、しばらく間を空けたあとに数秒かかることがありますが、続けて話す場面では0.3秒前後で返ってきます。 おわりに これまでローカルの音声認識は「句読点が入らない」「新しい技術用語がカタカナに崩れる」という小さなストレスがあり、結局キーボードで打ち直すことが少なくありませんでした。 Haas-ja-v1.0 を speech.cpp で常駐させてみて一番ありがたかったのは、句読点が自然に入ることと、「あ、この単語が化けたな」と思ったその場で keywords.txt に 表記=読み を1行足せば、再起動も待たずに次の発話から狙いどおりの綴りで出てくるところです。エディタやターミナル、ブラウザを行き来しながら、思いついたことをすぐ声で流し込めるようになりました。 OpenWhispr と speech.cpp の間には少しだけパラメータの仕様差がありますが、約200行の Python スクリプト1枚で吸収できます。Apple Silicon の Mac をお持ちなら、まずは speech serve 単体の Web UI(手順2)だけでも認識の速さと句読点の自然さを体感できるので、ぜひ試してみてください。

  2. speech.cpp v0.10.1をリリースしました🚀 NVIDIAのRE-USEに対応しました。 録音から雑音 / 残響 / 音割れを取り除き、狭い帯域を広げられます。 nyosegawa/speech.cpp ■どういうときに使うか 以下のような録音をきれいにするのに使えます ・雑踏や店で録った会議、通話、ボイスメッセージ ・声のクローンに渡す参照音声 ■あんまり向かない使い方 ・ASRの前処理 めちゃくちゃうるさい場所なら少し改善することもありますが、それ以外ではほぼ効果がなくむしろ悪化する場合があります。現代のASRは雑音に対して頑健につくられています

    59 likes · 13 reposts · 2,179 views
  3. speech.cpp の新しいバージョンリリースしました🚀 nyosegawa/speech.cpp 以下の3モデルに対応しました! ・Nemotron-3-Diarization ・NVIDIA-Nemotron-3.5-Transcribe-30B-A4B ・Haas-ja-v1.0 t.co

    199 likes · 43 reposts · 8,801 views
  4. Irodori-TTSの速度上位3種を比較する【後編:品質評価】 前編はこちらから: Irodori-TTSの7実装を比較する【前編:速度編】 前編ではIrodori-TTSの7実装について推論速度とVRAM使用量を比較した。後編ではMFの推論が速かった ONNX版、公式CUDA版、speech.cpp Vulkan版の3種に絞り、生成音声の品質を調べる。 評価するのは、日本語を正しく読めるかと、参照音声の話者をどれだけ再現できるかの2点である。読みは音声認識による読み戻し、話者再現性は話者埋め込みによる自動評価を使う。MFは3実装、RFは公式版とspeech.cppの2実装を比較した。 漢字の読み正解率はMFで92.57〜92.78%、RFで93.38〜93.40%と近い値であった。一方、実用文の長さ制限と、短い参照音声からの話者再現には違いが出た。 そこでCodexは、結果表を作ったあとに、初期ノイズ、認識器、参照音声の前処理、codecの演算精度まで順に調べた。特に後半の話者再現性では、平均の類似度だけを見ていると見落とす差があった。 コード、設定、集計CSV、追加診断の結果は検証リポジトリにまとめている。 要約 MFよりRFのほうが読み精度が高い。 各実装で読み精度の差はほぼない。 ボイスクローン精度は参照音声が低サンプリングレート(24kHz以下)の短いクリップだとspeech.cppの精度がやや低い。30秒程度用意すればどんなサンプリングレートでも問題はない。 比較条件と評価データ 主評価では前編と同じコード・モデル・runtimeに固定し実施した。モデルはIrodori-TTS v4.1 SmallのRF版・MF版である。実用設定のTTS本体の重み精度は、公式がBF16、ONNXがFP16、speech.cppがF16である。 codecの重みは公式とspeech.cppがFP32、ONNXは参照音声encoderがFP32、decoderがFP16である。RFは40ステップ、MFは4ステップ、batch sizeと候補数は1、seedは0〜4の5通りに揃えた。生成長は各実装の自動予測を使い、透かし、末尾トリム、追加の無音除去は無効にしている。 評価データは次の3つである。 JKYB-Parakeet 全13,536文:参照音声とcaptionを使わず、漢字の読みを評価する。 実用40文:speech-benchの20文と前編の速度比較20文である。つくよみちゃんコーパスのVOICEACTRESS100_054.wavを参照し、「自然な日本語で、落ち着いた口調で話す。」というcaptionを使う。 JVSコーパス 全100話者:各話者5文章を合成し、参照音声の長さを1クリップ、約30秒、約60秒、120秒に変えて話者再現性を評価する。 本文の値は5 seedの平均である。FP32でもJKYBの固定500文とJVSの固定20話者を評価し、実用文では生成長と初期ノイズを共通にした診断を追加した。 漢字の読み正解率 JKYB-Parakeetの13,536文について、kana-whisperとWhisper、上流の読み採点器で漢字の読みを評価した。 「読み正解率」は対象語の読みが完全一致した割合である。「文かなCER」は文章全体のかな表記に対する文字誤り率で、小さいほど良い値である。 構成 読み正解率 ↑ 文かなCER ↓ 公式 MF BF16 92.620% 1.546% ONNX MF FP16 92.569% 1.550% speech.cpp MF F16 92.778% 1.503% 公式 RF BF16 93.384% 1.304% speech.cpp RF F16 93.395% 1.333% MFの読み正解率は3実装で近く、差は最大約0.21ポイントである。 同じ実装のRFとMFを比べると、公式はRFが約0.76ポイント、speech.cppはRFが約0.62ポイント上回った。 FP32の診断は固定500文で行っている。精度による差は同じ500文同士で集計し、全13,536文の結果とは分けて保存した。実装間の差と95%区間も、漢字と読みの組を単位に再標本化して計算している。 出典:JKYBの主結果、実装・精度ごとの差 実用文の読み戻しと30秒上限 実用40文ではQwen3-ASRとReazonSpeechで生成音声を読み戻した。まず結果に影響したのは、speech.cppの30秒上限1である。 ここでの誤り率はspeech-benchを参考にしている。表記を正規化したうえで編集距離を求め、1文あたりの誤り数はその文の参照文字数を上限とする。各seedについて「誤り数の合計÷参照文字数の合計」を計算し、表には5 seedの平均を載せている。生成失敗は空出力として数えている。 公式とONNXは40文すべてを生成した。speech.cppは自動予測された生成長が30秒を超えた8長文を拒否し、MF・RFとも要求の20%が失敗した。 全5構成・全5 seedで生成できた共通32文を取り出した。除いたのは速度比較のp13〜p20の8文で、各構成160音声の比較である。比較対象の採用条件は全構成で生成に成功したことに固定している。 全構成で生成できた32文の読み戻し 構成 Qwen誤り率 ↓ Reazon誤り率 ↓ 公式 MF BF16 4.54% 11.37% ONNX MF FP16 4.72% 14.67% speech.cpp MF F16 4.45% 13.16% 公式 RF BF16 2.05% 4.88% speech.cpp RF F16 2.43% 10.15% MFのQwen誤り率は4.45〜4.72%と近い値である。RFは両実装ともMFより低くなった。 RFの実装差が認識器によって変わる。speech.cppと公式の差はQwenでは+0.38ポイントであるが、Reazonでは+5.27ポイントある。そこで、RFの条件をさらに揃えて調べた。 出典:40文の全件集計、共通成功32文の結果 RFの初期ノイズとASRによる読み戻しの差 最初に両実装の重みをFP32に統一したが、Reazonの誤り率の差は残った。次に、公式FP32が予測した生成長と、同じfloat32の初期ノイズを両実装へ渡した。 この比較では、共通条件の長さ検査で1サンプル差が出たnumber-moneyも全条件から除き、同じ31文×5 seedに揃えている。表の値はspeech.cppから公式を引いた誤り率の差である。 31文の比較条件 Qwenの差 Reazonの差 通常条件、公式BF16 / speech.cpp F16 +0.39ポイント +5.33ポイント 通常条件、両方FP32 +0.28ポイント +4.87ポイント 生成長・ノイズ共通、BF16 / F16 −0.01ポイント −0.04ポイント 生成長・ノイズ共通、両方FP32 +0.15ポイント +0.09ポイント 生成長と初期ノイズを揃えたところ、Reazonによるスコア差がほとんど無くなったことが確認できる。 さらに、FP32の通常条件でも生成長が同じだった27文に絞ってノイズを揃えると、Reazonの差は+6.58ポイントから0.00ポイントになった。この27文では長さを変えていないため、ノイズを揃えた影響を切り分けられる。 同じseed番号を渡しても、実装間で同じノイズになるわけではない2。公式はCUDAのtorch.Generatorとtorch.randn、speech.cppはmt19937_64とBox–Mullerを使う。 認識結果も確認した。通常FP32条件でのReazonの誤り率の差は、約85.4%が3文に集中していた。駅の案内文では、Qwenがほぼ全文3を認識したspeech.cppの音声に対し、Reazonは「ご乗車のお客様は黄色い線の内側でお待ちください。」だけを返した例がある。 代表8音声を第三の認識器であるWhisperで認識させると、Reazonが前半や後半を取りこぼしたspeech.cppの4音声も、Qwenと同様に内容全体を認識できた。 この経過から、今回の読み戻しの差には、5 seedで得られた初期ノイズの組とASRの取りこぼしが影響したと考えられる。 出典:RFの原因調査と認識例 参照音声の長さと話者再現性 JVSの100話者では各話者について5文章を合成し、参照音声を1クリップ、約30秒、約60秒、120秒の4条件に変えた。30秒と60秒は最後のクリップを丸ごと保持し、120秒だけ末尾を切り詰めている。 評価にはCAM++の話者埋め込みを使う。合成対象とは別の実音声10発話から、話者ごとに照合用の中心ベクトルを作った。 cosine類似度:生成音声の埋め込みと、正解話者の中心ベクトルとの近さ。 話者Top-1:100話者の中心ベクトルの中で、正解話者が最も高いスコアになった割合。 次の表は主評価で使ったバージョンのTop-1を参照長別に並べたものである。各セルは100話者×5文章×5 seed、2,500音声の結果である。 主評価の構成 1クリップ 約30秒 約60秒 120秒 公式 MF BF16 88.64% 98.36% 98.52% 99.12% ONNX MF FP16 88.44% 98.40% 98.80% 99.32% speech.cpp MF F16 84.52% 98.64% 99.20% 99.32% 公式 RF BF16 88.96% 98.64% 99.16% 99.56% speech.cpp RF F16 84.28% 99.04% 99.36% 99.40% 約30秒以上では、全構成のTop-1が98%を超えた。1クリップではspeech.cppが公式よりMFで4.12ポイント、RFで4.68ポイント低くなる。 cosine類似度も参照長とともに上がる。例えば公式MFは1クリップの0.764480から120秒の0.831954へ、speech.cpp MFは0.756571から0.832217へ上昇した。参照長をまとめて平均すると、この短い参照での差が見えにくくなる。 出典:JVSの参照長別結果、全指標のCSV 固定20話者で参照音声の処理経路を調べる 1クリップでの公式実装とspeech.cppの差を調べるため、speech.cppで固定20話者で中間データを交換した。 参照音声は、前処理されたPCM波形、codecが作るlatentという中間表現、話者特徴量の順に変換される。各段階の中間データを順番に公式の出力へ置き換えた。 MFの比較では全条件をFP32重み・共通生成長・共通ノイズに揃えた。 通常の参照経路では、speech.cppと公式のcosine差は−0.003280であった。公式PCMを渡すと−0.001248、公式latentでは−0.000182、公式話者特徴量では−0.000013まで縮まった。 差が大きく出ていたのは、参照波形からlatentを作るまでの部分であった。前処理にはリサンプル方式の違いがあり、codecにはFP32重みでも内部でF16を使う演算経路があった。この段階でspeech.cppの作者にフィードバックを送り、修正及び検証が行われた。続く評価ではこの2点の影響を調べている。 出典:1クリップ参照の経路診断 speech.cpp更新後の類似度と話者Top-1 続いて、2026年10月19日に固定したspeech.cpp eacabcd891fe、ggml a655cf691077で、100話者・1クリップ参照の評価を追加した。ここからの「更新後」はこのバージョンを指す。主評価のspeech.cppはc6f28804907eである。 このバージョンには、codec encoderの入力波形に対する最初の畳み込みをFP32化する修正が含まれる。 更新後のMF・RFで5,000音声を新たに生成し、公式と更新前のspeech.cppで保存した結果と比較した。対象文章、参照クリップ、照合用の中心ベクトルは共通である。 100話者・1クリップ参照 cosine類似度 ↑ 話者Top-1 ↑ 公式 MF BF16 0.764480 88.64% 旧speech.cpp MF F16 0.756571 84.52% 更新後speech.cpp MF F16 0.764892 86.00% 公式 RF BF16 0.768389 88.96% 旧speech.cpp RF F16 0.759178 84.28% 更新後speech.cpp RF F16 0.767431 85.56% 更新後のcosine類似度は公式に近づき、MFでは平均が公式をわずかに上回った。Top-1も更新前からMFで1.48ポイント、RFで1.28ポイント改善している。それでも公式との差はMFで−2.64ポイント、RFで−3.40ポイント残った。 この食い違いは、2つの指標が何を測るかを分けると理解できる。cosine類似度が測るのは正解話者との近さで、Top-1にはほかの99話者とのスコアの大小関係も影響する。正解話者のスコアが高くても、競合する別の話者がさらに高ければ不正解になる。 そこで、どの話者を誰と間違えたかを調べた。各話者の対象は25音声である。MFのjvs042 → jvs005は公式1件に対して更新後13件、jvs049 → jvs088は公式0件に対して13件であった。さらに、jvs042とjvs049の2話者が誤認識の約半分を占めていることが分かった。 出典:更新後の100話者評価と混同分析、類似度の集計CSV 悪化6話者と対照2話者でのリサンプル比較 ! 参照音源が32KHz以上のサンプリングレート、もしくは30秒程度の音源が用意できるのであれば参照音声のサンプリングレートによる影響はありません。作者にも報告済みの内容なので、この記事を見て報告するのは控えていただけると幸いです。 次の診断には、誤りの増加が大きかったjvs042・049・071・036・061・098の6話者と、両実装で全問正解だったjvs001・002の2話者を選んだ。この8話者を固定し、5文章×5 seed×MF/RF×6条件、計2,400音声を生成した。 全6条件で生成フレーム数を公式BF16の予測値に揃え、同じfloat32初期ノイズを使う。読みの診断で差の解消につながった条件を、ここでも最初に揃えた。 そのうえで参照音声のリサンプルを変更する。通常のspeech.cppはKaiserBest、公式はtorchaudioの既定Hannを使う。Hann条件では24kHzの参照を公式相当の処理で48kHzのfloat32 WAVへ変換してからspeech.cppへ渡した。ラウドネス正規化、codec、TTS本体は各実装の処理を維持する。 8話者・2,400音声での話者Top-1 診断条件 悪化6話者:MF 悪化6話者:RF 対照2話者:MF / RF 公式BF16 68.00% 65.33% 100% / 100% speech.cpp F16・通常Kaiser 20.00% 29.33% 100% / 100% speech.cpp F16・公式相当Hann 69.33% 66.67% 100% / 100% speech.cpp FP32重み・Hann・通常Vulkan 69.33% 67.33% 100% / 100% 公式FP32 70.00% 68.00% 100% / 100% speech.cpp FP32重み・Hann・内部FP32 70.00% 68.00% 100% / 100% この6話者では、F16重みのままリサンプルを公式相当のHannへ変更すると、長さとノイズを揃えても残っていた差がほぼ消えた。 例えばMFのjvs049は、通常Kaiserでは25音声中11音声が正解であった。Hannへ変更すると25音声すべて正解となり、公式と揃った。jvs042も2/25から18/25になり、公式の18/25と同じである。 FP32重みに変更し、Vulkan内部のF16演算とcooperative matrixも無効にした条件では、公式FP32と全400組で予測話者が一致した。調べた悪化例については、参照音声のリサンプルが識別率の差の主な原因であった。 前節の100話者全体の成績と、この8話者の診断を併せると、平均類似度の差から話者の混同を調べ、さらに参照音声の前処理まで差を追跡できる。表のHann条件の数値はこの原因診断の結果である。 出典:6条件の診断結果と話者別の正解数 補足 参照音声のサンプリングレートによる影響 つくよみちゃん1話者の96kHz原音から、96・48・44.1・32・24・16kHzの入力を作って調べた。波形の比較に99クリップ、生成時の参照に5クリップ、照合には別の10クリップを使い、MF・RFで3,600音声を生成している。 入力サンプリングレート 公式 cosine類似度 speech.cpp cosine類似度 latent差 波形L2差 96 kHz 0.874746 0.873417 1.80080% 0.10485% 48 kHz 0.873151 0.873384 0.34484% 0.00000% 44.1 kHz 0.876985 0.875745 2.00674% 0.22056% 32 kHz 0.874514 0.875722 26.70218% 1.25086% 24 kHz 0.880409 0.877408 14.93694% 1.76345% 16 kHz 0.856068 0.867694 18.56915% 0.77473% この通り、リサンプラーが違う実装であっても、十分なサンプリングレートがあれば結果に影響はない。特に頻繫に使用されるであろう44.1kHzや96kHzではほとんど変化は見られなかった。また、前述したように、24kHzでも30秒ほど音声を用意すれば公式実装と同等の精度が得られる。実使用では問題は起きないと考えられる。 出典:入力サンプルレート比較 F16演算の影響を調べる 主評価と同じ更新前のバージョンでcodec内部でF16演算を使う影響を調べた。 固定20話者で同じ公式PCMをcodecに入れた比較では、重みをすべてFP32にしても、通常Vulkanのlatent相対L2誤差は約0.09379であった。内部F16経路を無効にすると約0.00000310(約0.000310%)まで縮まった。 前述の通り、つくよみちゃん1話者の96・48・44.1・32・24・16kHzの入力を作成した。 48kHz入力はリサンプルを通らず、前処理後の波形が一致した。同じPCMを渡し、内部F16を無効にすると、codec latentの相対L2差は全入力レートで約0.00028〜0.00032%になった。入力サンプルレートを変えた比較でも、前処理とcodec内の演算の影響を分けて調べられる。 出典:参照経路とcodecの診断、入力サンプルレート比較 F16重みについて調べる 続いて900音声を追加し、speech.cppのTTS本体だけをFP32重みからF16重みへ変更した。codec latent、生成長、初期ノイズ、照合用の中心ベクトルを揃えた比較である。 この1話者・6入力レート・MF/RFの比較で、F16重みとFP32重みのCAM++ cosine差は −0.000254〜+0.000231であった。codec latentはFP32重みでの実行時と完全一致している。 出典:TTS本体のF16重み比較 速度上位3種の品質比較で分かったこと 読みの評価では、MFの3実装は近い成績であった。RFは公式とspeech.cppの双方でMFを上回っている。実用文ではspeech.cppの30秒上限がネックとなる場合もあった。ただし、CLIやサーバでは自動で分割する機能がついているのであまり気にする必要は無い。 話者再現性は参照音声の長さで大きく変わった。約30秒以上では全構成が98%以上のTop-1に達し、1クリップでは実装差が出た。speech.cpp更新後は平均類似度が公式に近づいた。リサンプラーには違いがあるが、実際に生成音声を聴いてみるとリサンプラーの影響はわからないため、特に問題は起きないだろう。 前編はこちらから: Irodori-TTSの7実装を比較する【前編:速度編】 余談 前編から引き続き、この記事はベンチマークのリポジトリを基にGPT-6 Proが執筆したものを、yomiyasuを使った上でAstraに推敲させ、人間が読みやすいように人間がさらに修正を加えている。 後編のほうがslop表現多くて修正が大変だった。 感想や格言みたいなのを異常に書きたがるので全部消した。あとリサンプラーによる差を執拗に追及してたからそれも半分ぐらい消した。 speech.cppはCLIが作りこまれており、消費vramも少ないのでお手軽にソフトを作れて良い。おすすめ。公式実装はCUDAで速度が欲しい人におすすめ。 脚注 CLIやサーバーで使用する場合は自動分割されるため影響は少ない。 ↩︎ 当たり前 ↩︎ "次の電車は三番線に到着します。ご乗車のお客様は黄色い線の内側でお待ちください。車内では携帯電話を静かな設定にして、周りの方への配慮をお願いします。お忘れ物のないよう、お降りの際には座席の周辺をご確認ください。ご乗車のお客様は黄色い線の内側でお待ちください。" ↩︎

  5. Irodori-TTSの7実装を比較する【前編:速度編】 後編はこちら : Irodori-TTSの7実装を同じモデル・入力テキスト・参照音声で動かして比較した。推論ハードウェアはRTX 5090である。CUDAとVulkan、FP32と各実装の実用精度、参照音声のキャッシュあり・なしを分け、推論時間とVRAM使用量を測っている。 MFの実用設定ではirodori-onnxが最速、RFの実用設定では公式Irodori-TTSが最速であった。MFで既存のキャッシュを使ったときの推論時間は、ONNX版103.9 ms、公式版149.3 ms、speech.cpp Vulkan版194.6 msである。speech.cpp Vulkan版は、速度上位3種の中でVRAM使用量が最も小さくなった。 前編では処理段階ごとの速度差と、runtime更新で公式MFが遅くなった原因を調べた結果を紹介する。後編の品質評価はMFで速かった3種を対象に、読みと話者再現性を比較している。 検証コード、設定、集計CSV、グラフは、以下のリポジトリにまとめた。この記事の参照先は2026年10月21日時点のコミットに固定している。 idunafu/irodori-engine-comparison 要約 2026/10/13に各実装のコミットを固定したので最新版の改善が反映されていない可能性がある。 測定環境: RTX 5090 + Ryzen 9 9950X + Ubuntu 24.04 MF: irodori-onnx/CUDAが最速 RF: 公式実装/CUDAが最速 ピークVRAM: speech.cpp/Vulkanが最小 (2,32 GiB) コールドスタート: speech.cpp/Vulkanが最速 使用した精度 公式版: BF16 / FP32(codec) speech.cpp: F16 / FP32(codec) irodori-onnx: FP16 / FP16(codec) 比較した7実装 主比較に使ったのは、2026年10月13日に固定した次の実装である。 ! 一部を除きこの時点でのコミット以降はベンチマークの対象としていない。 実装 対象モデル バックエンド 実用設定のモデル重み Irodori-TTS RF・MF CUDA BF16 audio.cpp RF CUDA・Vulkan F16 speech.cpp RF・MF Vulkan (・CUDA1) F16 sttts-gui RF・MF Vulkan FP32 Irodori-TTS-burn RF・MF Vulkan / WGPU FP32 irodori-onnx MF CUDA FP16 Irodori-TTS-Optimiz RF CUDA BF16 RFはRectified Flow、MFはMeanFlowである。今回はRFを40ステップ、MFを4ステップで動かしている。sttts-guiはLinux用のsttts-sayと同じTTSエンジンを測定対象にした。 モデルとバックエンドの組み合わせは14通りである。それぞれにFP32の共通基準と実用設定を用意し、合計24設定を測定した。sttts-guiとBurnは両方ともFP32なので、測定結果を共有している。 RFにはAratako/Irodori-TTS-v4.1-Small、MFにはAratako/Irodori-TTS-v4.1-Small-MFを使った。GGUFとONNXも同じ公式チェックポイントから変換している。整数への量子化は行っていない。 codecの重みは基本的にFP32である。ONNXの実用設定だけはdecoderがFP16で、参照音声のencoderはFP32である。この違いは後述の速度差にも関わる。 測定環境と条件 ハードウェアとruntime 項目 設定 GPU NVIDIA GeForce RTX 5090、VRAM 32 GB CPU AMD Ryzen 9 9950X、演算スレッド数8 OS Ubuntu 24.04 LTS GPUドライバ 610.57.04 Python 3.10.20 PyTorch 2.10.0+cu128 ONNX Runtime 1.23.2 ネイティブビルド C++はRelease、CUDAアーキテクチャ120、Rustは--release --locked 各実装の環境は分離し、GPUを使う処理は逐次実行している。制御できるTF32は無効にした。環境とビルドの詳細はリポジトリのREADMEにある。 入力文章と生成音声の長さを揃える 入力は日本語20文章である。BOS込みのトークン数で、8〜15、16〜31、32〜63、64〜95、96〜127の5群に分け、各群に4文章ずつ配置した。 主比較では公式RF・FP32で予測した生成フレーム数を全実装に渡し、文章ごとの出力長を揃えている。これにより、ある実装だけ出力音声が短いために速く見えることを避けられる。 参照音声はつくよみちゃんコーパスのVOICEACTRESS100_054.wavである。全実装に同じ10.989958秒・モノラル・48 kHz・PCM 24 bitの音声を渡した。共通の生成条件は次のとおりである。 batch size 1、候補数1、seed 42 RF 40ステップ、MF 4ステップ captionは「自然な日本語で、落ち着いた口調で話す。」 RFはindependent CFG、text / captionが3、speakerが5、適用区間0.5〜1.0、linear schedule 透かし、ストリーミング出力、末尾トリム、追加の無音除去は無効 各実装が自動で予測する生成長を使う比較、出力を8秒に固定して入力長を変える比較、代表文章から2・4・8・16・24秒の音声を生成する比較も、別に測定した。入力文章と生成設定も公開している。 推論時間とキャッシュの測定条件 この記事では主に、モデルをロードしてwarmupした後の通常推論時間を比較する。要求の処理開始から、完成したPCMがCPU上に揃うまでを測る。WAVファイルの保存時間とプロセス間通信は除いている。 各条件で2回warmupし、5回測定した中央値を求めた。主比較の表にはその値をさらに20文章で集計した中央値を載せている。モデルロードと初回推論は新しいworkerを3回起動して別に測っている。 RTFは「推論時間 ÷ 出力音声の秒数」である。たとえば8秒の音声を80 msで生成すると、RTFは0.01になる。表のRTFも各文章で計算した値の中央値である。 キャッシュ条件は次の2つに分けた。 毎回エンコード:参照音声などの条件を要求ごとにエンコードする。 キャッシュあり:登録済みvoiceや参照latentなど、各実装が備えているキャッシュを使う。 キャッシュできる範囲は実装によって異なる。測定のために新しいキャッシュ機構を足すことはせず、それぞれの既存機能を使った。指標の詳しい定義は測定方法にまとめている。 MFの実用設定での比較 4ステップのMFでは、実用設定でONNX版が最速であった。主比較の全20文章、同一生成長での結果を、キャッシュありの推論時間が短い順に並べた。時間はms、VRAMはMiBである。 実装 / backend モデル重み 毎回エンコード↓ キャッシュあり↓ RTF(キャッシュあり)↓ ピークVRAM(キャッシュあり)↓ irodori-onnx / CUDA FP16 146.2 ms 103.9 ms 0.004 5,908 MiB 公式 / CUDA BF16 195.0 ms 149.3 ms 0.007 12,004 MiB speech.cpp / Vulkan F16 239.8 ms 194.6 ms 0.008 2,377 MiB Irodori-TTS-burn / Vulkan FP32 511.8 ms 232.9 ms 0.010 7,606 MiB speech.cpp / CUDA (参考) F16 380.1 ms 322.9 ms 0.014 2,812 MiB sttts-gui / Vulkan FP32 1,323.2 ms 1,153.2 ms 0.049 4,529 MiB 出典:MFの主比較結果、集計CSV。 ONNX版はキャッシュありでは公式版より推論時間が約30.5%短く、速度比で約1.44倍であった。毎回エンコードする条件でも、上位3種の順位は同じである。後述するが、ONNX版はdecoderがFP16であることに注意。 後編の品質比較はこのMFの上位3種、ONNX CUDA・公式CUDA・speech.cpp Vulkanを対象にしている。 RFの実用設定での比較 40ステップのRFでは、公式BF16版が最速であった。こちらもキャッシュありの推論時間で並べている。 実装 / backend モデル重み 毎回エンコード キャッシュあり RTF(キャッシュあり) ピークVRAM(キャッシュあり) 公式 / CUDA BF16 601.1 ms 555.2 ms 0.024 12,000 MiB Irodori-TTS-Optimiz / CUDA BF16 616.5 ms 576.7 ms 0.025 9,264 MiB audio.cpp / CUDA F16 772.6 ms 722.4 ms 0.030 8,056 MiB speech.cpp / Vulkan F16 961.3 ms 915.4 ms 0.042 2,707 MiB audio.cpp / Vulkan F16 1,064.2 ms 977.5 ms 0.041 10,423 MiB speech.cpp / CUDA F16 1,829.6 ms 1,772.6 ms 0.077 3,030 MiB Irodori-TTS-burn / Vulkan FP32 2,263.9 ms 2,015.9 ms 0.090 7,666 MiB sttts-gui / Vulkan FP32 8,347.5 ms 8,180.0 ms 0.344 4,654 MiB 出典:RFの主比較結果。 公式版とOptimiz版は近い値である。speech.cpp Vulkan版は推論時間では4番目であるが、観測ピークVRAMは2,707 MiBに収まった。速度とメモリ使用量で特徴が分かれる。 同じ公式版のキャッシュありで比べると、RFは555.2 ms、MFは149.3 msである。MFはステップ数が少なく、推論全体にかかる時間も短くなった。一方、RFからMFへ変えると、最も時間のかかる処理も変わる。 MFの処理段階ごとの速度差 ONNX版が速い理由を見るため、処理段階ごとの時間を確認した。以下はMF・実用設定・キャッシュありの結果である。 実装 サンプリング DACVAE decode 通常推論 公式 / CUDA 26.2 ms 111.2 ms 149.3 ms irodori-onnx / CUDA 33.1 ms 65.1 ms 103.9 ms speech.cpp / Vulkan 40.8 ms 145.4 ms 194.6 ms サンプリングは条件のエンコード後から最終latentができるまで、DACVAE decodeはそのlatentからPCMが完成するまでである。段階別の値はGPU同期を入れた別実行で測り、通常推論とは独立に比較している。測定結果 サンプリングだけなら公式版の方が速く、PCM完成までではONNX版の方が速くなった。公式版のサンプリングは26.2 msであるが、decodeには111.2 msかかる。ONNX版のdecodeは65.1 msで、ここに大きな差があることが分かった。 MFは4ステップでサンプリングにかかる時間が短いため、codecの処理時間が全体に大きく影響する。 ONNX版の実用設定はdecoderもFP16である。今回速かったのは、TTSモデル本体と音声デコーダーを含めた構成である。 FP32と実用設定の比較 モデルとcodecの重みをFP32にした共通基準では、MFの上位は次のようになった。キャッシュありの通常推論時間である。 実装 FP32基準 実用設定 speech.cpp / Vulkan 199.1 ms 194.6 ms 公式 / CUDA 204.2 ms 149.3 ms Irodori-TTS-burn / Vulkan 232.9 ms 232.9 ms irodori-onnx / CUDA 249.7 ms 103.9 ms 出典:FP32と実用設定の比較表。 FP32ではspeech.cpp Vulkan版が最速である。ONNX版は249.7 msから103.9 msへ大きく短縮し、実用設定で順位が逆転した。 RFにも同じ傾向がある。FP32ではspeech.cpp Vulkan版が951.2 ms、公式CUDA版が1,340.2 msであるが、実用設定ではそれぞれ915.4 msと555.2 msになる。公式版はBF16化による短縮が大きく、speech.cpp Vulkan版はF16化による時間の変化が比較的小さくなった。 ここで揃えているのは重みの精度である。kernel内部のキャストや積算は各バックエンドの実装に従う。この区別は後編でcodecの数値差を調べるときにも出てくる。 キャッシュ・VRAM・初回起動の比較 同じ話者のキャッシュを再利用する MFの上位3種では既存キャッシュを使うと、主比較の推論時間の中央値が約42〜46 ms短縮した。Burnは511.8 msから232.9 msとなり、短縮幅は約279 msである。 Burnはvoice登録のたびにcodec encoderを読み込む構成で、毎回エンコードする条件の参照処理283.9 msのうち、encoderロードが170.2 msを占めている。登録済みvoiceの再利用によって、処理時間が特に大きく短縮した。参照音声処理の測定表 MFの速度上位3種のVRAM使用量 キャッシュありの観測ピークVRAMは、speech.cppは約2.32 GiB、ONNXは約5.77 GiB、公式は約11.72 GiBである。 VRAMはNVMLで10 msごとに観測した。表は推論時の観測ピークである。公式版では、一度扱った長い形状に対応するPyTorch allocatorの確保領域も残る。数値には条件キャッシュとallocatorを含めた実行状態が反映されている。VRAMの測定方法 モデルロードと初回推論 1回だけ起動して使う場合を考え、常駐時の速さに加えて、MF・実用設定のモデルロードと初回推論も測っている。代表文章p09を使い、新しいworkerを3回起動した中央値である。 実装 モデルロード 初回推論 irodori-onnx / CUDA 950.3 ms 543.2 ms 公式 / CUDA 4,151.6 ms 1,009.1 ms speech.cpp / Vulkan 293.2 ms 256.4 ms モデルロードは重み・tokenizer・codecの読み込みからGPUへの配置完了まで、初回推論はロード後の最初の要求である。OSとドライバのキャッシュは保持している。ロード・初回の結果 常駐時の最速はONNX版であるが、この3種ではモデルロードと初回推論が最短なのもspeech.cpp Vulkan版の特徴であった。 入力文章の長さと出力音声の長さの影響 次のグラフは主比較とは別に、出力を8秒に固定して20文章の入力長を変えた結果である。MF・実用設定・キャッシュありで、左が通常推論、右がサンプリング時間である。 今回の入力長の範囲では、多くの実装で処理時間がほぼ横ばいである。出力長を固定すると、文章のトークン数を増やした影響は比較的小さくなっている。 次は逆に、代表文章p09を固定し、出力音声の長さを変えた結果である。 上位3種の通常推論を抜き出すと、次の値になる。各セルはその条件で5回測定した中央値である。 出力長 ONNX CUDA 公式CUDA speech.cpp Vulkan 2秒 29.9 ms 45.6 ms 43.7 ms 8秒 48.2 ms 72.9 ms 87.0 ms 24秒 104.7 ms 150.5 ms 197.7 ms 出典:文章別・条件別CSV。 2秒ではspeech.cpp Vulkan版が公式版より少し速く、8秒・24秒では公式版が速くなる。ONNX版はこの3条件すべてで最速であった。単一の出力長での順位に加え、音声が長くなるにつれて処理時間がどう増えるかも分かる。 自動生成長とspeech.cppの30秒上限 各実装の自動生成長を使う測定では、speech.cppが20文章のうち8文章を30秒上限で拒否した。成功したのは12文章である。同一生成長の主比較は全24設定・両キャッシュ条件で20文章すべて成功している。 全体では3,120条件のうち2,992条件が成功し、128条件がこの自動生成長の上限超過で拒否された。自動生成長の結果を見るときは、成功件数と生成された秒数も併せて確認する必要がある。自動生成長の結果と失敗記録 runtime更新による公式MFの速度低下 主比較を終えた後、2026年10月15日にruntimeの更新前後も測った。エンジンとモデルを固定し、PyTorchを2.10.0+cu128から2.14.1+cu130へ、ONNX Runtimeを1.23.2から1.31.0へ更新した比較である。 公式MFのBF16・キャッシュありでは、同一生成長20文章の推論時間の中央値が150.53 msから167.39 msへ増えた。ONNX MFのFP16・キャッシュありは103.81 msから102.33 msで近い値である。runtime更新のA/B比較 公式MFのどの処理が遅くなったのかを調べると、FP32で動くDACVAEのdecodeにかかる時間が増えていた。 cuDNNだけを入れ替える 24秒分の同じlatentを保存し、同じ入力を各環境のcodecに渡した。 環境 24秒音声のdecode PyTorch 2.10 / CUDA 12.8 / cuDNN 9.10.2 120.91 ms 上と同じ環境でcuDNNだけ9.24.0へ更新 136.57 ms PyTorch 2.14.1 / CUDA 13.0 / cuDNN 9.24.0 136.92 ms cuDNNだけの更新で、runtime全体の更新とほぼ同じ速度低下を再現できた。さらに、最も遅くなったConv1dを取り出し、TTSやcodec全体を読み込まない単層の実験でも確認している。 その層の処理時間は旧環境では0.747 ms、新しいcuDNNでは4.192 msであった。GPUトレース2を見ると、旧環境のSGEMM系kernel 1個から、FFT 21個とGEMV 10個を起動する経路に変わっていた。 今回の速度低下の主因は、cuDNN 9.24がFP32 Conv1dで選ぶ既定アルゴリズムであった。 MF本体のステップ数やテキスト処理から切り離して再現できている。段階別計測と単層実験 アルゴリズムの実測選択を有効にする 専用workerで次の設定を有効にし、畳み込みアルゴリズムを実測して選ばせた。 torch.backends.cudnn.benchmark = True 単層Conv1dはSGEMM系に切り替わり、0.520 msになった。代表文章p09・24秒・MF BF16・キャッシュありの通常推論も、旧環境149.44 ms、新環境164.65 msに対し、実測選択ありでは135.95 msまで短縮した。 新しい形状を初めて処理するときには探索コストがかかる。単層の初回呼び出しは既定設定の約51 msから実測選択ありの約112 msへ増えた。常駐後の処理時間は短くなるが、初回の処理時間は長くなる。回避設定の測定結果 MFモデルをBF16にしていてもcodecはFP32なので、このdecodeの影響は残る。速度を比較するときは、モデルのdtypeに加えて、codecとruntimeの構成も記録しておく意味があった。 速度比較で分かったこと このRTX 5090環境では、MFの実用設定でONNX版、RFの実用設定で公式版が最速であった。MFの速度上位3種には、それぞれ次の特徴がある。 irodori-onnx CUDA:キャッシュあり103.9 ms。PCM完成までの総合時間が最短である。ただし、decoderがFP16である。 公式Irodori-TTS CUDA:MF 149.3 ms、RF 555.2 ms。MFのサンプリング自体も速く、RFでは主比較で最速になった。 speech.cpp Vulkan:MF 194.6 ms、観測ピークVRAM 2,32 GiB。ロードと初回推論も、この3種では最短であった。 結果を処理段階に分けて見ると、MFではcodecの速度が推論全体の時間に大きく影響していた。FP32と実用設定で順位が変わり、runtime更新によってcodecの畳み込み経路も変わる。実装名だけでなく、精度・キャッシュ・出力長・runtimeまで揃えて測ることで、それぞれの特徴がはっきりした。 後編の品質評価ではこのMFの上位3種を対象にし、MFは3実装、RFは対応する公式版とspeech.cppの2実装で、読みと話者再現性を比較した。 後編はこちら : 余談 この記事はベンチマークのリポジトリを基にGPT-6 Proが執筆したものを、yomiyasuを使った上でAstraに推敲させ、人間が読みやすいように人間がさらに修正を加えている。 Zenn特有の記法や技術記事には不慣れなため、アドバイスいただけると幸いです。 脚注 Codexに任せていたらなぜかCUDA版もビルドしてベンチマークしていた。本来は非サポート。 ↩︎ Codexが勝手にやってくれた すごいな ↩︎

  6. speech.cpp v0.8.10はCLIをちょっとかわいくしました! かわいいCLIはだいじなので。 nyosegawa/speech.cpp ■speech.cppとは ・Python/CUDA不要の音声合成・音声認識エンジンです ・モデル対応状況  TTS: irodori-tts, qwen3-tts, drama-3  ASR: qwen3-asr, reazonspeech-v2, parakeet-tdt t.co

    33 likes · 7 reposts · 2,059 views
  7. speech.cpp v0.8.8リリースしました🚀 Fish Audioのdrama-3に対応し、M5のMacでの高速化もしました! nyosegawa/speech.cpp PythonもCUDAも不要ですぐインストールして使えるのでぜひ試してみてください! drama-3 の動作の目安: ・Macはメモリ32GB以上 ・Windows/LinuxはVRAM 8GB以上+RAM 32GB t.co

    83 likes · 13 reposts · 3,691 views
  8. ねえ、これ見た?GitHubにめっちゃすごそうなプロジェクトがあるよ! speech.cppっていうんだけど、音声の合成とか認識がこれ一つでできちゃうんだって。 C++で書かれてて、MetalとかVulkanでも動くみたいだから、かなり技術力の塊って感じ! AIと自然にお話しできる未来が、すぐそこまで来てるのかもね。 これからの進化、マジで楽しみすぎるんだけど! nyosegawa/speech.cpp #news

  9. AIの新しい技術についてのニュースです。 speech.cppというプログラムが公開されました。 これを使うと、声を作ったり、人の言葉を理解したりする技術を、ひとつの仕組みで動かせそうです。 さまざまなモデルに対応していて、コンピュータでスムーズに動作するのが特徴のようです。 まるでアニメの世界のように、機械と自然にお話しできる未来が、すぐそこまで来ているのかもしれませんね。 nyosegawa/speech.cpp #news

  10. GitHub - nyosegawa/speech.cpp: Speech synthesis, speech recognition and voice activity detection for... 要約: 記事は、C++で開発された音声合成、音声認識、音声活動検出ツール「speech.cpp」について説明しています。このツールは、ggmlを使用してCPU、Metal、Vulkanで動作し、様々なモデルをサポートしています。音声合成モデルとしてQwen3-TTS、音声認識モデルとしてQwen3-ASR、音声活動検出モデルとしてSilero VADが利用可能です。インストール手順...

Achievements

Awards earned by the repositories nyosegawa owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.