ブラウザのストリーム音声をAIでリアルタイム文字おこしする
目標
- 音声ファイルではなく、ブラウザ上の音声をキャプチャし、AIで文字おこしを行う
- 完全なローカル推論
- ある程度の追従性・高速さ
- リアルタイムで!!!
- [optional] 自動翻訳・多言語対応
- [optional] 自動保存・大きく見やすく表示する
目標とする構成
🎧 YouTubeのようなブラウザで流れる音声をリアルタイム文字起こしする
ブラウザ →(出力)→ BlackHole → whisper-web(ブラウザ) → whisper-server → 文字起こし結果
- YouTube の音声を BlackHole に流す
- ブラウザ(whisper-web)が BlackHole を “マイク入力” として取得
- WebSocket 経由で whisper-server に送信
- whisper-server がリアルタイムで文字起こし
環境
- MACBook Pro M2 (Apple Slicon)
- whisper.cpp
インストール
brew install blackhole-2ch cmake ffmpeg
git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
bash ./models/download-ggml-model.sh medium # model(medium) バイナリをダウンロード 1.5G
git submodule update --init --recursive
cmake -B build -DGGML_METAL=ON .
cmake --build build --config Release
Python PIP の実行環境
仮想環境(venv)を作って、その中だけで pip を使う(macOS では必須)
Homebrew Python + venvの組み合わせが最も安定する
python3 -m venv .venv # .venv というフォルダ名で仮想環境を作成
source .venv/bin/activate # 起動する
pip3 sounddevice numpy # 必要なパッケージをインストール
Whisper-server の起動
overview of Wshiper-server
- Whisper(または Faster-Whisper)を API サーバーとして動かす仕組み
- HTTP 経由で音声を送信し、Whisper.cpp が文字起こしして返す REST API サーバー
- /inference に音声ファイルを送ると → Whisper がテキストに変換して返す
- JSON / SRT / VTT など複数形式で返却可能
いくつかの方向性を考慮:
🎧 1. whisper.cpp の公式リアルタイムデモを使う
🧩 2. whisper.cpp の HTTP Server を使ってリアルタイム化する
🎧 1. whisper.cpp の公式リアルタイムデモを使う
cmake -B build -DGGML_METAL=ON . では、EXAMPLESはビルドされないことがあるので、明示的にオプションをONにする。
もし、前述の手順でビルドしていれば、一度Buildを削除する
rm -rf build
cmake -B build -DGGML_METAL=ON -DWHISPER_BUILD_EXAMPLES=ON .
cmake --build build --config Release
examplesの中にStreamがあるのでそれを使う、ということだったのですが、なぜかビルドに含まれない。
SDL2 が必要だった (Simple DirectMedia Layer 2)
macOS では SDL2 が標準で入っていないため、
CMake は SDL2 を見つけられず、自動的に OFF にします。
brew install sdl2
ビルドをもう一度やりなおす・・
rm -rf build
cmake -B build -DWHISPER_BUILD_EXAMPLES=ON -DWHISPER_SDL2=ON .
cmake --build build --config Release
ビルドされたバイナリを登録する
echo 'export PATH="$HOME/whisper.cpp/build/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile
起動
whisper-stream -m {MODEL} -l {Language}
e.g.
whisper-stream -m models/ggml-medium.bin -l {Language}
🧩 2.HTTP Server を使ってリアルタイム化する(破棄)
結論としては、より複雑で精度もでなかった。しかし、この方法も用いることは可能。チャンクの最適化を図れば、なんとかなるかもしれない。
追記-
連続プロンプト(前回の出力を次に渡す)whisper-stream では、前回の認識結果を次の推論のプロンプトとして渡す という仕組みが内部で使われているので、精度の面では太刀打ちできないと見える
1.Whisper-serverを起動して音声を待ち受ける
2.音声を投げつけて、返答を受け取るPyを書く
whisper-server -m {MODEL} --host 127.0.0.1
Python vim 2_HTTPAPIstream_to_whisper.py
import sounddevice as sd
import requests
import numpy as np
import io
import wave
from scipy.signal import resample
API_URL = "http://localhost:8080/inference"
# Whisper.cpp が要求するサンプリングレート
TARGET_RATE = 16000
# マイクの実際のサンプリングレート(自動取得)
MIC_RATE = int(sd.query_devices(sd.default.device[0], 'input')['default_samplerate'])
CHUNK_DURATION = 0.25 # 0.25秒ごとに送信
CHUNK_SIZE = int(MIC_RATE * CHUNK_DURATION)
pcm_buffer = []
def pcm_to_wav_bytes(pcm_data, rate):
buffer = io.BytesIO()
with wave.open(buffer, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(rate)
wf.writeframes(pcm_data.tobytes())
return buffer.getvalue()
def resample_to_16k(pcm):
"""マイクのレート → 16kHz に変換"""
if MIC_RATE == TARGET_RATE:
return pcm
num_samples = int(len(pcm) * TARGET_RATE / MIC_RATE)
return resample(pcm, num_samples).astype(np.int16)
def send_to_whisper(pcm_chunk):
# 16kHz に変換
pcm_16k = resample_to_16k(pcm_chunk)
# WAV 変換
wav_bytes = pcm_to_wav_bytes(pcm_16k, TARGET_RATE)
files = {
"file": ("audio.wav", wav_bytes, "audio/wav")
}
response = requests.post(API_URL, files=files)
try:
text = response.json().get("text", "").strip()
if text:
print(text)
except:
print("error")
def audio_callback(indata, frames, time, status):
global pcm_buffer
pcm = np.frombuffer(indata, dtype=np.int16)
pcm_buffer.extend(pcm)
if len(pcm_buffer) >= CHUNK_SIZE:
chunk = np.array(pcm_buffer[:CHUNK_SIZE], dtype=np.int16)
del pcm_buffer[:CHUNK_SIZE]
send_to_whisper(chunk)
print(f"Recording ({CHUNK_DURATION}s chunks)... MIC={MIC_RATE}Hz → Whisper=16000Hz")
print("Press Ctrl+C to stop.")
with sd.RawInputStream(
samplerate=MIC_RATE,
channels=1,
dtype='int16',
callback=audio_callback
):
try:
while True:
pass
except KeyboardInterrupt:
print("Stopped.")
Whisper.cpp の HTTP API は連続音声に弱い
Whisper.cpp の /inference は「毎回独立した音声ファイル」として扱うため、連続音声の文脈がつながりません。もし「連続した会話」をしたいなら:
whisper-stream(SDL2)
whisper.cpp の streaming API
あるいは自作で文脈を保持する
今後の展望
- 日本語特化モデルを使ってみる(kotoba-whisper)
- 出力スタイルを調整する
- Whisper × LLaMA の音声アシスタント構築