# Мост между планами скилла и композицией HyperFrames

Скилл производит планы во **времени**, HyperFrames рендерит композицию по **кадрам**.
Этот файл описывает, что во что превращается, чтобы не пересчитывать одно и то же
дважды и не разъезжаться на полкадра.

## Что откуда берётся

| В композиции | Из какого файла | Поле |
|---|---|---|
| `data-duration` корня | `timeline.json` | `expected_duration` |
| `data-fps` | канвас | `skill_config.CANVASES[...].fps` |
| Базовые клипы | `timeline.json` | `clips[].global_start`, `global_end`, `source_in`, `speed` |
| Точки склейки | отчёт `render_transitions.py` | `cut_points[].centre` |
| Параметры перехода | `transition_library.json` | `hyperframes.out`, `.in`, `.ease` |
| Реплики звука | отчёт `render_transitions.py` | `sfx_events[]` |
| Блоки титров | `caption_map.json` | `blocks[]` с `word_ids`, `lines`, `font_size` |
| Слова | `timeline.json` | `words[]` |
| Главы | `chapters.json` | `chapters[]` |
| Окна выносок и б-роллов | `retention_plan.json` | `events[]` с `kind`, `at`, `until` |
| Кадрирование по спикеру | `retention_plan.json` | `subject.cx`, `subject.cy`, `subject.confidence` |
| Холодный старт и сдвиг корпуса | `timeline.json` | `cold_open.length`, `cold_open.shift_applied` |

**`retention_plan.json` — это и есть план слоя графики.** События с
`kind: "callout"` помечают моменты, где в кадре произносится число: его нужно
видеть, а не только слышать, и выноска — работа HyperFrames, а не субтитра.
События `kind: "broll"` — окна для перебивок. `subject` даёт измеренный центр
говорящего, поэтому плашку можно ставить относительно него, а не относительно
геометрического центра кадра.

**Производные скаляры таймлайна пересчитываются, а не наследуются.** После
холодного старта `first_word_start`, `last_word_end` и `tail_after_speech`
описывают уже другой файл. Композиция, читающая старые значения, начнёт графику
на полторы секунды раньше речи.

**Важно:** после переходов времена сдвигаются. `xfade` перекрывает клипы, поэтому
композиция обязана читать `cut_points`, а не исходные времена клипов — иначе титры
уедут на суммарную длительность переходов.

## Что делать в HyperFrames, а что в ffmpeg

Разделение не по вкусу, а по тому, что каждый инструмент умеет на самом деле.

| Задача | Где | Почему |
|---|---|---|
| Резка, ретайминг, склейка | ffmpeg | Кадрово точно, дёшево, проверяемо |
| Прожиг субтитров под платформу | ffmpeg + libass | Метрики шрифта, безопасная зона, 25 пресетов |
| Цветокоррекция | ffmpeg | Один проход, измеримый результат |
| Настоящий 3D-поворот, `rotateY` | HyperFrames | `squeeze_card` в ffmpeg — это сжатие кадра, а не поворот |
| Кинетическая типографика, морфинг | HyperFrames | GSAP/Lottie дают то, чего в ASS нет |
| Интерфейсные мокапы, графики, карты | HyperFrames | Это вёрстка, а не видеофильтр |
| Маски по фигуре, сложные reveal | HyperFrames | CSS clip-path выразительнее xfade |

Практический вывод: базовый ряд, речь и титры делает ffmpeg-часть скилла, а
HyperFrames добавляет слой графики поверх готовой базы. Обратный порядок заставляет
браузер рендерить 100 МБ видео покадрово.

## Параметры переходов уже подготовлены

Каждый рецепт в библиотеке несёт поле `hyperframes` с готовыми значениями:

```json
"hyperframes": {
  "out": {"x": [0, "-45%"], "blur": [0, 24], "rotate": [0, 1.5]},
  "in":  {"x": ["45%", 0],  "blur": [24, 0], "rotate": [-1.5, 0]},
  "ease": "power4.inOut"
}
```

Это те же значения, что описаны в `references/CAPCUT_TRANSITIONS.md` в виде рецептов:
длительность в кадрах, easing, направление. Для перехода, который в ffmpeg сделан
приближённо (`squeeze_card`, `cube_vertical`), в HyperFrames можно взять честный
`rotateY` с `perspective` из того же поля.

## Где может стоять плашка

Позиция графики измеряется, а не угадывается:

```bash
python scripts/find_overlay_zone.py base.mp4 --canvas reels   --skip "4.5-8,17.8-21" --caption-map caption_map.json
```

Отчёт даёт строки, занятые спикером, зону титров, безопасную зону платформы и
свободные полосы с флагом `recommended`. Высота плашки — из `best_band`.

На проверенном материале спикер занимал y 356..1658 из 1920. Свободна была только
полоса 0..356 сверху, максимум 316 px под карточку. Полосы «между лицом и титрами»,
которую подсказывает интуиция, там не было: карточка по предположению легла на лоб, а
после сдвига вниз — на глаза.

Правила:

- карточка целиком внутри измеренной полосы; не влезла — режь содержимое, а не лицо;
- низ вертикального кадра почти всегда под интерфейсом платформы — для графики он
  занят, даже если человек там не мешает;
- до рендера сотен кадров скомпонуй `hyperframes snapshot` поверх настоящего кадра
  базы и посмотри глазами.

`Canvas.graphics_box` в `scripts/skill_config.py` даёт грубую отправную точку для
типового кадрирования, но измерение всегда важнее этого значения по умолчанию.

## Обязательный цикл

```bash
npx skills add heygen-com/hyperframes
hyperframes doctor
hyperframes lint comp.html --json
hyperframes inspect comp.html --json
hyperframes preview comp.html
hyperframes render comp.html --out output.partial.mp4 --json
```

`lint` и `inspect` запускаются в режиме JSON, чтобы результат можно было положить в
отчёт, а не пересказывать.

## Правила композиции

- корневая длительность = `timeline.expected_duration`, ни больше ни меньше;
- базовые исходники и их звук используют один план склейки;
- б-роллы `muted` и лежат поверх;
- `data-start`, `data-duration`, `data-trim-start` берутся из таймлайна, а не
  подбираются;
- все GSAP/Lottie/Three.js-анимации привязаны к виртуальным кадрам, а не к
  `requestAnimationFrame`: иначе рендер и превью расходятся;
- последний таймлайн продлевается до последнего кадра, иначе хвост обрезается;
- сиды для любой случайности фиксированы — рендер должен быть детерминированным.

## Windows: повреждённый Chrome

`doctor` может принять повреждённый файл Chrome за установленный. Если
`snapshot`/`check` падают с `spawn EFTYPE`, сначала запусти найденный исполняемый файл
с `--version`. При невалидном файле очисти **только** браузерный кэш через
`hyperframes browser clear`, затем `browser ensure`, либо укажи проверенный
Chrome/Chromium в `HYPERFRAMES_BROWSER_PATH`.

Не удаляй кэш проекта и не подменяй HyperFrames самодельным растеризатором.

## Проверка совпадения слоёв

После рендера HyperFrames поверх базы прогоняй те же ворота:

```bash
python scripts/run_quality_gates.py output.partial.mp4 timeline.json caption_map.json
```

Если графика сдвинула длительность — `validate_full_end` это поймает. Если слой
графики закрыл титры — это видно на контрольных кадрах из
`render_word_checkpoints.py`.
