Vosk уходит в тень: Переходим на Whisper для потоковой расшифровки аудио

Pavel P
12.11.2025 13:49
 

Коротко: «как в Vosk по WebSocket, только на Whisper» делается тремя рабочими путями:

  • локально через faster-whisper (Python, CTranslate2, есть квазистриминг);
  • локально через whisper.cpp (есть готовый WS/HTTP сервер);
  • облачный провайдер со стримингом (если не критичен self-host).
  • Ниже даю самый практичный способ — WebSocket-сервер на Python + faster-whisper с браузерным PCM-стримом (16 kHz mono, int16). Это максимально похоже на ваш Vosk-сетап.

    Вариант A. WebSocket + faster-whisper (self-host)

    1) Клиент (браузер): снимаем микрофон → 16 kHz PCM16 → WebSocket

    a. Подключаем AudioWorklet, чтобы сразу отдать на сервер «сырые» 16-битные фреймы.

    public/pcm16-worklet.js


    class PCM16Processor extends AudioWorkletProcessor {
    constructor() {
    super();
    this.targetRate = 16000;
    this._resampleBuffer = [];
    this._srcRate = sampleRate; // системная частота, напр. 48000
    this._ratio = this._srcRate / this.targetRate;
    this._acc = 0;
    }

    process(inputs) {
    const input = inputs[0];
    if (!input || !input[0]) return true;

    const ch0 = input[0]; // Float32Array [ -1..1 ]
    // Ресэмплинг простым децимационным методом (для прод — лучше sinc/soxr)
    for (let i = 0; i < ch0.length; i++) {
    this._acc += 1;
    if (this._acc >= this._ratio) {
    const s = Math.max(-1, Math.min(1, ch0[i]));
    const int16 = s < 0 ? s * 0x8000 : s * 0x7FFF;
    this._resampleBuffer.push(int16);
    this._acc -= this._ratio;
    }
    }

    // Пакуем по 100 мс (1600 сэмплов @16k) — баланс латентности/нагрузки
    const CHUNK = 1600;
    while (this._resampleBuffer.length >= CHUNK) {
    const chunk = this._resampleBuffer.splice(0, CHUNK);
    const buf = new Int16Array(chunk);
    this.port.postMessage(buf.buffer, [buf.buffer]);
    }
    return true;
    }
    }
    registerProcessor('pcm16-processor', PCM16Processor);

    b. Инициализация в приложении:


    const ws = new WebSocket('wss://YOUR_HOST:2700/stt'); // ваш порт/путь
    ws.binaryType = 'arraybuffer';

    await audioContext.audioWorklet.addModule('/pcm16-worklet.js');
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const source = audioContext.createMediaStreamSource(stream);
    const node = new AudioWorkletNode(audioContext, 'pcm16-processor');

    node.port.onmessage = (e) => {
    if (ws.readyState === 1) ws.send(e.data); // raw PCM16LE
    };

    ws.onopen = () => {
    // «приветствие» как в vosk: meta-json перед бинарными фреймами
    ws.send(JSON.stringify({ op: 'start', format: 'pcm16le', rate: 16000, channels: 1, lang: 'ru' }));
    source.connect(node).connect(audioContext.destination); // dest можно не подключать, если не нужно эхо
    };

    ws.onmessage = (e) => {
    // сервер шлёт JSON с partial/final
    const msg = JSON.parse(e.data);
    if (msg.type === 'partial') console.log('PARTIAL:', msg.text);
    if (msg.type === 'final') console.log('FINAL :', msg.text, msg.ts, msg.te);
    };
    Если у вас уже есть свой AudioWorklet под Vosk — можно оставить его, лишь поменяв частоту/формат на 16 kHz PCM16 и протокол шины.

    2) Сервер (Python): WebSocket, буферизация, «квазистриминг» на faster-whisper

    Установка:


    python -m venv .venv && source .venv/bin/activate
    pip install faster-whisper websockets numpy webrtcvad uvloop
    # Для GPU: pip install ctranslate2>=4.0 && поставьте CUDA/cuDNN; faster-whisper сам его использует
    # Скачайте модель при первом старте: tiny / base / small / medium / large-v3

    Сервер server.py:


    import asyncio, json, numpy as np, websockets, webrtcvad, time
    from faster_whisper import WhisperModel

    MODEL_SIZE = "small" # баланс: tiny/base/small/medium/large-v3
    DEVICE = "cuda" # "cpu" или "cuda"
    COMPUTE_TYPE = "auto" # для GPU: "float16" / "int8_float16"
    CHUNK_MS = 100 # длина входного фрейма (как на клиенте)
    WINDOW_SEC = 8.0 # длина скользящего окна для расшифровки
    STEP_SEC = 2.0 # как часто запускать инференс
    SAMPLE_RATE = 16000

    model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)
    vad = webrtcvad.Vad(2) # 0..3, выше — строже к шуму

    async def transcribe_session(ws):
    buf = np.zeros(0, dtype=np.int16)
    last_infer = 0.0
    started = False

    async for msg in ws:
    # первые сообщения могут быть JSON (метаданные)
    if isinstance(msg, (str, bytes)) and isinstance(msg, str):
    try:
    meta = json.loads(msg)
    if meta.get("op") == "start":
    started = True
    await ws.send(json.dumps({"type":"ready"}))
    continue
    except Exception:
    pass

    # далее ожидаем бинарные PCM16LE чанки
    if isinstance(msg, (bytes, bytearray)):
    pcm = np.frombuffer(msg, dtype=np.int16)
    # простая VAD-обрезка тишины по краям чанка (опционально)
    frame_bytes = pcm.tobytes()
    is_speech = vad.is_speech(frame_bytes[:320*2], SAMPLE_RATE) if len(pcm) >= 320 else True
    if is_speech:
    buf = np.concatenate([buf, pcm])

    now = time.time()
    if now - last_infer >= STEP_SEC and buf.size > 0:
    last_infer = now
    # ограничим окно
    max_samples = int(WINDOW_SEC * SAMPLE_RATE)
    if buf.size > max_samples:
    buf = buf[-max_samples:]

    # faster-whisper не «истинно» стримит, но быстро обрабатывает окно
    segments, info = model.transcribe(
    buf.astype(np.float32) / 32768.0,
    language="ru",
    vad_filter=True,
    beam_size=1,
    best_of=1,
    no_speech_threshold=0.6,
    condition_on_previous_text=False,
    word_timestamps=True
    )

    # берём последнюю гипотезу как partial
    partial_text = ""
    ts, te = None, None
    for seg in segments:
    partial_text += seg.text
    ts, te = seg.start, seg.end

    await ws.send(json.dumps({
    "type": "partial",
    "text": partial_text.strip(),
    "ts": ts, "te": te
    }))

    # на закрытие допрожимаем «финал»
    if buf.size > 0:
    segments, info = model.transcribe(
    buf.astype(np.float32) / 32768.0,
    language="ru",
    vad_filter=True,
    beam_size=1,
    best_of=1,
    condition_on_previous_text=False,
    word_timestamps=True
    )
    final_text = "".join(seg.text for seg in segments).strip()
    if final_text:
    await ws.send(json.dumps({"type":"final","text":final_text}))

    async def main():
    async with websockets.serve(transcribe_session, host="0.0.0.0", port=2700, max_size=None):
    print("WS STT server on ws://0.0.0.0:2700/stt")
    await asyncio.Future()

    if __name__ == "__main__":
    try:
    import uvloop; uvloop.install()
    except Exception:
    pass
    asyncio.run(main())

    Что важно знать про «стрим» в Whisper:

  • У «ванильного» Whisper нет настоящего онлайн-декодера как у Vosk/kaldi. Трюк — крутить скользящее окно (например 6–10 сек) и регулярно пересобирать гипотезу.
  • Для низкой задержки удерживайте WINDOW_SEC небольшим (6–8 сек), а STEP_SEC — 0.5–2 сек.
  • faster-whisper на GPU работает существенно быстрее; на CPU берите tiny/base/small и compute_type=int8.
  • Можно улучшить устойчивость через webrtcvad (как в примере) и/или post-VAD по RMS.
  • Вариант B. whisper.cpp с готовым сервером

    Если нравится C++/без Python:

  • Сборка:

  • git clone https://github.com/ggerganov/whisper.cpp
    cd whisper.cpp && make -j
  • Есть примеры examples/server (HTTP/WS) и examples/stream.Запускаете сервер с вашей моделью ggml/gguf, а браузер шлёт PCM16 по WS.
  • Плюсы: минимальные зависимости, быстрый запуск; Минусы: меньше гибкости по VAD/постобработке, чем в Python-стеке.
  • Советы по миграции с Vosk

  • Формат аудио: оставьте тот же протокол (JSON «start» → бинарные PCM16 чанки → «end»), просто поменяйте серверную часть на Whisper.
  • Части/partial: у Vosk они «настоящие», у Whisper — «пересобранные» по окну. Для UX покажите «серую» подсказку (partial), а «чёрным» финализируйте по паузе (VAD) или по закрытию utterance (silence ≥ 500–800 мс).
  • Язык: фиксируйте language="ru" (или autodetect для многоязычия, но это +латентность).
  • Таймкоды: берите word_timestamps=True в faster-whisper — удобно подсвечивать слова.
  • Диааризация: при необходимости добавьте pyannote.audio поверх PCM-стрима параллельно.
  • Если хочется прям «настоящий» realtime (низкая задержка <300 мс)

    Это уже не Whisper в чистом виде. Либо используйте провайдеры со streaming ASR (Deepgram, AssemblyAI и т.п.), либо OpenAI-модели с realtime-интерфейсами (нужна отдельная интеграция и иной протокол). Для self-host — пока лучший компромисс именно через faster-whisper со скользящим окном.

    Мини-чеклист продакшена

  • Транспорт: WS ping/pong, авто-reconnect, очереди при backpressure.
  • Ограничения: max сессий, max битрейт (16 kHz mono достаточно).
  • Безопасность: JWT в первом JSON (как у вас с Vosk), CORS/origins.
  • Логи/метрики: время до первого partial, WER/длина фраз, GPU util.
  • Горячая смена модели: tiny/base/small/medium по настройке комнаты.
  • Хочешь — адаптирую этот пример под твой текущий Vosk-клиент (у тебя уже есть AudioWorklet и JWT на сокете) и под твой деплой (GPU/CPU, systemd, Nginx proxy, wss).

    0
    0
    0
    Опубликовано:
    Комментариев:0
    Репостов:0
    Просмотров: 0