Files
remotion_service/docs/consults/video-features-roadmap_v1_ru.html
2026-03-22 22:42:35 +03:00

984 lines
47 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Дорожная карта видеофич — Техническая консультация v1</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--bg: #0f1117;
--bg-card: #161922;
--bg-card-hover: #1c2030;
--border: #2a2f3e;
--text: #e4e6ed;
--text-dim: #8b8fa3;
--text-heading: #f0f2f7;
--accent: #6c5ce7;
--accent-light: #a29bfe;
--accent-bg: rgba(108, 92, 231, 0.1);
--green: #00cec9;
--green-bg: rgba(0, 206, 201, 0.1);
--yellow: #fdcb6e;
--yellow-bg: rgba(253, 203, 110, 0.1);
--red: #ff6b6b;
--red-bg: rgba(255, 107, 107, 0.1);
--blue: #74b9ff;
--blue-bg: rgba(116, 185, 255, 0.1);
--orange: #e17055;
--orange-bg: rgba(225, 112, 85, 0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
font-size: 15px;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: 60px 32px 120px;
}
/* Header */
.hero {
text-align: center;
margin-bottom: 72px;
padding: 64px 0;
position: relative;
}
.hero::before {
content: '';
position: absolute;
top: -60px;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(108,92,231,0.12) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
.hero * { position: relative; z-index: 1; }
.hero h1 {
font-size: 2.6rem;
font-weight: 800;
color: var(--text-heading);
letter-spacing: -0.03em;
margin-bottom: 16px;
background: linear-gradient(135deg, var(--text-heading) 0%, var(--accent-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero .meta {
color: var(--text-dim);
font-size: 0.9rem;
line-height: 1.8;
}
.hero .meta strong { color: var(--text); font-weight: 500; }
/* Sections */
h2 {
font-size: 1.6rem;
font-weight: 700;
color: var(--text-heading);
margin: 64px 0 24px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border);
letter-spacing: -0.02em;
}
h3 {
font-size: 1.15rem;
font-weight: 600;
color: var(--accent-light);
margin: 32px 0 12px;
}
p { margin: 12px 0; color: var(--text); }
p.dim { color: var(--text-dim); font-size: 0.9rem; }
strong { font-weight: 600; color: var(--text-heading); }
em { font-style: italic; color: var(--yellow); }
/* Feature cards */
.feature-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 36px;
margin: 24px 0;
transition: border-color 0.2s;
}
.feature-card:hover { border-color: var(--accent); }
.feature-card h2 {
margin-top: 0;
border: none;
padding: 0;
display: flex;
align-items: center;
gap: 12px;
}
.feature-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 700;
flex-shrink: 0;
}
.feature-num.f1 { background: var(--green-bg); color: var(--green); }
.feature-num.f2 { background: var(--blue-bg); color: var(--blue); }
.feature-num.f3 { background: var(--red-bg); color: var(--red); }
.feature-num.f4 { background: var(--orange-bg); color: var(--orange); }
/* Tags */
.tag {
display: inline-block;
padding: 3px 10px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.tag.easy { background: var(--green-bg); color: var(--green); }
.tag.medium { background: var(--yellow-bg); color: var(--yellow); }
.tag.hard { background: var(--red-bg); color: var(--red); }
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
background: var(--green-bg);
color: var(--green);
margin-bottom: 12px;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 0.88rem;
}
thead th {
background: rgba(108, 92, 231, 0.08);
color: var(--accent-light);
font-weight: 600;
text-align: left;
padding: 12px 16px;
border-bottom: 2px solid var(--border);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
tbody td {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
color: var(--text);
}
tbody tr:hover { background: var(--bg-card-hover); }
tbody tr:last-child td { border-bottom: none; }
.table-wrap {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
margin: 16px 0;
}
.table-wrap table { margin: 0; }
/* Code */
code {
font-family: 'JetBrains Mono', monospace;
background: rgba(108, 92, 231, 0.1);
color: var(--accent-light);
padding: 2px 7px;
border-radius: 5px;
font-size: 0.85em;
}
pre {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
overflow-x: auto;
margin: 16px 0;
font-size: 0.85rem;
line-height: 1.6;
}
pre code {
background: none;
padding: 0;
color: var(--text);
}
.keyword { color: var(--accent-light); }
.type-name { color: var(--green); }
.string-val { color: var(--yellow); }
.comment { color: var(--text-dim); }
/* Lists */
ul, ol {
margin: 12px 0;
padding-left: 24px;
}
li {
margin: 6px 0;
color: var(--text);
}
li::marker { color: var(--accent-light); }
/* Callout */
.callout {
border-left: 3px solid;
padding: 16px 20px;
margin: 20px 0;
border-radius: 0 10px 10px 0;
font-size: 0.93rem;
}
.callout.highlight {
border-color: var(--accent);
background: var(--accent-bg);
}
.callout.warning {
border-color: var(--yellow);
background: var(--yellow-bg);
}
.callout.danger {
border-color: var(--red);
background: var(--red-bg);
}
.callout.info {
border-color: var(--blue);
background: var(--blue-bg);
}
.callout.success {
border-color: var(--green);
background: var(--green-bg);
}
/* Overview grid */
.overview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin: 24px 0;
}
.overview-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
}
.overview-item .label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
margin-bottom: 4px;
}
.overview-item .value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-heading);
}
.overview-item .value.accent { color: var(--accent-light); }
.overview-item .value.green { color: var(--green); }
.overview-item .value.yellow { color: var(--yellow); }
/* Timeline */
.timeline {
margin: 24px 0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
}
.timeline-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.timeline-label {
width: 200px;
text-align: right;
color: var(--text-dim);
flex-shrink: 0;
}
.timeline-bar {
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
padding: 0 12px;
font-weight: 500;
font-size: 0.75rem;
color: #fff;
white-space: nowrap;
}
/* Section divider */
.divider {
height: 1px;
background: linear-gradient(to right, transparent, var(--border), transparent);
margin: 48px 0;
}
/* Risks */
.risk-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.risk-item:last-child { border-bottom: none; }
.risk-icon {
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
flex-shrink: 0;
margin-top: 2px;
background: var(--yellow-bg);
color: var(--yellow);
}
/* MVP comparison */
.mvp-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 20px 0;
}
.mvp-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
.mvp-box h4 {
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 10px;
}
.mvp-box.mvp h4 { color: var(--green); }
.mvp-box.full h4 { color: var(--accent-light); }
.mvp-box p { font-size: 0.88rem; margin: 0; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* Print */
@media print {
body { background: #fff; color: #1a1a2e; }
.feature-card, .table-wrap, pre, .callout, .overview-item, .mvp-box {
background: #f8f9fa;
border-color: #dee2e6;
}
}
@media (max-width: 640px) {
.container { padding: 32px 16px 80px; }
.hero h1 { font-size: 1.8rem; }
.overview-grid { grid-template-columns: 1fr; }
.mvp-compare { grid-template-columns: 1fr; }
.timeline-label { width: 120px; font-size: 0.7rem; }
}
</style>
</head>
<body>
<div class="container">
<!-- Hero -->
<div class="hero">
<h1>Дорожная карта видеофич</h1>
<p class="meta">
Техническая консультация v1<br>
<strong>22 марта 2026</strong><br><br>
ML/AI-инженер &middot; Backend-архитектор &middot; Remotion-инженер<br>
Frontend-архитектор &middot; DevOps-инженер &middot; Инженер по производительности
</p>
</div>
<!-- Overview -->
<h2>Общая картина</h2>
<div class="overview-grid">
<div class="overview-item">
<div class="label">Всего фич</div>
<div class="value accent">4</div>
</div>
<div class="overview-item">
<div class="label">MVP все фичи</div>
<div class="value green">2634 дня</div>
</div>
<div class="overview-item">
<div class="label">Полные версии</div>
<div class="value yellow">4465 дней</div>
</div>
<div class="overview-item">
<div class="label">Один разработчик</div>
<div class="value">68 недель</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>#</th>
<th>Фича</th>
<th>Сложность</th>
<th>MVP</th>
<th>Полная версия</th>
<th>Доп. инфраструктура</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="feature-num f1" style="width:28px;height:28px;font-size:0.8rem;">1</span></td>
<td><strong>Продвинутые шаблоны Remotion</strong></td>
<td><span class="tag easy">Легко</span></td>
<td>34 дня</td>
<td>34 дня</td>
<td style="color:var(--green)">Ничего</td>
</tr>
<tr>
<td><span class="feature-num f2" style="width:28px;height:28px;font-size:0.8rem;">2</span></td>
<td><strong>Детекция вирусных моментов</strong></td>
<td><span class="tag medium">Средне</span></td>
<td>57 дней</td>
<td>812 дней</td>
<td>API-ключ для LLM</td>
</tr>
<tr>
<td><span class="feature-num f3" style="width:28px;height:28px;font-size:0.8rem;">3</span></td>
<td><strong>Авто-монтаж и трекинг лица</strong></td>
<td><span class="tag hard">Сложно</span></td>
<td>1215 дней</td>
<td>3045 дней</td>
<td>Фаза 2: GPU-воркер</td>
</tr>
<tr>
<td><span class="feature-num f4" style="width:28px;height:28px;font-size:0.8rem;">4</span></td>
<td><strong>Конвертация в Shorts (9:16)</strong></td>
<td><span class="tag medium">Средне</span></td>
<td>68 дней</td>
<td>+34 дня</td>
<td style="color:var(--green)">Ничего</td>
</tr>
</tbody>
</table>
</div>
<p class="dim">Реалистичный прогноз для одного разработчика: <strong>68 недель</strong> (все MVP) или <strong>34 месяца</strong> (полные версии).</p>
<div class="divider"></div>
<!-- Feature 1 -->
<div class="feature-card">
<h2><span class="feature-num f1">1</span> Продвинутые шаблоны Remotion</h2>
<span class="status-badge">Спецификация и план готовы</span>
<p><strong>Что делаем:</strong> Расширяем <code>CaptionStyleSchema</code> четырьмя новыми стилями подсветки слов (<code>pop_in</code>, <code>karaoke</code>, <code>bounce</code>, <code>glow_pulse</code>), двумя переходами (<code>zoom_in</code>, <code>drop_in</code>), тремя полями (<code>word_entrance</code>, <code>highlight_rotation_deg</code>, <code>text_transform</code>). Добавляем два системных пресета: &laquo;Shorts&raquo; и &laquo;Podcast&raquo;.</p>
<p><strong>Где трогаем код:</strong> Расширение схемы в Remotion + бэкенде, логика рендеринга в <code>Captions.tsx</code>, Alembic-миграция для пресетов, контролы в StyleEditor на фронте.</p>
<div class="callout success">
Особый интерес специалистов не требуется — всё спроектировано, новой инфраструктуры нет. Самая безрисковая фича в этом списке.
</div>
<p class="dim">
Спецификация: <code>docs/superpowers/specs/2026-03-21-advanced-remotion-templates-design.md</code><br>
План: <code>docs/superpowers/plans/2026-03-21-advanced-remotion-templates.md</code>
</p>
</div>
<!-- Feature 2 -->
<div class="feature-card">
<h2><span class="feature-num f2">2</span> Детекция вирусных моментов</h2>
<div class="callout highlight">
За <strong>$0.005 за видео</strong> мы можем автоматически находить самые цепляющие фрагменты в подкастах и интервью. Пять копеек — и AI выкладывает тебе на блюдце моменты, которые зрители пересылают друг другу.
</div>
<h3>Архитектура</h3>
<p><strong>LLM API:</strong> Gemini 2.5 Flash — лучшая поддержка русского языка, $0.15/$0.60 за 1М токенов. Альтернатива: GPT-4o-mini. Стоимость анализа одного 30-минутного видео: ~$0.005.</p>
<p><strong>Аудио-подкрепление:</strong> <code>librosa</code> для кривых RMS-энергии — уточняет границы клипов до естественных пауз, повышает скор для энергичных сегментов. ~20МБ, обработка 30 мин аудио &lt;10 секунд.</p>
<h3>Пайплайн</h3>
<ol>
<li>Берём транскрипцию из БД</li>
<li><code>librosa</code> считает огибающую энергии (разрешение 100мс)</li>
<li>LLM анализирует текст через промпт со structured JSON output</li>
<li>Постобработка: привязка границ к точкам низкой энергии, расчёт energy-скоров</li>
<li>Сохраняем клипы в новую таблицу <code>clips</code></li>
</ol>
<h3>Бэкенд</h3>
<p><strong>Новый модуль:</strong> <code>clips</code> — хранит найденные клипы со связями project / file / job.</p>
<p><strong>Модель клипа:</strong></p>
<pre><code><span class="type-name">Clip</span> {
project_id: <span class="type-name">UUID</span> (FK projects)
source_file_id: <span class="type-name">UUID</span> (FK files)
job_id: <span class="type-name">UUID?</span> (FK jobs)
title: <span class="type-name">str</span>
start_ms: <span class="type-name">int</span>
end_ms: <span class="type-name">int</span>
score: <span class="type-name">float</span>
source_type: <span class="string-val">"viral_detected"</span> | <span class="string-val">"user_created"</span> | <span class="string-val">"auto_generated"</span>
status: <span class="string-val">"pending"</span> | <span class="string-val">"approved"</span> | <span class="string-val">"rejected"</span> | <span class="string-val">"exported"</span>
meta: <span class="type-name">JSON?</span> <span class="comment">(рассуждения LLM, теги, хэштеги)</span>
}</code></pre>
<p><strong>Новый тип джоба:</strong> <code>VIRAL_DETECT</code> в <code>JobTypeEnum</code>. Актор вызывает LLM API через <code>httpx</code> из Dramatiq-воркера.</p>
<h3>Фронтенд</h3>
<ul>
<li>Новый <code>ViralClipsStep</code> в визарде проекта</li>
<li>Список клипов с превьюшками, скорами, кнопками принять/отклонить</li>
<li>Модалка редактирования клипа с видео-превью</li>
<li>Новый тип джоба <code>VIRAL_DETECT</code> в обработке нотификаций</li>
</ul>
<h3>Ключевые цифры</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Метрика</th><th>Значение</th></tr></thead>
<tbody>
<tr><td>Точность (precision)</td><td>5070%</td></tr>
<tr><td>Полнота (recall)</td><td>6080%</td></tr>
<tr><td>Время обработки</td><td>1020 секунд</td></tr>
<tr><td>Стоимость за видео</td><td style="color:var(--green)">~$0.005</td></tr>
<tr><td>1 000 видео/месяц</td><td style="color:var(--green)">~$5</td></tr>
<tr><td>Новые зависимости</td><td>~30 МБ</td></tr>
</tbody>
</table>
</div>
<div class="callout info">
10–20 секунд и пять долларов за тысячу видео. Вдумайтесь в эти цифры.
</div>
<h3>Риски</h3>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Качество промпт-инжиниринга</strong> определяет ценность фичи — придётся итерировать по фидбеку</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Визуальные моменты</strong> (мимика, физическая комедия) из текста не ловятся — ~20–30% проходят мимо</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Качество транскрипции критично</strong> — Whisper <code>tiny</code> даёт ~25% WER; для вирусной детекции минимум <code>small</code></div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>LLM галлюцинирует таймстемпы</strong> — обязательно валидировать метки времени</div></div>
<h3>MVP vs Полная версия</h3>
<div class="mvp-compare">
<div class="mvp-box mvp">
<h4>MVP (57 дней)</h4>
<p>Только текстовый анализ через LLM, без аудио-энергии. Возвращает клипы со скорами. Пользователь ревьюит и принимает/отклоняет.</p>
</div>
<div class="mvp-box full">
<h4>Полная (812 дней)</h4>
<p>Добавляем librosa-анализ энергии, few-shot примеры из принятых клипов, пакетную обработку, прямой экспорт в 9:16.</p>
</div>
</div>
</div>
<!-- Feature 3 -->
<div class="feature-card">
<h2><span class="feature-num f3">3</span> Авто-монтаж и трекинг лица</h2>
<div class="callout danger">
Самая амбициозная фича. Самая сложная. Загружаете подкаст с двумя спикерами — на выходе динамичное вертикальное видео, где камера сама «следит» за говорящим.
</div>
<h3>Архитектура</h3>
<p><strong>Детекция лиц:</strong> MediaPipe BlazeFace (Apache 2.0, ~2МБ модель, 3060 FPS на CPU). Сэмплируем на 3 FPS. Зависимость: <code>mediapipe</code> (~30МБ).</p>
<p><strong>Диаризация спикеров:</strong> pyannote.audio 3.1 (MIT, ~10% DER, self-hosted). CPU: 0.170.33x реального времени. GPU: 1–2 мин на 30 мин аудио. Зависимости: <code>pyannote-audio</code> (~200МБ) + <code>torchaudio</code> (~5080МБ).</p>
<p><strong>Маппинг лицо-спикер:</strong></p>
<ul>
<li><strong>Фаза 1:</strong> Эвристика по временной корреляции. 70–85% точности для двух спикеров. ~100 строк Python.</li>
<li><strong>Фаза 2:</strong> TalkNet-ASD — анализ губ + аудио. 92.3% точности. Нужен GPU.</li>
</ul>
<h3>Видео-композитинг (Remotion)</h3>
<p>Динамический кроп через CSS <code>transform: scale() translate()</code> на <code>&lt;Video&gt;</code> внутри контейнера с <code>overflow: hidden</code>. GPU-ускоренная браузерная операция — бесплатная по производительности.</p>
<h3>Новые Remotion-композиции</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Композиция</th><th>Назначение</th><th>Фаза</th></tr></thead>
<tbody>
<tr><td><code>CaptionedVideo</code></td><td>Наложение субтитров (существует)</td><td>Текущая</td></tr>
<tr><td><code>ShortsVideo</code></td><td>Статический кроп + субтитры в 9:16</td><td>Фича 4</td></tr>
<tr><td><code>AutoEditVideo</code></td><td>Кроп с трекингом лица + монтаж + субтитры</td><td>Фича 3</td></tr>
</tbody>
</table>
</div>
<h3>Формат данных кропа</h3>
<pre><code><span class="keyword">type</span> <span class="type-name">FaceKeyframe</span> = {
time: <span class="type-name">number</span>; <span class="comment">// секунды</span>
x: <span class="type-name">number</span>; <span class="comment">// центр лица, 0.0–1.0</span>
y: <span class="type-name">number</span>; <span class="comment">// центр лица, 0.0–1.0</span>
width: <span class="type-name">number</span>; <span class="comment">// ширина bbox, 0.01.0</span>
height: <span class="type-name">number</span>; <span class="comment">// высота bbox, 0.01.0</span>
speakerId?: <span class="type-name">string</span>;
};
<span class="keyword">type</span> <span class="type-name">CropTrack</span> = {
keyframes: <span class="type-name">FaceKeyframe</span>[];
interpolation: <span class="string-val">"linear"</span> | <span class="string-val">"ease"</span> | <span class="string-val">"smooth"</span>;
zoom: <span class="type-name">number</span>; <span class="comment">// базовый множитель зума</span>
safeMargin: <span class="type-name">number</span>; <span class="comment">// отступ вокруг лица (0.1 = 10%)</span>
};</code></pre>
<h3>Бэкенд</h3>
<p><strong>Новые типы джобов:</strong> <code>FACE_DETECT</code>, <code>SPEAKER_DIARIZE</code>. Результаты хранятся в <code>Job.output_data</code> (JSON).</p>
<p><strong>Отделение ML-сервиса:</strong></p>
<ul>
<li><strong>Фаза 1:</strong> В Dramatiq-воркерах. MediaPipe + pyannote добавляют ~280МБ к образу.</li>
<li><strong>Фаза 2:</strong> Отдельный контейнер <code>ml-worker</code> на выделенных очередях Dramatiq.</li>
</ul>
<h3>Время обработки (30-мин 1080p видео)</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Шаг</th><th>CPU</th><th>GPU</th></tr></thead>
<tbody>
<tr><td>Извлечение аудио (FFmpeg)</td><td>1020 сек</td><td>1020 сек</td></tr>
<tr><td>Детекция лиц (MediaPipe, 3 FPS)</td><td>12 мин</td><td>1015 сек</td></tr>
<tr><td>Диаризация спикеров (pyannote)</td><td style="color:var(--red);font-weight:600">1530 мин</td><td style="color:var(--green)">12 мин</td></tr>
<tr><td>Маппинг лицо-спикер</td><td>&lt; 1 сек</td><td>&lt; 1 сек</td></tr>
<tr><td>Рендер Remotion</td><td>1030 мин</td><td>1030 мин</td></tr>
<tr><td><strong>Итого</strong></td><td><strong>3580 мин</strong></td><td><strong style="color:var(--green)">1640 мин</strong></td></tr>
</tbody>
</table>
</div>
<h3>Требования к памяти</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Конфигурация</th><th>Пиковое потребление RAM</th></tr></thead>
<tbody>
<tr><td>Whisper base + pyannote (параллельно)</td><td>812 ГБ</td></tr>
<tr><td>Whisper medium + pyannote (параллельно)</td><td>1216 ГБ</td></tr>
<tr><td>Рекомендуемый лимит ML-воркера</td><td style="color:var(--yellow)">16 ГБ, <code>--threads 1</code></td></tr>
</tbody>
</table>
</div>
<h3>Фронтенд</h3>
<ul>
<li>Превью трекинга лица: видеоплеер с наложением bounding box через canvas</li>
<li>Трек спикеров в TimelinePanel</li>
<li>Контролы: слайдер зума, скорость перехода, выбор спикера</li>
<li>Переключатель &laquo;до/после&raquo;</li>
</ul>
<h3>Ключевые цифры</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Метрика</th><th>Значение</th></tr></thead>
<tbody>
<tr><td>Точность детекции лиц</td><td>~90%</td></tr>
<tr><td>DER диаризации</td><td>~10%</td></tr>
<tr><td>Маппинг Фаза 1</td><td>7085%</td></tr>
<tr><td>Маппинг Фаза 2 (TalkNet)</td><td style="color:var(--green)">~92%</td></tr>
<tr><td>Новые зависимости</td><td>~280 МБ</td></tr>
<tr><td>GPU обязателен?</td><td>Нет для Фазы 1</td></tr>
</tbody>
</table>
</div>
<h3>Риски</h3>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Маппинг лицо-спикер</strong> — каждое пятое назначение может быть неверным. Нужна ручная корректировка.</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Диаризация на CPU</strong> — бутылочное горлышко. 15–30 мин на 30-мин видео.</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Конфликты PyTorch</strong> между Whisper и pyannote.</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Потеря качества</strong> при кропе 16:9 → 9:16 — остаётся ~31.6% ширины. Минимум 1080p.</div></div>
<div class="risk-item"><div class="risk-icon">!</div><div><strong>Скачивание моделей</strong> pyannote (~100МБ) требует принятия лицензии HF. Обрабатывать в Dockerfile.</div></div>
<h3>MVP vs Полная версия</h3>
<div class="mvp-compare">
<div class="mvp-box mvp">
<h4>MVP (1215 дней)</h4>
<p>Детекция лиц. Пользователь выбирает лицо вручную. Статический кроп. Без диаризации. Один спикер.</p>
</div>
<div class="mvp-box full">
<h4>Полная (3045 дней)</h4>
<p>Диаризация + маппинг. Динамический кроп за активным спикером. Spring()-переходы. Сплит-скрин. Несколько спикеров.</p>
</div>
</div>
</div>
<!-- Feature 4 -->
<div class="feature-card">
<h2><span class="feature-num f4">4</span> Конвертация в вертикальные Shorts (9:16)</h2>
<h3>Архитектура</h3>
<p>Сначала кроп, потом субтитры — всегда. Один проход рендеринга через новую композицию <code>ShortsVideo</code>.</p>
<p><strong>Спецификация кропа:</strong></p>
<pre><code><span class="keyword">type</span> <span class="type-name">CropConfig</span> = {
mode: <span class="string-val">"static"</span> | <span class="string-val">"keyframe"</span>;
staticCrop?: { x: <span class="type-name">number</span>; y: <span class="type-name">number</span>; zoom: <span class="type-name">number</span> };
keyframes?: <span class="type-name">Array</span>&lt;{ time: <span class="type-name">number</span>; x: <span class="type-name">number</span>; y: <span class="type-name">number</span>; zoom: <span class="type-name">number</span> }&gt;;
interpolation?: <span class="string-val">"linear"</span> | <span class="string-val">"ease"</span> | <span class="string-val">"smooth"</span>;
};</code></pre>
<h3>Бэкенд</h3>
<ul>
<li><strong>Новый тип джоба:</strong> <code>ASPECT_CONVERT</code></li>
<li><strong>Новый тип артефакта:</strong> <code>VERTICAL_VIDEO</code></li>
<li>Функция <code>crop_to_vertical()</code> в <code>media/service.py</code></li>
</ul>
<h3>Фронтенд</h3>
<ul>
<li>Превью кропа: перетаскиваемый прямоугольник 9:16 поверх видеоплеера</li>
<li>Side-by-side: оригинал 16:9 vs обрезанное 9:16</li>
<li>Интеграция с Фичей 2: кнопка &laquo;Конвертировать в Short&raquo; на каждом клипе</li>
<li>Интеграция с Фичей 3: авто-кроп из данных детекции лица</li>
</ul>
<h3>Время обработки</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Подход</th><th>30-мин видео</th></tr></thead>
<tbody>
<tr><td>FFmpeg кроп (без субтитров)</td><td>1236 мин</td></tr>
<tr><td>Remotion кроп + субтитры</td><td>1145 мин</td></tr>
<tr><td>FFmpeg с NVENC</td><td style="color:var(--green);font-weight:600">35 мин</td></tr>
</tbody>
</table>
</div>
<h3>MVP vs Полная версия</h3>
<div class="mvp-compare">
<div class="mvp-box mvp">
<h4>MVP (68 дней)</h4>
<p>Ручной выбор кропа. Перетаскиваемый прямоугольник. <code>ShortsVideo</code> рендерит кроп + субтитры.</p>
</div>
<div class="mvp-box full">
<h4>Полная (+34 дня)</h4>
<p>Авто-кроп из трекинга лица. Конвертация в один клик. Пакетная обработка.</p>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Timeline -->
<h2>Рекомендуемый порядок разработки</h2>
<div class="timeline">
<div class="timeline-row">
<div class="timeline-label">Неделя 12</div>
<div class="timeline-bar" style="width:15%;background:var(--green);">Шаблоны</div>
</div>
<div class="timeline-row">
<div class="timeline-label">Неделя 24</div>
<div class="timeline-bar" style="width:30%;background:var(--blue);">Вирусная детекция</div>
</div>
<div class="timeline-row">
<div class="timeline-label">Неделя 46</div>
<div class="timeline-bar" style="width:30%;background:var(--orange);">9:16 кроп MVP</div>
</div>
<div class="timeline-row">
<div class="timeline-label">Неделя 614</div>
<div class="timeline-bar" style="width:80%;background:linear-gradient(90deg, var(--red), #c0392b);">Трекинг лица</div>
</div>
<div class="timeline-row">
<div class="timeline-label">Неделя 1415</div>
<div class="timeline-bar" style="width:15%;background:var(--orange);">9:16 апгрейд</div>
</div>
</div>
<h3>Почему именно так</h3>
<ol>
<li><strong>Шаблоны первыми</strong> — готовы к реализации, нулевой риск, моментальная польза</li>
<li><strong>Вирусная детекция второй</strong> — лучшее соотношение пользы к трудозатратам ($0.005/видео)</li>
<li><strong>9:16 MVP третьим</strong> — создаёт <code>ShortsVideo</code>, которую расширит Фича 3</li>
<li><strong>Трекинг лица последним</strong> — самая сложная; к этому моменту спрос уже валидирован</li>
<li><strong>Апгрейд 9:16</strong> — тривиален, когда трекинг лица уже даёт позиции</li>
</ol>
<div class="divider"></div>
<!-- Cost Analysis -->
<h2>Анализ стоимости</h2>
<h3>Стоимость обработки одного видео</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Уровень</th><th>Состав</th><th>Вычисления</th><th>LLM API</th><th>Итого</th><th>Время</th></tr></thead>
<tbody>
<tr><td>Только CPU</td><td>Всё на CPU</td><td>$0.05</td><td>$0.06</td><td style="color:var(--green);font-weight:700">$0.11</td><td>3580 мин</td></tr>
<tr><td>GPU (T4)</td><td>ML на GPU</td><td>$0.11</td><td>$0.06</td><td>$0.17</td><td>1640 мин</td></tr>
<tr><td>GPU + NVENC</td><td>Всё на GPU</td><td>$0.13</td><td>$0.06</td><td>$0.19</td><td style="color:var(--green)">1015 мин</td></tr>
</tbody>
</table>
</div>
<div class="callout highlight">
Одиннадцать центов на CPU. Девятнадцать с GPU. Меньше двадцати центов за полный пайплайн с AI-анализом, трекингом лица и кодированием видео.
</div>
<h3>Месячная стоимость инфраструктуры (100 видео/мес)</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Сценарий</th><th>Стоимость</th></tr></thead>
<tbody>
<tr><td>Только CPU (текущая инфра)</td><td>~$11 + сервер</td></tr>
<tr><td>Modal serverless GPU</td><td>~$21/мес</td></tr>
<tr><td>Spot GPU (g4dn.xlarge)</td><td>~$115/мес</td></tr>
<tr><td>Постоянный GPU</td><td>~$380/мес</td></tr>
</tbody>
</table>
</div>
<div class="callout info">
<strong>Рекомендация:</strong> Начинаем на CPU. Переходим на Modal serverless GPU, когда время ожидания в очереди превышает 15 минут. При 500+ видео/день — смотрим на spot-инстансы.
</div>
<h3>Предлагаемые тарифы SaaS</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Тариф</th><th>Цена</th><th>Ограничения</th><th>Себестоимость</th><th>Маржа</th></tr></thead>
<tbody>
<tr><td><strong>Free</strong></td><td>$0</td><td>Видео до 10 мин, низкий приоритет</td><td>~$0.04/видео</td><td style="color:var(--text-dim)">Маркетинг</td></tr>
<tr><td><strong>Pro</strong></td><td>$1530/мес</td><td>Видео до 30 мин, GPU ML</td><td>~$0.17 при 50 видео</td><td style="color:var(--green)">6080%</td></tr>
<tr><td><strong>Business</strong></td><td>$50100/мес</td><td>Видео до 60 мин, приоритет, NVENC</td><td>~$0.38/видео</td><td style="color:var(--green)">7085%</td></tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<!-- Infrastructure -->
<h2>Инфраструктурные решения</h2>
<h3>Отделение ML-сервиса</h3>
<div class="mvp-compare">
<div class="mvp-box mvp">
<h4>Фаза 1</h4>
<p>ML в Dramatiq-воркерах. MediaPipe + pyannote добавляют ~280МБ. PyTorch уже установлен через Whisper.</p>
</div>
<div class="mvp-box full">
<h4>Фаза 2</h4>
<p>Отдельный <code>ml-worker</code> контейнер. Тот же код, другой образ (<code>Dockerfile.ml</code>), другие лимиты ресурсов.</p>
</div>
</div>
<pre><code>docker-compose up <span class="comment"># По умолчанию: без ML-воркера</span>
docker-compose --profile ml up <span class="comment"># С ML-воркером</span></code></pre>
<div class="callout warning">
<strong>НЕ строить отдельный HTTP-микросервис.</strong> Dramatiq уже обеспечивает очередь джобов, ретраи, прогресс и отмену. HTTP service discovery — оверхед с нулевой пользой для асинхронных нагрузок.
</div>
<h3>Немедленные оптимизации</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Действие</th><th>Эффект</th><th>Трудозатраты</th></tr></thead>
<tbody>
<tr><td>PyTorch на CPU-only индекс</td><td style="color:var(--green)">-800МБ образ</td><td>1 час</td></tr>
<tr><td>Исправить <code>REMOTION_SERVICE_URL</code></td><td>Баг-фикс</td><td>5 мин</td></tr>
<tr><td>Лимиты ресурсов docker-compose</td><td>Предотвращение каскадных OOM</td><td>30 мин</td></tr>
<tr><td>Пулы очередей Dramatiq</td><td>Предотвращение голодания воркеров</td><td>23 часа</td></tr>
</tbody>
</table>
</div>
<p class="dim">Четыре задачи. Суммарно полдня. Экономия: 800МБ, один баг, и страховка от OOM.</p>
<div class="divider"></div>
<!-- Tech Stack Summary -->
<h2>Сводка по технологическому стеку</h2>
<h3>Новые зависимости</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Пакет</th><th>Размер</th><th>Назначение</th><th>Фича</th></tr></thead>
<tbody>
<tr><td><code>google-generativeai</code> / <code>openai</code></td><td>~10 МБ</td><td>LLM API клиент</td><td><span class="feature-num f2" style="width:22px;height:22px;font-size:0.7rem;">2</span></td></tr>
<tr><td><code>librosa</code></td><td>~20 МБ</td><td>Анализ энергии аудио</td><td><span class="feature-num f2" style="width:22px;height:22px;font-size:0.7rem;">2</span></td></tr>
<tr><td><code>mediapipe</code></td><td>~30 МБ</td><td>Детекция лиц</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
<tr><td><code>pyannote-audio</code></td><td>~200 МБ</td><td>Диаризация спикеров</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
<tr><td><code>torchaudio</code></td><td>~5080 МБ</td><td>Обработка аудио</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
<tr><td><strong>Итого</strong></td><td style="color:var(--yellow);font-weight:700">~310340 МБ</td><td></td><td></td></tr>
</tbody>
</table>
</div>
<h3>Новые модули, композиции, типы джобов</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Элемент</th><th>Назначение</th><th>Фича</th></tr></thead>
<tbody>
<tr><td>Модуль <code>clips</code></td><td>CRUD клипов, ревью</td><td><span class="feature-num f2" style="width:22px;height:22px;font-size:0.7rem;">2</span></td></tr>
<tr><td>Композиция <code>ShortsVideo</code></td><td>Статический кроп + субтитры 9:16</td><td><span class="feature-num f4" style="width:22px;height:22px;font-size:0.7rem;">4</span></td></tr>
<tr><td>Композиция <code>AutoEditVideo</code></td><td>Динамический кроп + субтитры</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
<tr><td>Джоб <code>VIRAL_DETECT</code></td><td>LLM-анализ транскрипции</td><td><span class="feature-num f2" style="width:22px;height:22px;font-size:0.7rem;">2</span></td></tr>
<tr><td>Джоб <code>ASPECT_CONVERT</code></td><td>9:16 кроп</td><td><span class="feature-num f4" style="width:22px;height:22px;font-size:0.7rem;">4</span></td></tr>
<tr><td>Джоб <code>FACE_DETECT</code></td><td>Детекция лиц</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
<tr><td>Джоб <code>SPEAKER_DIARIZE</code></td><td>Диаризация</td><td><span class="feature-num f3" style="width:22px;height:22px;font-size:0.7rem;">3</span></td></tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<!-- Cross-cutting Issues -->
<h2>Сквозные проблемы</h2>
<p class="dim">Шесть специалистов — шесть взглядов на одну кодовую базу.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Проблема</th><th>Кто</th><th>Приоритет</th><th>Действие</th></tr></thead>
<tbody>
<tr><td>PyTorch тащит CUDA (+800МБ)</td><td>DevOps</td><td><span class="tag hard" style="font-size:0.65rem;">Высокий</span></td><td>CPU-only PyTorch индекс</td></tr>
<tr><td>Воркер упадёт по OOM на ML-джобах</td><td>Performance</td><td><span class="tag hard" style="font-size:0.65rem;">Высокий</span></td><td>Пулы очередей, <code>--threads 1</code></td></tr>
<tr><td><code>_get_job_status_sync()</code> течёт соединениями</td><td>Performance</td><td><span class="tag hard" style="font-size:0.65rem;">Высокий</span></td><td>Починить до новых акторов</td></tr>
<tr><td>Нет очистки <code>/tmp</code> при OOM</td><td>Performance</td><td><span class="tag medium" style="font-size:0.65rem;">Средний</span></td><td>Периодическая очистка / cron</td></tr>
<tr><td><code>tasks/service.py</code> — 1 674 строки</td><td>Backend</td><td><span class="tag medium" style="font-size:0.65rem;">Средний</span></td><td>Декоратор/контекст-менеджер</td></tr>
<tr><td><code>REMOTION_SERVICE_URL</code> неверный</td><td>DevOps</td><td><span class="tag medium" style="font-size:0.65rem;">Средний</span></td><td>Исправить на <code>http://remotion:3001</code></td></tr>
<tr><td>Нет лимитов ресурсов Docker</td><td>DevOps</td><td><span class="tag medium" style="font-size:0.65rem;">Средний</span></td><td>Добавить memory/CPU лимиты</td></tr>
<tr><td>Whisper в ML-сервис</td><td>Backend</td><td><span class="tag easy" style="font-size:0.65rem;">Низкий</span></td><td>Запланировать при Фазе 2</td></tr>
<tr><td><code>isCurrent</code> в Captions.tsx</td><td>Remotion</td><td><span class="tag easy" style="font-size:0.65rem;">Низкий</span></td><td>Сравнивать по индексу</td></tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<!-- Specialists -->
<h2>Отчёты специалистов</h2>
<p class="dim">Ключевые файлы, которые изучал каждый:</p>
<ul>
<li><strong>ML-инженер:</strong> <code>transcription/service.py</code>, <code>tasks/service.py</code>, <code>pyproject.toml</code></li>
<li><strong>Backend-архитектор:</strong> <code>tasks/service.py</code>, <code>jobs/schemas.py</code>, <code>media/service.py</code>, <code>captions/service.py</code>, <code>docker-compose.yml</code></li>
<li><strong>Remotion-инженер:</strong> <code>Composition.tsx</code>, <code>Captions.tsx</code>, <code>Root.tsx</code>, <code>useCaptions.ts</code>, все типы</li>
<li><strong>Frontend-архитектор:</strong> <code>TimelinePanel/</code>, <code>FragmentsStep/</code>, <code>WizardContext.tsx</code>, <code>notifications/</code></li>
<li><strong>DevOps-инженер:</strong> <code>docker-compose.yml</code>, <code>Dockerfile</code>, <code>pyproject.toml</code>, <code>uv.lock</code></li>
<li><strong>Инженер по производительности:</strong> <code>tasks/service.py</code>, <code>media/service.py</code>, <code>transcription/service.py</code>, <code>docker-compose.yml</code></li>
</ul>
</div>
</body>
</html>