# Умная нарезка речи и скоростные рампы

Задача: убрать вздохи, причмокивания и мёртвые паузы, не тронув ни одного слова,
и подтянуть темп так, чтобы это не было слышно.

## Классификация паузы по звуку, а не по длине

Транскрипт говорит, когда есть речь. Он не говорит, что находится в 0.4 секунды
между двумя словами: вздох, причмокивание, пауза раздумья или мёртвая комната.
Резать их одинаково — так получается «пулемётная» нарезка; не резать вовсе — так
получается вялый ролик.

Каждая пауза измеряется:

* **уровень** относительно медианной громкости речи самого спикера — абсолютные dBFS
  ничего не говорят о тихой записи;
* **спектр**: центроид и доля энергии выше 1.5 кГц;
* **форма**: крест-фактор и количество атак, чтобы отличить щелчок от протяжённого
  вдоха.

### Главный вывод из реальных замеров

На настоящем материале уровень оказался **единственным надёжным признаком**. Доля
низких частот сильно перекрывается: у озвученной речи медиана 0.31, у вздохов
0.16–0.30. А по уровню разделение чистое — настоящие вздохи сидели на 9–19 дБ ниже
речи, тогда как каждый «вздох» в пределах 3 дБ от уровня речи оказывался речью,
которую Whisper не покрыл границами слов.

Поэтому всё, что громче `речь − 6 дБ`, помечается `speech_bleed` и **не режется
вообще**. На тестовом видео это спасло 13 фрагментов, которые иначе были бы обрезаны
как вздохи.

### Порядок решений (безопасный вариант побеждает)

| Класс | Условие | Что остаётся |
|---|---|---|
| `coarticulation` | короче 120 мс | всё |
| `speech_bleed` | громче `речь − 6 дБ` | всё |
| `click` | короткая, крест > 18 дБ, мало атак | 30–80 мс |
| `sentence_pause` | после точки, ≤ 0.9 с | 180–380 мс |
| `breath` | ВЧ ≥ 0.35 или центроид ≥ 1800 Гц | 70–260 мс |
| `dead_air` | тихая и длиннее 0.7 с | 120–300 мс |
| `thinking_pause` | тихая | 140–300 мс |
| `uncertain` | ни тихо, ни похоже на вздох | 260–450 мс, помечается для прослушивания |

Вздох никогда не удаляется полностью: без воздуха следующая фраза звучит склеенной.

Стили `gentle` / `balanced` / `tight` меняют только эти бюджеты.

## Филлеры: звуки и слова — разные вещи

* **Звуки хезитации** («э», «ээ», «эм», «мм», «кхм») удаляются безопасно, если стоят
  отдельно от соседних слов. Примыкающий к слову звук не вынуть без обрезки речи.
* **Слова-филлеры** («ну», «вот», «типа», «значит», «короче», «просто») **никогда не
  удаляются автоматически**. Они держат ритм и часто меняют смысл: «вот» как
  указатель, «значит» как связка. Они только попадают в отчёт как кандидаты для
  человека.

## Рез всегда по середине паузы

Подрезка забирает **середину** промежутка и оставляет полоску с двух сторон, поэтому
и отпускание предыдущего слова, и воздух перед следующим сохраняются. Это ровно то,
что отличает подтянутый монтаж от очевидно склеенного.

## Скоростные рампы

Смена скорости происходит **только на склейке**. Внутри непрерывного звука рампа
слышна, на стыке — нет.

Видео получает `setpts=PTS/k`, звук — `rubberband` с тем же множителем, поэтому
липсинк сохраняется по построению. `rubberband` выбран не на вкус: замер показал
точность до миллисекунды на всех проверенных скоростях, тогда как `atempo` ошибается
на ±10 мс за 10 секунд.

Пределы: 0.90–1.20. Выше 1.15 русская речь начинает звучать спешно даже с
сохранением формант, ниже 0.92 — как инсценировка.

При `--ramp` скорость выбирается по содержанию: сегмент с цифрой или акцентом
притормаживается до 0.97, длинный непрерывный кусок ускоряется на 6%, короткая фраза
с точкой остаётся как есть.

## Границы сегментов выравниваются по кадрам

Подрезка звука сэмплово точна, а видео привязано к кадровой сетке. Сегмент, чья
длина не кратна длительности кадра, получает картинку и звук слегка разной длины.
`concat` выравнивает потоки на каждом стыке, и рассинхрон накапливается: семнадцать
сегментов на 30 кадрах/с дают до трети секунды.

`quantise_to_frames()` округляет **выходную** длину до целого числа кадров и
пересчитывает исходный интервал обратно. Каждый рез смещается не больше чем на
полкадра, зато потоки становятся ровно одинаковой длины.

Проверено на реальном материале: 3100 кадров ожидалось — 3100 получено, длительности
видео- и аудиопотока совпали до миллисекунды.

## Что гарантирует отчёт

* `words_in` = `words_out` — ни одно слово не потеряно. Если нет, скрипт падает.
* `words_clipped` — сколько слов задето границей сегмента.
* `kept_whole_because_probably_speech` — сколько паузам не тронули по уровню.
* `uncertain_gaps` — что послушать вручную.
* `filler_candidates_for_review` — что человек может убрать сам.

## Порядок команд

```bash
python scripts/plan_speech_recut.py source.mp4 transcripts/source_001.words.json \
  --out recut_plan.json --timeline timeline_recut.json \
  --style balanced --speed 1.06 --ramp --report recut_report.json

python scripts/render_speech_recut.py recut_plan.json base_cut.mp4 --canvas reels
```

Субтитры после этого строятся **по `timeline_recut.json`**, а не по исходному
транскрипту: слова стоят на новых местах.
