Track Muxer: различия между версиями

Материал из wolfram
Перейти к навигации Перейти к поиску
Импорт технической статьи об алгоритмах conform (зрение/звук)
м Динамическая сетка make_T(dur) вместо фикс. arange(2.5,1400) — укладка любой длительности
 
(не показаны 2 промежуточные версии этого же участника)
Строка 2: Строка 2:
'''Назначение документа.''' Полное инженерное описание двух алгоритмов выравнивания, на которых стоит модуль <code>conform</code>: '''анализатора зрения''' (строит карту таймлайна по видеоряду) и '''аудио-доводки band/muq''' (правит остаточный сдвиг звука поверх зрения). Документ самодостаточен: математика, геометрия, рекурренты, точные константы и их обоснования, схемы пайплайнов и разбор реальных диагностических графиков. Читатель — инженер, который при желании сможет воспроизвести метод.
'''Назначение документа.''' Полное инженерное описание двух алгоритмов выравнивания, на которых стоит модуль <code>conform</code>: '''анализатора зрения''' (строит карту таймлайна по видеоряду) и '''аудио-доводки band/muq''' (правит остаточный сдвиг звука поверх зрения). Документ самодостаточен: математика, геометрия, рекурренты, точные константы и их обоснования, схемы пайплайнов и разбор реальных диагностических графиков. Читатель — инженер, который при желании сможет воспроизвести метод.


'''Конвенция знаков (сквозная).''' Сдвиг измеряем в ''кадрах рефа''; '''правее (дубль отстаёт) = +''', левее (спешит) = −. Один кадр = '''41.708 мс''' (23.976 fps, <code>FRAME</code> в <code>anchor/params.py</code>). Сетка анализа звука <code>T = arange(2.5, 1400, 0.5)</code> с, шаг <code>STEP = 0.5</code> с.
'''Конвенция знаков (сквозная).''' Сдвиг измеряем в ''кадрах рефа''; '''правее (дубль отстаёт) = +''', левее (спешит) = −. Один кадр = '''41.708 мс''' (23.976 fps, <code>FRAME</code> в <code>anchor/params.py</code>). Сетка анализа <code>T</code> '''строится от реальной длительности пары''' функцией <code>make_T(dur)</code> (см. §0.3), шаг <code>STEP = 0.5</code> с.


'''Источники.''' Всё, что ниже, сверено с боевым кодом <code>src/track_muxer/conform/**</code> и отчётами <code>doc/reports/**</code>. Ключевые файлы перечислены в Приложении Б.
'''Источники.''' Всё, что ниже, сверено с боевым кодом <code>src/track_muxer/conform/**</code> и отчётами <code>doc/reports/**</code>. Ключевые файлы перечислены в Приложении Б.
Строка 111: Строка 111:
|-
|-
| <code>T</code>
| <code>T</code>
| сетка анализа звука
| сетка анализа (звук и зрение)
| <code>arange(2.5, 1400, 0.5)</code> с
| <code>make_T(dur) = arange(2.5, dur, 0.5)</code> с
|-
| <code>make_T(dur)</code>
| строит сетку от длины пары
| от <code>T0 = 2.5</code> с до конца дорожки <code>dur</code>
|-
|-
| <code>STEP</code>
| <code>STEP</code>
Строка 134: Строка 138:
| <math display="inline">\in[-1,1]</math>, на практике <math display="inline">[0,1]</math>
| <math display="inline">\in[-1,1]</math>, на практике <math display="inline">[0,1]</math>
|}
|}
<div style="border-left:5px solid #aaa; background:#f6f6f6; padding:6px 12px; margin:10px 0;">
'''Сетка <code>T</code> — от реальной длины пары.''' <code>T = make_T(dur)</code> строит узлы от <code>T0 = 2.5</code> с до конца дорожки <code>dur</code> с шагом <code>STEP</code>, где <code>dur</code> — длительность рефа, на чей таймлайн кладётся дубль (<code>dur_ref</code> у зрения, <code>n_out/sr</code> у звука). Сетка одна на '''оба''' слоя: зрение (<code>vision_detect.build_map</code> зовёт <code>make_T(dur_ref)</code>) и звук (<code>audio_anchor</code> зовёт <code>make_T(n_out/sr)</code>) анализируют по ней, поэтому покрывается дорожка '''любой длительности''' — и 4-минутный ролик, и многочасовой фильм.
Раньше сетка была '''фиксированной''' <code>arange(2.5, 1400, 0.5)</code> (до 1399.5 с). Это молча предполагало, что любая серия длиннее ~23.4 мин, и ломало длину в обе стороны: на коротких хвостовые окна вылезали за конец дорожки и <code>band</code> падал на <code>torch.stack</code> (ragged-тензор), а на длинных (&gt; 23.4 мин) анализ '''обрывался на 23-й минуте''' — дальше сдвиг «замораживался» на последнем измеренном значении (и в зрении, и в звуке, и на графике). <code>make_T(dur)</code> снимает оба дефекта.
Верхняя граница сетки — ровно <code>dur</code> (а не <code>dur − T0</code>): зрению достаточно '''наличия кадров''' (окна у него нет), а у <code>band</code>/<code>multispec</code> хвостовые окна, выходящие за конец дорожки, гасит '''леаф-кламп''' — старт окна клампится в <code>[0, n−w]</code>, реально вышедшие узлы дают <code>o=NaN</code>, <code>w=0</code> и на укладку не влияют. Сетка передаётся '''параметром''' по всему тракту (а не мутирует глобал) — при параллельной очереди серии разной длины не мешают друг другу. Глобальный <code>params.T</code> остаётся лишь дефолтом для стендов и тестов.
</div>


Дальше — два алгоритма по слоям.
Дальше — два алгоритма по слоям.
Строка 144: Строка 156:


Файлы: <code>features.py</code>, <code>kernel/coarse.py</code>, <code>kernel/dropdtw.py</code>, <code>kernel/band_align.py</code>, <code>vision_detect.py</code>, разбор правок в <code>align.py</code>. Это '''единственный''' построитель видео-карты в conform (прежний «скат» <code>_build_map_linear</code> удалён 2026-06-18).
Файлы: <code>features.py</code>, <code>kernel/coarse.py</code>, <code>kernel/dropdtw.py</code>, <code>kernel/band_align.py</code>, <code>vision_detect.py</code>, разбор правок в <code>align.py</code>. Это '''единственный''' построитель видео-карты в conform (прежний «скат» <code>_build_map_linear</code> удалён 2026-06-18).
<a name="1-геометрия-задачи"></a>


<span id="1-геометрия-задачи-карта-как-монотонная-кусочно-линейная-кривая"></span>
<span id="1-геометрия-задачи-карта-как-монотонная-кусочно-линейная-кривая"></span>
Строка 177: Строка 187:
   └──┴──────┴──────────────────────────────►  t_реф</pre>
   └──┴──────┴──────────────────────────────►  t_реф</pre>
Задача зрения — '''восстановить эту ломаную из зашумлённых измерений соответствия кадров'''. Шум двух видов: (а) ложные соответствия (двойки, лого, тёмные сцены), (б) пропуски (вырезы/вставки рвут непрерывность). Поэтому метод строится в три эшелона надёжности: грубая монотонная цепочка (§3) → точное выравнивание с пропусками (§4) → робастная ломаная поверх (§6).
Задача зрения — '''восстановить эту ломаную из зашумлённых измерений соответствия кадров'''. Шум двух видов: (а) ложные соответствия (двойки, лого, тёмные сцены), (б) пропуски (вырезы/вставки рвут непрерывность). Поэтому метод строится в три эшелона надёжности: грубая монотонная цепочка (§3) → точное выравнивание с пропусками (§4) → робастная ломаная поверх (§6).
<a name="2-srm-отпечаток-кадра"></a>


<span id="2-srm-отпечаток-кадра"></span>
<span id="2-srm-отпечаток-кадра"></span>
Строка 260: Строка 268:
🖼️ '''ИЗОБРАЖЕНИЕ 2.1 — «Что видит SRM».''' Три колонки: (1) исходный кадр видео с логотипом студии в углу; (2) карта отклика <math display="inline">K_B</math> (контуры/текстура, лого почти не выделяется — оно гладкое); (3) карта отклика <math display="inline">D_1</math> (вертикальные края). Подпись: «Отпечаток строится по высокочастотному остатку → устойчив к яркости и логотипу».
🖼️ '''ИЗОБРАЖЕНИЕ 2.1 — «Что видит SRM».''' Три колонки: (1) исходный кадр видео с логотипом студии в углу; (2) карта отклика <math display="inline">K_B</math> (контуры/текстура, лого почти не выделяется — оно гладкое); (3) карта отклика <math display="inline">D_1</math> (вертикальные края). Подпись: «Отпечаток строится по высокочастотному остатку → устойчив к яркости и логотипу».
</div>
</div>
<a name="3-грубый-проход"></a>
<span id="3-грубый-проход-матрица-косинусов-надёжные-якоря-lis"></span>
<span id="3-грубый-проход-матрица-косинусов-надёжные-якоря-lis"></span>
== 3. Грубый проход: матрица косинусов, надёжные якоря, LIS ==
== 3. Грубый проход: матрица косинусов, надёжные якоря, LIS ==
Строка 309: Строка 315:


Полная матрица <math display="inline">G</math> имеет размер <math display="inline">N_s\times N_r</math> — для серии это гигабайты. Оконный проход режет дубль на окна по <code>CWIN=8000</code> кадров с перекрытием <code>COV=2000</code>; '''seed-offset перетекает из окна в окно''', и в каждом окне ищется ref только в полосе <math display="inline">\pm</math><code>CSR=4000</code> вокруг seed. Память и время ограничены окном (не растут с длиной файла), а результат '''бит-в-бит совпал''' с полным проходом на 26 кейсах.
Полная матрица <math display="inline">G</math> имеет размер <math display="inline">N_s\times N_r</math> — для серии это гигабайты. Оконный проход режет дубль на окна по <code>CWIN=8000</code> кадров с перекрытием <code>COV=2000</code>; '''seed-offset перетекает из окна в окно''', и в каждом окне ищется ref только в полосе <math display="inline">\pm</math><code>CSR=4000</code> вокруг seed. Память и время ограничены окном (не растут с длиной файла), а результат '''бит-в-бит совпал''' с полным проходом на 26 кейсах.
<a name="4-drop-dtw-выравнивание-с-пропусками"></a>


<span id="4-drop-dtw-выравнивание-с-пропусками"></span>
<span id="4-drop-dtw-выравнивание-с-пропусками"></span>
Строка 375: Строка 379:
</math>
</math>


Выброс этого кадра (ход drop-syn) разрешён '''только если''' <math display="inline">\text{colmin}_k>\text{MATCH\_THR}</math> (<math display="inline">=0.30</math>); иначе цена выброса <math display="inline">=\infty</math> — кадр с хорошим матчем защищён от выбрасывания.
Выброс этого кадра (ход drop-syn) разрешён '''только если''' <math display="inline">\text{colmin}_k>\text{MATCH}\_\text{THR}</math> (<math display="inline">=0.30</math>); иначе цена выброса <math display="inline">=\infty</math> — кадр с хорошим матчем защищён от выбрасывания.


<span id="44-free-start-заставка-начала-выпадает-сама"></span>
<span id="44-free-start-заставка-начала-выпадает-сама"></span>
Строка 405: Строка 409:
}}
}}
Итог слоя §4 — массив <code>pred</code> длины <math display="inline">N_s</math>: для каждого кадра дубля либо кадр рефа, либо <code>-1</code> (выброшен).
Итог слоя §4 — массив <code>pred</code> длины <math display="inline">N_s</math>: для каждого кадра дубля либо кадр рефа, либо <code>-1</code> (выброшен).
<a name="5-разбор-правок"></a>


<span id="5-разбор-правок-вырезы-вставки-налипания"></span>
<span id="5-разбор-правок-вырезы-вставки-налипания"></span>
Строка 441: Строка 443:


Free-start мог выкинуть '''совпадающее''' начало/конец (общий опенинг при разном вступлении BD↔WEB). Берём выброшенный кусок дубля против непокрытого куска рефа, гоняем <code>band_align</code> на подзадаче и вписываем вернувшиеся матчи (где <math display="inline">\cos></math><code>EDGE_COS_MIN=0.5</code>).
Free-start мог выкинуть '''совпадающее''' начало/конец (общий опенинг при разном вступлении BD↔WEB). Берём выброшенный кусок дубля против непокрытого куска рефа, гоняем <code>band_align</code> на подзадаче и вписываем вернувшиеся матчи (где <math display="inline">\cos></math><code>EDGE_COS_MIN=0.5</code>).
<a name="6-укладка-ломаной"></a>


<span id="6-укладка-ломаной-ow--детект-ступеней--кусочно-линейная-карта"></span>
<span id="6-укладка-ломаной-ow--детект-ступеней--кусочно-линейная-карта"></span>
Строка 500: Строка 500:


<math display="block">
<math display="block">
\text{рез реален}\iff \big|\operatorname{wmedian}(o_R,w^3)-\operatorname{wmedian}(o_L,w^3)\big|\ge\text{VC\_THR}.
\text{рез реален}\iff \big|\operatorname{wmedian}(o_R,w^3)-\operatorname{wmedian}(o_L,w^3)\big|\ge\text{VC}\_\text{THR}.
</math>
</math>


Строка 543: Строка 543:


Файлы: <code>anchor/apply.py</code> (оркестровка), <code>anchor/maps/band.py</code> (метрика), <code>anchor/detect.py</code> (общий аппарат детекта, тот же, что у зрения), <code>anchor/assemble.py</code>. Метод по умолчанию — '''band''' (DSP, 48 полос, без модели/лицензии); альтернатива '''muq''' (GPU-эмбеддинги, та же сборка).
Файлы: <code>anchor/apply.py</code> (оркестровка), <code>anchor/maps/band.py</code> (метрика), <code>anchor/detect.py</code> (общий аппарат детекта, тот же, что у зрения), <code>anchor/assemble.py</code>. Метод по умолчанию — '''band''' (DSP, 48 полос, без модели/лицензии); альтернатива '''muq''' (GPU-эмбеддинги, та же сборка).
<a name="7-зачем-аудио-после-видео"></a>


<span id="7-зачем-аудио-после-видео"></span>
<span id="7-зачем-аудио-после-видео"></span>
Строка 556: Строка 554:


Поэтому второй слой меряет '''остаточный''' сдвиг аудио дубля (уже лежащего по видео-карте) относительно аудио рефа и доводит его. Важно: аудио-слой работает '''после''' простановки тишины в монтажных резах — он должен видеть реф/тишину в вырезе (как боевой эталон), а не сырой несинхронный дубль, иначе на стыке зрение↔аудио появляется ложный краевой рез.
Поэтому второй слой меряет '''остаточный''' сдвиг аудио дубля (уже лежащего по видео-карте) относительно аудио рефа и доводит его. Важно: аудио-слой работает '''после''' простановки тишины в монтажных резах — он должен видеть реф/тишину в вырезе (как боевой эталон), а не сырой несинхронный дубль, иначе на стыке зрение↔аудио появляется ложный краевой рез.
<a name="8-принцип-зрячих-свидетелей"></a>


<span id="8-принцип-зрячих-свидетелей"></span>
<span id="8-принцип-зрячих-свидетелей"></span>
Строка 592: Строка 588:
🖼️ '''ИЗОБРАЖЕНИЕ 8.1 — «Почему полосы видят сквозь голос».''' Спектрограмма окна озвучки: горизонтальными лентами выделены 48 лог-полос; полосы, занятые голосом диктора (≈300–3000 Гц), подсвечены красным («низкий контраст, малый вес»), полосы чистой музыки/эффектов — зелёным («высокий контраст, голосуют»). Снизу — та же сцена в рефе. Подпись: «Взвешенная медиана по полосам игнорирует испорченные голосом полосы — без знания, где голос».
🖼️ '''ИЗОБРАЖЕНИЕ 8.1 — «Почему полосы видят сквозь голос».''' Спектрограмма окна озвучки: горизонтальными лентами выделены 48 лог-полос; полосы, занятые голосом диктора (≈300–3000 Гц), подсвечены красным («низкий контраст, малый вес»), полосы чистой музыки/эффектов — зелёным («высокий контраст, голосуют»). Снизу — та же сцена в рефе. Подпись: «Взвешенная медиана по полосам игнорирует испорченные голосом полосы — без знания, где голос».
</div>
</div>
<a name="9-band-метрика-48-полос"></a>
<span id="9-band-метрика-48-полос"></span>
<span id="9-band-метрика-48-полос"></span>
== 9. band-метрика: 48 полос ==
== 9. band-метрика: 48 полос ==
Строка 679: Строка 673:
'''muq как альтернатива.''' Метод <code>muq</code> заменяет полосовую огибающую на GPU-эмбеддинги музыкальной модели, но '''дальше всё то же''' — та же поверхность <code>(o,w)</code>, тот же детект <code>detect.py</code>, та же сборка <code>assemble.py</code>. band не требует новых зависимостей (только torch/numpy) и потому выбран дефолтом.
'''muq как альтернатива.''' Метод <code>muq</code> заменяет полосовую огибающую на GPU-эмбеддинги музыкальной модели, но '''дальше всё то же''' — та же поверхность <code>(o,w)</code>, тот же детект <code>detect.py</code>, та же сборка <code>assemble.py</code>. band не требует новых зависимостей (только torch/numpy) и потому выбран дефолтом.
</div>
</div>
<a name="10-грубая-off0--следящий-band"></a>
<span id="10-грубая-off0--следящий-band"></span>
<span id="10-грубая-off0--следящий-band"></span>
== 10. Грубая off0 + следящий band ==
== 10. Грубая off0 + следящий band ==
Строка 706: Строка 698:


Так узкое точное окно никогда не «теряет» крупный сдвиг — он уже снят грубой тропой, а band доводит лишь малый остаток. (Урок <code>EXPERIMENTS_coarse_band_follow.md</code>: GCC в грубом проходе подавляется голосом, band-грубый с медианой по полосам — нет.)
Так узкое точное окно никогда не «теряет» крупный сдвиг — он уже снят грубой тропой, а band доводит лишь малый остаток. (Урок <code>EXPERIMENTS_coarse_band_follow.md</code>: GCC в грубом проходе подавляется голосом, band-грубый с медианой по полосам — нет.)
<a name="11-детект-ступеней-и-кривая-дрейфа"></a>


<span id="11-детект-ступеней-и-кривая-дрейфа"></span>
<span id="11-детект-ступеней-и-кривая-дрейфа"></span>
Строка 735: Строка 725:


<math display="block">
<math display="block">
\text{lim}=\frac{\text{max\_pct\_s}}{100}\cdot\text{STEP}\cdot\frac{1000}{\text{FRAME}}\approx0.15\ \text{кадра/узел}.
\text{lim}=\frac{\text{max}\_\text{pct}\_\text{s}}{100}\cdot\text{STEP}\cdot\frac{1000}{\text{FRAME}}\approx0.15\ \text{кадра/узел}.
</math>
</math>


Строка 755: Строка 745:
         рез</pre>
         рез</pre>
Слепой старт подтягивается '''к''' плато, а не плато к старту. Первый кусок (без реза слева) — по-старому: левый край = опора. Приёмка: 47/55 дорожек не тронуты, изменения = только исправления (память <code>project_band_postcut_anchor_fix</code>).
Слепой старт подтягивается '''к''' плато, а не плато к старту. Первый кусок (без реза слева) — по-старому: левый край = опора. Приёмка: 47/55 дорожек не тронуты, изменения = только исправления (память <code>project_band_postcut_anchor_fix</code>).
<a name="12-варп-тишина-в-резах-заполнение-рефом"></a>


<span id="12-варп-тишина-в-резах-заполнение-рефом"></span>
<span id="12-варп-тишина-в-резах-заполнение-рефом"></span>
Строка 820: Строка 808:


Файл <code>anchor/plots_unified.py</code>. Проектная установка: алгоритмы доведены до состояния, где '''качество читается по графику, без ручной сверки в Premiere'''. Беглый взгляд на любой график даёт чёткий ответ «всё в порядке или нет». Поэтому график — не украшение, а главный инструмент приёмки и отладки.
Файл <code>anchor/plots_unified.py</code>. Проектная установка: алгоритмы доведены до состояния, где '''качество читается по графику, без ручной сверки в Premiere'''. Беглый взгляд на любой график даёт чёткий ответ «всё в порядке или нет». Поэтому график — не украшение, а главный инструмент приёмки и отладки.
<a name="13-зачем-графики"></a>


<span id="13-зачем-графики-и-что-на-них-видно"></span>
<span id="13-зачем-графики-и-что-на-них-видно"></span>
Строка 845: Строка 831:
| сравнение двух панелей в одной зоне
| сравнение двух панелей в одной зоне
|}
|}
<a name="14-дрейф-форма"></a>


<span id="14-дрейф-форма-и-робастная-база-тейласена"></span>
<span id="14-дрейф-форма-и-робастная-база-тейласена"></span>
Строка 875: Строка 859:
🖼️ '''ИЗОБРАЖЕНИЕ 14.1 — «Сырой сдвиг vs дрейф-форма».''' Два графика друг над другом для одной дорожки: сверху сырой <code>shift(t)</code> (линия уходит на −600 кадров, мелкие детали не видны); снизу та же дорожка в дрейф-форме (база вычтена, всё колеблется в ±15 кадров вокруг 0, видны и плато, и ступенька реза). Подпись: «Дрейф-форма вытаскивает структуру, которую крупный масштаб прячет».
🖼️ '''ИЗОБРАЖЕНИЕ 14.1 — «Сырой сдвиг vs дрейф-форма».''' Два графика друг над другом для одной дорожки: сверху сырой <code>shift(t)</code> (линия уходит на −600 кадров, мелкие детали не видны); снизу та же дорожка в дрейф-форме (база вычтена, всё колеблется в ±15 кадров вокруг 0, видны и плато, и ступенька реза). Подпись: «Дрейф-форма вытаскивает структуру, которую крупный масштаб прячет».
</div>
</div>
<a name="15-слои-и-разбор-кейсов"></a>
<span id="15-слои-цветокодирование-разбор-реальных-кейсов"></span>
<span id="15-слои-цветокодирование-разбор-реальных-кейсов"></span>
== 15. Слои, цветокодирование, разбор реальных кейсов ==
== 15. Слои, цветокодирование, разбор реальных кейсов ==
Строка 927: Строка 909:
<span id="приложения"></span>
<span id="приложения"></span>
= Приложения =
= Приложения =
<a name="приложение-а-константы"></a>


<span id="приложение-а-константы-и-их-обоснования"></span>
<span id="приложение-а-константы-и-их-обоснования"></span>
Строка 1155: Строка 1135:
⏱ '''Правило прогонов''' (память <code>feedback_warn_long_runs</code>): любой тест ≤10 мин — параллелить (клипы <code>--workers 8</code>, конформы ×6 пар, синтетика <code>--workers 24</code>) или резать объём; дольше 10 мин — только с явного согласия пользователя, оценку времени — заранее.
⏱ '''Правило прогонов''' (память <code>feedback_warn_long_runs</code>): любой тест ≤10 мин — параллелить (клипы <code>--workers 8</code>, конформы ×6 пар, синтетика <code>--workers 24</code>) или резать объём; дольше 10 мин — только с явного согласия пользователя, оценку времени — заранее.
</div>
</div>
<a name="приложение-б-карта-файлов"></a>
<span id="приложение-б-карта-файлов"></span>
<span id="приложение-б-карта-файлов"></span>
== Приложение Б. Карта файлов ==
== Приложение Б. Карта файлов ==

Текущая версия от 06:20, 20 июня 2026

Назначение документа. Полное инженерное описание двух алгоритмов выравнивания, на которых стоит модуль conform: анализатора зрения (строит карту таймлайна по видеоряду) и аудио-доводки band/muq (правит остаточный сдвиг звука поверх зрения). Документ самодостаточен: математика, геометрия, рекурренты, точные константы и их обоснования, схемы пайплайнов и разбор реальных диагностических графиков. Читатель — инженер, который при желании сможет воспроизвести метод.

Конвенция знаков (сквозная). Сдвиг измеряем в кадрах рефа; правее (дубль отстаёт) = +, левее (спешит) = −. Один кадр = 41.708 мс (23.976 fps, FRAME в anchor/params.py). Сетка анализа T строится от реальной длительности пары функцией make_T(dur) (см. §0.3), шаг STEP = 0.5 с.

Источники. Всё, что ниже, сверено с боевым кодом src/track_muxer/conform/** и отчётами doc/reports/**. Ключевые файлы перечислены в Приложении Б.


0. Постановка задачи и общая архитектура

0.1. Что мы выравниваем

Дан эпизод сериала (или отдельный фильм) в виде набора файлов разных озвучек (студий). Все они содержат один и тот же видеоряд (с точностью до монтажных отличий: разные заставки, вырезы, вставки, иногда другой fps), но разные аудиодорожки. Нужно для каждой пары (референс, дубль) так преобразовать аудио дубля во времени, чтобы оно синхронно легло на таймлайн референса — и потом смуксировать все дорожки в один MKV.

Главная трудность — это не «найти один сдвиг». Сдвиг переменный по всей длине:

Источник рассинхрона Как выглядит на карте (t_реф → t_дубль)
Постоянный сдвиг (priming, encoder delay) вертикальный параллельный перенос
Дрейф скорости (разный fps, PAL/NTSC) наклон кривой ≠ 1
Монтажный вырез в дубле (короче рефа) ступень вверх (разрыв)
Монтажная вставка в дубле (длиннее рефа) ступень вниз (разрыв)
Заставка/реклама в начале свободный левый конец

И всё это — на контенте, где прямое применение обоих слоёв теряет чувствительность: видеоряд может идти «на двойках» (соседние кадры идентичны — анимация на 2 кадра, статичные планы), музыка повторяется (корреляция фиксируется на такт), у дубля чужой голос поверх общей музыки.

🖼️ ИЗОБРАЖЕНИЕ 0.1 — «Проблема в одной картинке». Слева: две киноленты (реф и дубль) друг под другом, на дубле — вырезанный кусок (короче) и вставленная заставка в начале (длиннее), стрелки соответствия кадров между лентами расходятся (не параллельны). Справа: те же соответствия как точки в осях t_реф(X) / t_дубль(Y) — складываются в ломаную линию с наклоном и двумя вертикальными ступенями. Подпись: «Выравнивание = восстановить эту ломаную».

0.2. Двухслойная архитектура: зрение строит карту, звук доводит

Фундаментальное решение проекта (отчёт BREAKTHROUGH_VIDEO_SYNC.md): видеоряд — общий инвариант всех озвучек, поэтому грубую и структурную карту строим по видео, а аудио используем лишь как тонкую доводку остатка, который видео физически не видит (аудио источника бывает смещено относительно своего же видео).

Почему именно так, а не «всё по аудио» или «всё по видео»:

  • Только аудио упирается в потолок: 5 разных аудио-методов (PHAT, MFCC, 2D-спектр) дают одни и те же промахи на повторяющемся контенте — это не алгоритмическая, а информационная нехватка в одном аудиоканале (BREAKTHROUGH_VIDEO_SYNC.md §2).
  • Только видео не видит сдвиг аудио относительно видео внутри самого источника (AV-sync delta, студийный сдвиг M&E) — его видит лишь аудио.
  • Поэтому: видео даёт структуру (где вырез, где вставка, какой дрейф), аудио доводит субкадровый остаток там, где видео слепо по физике (двойки) или где аудио смещено относительно картинки.

0.3. Сквозные величины и обозначения

Обозначение Смысл Значение / где
FRAME мс на кадр 41.708 (≈23.976 fps)
T сетка анализа (звук и зрение) make_T(dur) = arange(2.5, dur, 0.5) с
make_T(dur) строит сетку от длины пары от T0 = 2.5 с до конца дорожки dur
STEP шаг сетки 0.5 с
o[t] измеренный сдвиг в точке t кадры рефа
w[t] уверенность измерения норм. по медиане
pred[j] для кадра дубля j — кадр рефа (или −1) целое
cos мера схожести двух кадров [1,1], на практике [0,1]

Сетка T — от реальной длины пары. T = make_T(dur) строит узлы от T0 = 2.5 с до конца дорожки dur с шагом STEP, где dur — длительность рефа, на чей таймлайн кладётся дубль (dur_ref у зрения, n_out/sr у звука). Сетка одна на оба слоя: зрение (vision_detect.build_map зовёт make_T(dur_ref)) и звук (audio_anchor зовёт make_T(n_out/sr)) анализируют по ней, поэтому покрывается дорожка любой длительности — и 4-минутный ролик, и многочасовой фильм.

Раньше сетка была фиксированной arange(2.5, 1400, 0.5) (до 1399.5 с). Это молча предполагало, что любая серия длиннее ~23.4 мин, и ломало длину в обе стороны: на коротких хвостовые окна вылезали за конец дорожки и band падал на torch.stack (ragged-тензор), а на длинных (> 23.4 мин) анализ обрывался на 23-й минуте — дальше сдвиг «замораживался» на последнем измеренном значении (и в зрении, и в звуке, и на графике). make_T(dur) снимает оба дефекта.

Верхняя граница сетки — ровно dur (а не dur − T0): зрению достаточно наличия кадров (окна у него нет), а у band/multispec хвостовые окна, выходящие за конец дорожки, гасит леаф-кламп — старт окна клампится в [0, n−w], реально вышедшие узлы дают o=NaN, w=0 и на укладку не влияют. Сетка передаётся параметром по всему тракту (а не мутирует глобал) — при параллельной очереди серии разной длины не мешают друг другу. Глобальный params.T остаётся лишь дефолтом для стендов и тестов.

Дальше — два алгоритма по слоям.



Часть I. Зрение — построитель видео-карты

Файлы: features.py, kernel/coarse.py, kernel/dropdtw.py, kernel/band_align.py, vision_detect.py, разбор правок в align.py. Это единственный построитель видео-карты в conform (прежний «скат» _build_map_linear удалён 2026-06-18).

1. Геометрия задачи: карта как монотонная кусочно-линейная кривая

Всё зрение сводится к одной геометрической цели — построить функцию

τ:tрефtдубль,

которая каждому моменту таймлайна референса сопоставляет момент в дубле, откуда брать звук. У этой функции жёсткая физическая структура:

  • кусочно-линейная — внутри монтажного блока скорость постоянна (дрейф = наклон);
  • монотонно неубывающая — время не идёт вспять (контент не переставляется);
  • с вертикальными разрывами на монтажных стыках — вырез/вставка = ступень.
 t_дубль
   ▲
   │                                        ╱ (наклон ≈ fps_реф/fps_дубль)
   │                                   ╱╱╱
   │            вставка ↓         ╱╱╱
   │                        ┌────╱           ← разрыв вниз: в дубле лишний кусок
   │                        ┊
   │                   ╱╱╱╱╱┘
   │              ╱╱╱╱
   │   вырез ↑   ╱
   │         ┌──┘                            ← разрыв вверх: в дубле кусок вырезан
   │      ╱╱╱┊
   │  ╱╱╱╱   ┊
   └──┴──────┴──────────────────────────────►  t_реф

Задача зрения — восстановить эту ломаную из зашумлённых измерений соответствия кадров. Шум двух видов: (а) ложные соответствия (двойки, лого, тёмные сцены), (б) пропуски (вырезы/вставки рвут непрерывность). Поэтому метод строится в три эшелона надёжности: грубая монотонная цепочка (§3) → точное выравнивание с пропусками (§4) → робастная ломаная поверх (§6).

2. SRM-отпечаток кадра

Чтобы сравнивать кадры рефа и дубля, каждый кадр сворачивается в вектор-отпечаток так, чтобы схожесть контента была устойчива к различиям источника (BD vs Web, разный кодек, яркость, логотип студии). Наивный путь — сравнивать пиксели яркости — ломается на первом же логотипе и перепаде гаммы. Решение — SRM (Spatial Rich Model): отпечаток не яркости, а остаточного высокочастотного сигнала (текстуры/краёв).

Отступление: что такое «отпечаток кадра» и SRM. Отпечаток (дескриптор) — это короткий числовой слепок картинки: вектор чисел, по которому два кадра можно сравнить, не сличая их попиксельно. Хороший отпечаток одинаков у одного и того же кадра в разном качестве и не сбивается от логотипа или яркости. SRM (Spatial Rich Model) — приём из цифровой криминалистики изображений: вместо самой картинки берут её высокочастотный остаток — то, что остаётся после вычитания плавных перепадов яркости (контуры, мелкая текстура). Именно остаток устойчив к перекодированию и смене источника.

2.1. Конвейер построения (features.py::build_srm)

Пошагово. Кадр приводится к серому g размера 128×72 (GW×GH).

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

Здесь два фиксированных ядра свёртки:

KB=(121242121),D1=(000011000).

  • KB — дискретный оператор второго порядка (лапласиан-подобный, сумма коэффициентов =0). Он зануляет постоянную и линейную составляющую яркости и реагирует на кривизну — текстуру, контуры, мелкие детали. Именно эта высокочастотная «подпись» одинакова у одного и того же кадра в разных рипах.
  • D1 — первая горизонтальная разность (/x), ловит вертикальные края и направление градиента.

Свёртка идёт с краевым режимом reflect: rK=(g*KB), rD=(g*D1). Далее — обрезка выбросов и нормировка каждого канала по отдельности:

r~K=clip(rK,3,3)clip(rK,3,3)2+ε,r~D=clip(rD,3,3)clip(rD,3,3)2+ε.

Обрезка ±3 давит редкие яркие выбросы (блики, субтитры), L2-нормировка убирает зависимость от общего контраста кадра. Итоговый вектор кадра — конкатенация обоих каналов, масштабированная на 1/2:

v=12[r~Kr~D]18432,18432=212872.

2.2. Почему cos = v_1\cdot v_2 напрямую

Простыми словами: косинусная близость. Если каждый кадр представить стрелкой (вектором) в многомерном пространстве, то мерой их похожести служит угол между стрелками: сонаправленные (один контент) дают косинус ≈ 1, перпендикулярные (разный контент) ≈ 0. Косинус зависит только от направления, не от длины стрелок, — поэтому он устойчив к общей яркости и контрасту.

Множитель 1/2 выбран так, что готовый вектор уже нормирован: поскольку каждый канал имеет единичную норму,

v22=12(r~K22+r~D22)=12(1+1)=1.

Поэтому скалярное произведение двух отпечатков сразу равно косинусной близости, и она же — среднее канальных косинусов:

v1v2=12(cosKB+cosD1)=cos(v1,v2)[1,1].

Это ключ к скорости всего зрения: матрица всех попарных схожестей — одно матричное умножение G=CsCr (косинусы без отдельной нормировки), а стоимость для Drop-DTW — просто C=1cos.

Инженерные детали, ломающие бит-в-бит совпадение, если их нарушить (features.py): ffmpeg -vsync 0 (passthrough — индекс кадра = позиция во времени), float16, порядок clip → norm → concat. Декод потоковый, при low_mem фичи пишутся в memmap, значения идентичны in-RAM пути.

🖼️ ИЗОБРАЖЕНИЕ 2.1 — «Что видит SRM». Три колонки: (1) исходный кадр видео с логотипом студии в углу; (2) карта отклика KB (контуры/текстура, лого почти не выделяется — оно гладкое); (3) карта отклика D1 (вертикальные края). Подпись: «Отпечаток строится по высокочастотному остатку → устойчив к яркости и логотипу».

3. Грубый проход: матрица косинусов, надёжные якоря, LIS

Цель первого эшелона — получить грубую, но монотонную оценку сдвига off(j) для каждого кадра дубля j, чтобы потом узкая полоса Drop-DTW искала точное соответствие не во всём рефе, а в коридоре вокруг этой линии. Файл kernel/coarse.py.

3.1. Надёжные якоря (_anchors)

Кадры прореживаются с шагом K=8 (грубому проходу субкадровая точность не нужна). Строится матрица косинусов всех прореженных synth × ref:

G=CsCr,Gm,r=cos(vmsyn,vrref).

Для каждого кадра дубля m берётся лучший партнёр и его близость bestm=argmaxrGm,r, bestvm=maxrGm,r. Затем окрестность ±W (W=80 прореженных кадров) этого пика зануляется, и берётся второй максимум secondm. Кадр становится надёжным якорем только если выполнены оба условия:

bestvm>VHIпик высокийbestvmsecondm>CMINпик одинок (различим),VHI=0.60,CMIN=0.12.

Первое условие отсекает мусорные сцены (тьма, переход), второе — двойки и повторы: если в рефе есть второй почти такой же кадр, отрыв мал и кадр НЕ берётся в якоря. Это прямой ответ на главную проблему повторяющегося контента.

3.2. Монотонная цепочка через LIS (lis_nd, coarse_robust)

Отступление: зачем «длиннейшая неубывающая подпоследовательность» (LIS). Среди якорей есть верные и единичные ложные. Верные обязаны идти «по возрастанию»: чем позже кадр в дубле, тем позже его партнёр в рефе (время не отматывается назад). LIS — классический алгоритм, который из разбросанных точек вытаскивает самую длинную цепочку, идущую только вверх; всё, что в неё не уложилось, почти наверняка ложные пары. Так костяк соответствия очищается без порогов.

Надёжные якоря ещё могут содержать одиночные ложные пары. Истинное соответствие монотонно по рефу, поэтому из набора якорей (synth_кадр → ref_кадр) извлекается длиннейшая неубывающая по ref подпоследовательность (Longest Non-Decreasing Subsequence, O(nlogn) через хвостовые индексы + бинарный поиск). Всё, что не легло в монотонную цепочку, отбрасывается как шум. По выжившим якорям линейной интерполяцией строится off(j) для всех кадров:

off(j)=interp(j; {ak}, {rkak}).

 ref                     LIS оставляет монотонный костяк, шум выпадает
  ▲        ✗(ложный)
  │   ●───●        ✗
  │  ╱       ●───●───●
  │ ●                   ●───●     ← возрастающая цепочка = реальное соответствие
  │╱            ✗
  └───────────────────────────►  synth (кадр дубля)

3.3. Оконный вариант для длинных файлов (coarse_windowed)

Полная матрица G имеет размер Ns×Nr — для серии это гигабайты. Оконный проход режет дубль на окна по CWIN=8000 кадров с перекрытием COV=2000; seed-offset перетекает из окна в окно, и в каждом окне ищется ref только в полосе ±CSR=4000 вокруг seed. Память и время ограничены окном (не растут с длиной файла), а результат бит-в-бит совпал с полным проходом на 26 кейсах.

4. Drop-DTW: выравнивание с пропусками

Второй эшелон — точное соответствие кадров. Обычный DTW (Dynamic Time Warping) обязан сопоставить каждый кадр — он не умеет сказать «этого куска в рефе нет, это вставка». А у нас именно вырезы и вставки. Решение — Drop-DTW: DTW, которому разрешено выбрасывать кадры с обеих сторон за фиксированный штраф. Это ровно как diff для текста или выравнивание последовательностей ДНК: ищем общий костяк, непарные куски помечаем как «вставка»/«удаление». Файл kernel/dropdtw.py.

4.1. Стоимость и базовая рекуррента (drop_dtw)

Стоимость сопоставить кадр рефа i с кадром дубля k:

Ci,k=1cos(viref,vksyn)[0,2].

Простыми словами. Задача — перебрать все способы «растянуть / сжать / пропустить» кадры так, чтобы суммарная непохожесть сопоставленных кадров была минимальной, и сделать это не наивным перебором (астрономически долго), а накоплением лучшего ответа клетка за клеткой — это и есть динамическое программирование. Ниже — правило, по которому заполняется таблица лучших частичных ответов.

Динамика заполняет таблицу Di,k — минимальную накопленную стоимость пути, пришедшего в клетку (i,k). На каждом шаге доступны пять ходов (диагональ, два варпа, два выброса); старт — отдельная инициализация (свободный левый конец):

Di,k=min{Di1,k1+Ci,kдиагональ (1:1 совпадение)Di1,k+Ci,kвертикальный варп (ref тянется)Di,k1+Ci,kгоризонтальный варп (дубль тянется)Di,k1+DROPвыброс кадра дубля (вставка)Di1,k+DROPвыброс кадра рефа (вырез)

с штрафом DROP=0.20. Backpointer Bi,k хранит, какой ход выбран, для восстановления пути. Свободные концы:

  • свободный стартD[i,0] = C[i,0] для всех i: путь может начаться с любой строки рефа (дубль не обязан стартовать с самого начала рефа);
  • свободный конец — обратный ход начинается с argmin последнего столбца: путь может закончиться на любой строке рефа.

Восстановление пути (backtrack) даёт три выхода: pred[k] (для кадра дубля k — его кадр рефа, либо -1), drop_syn (маска выброшенных кадров дубля = вставки), drop_ref (список выброшенных кадров рефа = вырезы).

🖼️ ИЗОБРАЖЕНИЕ 4.1 — «Путь по матрице Drop-DTW». Тепловая карта стоимости Ci,k (тёмное = похоже), ось X = кадры дубля, ось Y = кадры рефа. Поверх — оптимальный путь: диагональные участки (совпадение), один горизонтальный «провал» (выброс кадров дубля = вставка) и один вертикальный «прыжок» (выброс кадров рефа = вырез). Сбоку — легенда шести ходов. Подпись: «Зелёная линия — карта; разрывы = монтажные стыки».

4.2. Аффинный штраф: острый шов вместо размазни (drop_dtw_affine)

Если вырез длинный, выбрасывать его «по одному кадру» дорого и нестабильно — путь норовит размазать выброс или убежать на логотипе. Аффинная (two-state) динамика берёт открытие прогона выброса дороже, чем его продолжение (как gap-open / gap-extend в биоинформатике):

  • состояние M — «матч/варп» (платит C);
  • состояние Dr — «внутри выброса рефа»: открыть из M за OPEN=0.20, продлить из Dr за EXT=0.02;
  • выброс кадра дубля (вставка) — плоская цена DSYN=0.30 внутри M.

Dr[i,k]=min(M[i1,k]+OPEN,Dr[i1,k]+EXT).

Дёшево продлевать (0.02) → длинный вырез схлопывается в один чистый шов, а не в рваную лесенку. На ровных логотипных участках состояние Dr не открывается → путь не уходит в сторону.

4.3. Guard: нельзя выбросить кадр с хорошим матчем (drop_dtw_affine_guard)

Без защиты прогон выброса склонен захватывать хвосты совпадающих кадров (где cos=1), раздувая вставку. Guard заранее считает для каждого кадра дубля k лучший достижимый матч по столбцу:

colmink=miniCi,k.

Выброс этого кадра (ход drop-syn) разрешён только если colmink>MATCH_THR (=0.30); иначе цена выброса = — кадр с хорошим матчем защищён от выбрасывания.

4.4. Free-start: заставка начала выпадает сама

Реклама/заставка в начале дубля не имеет пары в рефе. Free-start даёт свободный левый конец по дублю: путь может стартовать с любой клетки, выбросив ведущие кадры [0..k1] по цене вставки kDSYN. Штраф растёт с k — поэтому короткая заставка выпадает, но путь не «улетает» далеко.

старт в (i,k):cost=Ci,k+kDSYN.

4.5. Следящая полоса (band_align)

Drop-DTW на полной матрице Ns×Nr невозможен по памяти. Поэтому он считается в узкой полосе вокруг грубой линии off (§3): дубль режется на куски по CHUNK=4000 кадров с перекрытием OVERLAP=700; для каждого куска ref-окно берётся как [min(refk)MARG, max(refk)+MARG], полуширина MARG=120 кадров (≈5 с — покрывает обычную правку). На стыках кусков для каждого кадра берётся ответ из того куска, где кадр дальше от края (надёжнее):

 кусок A:  ├──────────────────┤
 кусок B:              ├──────────────────┤
                       └ overlap ┘
 кадр в overlap → берём из того куска, где он ГЛУБЖЕ (центр надёжнее краёв)

Итог слоя §4 — массив pred длины Ns: для каждого кадра дубля либо кадр рефа, либо -1 (выброшен).

5. Разбор правок: вырезы, вставки, налипания

pred сырой: в нём вперемешку настоящие монтажные правки и артефакты (слепые зоны зрения, налипания). Их разбирает единое физическое правило в align.py, а не набор порогов под контент.

5.1. Правка реальна только при стойком сдвиге УРОВНЯ offset (_level_decide)

Ключевая идея: считаем поканальный offset на сопоставленных кадрах offasg=pred[asg]asg. Для каждого выброшенного блока (drop-syn) сравниваем медиану уровня offset до и после блока (окно LEVEL_WIN_S=4 с):

Δ=|median(offпосле)median(offдо)|.

  • Δ LEVEL_EDIT_S (1 с) или блок короче LEVEL_MIN_INS_S (2 с) → слепая зона / дрожь → восстановить интерполяцией (заодно перекрывает парный пробел рефа). Ложное восстановление безвредно по построению.
  • иначе → реальная вставка → оставить выброшенной.

Любой оставшийся пробел рефа при непрерывном дубле > CUT_MIN_S (0.3 с) — это реальный вырез. Доказано: пробел рефа при непрерывном дубле всегда сдвигает уровень ⇒ слепых вырезов не остаётся.

5.2. Фикс «пилы»: вырез только при устойчивом сдвиге, а не при выбросе на статике

На статике (длинный неподвижный план) зрение может дать временный выброс offset, который возвращается — это не вырез, а артефакт. Соседние пробелы склеиваются в кластер (LEVEL_CUT_COALESCE_S=3 с); если в пределах LEVEL_CUT_RECOVER_S (8 с) уровень возвращается к доврезному — кластер восстанавливается (пила убрана), если держится — схлопывается в один интервал выреза (а не лесенку коротких тишин).

5.3. Детектор «налипания» (_creep_drop, кейс case/04)

Особый дефект: вставка-повтор своего же контента с наложенным текстом присваивается Drop-DTW «ползучим ходом» вместо выброса. Сигнал кричащий: присвоенные кадры по содержимому чужие своим реф-партнёрам — cos0.000.02 на много секунд подряд, тогда как честные матчи даже в тёмных сценах держат cos0.22. Механика: скользящая медиана cos присвоенных кадров (окно ~1 с); связные зоны ниже CREEP_COS=0.15 длиннее CREEP_MIN_S=3 с → pred=-1. Дальнейшую судьбу решает _level_decide (§5.1) — ложное срабатывание безвредно.

5.4. Возврат краёв (_recover_edges)

Free-start мог выкинуть совпадающее начало/конец (общий опенинг при разном вступлении BD↔WEB). Берём выброшенный кусок дубля против непокрытого куска рефа, гоняем band_align на подзадаче и вписываем вернувшиеся матчи (где cos>EDGE_COS_MIN=0.5).

6. Укладка ломаной: (o,w) → детект ступеней → кусочно-линейная карта

Финал зрения (vision_detect.py) — превратить дискретный pred в гладкую кусочно-линейную карту с явными разрывами на резах. Это перенос сильного аудио-аппарата (anchor/detect.py) на выход зрения: зрение видит структуру сдвига точнее аудио, а прежний «скат» (maximum.accumulate) её подавлял.

6.1. Из pred в узлы сетки (o, w) (vision_ow)

Для сопоставленных кадров считаем время в рефе и сдвиг в кадрах:

tkref=pred[k]fpsref,shk=(kfpsdubtkref)fpsref.

На каждом узле сетки tT в окне ±VIS_WIN (0.3 с) берём взвешенную медиану сдвига (вес = cos), и оцениваем уверенность как медиану cos, умноженную на долю согласных (anchor-«agree») — кадров, чей сдвиг в пределах tol_fr=2 кадра от медианы:

o[t]=wmedian(sh, cos),w[t]=median(cos)1n#{|sho|2}agree.

Взвешенная медиана — робастная статистика без порогов: сортируем по значению, идём по кумулятивному весу до половины. Пустые узлы интерполируются (w=0), веса нормируются на медиану положительных.

6.2. Детект ступеней: DP-сегментация ломаными (detect.warp_curve)

Кривую o(t) режем на сегменты, в каждом — одна взвешенная прямая; стыки сегментов = кандидаты в резы. Это DP по префиксным суммам.

Отступление: робастная прямая (IRLS). Обычная прямая по методу наименьших квадратов легко уводится одной выбросной точкой. IRLS (итеративно перевзвешенный МНК) лечит это: проводим прямую, смотрим, кто далеко отклонился, уменьшаем таким точкам вес и проводим заново. Через пару итераций прямая опирается на основную массу точек и игнорирует выбросы.

Взвешенная прямая (wfit, IRLS). Для точек (t,y) с весами w оценка наклона a и сдвига b — обычная взвешенная регрессия, но в 2 итерации с даунвейтом выбросов (Iteratively Reweighted Least Squares):

a=w(tt¯)(yy¯)w(tt¯)2,b=y¯at¯,wwexp[(r2.5s)2],

где r=y(at+b) — остаток, s — взвешенный СКО. Второй проход гасит выбросы → прямая не ведётся за единичными ложными узлами.

Сегментация (_wsegment). Стоимость сегмента [i,j) — взвешенная SSE наилучшей прямой (берётся за O(1) из шести префиксных сумм w,wt,wy,wt2,wty,wy2). DP минимизирует суммарную ошибку плюс штраф PEN за каждый сегмент (чем больше PEN, тем меньше резов), при минимальной длине сегмента msize:

opt[j]=minijmsize(opt[i]+SSE(i,j)+PEN).

Наклон каждого сегмента ограничен потолком ±SMAX (физика дрейфа ≤~2%). Рез ставится на стыке, если разрыв прямых MIN_FR (3 кадра ≈ 120 мс).

6.3. Фильтр резов: устойчивость уровня на большом окне (video_cut_filter)

DP-сегментация склонна давать избыточные стыки; настоящий рез отличается тем, что уровень offset устойчиво разный слева и справа на большом окне ±VC_WIN_S (80 с), и считать его надо по надёжным узлам. Берём взвешенную медиану уровня слева/справа (робастна к «холмику» в окне — холмик у слепой зоны зрения возвращается, и медиана его игнорирует):

рез реален|wmedian(oR,w3)wmedian(oL,w3)|VC_THR.

Два важных нюанса:

  • вес-гейт VC_GATE=0.55: в окне берутся только узлы с весом выше квантиля 0.55 (_gmask) — всплески у слепых зон отбрасываются;
  • окно ограничено соседними резами (как tail_filter в аудио): на узком сегменте между двумя близкими резами окно ±80 с не должно перехлёстывать через соседний рез, иначе поймает чужой уровень и ложно подтвердит рез. Регрессия 0/36 дорожек.

6.4. Кусочно-линейная укладка (piecewise_lines)

Между резами кладём одну робастную прямую (wfit, наклон VIS_SMAX=0.45 к/с), изломы только на резах. Это усредняет весь кусок и потому нечувствительно к мелкой ряби o. Итог — кривая сдвига на сетке T.

6.5. Сборка карты (build_map)

shift(t)=interp(t; T, curve),τ(t)=t+shift(t)FRAME1000 [с].

На резах в shift ступень (разрыв) — её перекрывает тишина (вызывающий разносит контент на монтажном стыке). Между резами карта монотонна (наклон VIS_SMAX). Выход: (grid, tg_s, cuts, o, w, curve)tg_s идёт в ресэмпл аудио, o/w/curve — в графики, cuts — в тишину.

Итог Части I. Зрение превращает два видеофайла в карту τ(t) «время рефа → время дубля» — монотонную кусочно-линейную, с явными монтажными резами, устойчивую к двойкам, лого и слепым зонам. По этой карте аудио дубля ресэмплится на таймлайн рефа; в резах ставится тишина. Дальше — слой звука.



Часть II. Звук — доводка band/muq поверх зрения

Файлы: anchor/apply.py (оркестровка), anchor/maps/band.py (метрика), anchor/detect.py (общий аппарат детекта, тот же, что у зрения), anchor/assemble.py. Метод по умолчанию — band (DSP, 48 полос, без модели/лицензии); альтернатива muq (GPU-эмбеддинги, та же сборка).

7. Зачем аудио после видео

Зрение положило дубль по картинке. Но между видео и аудио внутри самого файла бывает сдвиг, которого на картинке не видно:

  • AV-sync delta — разный AAC priming / encoder delay (в JoJo измерено стабильные +89 мс между файлами, BREAKTHROUGH_VIDEO_SYNC.md §4.4);
  • студийный сдвиг M&E — студия подвинула фон/музыку в миксе относительно своего видео;
  • двойки — видео физически не различает соседние одинаковые кадры (предел ±83 мс), аудио снимает это ограничение.

Поэтому второй слой меряет остаточный сдвиг аудио дубля (уже лежащего по видео-карте) относительно аудио рефа и доводит его. Важно: аудио-слой работает после простановки тишины в монтажных резах — он должен видеть реф/тишину в вырезе (как боевой эталон), а не сырой несинхронный дубль, иначе на стыке зрение↔аудио появляется ложный краевой рез.

8. Принцип зрячих свидетелей

Отчёт METHODOLOGY_audio_matching.md фиксирует намертво: наивная корреляция звука слепа, и это легко принять за «аудио измерить нельзя». Озвучка = чужой голос поверх той же музыки и эффектов (M&E). Прямой NCC сырого сигнала ловит несовпадение голоса и возвращает шум. Лечится не другим инструментом, а изоляцией инварианта ПЕРЕД корреляцией. У каждого искажения дубляжа есть то, что оно не трогает:

Искажение дубляжа Что губит Инвариант (что не трогает) Как изолируем
Чужой голос диктора часть частотных полос чистые полосы M&E 48 лог-полос + взвеш. медиана
Другой мастеринг/тембр тонкую структуру волны динамику громкости лог-огибающая RMS
Повтор музыкального такта однозначность пика окна с одиночным пиком comb-veto

Свидетели разной физики теряют чувствительность в разных местах → их сумма покрывает всю длину. Ниже подробно про основной свидетель боевого band — многополосную метрику (она же несёт оба первых инварианта: полосы против голоса, лог-энергия против мастеринга).

🖼️ ИЗОБРАЖЕНИЕ 8.1 — «Почему полосы видят сквозь голос». Спектрограмма окна озвучки: горизонтальными лентами выделены 48 лог-полос; полосы, занятые голосом диктора (≈300–3000 Гц), подсвечены красным («низкий контраст, малый вес»), полосы чистой музыки/эффектов — зелёным («высокий контраст, голосуют»). Снизу — та же сцена в рефе. Подпись: «Взвешенная медиана по полосам игнорирует испорченные голосом полосы — без знания, где голос».

9. band-метрика: 48 полос

Файл anchor/maps/band.py. Рецепт NB48: sr=16000, NB=48 лог-полос в диапазоне 5014000 Гц, окно win=5 с, STFT nfft=2048, hop=256 (⇒ кадровый темп огибающей fps=sr/hop=62.5 Гц), агрегация wmedian, качество agree.

9.1. Полосовые огибающие (_benv)

Отступление: STFT и полосы. STFT (кратковременное преобразование Фурье) режет звук на короткие кусочки и для каждого показывает, сколько в нём энергии на каждой частоте, — получается спектрограмма «время × частота». Мы группируем частоты в 48 логарифмических полос (как деления на эквалайзере) и следим за громкостью каждой полосы во времени — это и есть «огибающая полосы». Лог-шкала частот ближе к слуху: низкие частоты дробятся мельче, высокие — крупнее.

Для окна сигнала x берём STFT и мощность P=|Z|2. Лог-частотная полосовая маска BM{0,1}NB×(nfft/2+1) (границы — logspace(50,14000,49)) суммирует мощность по полосам:

Eb,t=fBMb,fPf,t,E~b,t=log(1+Eb,t).

Каждая полоса z-нормируется по времени (убираем общий уровень и масштаб — это инвариант к мастерингу):

E^b,t=E~b,tmeantE~bstdtE~b+ε.

log1p + z-score — это и есть «лог-огибающая громкости полосы»: динамика, общая у дубля и рефа, остаётся; тембр выкидывается.

9.2. По-полосный лаг через кросс-корреляцию (_om_core)

Для каждой полосы независимо считаем кросс-корреляцию огибающих рефа E^br и дубля E^bd по времени (через FFT, знак EdEr даёт «правее=+»):

ccb[]=(E^bdE^br)[],[Mf,Mf],  Mf=maxlagfps.

Выраженность пика полосы (насколько он торчит над фоном):

promb=maxccb[]medianccb[].

9.3. Агрегация: взвешенная медиана по полосам (agg="wmedian")

Вместо суммы корреляций (её подавляет голос) — по-полосный аргмакс-лаг и взвешенная медиана этих лагов по 48 полосам, вес = prombpromp (promp=1):

bshb=argmaxccb[],^=wmedian({bshb}, {promb}).

Медиана робастна: даже если голос испортил треть полос и они показывают «не туда», медиана держится за большинство чистых. Финальный сдвиг уточняется параболической интерполяцией вокруг ^ по агрегированной поверхности и переводится в кадры:

off=^12y+yy+2y0+yfps1000FRAME [кадры].

9.4. Качество = согласие полос (quality="agree")

Уверенность узла — доля полос, чей собственный аргмакс-лаг согласен с итоговым сдвигом (в пределах tol=2 кадра), взвешенная по выраженности:

w=bpromb𝟏[|bshboff|2]bpromb+ε.

Это самопроверка без внешней истины: если полосы единодушны — узлу можно верить; если разбрелись (повтор/тишина/голос везде) — w мал, и детект/укладка такой узел не слушают.

muq как альтернатива. Метод muq заменяет полосовую огибающую на GPU-эмбеддинги музыкальной модели, но дальше всё то же — та же поверхность (o,w), тот же детект detect.py, та же сборка assemble.py. band не требует новых зависимостей (только torch/numpy) и потому выбран дефолтом.

10. Грубая off0 + следящий band

Рабочее окно band узкое — ±MAXLAG=0.7 с (точность ценой диапазона). Но сдвиг опенинга бывает крупнее. Поэтому, как и в зрении, две стадии: грубая «тропа» + точное слежение (anchor/apply.py).

10.1. Грубая off0 (_coarse_off0)

Та же band-метрика, но с широким окном лага ±COARSE_LAG_S=2.5 с — она видит большой сдвиг. Робастность только статистикой, без порогов под контент:

  1. страж края — если аргмакс на границе ±2.5 с, это «нет пика», вес обнуляется;
  2. скользящая взвешенная медиана (окно med_win_s=5 с) сдвига;
  3. гейт согласия соседей — узел стабилен, если |omed|stab_tol=4 кадра;
  4. повторная медиана только по стабильным + интерполяция → непрерывная off0(t).

10.2. Слежение (band.build_arr ±0.7с)

Дубль предварительно варпится по off0 (грубая коррекция), и band с узким окном ±0.7 с меряет остаток на пред-варпленном дубле. Полный сдвиг — сумма:

o(t)=off0(t)+oост(t).

Так узкое точное окно никогда не «теряет» крупный сдвиг — он уже снят грубой тропой, а band доводит лишь малый остаток. (Урок EXPERIMENTS_coarse_band_follow.md: GCC в грубом проходе подавляется голосом, band-грубый с медианой по полосам — нет.)

11. Детект ступеней и кривая дрейфа

11.1. Детект — тот же аппарат, что у зрения (detect.detect)

(o,w) идёт в тот же warp_curve (DP-сегментация взвешенными ломаными, §6.2), затем:

  • tail_filter — рез реален только при устойчивом сдвиге хвоста: медиана уровня до/после на окне ±win=25 с, ограниченном соседними резами, с вес-гейтом; величина реза = это смещение. Аналог video_cut_filter из зрения, но для аудио.
  • _edge_refine — отдельно выделяет рез в первом/последнем сегменте (краевая зона, короче msize), где обычная сегментация его не выделит.

11.2. Кривая дрейфа: Надарая–Уотсон + потолок скорости (_smooth_drift_curve)

Простыми словами: сглаживание Надарая–Уотсона. Это «скользящее среднее с приоритетом надёжным точкам»: значение кривой в каждый момент — среднее соседних измерений, но уверенные точки (большой вес w) тянут сильнее, а шумные почти не влияют. Ширина «гауссова окна» σ задаёт, насколько далёких соседей ещё учитывать.

Внутри куска между резами строим гладкую кривую, которая следит за горками (плавный дрейф) и стоит на шуме. Это ядерное сглаживание Надарая–Уотсона с весом wqpow (qpow=3, гауссово ядро σ=3 с):

sm(t)=(ow3)*Gσw3*Gσ+ε.

Поверх — потолок скорости изменения сдвига: между соседними узлами |Δsm| не больше lim, что соответствует ускорению max_pct_s (1.25 %/с = SMAX):

lim=max_pct_s100STEP1000FRAME0.15 кадра/узел.

Потолок не даёт кривой прыгнуть за единичными яркими выбросами в слепой зоне.

11.3. Опора после реза (фикс 2026-06-18)

Тонкость: в куске, который начинается с реза, band теряет чувствительность на самом монтажном стыке (тусклые выбросы сразу за резом). Если вести потолок скорости от левого края куска, он занижает старт, и кривая ~35 с медленно вытягивается к плато. Идея пользователя: расходиться в обе стороны от самого уверенного якоря в первых anchor_lookback_s=30 с куска (= начало плато):

   сдвиг
     ▲                  ● ● ● ● ● ● ●  ← плато (уверенные якоря)
     │      опора ↑ argmax w в первых 30с
     │     ╱   ╲        clamp влево к резу + вправо вдоль плато
     │  ✗ ╱     ╲
     │ слепой    
     │ старт      
     ┼──┊─────────────────────────────►  t
        рез

Слепой старт подтягивается к плато, а не плато к старту. Первый кусок (без реза слева) — по-старому: левый край = опора. Приёмка: 47/55 дорожек не тронуты, изменения = только исправления (память project_band_postcut_anchor_fix).

12. Варп, тишина в резах, заполнение рефом

12.1. Кусочный варп (_warp_piecewise)

Дубль ресэмплится по кускам между резами: внутри куска — гладкая wcurve (горки), на резе — резкий стык. Для выходного отсчёта t источник в дубле:

src(t)=(t+wcurve(t)FRAME1000)sr,out[t]=interp(src(t)).

12.2. Тишина в резах

Непрерывный варп на монтажном стыке «переиграл» бы звук дважды, поэтому в резах ставится тишина (контент разносится):

  • Δ<0 (нехватка дубля) → тишина |Δ| по центру реза;
  • Δ>0 (лишнее вырезано) → узкий шов ±0.15 с.

12.3. Заполнение тишины синхронным рефом (_fill_silence_from_ref)

Финальный проход — только после полного band/muq, когда дорожка уже синхронна рефу. Там, где озвучка молчит, а реф звучит, подставляем реф с кроссфейдом. Порог тишины измерен:

RMSdBFS=20log10(RMS32768+ε),fillRMSдубль<90 дБ  RMSреф90 дБ.

Порог 90 дБ — в центре измеренного плато настоящих дыр (ниже 80), далеко от обрыва тихого контента (4060), окно RMS 20 мс, мин. длина зоны 0.15 с, кроссфейд 30 мс. Где у обоих тишина — остаётся тишина; озвучка не теряется (трогаем только пустоты).

Запрет (намертво). fill_cuts — заливка вырезов рефом по видео, до анализа — запрещён навсегда: вставка в ещё несинхронную дорожку плодит мнимый рассинхрон. Разрешено только fill_silence — заполнение после выравнивания, по синхронной дорожке (память feedback_no_audio_replacement_fill_cuts).

12.4. Кросс-модальный детектор студийного A/V-десинка (detect_av_desync)

Read-only диагностика (на wav не влияет). После укладки out уже на сетке рефа; широким PHAT-окном (±2.5 с, полоса 50–13500 Гц) меряем остаточный лаг M&E к реф-аудио по окнам. Если он большой и устойчивый (медиана велика, MAD мал) — аудио источника смещено относительно его же видео (студийный дефект, надёжно не чинится) → дорожка помечается красным. Паттерн валидирован: AniLibria ep01 kamennyj-okean медиана −40к / MAD 2.2к против ~0/~0 у 10 здоровых.

Итог Части II. Поверх видео-карты звук band/muq снимает остаточный сдвиг аудио, который видео не видит: многополосная метрика делает корреляцию зрячей сквозь голос и мастеринг, грубая off0 ловит крупный сдвиг, следящий band доводит остаток, общий аппарат detect.py ставит резы, кривая дрейфа с потолком скорости и опорой-после-реза кладёт гладко, тишина заполняется синхронным рефом. Дальше — как всё это читается на графиках.



Часть III. Графики — диагноз с одного взгляда

Файл anchor/plots_unified.py. Проектная установка: алгоритмы доведены до состояния, где качество читается по графику, без ручной сверки в Premiere. Беглый взгляд на любой график даёт чёткий ответ «всё в порядке или нет». Поэтому график — не украшение, а главный инструмент приёмки и отладки.

13. Зачем графики и что на них видно

Единый график строится на каждую пару и показывает оба слоя сразу: верхняя панель — зрение (видео-укладка, всегда), нижняя — звук (доводка band/muq, если включена), на общей оси времени с синхронным зумом и ховером. Один взгляд отвечает на четыре вопроса:

Вопрос Где смотреть
Держится ли укладка за надёжные данные? цвет точек (уверенность): линия должна идти по ярким, не по тусклым
Есть ли необъяснённый перекос/увод? отклонение линии от облака точек
Верно ли найдены монтажные резы? вертикальные пунктиры и их подписи (мс)
Согласны ли зрение и звук? сравнение двух панелей в одной зоне

14. Дрейф-форма и робастная база Тейла–Сена

Сырой сдвиг по всей серии может быть сотни кадров (крупный масштаб оси), и на нём не видно ни плато, ни ступеней в десяток кадров. Поэтому обе панели рисуются в дрейф-форме — вычитается робастная линейная база, и всё разворачивается вокруг нуля.

Отступление: база Тейла–Сена. Чтобы провести «среднюю линию» через облако точек и не дать единичным выбросам её перекосить, берут наклон между каждой парой точек и выбирают медианный — половина пар «за», половина «против». Это устойчивее обычной прямой и не требует никаких порогов отбраковки.

База оценивается методом Тейла–Сена (_baseline) — он устойчив к выбросам, в отличие от МНК: на выборке точек (до 400) берутся все попарные наклоны, и базовый наклон — их медиана; сдвиг — медиана остатков:

a=mediani<jyjyitjti,b=mediank(ykatk).

База считается по телу дорожки (исключая голову/заставку: t>head+5 с). Затем:

devякорь=shift(at+b),devкривая=curve(aT+b),

а предел оси берётся как max(12, p99(|dev|)+4) кадров — авто-масштаб под реальный разброс. На резах в линию вставляется NaN (_breaks), чтобы она не соединяла куски через разрыв — ступень видна как обрыв, а не как вертикаль.

🖼️ ИЗОБРАЖЕНИЕ 14.1 — «Сырой сдвиг vs дрейф-форма». Два графика друг над другом для одной дорожки: сверху сырой shift(t) (линия уходит на −600 кадров, мелкие детали не видны); снизу та же дорожка в дрейф-форме (база вычтена, всё колеблется в ±15 кадров вокруг 0, видны и плато, и ступенька реза). Подпись: «Дрейф-форма вытаскивает структуру, которую крупный масштаб прячет».

15. Слои, цветокодирование, разбор реальных кейсов

15.1. Слои единого графика

Верхняя панель (ЗРЕНИЕ):

  • точки якорей (tреф, devякорь), цвет = cos (viridis, 0…1) — надёжность каждого видео-якоря;
  • оранжевая линия — карта зрения в дрейф-форме, с разрывами на резах;
  • crimson-пунктиры — резы зрения с подписью величины (мс);
  • заголовок несёт абсолютную медиану сдвига.

Нижняя панель (ЗВУК):

  • точки (T, o), цвет = w (cividis, 0…1.2) — уверенность band;
  • серые точки — gcc-свидетель (±2.5 с), справочно;
  • зелёная линия — кривая band (дрейф), с разрывами на резах;
  • purple-пунктиры — резы детекта аудио.

gcc — НЕ судья. Серая gcc-линия на контенте проекта (закадр+музыка) почти слепа и скачет ±40к = шум; её нельзя брать за подтверждение укладки. Судьи сходимости — сам band (48 полос) / muq + уши/глаза на наложенных дорожках (память feedback_gcc_ncc_blind_judge). gcc на графике — фон, не вердикт.

15.2. Как цвет читается мгновенно

Цвет точки = уверенность. Тёмные (фиолетовые/синие) — шумные, ярких (жёлтые/зелёные) — надёжные. Правильная линия идёт по ярким точкам. Если линия отклонилась, а рядом плотное облако ярких — это сигнал проблемы: либо увод укладки, либо линия последовала за тусклыми выбросами на краю. Это и есть «диагноз с одного взгляда».

🖼️ ИЗОБРАЖЕНИЕ 15.1 — «Эталон единого графика (здоровая дорожка)». Реальный скрин двухпанельного графика: вверху зрение — плотное жёлтое облако якорей точно на 0, оранжевая линия плоская, резов нет; внизу звук — узкое облако вокруг 0, зелёная линия плоская. Подпись: «Так выглядит ОК: линии по ярким точкам, ноль отклонения, нет резов». (Скрин взять из <серия>/_plots/<stem>__track.png любой здоровой пары.)

🖼️ ИЗОБРАЖЕНИЕ 15.2 — «Перекос укладки (до фикса опоры-после-реза)». Скрин зоны 180–220 с AniDUB: после реза линия band провалилась вниз за парой тусклых (тёмных) якорей у края, хотя выше плотное плато ярких якорей на другом уровне. Рядом — тот же участок после фикса: линия держится за плато (опора = увереннейший якорь начала плато), слепой старт подтянут к плато. Подпись: «Линия не должна вестись за тусклыми единицами на краю».

🖼️ ИЗОБРАЖЕНИЕ 15.3 — «Ложный рез из-за слепоты зрения (ep05 Amazing, 950–979 с)». Скрин зоны с двумя близкими резами: до фикса между ними узкий клин и лишний рез (окно проверки ±80 с перехлестнуло через соседний рез и поймало чужой уровень); после фикса (окно ограничено соседними резами) лишний рез убран. Подпись: «Окно проверки уровня ограничивается соседними резами — иначе чужой уровень ложно подтверждает рез».

15.3. Носители: PNG-превью и интерактивный HTML

  • PNG (matplotlib, Agg) — статичная миниатюра (1 или 2 панели) для быстрого взгляда в панели/отчёте.
  • HTML (plotly, self-contained, открывается офлайн) — для детального разбора. Точки и линия в WebGL (scattergl), чтобы линия рисовалась поверх точек независимо от порядка; ховер x unified, зум любой зоны вручную. Отдельные PNG каждого реза не создаём — HTML позволяет приблизить любой стык (решение 2026-06-18).

Read-only по контракту: падение рендера не роняет conform (ловит вызывающий).



Приложения

Приложение А. Константы и их обоснования

Все ключевые числа калиброваны не «на глаз», а по плато нечувствительности: на свипе по диапазону результат (резы по датасету) не меняется — значит подстройки под контент нет.

Зрение

Константа Значение Файл Смысл / обоснование
GW×GH 128×72 features размер серого кадра под SRM
clip T ±3 features обрезка выбросов отклика фильтров
dim 18432 features 212872, два канала
K (coarse) 8 coarse прорежение грубого прохода
W 80 coarse полуокно зануления при поиске 2-го пика
VHI 0.60 coarse порог «пик высокий» (надёжный якорь)
CMIN 0.12 coarse порог различимости (пик одинок → не двойка)
DROP 0.20 dropdtw штраф выброса (базовый Drop-DTW)
OPEN/EXT 0.20 / 0.02 dropdtw gap-open / gap-extend (острый шов)
DSYN 0.30 dropdtw/align плоская цена выброса кадра дубля (вставка)
MATCH_THR 0.30 dropdtw/align guard: кадр с матчем лучше этого нельзя выбросить
MARG 120 band_align полуширина следящей полосы (≈5 с)
CHUNK/OVERLAP 4000 / 700 band_align кусок Drop-DTW и перекрытие
VIS_WIN 0.3 vision_detect окно агрегации якорей (плато 0.2–0.5)
VIS_PEN 800 vision_detect штраф DP-сегментации (плато 200–3000)
VIS_QPOW 3.0 vision_detect степень веса = (cos·agree)³ (плато 2–3)
VIS_SMAX 0.45 vision_detect потолок наклона прямых, к/с (плато 0.3–0.6)
VIS_MSIZE 30 vision_detect мин. длина сегмента, с (плато 8–60)
VC_WIN_S 80 vision_detect окно проверки устойчивости уровня (плато 45–120)
VC_GATE 0.55 vision_detect вес-гейт надёжных узлов (плато 0.3–0.8)
VC_THR 200 мс vision_detect порог величины реза (плато 60–400 мс)

Звук

Константа Значение Файл Смысл / обоснование
sr 16000 band частота анализа
NB 48 band число лог-полос (50–14000 Гц)
nfft/hop 2048 / 256 band STFT (темп огибающей 62.5 Гц)
win 5 с band окно анализа
MAXLAG 0.7 с band рабочее окно лага (точность)
COARSE_LAG_S 2.5 с apply окно грубой off0 (видит сдвиг опенинга)
tol 2 кадра band допуск согласия полос (agree)
FRAME 41.708 мс params мс/кадр (23.976 fps)
STEP 0.5 с params шаг сетки T
MIN_FR 3 (120 мс) params порог величины реза
PEN 700 params штраф DP-сегментации (детект)
QPOW 3.0 params степень веса якоря
SMAX 0.3 params макс. наклон сегмента (≤~2%)
MSIZE_S 20 с params мин. длина сегмента
max_pct_s 1.25 %/с apply потолок скорости кривой дрейфа
anchor_lookback_s 30 с apply окно поиска опоры-после-реза
SIL_FILL_DB −90 дБ align порог тишины озвучки (центр плато дыр)
XFADE 30 мс align кроссфейд заполнения рефом

Правило прогонов (память feedback_warn_long_runs): любой тест ≤10 мин — параллелить (клипы --workers 8, конформы ×6 пар, синтетика --workers 24) или резать объём; дольше 10 мин — только с явного согласия пользователя, оценку времени — заранее.

Приложение Б. Карта файлов

src/track_muxer/conform/
├── features.py              SRM-отпечатки кадров (KB/D1, L2, dim 18432)
├── kernel/
│   ├── coarse.py            грубый проход: G=cs·crᵀ, якоря, LIS, оконный
│   ├── dropdtw.py           Drop-DTW (base/affine/guard/free_start)
│   └── band_align.py        следящая полоса + склейка кусков
├── vision_detect.py         УКЛАДКА зрения: (o,w) → детект → ломаная (ЕДИНСТВЕННАЯ карта)
├── align.py                 оркестровка пары: pred → разбор правок → ресэмпл → звук → график
└── anchor/                  АУДИО-доводка band/muq поверх зрения
    ├── apply.py             coarse_off0, следящий band, smooth_drift_curve, варп, fill
    ├── detect.py            ОБЩИЙ аппарат: wfit, warp_curve, tail_filter, edge_refine
    ├── assemble.py          freeze + сборка варпа
    ├── params.py            FRAME, T, STEP, PEN, QPOW, SMAX, MSIZE_S …
    ├── maps/band.py         48-полосная DSP-метрика (без модели) — дефолт
    ├── maps/muq.py          GPU-эмбеддинги (альтернатива)
    ├── plots_unified.py     ЕДИНЫЙ график зрение+звук (PNG + HTML)
    └── plots_html.py        отдельный HTML band (легаси-путь)
Тема Отчёт (суть)
Прорыв видео-синхрона doc/reports/audio_align/BREAKTHROUGH_VIDEO_SYNC.md
Методика зрячих свидетелей аудио doc/reports/audio_bench/METHODOLOGY_audio_matching.md
Грубый→band-follow doc/reports/audio_bench/EXPERIMENTS_coarse_band_follow.md
Аудио-аанкер (band/muq) doc/reports/audio_bench/EXPERIMENTS_audio_auto.md
Архитектура conform doc/conform_architecture_audit.md §14



Версия документа: 2026-06-18, под архитектуру «анализатор зрения — единственная видео-карта; band/muq — доводка поверх». Числа и формулы сверены с боевым кодом src/track_muxer/conform/**. При расхождении кода и документа — приоритет у кода, документ актуализировать.