Я делаю голосового ИИ-приёмщика для малого бизнеса — назвал её Айкой. Клиент звонит (пока — говорит через демо на сайте), Айка отвечает голосом: рассказывает про услуги, цены, записывает на разбор. Собрана на российском стеке: распознавание и синтез — Yandex SpeechKit, «мозг» — Claude. Классическая трёхзвенка.

6 июля OpenAI выпустил gpt-realtime — обновление своего движка «речь в речь» (speech-to-speech). Лента запестрела: «говорит как человек», «можно перебивать», «латентность упала». Я занервничал: неужели мой проверенный стек уже вчерашний день?

Гадать по чужим демкам не хотелось. За вечер я собрал прототип на OpenAI, откопал, что у Яндекса есть свой Realtime, замерил задержку у всех трёх подходов — и по итогу переписал прод. Ниже — как собирал, на какие грабли наступил, что показали замеры и почему для российского прода вывод оказался не в пользу хайпового OpenAI.

Speech-to-speech против трёхзвенной цепочки

Классический голосовой бот — три отдельных сервиса в цепочке:

  1. STT (speech-to-text) — распознаёт речь в текст.

  2. LLM — думает над текстом, генерирует ответ.

  3. TTS (text-to-speech) — озвучивает ответ.

Каждое звено добавляет свою латентность и теряет просодию: STT выкидывает интонацию, TTS генерирует её заново с нуля. Три сетевых round-trip, три обработки. Моя Айка устроена именно так.

Realtime схлопывает три звена в одну модель: она принимает аудио и выдаёт аудио напрямую, сохраняя интонацию и умея реагировать на перебивание прямо в процессе генерации. Отсюда и обещанная «живость».

Прототип на OpenAI: WebRTC, ephemeral-токен и один неприятный грабль

Realtime у OpenAI работает поверх WebRTC (для браузера) или WebSocket. Взял WebRTC. Схема:

  1. Бэкенд создаёт ephemeral-токен (короткоживущий ключ), чтобы не светить основной API-ключ в браузере.

  2. Браузер поднимает RTCPeerConnection, добавляет дорожку с микрофона, заводит data-channel для событий.

  3. Обменивается SDP-оффером с OpenAI, получает ответ, играет входящий аудиопоток.

Грабль, на котором я потерял время. Браузерный обмен SDP по старой памяти отправил на https://api.openai.com/v1/realtime?model=.... В ответ — 400:

The Realtime Beta API is no longer supported. Please use /v1/realtime for the GA API.

Beta-форму выпилили. Актуальный GA-эндпоинт для WebRTC-хендшейка:

const resp = await fetch("https://api.openai.com/v1/realtime/calls", {
  method: "POST",
  body: offer.sdp,
  headers: {
    Authorization: `Bearer ${EPHEMERAL_KEY}`,
    "Content-Type": "application/sdp",
  },
});
const answer = { type: "answer", sdp: await resp.text() };
await pc.setRemoteDescription(answer);

Модель задаётся в конфиге сессии при создании токена, в URL хендшейка её уже не нужно.

Прототип заработал, и первое впечатление — вау: отвечает почти без пауз, держит перебивание, звучит по-человечески. Но дальше начались проблемы, которые для российского прода оказались фатальными.

Русский у OpenAI: род чинится, акцент — нет

Первое, что услышал: голос отвечает бодро, но с заметным английским акцентом на русском. Голоса Realtime (marin, cedar и другие) натренированы в первую очередь на английском, русский для них вторичен.

Второе: женский голос, которому я дал женское имя, говорил о себе в мужском роде — «не понял», «готов». В английском рода нет, модель его не согласует.

Род я вылечил промптом за минуту — жёстко прописал женский род и примеры. А вот акцент промптом не лечится. Инструкции «говори как носитель, без акцента» интонацию чуть ровняют, но фонетика живёт в аудио-движке модели, куда системный промпт не достаёт. Fine-tune голоса Realtime не даёт — это архитектурное ограничение.

Плюс два барьера, не зависящих от кода: OpenAI в РФ заблокирован (клиенту без VPN живой агент просто не подключится), и оплата из России — отдельный квест. Для демо под VPN — терпимо. Для прода, где на том конце реальный клиент российского бизнеса, — стоп.

Поворот: а у Яндекса-то есть свой Realtime

Пока я разбирался с акцентом, всплыл факт, который сначала прошёл мимо: Яндекс ещё в сентябре 2025 запустил собственный Realtime API на платформе AI Studio. Тот же speech-to-speech, одна модель на вход-выход аудио, server-side VAD, function calling — и заявленная задержка «до секунды». Только русский для него родной, работает из РФ без VPN, оплата рублями.

То есть у меня под носом был движок, который снимает все три барьера OpenAI разом. Надо было щупать.

Грабли доступа к Яндексу (честный дневник)

Тут началось самое весёлое. Реквизиты вроде есть (сервисный аккаунт остался с экспериментов по SpeechKit), а Realtime не пускает.

Первое. WebSocket-соединение поднимается, но на session.update прилетает Permission denied. Причина: у сервисного аккаунта были только роли SpeechKit (ai.speechkit-*), а Realtime — это ещё и языковая модель. Добавил ai.languageModels.user — YandexGPT ожил, но Realtime всё равно ругается, теперь конкретнее:

Runtime error: PERMISSION_DENIED: Permission denied on view model in folder ...

Не хватало права видеть саму realtime-модель. Решается агрегированной ролью ai.models.user (доступ ко всем моделям AI Studio). Итого realtime-агенту нужны сразу: речь (ai.speechkit-stt.user + ai.speechkit-tts.user), мозг (ai.languageModels.user) и доступ к самой модели (ai.models.user).

Второе, неожиданное. Роли я хотел выдать через yc CLI автоматом — не вышло:

OAuth token for user ..., issued after '2026-06-01', is not supported for IAM token exchange

Яндекс с 1 июня 2026 закрыл обмен пользовательских OAuth-токенов на IAM-токен. То есть привычный автоматический путь «получил OAuth → обменял на IAM → раздал роли CLI» больше не работает — роли пришлось назначать руками в консоли. Мелочь, но время съело, и на неё стоит закладываться, если пишете автоматизацию.

Третье. Старый API-ключ был создан со scope-ограничением под SpeechKit — к foundation-моделям он не пускал ни с какими ролями. Пришлось создать новый ключ без ограничения области.

Когда все три роли на месте и ключ свежий — сессия наконец поднялась.

Что показали замеры

Метод одинаковый для всех трёх: синтезирую вопрос через SpeechKit, скармливаю движку и засекаю время от конца речи до первого звука ответа. По несколько разных вопросов на каждый движок, не один прогон.

Подход

Задержка ответа

Условия замера

Яндекс Realtime

~330 мс (279–373)

прямое подключение из РФ

OpenAI Realtime

~740 мс (603–855)

из РФ под VPN (иначе никак)

Наша трёхзвенка (SpeechKit + Claude)

~4,4 с (3,7–5,7)

прямое подключение из РФ

Сразу честная оговорка, без неё числа врут: замер OpenAI сделан под VPN, потому что напрямую из России он не работает вообще. Сетевой крюк за границу неизбежен для российского пользователя — часть его 740 мс это именно дорога до заграничного сервера и обратно. Без VPN OpenAI был бы ещё быстрее, но такого сценария у российского клиента просто нет, так что мерить честнее именно так, как он реально доступен.

Отдельно отмечу: я сам ждал у OpenAI куда большей задержки — на слух в живом разговоре казалось «секунды две». Замер это опроверг: 740 мс. Ухо в разговоре считает вместе с собственными паузами и обдумыванием — верить надо секундомеру, а не ощущению.

Вывод по скорости: оба speech-to-speech движка на порядок быстрее трёхзвенки. Яндекс ещё и вдвое шустрее OpenAI — частью потому, что работает напрямую, без заграничного крюка. Трёхзвенка честно суммирует латентность трёх сетевых вызовов подряд — оттого 4-5 секунд, которые в живом диалоге ощущаются как «бот завис».

Миграция прода за вечер

Убедившись, что Яндекс Realtime и быстрый, и по-русски, и в РФ, я решил не оставлять это экспериментом, а переписать Айку. Вот что всплыло по дороге.

Из браузера напрямую к Яндексу нельзя. У OpenAI был ephemeral-токен для браузера — у Яндекса такого нет, а браузерный WebSocket не умеет ставить заголовок Authorization: Api-Key. Значит нужен backend-прокси: браузер ↔ мой сервер ↔ Яндекс. Сервер держит ключ и перекачивает аудио. У меня как раз был бэкенд от трёхзвенки (FastAPI, веб-сокет для микрофона) — в него я добавил новый эндпоинт-мост рядом со старым, не ломая рабочую версию:

@app.websocket("/voice/stream-rt")
async def voice_stream_rt(ws: WebSocket):
    async with websockets.connect(
        RT_URL, additional_headers={"Authorization": f"Api-Key {YC_API_KEY}"}
    ) as yc:
        await yc.send(json.dumps(session_cfg))   # промпт, голос alena, server_vad, tools
        await yc.send(json.dumps({"type": "response.create"}))  # приветствие
        # две задачи: браузер→Яндекс (аудио) и Яндекс→браузер (аудио+события)
        await asyncio.gather(browser_to_yc(), yc_to_browser())

Схема событий у Яндекса почти один в один с OpenAI: input_audio_buffer.append на вход, response.output_audio.delta на выход, input_audio_buffer.speech_started для перебивания, function calling через response.output_item.done. Кто делал под OpenAI Realtime — тут будет как дома.

Живой звук вместо MP3. Старый фронт играл ответ MP3-кусками через <audio>. Realtime отдаёт сырой PCM, и перекодировать его в MP3 на лету — значит вернуть ту задержку, которую мы только что убрали. Поэтому фронт теперь играет PCM напрямую через Web Audio с бесшовным планированием:

function pushPCM(int16) {
  const f32 = Float32Array.from(int16, s => s / 32768);
  const buf = ctx.createBuffer(1, f32.length, OUT_RATE);
  buf.getChannelData(0).set(f32);
  const src = ctx.createBufferSource();
  src.buffer = buf; src.connect(ctx.destination);
  if (nextTime < ctx.currentTime + 0.06) nextTime = ctx.currentTime + 0.12; // джиттер-буфер
  src.start(nextTime);
  nextTime += buf.duration;
}

Промпт под голос, а не под JSON. Мой прод-промпт был написан под трёхзвенку и заставлял LLM возвращать JSON (reply, intent, booking). Realtime так не работает — он говорит естественно и дёргает функции. Если скормить ему инструкцию «возвращай JSON», модель начнёт зачитывать фигурные скобки вслух. Пришлось отрезать весь блок формата ответа, оставив только роль, факты, цены и жёсткое «не выдумывай». Богатый промпт заодно вылечил галлюцинации, которыми грешила первая, компактная версия инструкции.

Перебивание без наушников. Speech-to-speech держит перебивание нативно, но есть подвох: если микрофон открыт всё время, агент через колонки слышит собственный синтез и перебивает сам себя. Классическое решение — наушники. Оказалось, браузерный echoCancellation: true вытягивает и без них: в реальном тесте на ноутбучных колонках агент чужую речь ловил, а своё эхо — нет. Держим микрофон открытым всегда, полагаемся на эхоподавление браузера.

К концу вечера демо на сайте отвечало голосом Alena за доли секунды, держало перебивание и перестало сочинять несуществующие услуги. Старую трёхзвенку оставил в коде отдельным эндпоинтом — на случай отката.

Вывод

Не «кто победил вообще» — по чистой человечности голоса OpenAI Realtime прекрасен, спорить глупо. Но для российского прода выбор оказался однозначным:

  • OpenAI Realtime — живой, но с акцентом в русском, заблокирован в РФ и с квестом по оплате. Для клиента, чьи пользователи сидят без VPN, он просто не подключится.

  • Яндекс Realtime — тот же скачок «речь в речь», но родной русский без акцента, работает из РФ напрямую (и оттого быстрее в реальных условиях), оплата рублями, аудио по тарифу порядка пары рублей за минуту. Ценой — возни с доступами и того, что из браузера нужен прокси-слой.

  • Старая трёхзвенка — надёжно и предсказуемо, но четыре-пять секунд задержки в живом разговоре ощущаются как «бот завис», и перебивание рвёт цепочку.

Поэтому свою Айку я перевёл на Яндекс Realtime. Не ради хайпа — ради того, что в реальном звонке российского клиента она теперь отвечает за доли секунды, чистым русским, и её можно перебить на полуслове.

Голос Яндекс Realtime — героя этой статьи — можно послушать вживую прямо сейчас: демо Айки на pervyyii.ru/aika (это ровно тот агент, миграцию которого я описал выше). А записи голоса OpenAI Realtime и старой трёхзвенки на одной фразе — чтобы сравнить акцент и задержку ушами — плюс исходники прототипов выложил в блоге: pervyyii.ru/blog/realtime-vs-aika/.