Як зібрати селфхост войс-агента без жодного платного API

💡 Усі статті, обговорення, новини про AI — в одному місці. Приєднуйтесь до AI спільноти!

Ми з командою зібрали голосового агента без зовнішніх AI API. Розпізнавання мовлення, мовна модель і синтез голосу працюють на одному орендованому сервері. Далі про те, як усе запустили, на чому виграли час і чому вільна пам’ять на відеокарті ще не означає, що можна додати більше розмов.

У наших тестах агент починав відповідати через 0,70–0,85 с після завершення фрази. Мовна модель і синтез разом займали близько 12,4 ГіБ VRAM-пам’яті відеокарти. Тестували англійською з Gemma 4 12B, стандартним голосом CosyVoice і нашим доопрацьованим сервером синтезу. Тож цифри нижче стосуються саме цього набору, а не будь-якої мови чи задачі.

Стаття для AI та backend-розробників, які придивляються до власного голосового стеку. Покажу налаштування, результати й розрахунок витрат.

Чому свій стек і чому каскад

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

Під self-hosted тут маємо на увазі власне розгортання на орендованому сервері, а не власне фізичне залізо. Хостинг-провайдер нікуди не зникає. Оренду платимо навіть тоді, коли дзвінків немає, а фіксованої суми вистачає лише в межах потужності машини.

Ми розглядали три підходи. У гібриді модель отримує звук і повертає текст, який озвучує окремий синтезатор. У speech-to-speech на вході й виході звук. А в каскаді кожен компонент робить свою роботу:

Аудіо → розпізнавання → текст → LLM → синтез → аудіовідповідь

Обрали каскад: так можна окремо замінити розпізнавання, мовну модель або голос. Далі називатиму розпізнавання ASR, мовну модель LLM, а синтез мовлення TTS.

Паралельно детектор вирішує, чи людина вже договорила. Поспішіть, і агент переб’є. Зачекає задовго, і отримаємо незручну паузу. Саме розпізнавання працює ще під час мовлення. LLM теж віддає текст частинами, а TTS починає озвучувати перший придатний фрагмент. Нам важливо, коли людина почує перший звук, а не коли згенерується вся відповідь.

Що працює на сервері

Silero VAD + Smart Turn v3. Silero знаходить голосову активність і паузи. Smart Turn слухає аудіо після паузи та оцінює, чи завершена репліка. Він аналізує саме звук, а не розділові знаки в розпізнаному тексті. Цей механізм описано в документації Pipecat.

Nemotron ASR Streaming 0.6B. Запускаємо int8-версію через sherpa-onnx на CPU: приблизно 1,0 ГБ оперативної пам’яті й жодної VRAM. Подаємо звук чанками, тобто фрагментами, по 1120 мс. Це не означає, що після кожної репліки додається ще 1120 мс тиші: більшу частину звуку модель уже обробила, поки людина говорила.

Gemma 4 12B QAT Q4_0. Цю версію моделі обрали, щоб вона помістилася на GPU поруч із TTS. QAT означає навчання з урахуванням квантизації. Файл ваг займає близько 7 ГБ, але під час роботи модель використовує 8,03 ГіБ VRAM. Тому розмір файлу ще не є відповіддю на питання, скільки пам’яті треба на картці.

CosyVoice 3 0.5B. Використовуємо Fun-CosyVoice3-0.5B-2512: синтез займає 4,32 ГіБ VRAM. Нас цікавило клонування голосу з трисекундного запису, але заміри робили зі стандартним голосом. Серед дев’яти мов у README CosyVoice 3 української немає. Тобто ці результати ще не означають, що ми зібрали готового україномовного агента.

Pipecat. Він з’єднує сервіси, передає кадри й обробляє перебивання. До LLM звертається через локальний OpenAI-сумісний інтерфейс llama-server. Для ASR і TTS ми написали власні сервіси. Просто встановити оригінальні проєкти недостатньо, щоб повторити наші цифри: потрібні ще адаптери й зміни в сервері синтезу.

Як усе запустили й прискорили

Спочатку перевірили саму машину

Сервер із RTX 4090 коштував $0,402/год. У нашому середовищі було 23 процесорні ядра, Ubuntu 22.04, драйвер NVIDIA 580 і Linux 6.8. Дивитися лише на назву відеокарти не варто: ASR працює на CPU, і процесор теж впливає на результат.

Після запуску перевіряли синтез на одній фразі й робили прогрівальний дзвінок. Наш орієнтир — близько 334 мс до першого звуку TTS.

І ще до тестів варто розібратися з мережею. У нашому тарифі внутрішні порти отримували випадкові зовнішні номери. Це ускладнило WebRTC, тому тестували через WebSocket. Йдеться про особливість нашого підключення, а не про те, що WebRTC взагалі не працює через NAT.

LLM: менше повторних обчислень і роздумів

Запускаємо LLM через llama.cpp, зібраний із CUDA:

llama-server \
  -m gemma-4-12B-it-QAT-Q4_0.gguf \
  -ngl 99 -fa on \
  -c 8192 -np 2 \
  --cache-type-k q8_0 --cache-type-v q8_0 \
  --swa-full --cache-reuse 4 \
  --jinja --reasoning-format deepseek

-np 2 — це два слоти для обробки запитів, а не ліміт у дві розмови. Кеш залежить від контексту й того, як використовуються слоти. Тому швидкість одного запиту з уже підготовленим кешем не можна автоматично обіцяти всім сесіям під навантаженням.

Кеш: не рахувати те саме ще раз. У нашому запуску —swa-full —cache-reuse 4 зменшили повторне обчислення токенів на 33%. За це заплатили приблизно 350 МБ пам’яті.

За документацією llama-server, —swa-full вмикає повнорозмірний кеш sliding-window attention, а —cache-reuse 4 задає мінімальний розмір фрагмента для спроби повторно використати кеш. Самі прапорці не обіцяють мінус 33% у будь-якому запуску результат залежить від налаштувань.

Історію розмови ми тільки дописуємо, не переписуємо її початок: інакше доводиться обробляти більше токенів заново. Дивимося на timings.prompt_n — це кількість токенів, які сервер фактично обробив. Слот прогріваємо після підключення. Саме на такому вже «теплому» ході отримали 68 мс до першого токена, а не на холодному старті.

Thinking: роздуми теж коштують часу. У наших тестах модель витрачала на них 250–460 токенів, і перший звук міг з’явитися аж через п’ять секунд. Для голосу ми вимикаємо thinking у кожному запиті до llama-server:

{"chat_template_kwargs": {"enable_thinking": false}}

Це фрагмент запиту до моделі. Не плутайте його з —reasoning-format deepseek у команді сервера: той визначає формат обробки роздумів, але не вимикає їх.

TTS: допрацювали сервер CosyVoice

Початковий запуск CosyVoice був надто повільним для нашого сценарію. Ось що змінили й що отримали в замірах:

Додавати всі ці виграші в одну суму не варто: вони стосуються різних частин роботи. Після скорочення кількості кроків flow з 10 до 6 на слух різниці не помітили, але сліпого порівняння якості тут не наводимо.

Ми також не чекаємо, поки LLM напише все. Щойно з’являється розділовий знак, віддаємо перший фрагмент у TTS. 334 мс до першого звук результат нашого доопрацьованого сервера, а не стандартного запуску CosyVoice.

Паузи: швидше та не завжди краще

Тут підбирали два інтервали VAD/turn: скільки тиші чекати й скільки часу загалом давати на завершення репліки. Почали з 0,5 / 1,2 с. Пара 0,5 / 0,8 с дала близько 110 мс виграшу, а 0,3 / 0,5 с 330 мс.

Для коротких запитань підійшов швидший варіант. Але коли агент раніше вирішує, що людина договорила, треба вміти прийняти продовження її фрази й скасувати вже неактуальну відповідь. Для адрес, номерів і дат із паузами всередині залишаємо 0,5 / 1,2 с. Однієї вдалої пари для всіх розмов немає.

Що показали тести

Коли чути першу відповідь

Від кінця фрази людини до початку відповіді агента в наших замірах минало 0,70–0,85 с. Окремо дивилися на такі показники:

Етапи йдуть один за одним, тому їхня сума і дає ті 0,70–0,85 с. 144 мс рахуються від першого токена, тож 68 мс туди не входять. Час на визначення кінця репліки та мережу до клієнта в цю суму не входить. Діапазон 0,70–0,85 с це не p95 чи p99. Він не означає, що 95% або 99% відповідей уклалися в цей час: такого розподілу затримок тут не наводимо.

Скільки розмов можна вести одночасно

Тест відкривав справжні з’єднання через ту саму точку входу, що й браузер. Кожна сесія програвала записані фрази: чотири ходи з паузами. Для 1, 2, 4 та 8 сесій зафіксували відповідно 4/4, 8/8, 15/16 і 31/32 ходи.

До чотирьох сесій помітної зміни затримки не було. На восьми додалося приблизно 350 мс. На дванадцяти найповільніші відповіді вже починалися через 6 с і більше це весь час очікування, а не ще шість секунд зверху.

Тому вісім сесій наш орієнтир для цього сценарію, а не фізична межа RTX 4090. І «нуль помилок» ми не заявляємо: частину ходів тест не зарахував, а самі лічильники не пояснюють, чому.

Найважливіший момент: уперлися не в пам’ять, а в чергу синтезу. VRAM під навантаженням трималася в межах 12 728–12 748 МіБ, але TTS брав лише один запит за раз. Вільне місце на картці саме по собі цю чергу не прискорює.

Щоб попередньо прикинути кількість сесій, використовували таку формулу:

Час TTS на хід ≈ час до першого звуку + RTF × тривалість відповіді
Сесії ≈ інтервал між ходами однієї сесії / час TTS на хід

RTF показує, скільки секунд обробки потрібно на секунду аудіо. У нас він був близько 0,1, а відповідь тривала 4 с. Отримуємо 0,334 + 0,1 × 4 = 0,734 с на хід. За інтервалу між ходами 6,7–8,6 с виходить приблизно 9–12 сесій.

Формулу ми звірили з навантажувальним тестом на семи машинах: на шести результат розійшовся не більш ніж на 20 %. На RTX 4090 формула дає від 9 сесій, тест показав 8. На дванадцяти сесіях найповільніші відповіді вже перевищували 6 с.

Перебивання перевіряли окремим 17-хвилинним дзвінком. Агент зупиняв свою відповідь, але коротке «ага» не сприймав як повноцінне перебивання. Пам’ять у цьому прогоні залишалася в межах 12 064–12 068 МіБ. Це ще не перевірка безперервної роботи протягом кількох діб.

Коли оренда дешевша за API

Рахуємо оренду за нашою ставкою $0,402/год і умовними 730 годинами на місяць:

0,402 × 730 = $293,46 на місяць

Для API беремо $0,0556/хв із нашого кошторису каскаду Deepgram + Gemini Flash + ElevenLabs. Це ставка, яку ми заклали під свій трафік, а не перевірений тут поточний прайс. Детального розрахунку за токенами, символами й тарифами в матеріалі немає.

Щоб знайти обсяг, за якого суми зрівняються, ділимо місячну оренду на ціну хвилини API:

293,46 / 0,0556 ≈ 5 278 хв/місяць

Після цієї позначки оренда дешевша за цей конкретний API-сценарій. Але це ще не означає, що окупилися розробка й підтримка.

А звідки взялися $0,00084 за хвилину? Із розрахунку, в якому всі вісім сесій зайняті цілодобово:

Теоретичний обсяг = 8 × 60 × 730 = 350 400 хв/місяць
Оренда на хвилину = 293,46 / 350 400 ≈ $0,00084

Місячного тесту, який підтверджує такий обсяг, у матеріалі немає. Це теоретична ціна оренди на хвилину за повного завантаження, а не універсальна собівартість розмови. За 20 000 хвилин вона вже становить $0,0147. Крім загального обсягу, треба дивитися, скільки розмов припадає на пік і яку затримку можна допустити.

У цьому порівнянні немає витрат на розробку, підтримку, моніторинг, зберігання, додаткову мережу, телефонію й резервну машину. Інша ціна API теж змінює картину: за умовних $0,017/хв точка рівності зсувається до 17,3 тисячі хвилин.

Що ще перевірити перед запуском

Чи добре агент виконує задачу. Тут ми не порівнювали з хмарою точність ASR, природність голосу й виконання задач. Швидко заговорити ще не те саме, що правильно зрозуміти людину й допомогти їй. Мову свого продукту теж потрібно перевіряти окремо.

Що відбувається з даними й голосом. Відмова від зовнішніх AI API не робить систему приватною автоматично. Залишаються доступ до сервера, записи, логи, строки їх зберігання та інфраструктура орендодавця. А можливість клонувати голос із короткого запису не замінює дозволу людини на його використання.

Як усе працює під вашим навантаженням. Потрібні довші діалоги, реальні тривалості відповідей, піковий трафік і розбір незарахованих ходів. Лише після цього варто визначати робочу кількість одночасних сесій.

А в підсумку

Повний голосовий каскад без платних AI API помістився на одному сервері: ASR працює на CPU, LLM і TTS на RTX 4090. Але вибрати моделі було недостатньо. Результат: кеш, прогрів, передавання тексту частинами, зміни в TTS і правильна обробка перебивань.

Головний технічний висновок для нас: дивитися треба не лише на VRAM, а й на те, де запити стають у чергу. Головний висновок щодо витрат: низька ціна хвилини без пояснення завантаження мало про що говорить.

Власне розгортання дало нам контроль над компонентами й передбачувану оренду. Чи варто повторювати цей шлях , залежить від вашого трафіку, вимог до даних і витрат на підтримку.

Буду радий обговорити деталі в коментарях і поділитися досвідом інтеграції.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую за цікаву статтю . 👏

Дякуємо, Назаре! 🔥 Тішимось, що ти з нами й що можемо робити такі круті речі разом 🙌 Якщо вам відгукується те, що ми робимо, заглядайте до нас, зараз шукаємо людей у команду 😉