feat: rename Product Strategist to Product Lead, add lead coordination + dual-mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniil
2026-03-22 22:42:35 +03:00
parent 6430ab3eff
commit 27e03cc56c
20 changed files with 6305 additions and 14 deletions
@@ -0,0 +1,984 @@
<!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>