---
name: video-factory-pipeline
description: Полный пайплайн монтажа видео — от сырых клипов до готового ролика с музыкой. FFmpeg, кроссфейды, тайтлы, Suno через kie.ai. Закинул видео → получил результат.
---

# Video Factory Pipeline

Полный конвейер монтажа видео. Берёт сырые клипы из Input/, делает профессиональную нарезку 1 мин, генерирует музыку через Suno (kie.ai), выдаёт готовый MP4.

## Структура проекта

```
~/Desktop/VideoFactory/
├── Input/          # Сюда кидать .mp4/.mov исходники + brief.md
├── Output/         # Готовые ролики
├── Music/          # Скачанная музыка
├── Work/           # Временные файлы (можно удалять)
├── video_factory.py      # Базовый скрипт (конкатенация)
├── edit_montage_v4.py    # Профессиональный монтаж (EDL + музыка)
└── README.txt
```

## Быстрый старт

```bash
# 1. Зависимости: ffmpeg + requests
pip3 install requests
# Если нет brew — скачать static ffmpeg в ~/.local/bin/
curl -L https://evermeet.cx/ffmpeg/ffmpeg-7.1.zip -o /tmp/ffmpeg.zip
cd /tmp && unzip ffmpeg.zip && mv ffmpeg ~/.local/bin/ && chmod +x ~/.local/bin/ffmpeg

# 2. Настроить папки
python3 ~/Desktop/VideoFactory/video_factory.py --setup

# 3. Кинуть видео .mp4 в Input/
# 4. Запустить монтаж
cd ~/Desktop/VideoFactory && python3 edit_montage_v4.py
```

## Профессиональный монтаж (1 мин)

### Процесс
1. **План** — разбить сцены по актам (Partenza → Terra → Mercato → Ritorno)
2. **EDL** — выбрать 10-14 лучших клипов через vision analysis, задать длительность (4-10 сек)
3. **Рендер** — scale+pad до 720×1280 (+fade in/out + drawtext)
4. **Склейка** — concat filter с кроссфейдами (НЕ -c copy — не работает с разными разрешениями)
5. **Музыка** — генерация через kie.ai (Suno V3_5, инструментал)
6. **Микс** — оригинал + музыка (weights=1 0.7)

### План монтажа — формат EDL
```python
EDL = [
    # (filename, duration_sec, title_text, act_label)
    ("clip1.mp4", 5, "Vela",       "⛵ Partenza"),
    ("clip2.mp4", 5, "Mare aperto","⛵ Partenza"),
    ("clip3.mp4", 4, "Stromboli",  "⛵ Partenza"),
    ("clip4.mp4", 4, "Costa",      "🏘️ Terra"),
]
```

### Определение сцен через vision_analyze
Перед монтажом — проанализировать содержимое:
- Извлечь кадр из каждого видео: `ffmpeg -y -ss 00:00:02 -i clip.mp4 -vframes 1 -q:v 2 frame.jpg`
- vision_analyze для категоризации (sailing / town / market / sunset)
- Сгруппировать по датам, построить нарратив

## Ключевые FFmpeg команды (проверены на macOS High Sierra)

**Scale+pad в 720×1280 портрет:**
```bash
ffmpeg -i input.mp4 -vf "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:color=black" output.mp4
```

**Fade in/out + drawtext title (без boxborder_w — нет в старых ffmpeg):**
```bash
ffmpeg -i input.mp4 -vf "fade=t=in:st=0:d=0.3,fade=t=out:st=9.7:d=0.3,drawtext=text='Title':fontsize=28:fontcolor=white:x=(w-text_w)/2:y=h-th-45:box=1:boxcolor=black@0.3" output.mp4
```

**Concat с пережатием (НЕ stream copy):**
```bash
ffmpeg -y -i seg1.mp4 -i seg2.mp4 \
  -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" output.mp4
```

**Общие fade in/out для готового видео:**
```bash
ffmpeg -i input.mp4 -vf "fade=t=in:st=0:d=0.5,fade=t=out:st=58.5:d=1.0" \
  -af "afade=t=in:st=0:d=0.5,afade=t=out:st=58.5:d=1.0" output.mp4
```

**Извлечение кадра для анализа:**
```bash
ffmpeg -y -ss 00:00:02 -i clip.mp4 -vframes 1 -q:v 2 frame.jpg
```

**Получение длительности:**
```bash
ffmpeg -i clip.mp4 2>&1 | grep -E "Duration"
```

**Заметка:** macOS grep НЕ поддерживает `-P` (Perl regex). Использовать `-E` (extended regex) вместо этого.

## Музыка через Suno (kie.ai)

### Архитектура
```
Mac → VPS:157.173.122.51:18999 (veo3-proxy) → api.kie.ai (с Authorization Bearer)
```
Прокси-скрипт: `/opt/hermes-data/veo3-proxy.py` на сервере. Просто добавляет API-ключ и forward-ит.

### API
- **POST** `/api/v1/generate` — создаёт трек
- **Модели (от дешёвых):** V3_5 → V4 → V4_5 → V4_5PLUS → V4_5ALL → V5 → V5_5
- **Использовать V3_5** — самая дешёвая
- **callBackUrl — ОБЯЗАТЕЛЕН** и должен быть доступен с VPS (не localhost!)
- **НЕТ status-check эндпоинта** — никак не проверить статус задачи

### Вызов (V3_5 — дешёвый):
```bash
curl -X POST http://157.173.122.51:18999/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{
    "model": "V3_5",
    "customMode": false,
    "gpt_description_prompt": "acoustic guitar mediterranean travel, relaxing, instrumental",
    "instrumental": true,
    "callBackUrl": "https://n8n.valentinnews.sailor.bar/webhook/suno-callback"
  }'
```

**Ответ:** `{"code":200, "data": {"taskId": "..."}}` — трек создан, жди колбэк.

### Callback handler (системный сервис)
На VPS работает systemd-сервис `suno-callback.service`, слушающий порт 18998.
- **Скрипт**: `/root/suno_callback_v2.py` — обрабатывает `source_stream_audio_url` и `stream_audio_url`
- **Логи**: `journalctl -u suno-callback`
- **Статус**: `systemctl status suno-callback`
- **Перезапуск**: `systemctl restart suno-callback`
- **Выходная директория**: `/root/suno_music/` — MP3 сохраняются автоматически с именем по title трека
- **Callback URL**: `https://n8n.valentinnews.sailor.bar/suno-callback` — проксируется nginx на VPS на порт 18998

### Флоу генерации музыки
1. **Проверить** что callback сервер жив: `systemctl status suno-callback` (или `journalctl -u suno-callback` для логов)
2. **POST** запрос на генерацию с callBackUrl — модель V3_5 для тестов
3. **Ждать ~30-60с** — callback придёт на сервер
4. **Проверить** `/root/suno_music/` — там появится MP3 с именем из title трека
5. **Скопировать на Mac:**
   - ⚠️ Из-за кириллицы и пробелов в названии SCP ломается
   - ✅ **Сначала сделать копию с латинским именем:** `cp /root/suno_music/Оригинал\ имя.mp3 /root/suno_music/simple_name.mp3`
   - ✅ Потом `scp root@VPS:/root/suno_music/simple_name.mp3 /local/path/
6. **Проверить длительность:** `ffmpeg -i track.mp3 2>&1 | grep Duration`
7. **Обновить** путь к музыке в edit_*.py и запустить монтаж

### Проблемы и решения
- ❌ **Кредиты ушли, трека нет** — callback сервер не работал или callBackUrl пустой. Решение: перезапустить сервис и генерировать заново
- ❌ **"tags contain artist name"** — в prompt/style упомянута реальная группа (Rammstein и т.д.). Решение: описывать звук без имён
- ❌ **Английский вокал вместо русского** — Suno игнорирует customPrompt. Решение: добавить "singing in Russian language only, Russian male vocals" в prompt и style
- ❌ **audio_url пустое** — реальные ссылки в `data[i].source_stream_audio_url` (v2 handler обрабатывает это автоматически)
- ✅ **Проверка API**: сделать POST с callBackUrl — если 200 с taskId — API работает

### Ошибки API и их значения
| Ошибка | Причина | Решение |
|--------|---------|---------|
| `401 Unauthorized` | Неверный/просроченный API-ключ | Проверить VEO3_API_KEY в `veo3-proxy.py` на сервере |
| `422 "model error"` | Неверное имя модели | Использовать: V3_5, V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5 |
| `422 "customMode cannot be null"` | Не передан `customMode` | Добавить `"customMode": false` |
| `422 "Please enter callBackUrl"` | Не передан callBackUrl | Обязательное поле (даже тестовый запрос) |
| `200` с taskId | Успех | Трек генерится, жди колбэк |

### Ambient-заглушка (если Suno не работает)
```bash
ffmpeg -f lavfi -i "aevalsrc=sin(55*2*PI*t)*exp(-t/20)+sin(82.5*2*PI*t)*0.3+sin(110*2*PI*t)*0.15:d=60" \
  -af "volume=0.06,lowpass=f=350" ambient.wav
```
Микс с видео:
```bash
ffmpeg -y -i video.mp4 -i ambient.wav \
  -filter_complex "[1:a]afade=t=in:st=0:d=2,afade=t=out:st=58:d=2,volume=0.3[amb];[0:a][amb]amix=inputs=2:duration=first:weights=1 0.7[audio]" \
  -map 0:v -map "[audio]" -c:v copy -c:a aac output.mp4
```

## Анализ энергии трека (для синхронизации с видео)

```python
# Конвертировать в PCM и проанализировать
ffmpeg -y -i track.mp3 -ac 1 -ar 22050 -f s16le -t 120 /tmp/audio.raw

# Python: найти громкие сегменты
import numpy as np
samples = np.fromfile('/tmp/audio.raw', dtype=np.int16)
fs = 22050
energy = np.array([np.sqrt(np.mean(samples[i*fs:(i+1)*fs].astype(float)**2))
                   for i in range(len(samples)//fs)])
# Самые громкие секунды
peak_idx = np.argsort(energy)[-10:][::-1]
print("Peaks at seconds:", peak_idx)
# Группировать в loud сегменты
threshold = np.percentile(energy, 70)
```

## Шаблон нарратива для 30-секундного видео

### "Море → Дом" (семья и друзья)
```
1. 🌊 0-7с   Море, волны      "Нас мотало по морям и океанам"
2. ⛵ 7-15с  Путь, штормы     "Через штормы и штили"
3. ⚓ 15-23с Порт, прибытие   "Порт встречает огнями"
4. ❤️ 23-30с Закат            "Но лучшее место — семья и друзья"
```

### "Рамштайн" (экшн, хард)
```
1. 🌊 0-7с   Шторм, волны     агрессивный гитарный рифф
2. 🌋 7-15с  Вулкан, стихия   ударные, дроп
3. ⚡ 15-45с Экшн-сцены       припев, тяжёлый бридж  
4. 🌅 45-60с Финал, закат     финальный аккорд
```

### Параметры render_clip для нарратива
- **Разрешение**: 720×1280 (portrait)
- **Fade**: 0.3s in/out на клип
- **Текст**: 42px title (white), 22px subtitle (#FFD700 gold)
- **Бокс**: black@0.3-0.4 под текст
- **Шрифт**: Helvetica (/System/Library/Fonts/Helvetica.ttc)

## Музыка с русским вокалом

### Промпт для русских текстов
```json
{
  "model": "V4",
  "customMode": true,
  "prompt": "описание звука + Russian male vocals, singing in Russian language only",
  "style": "Russian Rock, Heavy Metal, ...",
  "customPrompt": "[Verse] Текст на русском / [Chorus] Припев"
}
```

### Важно:
- Suno НЕ всегда точно следует customPrompt — может сгенерировать свой текст
- Чтобы повысить шанс русского вокала: упомянуть "Russian language only" в prompt И style
- Название трека Suno даёт своё (например "Трещины Штормов") — не пугаться
- Если трек на английском — генерировать заново с усиленным указанием языка

```markdown
# Задание
Название: My Video
Длительность: 1:00
Музыка: acoustic guitar mediterranean, relaxing
Формат: 720x1280

# Видео (список файлов из Input/)
clip1.mp4 — описание
clip2.mp4 — описание

# Порядок (хронология)
clip1.mp4, clip2.mp4, clip3.mp4
```

## Питфоллы и советы (собрано на практике)

### FFmpeg
- ❌ `boxborder_w` НЕТ в ffmpeg 7.1 на macOS — просто убрать параметр
- ❌ `-c copy` для concat НЕ РАБОТАЕТ если файлы разного размера/кодека — всегда пережимать через filter_complex
- ❌ drawtext с эмодзи (⛵🏘️🌊) может падать — экранировать или не использовать в drawtext
- ✅ `-preset ultrafast` для черновиков, `fast` для финала
- ✅ `-movflags +faststart` всегда для веб-плейбека
- ✅ `-crf 23` — хороший баланс скорость/качество, `20-22` для финала
- ✅ Удалять `Work/segments*` после сборки (забивают диск — по 50-100 MB на сегмент)

### macOS (High Sierra)
- ❌ Нет Homebrew — ffmpeg ставить static binary в ~/.local/bin/
- ❌ grep без `-P` — использовать `-E`
- ❌ Очень медленный — ultrafast обязателен, иначе 10+ минут на сборку
- ✅ Python 3.12 есть, pip работает

### Процесс
- ❌ `generate_music()` в `video_factory.py` — **НЕ РАБОТАЕТ**. Функция делает polling `/api/v1/suno/get-music-details`, которого нет в API. Кредиты уходят, трек не получить. Использовать только callback-подход.
- ❌ Не слать Suno-запрос без работающего callBackUrl — кредиты сгорят
- ❌ Не использовать чужие Telegram-боты (Арчи) для своих проектов
- ✅ Сначала vision analysis → потом EDL → потом монтаж
- ✅ Хронология по датам файлов (mtime) — лучший порядок для travel-видео

## Сценарий использования

1. Пользователь кидает `*.mp4` в `~/Desktop/VideoFactory/Input/`
2. Запустить vision analysis для всех клипов — понять что где
3. Сгруппировать по сценам (sailing/town/market/sunset)
4. Построить EDL: 4 акта × 3 клипа, ~5 сек каждый
5. Написать/edit_montage_v4.py — заполнить EDL
6. Запустить `python3 edit_montage_v4.py`
7. Если Suno не работает — ambient fallback
8. Готовый ролик в `Output/`

## Короткий формат (30 сек) — семейная тема

Для коротких роликов с нарративом "путь → дом":

### EDL для 30 секунд (4 акта × 2 клипа)
```python
EDL = [
    # ACT 1: МОРЕ (0-7s) — нас мотало по волнам
    ("open_sea.mp4", 0, 4, "🌊 Мотало по волнам", "Нас мотало по морям и океанам"),
    ("coast.mp4", 5, 3, "🌊 По чужим берегам", "По чужим берегам"),
    # ACT 2: ПУТЬ (7-15s) — шли через штормы
    ("storm.mp4", 0, 4, "⛵ Через штормы", "Через штормы и штили"),
    ("approaching.mp4", 2, 4, "🏝️ К берегу", "К родному берегу"),
    # ACT 3: ПОРТ (15-23s) — прибытие, тепло
    ("harbor.mp4", 5, 4, "⚓ Порт", "Порт встречает огнями"),
    ("marina.mp4", 3, 4, "🏡 Причал", "Тёплый причал"),
    # ACT 4: ДОМ/ЗАКАТ (23-30s) — финал, семья
    ("town.mp4", 3, 4, "❤️ Дом", "Но лучшее место —"),
    ("sunset.mp4", 20, 6, "🌅 Семья", "Семья и друзья"),
]
```

### Синхронизация музыки с видео по энергетике
```bash
# Scene detection
ffmpeg -i video.mp4 -filter:v "select='gt(scene,0.04)',showinfo" -f null - 2>&1 | grep "pts_time"
# Energy analysis (Python)
# Convert to raw PCM, measure RMS per second, find peaks
```

### Suno customPrompt для русских текстов
```json
"customPrompt": "[Intro] текст / [Verse] куплет / [Chorus] припев / [Outro] концовка"
```
API поддерживает маркеры: `[Intro]`, `[Verse]`, `[Chorus]`, `[Bridge]`, `[Drop]`, `[Outro]`

## Создание скрипта для монтажа
См. шаблон `templates/edit_family.py` или создай свой EDL по образцу выше.

## ⚠️ Ключевые уроки по монтажу (собрано на практике)

**❌ КОМПЛЕКСНЫЕ ПРАВИЛА АВТОМАТИЗАЦИИ ОТКЛОНЕНЫ.**
Попытки закодировать классические правила монтажа (Пудовкин, Скорсезе, Эдгар Райт, Годар и т.д.) в виде алгоритмов — zoompan/Ken Burns, smart-select по motion, beat-sync, audio ducking — пользователь оценил как «криво монтирует». 

**Вывод: правила монтажа — это принципы ДЛЯ ЧЕЛОВЕКА, не алгоритмы, которые можно закодировать.**

**✅ РАБОЧИЙ ПОДХОД (edit_family.py):**
- Простой EDL: 8 клипов, `(файл, start, dur, title, subtitle)`
- Crossfade 0.3s
- Drawtext (title + subtitle)
- Музыка накладывается сверху `[music]volume=0.35 + [ambient]volume=0.10`
- Никакого zoompan, smart-select, beat-sync
- Никакой автоматической расстановки дропов

**Флоу создания видео:**
1. Пользователь говорит идею/текст
2. Ты пишешь простой скрипт с EDL
3. Показываешь результат
4. Если надо — вручную правишь тайминги/порядок по просьбе пользователя

Не пытаться автоматизировать творчество.

## Шаблон edit_family.py (рабочий, одобрен)

```python
# Простой EDL: (имя_файла, start_сек, длительность, заголовок, субтитр)
EDL = [
    ("document_id1.mp4", 0, 4, "🌊 Мотало по волнам", "Нас мотало по морям и океанам"),
    ("document_id2.mp4", 5, 3, "🌊 По чужим берегам", "По чужим берегам"),
    ("document_id3.mp4", 0, 4, "⛵ Через штормы", "Через штормы и штили"),
    ("document_id4.mp4", 2, 4, "🏝️ К берегу", "К родному берегу"),
    ("document_id5.mp4", 5, 4, "⚓ Порт", "Порт встречает огнями"),
    ("document_id6.mp4", 3, 4, "🏡 Марина", "Тёплый причал"),
    ("document_id7.mp4", 3, 4, "❤️ Дом", "Но лучшее место —"),
    ("document_id8.mp4", 20, 6, "🌅 Семья и друзья", "Семья и друзья"),
]
```

Каждый клип: scale+pad до 720×1280, fade 0.3s in/out, drawtext внизу.
Склейка: concat filter с пережатием (НЕ stream copy!).
Музыка: `[1:a]volume=0.35[music];[0:a]volume=0.10[amb];amix=inputs=2`

## Suno callback handler v2 (systemd)

### Скрипт `/root/suno_callback_v2.py`
- Слушает порт 18998
- Обрабатывает `source_stream_audio_url` (основной) и `stream_audio_url` (запасной) — v1 смотрел только `audio_url` который пустой
- Автоматически сохраняет MP3 в `/root/suno_music/` с названием из `data[i].title`
- В случае ошибки сохраняет JSON с URL для ручного скачивания

### Systemd сервис
```bash
# Статус
systemctl status suno-callback
# Логи
journalctl -u suno-callback
# Рестарт
systemctl restart suno-callback
```

### Файл сервиса `/etc/systemd/system/suno-callback.service`
```ini
[Unit]
Description=Suno Music Callback Handler
After=network.target

[Service]
ExecStart=/usr/bin/python3 /root/suno_callback_v2.py
WorkingDirectory=/root/suno_music
Restart=always
User=root

[Install]
WantedBy=multi-user.target
```

### Известные проблемы
- ❌ `callBackUrl` пустой → трек генерится (кредиты уходят), но результат потерян
- ❌ callback сервер не работал → трек сгенерился, но никто не принял
- ❌ **BrokenPipeError в callback handler** — nginx проксирует запрос от Suno, но закрывает соединение до того как Python успевает ответить. Handler крашится на `send_response()` внутри except-блока. **Фикс:** обернуть send_response в try/except или игнорировать BrokenPipeError при отправке ответа
- ❌ **Nginx таймаут** — nginx может иметь `proxy_read_timeout` меньше чем время скачивания трека. Проверить конфиг nginx на VPS
- ✅ callback URL `https://n8n.valentinnews.sailor.bar/suno-callback` → nginx проксирует на localhost:18998

## Suno prompting — русский вокал и стили

### Промпт для русских текстов
```json
{
  "model": "V3_5",
  "customMode": true,
  "prompt": "описание звука, Russian male vocals, singing in Russian language only",
  "style": "Russian Rock, Power Ballad, ...",
  "customPrompt": "[Verse] Строка 1 / [Chorus] Припев"
}
```

### Поддерживаемые маркеры customPrompt
`[Intro]`, `[Verse]`, `[Chorus]`, `[Bridge]`, `[Drop]`, `[Outro]`

### Важные открытия
- Suno НЕ всегда следует customPrompt — может игнорировать.
- **Чтобы вокал был на русском**: слово "Russian" должно быть в prompt, style И желательно в customPrompt.
- **Не упоминать реальные группы**: "Rammstein" в style → 400 ошибка "contains artist name". Писать "German industrial metal" или "Neue Deutsche Härte".
- **V3_5 — самая дешёвая модель** для тестов (в 2-3 раза дешевле V4/V5).
- **V4/V4_5** — лучше для детальных промптов с русским текстом.
- **Без `customMode: true`** — не работает customPrompt.

## Цены моделей (kie.ai)
- **V3_5** — самая дешёвая, для экспериментов ✅
- **V4** — средняя, хорошее качество
- **V4_5/V4_5PLUS/V4_5ALL** — дороже, лучше следуют промпту
- **V5/V5_5** — топ, но дорого

Для тестов всегда V3_5.

## Создание 30-секундного нарративного видео

### Шаблон текста (семья/море/дом)
```
[Verse]
Нас мотало по морям и океанам,
По чужим портам, по разным странам.
Шторм врывался в наши паруса,
Но в глазах горели небеса.

[Chorus]
Но лучше места, чем с семьёй и друзьями,
Мы не нашли за семью морями.
Где бы мы ни были — помни одно:
Дом там, где сердце, где тепло.

[Verse 2]
Волны били, ветер завывал,
Каждый из нас что-то там искал.
А нашли мы тихий наш причал,
Где никто из нас не отвечал.

[Outro]
Семь морей, семь ветров,
Но один только дом.
Где нас любят и ждут —
Мы его назовём...
```

### Флоу создания
1. Придумать текст на русском → показать пользователю
2. После одобрения — сгененировать через Suno V3_5 (дешёво)
3. Проверить callback: `systemctl status suno-callback`
4. Ждать 30-60с пока придёт MP3 в `/root/suno_music/`
5. Скопировать на Mac (через latin-имя, не кириллицу!)
6. Запустить `montage_v5.py` — он сам проанализирует трек и расставит дропы

## Автоматизация (TODO)
- LLM-анализ содержимого через vision вместо ручного просмотра
- auto-EDL генерация из brief.md
- Telegram-доставка готового ролика (через отдельного бота, НЕ Арчи)
- Sidechain compression для профессионального audio ducking
- Speed ramp — ускорять/замедлять клип под дропы
