Стемы и синхрон сцены: как я сделал живучий сервис для команды прославления

Год назад у меня был сервис, который родился из очень приземлённой боли: песни для команды прославления разъезжались по чатам, правки терялись, актуальные версии приходилось выцарапывать из переписок. Тогда всё выглядело почти смешно: PHP на самом бюджетном shared-хостинге, React на фронте, деплой вручную по FTP. В финале я бодро пообещал три вещи - добавить стемы, сделать синхронизацию "сцены" и уехать с шареда. Звучало как "пара вечеров". В реальности каждая из задач оказалась отдельной маленькой войной.

Сразу расставлю границы: нейросети я не "изобретал". Разделение на дорожки и музыкальный анализ я делаю через сторонние API. Но всё, что вокруг - как доставить результат пользователю, как не завалить сервер, как пережить сбои, как подружить это со "сценой" и репетицией - вот это полностью моя зона ответственности. И именно там меня поджидала основная мясорубка.

Стемы: семь попыток, из которых выжила одна

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

1) Локальный Demucs. Теоретически красиво: автономно, бесплатно, качество достойное. Практически - провал с порога. На дешёвом хостинге нет GPU, а CPU-обработка занимает минуты. Плюс лимит запросов около 120 секунд. Оно даже не "медленно", оно просто не помещается в рамки.

2) Replicate. Рабочий вариант, но модель оплаты "за секунду GPU" мгновенно превращает бесплатный продукт в кассу без выручки. И, что важнее, на выходе - только дорожки. Ни аккордов, ни ритма, ни структуры. Музыканту нужен разбор песни, а не папка с WAV-файлами.

3) fal.ai. Первый кандидат, который реально добежал до прода. Минусы те же: платно, максимум шесть дорожек, только разделение. Зато именно на этом этапе я впервые уронил весь проект одной песней - точнее, шестью дорожками одновременно (ниже расскажу).

4) LALAL.AI. Там модель такая: за один проход получаешь два трека. Хочешь восемь дорожек - гоняй файл по кругу снова и снова и плати за каждый круг. Экономика для бесплатного сценария не просто плохая - она не существует.

5) Собственный GPU-сервис. Качество - огонь, моделей много, гибкости хоть отбавляй. Но это уже отдельная инфраструктура: видеокарта, очереди, ретраи, мониторинг, а главное - счёт даже за простой. Для одного разработчика и небольшого продукта это "слишком рано", иначе ты обслуживаешь железо вместо продукта.

6) Music.ai. Самая обидная попытка. На бумаге - идеальный вариант: официальный платный API, который умеет всё сразу (стемы, аккорды, секции). То, что нужно. В реальности - задачи регулярно падали с INTERNAL_ERROR после 65-120 минут ожидания. Полтора часа ожидания, чтобы получить ошибку, и ещё и заплатить за попытку из своего кармана. Поддержка молчала. Итог: ключ закомментирован на всех серверах, а в правилах проекта отдельным пунктом стоит проверка перед деплоем, чтобы случайная конфигурация не отправила трафик назад в эту дорогую и нестабильную ветку.

7) Moises API. Победитель по совокупности. За 3-8 минут он возвращает "пакет целиком": 7-8 дорожек, аккорды с таймингом, биты, темп, тональность и секции (куплет/припев/бридж). Не идеально, но предсказуемо и достаточно качественно, чтобы люди реально репетировали по результату.

Из этого родилось правило, которое я буквально записал себе "кровью" и повесил как принцип продукта: одно не отменяет другого. Если пришли дорожки, но не распознались аккорды - показываем дорожки. Если аккорды есть, а разделение упало - показываем аккорды. Если темп определился, а секции нет - всё равно даём то, что есть. Пользователь не должен упираться в пустой экран из‑за того, что одна операция из трёх не сложилась.

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

Как шесть дорожек убивали не вкладку - весь сервис целиком

Первая схема доставки казалась логичной: PHP скачивает дорожки из хранилища и сам отдаёт их в браузер. Одна точка входа, никаких "чужих доменов", всё под контролем. Это решение оказалось катастрофой.

Проблема простая и беспощадная: воркер PHP занят всё время, пока играет дорожка. Песня - пять-семь минут. Шесть дорожек - шесть воркеров "на удержании соединения". А на дешёвом хостинге пул воркеров - две-три штуки. Итог предсказуем: пока кто-то слушает стемы, весь проект начинает отвечать 503. Ломается не только страница песни - падает чат, расписание, всё.

Эта неделя врезалась в память: сначала дорожки падали мгновенно - оказалось, кэш пытался стянуть ~50 МБ до первого отданного байта, а nginx рвал соединение. Затем пошли тотальные 503. Потом Chrome начал блокировать часть дорожек, потому что я одновременно создавал шесть тегов `

Вывод я записал максимально грубо, чтобы не спорить с собой в будущем: PHP должен один раз отдать JSON со ссылками, а дальше браузер обязан общаться с хранилищем напрямую. Ни один воркер не должен участвовать в передаче аудио.

И вот тут всплыла контринтуитивная деталь, из‑за которой я успел сделать, замерить и откатить. Казалось бы, убрать посредника - значит ускорить. Я убрал прокси и увидел, что на части телефонов (особенно на мобильной сети) старт проигрывания стал менее стабильным: где-то дольше "думает", где-то чаще пересоздаёт соединение, где-то агрессивнее ведёт себя кэш. В итоге схема стала гибридной: прямые ссылки из JSON, правильные заголовки кэширования, аккуратная подготовка файлов в хранилище и ограничение параллельных запросов на фронте. Быстрее стало не "от отсутствия прокси", а от того, что поток перестал жить внутри PHP и перестал сжигать воркеры.

Движок: три поколения и один упрямый iPhone

Отдельным слоем боли оказался плеер. На десктопе многое прощается: вкладка мощная, сеть ровная, браузер зрелый. Мобильный Safari - другой мир, где ты постоянно чувствуешь, что платформа тебе не друг.

Первое поколение плеера было "наивным": шесть `

Почему я всё-таки съехал с дешёвого хостинга

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

Меня добили три вещи:
1) малый пул воркеров и невозможность нормально управлять процессами;
2) ограничения по времени выполнения и непредсказуемые рестарты;
3) отсутствие нормальной наблюдаемости: когда всё падает, тебе нужно видеть очередь, тайминги, ошибки, ретраи - а не гадать по обрывкам логов.

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

"Синхрон сцены": функция, которая психологически выжала досуха

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

Я пришёл к тому, что синхрон должен быть максимально терпимым к реальности. Люди заходят с разными телефонами, с разной задержкой, иногда с плохим интернетом. Поэтому:
- события должны быть идемпотентными (повтор пришёл - ничего страшного);
- клиент обязан уметь догонять "текущее состояние", а не проигрывать весь лог с начала времён;
- любая команда должна быть обратимой или хотя бы безопасной (если кто-то случайно тапнул - репетиция не должна умирать).

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

Live Pro за три дня: когда продукт начинает бесить тебя самого

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

Самым ценным итогом стало не количество фич, а перечень того, что нельзя делать:
- нельзя прятать ошибки за вечным "загрузка...";
- нельзя завязывать критичную логику на одну внешнюю операцию;
- нельзя давать UI, который выглядит уверенно, когда система на самом деле в неопределённом состоянии.

Тесты, которые врали, и код, который "возвращался"

Я несколько раз ловил ситуации, когда тесты зелёные, а у пользователей всё ломается. Причина оказалась скучной: тесты проверяли "счастливый путь", а продукт жил в мире таймаутов, частичных результатов и падений внешних API. После этого я начал считать нормой не "успешный ответ", а деградацию: что увидит человек, если пришли только аккорды? если дорожки пришли без битов? если ссылка на один из файлов протухла? если задача застряла?

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

Несколько выводов спустя эти три месяца

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

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

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

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

Прокрутить вверх