Speech-to-Speech пайплайн: локальный ассистент на STT, LLM и TTS | 2026 | AiManual
AiManual Logo Ai / Manual.
19 Фев 2026 Гайд

Голосовой ассистент без облаков: собираем полный StS-пайплайн на двух RTX 3090

Пошаговое руководство по сборке автономного голосового ассистента с эмоциональным TTS на базе двух RTX 3090. STT, LLM и синтез речи полностью локально.

Зачем вообще собирать локальный 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])
💡
VAD параметры нужно подбирать под микрофон и акустику комнаты. Слишком низкий threshold – система будет реагировать на шум холодильника. Слишком высокий – пропустит тихие слова.

Шаг 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 секунды от речи до речи. Но если нужно меньше секунды:

  1. Замени Whisper large-v3 на distil-large-v3 – в 2 раза быстрее с минимальной потерей точности.
  2. Используй Pocket TTS вместо Sonya для действительно легковесного синтеза. Качество ниже, но скорость – 50 мс на генерацию.
  3. Квантуй LLM до 3-bit с помощью AWQ вместо 4-bit – еще 30% экономии памяти.
  4. Используй Continuous batching в vLLM-Omni для обработки нескольких запросов параллельно.

Что в итоге получится

Рабочий голосовой ассистент, который:

  • Не отправляет твои данные в облака
  • Говорит с эмоциями (можно даже научить сарказму)
  • Работает с задержкой 1.5-2 секунды
  • Понимает контекст разговора
  • Может быть интегрирован в домашнюю автоматизацию

Самый неочевидный совет в конце: запиши 10-15 минут своих разговоров и дообучи TTS на этих данных. Даже часовая тонкая настройка сделает голос в разы естественнее. Модель научится твоим интонациям, любимым словечкам, паузам перед сложными мыслями.

И последнее – не пытайся сделать идеально с первого раза. Собери минимально рабочий пайплайн, заставь его просто работать, а потом уже оптимизируй задержки и качество. Потому что перфекционизм – главный враг локальных AI проектов.