Зачем вообще собирать локальный StS, если есть ChatGPT с голосом?
Потому что облачные решения в 2026 году все еще шпионят за тобой. Каждый твой запрос, каждая пауза, каждый смешок анализируется, кластеризуется и продается. Плюс задержки в 2-3 секунды убивают естественность диалога. А еще – попробуй заставить облачного ассистента говорить с сарказмом или шепотом.
Локальный пайплайн Speech-to-Speech – это полный контроль. Никаких лимитов на запросы, никакой цензуры (хотя тут уже на твоей совести), возможность тонкой настройки каждого компонента. И главное – работает даже когда интернет отвалился.
Важный момент: две RTX 3090 выбраны не просто так. Одна карта будет работать с Whisper и TTS, вторая – с LLM. Так мы добиваемся минимальной задержки, потому что модели не дерутся за видеопамять.
Архитектура, которая не будет тормозить
Самый частый провал при сборке локальных ассистентов – попытка запихнуть все на одну карту. Whisper large-v3 жрет 10 ГБ, Llama 3.2 11B – еще 22 ГБ, а эмоциональный TTS вроде Sonya TTS требует своих 6-8 ГБ. Итог: out of memory после третьей фразы.
Правильная схема выглядит так:
| Компонент | Модель | VRAM | Карта |
|---|---|---|---|
| STT (распознавание) | Whisper large-v3 | ~10 ГБ | RTX 3090 #1 |
| LLM (мозги) | Qwen2.5 7B Instruct | ~14 ГБ (4-bit) | RTX 3090 #2 |
| TTS (синтез) | Sonya TTS v2.1 | ~6 ГБ | RTX 3090 #1 |
| Диалоговый менеджер | FastAPI + Redis | RAM | CPU |
Почему такая конфигурация? Whisper и Sonya TTS отлично уживаются на одной карте – они редко работают одновременно. LLM живет отдельно, потому что она должна быть всегда в памяти для минимальной задержки ответа.
Шаг 1: Ставим Whisper large-v3 с аппаратным ускорением
Faster-Whisper – это must have в 2026 году. Обычный Whisper от OpenAI тормозит даже на 3090, потому что не использует CTranslate2. А нам нужна задержка меньше 500 мс на распознавание.
# Не делай так (это медленно):
pip install openai-whisper
# Делай так:
pip install faster-whisper
pip install ctranslate2 --extra-index-url https://pypi.nvidia.com
Ключевой момент – сборка CTranslate2 с поддержкой CUDA 12.4 (актуально на февраль 2026). Без этого ускорения не будет.
# Правильная инициализация Whisper на конкретной карте
import torch
from faster_whisper import WhisperModel
# Явно указываем первую карту для STT
torch.cuda.set_device(0)
model = WhisperModel(
"large-v3",
device="cuda",
compute_type="float16", # Не используй int8 для качества
download_root="./models/whisper"
)
# Функция распознавания с VAD (Voice Activity Detection)
def transcribe_audio(audio_path):
segments, info = model.transcribe(
audio_path,
beam_size=5,
vad_filter=True, # Это критично для диалога
vad_parameters=dict(
threshold=0.5,
min_speech_duration_ms=250,
min_silence_duration_ms=200
)
)
return " ".join([segment.text for segment in segments])
Шаг 2: LLM, которая понимает контекст диалога
Llama 3.2 11B – хороша, но для диалога в 2026 году лучше подходит Qwen2.5 7B Instruct. У нее встроенная поддержка system prompt для управления стилем ответов, плюс она отлично квантуется до 4-bit без потери качества.
Ставить будем через vLLM-Omni – это единственный фреймворк, который нормально работает с распределением моделей по нескольким GPU. Обычный vLLM или Ollama с этим справляются плохо.
# Устанавливаем vLLM-Omni с поддержкой Windows (да, в 2026 это работает)
pip install vllm-omni
pip install flash-attn --no-build-isolation # Для ускорения внимания
from vllm_omni import LLM, SamplingParams
import torch
# Вторая карта для LLM
torch.cuda.set_device(1)
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
quantization="awq", # AWQ вместо GPTQ – меньше потерь
gpu_memory_utilization=0.85, # Оставляем место для кеша
max_model_len=8192, # Длинный контекст для истории диалога
tensor_parallel_size=1 # На одной карте
)
# System prompt, который задает характер ассистента
system_prompt = """Ты – голосовой ассистент Алиса. Отвечай кратко, естественно, как в живом диалоге.
Используй разговорные конструкции. Не будь слишком формальной.
Максимальная длина ответа – 2 предложения."""
def generate_response(user_input, dialog_history):
# Форматируем историю диалога
messages = [
{"role": "system", "content": system_prompt},
*dialog_history[-6:], # Берем последние 6 реплик
{"role": "user", "content": user_input}
]
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=150, # Ограничиваем длину ответа
stop=["\n\n", "Алиса:", "Пользователь:"] # Стоп-токены
)
outputs = llm.generate(messages, sampling_params)
return outputs[0].outputs[0].text.strip()
Шаг 3: Эмоциональный TTS, который не звучит как робот
Вот здесь большинство спотыкаются. Берут какую-нибудь XTTS v2 и удивляются, почему голос плоский как доска. Проблема в том, что стандартные TTS не умеют передавать эмоции из текста.
Решение – Sonya TTS v2.1 с поддержкой эмоциональных меток. Модель понимает маркеры вроде [happy], [sarcastic], [whisper] и меняет интонацию соответственно.
# Установка Sonya TTS (требуется PyTorch 2.3+)
git clone https://github.com/sonya-tts/sonya-tts-v2
cd sonya-tts-v2
pip install -e .
# Загружаем эмоциональную модель
python download_models.py --model emotional-v2 --voice russian-female-1
import torch
from sonya_tts import SonyaTTS
# Возвращаемся на первую карту
torch.cuda.set_device(0)
tts = SonyaTTS(
model_path="./models/sonya/emotional-v2",
voice="russian-female-1",
device="cuda:0"
)
def detect_emotion(text):
"""Примитивный детектор эмоций по ключевым словам"""
text_lower = text.lower()
if any(word in text_lower for word in ["отлично", "рад", "супер", "здорово"]):
return "[happy]"
elif any(word in text_lower for word in ["грустно", "жаль", "печально"]):
return "[sad]"
elif "?" in text:
return "[question]"
else:
return "[neutral]"
def text_to_speech_with_emotion(text):
emotion = detect_emotion(text)
# Добавляем эмоциональную метку в начало текста
emotional_text = f"{emotion} {text}"
audio = tts.generate(
emotional_text,
speed=1.0,
temperature=0.3 # Чем выше, тем более "творческий" голос
)
return audio
Альтернатива – Qwen3 TTS в vLLM-Omni, если нужна поддержка multiple voices on the fly. Но Sonya лучше справляется с эмоциями.
Шаг 4: Диалоговый менеджер – мозг пайплайна
Самый недооцененный компонент. Без нормального диалогового менеджера получится просто цепочка: распознал → ответил → синтезировал. Нужно управление состоянием, обработка прерываний, контроль длительности пауз.
from fastapi import FastAPI, WebSocket
import asyncio
import json
import redis
from collections import deque
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, db=0)
class DialogManager:
def __init__(self, max_history=10):
self.history = deque(maxlen=max_history)
self.is_listening = True
self.current_speaker = None
def add_to_history(self, role, text):
self.history.append({"role": role, "content": text})
# Сохраняем в Redis для persistence
r.rpush("dialog_history", json.dumps({"role": role, "content": text}))
def get_history(self):
return list(self.history)
def handle_interruption(self, new_audio):
"""Если пользователь начал говорить, пока ассистент отвечает"""
if not self.is_listening:
self.is_listening = True
# Прерываем текущий TTS
self.current_speaker.stop()
return True
return False
manager = DialogManager()
@app.websocket("/ws/assistant")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
# 1. Получаем аудио от клиента
audio_data = await websocket.receive_bytes()
# 2. Сохраняем во временный файл для Whisper
with open("temp_audio.wav", "wb") as f:
f.write(audio_data)
# 3. Распознаем речь
user_text = transcribe_audio("temp_audio.wav")
manager.add_to_history("user", user_text)
# 4. Генерируем ответ через LLM
history = manager.get_history()
assistant_text = generate_response(user_text, history)
manager.add_to_history("assistant", assistant_text)
# 5. Синтез речи с эмоциями
audio_output = text_to_speech_with_emotion(assistant_text)
# 6. Отправляем обратно
await websocket.send_bytes(audio_output.tobytes())
Шаг 5: Собираем все вместе и оптимизируем
Теперь самая сложная часть – заставить это работать с низкой задержкой. Проблема в том, что каждая модель загружается отдельно, и между вызовами есть overhead.
Решение: запускаем каждую модель в отдельном процессе с привязкой к конкретному GPU, а общение между ними через очереди.
# pipeline_orchestrator.py
import multiprocessing as mp
from queue import Queue
import torch
def stt_worker(input_queue, output_queue, gpu_id):
torch.cuda.set_device(gpu_id)
# Инициализируем Whisper здесь
while True:
audio_path = input_queue.get()
text = transcribe_audio(audio_path)
output_queue.put(("stt_result", text))
def llm_worker(input_queue, output_queue, gpu_id):
torch.cuda.set_device(gpu_id)
# Инициализируем LLM здесь
while True:
text = input_queue.get()
response = generate_response(text)
output_queue.put(("llm_result", response))
def tts_worker(input_queue, output_queue, gpu_id):
torch.cuda.set_device(gpu_id)
# Инициализируем TTS здесь
while True:
text = input_queue.get()
audio = text_to_speech_with_emotion(text)
output_queue.put(("tts_result", audio))
# Запускаем три воркера на разных GPU
stt_queue = Queue()
llm_queue = Queue()
tts_queue = Queue()
result_queue = Queue()
mp.Process(target=stt_worker, args=(stt_queue, llm_queue, 0)).start()
mp.Process(target=llm_worker, args=(llm_queue, tts_queue, 1)).start()
mp.Process(target=tts_worker, args=(tts_queue, result_queue, 0)).start()
Внимание: multiprocessing с CUDA – это боль. Убедись, что каждая модель инициализируется ВНУТРИ своего процесса, иначе получишь CUDA context errors.
Где все ломается: самые частые ошибки
- CUDA out of memory после часа работы – это memory leak. Проверь, что ты удаляешь промежуточные тензоры с помощью
torch.cuda.empty_cache()после каждого запроса. - Задержка растет с каждым запросом – история диалога накапливается в LLM prompt. Ограничивай историю 6-8 репликами или используй summary technique.
- TTS говорит монотонно, несмотря на эмоциональные метки – скорее всего, модель не дообучена на русских эмоциях. Попробуй дообучить Sonya TTS на своих данных.
- Whisper путает слова в шумной комнате – добавь предварительную обработку аудио через noise reduction (RNNoise) и увеличивай
vad_thresholdдо 0.7.
А если хочется еще быстрее?
На двух RTX 3090 можно добиться задержки 1.2-1.5 секунды от речи до речи. Но если нужно меньше секунды:
- Замени Whisper large-v3 на distil-large-v3 – в 2 раза быстрее с минимальной потерей точности.
- Используй Pocket TTS вместо Sonya для действительно легковесного синтеза. Качество ниже, но скорость – 50 мс на генерацию.
- Квантуй LLM до 3-bit с помощью AWQ вместо 4-bit – еще 30% экономии памяти.
- Используй Continuous batching в vLLM-Omni для обработки нескольких запросов параллельно.
Что в итоге получится
Рабочий голосовой ассистент, который:
- Не отправляет твои данные в облака
- Говорит с эмоциями (можно даже научить сарказму)
- Работает с задержкой 1.5-2 секунды
- Понимает контекст разговора
- Может быть интегрирован в домашнюю автоматизацию
Самый неочевидный совет в конце: запиши 10-15 минут своих разговоров и дообучи TTS на этих данных. Даже часовая тонкая настройка сделает голос в разы естественнее. Модель научится твоим интонациям, любимым словечкам, паузам перед сложными мыслями.
И последнее – не пытайся сделать идеально с первого раза. Собери минимально рабочий пайплайн, заставь его просто работать, а потом уже оптимизируй задержки и качество. Потому что перфекционизм – главный враг локальных AI проектов.