TL;DR

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

  • Боль: Аренда GPU-сервера стоит дорого ($300–$1000/мес), а простаивает он 90% времени. Serverless GPU решения часто имеют ограничения по времени выполнения или окружению, в которые видеорендеринг не влезает.

  • Решение: Самописная оркестрация "Одноразовых Воркеров" (Disposable Workers). Основной бекенд (Node.js) через API облака поднимает VPS с GPU только на момент рендера задачи и уничтожает его сразу после завершения.

  • Стек: Node.js (Orchestrator & Worker main process), Redis (State Machine), Python (Worker components), DigitalOcean API, FFmpeg, LLM провайдеры.

  • Экономика: Расходы снизились с $1100/мес (постоянный сервер) до ~$130/мес (оплата поминутно только за время рендера).

  • Применимо: Для асинхронных тяжелых задач (видеорендеринг, запуск локальных LLM, 3D-моделирование), где допустима задержка старта (Cold Start) в 3–5 минут.

  • Не применимо: Для Real-time задач и высоконагруженных систем с мгновенным откликом.

Одна из первых попыток объяснить что делает CoreCut
Одна из первых попыток объяснить что делает CoreCut

Кто о чем, а я опять о стартапах, пет-проектах и всяком таком. Сегодня хочу поделиться своими техническими инсайтами из проекта CoreCut, который я пилю в одиночку при помощи т.н "ВАЙБКОДИНГА"(с). Сразу попрошу прощения, из РФ работает только через VPN.

Проблема и экономика

Когда ты пилишь AI-стартап на свои, математика простая: сервер с нормальной видеокартой стоит от $300 в месяц. Если у тебя 10 пользователей, он простаивает 90% времени, сжигая деньги. Если у тебя 1000 пользователей, он ложится, и все ждут рендера часами.

Я понял, что нужно логически разделить дорогие операции(видеопроцессинг, whisper, локальные модели, если понадобятся) и дешевые(вызовы API, ЛК пользователя и т.п.). Мне интуитивно показалось, что дешевые операции не будут требовать масштабирования сразу после старта сервиса, а вот дорогие - вполне. Забегая наперед, это было удачное предположение.

А вот с тем, чтобы заставить работать эту историю с GPU работать так, как мне надо пришлось попотеть. Главная проблема заключалась в том, что у Digital Ocean, если ты покупаешь GPU droplet, то ты платишь за него даже если он выключен. То есть не получится просто делать boot и shutdown после завершения задачи. Тогда можно его просто не выключать. А стоит нужный мне 48GB VRAM 1.57$\час! Для меня, как для безработного стартапера, 1100$\месяц за очередную игрушку - очень дорого.

Статья расходов

Постоянный сервер (24/7)

On-Demand (Эфимерные воркеры)

Время работы

720 часов / мес

~85 часов / мес (10 видео * (12 мин рендер + 5 мин boot))

Стоимость compute

$1,130 / мес

~$133 / мес

Простой

Плачу за воздух 90% времени

0% (воркер умирает, если нет задач)

Накладные расходы

0 минут (всегда готов)

+3-5 минут на старт машины (boot + git pull)

Итого экономия

~8.5 раз (~$1000)

Решение заключалось в том, чтобы поднять GPU, все на ней строить, сделать snapshot, удалить GPU. А потом создавать новые из этого снапшота и удалять целиком. Звучало довольно просто до тех пор, пока я не столкнулся с тем, что обновление какого-то компонента(whisper, gpu-renderer или ai-editor) требовало от меня обновления этого самого снапшота. Потратил несколько недель на то, чтобы сделать эту часть с gpu-worker так, как я хочу.

Локальный Proof of Concept

Как и многие айтишники я оказался на рынке труда. Но сидеть сложа руки я не люблю и если гора не идет к Магомету, Магомет идет к горе. И я пошел сам создавать себе рабочее место, как настоящий миллениал инфантил - решил вести вайбкодинг стримы. Да-да, мама, я буду блогером! Идея стримов была очень простая: в режиме онлайн занимаюсь вайбкодингом. Создаю новые проекты с 0 прям на стримах. Так у меня появились raspisochka.ru и replayka.ru. Я просто вайбкодил, ни строчки(кроме .env) сам не писал, мучал нейросети. С одной стороны обе гипотезы себя не оправдали в моих глазах, несмотря на то, что у реплайки даже юнит-экономика отлично сходится, а с другой - я неплохо набил руку в вайбкодинге.

И в какой-то момент я понял, что стримы, которые длятся по 2-5 часов смотреть довольно тяжело. Стримы сами по себе это вообще очень специфический контент. Лично я такое не смотрю, например. Но при этом, часовой стрим обычно можно ужать в видео 15-20 минут с нормальным нарративом и насыщенным контентом. Монтировать я не умею, учиться этому не хотел и подумал: "А что если это сделает нейронка?"

Так, у меня зародилось зерно мысли продукта, который я в последствии назвал CoreCut. Моя идея заключалась в том, что я скармливаю свой стрим, а на выходе получаю готовое для публикации summary видео, где вырезано все лишнее и оставлено только главное, CORE стрима.

Шаг 1. У меня на машине работает

Я никогда не обрабатывал видео. Тем более при помощи ИИ. Я веб-обезьяна, мне бы JSON перекладывать с полки на полку. По этому как любой "нормальный человек" я пошел в ChatGPT с целью выяснить а как вообще это провернуть. Через 30 минут я для себя в голове нарисовал схему:
1. На вход получаем видео
2. Извлекаем при помощи ffmpeg из него аудиодорожку
3. Аудиодорожку прогоняем через whisper и получаем текстовый транскрипт с временными метками - segments.json
4. Скармливаем этот json нейронке чтобы она отобрала нужные сегменты по критериям и выбросила все лишние. На выходе новый segments.json
5. При помощи ffmpeg вырезаем кусочки, представленные в segments.json
6. Тем же ffmpeg склеиваем вместе полученные сегменты. И им же накладываем субтитры
7. Результат: финальное видео с субтитрами

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

Я вооружился одним из своих вайбкодинг стримов, длительностью 1 час 20 минут, прогнал через локально установленный Whisper, использовал модель large-v2, если вдруг кому интересно. И получил на выход segments.json. Открыл его и сильно порадовался тому, насколько хорошо оно извлекло текст из плохого звука. Взял этот segments.json и с ним пошел в обычный веб-интерфейс ChatGPT. Тогда по моему 4o хайповала. Написал промпт в духе

Ты ИИ-видеоредактор, который должен из входных метаданных видео и его текстового транскрипта составить segments.json нового видео.

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

Немного магии bash
#!/usr/bin/env bash

# Может немного отличаться от скрипта, который у меня был
# Но суть абсолютно таже самая и тоже просто сгенерированно

set -euo pipefail

SRC="${1:-source.mp4}"
JSON="${2:-new_segments.json}"
OUT="${3:-final.mp4}"

if ! command -v jq >/dev/null; then
  echo "Error: jq is required. Install with: sudo apt-get install jq (or brew install jq)"
  exit 1
fi

if ! command -v ffmpeg >/dev/null; then
  echo "Error: ffmpeg is required."
  exit 1
fi

# Ensure JSON is a non-empty array
count="$(jq 'length' "$JSON")"
if [[ "$count" -eq 0 ]]; then
  echo "Error: no segments found in $JSON"
  exit 1
fi

# Detect if source has audio
if ffprobe -v error -select_streams a \
  -show_entries stream=index -of csv=p=0 "$SRC" | grep -q .; then
  HAS_AUDIO=1
else
  HAS_AUDIO=0
fi

# Read start/end pairs from JSON
mapfile -t SEGMENTS < <(jq -r '.[] | "\(.start) \(.end)"' "$JSON")

vfilters=()
afilters=()
concat_inputs=()

i=0
for line in "${SEGMENTS[@]}"; do
  start="$(awk '{print $1}' <<<"$line")"
  end="$(awk '{print $2}' <<<"$line")"

  vlabel="v$i"
  alabel="a$i"

  vfilters+=("[0:v]trim=start=${start}:end=${end},setpts=PTS-STARTPTS[$vlabel]")

  if [[ "$HAS_AUDIO" -eq 1 ]]; then
    afilters+=("[0:a]atrim=start=${start}:end=${end},asetpts=PTS-STARTPTS[$alabel]")
    concat_inputs+=("[$vlabel][$alabel]")
  else
    concat_inputs+=("[$vlabel]")
  fi

  i=$((i+1))
done

n="$i"

# Build filter_complex
filter_complex="$(printf '%s;' "${vfilters[@]}")"
if [[ "$HAS_AUDIO" -eq 1 ]]; then
  filter_complex+="$(printf '%s;' "${afilters[@]}")"
  filter_complex+="$(printf '%s' "${concat_inputs[@]}")concat=n=${n}:v=1:a=1[outv][outa]"
  map_args=(-map "[outv]" -map "[outa]")
  audio_args=(-c:a aac -b:a 192k)
else
  filter_complex+="$(printf '%s' "${concat_inputs[@]}")concat=n=${n}:v=1:a=0[outv]"
  map_args=(-map "[outv]")
  audio_args=()
fi

ffmpeg -hide_banner -y -i "$SRC" \
  -filter_complex "$filter_complex" \
  "${map_args[@]}" \
  -c:v libx264 -preset medium -crf 18 \
  "${audio_args[@]}" \
  -movflags +faststart \
  "$OUT"

echo "Wrote $OUT"

И на удивление, это сработало с первого раза. И сработало СИЛЬНО лучше, чем я ожидал. Этот самый первый результат я поместил к себе на канал в качестве референса на который я дальше буду опираться. Потом я выяснил, что первый раз мне просто как-то несказанно повезло и буду еще долго идти к воспроизводимости подобного результата. Но я об этом еще не знал.

Когда эта сторона медали было готова я начал думать о том, как это масштабировать до сервиса и что мне для этого нужно.

Архитектура: оркестратор и GPU-воркеры

Эволюция. Разделяй и властвуй. Появление Оркестратора

Я понял, что нужно логически разделить дорогие операции(видеопроцессинг, whisper, локальные модели, если понадобятся) и дешевые(вызовы API, ЛК пользователя и т.п.). Мне интуитивно показалось, что дешевые операции не будут требовать масштабирования сразу после старта сервиса, а вот дорогие - вполне. Забегая наперед, это было удачное предположение.

  1. Мозг (Orchestrator): Дешевый VPS (5$/мес, Node.js). Принимает заказы, хранит стейт, пинает воркеров.

  2. Мышцы (GPU Workers): Дорогие, мощные машины, которые делают работу.

Это решение позволило мне сделать API доступным 24\7(простите, друзья, из РФ пока к сожалению только через VPN), но не держать онлайн дорогущие GPU машины. Сам оркестратор я наколбасил довольно быстро, там обычный NodeJS, BullMQ, Redis, Postgres и собственно все. По сути обычная апишка, ничего интересного.

Обновление образов, self-destroy и GC

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

  1. Оркестратор видит задачу в статусе PROCESSING_GPU.

  2. Через API провайдера (DigitalOcean/AWS) он создает новый дроплет из заранее подготовленного снапшота.

  3. Инъекция контекста: Когда Оркестратор создает дроплет через API DigitalOcean, он не просто поднимает "голую" машину:
    Оркестратор ждет, пока поднимется SSH.
    Заходит по SSH и заливает свежий .env файл и ecosystem.config.js для PM2
    Обновление кода и зависимостей(подробнее ниже)
    Воркер при старте ( worker-direct.js ) читает эти переменные и понимает:
    "Ага, я родился, чтобы обработать Job #12345".

  4. Дроплет просыпается, стягивает свежий код воркера (gpu-worker), обновляет другие зависимости при необходимости, выполняет задачу и... убивает сам себя.

Обновление кода:

У меня есть "Золотой образ" (Snapshot) весом в 50GB. Там лежат CUDA-драйверы, PyTorch, FFmpeg и веса тяжелых моделей. Это обновляется редко. А вот сам JS/Python код меняется часто. По этому при старте воркера запускается скрипт обновления (упрощенно)

  # Сценарий при старте (user-data / startup script)
    git -C /app/gpu-worker pull origin main
    npm install
    source venv/bin/activate && pip install -r requirements.txt
    pm2 restart worker

Это позволяет мне выкатить фикс бага в прод за 30 секунд, не пересобирая 50-гигабайтный образ.

Цена вопроса: Пользователь ждет результата на 3-5 минут дольше. Для B2C сервиса, где юзер готов пойти сделать чай, пока нейронка "думает", это идеальный компромисс. Я меняю 5 минут ожидания юзера на $1000 которые платил бы из своего кармана.

Самоуничтожение (IdleMonitor)

Воркер сам несет ответственность за свою смерть. Внутри worker-direct.js крутится IdleMonitor. Это таймер обратного отсчета.

  • Есть задача? Таймер сбрасывается.

  • Нет задач 3 минуты? Воркер посылает прощальный сигнал Оркестратору (/api/droplet/destroy) и вызывает системную команду на уничтожение.

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

Контроль

Конечно, полагаться только на суицидальные наклонности воркера нельзя. Бывает всякое: segfault в питоне, отвалилась сеть, скрипт завис до того, как вызвал API уничтожения.

Для этого в Оркестраторе есть "Уборщик" (Garbage Collector). Раз в час запускается крон, который сверяет список живых дроплетов в DigitalOcean со списком активных задач в Redis.

  • Если дроплет есть, а задачи под него нет (или она давно FAILED): DELETE /droplets/:id

  • Если дроплет есть и задача висит в processing > 1 часа: DELETE /droplets/:id

  • Это "Защита от банкротства" на случай, если мой код IdleMonitor забагует.

Примерно так это все выглядело на старте
Примерно так это все выглядело на старте

State Machine

Поскольку видеопроцессинг штука долгая и асинхронная, хранить состояние "в памяти" Node.js нельзя (сервер перезагрузится и всё потеряем). Я реализовал State Machine поверх Redis(но последнее состояние храню и в postgres)

Почему это важно? Статус WAITING_TRANSCRIPT_APPROVAL это Checkpoint. В этот момент никаких GPU-воркеров не существует! Они все убиты. Система замирает и потребляет $0, ожидая, пока юзер в веб-интерфейсе поправит текст транскрипции. Как только он жмет "Approve", Оркестратор понимает: "Пора тратить деньги", и поднимает новый дроплет для следующего этапа.

Упрощенная state machine
Упрощенная state machine

Почему упростил AI-логику

Vibecoding и усложнение логики

Все с самого начала и до конца было написано нейронками. Это позволило очень быстро реализовывать идеи и понимать, что они плохие. Параллельно с работой над инфраструктурой и оркестрацией я занимался усложнением самого процесса получения из оригинальных segments.json новых сегментов для склейки финального видео. Я начал активно искать пилотных пользователей, чтобы проверить на каком-то другом контенте, а не своих собственных стримах, как оно работает.

И главной проблемой для меня тогда оказалось то, что рассчет на то, что я могу просто скормить оригинальный segments.json и получить адекватный итог с одного запроса через API просто не получается. Я попробовал разные модельки: Llama 3.1 70B Instruct, Qwen 2.5, Deepseek R1 и еще кучу всего, включая модели open AI. Особенно это было хорошо видно на действительно длинных и насыщенных контентом видео. И я начал пробовать подходить к этому вопросу с разных сторон.

Несколько запросов

Самым очевидным решением было просто отправлять не одним запросом, а разбить на какие-нибудь чанки и делать несколько запросов, потом склеивать ответы с лучшими "кандидатами" и следующим запросом в reasoning модель собирать результат.

Это оказалось рабочей схемой, которая используется в CoreCut по сей день, однако, она не давала решение всех проблем. Качество финальных видео повысилось значительно, но все еще было далеко даже до того, что я получил в самый первый раз. Я пробовал разное, например, в какой-то момент я подумал, что будет круто взять сегменты и сделать из них некую новую сущность windows. Окно(window) это такой сегмент в который включено какое-то(настраиваемое) количество прошло и следующего сегмента. Например, если мы настроили window overlap на 15% это значит, что 15% начала этого окна это конец предыдущего, а 15% конца этого окна - конец следующего.

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

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

video -> ffmpeg -> audio -> whisper -> transcript.json -> convert_transcript(python + bert model) -> transcript_for_segmentation.json -> segmentation(python) -> chapters.json && report.txt -> summarization(python X lexrank) -> summary.txt + clips.json -> LLM -> segments.json -> ffmpeg -> resulting video.

Разворачиваемся. Упрощаемся

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

Я пытался объяснять, что вот, если у вас длинный контент, я могу за несколько минут сделать из него видеосаммари. Такие саммари смотреть легче, чем полноценный вебинар или лекцию. И никто не понимал. При этом, некоторым пользователям, я как бы между делом говорил: "А еще оно в теории может делать шортсы"(оно не делало, но это было СИЛЬНО проще сделать, чем саммари). А они такие: "Шортсы? О, вот это нам надо!".

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

Удаленный из проекта human in the loop редактор
Удаленный из проекта human in the loop редактор

Я выпилил 80% функционала, который сделал, чтобы выйти на рынок. Я убрал эмбеддинги и RAG, оставил окна и несколько запросов. Я внедрил open router чтобы быстрее тестировать разные комбинации запросов к разным моделям нейронок. Я убрал огромную фичу Human-in-the-loop, которая позволяла пользователю вмешиваться в промежуточные этапы работы. Я нещадно резал все, что делал несколько месяцев и за 2 вечера все-таки подготовил первый релиз.

В viral_clips.py вместо 1000 строк кода анализа пара грамотных запрос к контекстному окну модели, который оценивает "хуки", "эмоциональные пики" и "завершенность мысли".

Ирония: Инфраструктура стала "железной" и детерминированной (Redis, States, API), а бизнес-логика стала "мягкой" и вероятностной (AI).

Риски, костыли и границы применимости

Конечно, у такого "гаражного" подхода есть свои лимиты. Я не строю Netflix, поэтому некоторые углы срезаны осознанно.

  1. Что будет, если придет 1000 человек одновременно?
    Оркестратор не имеет жесткого лимита на создание дроплетов (кроме дефолтного лимита DigitalOcean на аккаунт). Если будет пик, я просто упрусь в лимит API провайдера, и задачи встанут в очередь (QUEUED)
    Риск: Пользователи будут ждать долго.
    Решение: Сейчас у меня мало пользователей и решать эту проблему пока смысла нет. Но если вырасту, придется внедрять Pool Manager, который держит пару "горячих" машин в резерве.

  2. Идемпотентность и "Гонки" (Race Conditions)
    Самый страшный сон - когда задача упала по таймауту, Оркестратор решил её перезапустить, а старый зомби-воркер вдруг ожил и начал писать в ту же папку на S3.
    Решение: У каждой попытки запуска есть свой уникальный executionId внутри jobId. Артефакты пишутся в S3 с префиксом, включающим этот ID. Воркер перед финальной загрузкой результата проверяет через API Оркестратора: "Я все еще главный исполнитель этой задачи?". Если нет он просто умирает, не портя данные.

  3. Хранение терабайтов видео
    Видео весят много. Хранить их на диске Оркестратора нельзя (забьется за час).
    Решение: Все промежуточные файлы (транскрипты, нарезки) и финальные видео летят в DigitalOcean Spaces (S3-compatible). У ссылок настроен Presigned URL с TTL. Сами файлы имеют Lifecycle Policy исходники удаляются через 24 часа, финальные рендеры хранятся 30 дней. Это позволяет не превратить S3 в свалку истории.

  4. Время на "разогрев" (Cold Start)
    Главный минус моей схемы - Cold Start. Пользователь ждет лишние 3-5 минут, пока поднимется машина, скачаются обновления кода и настроится окружение.
    Компромисс: Я продаю эти 5 минут ожидания за $1000 экономии в месяц. Для B2C сервиса "ленивой обработки видео" это приемлемо. Для Real-time процессинга (например, фильтры в прямом эфире) моя архитектура не подошла бы совсем.

Выводы

  1. Экономика определяет архитектуру. Эфимерные воркеры позволили мне снизить косты в 10 раз по сравнению с постоянным сервером.

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

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

  4. Лучшая инфраструктура - та, которой нет. Мы привыкли тащить "взрослые" технологии (K8s, микросервисы) в стартапы с первого дня. Но простой Bash-скрипт и прямые API-вызовы облака сэкономили мне $1000/мес и кучу нервов. Для инди-хакера "Time-to-Market" и низкий "Burn Rate" важнее академической правильности. Когда мое костыльное решение упрется в потолок возможностей, у меня будут деньги на организацию более взрослой инфраструктуры. А сейчас это просто лишняя трата времени и денег

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

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


  1. cry_san
    25.11.2025 07:53

    А можно пример? Было, стало.

    В частности интересует такое - Я пытался объяснять, что вот, если у вас длинный контент, я могу за несколько минут сделать из него видеосаммари. Такие саммари смотреть легче, чем полноценный вебинар или лекцию.


  1. dsrk_dev
    25.11.2025 07:53

    А не думал о том ччтоб держать gpu машину дома? З 1-2k$ вполне можно собрать что-нибудь с той же б\у 3090