Когда я только начинал Tunio, я хотел просто познакомиться с Kubernetes. В итоге получилось построить полноценную платформу для радио с AI-музыкой, новостями, прогнозами погоды, подкастами, гео-кластеризацией и TTS-ведущими - без команды, инвестиций и грантов. Эта статья - о том, как из pet-проекта вырос продакшн-сервис с реальными клиентами, и какие технические фэйлы и открытия случились по дороге.
Это заключительная техническая статья, завершающая мою историю о создании 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-диджеи, которые будут шутить лучше меня.
evgeniy_kudinov
Спасибо, отличный проект. Экспериментировали ли с локальными моделями для генерации музыки или речи?
icevl Автор
Спасибо. На сегодняшний день из доступных и быстро работающих на CPU решений можно выделить проект https://github.com/OHF-Voice/piper1-gpl - в репозитории есть демки. Звучит, конечно, не так живо, как ElevenLabs, но вполне приемлемо и бесплатно. Я использовал этот проект, написал обёртку, которая отдаёт результат по HTTP, и предоставляю эти модели как более дешёвую альтернативу (сейчас - бесплатно).