Когда я только начинал Tunio, я хотел просто познакомиться с Kubernetes. В итоге получилось построить полноценную платформу для радио с AI-музыкой, новостями, прогнозами погоды, подкастами, гео-кластеризацией и TTS-ведущими - без команды, инвестиций и грантов. Эта статья - о том, как из pet-проекта вырос продакшн-сервис с реальными клиентами, и какие технические фэйлы и открытия случились по дороге.


Это заключительная техническая статья, завершающая мою историю о создании AI-радио-платформы с нуля. Предыдущие материалы можно найти на Хабре:

  1. Как я создал полностью автоматизированное онлайн радио с AI ведущими и музыкой

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

Дисклеймер

Так получилось, что вскоре после начала разработки платформы я познакомился по ближе с Claude Code и Codex, и заодно начал использовать агентов в отдельных задачах - чтобы решить несколько сложных для меня проблем в областях, где мне не хватало компетенций:

  1. Когда нужно было локализовать 600+ строк интерфейса на три языка, я просто дал агенту JSON и сказал "портируй Русский словарь на Английский, Казахский и Армянский языки". Через 5 минут всё было готово. Тот же подход сработал для SEO-текстов и блогов.

  2. Когда появились первые клиенты, стало важно не просто "чтобы работало", а чтобы система обеспечивала стабильность вещания и соответствовала ожидаемому SLA по аптайму и задержкам (в том числе на стороне клиента). Я решил сделать Android-приложение (launcher), которое запускалось бы на ТВ-приставке, умело подниматься после перезагрузки, сохраняло PIN трансляции, кэшировало небольшое количество треков на случай обрыва интернета или сбоев в моей инфраструктуре, а также автоматически восстанавливало соединение, продолжая воспроизводить музыку из аварийного хранилища. Я выбрал Flutter - несмотря на то, что времени на изучение почти не было, за один выходной удалось собрать приложение, загрузить его в Google Play и закрыть этот вопрос.

  3. Claude code помогал ревьювить дизайн и типографику баннеров, для Google Play, фон для которых рисовала Sora а я верстал в фигме все остальное:

баннеры для Google Play
баннеры для Google Play

В целом использование агентов значительно ускорило разработку и процесс обучения, сделав его гораздо менее мучительным. Иногда создавалось ощущение, что вместе со мной работает целая команда дизайнеров, верстальщиков, аналитиков и backend-разработчиков, а моя собственная роль сводилась к тщательному ревью их задач. Со временем я всё чаще занимался генерацией идей, архитектурой и код-ревью, а напрямую программировал всё меньше. Даже при возникновении проблем с Kubernetes это уже не отнимало у меня всю энергию: путь к получению опыта значительно сократился, и я смог сосредоточиться только на главной цели - запустить отказоустойчивый проект в Kubernetes.

Единственное место, где моя идея никак не дружила с моими знаниями, - это создание фонового лаунчера, который запускается вместе с системой на TV-приставке и воспроизводит пользовательский поток через разъём 3.5 мм по PIN.

Поскольку во Flutter я новичок, за один вечер с помощью ИИ мне удалось воплотить идею в жизнь - код получился ужасный, но всё взлетело.

TV-Box launcher, который подключается к потоку по PIN и в фоновом режиме воспроизводит клиентскую станцию из Icecast2, автоматически переключаясь на локальный кэш при потере сети.
TV-Box launcher, который подключается к потоку по PIN и в фоновом режиме воспроизводит клиентскую станцию из Icecast2, автоматически переключаясь на локальный кэш при потере сети.

Итоговая архитектура

Хоть всё и начиналось как pet-проект, спустя восемь месяцев я понимаю, что без Kubernetes реализовать подобную идею с быстрым запуском и отключением клиентских радиостанций было бы крайне сложно. Сейчас все процессы управляются через Control Plane API.

Единственная проблема возникла на старте: BGP-сессии на некоторых нодах не поднимались. (DC блокировал 179/tcp, пришлось переключить на нестандартный порт)

Архитектура Tunio.AI c гео-распределением
Архитектура Tunio.AI c гео-распределением

Основной кластер:

Основной кластер (namespace: default) включает:

  • PostgreSQL (мастер-сервер) - основная база данных.

  • API-сервер / Backend - выполняет чтение и запись из мастера, делает запросы в kubernetes для управления клиентскими радио инстансами.

  • Фронтенды (cp.tunio.ai, app.tunio.ai, tunio.ai) - пока без гео-распределения.

  • RabbitMQ - используется для гео-распределённых радиостанций, работающих с read-only репликами базы. В брокере фиксируются события: переключение треков, изменение плейлистов и другие результаты работы менеджеров радиостанций.

  • Фоновые задачи (TTS/Загрузка музыки) - конвертация новостей, объявлений, подкастов и прогнозов погоды в речь. Эти процессы очень рессурсоёмкие и выполняются на отдельном хосте и не влияют на I/O API, базу данных или клиентские радиостанции.

  • Почтовый сервис - Почтовый сервис получает сообщения по AMQP и отправляет письма на основе шаблонов.

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

  • Биллинг-сервис - запускается по расписанию (CronJob, 1 раз в сутки).

  • Система метрик - сбор и визуализация показателей (Grafana, Prometheus). Нагрузка k8s нод, слушатели радио, нагрузка на базы.

  • Рестриминг-сервисы - ретрансляция клиентских радио потоков в YouTube, VK Live, Telegram и другие платформы.

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

RabbitMQ я выбрал вместо Kafka из соображений практичности. Kafka отлично подходит для event streaming и аналитики, но требует больше ресурсов и внимания со стороны DevOps. В моём случае приоритетом были простота, надёжность и минимальные накладные расходы на обслуживание - поэтому RabbitMQ стал оптимальным выбором.

Гео-кластеризация

Для обеспечения стабильного подключения конечных клиентов было решено реализовать гео-кластеризацию.

Пример региона (region-ru1) - это отдельный namespace Kubernetes, содержащий:

  • PostgreSQL (read-only репликация) - физическая реплика базы данных.

  • Icecast2-сервер - потоковая передача клиентских радио конечному пользователю.

  • Redis-сервер - кэш и хранение данных (время последнего проигрывания трека, объявления и прочее).

  • Liquidsoap-инстансы - по одному на каждую радиостанцию клиента. Я использовал старую версию Liquidsoap (v2.0.3), поскольку она достаточно нетребовательна к ресурсам. Для каждого инстанса выделено 200 m CPU - этого оказалось вполне достаточно.

  • Радио-ротатор - Управляет эфиром (инстансом Liquidsoap) через telnet - по локальному подключению к Liquidsoap внутри namespace. Менеджер добавляет в поток музыку, контент, джинглы и объявления, а также может изменять громкость в реальном времени - например, делать объявления громче фоновой музыки. При этом, важно, что коннект не зависит от CoreDNS. И идет напрямую на имя сервиса что бы при нарушении сетевой связанности в гео-кластере ротатор продолжал подключаться к транслятору.

Особенности:

  • Когда пользователь создает свою станцию, деплой выполняется на выделенные рабочие ноды кластера с соответствующим лейблом (region=ru1, region=kz1 и т. д.), расположенные, например, в РФ или Казахстане. Регион назначается один раз на радио-станцию при создании и больше не меняется. Ссылка на поток при этом выглядит так: https://region-ru1.tunio.ai/main.aac (main - название станции)

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

  • Физическая репликация позволяет снизить нагрузку на мастер и повышает отказоустойчивость в случае инцидентов.

Сторонние сервисы

  • Объектное хранилище для контента и бэкапов - Яндекс Object Storage. Я уже писал в предыдущей статье о том, как официальная либа AWS легко перенастраивается на работу с Яндексом.

  • Отправка писем - Mailersend.com (до 3000 писем в месяц бесплатно).

  • TTS-сервисы - ElevenLabs, SaluteSpeech, локальный Piper TTS

  • Музыка - Suno и ElevenLabs

  • Генерация изображений (обложки для подкастов и радиостанций) - API Replicate, модель FluxSchnell.

  • LLM-задачи (категоризация, фильтрация новостей и другое) - OpenAI, DeepSeek.

VAS - услуги с добавленной стоимостью

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

В итоге для прогноза погоды “сшиваются” вместе: интро (заданное пользователем), прогноз по каждому городу на сегодня и завтра, и аутро. Получился динамичный и полезный блок, собранный по кусочкам как лего.

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

Каждая новость проходит несколько этапов:

- фильтрацию от рекламы,

- проверку на полноту и полезность (например, если это просто картинка и одна строка текста - новость игнорируется),

- суммаризацию.

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

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

CI

Основной репозиторий организован как модульный монолит. В нём собраны все ключевые компоненты - от API-сервера до менеджера клиентских эфиров. Сейчас это 16 компонентов, которые используют общие утилиты: работу с биллингом, генерацию аудио, отправку уведомлений и писем, Sentry, TTS и другие модули. После сборки каждый компонент упаковывается в отдельный контейнер со своей зоной ответственности, а результат их работы отправляется либо в объектное хранилище либо в RabbitMQ или туда и туда.

Github Actions пока что обходится абсолютно бесплатно. По пушу в main в matrix запускается сборка Docker-образов с последующей загрузкой их в GitHub Container Registry. После успешной сборки контейнера в некоторых случаях (например, при деплое фронтендов или почтового сервиса) выполняется перезапуск пода в Kubernetes:

- name: Deploy
  uses: actions-hub/kubectl@master
  env:
    KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
  with:
    args: rollout restart deployment <deployment-name>
Github Actions бесплатно собирают не только Core функционал, но еще и фронты и мобильные приложения
Github Actions бесплатно собирают не только Core функционал, но еще и фронты и мобильные приложения

Проблемы при хостинге realtime-решений

При хостинге систем с real-time-аудио важно заранее понимать, с какими ограничениями можно столкнуться. Я прочувствовал это на практике.

Поскольку всё начиналось как pet-проект, я выбрал самое бюджетное решение - арендовал несколько VPS и разнёс тяжелые фоновые задачи и пользовательские радиостанции по разным узлам. Всё работало стабильно, пока не появился реальный трафик.

Тогда я заметил, что клиентские потоки начали буферизироваться. Запустив mpstat, я с удивлением обнаружил, что выделенные ядра фактически не дают ожидаемой производительности: гипервизор “подъедал” CPU-ресурсы. Судя по всему, “соседи” по хостингу активно нагружали процессор, из-за чего мои станции недополучали вычислительную мощность и начинали заикаться при передаче аудио в icecast.

такая неудача может случиться на VPS и это критично для real-time приложений
такая неудача может случиться на VPS и это критично для real-time приложений

%steal (Steal Time) - это доля времени, в течение которого виртуальная машина готова выполнять задачи, но физический процессор в этот момент занят другими ВМ на том же хосте.

Иными словами, это время, "украденное" гипервизором у твоей VPS.

Столкнувшись с этой проблемой, я вдруг понял, что мой pet-проект уже достаточно вырос, чтобы "выйти из клетки" - и перенёс его на собственный выделенный сервер без соседей.

В заключении

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

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

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

Всем спасибо!

Ссылки

А впереди - новые эксперименты: умные эфиры, звонки в студию, очень много публичного API, динамическая музыка в зависимости от погоды и возможно, AI-диджеи, которые будут шутить лучше меня.

Комментарии (2)


  1. evgeniy_kudinov
    27.10.2025 02:52

    Спасибо, отличный проект. Экспериментировали ли с локальными моделями для генерации музыки или речи?


    1. icevl Автор
      27.10.2025 02:52

      Спасибо. На сегодняшний день из доступных и быстро работающих на CPU решений можно выделить проект https://github.com/OHF-Voice/piper1-gpl - в репозитории есть демки. Звучит, конечно, не так живо, как ElevenLabs, но вполне приемлемо и бесплатно. Я использовал этот проект, написал обёртку, которая отдаёт результат по HTTP, и предоставляю эти модели как более дешёвую альтернативу (сейчас - бесплатно).