Когда я только начинал Tunio, я хотел просто познакомиться с Kubernetes. В итоге получилось построить полноценную платформу для радио с AI-музыкой, новостями, прогнозами погоды, подкастами, гео-кластеризацией и TTS-ведущими - без команды, инвестиций и грантов. Эта статья - о том, как из pet-проекта вырос продакшн-сервис с реальными клиентами, и какие технические фэйлы и открытия случились по дороге.
Это заключительная техническая статья, завершающая мою историю о создании AI-радио-платформы с нуля. Предыдущие материалы можно найти на Хабре:
Как я создал полностью автоматизированное онлайн радио с AI ведущими и музыкой
От идеи до платформы: полгода разработки собственного AI радио
Послушайте, что в итоге получилось в эфире. Это примеры продвинутых случаев, выходящие за рамки обычной фоновой музыки:
Пример AI автоматизированного радио (новости, подкасты, прогноз погоды, джинглы) на Русском
Пример AI автоматизированного радио (новости, подкасты, прогноз погоды, джинглы) на Английском
Дисклеймер
Так получилось, что вскоре после начала разработки платформы я познакомился по ближе с Claude Code и Codex, и заодно начал использовать агентов в отдельных задачах - чтобы решить несколько сложных для меня проблем в областях, где мне не хватало компетенций:
Когда нужно было локализовать 600+ строк интерфейса на три языка, я просто дал агенту JSON и сказал "портируй Русский словарь на Английский, Казахский и Армянский языки". Через 5 минут всё было готово. Тот же подход сработал для SEO-текстов и блогов.
Когда появились первые клиенты, стало важно не просто "чтобы работало", а чтобы система обеспечивала стабильность вещания и соответствовала ожидаемому SLA по аптайму и задержкам (в том числе на стороне клиента). Я решил сделать Android-приложение (launcher), которое запускалось бы на ТВ-приставке, умело подниматься после перезагрузки, сохраняло PIN трансляции, кэшировало небольшое количество треков на случай обрыва интернета или сбоев в моей инфраструктуре, а также автоматически восстанавливало соединение, продолжая воспроизводить музыку из аварийного хранилища. Я выбрал Flutter - несмотря на то, что времени на изучение почти не было, за один выходной удалось собрать приложение, загрузить его в Google Play и закрыть этот вопрос.
Claude code помогал ревьювить дизайн и типографику баннеров, для Google Play, фон для которых рисовала Sora а я верстал в фигме все остальное:

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

Итоговая архитектура
Хоть всё и начиналось как pet-проект, спустя восемь месяцев я понимаю, что без Kubernetes реализовать подобную идею с быстрым запуском и отключением клиентских радиостанций было бы крайне сложно. Сейчас все процессы управляются через Control Plane API.
Единственная проблема возникла на старте: BGP-сессии на некоторых нодах не поднимались. (DC блокировал 179/tcp, пришлось переключить на нестандартный порт)

Основной кластер:
Основной кластер (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>
Проблемы при хостинге realtime-решений
При хостинге систем с real-time-аудио важно заранее понимать, с какими ограничениями можно столкнуться. Я прочувствовал это на практике.
Поскольку всё начиналось как pet-проект, я выбрал самое бюджетное решение - арендовал несколько VPS и разнёс тяжелые фоновые задачи и пользовательские радиостанции по разным узлам. Всё работало стабильно, пока не появился реальный трафик.
Тогда я заметил, что клиентские потоки начали буферизироваться. Запустив mpstat, я с удивлением обнаружил, что выделенные ядра фактически не дают ожидаемой производительности: гипервизор “подъедал” CPU-ресурсы. Судя по всему, “соседи” по хостингу активно нагружали процессор, из-за чего мои станции недополучали вычислительную мощность и начинали заикаться при передаче аудио в icecast.

%steal (Steal Time) - это доля времени, в течение которого виртуальная машина готова выполнять задачи, но физический процессор в этот момент занят другими ВМ на том же хосте.
Иными словами, это ��ремя, "украденное" гипервизором у твоей VPS.
Столкнувшись с этой проблемой, я вдруг понял, что мой pet-проект уже достаточно вырос, чтобы "выйти из клетки" - и перенёс его на собственный выделенный сервер без соседей.
В заключении
Я постарался завершить свой цикл статей о создании собственной радиоплатформы техническими деталями - как и обещал ранее. Возможно, кто-то в будущем столкнётся с похожими задачами или уже решал их, и мой опыт окажется полезным.
После публикации первых двух статей ко мне пришло много людей, которые делились своими идеями и мечтами. Некоторые из них удалось воплотить в платформе Tunio - помочь кому-то решить конкретную задачу или реализовать давнее желание.
Спасибо всем, кто откликнулся! Благодаря Хабру и вашей обратной связи у меня появилось множество интересных знакомств и та самая энергия, чтобы довести начатое до полноценного продукта - вырасти из локального радио, которое звучало у меня в браузере во время работы, до эфиров в крупных фитнес-центрах, павильонах виртуальной реальности и на площадках интернет-радио.
Всем спасибо!
Ссылки
А впереди - новые эксперименты: умные эфиры, звонки в студию, очень много публичного API, динамическая музыка в зависимости от погоды и возможно, AI-диджеи, которые будут шутить лучше меня.