Как я в одиночку написал сервис для расшифровки видео — и три раза чуть не забросил из-за одной и той же ошибки
Habr ·

Дисклеймер сразу: я не собирался писать «ещё один стартап». Мне просто было лень руками пересматривать видео, чтобы выписать из него тезисы. Дальше — история о том, как решение личной проблемы обросло LLM-цепочками, защитой от SSRF и вопросами вроде «а что если сертификат не выпустится». Сервис называется postvid.ru : ссылка на видео или файл на входе, расшифровка, тезисы, таблица, презентация и карточки для карусели на выходе. Стек, если коротко - FastAPI — веб-приложение и API, ничего экзотического. - faster-whisper , модель small — расшифровка речи. Брал именно small , потому что сервер без GPU, а medium на CPU превращает ожидание в пытку. - YandexGPT / любой OpenAI-совместимый API — выделение тезисов, с фолбэком на локальную модель через Ollama и на офлайн-алгоритм без нейросети вообще. - Pillow — генерация карточек карусели. - openpyxl, python-pptx — таблица и презентация из тех же тезисов. - SQLite — аккаунты, сессии, подписки, платежи. Да, SQLite, не Postgres — для текущей нагрузки хватает с запасом, а лишнюю инфраструктуру я себе усложнять не стал. - Docker Compose + Caddy — деплой и автовыпуск TLS-сертификатов сразу для нескольких доменов. - ЮKassa — оплата и автопродление. Пайплайн ``` ссылка/файл → проверка SSRF → faster-whisper (текст + таймкоды) → LLM (тезисы + цитаты + время) → {xlsx, pptx, PNG-карусель, srt} ``` Задача обрабатывается в пуле потоков, статус лежит в памяти процесса и отдаётся polling'ом через /api/jobs/{id} . На бумаге — просто. На практике каждый пункт этой схемы принёс свой сюрприз, и вот четыре самых запоминающихся. Грабли №1: облако решило отдохнуть, а сервис — вместе с ним Первая версия дёргала облачную LLM напрямую, без всякой защиты. Всё было отлично, пока однажды ночью модель не легла на пару минут. Для меня это была строчка в логе. Для пользователей в этот момент — «сервис сломан, ничего не работает». Неприятное ощущение, когда узнаёшь об этом постфактум. Сделал цепочку: 1. Облачная модель. 2. Если недоступна — локальная модель через Ollama на том же сервере. 3. Если и её нет — офлайн-алгоритм: рубит текст на предложения и выбирает самые «весомые» по простым эвристикам. Плюс кулдаун: если облако упало, следующая минута задач сразу идёт в запасной вариант, не тратя время на повторный обречённый запрос. На странице результата честно пишется, какой движок сработал — cloud , local или offline . Мне кажется, это важнее любой красивой анимации загрузки: пользователь должен понимать, почему результат вдруг стал немного хуже. Грабли №2: «просто вставь ссылку» — ха Казалось бы, что сложного в том, чтобы скачать видео по ссылке. Сложного оказалось много: нужно отсечь запросы во внутреннюю сеть сервера (привет, SSRF), проверить, что схема — именно http(s), и не пускать всё подряд. ```python def validate_public_url(url: str, allowed_hosts: set[str] | None = None) -> None: parsed = urlparse(url.strip()) if parsed.scheme not in ("http", "https"): raise SourceRejected("Разрешены только http(s) ссылки") # резолвинг хоста, отказ на приватные/ Читать далее
Дисклеймер сразу: я не собирался писать «ещё один стартап». Мне просто было лень руками пересматривать видео, чтобы выписать из него тезисы. Дальше — история о том, как решение личной проблемы обросло LLM-цепочками, защитой от SSRF и вопросами вроде «а что если сертификат не выпустится». Сервис называется postvid.ru : ссылка на видео или файл на входе, расшифровка, тезисы, таблица, презентация и карточки для карусели на выходе. Стек, если коротко - FastAPI — веб-приложение и API, ничего экзотического. - faster-whisper , модель small — расшифровка речи. Брал именно small , потому что сервер без GPU, а medium на CPU превращает ожидание в пытку. - YandexGPT / любой OpenAI-совместимый API — выделение тезисов, с фолбэком на локальную модель через Ollama и на офлайн-алгоритм без нейросети вообще. - Pillow — генерация карточек карусели. - openpyxl, python-pptx — таблица и презентация из тех же тезисов. - SQLite — аккаунты, сессии, подписки, платежи. Да, SQLite, не Postgres — для текущей нагрузки хватает с запасом, а лишнюю инфраструктуру я себе усложнять не стал. - Docker Compose + Caddy — деплой и автовыпуск TLS-сертификатов сразу для нескольких доменов. - ЮKassa — оплата и автопродление. Пайплайн ``` ссылка/файл → проверка SSRF → faster-whisper (текст + таймкоды) → LLM (тезисы + цитаты + время) → {xlsx, pptx, PNG-карусель, srt} ``` Задача обрабатывается в пуле потоков, статус лежит в памяти процесса и отдаётся polling'ом через /api/jobs/{id} . На бумаге — просто. На практике каждый пункт этой схемы принёс свой сюрприз, и вот четыре самых запоминающихся. Грабли №1: облако решило отдохнуть, а сервис — вместе с ним Первая версия дёргала облачную LLM напрямую, без всякой защиты. Всё было отлично, пока однажды ночью модель не легла на пару минут. Для меня это была строчка в логе. Для пользователей в этот момент — «сервис сломан, ничего не работает». Неприятное ощущение, когда узнаёшь об этом постфактум. Сделал цепочку: 1. Облачная модель. 2. Если недоступна — локальная модель через Ollama на том же сервере. 3. Если и её нет — офлайн-алгоритм: рубит текст на предложения и выбирает самые «весомые» по простым эвристикам. Плюс кулдаун: если облако упало, следующая минута задач сразу идёт в запасной вариант, не тратя время на повторный обречённый запрос. На странице результата честно пишется, какой движок сработал — cloud , local или offline . Мне кажется, это важнее любой красивой анимации загрузки: пользователь должен понимать, почему результат вдруг стал немного хуже. Грабли №2: «просто вставь ссылку» — ха Казалось бы, что сложного в том, чтобы скачать видео по ссылке. Сложного оказалось много: нужно отсечь запросы во внутреннюю сеть сервера (привет, SSRF), проверить, что схема — именно http(s), и не пускать всё подряд. ```python def validate_public_url(url: str, allowed_hosts: set[str] | None = None) -> None: parsed = urlparse(url.strip()) if parsed.scheme not in ("http", "https"): raise SourceRejected("Разрешены только http(s) ссылки") # резолвинг хоста, отказ на приватные/ Читать далее