Как стать автором
Обновить

От идеи до платформы: полгода разработки собственного AI радио

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров3.3K

В своей предыдущей статье я рассказал читателям Хабра о пути, который привёл меня к разработке автоматизированного AI-радио с новостными блоками, подкастами и музыкальным контентом. Я получил много ценных отзывов — спасибо за это! Работа над AI-вещанием переросла в полноценную платформу. В этом посте я также расскажу о ряде неожиданных проблем, с которыми столкнулся при создании современной публичной стриминг-платформы — возможно, это убережёт вас от тех же ошибок.


Платформа

Поскольку я не стал ограничиваться одним эфиром и решил разделить вещание по жанрам, со временем меня перестал устраивать ручной процесс создания трансляций через базу данных и выполнение kubectl-команд. Кроме того, я получил несколько запросов от пользователей Хабра на запуск аналогичных тематических стримов.

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

Хороший программист — ленивый программист

Идея в том, что «ленивый» программист будет стараться автоматизировать всё, что можно, чтобы не делать одну и ту же работу дважды.

Самым сложным, пожалуй, был выбор названия для будущей платформы. В итоге я остановился на «Tunio» — производное от tune (настройка) и I/O (ввод/вывод).

Ошибки на старте

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

  • Конвертация и вещание в MP3
    Изначально я настроил вещание, конвертацию и хранение в MP3. Уже при реализации ретрансляции на стриминговые платформы (YouTube, VK Live, Telegram) стало ясно, что выбранный формат требует перекодирования потока в AAC, поскольку современные платформы работают с ним и/или с контейнером M4A. Это создавало значительную нагрузку на CPU даже при одном стриме. Контейнер M4A для AAC также удобен тем, что позволяет упаковывать метаданные (включая обложки) и корректно сохраняет длительность трека — в отличие от "сырого" AAC.
    Если вы не используете в Liquidsoap эффекты вроде crossfade и обработку звука, можно транслировать поток напрямую в режиме passthrough, без перекодирования — и тогда нагрузка на процессор практически отсутствует.

  • Использование исключительно платных TTS-решений
    Изначально я использовал только платные TTS-провайдеры для генерации новостей, подкастов и джинглов. Однако быстро столкнулся с тем, что баланс расходовался очень быстро, что ограничивало масштабирование и развитие проекта. После того как я обнаружил Piper TTS — self-hosted движок, который легко запускается в Docker и работает довольно шустро даже на CPU — ситуация кардинально изменилась. Возможность генерировать неограниченное количество подкастов, новостей на разных языках и объявлений дала мощный толчок развитию платформы, при этом без дополнительных расходов — что критично для проекта на ранней стадии.

  • Заказ джинглов у профессиональных студий
    Каждый раз процесс заказа джинглов превращался в длительную переписку со студией: согласование голосов, ожидание доступности дикторов (которые нередко оказывались заняты), а результат не всегда соответствовал ожиданиям. Когда я начал создавать джинглы самостоятельно, используя голоса от ElevenLabs, большинство проблем исчезло. Я получил именно тот результат, который мне был нужен — быстро, недорого и без лишней волокиты. Также у меня появилась возможность создавать джинглы сразу на нескольких языках. Примеры джинглов с голосами ElevenLabs:

    Думаю, вы согласитесь, что в контексте AI-радио джинглы с голосами AI звучат органичнее, чем записанные с участием настоящих дикторов.

  • Проверка уникальности новостей через теги
    Первоначально я пытался проверять уникальность новостей по тегам, которые генерировались из текста новости. Но создание тегов — не идемпотентная операция: одна и та же новость при разных запусках давала разные теги, несмотря на попытки адаптировать промпты. Решением стали векторные эмбеддинги. Каждая новость преобразуется в вектор с помощью модели text-embedding-ada-002 от OpenAI, после чего сохраняется в базу. При добавлении новой новости я нахожу ближайшую по смыслу, и если score меня не устраивает, не пропускаю новость дальше:

SELECT *, embedding <-> $1 AS score FROM your_table_name WHERE created_at >= $2 ORDER BY score LIMIT 1;

Это дало надёжную проверку на дублирование и повысило качество контента.

  • Фронтенд на Preact
    Изначально я написал фронтенд на Preact ради лёгкости и скорости. Но отсутствие серверного рендеринга (SSR) и SEO в итоге стало критичным. Пришлось мигрировать на Next.js, что дало все необходимые преимущества: SEO, SSR, удобную маршрутизацию и гибкую архитектуру.

  • Telegram как источник новостей
    Сначала я полагал, что Telegram может служить универсальным источником мировых новостей. Однако на практике он оказался ориентирован на русскоязычную аудиторию. Поэтому я добавил поддержку RSS-лент, которые, как оказалось, до сих пор активно используются на Западе. Через них я подключил новостные потоки по различным тематикам, и пропустил их через ту же цепочку обработки: фильтрация, категоризация, суммаризация, блокирование негатива, политики и рекламы

  • Ожидания от Telegram Min App
    Изначально я надеялся, что Telegram mini app станет основной платформой для потребления контента. Но впоследствии стало понятно, что нужно полноценное мобильное приложение. Я реализовал Android-приложение на React Native (Expo) с использованием WebView — это позволило быстро запустить клиент с базовым функционалом.

  • 100% ручная работа
    Backend, frontend, mobile app я писал самостоятельно, чтобы прокачать навыки программирования на Go, Typescript, посмотреть на современные подходы во frontend'е. Однако на практике оказалось, что значительная часть задач — рутинные. После того как я начал активно использовать промпт-программирование с использованием агентов (в частности, Sonnet), темпы разработки выросли в разы. Особенно frontend задачи, будто мне выполняет команда программистов, остается только делать код-ревью.

Ambient и переключение источников

Популярные трансляции на YouTube в стиле “Lofi hip-hop 24/7” натолкнули меня на идею интеграции ambient-звуков в эфир. Liquidsoap оказался гораздо мощнее, чем я предполагал — он позволяет легко создавать многослойный эфир и управлять слоями, например, через Telnet.

Это открывает широкие возможности: можно добавлять в эфир любые ambient-звуки — будь то локальный файл, зацикленный в проигрывании (например дождь), или внешний поток (например, трансляция переговоров авиа-диспетчеров).

Более того, оказалось совсем несложно реализовать кнопку для переключения на живое вещание — например, если есть необходимость переключить трансляцию на человеческого ведущего.

Как это работает:

Liquidsoap поднимает сервер для приёма потока через ffmpeg и, получая аудио от вашего live-ведущего (например, из OBS), выводит его в эфир:

live_source = input.external.rawaudio(
  buffer=0.1,
  max=0.3,
  restart=true,
  restart_on_error=true,
  "while true; do ffmpeg -f flv -listen 1 -i rtmp://0.0.0.0:8181/live -threads 1 -preset ultrafast -tune zerolatency -f wav -acodec pcm_s16le -ac 2 -ar 44100 - 2>/dev/null || sleep 1; done"
)

Остаётся только добавить переключатель с помощью switch в скрипте Liquidsoap — и вы можете динамически переключаться между источниками.

Кроме того, вы можете расширить Telnet-интерфейс собственными командами: включение/отключение ambient-аудио, переход в live-режим, и любые другие функции управления эфиром.

Запуск и остановка радио потока

Изначально проект появился как способ разобраться с Kubernetes. Однако позже я понял, что выбор этой технологии оказался крайне удачным именно для такого рода задач.

После того как пользователь настраивает свой стрим и запускает его, в Kubernetes создаются два Pod'а: один с Liquidsoap, другой — с менеджером эфира, который управляет воспроизведением Liquidsoap через Telnet. Оба Pod'а обязательно должны быть размещены на одной ноде, поскольку менеджер подготавливает нужные аудиофайлы и сохраняет их на диск, откуда их затем воспроизводит Liquidsoap.

Можно было бы объединить оба контейнера в один Pod, но тогда я бы потерял возможность обновлять менеджер эфира без остановки трансляции. Liquidsoap обновляется крайне редко, а вот логика управления эфиром постоянно развивается, и не хотелось бы прерывать вещание при каждом деплое.

Менеджер эфира формирует плейлист с запасом примерно на 10 минут вперёд. Это даёт буфер времени — если что-то пойдёт не так, у меня есть 10 минут, чтобы всё исправить, прежде чем плейлист опустеет.

Остановка стрима происходит аналогично: удалением обоих Pod'ов — с Liquidsoap и менеджера.

Основная нагрузка ложится на Pod с Liquidsoap. Поскольку я использую crossfade, ambient-слои, а также обработку звука (нормализацию и эквалайзер), я не могу использовать passthrough-трансляцию в AAC. В результате, на 7-ядерной виртуальной машине с процессором Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz удаётся стабильно поддерживать около 15 Liquidsoap-подов.

Менеджер эфира почти не потребляет ресурсов — особенно в те моменты, когда виртуальный ведущий не активен.

Виртуальный ведущий

Один из читателей Хабра, работающий в сфере радиовещания уже несколько десятков лет, подсказал интересную идею для реализации виртуального ведущего. Суть в том, чтобы ведущий связывал два трека: в конце одного объявлял его название, вставлял короткую связующую фразу и затем называл следующий трек.

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

Вот как это устроено:

  1. Я заранее сгенерировал аудио версии с названиями всех треков в библиотеке — на русском и английском языках.

  2. Сформировал шаблонные вступительные фразы, например: «Это была композиция...», «Вы слушали...» и тоже их сгенерировал в аудио

  3. Подготовил связующие фразы, такие как: «А далее в эфире...» также в аудио.

  4. На основе этого контента и рандома реализовал сборку аудиофайлов, содержащих фразы вида:
    «Это была композиция X, а далее в эфире — Y».

Далее, при подготовке трека X для плейлиста (если рандом решает что вступит ведущий):

  • Понижаю громкость в конце трека X

  • Накладываю сгенерированную голосовую вставку

  • Склеиваю всё с помощью ffmpeg

  • Добавляю следующий трек Y сразу за X в плейлист, создавая эффект непрерывного и "живого" эфира с участием ведущего.

Таким образом, удалось добиться эффекта настоящего ведущего без дополнительных затрат на онлайн-TTS в реальном времени.

Ретрансляция

Когда у вас уже есть предварительно сконвертированные материалы, готовые для стриминга, например:

  • Аудио: AAC (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 122 kb/s

  • Видео: H.264 (High) (avc1 / 0x31637661), yuv420p (progressive)

Рестриминг выполняется с минимальным потреблением ресурсов, поскольку отсутствует необходимость перекодирования. Пример команды ffmpeg для непрерывного рестриминга:

ffmpeg -stream_loop -1 -re -i video.mp4 -re -i https://app.tunio.ai/live/main_live \
-c:v copy -c:a copy -map 0:v:0 -map 1:a:0 -f flv rtmp://a.rtmp.youtube.com/live2/<key>

Для автоматизации этого процесса я разработал два сервиса:

  • Relay Controller

  • Relay Worker

Relay Controller получает по API payload с параметрами источников (видео, аудио), целевым URL и ключом трансляции. Он находит живой, наименее загруженный worker и передаёт ему команду на запуск трансляции.

Relay Worker, получив команду, проверяет наличие видео-обложки для стрима (если нет — загружает из S3), после чего запускает бесконечный процесс ffmpeg. В случае сбоя ffmpeg автоматически перезапускается через 10 секунд.

Кроме того, Worker при старте регистрируется у Controller’а и регулярно отправляет heartbeat с метриками: загрузкой CPU и списком активных стримов.

Если worker падает или перезапускается, при старте он восстанавливает свои активные трансляции, запрашивая их у Controller’а. Аналогично, если упал controller, worker при следующем heartbeat передаёт ему текущую актуальную информацию о всех запущенных стримах, позволяя системе продолжить работу без потерь.

Что удалось вынести в платформу:

Пока удалось перенести в интерфейс лишь часть ранее реализованного функционала. На текущий момент уже доступны следующие возможности:

  1. Создание и управление потоком
    Пользователь может создать стрим, выбрать музыкальные жанры, задать параметры дополнительного контента: включение/отключение новостей, джинглов и подкастов.

  2. Джинглы
    Можно создать любое количество плейлистов с джинглами, загрузить в них аудиофайлы, прикрепить плейлист к стриму и задать интервал (в количестве треков), с которым джинглы будут автоматически вставляться в эфир.

  3. Объявления
    По предложению одного из читателей Хабра реализована функция озвученных объявлений. Пользователь может ввести текст, выбрать голосовую модель (TTS), прослушать результат, а затем либо сразу воспроизвести объявление в эфире, либо задать интервал для его регулярного звучания.

  4. Регулярные аудиовставки
    Поддерживается загрузка пользовательских аудиофайлов с возможностью указания времени начала и окончания трансляции, а также интервала между повторами (в треках). Такие вставки могут автоматически добавляться в эфир.

  5. Рестриминг
    Пользователь может ретранслировать свой поток на YouTube, VK Live или даже напрямую в группу или канал Telegram, указав соответствующий ключ трансляции.

А что дальше?

С абсолютным удивлением я обнаружил, что существует запрос на такую платформу, где пользователи могли бы создавать собственные стримы с персонализированным контентом, не нарушающим авторские права. В связи с этим я планирую реализовать возможность генерации подкастов индивидуально для каждого пользователя (в настоящее время подкасты создаются глобально — для всей платформы).

С появлением self-hosted TTS-решений эти возможности практически не ограничены. Можно генерировать подкасты по заданному промпту, используя один или несколько виртуальных ведущих. Современные модели стремительно развиваются — некоторые уже умеют воспроизводить дыхание, шумы, интонации и даже эмоции. Лично я считаю, что TTS-технологии в ближайшее время смогут занять значительную часть ниши подкастов — и делать это на весьма достойном уровне.

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

Заключение

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

Теги:
Хабы:
+22
Комментарии11

Публикации

Работа

DevOps инженер
32 вакансии

Ближайшие события