Коротко: «как в 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).