Локальное распознавание речи на Python: диарризация и потоковая обработка | AiManual
AiManual Logo Ai / Manual.
04 Фев 2026 Гайд

Сборка локального ASR на Python: когда облака бесят, а конфиденциальность не шутка

Полный гайд по сборке локального ASR на Python без облаков. Whisper, диарризация, потоковая обработка и полный контроль над данными. Работает на обычной видеока

Забудьте про ежемесячные счета от Google Speech-to-Text и AWS Transcribe. Забудьте про нервные согласия на передачу данных. Забудьте про задержки в сети, когда нужно распознать речь в реальном времени. Если вы читаете это, значит устали от облачной зависимости в ASR (Automatic Speech Recognition).

Я собрал десятки таких систем за последние три года — для call-центров, судебных процессов, медицинских консультаций. И каждый раз одно и то же: клиенты в ужасе от того, что их разговоры улетают в неизвестность. Особенно после того скандала в 2025-м, когда одна крупная платформа "случайно" использовала аудио для тренировки своих моделей.

Сегодня соберем систему, которая работает на вашем железе. Полностью. Без единого запроса в интернет. С диарризацией (кто что сказал) и потоковой обработкой в реальном времени. И да, это будет работать на RTX 3060 или даже на CPU, если готовы ждать подольше.

Почему локальный ASR в 2026 — это не безумие, а необходимость

Вот что происходит, когда вы отправляете аудио в облако:

  • Ваши данные становятся чьим-то тренировочным датасетом. Даже если в SLA написано обратное. Проверено на практике.
  • Задержки убивают интерактивность. 200-300ms на сеть + обработка = пользователь уже перебивает.
  • Стоимость растет непредсказуемо. Особенно если у вас всплески трафика.
  • Вы не можете кастомизировать модель под свой акцент, терминологию или шумный фон.

Главный миф: "Локальные модели хуже облачных". В 2026 это уже не так. Whisper-large-v3-turbo (вышедший в конце 2025) по точности бьет большинство коммерческих API для английского и русского. А если дообучить на своих данных — вообще небо и земля.

Что нам понадобится (и почему именно это)

Компонент Что выбираем Почему не альтернатива
Ядро ASR Whisper (OpenAI) или Qwen3-ASR Parakeet устарел, Wav2Vec2 требует тонкой настройки. Whisper работает из коробки, Qwen3-ASR поддерживает 52 языка.
Диарризация Pyannote 3.0+ с дообучением Старые версии pyannote ломались на перекрывающейся речи. В 3.0 это частично исправили, но лучше дообучить на своих данных.
Потоковая обработка Faster-Whisper + кастомный буфер Нативный Whisper ждет весь файл. Faster-Whisper режет на чанки и обрабатывает параллельно.
Аудио захват PyAudio или SoundDevice Просто работает. Для сложных сценариев есть PortAudio, но это overkill.

Если сомневаетесь между Whisper и Qwen3-ASR — посмотрите мое сравнение моделей для английского. Для русского Whisper пока стабильнее, но Qwen3-ASR догоняет и поддерживает кучу экзотических языков.

1 Ставим железо на место: от установки до первой проверки

Сначала убедимся, что у нас есть CUDA. Без нее на CPU будет больно.

# Проверяем CUDA
nvidia-smi

# Если нет — ставим (для Ubuntu 22.04+)
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-4

Важно: На февраль 2026 актуальна CUDA 12.4. Не ставьте 11.x — некоторые новые оптимизации Whisper требуют именно 12+.

Теперь Python-окружение. Я ненавижу conda, поэтому покажу через venv:

python -m venv asr_env
source asr_env/bin/activate  # или asr_env\Scripts\activate на Windows

# Core-зависимости
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124
pip install faster-whisper transformers pyannote.audio pyaudio sounddevice

Если pyaudio не ставится на Windows — качайте prebuilt wheels. На Linux может потребоваться libportaudio.

2 Берем модель Whisper и заставляем ее летать

Первая ошибка новичков — качать огромную large-v3 (6.7GB) для тестов. Начните с small или medium.

from faster_whisper import WhisperModel
import time

# НЕ ТАК: model = WhisperModel("large-v3", device="cuda") — будет долго грузиться
# ТАК:
model = WhisperModel("medium", device="cuda", compute_type="float16")

# Проверяем
start = time.time()
segments, info = model.transcribe("test_audio.wav", language="ru")
print(f"Detected language: {info.language}, probability: {info.language_probability:.2f}")

for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

print(f"Time: {time.time() - start:.2f}s")

compute_type="float16" ускоряет inference в 1.5-2 раза почти без потери точности на современных GPU. Если у вас карта без поддержки float16 (очень старая), используйте "int8", но будет чуть менее точно.

💡
Секрет скорости: Whisper по умолчанию использует beam search размером 5. Для потоковой обработки поставьте beam_size=1. Точность упадет на 2-3%, но скорость вырастет в 3 раза. Для диалогов это часто приемлемо.

3 Диарризация: кто, когда и что сказал

Вот здесь начинается настоящая боль. Готовые модели pyannote часто путают голоса, особенно если в комнате эхо или несколько женщин с похожими тембрами.

Сначала базовый вариант:

from pyannote.audio import Pipeline

# Качаем модель с HuggingFace
# Требуется токен (бесплатный, но нужно зарегистрироваться)
pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-3.1",
    use_auth_token="YOUR_HF_TOKEN"
)
pipeline.to("cuda")

# Применяем
diarization = pipeline("meeting.wav")

for turn, _, speaker in diarization.itertracks(yield_label=True):
    print(f"Speaker {speaker}: from {turn.start:.1f}s to {turn.end:.1f}s")

И сразу проблема: модель обучена в основном на английских датасетах. Для русского нужно дообучать. Если нет размеченных данных — попробуйте трюки из моего гайда по диарризации.

Альтернатива — простенькая кластеризация по embeddings:

from pyannote.audio import Inference
import numpy as np
from sklearn.cluster import AgglomerativeClustering

embedding_model = Inference(
    "pyannote/wespeaker-voxceleb-resnet34-LM",
    window="whole"
)

# Извлекаем эмбеддинги каждые 0.5 секунды
embeddings = []
timestamps = []
for start in np.arange(0, duration, 0.5):
    embedding = embedding_model({"waveform": waveform, "sample_rate": sr})
    embeddings.append(embedding)
    timestamps.append(start)

# Кластеризуем (предполагаем 2 спикера)
clustering = AgglomerativeClustering(n_clusters=2).fit(embeddings)
labels = clustering.labels_

Это грубее, но работает без токена и иногда стабильнее на нестандартных акцентах.

4 Потоковая обработка: от записи до текста в реальном времени

Вот тут Whisper показывает не самую лучшую сторону. Он обучен на 30-секундных чанках и любит контекст. Но нам нужно показывать текст по мере говорения.

Мое решение — двойной буфер:

import sounddevice as sd
import numpy as np
from collections import deque
from threading import Thread

class StreamingASR:
    def __init__(self, model_name="medium", device="cuda"):
        self.model = WhisperModel(model_name, device=device, compute_type="float16")
        self.buffer = deque(maxlen=16000*30)  # 30 секунд
        self.results = []
        self.is_recording = False
        
    def callback(self, indata, frames, time, status):
        """Вызывается при каждом заполнении буфера звуковой карты"""
        if status:
            print(f"Audio error: {status}")
        self.buffer.extend(indata[:, 0])  # берем один канал
        
    def process_chunk(self):
        """Обрабатывает накопленный буфер раз в 5 секунд"""
        while self.is_recording:
            if len(self.buffer) >= 16000*5:  # 5 секунд
                audio_np = np.array(self.buffer)
                segments, _ = self.model.transcribe(audio_np, language="ru", beam_size=1)
                for seg in segments:
                    self.results.append(seg.text)
                    print(f"Live: {seg.text}")
                # Очищаем обработанное
                for _ in range(16000*4):  # оставляем 1 секунду контекста
                    if self.buffer:
                        self.buffer.popleft()
            time.sleep(0.1)
    
    def start(self):
        self.is_recording = True
        # Поток для обработки
        process_thread = Thread(target=self.process_chunk)
        process_thread.start()
        
        # Начинаем запись
        with sd.InputStream(callback=self.callback, channels=1, samplerate=16000):
            while self.is_recording:
                sd.sleep(1000)
                # Здесь можно добавить условие остановки по кнопке

Почему буфер на 30 секунд, а обрабатываем по 5? Whisper работает лучше с контекстом. Мы всегда подаем последние 5 секунд + предыдущие 25 как историю. Это уменьшает ошибки на границах предложений.

Собираем все вместе: production-ready система

Теперь склеим ASR, диарризацию и потоковую обработку в одну систему. Цель: запись совещания, где в реальном времени показывается, кто что говорит.

import queue
import threading
from dataclasses import dataclass
from typing import List

@dataclass
class Utterance:
    speaker: str
    text: str
    start: float
    end: float

class MeetingTranscriber:
    def __init__(self):
        self.asr_model = WhisperModel("medium", device="cuda", compute_type="float16")
        self.diarization_model = Pipeline.from_pretrained(...)
        self.audio_queue = queue.Queue()
        self.results: List[Utterance] = []
        
    def audio_capture_thread(self):
        """Захватывает аудио и кладет в очередь"""
        def callback(indata, frames, time, status):
            self.audio_queue.put(indata.copy())
        
        with sd.InputStream(callback=callback, channels=1, samplerate=16000,
                           blocksize=1600):  # 100ms chunks
            while True:
                sd.sleep(1000)
    
    def processing_thread(self):
        """Обрабатывает аудио, делает диарризацию и ASR"""
        buffer = np.array([])
        
        while True:
            # Собираем 10 секунд аудио
            while len(buffer) < 16000*10:
                chunk = self.audio_queue.get()
                buffer = np.concatenate([buffer, chunk[:, 0]])
            
            # Диарризация
            diarization = self.diarization_model({"waveform": buffer, "sample_rate": 16000})
            # ASR
            segments, _ = self.asr_model.transcribe(buffer, language="ru", beam_size=1)
            
            # Сопоставляем спикеров и текст
            for segment in segments:
                # Находим, какой спикер говорил в это время
                speaker = self.find_speaker_for_interval(diarization, segment.start, segment.end)
                utterance = Utterance(
                    speaker=speaker,
                    text=segment.text,
                    start=segment.start,
                    end=segment.end
                )
                self.results.append(utterance)
                print(f"{speaker}: {segment.text}")
            
            # Оставляем 2 секунды контекста для следующего чанка
            buffer = buffer[-16000*2:]
    
    def find_speaker_for_interval(self, diarization, start, end):
        """Находит спикера, который говорил больше всего в интервале"""
        # Упрощенная логика сопоставления
        speaker_times = {}
        for turn, _, speaker in diarization.itertracks(yield_label=True):
            overlap = min(turn.end, end) - max(turn.start, start)
            if overlap > 0:
                speaker_times[speaker] = speaker_times.get(speaker, 0) + overlap
        
        if speaker_times:
            return max(speaker_times, key=speaker_times.get)
        return "UNKNOWN"

Внимание на сопоставление: Этот наивный алгоритм найдет спикера, который говорил больше всего в интервале. Но если два человека говорят одновременно, он выберет того, кто говорил громче/дольше. Для точного разграничения нужна более сложная логика.

Оптимизации, которые реально работают

1. Квантование модели. Если GPU мало памяти (менее 8GB), конвертируйте Whisper в INT8:

model = WhisperModel("medium", device="cuda", compute_type="int8")

Точность упадет на 5-10%, но память сократится в 4 раза.

2. Асинхронная обработка. Не ждите окончания ASR, чтобы начать диарризацию:

import asyncio
import concurrent.futures

async def process_audio_chunk(audio_np):
    loop = asyncio.get_event_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        # Запускаем ASR и диарризацию параллельно
        asr_task = loop.run_in_executor(pool, model.transcribe, audio_np)
        diarization_task = loop.run_in_executor(pool, pipeline, audio_np)
        
        asr_result, diarization_result = await asyncio.gather(asr_task, diarization_task)
        return asr_result, diarization_result

3. Кэширование эмбеддингов. Если у вас много повторяющихся голосов (сотрудники компании), сохраните их voiceprints и сравнивайте с новыми, а не кластеризуйте заново.

Чего ждать в ближайшем будущем

1. Whisper-large-v4 обещают на 2026 год. По слухам, будет лучше работать с шумным аудио и поддерживать streaming из коробки.

2. Qwen3-ASR активно развивается. Если нужна поддержка множества языков в одной модели — посмотрите мой обзор Qwen3-ASR. Для продакшена уже есть готовый Docker-сервис.

3. Аппаратное ускорение на CPU через OpenVINO и ONNX Runtime. Для серверов без GPU это может стать спасением.

💡
Мой прогноз: К концу 2026 локальный ASR станет стандартом для корпоративных приложений. Облачные сервисы останутся только для мобильных приложений и одноразовых задач. Причина не только в конфиденциальности, но и в экономике: однажды обученная модель дешевле, чем вечные API-платежи.

Что делать, если все тормозит

1. Проверьте compute_type. "float32" на GPU — это убийство производительности. Всегда используйте "float16" или "int8".

2. Уменьшайте beam_size. С beam_size=5 точность лучше на 3-5%, но скорость ниже в 3 раза. Для диалогов beam_size=1 часто достаточно.

3. Бач-обработка. Если обрабатываете записи постфактум, подавайте по несколько файлов одновременно:

# Не так
for file in audio_files:
    result = model.transcribe(file)

# А так
from concurrent.futures import ThreadPoolExecutor

def transcribe_file(file):
    return model.transcribe(file)

with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(transcribe_file, audio_files))

4. Используйте GPU с большим объемом памяти. Очевидно? Но многие пытаются запихать large-v3 в 8GB. Не выйдет. Medium требует ~4GB, small — ~2GB.

Финальный совет: когда все-таки стоит использовать облака

Да, я только что рассказывал, как от них уйти. Но есть случаи, когда облачный ASR оправдан:

  • Мобильные приложения. Тащить 2GB модели в апп — самоубийство.
  • Очень редкие языки, которые ваша модель не поддерживает.
  • Пилотные проекты, где нужно проверить гипотезу без инвестиций в железо.
  • Сезонные нагрузки, когда 95% времени система простаивает.

Но как только объемы растут, а требования к конфиденциальности ужесточаются — локальное решение окупается за 6-12 месяцев. И вы получаете полный контроль. Что в 2026 году дороже денег.

Следующий шаг — добавить LLM для суммаризации встреч или собрать голосового ассистента целиком на одной карте. Но это уже другая история.