🎤

Windows上でVoskを動かしてリアルタイム音声認識

に公開

通話やPCの音声をリアルタイムで字幕化したいときに便利なのが Vosk
この記事では、マイク入力だけでなく VB-Audio Virtual Cableを利用して通話アプリの音声も認識できる環境を構築する手順をまとめます。


1. 事前準備

Python インストール

  • Windows 向けにPython公式 から3.12をインストール。
  • PATH に追加するオプションにチェックを入れてください。

必要ライブラリのインストール

PowerShell で以下を実行:

pip install sounddevice vosk

2. Vosk 日本語モデルの準備

Vosk公式から日本語モデルをダウンロードします

解凍したフォルダを C:\models\vosk-model-ja-0.22 に配置してください。
smallモデルもありますが、ある程度の精度を求めるなら大モデルを推奨。


3. VB-Audio Virtual Cable(通話音声取り込み用)

通話アプリの音声を直接取得するために VB-Audio Virtual Cable を使います。

  • 公式サイト から
    VB-CABLE_Setup_x64.exe をダウンロード
  • 管理者権限で実行し Install → 再起動
  • 通話アプリ(例:MicroSIP)で「再生デバイス」を CABLE Input に設定

4. Python スクリプト

マイク入力と Virtual Cable の両方に対応し、リアルタイムに字幕を表示します。

import tkinter as tk
from tkinter import ttk, messagebox
import threading, queue, json
import sounddevice as sd
from vosk import Model, KaldiRecognizer

# ===== 設定 =====
MODEL_PATH = r"C:\models\vosk-model-ja-0.22"
SAMPLE_RATE = 16000
BLOCKSIZE   = 8000
MODE = "mic"        # "mic" または "vb-cable"
VB_CABLE_NAME = "CABLE Output"
# =================

def find_vb_cable_input():
    devs = sd.query_devices()
    for i, d in enumerate(devs):
        name = d.get("name", "")
        if d.get("max_input_channels", 0) >= 1 and VB_CABLE_NAME.lower() in name.lower():
            return i
    return None

class RecognizerThread(threading.Thread):
    def __init__(self, q, device_index=None):
        super().__init__(daemon=True)
        self.q = q
        self.running = True
        self.model = Model(MODEL_PATH)
        self.rec = KaldiRecognizer(self.model, SAMPLE_RATE)
        self.device_index = device_index

    def _cb(self, indata, frames, time_info, status):
        if status:
            print(status)
        self.q.put(bytes(indata))

    def run(self):
        with sd.RawInputStream(
            samplerate=SAMPLE_RATE, blocksize=BLOCKSIZE,
            dtype='int16', channels=1,
            callback=self._cb, device=self.device_index
        ):
            while self.running:
                sd.sleep(50)

    def stop(self):
        self.running = False

class App:
    def __init__(self, root):
        self.root = root
        root.title("Live Captions (Mic/VB-Cable – Vosk)")
        frm = ttk.Frame(root, padding=8); frm.grid(sticky="nsew")
        root.rowconfigure(0, weight=1); root.columnconfigure(0, weight=1)

        ttk.Label(frm, text="Partial(リアルタイム)").grid(row=0, column=0, sticky="w")
        self.partial = tk.Text(frm, height=6, wrap="word"); self.partial.grid(row=1, column=0, sticky="nsew")
        ttk.Label(frm, text="Final(確定文/ログ)").grid(row=2, column=0, sticky="w", pady=(8,0))
        self.final = tk.Text(frm, height=12, wrap="word"); self.final.grid(row=3, column=0, sticky="nsew")

        btns = ttk.Frame(frm); btns.grid(row=4, column=0, sticky="e", pady=(8,0))
        self.start_btn = ttk.Button(btns, text="Start", command=self.on_start)
        self.stop_btn  = ttk.Button(btns, text="Stop",  command=self.on_stop, state="disabled")
        self.start_btn.grid(row=0, column=0, padx=4); self.stop_btn.grid(row=0, column=1, padx=4)

        frm.rowconfigure(1, weight=1); frm.rowconfigure(3, weight=2); frm.columnconfigure(0, weight=1)

        self.q_audio = queue.Queue()
        self.rec_thread = None
        self.model = Model(MODEL_PATH)
        self.rec    = KaldiRecognizer(self.model, SAMPLE_RATE)

        self.root.after(50, self._pump)

    def on_start(self):
        dev_index = None
        if MODE.lower() == "vb-cable":
            dev_index = find_vb_cable_input()
            if dev_index is None:
                messagebox.showerror("デバイス未検出", f"'{VB_CABLE_NAME}' が見つかりません")
                return
        self.rec_thread = RecognizerThread(self.q_audio, device_index=dev_index)
        self.rec_thread.start()
        self.start_btn.config(state="disabled"); self.stop_btn.config(state="normal")

    def on_stop(self):
        if self.rec_thread:
            self.rec_thread.stop()
            self.rec_thread = None
        self.start_btn.config(state="normal"); self.stop_btn.config(state="disabled")

    def _pump(self):
        try:
            while True:
                data = self.q_audio.get_nowait()
                if self.rec.AcceptWaveform(data):
                    res = json.loads(self.rec.Result())
                    text = res.get("text", "").strip()
                    if text:
                        self.final.insert("end", text + "\n")
                        self.final.see("end")
                    self.partial.delete("1.0", "end")
                else:
                    pres = json.loads(self.rec.PartialResult())
                    ptxt = pres.get("partial", "").strip()
                    self.partial.delete("1.0", "end")
                    self.partial.insert("end", ptxt)
                    self.partial.see("end")
        except queue.Empty:
            pass
        self.root.after(50, self._pump)

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.protocol("WM_DELETE_WINDOW", lambda: (app.on_stop(), root.destroy()))
    root.mainloop()

5. 実行方法

マイク入力で試す

py .\ファイル名.py
  • Start ボタン →
    話すと「Partial」にリアルタイム表示、「Final」に確定文がログされます

通話音声(VB-Cable)を使う場合

  1. MODE = "vb-cable" に変更して保存
  2. 通話アプリの出力デバイスを CABLE Input に設定
  3. スクリプト実行 → 通話の音声が字幕化されます

まとめ

  • Vosk は軽量かつ高精度に日本語音声を認識できるOSSライブラリ
  • VB-Audio Virtual Cable を組み合わせれば、通話アプリやシステム音声も字幕化可能
  • GUI付きで Partial/Final を分けて表示することで、会話中の利用にも適します

この仕組みをベースに、後段で自然言語処理や要約、検索連携を加えることも可能です。

Discussion