Зачем локальный STT

Whisper от OpenAI — локальная нейросеть для распознавания речи. Работает без интернета, без API-ключей, поддерживает русский язык с достойной точностью. Для голосовых заметок, транскрипции записей и голосового управления — недооценённый инструмент.

Облачные альтернативы (Google STT, Yandex SpeechKit) дают чуть лучшее качество на русском, но требуют передачи данных на сторонние серверы и оплаты. Для чувствительных данных или высокого объёма — локальный Whisper выигрывает.

Модели: выбор под задачу

Whisper поставляется в нескольких размерах. Для CPU выбор критичен:

Модель      Размер   Скорость (CPU)   Точность
tiny        39 MB    ~32x             базовая
base        74 MB    ~16x             приемлемая
small       244 MB   ~6x              хорошая
medium      769 MB   ~2x              очень хорошая
large-v3    1550 MB  ~1x              максимальная

«32x» означает — 1 минута аудио обрабатывается за ~2 секунды. На GPU все модели работают значительно быстрее. Для голосовых заметок (короткие фразы, бытовая лексика) хватает small. Для технических записей, интервью или транскрипции встреч — medium.

Установка

pip install openai-whisper

# Для ускорения на CPU (рекомендуется)
pip install faster-whisper

# Зависимость: ffmpeg
# Ubuntu:
apt install -y ffmpeg
# macOS:
brew install ffmpeg

Базовое использование

import whisper

model = whisper.load_model("medium")

# Транскрипция файла
result = model.transcribe("audio.mp3", language="ru")
print(result["text"])

# Транскрипция с временными метками
result = model.transcribe("audio.mp3", language="ru", word_timestamps=True)
for segment in result["segments"]:
    start = segment["start"]
    end = segment["end"]
    text = segment["text"]
    print(f"[{start:.1f}s - {end:.1f}s] {text}")

Чанкинг для параллельной обработки

Проблема: длинные аудио (30+ минут) обрабатываются медленно последовательно. Решение — разбить на куски по 15 секунд и обработать параллельно. Важный нюанс: чанки нужно делать с перекрытием, чтобы не терять слова на стыке.

import whisper
import numpy as np
from concurrent.futures import ThreadPoolExecutor, as_completed
from pydub import AudioSegment

def load_audio_chunks(path: str, chunk_sec: int = 15, overlap_sec: int = 1):
    """Загружает аудио и нарезает на куски с перекрытием."""
    audio = AudioSegment.from_file(path)
    sr = 16000
    audio = audio.set_frame_rate(sr).set_channels(1)

    samples = np.frombuffer(audio.raw_data, dtype=np.int16).astype(np.float32) / 32768.0

    chunk_len = chunk_sec * sr
    overlap_len = overlap_sec * sr
    step = chunk_len - overlap_len

    chunks = []
    start = 0
    while start < len(samples):
        end = min(start + chunk_len, len(samples))
        chunks.append((start / sr, samples[start:end]))
        start += step

    return chunks

def transcribe_chunk(model, chunk_audio: np.ndarray, start_time: float):
    """Транскрибирует один чанк."""
    result = model.transcribe(chunk_audio, language="ru", fp16=False)
    return start_time, result["text"].strip()

def transcribe_parallel(path: str, model_name: str = "medium", workers: int = 4):
    """Параллельная транскрипция с чанкингом."""
    model = whisper.load_model(model_name)
    chunks = load_audio_chunks(path, chunk_sec=15, overlap_sec=1)

    results = {}
    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {
            executor.submit(transcribe_chunk, model, audio, ts): ts
            for ts, audio in chunks
        }
        for future in as_completed(futures):
            ts, text = future.result()
            if text:
                results[ts] = text

    # Собираем в порядке временных меток
    transcript = " ".join(results[ts] for ts in sorted(results))
    return transcript

if __name__ == "__main__":
    text = transcribe_parallel("meeting.mp3", model_name="medium")
    print(text)

faster-whisper: ускорение без GPU

faster-whisper использует CTranslate2 для оптимизации — на CPU работает в 4x быстрее оригинального Whisper при том же качестве. Приоритетный выбор для CPU-серверов.

from faster_whisper import WhisperModel

# Загрузка модели (первый раз скачает)
model = WhisperModel("medium", device="cpu", compute_type="int8")

# Транскрипция
segments, info = model.transcribe("audio.mp3", language="ru")
print(f"Язык: {info.language}, вероятность: {info.language_probability:.2f}")

full_text = ""
for segment in segments:
    print(f"[{segment.start:.1f}s] {segment.text}")
    full_text += segment.text + " "

print("\n--- Полный текст ---")
print(full_text.strip())

Интеграция с Telegram-ботом

from aiogram import Router, F
from aiogram.types import Message, Voice
import whisper
import tempfile
import os

router = Router()
model = whisper.load_model("small")  # загружаем один раз

@router.message(F.voice)
async def handle_voice(message: Message, bot):
    # Скачать голосовое
    voice: Voice = message.voice
    file = await bot.get_file(voice.file_id)

    with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
        await bot.download_file(file.file_path, tmp.name)
        tmp_path = tmp.name

    try:
        result = model.transcribe(tmp_path, language="ru")
        text = result["text"].strip()
        await message.reply(f"📝 {text}")
    finally:
        os.unlink(tmp_path)

Практические применения

# Экспорт субтитров в SRT
result = model.transcribe("video.mp4", language="ru", word_timestamps=True)

with open("subtitles.srt", "w", encoding="utf-8") as f:
    for i, segment in enumerate(result["segments"], 1):
        start = segment["start"]
        end = segment["end"]
        text = segment["text"].strip()

        def fmt(t):
            h = int(t // 3600)
            m = int((t % 3600) // 60)
            s = int(t % 60)
            ms = int((t % 1) * 1000)
            return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

        f.write(f"{i}\n{fmt(start)} --> {fmt(end)}\n{text}\n\n")