0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカル推論でブラウザ上の音声をリアルタイム文字起こしする

0
Last updated at Posted at 2025-12-31

ブラウザのストリーム音声を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 の実行環境

:point_up: 仮想環境(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 の音声アシスタント構築
0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?