PunchFadahunsi donates transformer to Osun community, seeks support for TinubuThe Jerusalem PostIran-backed militias trample US flag, exposing Iraq’s sovereignty problem - analysisESPN15 predictions for this weekend's games: Upsets, sleepers, TD scorers and moreInquirerWoman killed, 2 injured in Pangasinan tricycle crashESPN Deportes¿Cómo le fue a Checo Pérez en el GP de Bahréin?Anadolu AjansıBursa'da çıkan orman yangınına müdahale ediliyorWirtualna PolskaŻona zmarłego generała rusza do wyborów. Wszystko przez decyzję jego brataABC NewsUS Marine arrested on suspicion of murder after 'brutal and heinous' death in JapanRFI 中文拉脱维亚议会选举:尽管民粹和主权势力崛起,亲欧洲和乌克兰政党获胜Rai NewsOperaio morto a Caivano, la moglie: “Voglio giustizia”. Lavorava su un tettoFootball ItaliaBove reveals precious Calafiori advice for an Italian in LondonkickerDortmunds Außenverteidiger: Säule und Sorgen
The Daily Newsstand · Free, Always
Sunday, October 4, 2026

Как я писал расшифровщик записей, но в итоге получил бота для Яндекс.Телемост, с функциями которые даже не снились

Translate

Всем привет. Меня зовут Алексей, пишу тут впервые. Работаю в небольшом рекламном агентстве, в основном backend и всякое смежное. По вечерам реверс‑инжиниринг, чисто для удовольствия. Стек у меня был максимально предсказуемый. PHP, немного JS с TS, Python. Скучно. Захотелось нового, и я полез в Go.

Пару лет назад, когда ElevenLabs только появился, я на коленке собрал питоновский скрипт: он гонял записи моих созвонов через их API и отдавал текст. Поигрался неделю и забросил, как это обычно и бывает. А летом 2026-го решил, что пора собрать нормальное портфолио и довести пет‑проекты до чего‑то, что не стыдно показать. Вот и вернулся к тому расшифровщику.

Спойлер: расшифровщик я довёл. А потом он как‑то сам, на чистом энтузиазме, оброс WebRTC‑ботом, диаризацией, нейросетевыми заметками, Телеграм‑ботом, MCP‑сервером и самым прикольным, это подключение к видеовстрече по средству звонком на специальный номер и вводом кода, ибо опыт с IP телефонией есть, и принцип ее работы тоже понятен.

Начало начал

Взять аудио, скинуть в ElevenLabs Scribe, получить текст с разбивкой по спикерам. Все вроде довольно было просто. Так что, первая версия, черновик был сделан буквально за пару вечеров. Файл прилетает через multipart, падает в очередь, воркер дёргает API, результат складывается в Postgres. Фронт на React, бэк на chi и pgx. Ничего сложного, не?
Ну не совсем, так как я в основном занимаюсь беком, во фронт не лезу, пришлось через AI делать интерфейс, хорошо что опыт в этом есть и ранее делал небольшую библиотеку компонентов, которая до боли похожа на OpenWebUI. Она и легла в основу и был построен довольно простой интерфейс и очень удобный.

Интерфейс

Интерфейс

Так идем далее, через время пришлось прикручивать авторизацию, и я не хотел сильно этим заниматься, по этому поступил по самому тупому сценарию, это проверка на наличия пользователя по корп. почте и выдача JWT. Конечно, ранее там был баг, связанные с timeout, что он мог до 5 минут проверять есть ли пользователь, но сейчас у меня 15 секунд и все, пока!

func VerifyIMAPCredentials(host, port, email, password string) bool {
    dialer := &net.Dialer{Timeout: 10  time.Second}
    conn, err := client.DialWithDialerTLS(dialer, host+":"+port,
        &tls.Config{ServerName: host})
    if err != nil {
        return false
    }
    defer conn.Logout()
    conn.Timeout = 15  time.Second
    return conn.Login(email, password) == nil
}

И все это работало долгое время, пока не клюнул петух в одно место и мне захотелось большего

Переломный момент, или как пришлось разбираться с работой Яндекс.Телемоста

Я с коллегами часто созваниваемся в Яндекс.Телемосте. И мне уже надоели костыли с OBS либо со скачиванием записи разговора, и последующей его конвертации в wav. И в этот момент, я задумался, о том что есть же решения по обходу белых списков с помощью SFU телемоста, так значит есть вероятность закинуть бота в созвон.

Телемост внутри это WebRTC. Медиа идёт по SRTP, а вся логика (кто в комнате, какие дорожки активны, ICE‑кандидаты, когда сервер переезжает) гоняется по WebSocket поверх своего JSON‑протокола. Описания нигде нет. Так что дальше пошли вечера с DevTools, разбор бандла и чтение WS‑трафика по строчкам, как книжку.

Документ, который я написал о его работе я тут выкладывать не буду. Но скажу примерно, как сейчас все работает: бот прикидывается веб‑клиентом. Ходит по тому же фронтовому API, поднимает RTCPeerConnection, шлёт и принимает сигналы.

Вот тут пошел первый баг. Бот заходит, обменивается рукопожатиями с сервером, и ровно через пять секунд соединение падает. Каждый раз. Пять секунд, как по будильнику. Я сидел в логах, в дампах час и тупил, пока не дошло. Я генерил instance-id заново на каждый запрос. Сервер цепляет сессию именно по нему. Видит «новый» id, не узнаёт посетителя, рвёт коннект. Решение было простое, я зафиксировал id на всю сессию, и всё.

client := &telemost.Client{
    HTTP:       http.DefaultClient,
    Cookie:     cookie,
    AppVersion: "3.2.0",
    InstanceID: uuid.NewString(), // ОДИН на всю сессию, не на запрос!
}

Дальше проще. Бот подписывается на входящие дорожки, в pion это один колбэк:

subPC.OnTrack(func(track webrtc.TrackRemote, r webrtc.RTPReceiver) {
    switch {
    case strings.EqualFold(track.Codec().MimeType, webrtc.MimeTypeOpus):
        s.recordTrack(track, mid)       // голос участника в .ogg
    case strings.EqualFold(track.Codec().MimeType, webrtc.MimeTypeVP8):
        s.recordScreenTrack(track)      // демонстрация экрана в .ivf
    }
})

Голос каждого участника пишется в свой .ogg. С демонстрацией экрана повозиться пришлось: VP8 приходит кусками, нужен samplebuilder чтобы собрать целые кадры, плюс руками ловить первый ключевой кадр, иначе запись стартует с каши из артефактов. И ещё отдельно просить у Телемоста высокое разрешение, иначе прилетает мыло.

На выходе куча дорожек, ffmpeg сводит их в один файл, файл уходит в расшифровщик. Всё, петля замкнулась. Бот сам сидит в созвоне и сам достаёт звук.

В последствии обнаружил один баг, гостевой вход работает, но не везде. Если у встречи включён «зал ожидания» или подтверждение входа, бот как гость не зайдёт, нужны куки хозяина. Мелочь, но на паре звонков я на это налетел и не сразу понял, почему бот молча висит снаружи.

Заигрывание с ботом, или что на него вышло повесить

Так, бот у нас есть, он умеет в обе стороны, получать и отдавать информацию. Ради эксперимента я подключил Gemini Live, дописал некоторые скиллы, навыки. И теперь во время созвона, можно обратиться к боту, за информацией, или запомнить что нибудь важное. ИИ это выполнит и сообщит, об этом прямо на встрече.

Mindmap

Mindmap

Также получилось привязать Gemini Flash 3.8, на саммари и прочие фичи, по типу Mindmap, и названия встречи. Вообщем мелочи.

Тут стоит сказать про один трюк, который потом выстрелит ещё раз. Голос в дорожку бота я не кодирую руками, cgo и libopus тащить было лень. В образе есть ffmpeg, вот он и жмёт PCM в Opus на лету, а я потом режу поток на пакеты по 20 мс и отдаю в трек. Простая схема, но она мне ещё пригодится в самой дикой фиче. Конечно, это огромный костыль и из‑за него есть небольшие задержки, но вскоре исправлю.

MCP

Потом захотелось, чтобы в мои разговоры могли ходить другие модели. Прикрутил MCP‑сервер, это такой протокол, по которому ИИ ходит в твои данные как в набор инструментов. Выдаёшь себе персональный ключ, вставляешь в клиент, и всё, тот же Claude, ChatGPT умеет искать по моим созвонам, читать расшифровки и тянуть саммари. По сути чат с собственными встречами, только из любимого клиента. Мелочь, а приятно.

Окно настроек, с описанием MCP

Окно настроек, с описанием MCP

Самое интересное, звонок в Яндекс.Телемост с обычного телефона

А вот теперь то, ради чего половина статьи. Опыт с IP телефонией у меня есть, так что идея легла сама. Что если человеку не нужен ни ноут, ни Телемост, а только телефон в кармане? Звонишь на номер, голосом набираешь пятизначный код встречи, и ты в видеозвонке обычным участником. Телемост про SIP ничего не знает. Зато у меня уже есть бот, живой участник WebRTC. Значит нужен переходник телефон в бота. Сигналинг взял на sipgo, он неожиданно нормальный. Медиа снова на pion.

Дальше пара нюансов, на которых я честно залип. Первый, DTMF это не звук. Цифры с телефона летят не тоном в аудио, а отдельными RTP‑пакетами специального типа (RFC 2833). В пакете код клавиши, бит окончания, длительность.

// telephone-event, 4 байта
//  0                   1                   2                   3
// |     event     |E|R| volume    |          duration             |
digit := payload[0]        // 0-9, 10 = '*', 11 = '#'
end   := payload[1]&0x80 != 0   // E-бит: нажатие завершилось

Второй нюанс, на который потратил больше времени. Код срабатывал на цифру раньше времени. Набираю 6317, а в буфере уже 63177, откуда лишняя семёрка. Полез в логи и увидел, что Zoiper на одно нажатие иногда шлёт два события с разными таймстампами, и последняя цифра дублируется. Добавил дебаунс одинаковой цифры, а сами коды стал генерить без соседних одинаковых цифр, чтобы дебаунс случайно не сожрал нормальный ввод.

А сам звук это тот самый трюк с ffmpeg, только теперь в обе стороны. Голос абонента приходит как PCMU 8кГц, я декодирую его в PCM, ffmpeg жмёт в Opus 48кГц, и это льётся в дорожку бота. А обратно микс участников тем же путём уходит в трубку. В Телемосте у бота загорается микрофон, и его слышно как живого человека, а сам абонент слышит встречу.

И да, звук двусторонний. Абонент и говорит в встречу, и слышит её в трубке. Полоса телефонная, 8 килогерц, для музыки узко, но голос передаёт нормально. По ощущениям обычный созвон, только ты в нём через телефон. Набрал номер, ввёл код, и ты на встрече.

Стек, если коротко

  • Backend: Go. chi, pgx, pion/webrtc, pion/rtp, sipgo, gorilla/websocket. Postgres.

  • AI: ElevenLabs Scribe на распознавание, Gemini Live и Gemini Flash на заметки, саммари и mindmap.

  • Медиа: ffmpeg на Opus и сведение дорожек.

  • Front: React и TypeScript, интерфейс на своей библиотеке компонентов.

  • Прочее: Docker Compose, nginx.

Что в итоге

Хотел переписать питоновский скрипт на Go для галочки в резюме. Получил бота, который сам сидит в созвонах, пишет их, ведёт заметки нейронкой, поднимается кнопкой из Телеграма, отдаёт данные другим ИИ по MCP, и в который можно позвонить с мобилы и оказаться на встрече. Go зашёл, горутины, каналы и pion сделали процесс почти приятным.

Если тема зайдёт, во второй части подробно разберу сигналинг Телемоста.

Спасибо, что дочитали, надеюсь Яндекс не постучится, как было у меня с Rutube. Текст в некоторых местах был написан с помощью ИИ, так как мне было немного структуризировать логически.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.