Track Muxer
Назначение документа. Полное инженерное описание двух алгоритмов выравнивания, на которых стоит модуль conform: анализатора зрения (строит карту таймлайна по видеоряду) и аудио-доводки band/muq (правит остаточный сдвиг звука поверх зрения). Документ самодостаточен: математика, геометрия, рекурренты, точные константы и их обоснования, схемы пайплайнов и разбор реальных диагностических графиков. Читатель — инженер, который при желании сможет воспроизвести метод.
Конвенция знаков (сквозная). Сдвиг измеряем в кадрах рефа; правее (дубль отстаёт) = +, левее (спешит) = −. Один кадр = 41.708 мс (23.976 fps, FRAME в anchor/params.py). Сетка анализа звука T = arange(2.5, 1400, 0.5) с, шаг 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
|
сетка анализа звука | arange(2.5, 1400, 0.5) с
|
STEP
|
шаг сетки | 0.5 с |
o[t]
|
измеренный сдвиг в точке t | кадры рефа |
w[t]
|
уверенность измерения | норм. по медиане |
pred[j]
|
для кадра дубля j — кадр рефа (или −1) | целое |
| cos | мера схожести двух кадров | , на практике |
Дальше — два алгоритма по слоям.
Часть 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).
<a name="1-геометрия-задачи"></a>
1. Геометрия задачи: карта как монотонная кусочно-линейная кривая
Всё зрение сводится к одной геометрической цели — построить функцию
которая каждому моменту таймлайна референса сопоставляет момент в дубле, откуда брать звук. У этой функции жёсткая физическая структура:
- кусочно-линейная — внутри монтажного блока скорость постоянна (дрейф = наклон);
- монотонно неубывающая — время не идёт вспять (контент не переставляется);
- с вертикальными разрывами на монтажных стыках — вырез/вставка = ступень.
t_дубль ▲ │ ╱ (наклон ≈ fps_реф/fps_дубль) │ ╱╱╱ │ вставка ↓ ╱╱╱ │ ┌────╱ ← разрыв вниз: в дубле лишний кусок │ ┊ │ ╱╱╱╱╱┘ │ ╱╱╱╱ │ вырез ↑ ╱ │ ┌──┘ ← разрыв вверх: в дубле кусок вырезан │ ╱╱╱┊ │ ╱╱╱╱ ┊ └──┴──────┴──────────────────────────────► t_реф
Задача зрения — восстановить эту ломаную из зашумлённых измерений соответствия кадров. Шум двух видов: (а) ложные соответствия (двойки, лого, тёмные сцены), (б) пропуски (вырезы/вставки рвут непрерывность). Поэтому метод строится в три эшелона надёжности: грубая монотонная цепочка (§3) → точное выравнивание с пропусками (§4) → робастная ломаная поверх (§6).
<a name="2-srm-отпечаток-кадра"></a>
2. SRM-отпечаток кадра
Чтобы сравнивать кадры рефа и дубля, каждый кадр сворачивается в вектор-отпечаток так, чтобы схожесть контента была устойчива к различиям источника (BD vs Web, разный кодек, яркость, логотип студии). Наивный путь — сравнивать пиксели яркости — ломается на первом же логотипе и перепаде гаммы. Решение — SRM (Spatial Rich Model): отпечаток не яркости, а остаточного высокочастотного сигнала (текстуры/краёв).
Отступление: что такое «отпечаток кадра» и SRM. Отпечаток (дескриптор) — это короткий числовой слепок картинки: вектор чисел, по которому два кадра можно сравнить, не сличая их попиксельно. Хороший отпечаток одинаков у одного и того же кадра в разном качестве и не сбивается от логотипа или яркости. SRM (Spatial Rich Model) — приём из цифровой криминалистики изображений: вместо самой картинки берут её высокочастотный остаток — то, что остаётся после вычитания плавных перепадов яркости (контуры, мелкая текстура). Именно остаток устойчив к перекодированию и смене источника.
2.1. Конвейер построения (features.py::build_srm)
Пошагово. Кадр приводится к серому размера (GW×GH).
Простыми словами: свёртка ядром. Свёртка — это проход маленькой матрицей-окном по всем пикселям: в каждой точке берётся взвешенная сумма соседних пикселей. Подбором весов окно настраивают на нужный признак — перепад яркости, край, текстуру. Сумма весов, равная нулю, означает, что окно «не замечает» ровной заливки и реагирует только на изменения.
Здесь два фиксированных ядра свёртки:
- — дискретный оператор второго порядка (лапласиан-подобный, сумма коэффициентов ). Он зануляет постоянную и линейную составляющую яркости и реагирует на кривизну — текстуру, контуры, мелкие детали. Именно эта высокочастотная «подпись» одинакова у одного и того же кадра в разных рипах.
- — первая горизонтальная разность (), ловит вертикальные края и направление градиента.
Свёртка идёт с краевым режимом reflect: , . Далее — обрезка выбросов и нормировка каждого канала по отдельности:
Обрезка давит редкие яркие выбросы (блики, субтитры), L2-нормировка убирает зависимость от общего контраста кадра. Итоговый вектор кадра — конкатенация обоих каналов, масштабированная на :
2.2. Почему cos = v_1\cdot v_2 напрямую
Простыми словами: косинусная близость. Если каждый кадр представить стрелкой (вектором) в многомерном пространстве, то мерой их похожести служит угол между стрелками: сонаправленные (один контент) дают косинус ≈ 1, перпендикулярные (разный контент) ≈ 0. Косинус зависит только от направления, не от длины стрелок, — поэтому он устойчив к общей яркости и контрасту.
Множитель выбран так, что готовый вектор уже нормирован: поскольку каждый канал имеет единичную норму,
Поэтому скалярное произведение двух отпечатков сразу равно косинусной близости, и она же — среднее канальных косинусов:
Это ключ к скорости всего зрения: матрица всех попарных схожестей — одно матричное умножение (косинусы без отдельной нормировки), а стоимость для Drop-DTW — просто .
Инженерные детали, ломающие бит-в-бит совпадение, если их нарушить (features.py): ffmpeg -vsync 0 (passthrough — индекс кадра = позиция во времени), float16, порядок clip → norm → concat. Декод потоковый, при low_mem фичи пишутся в memmap, значения идентичны in-RAM пути.
🖼️ ИЗОБРАЖЕНИЕ 2.1 — «Что видит SRM». Три колонки: (1) исходный кадр видео с логотипом студии в углу; (2) карта отклика (контуры/текстура, лого почти не выделяется — оно гладкое); (3) карта отклика (вертикальные края). Подпись: «Отпечаток строится по высокочастотному остатку → устойчив к яркости и логотипу».
<a name="3-грубый-проход"></a>
3. Грубый проход: матрица косинусов, надёжные якоря, LIS
Цель первого эшелона — получить грубую, но монотонную оценку сдвига off(j) для каждого кадра дубля , чтобы потом узкая полоса Drop-DTW искала точное соответствие не во всём рефе, а в коридоре вокруг этой линии. Файл kernel/coarse.py.
3.1. Надёжные якоря (_anchors)
Кадры прореживаются с шагом (грубому проходу субкадровая точность не нужна). Строится матрица косинусов всех прореженных synth × ref:
Для каждого кадра дубля берётся лучший партнёр и его близость , . Затем окрестность ( прореженных кадров) этого пика зануляется, и берётся второй максимум . Кадр становится надёжным якорем только если выполнены оба условия:
Первое условие отсекает мусорные сцены (тьма, переход), второе — двойки и повторы: если в рефе есть второй почти такой же кадр, отрыв мал и кадр НЕ берётся в якоря. Это прямой ответ на главную проблему повторяющегося контента.
3.2. Монотонная цепочка через LIS (lis_nd, coarse_robust)
Отступление: зачем «длиннейшая неубывающая подпоследовательность» (LIS). Среди якорей есть верные и единичные ложные. Верные обязаны идти «по возрастанию»: чем позже кадр в дубле, тем позже его партнёр в рефе (время не отматывается назад). LIS — классический алгоритм, который из разбросанных точек вытаскивает самую длинную цепочку, идущую только вверх; всё, что в неё не уложилось, почти наверняка ложные пары. Так костяк соответствия очищается без порогов.
Надёжные якоря ещё могут содержать одиночные ложные пары. Истинное соответствие монотонно по рефу, поэтому из набора якорей (synth_кадр → ref_кадр) извлекается длиннейшая неубывающая по ref подпоследовательность (Longest Non-Decreasing Subsequence, через хвостовые индексы + бинарный поиск). Всё, что не легло в монотонную цепочку, отбрасывается как шум. По выжившим якорям линейной интерполяцией строится off(j) для всех кадров:
ref LIS оставляет монотонный костяк, шум выпадает ▲ ✗(ложный) │ ●───● ✗ │ ╱ ●───●───● │ ● ●───● ← возрастающая цепочка = реальное соответствие │╱ ✗ └───────────────────────────► synth (кадр дубля)
3.3. Оконный вариант для длинных файлов (coarse_windowed)
Полная матрица имеет размер — для серии это гигабайты. Оконный проход режет дубль на окна по CWIN=8000 кадров с перекрытием COV=2000; seed-offset перетекает из окна в окно, и в каждом окне ищется ref только в полосе CSR=4000 вокруг seed. Память и время ограничены окном (не растут с длиной файла), а результат бит-в-бит совпал с полным проходом на 26 кейсах.
<a name="4-drop-dtw-выравнивание-с-пропусками"></a>
4. Drop-DTW: выравнивание с пропусками
Второй эшелон — точное соответствие кадров. Обычный DTW (Dynamic Time Warping) обязан сопоставить каждый кадр — он не умеет сказать «этого куска в рефе нет, это вставка». А у нас именно вырезы и вставки. Решение — Drop-DTW: DTW, которому разрешено выбрасывать кадры с обеих сторон за фиксированный штраф. Это ровно как diff для текста или выравнивание последовательностей ДНК: ищем общий костяк, непарные куски помечаем как «вставка»/«удаление». Файл kernel/dropdtw.py.
4.1. Стоимость и базовая рекуррента (drop_dtw)
Стоимость сопоставить кадр рефа с кадром дубля :
Простыми словами. Задача — перебрать все способы «растянуть / сжать / пропустить» кадры так, чтобы суммарная непохожесть сопоставленных кадров была минимальной, и сделать это не наивным перебором (астрономически долго), а накоплением лучшего ответа клетка за клеткой — это и есть динамическое программирование. Ниже — правило, по которому заполняется таблица лучших частичных ответов.
Динамика заполняет таблицу — минимальную накопленную стоимость пути, пришедшего в клетку . На каждом шаге доступны пять ходов (диагональ, два варпа, два выброса); старт — отдельная инициализация (свободный левый конец):
с штрафом . Backpointer хранит, какой ход выбран, для восстановления пути. Свободные концы:
- свободный старт —
D[i,0] = C[i,0]для всех : путь может начаться с любой строки рефа (дубль не обязан стартовать с самого начала рефа); - свободный конец — обратный ход начинается с
argminпоследнего столбца: путь может закончиться на любой строке рефа.
Восстановление пути (backtrack) даёт три выхода: pred[k] (для кадра дубля — его кадр рефа, либо -1), drop_syn (маска выброшенных кадров дубля = вставки), drop_ref (список выброшенных кадров рефа = вырезы).
🖼️ ИЗОБРАЖЕНИЕ 4.1 — «Путь по матрице Drop-DTW». Тепловая карта стоимости (тёмное = похоже), ось X = кадры дубля, ось Y = кадры рефа. Поверх — оптимальный путь: диагональные участки (совпадение), один горизонтальный «провал» (выброс кадров дубля = вставка) и один вертикальный «прыжок» (выброс кадров рефа = вырез). Сбоку — легенда шести ходов. Подпись: «Зелёная линия — карта; разрывы = монтажные стыки».
4.2. Аффинный штраф: острый шов вместо размазни (drop_dtw_affine)
Если вырез длинный, выбрасывать его «по одному кадру» дорого и нестабильно — путь норовит размазать выброс или убежать на логотипе. Аффинная (two-state) динамика берёт открытие прогона выброса дороже, чем его продолжение (как gap-open / gap-extend в биоинформатике):
- состояние — «матч/варп» (платит );
- состояние — «внутри выброса рефа»: открыть из за
OPEN=0.20, продлить из заEXT=0.02; - выброс кадра дубля (вставка) — плоская цена
DSYN=0.30внутри .
Дёшево продлевать (0.02) → длинный вырез схлопывается в один чистый шов, а не в рваную лесенку. На ровных логотипных участках состояние не открывается → путь не уходит в сторону.
4.3. Guard: нельзя выбросить кадр с хорошим матчем (drop_dtw_affine_guard)
Без защиты прогон выброса склонен захватывать хвосты совпадающих кадров (где ), раздувая вставку. Guard заранее считает для каждого кадра дубля лучший достижимый матч по столбцу:
Выброс этого кадра (ход drop-syn) разрешён только если Невозможно разобрать выражение (синтаксическая ошибка): {\textstyle \text{colmin}_k>\text{MATCH\_THR}} (); иначе цена выброса — кадр с хорошим матчем защищён от выбрасывания.
4.4. Free-start: заставка начала выпадает сама
Реклама/заставка в начале дубля не имеет пары в рефе. Free-start даёт свободный левый конец по дублю: путь может стартовать с любой клетки, выбросив ведущие кадры по цене вставки . Штраф растёт с — поэтому короткая заставка выпадает, но путь не «улетает» далеко.
4.5. Следящая полоса (band_align)
Drop-DTW на полной матрице невозможен по памяти. Поэтому он считается в узкой полосе вокруг грубой линии off (§3): дубль режется на куски по CHUNK=4000 кадров с перекрытием OVERLAP=700; для каждого куска ref-окно берётся как , полуширина MARG=120 кадров (≈5 с — покрывает обычную правку). На стыках кусков для каждого кадра берётся ответ из того куска, где кадр дальше от края (надёжнее):
кусок A: ├──────────────────┤
кусок B: ├──────────────────┤
└ overlap ┘
кадр в overlap → берём из того куска, где он ГЛУБЖЕ (центр надёжнее краёв)
Итог слоя §4 — массив pred длины : для каждого кадра дубля либо кадр рефа, либо -1 (выброшен).
<a name="5-разбор-правок"></a>
5. Разбор правок: вырезы, вставки, налипания
pred сырой: в нём вперемешку настоящие монтажные правки и артефакты (слепые зоны зрения, налипания). Их разбирает единое физическое правило в align.py, а не набор порогов под контент.
5.1. Правка реальна только при стойком сдвиге УРОВНЯ offset (_level_decide)
Ключевая идея: считаем поканальный offset на сопоставленных кадрах . Для каждого выброшенного блока (drop-syn) сравниваем медиану уровня offset до и после блока (окно LEVEL_WIN_S=4 с):
-
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 «ползучим ходом» вместо выброса. Сигнал кричащий: присвоенные кадры по содержимому чужие своим реф-партнёрам — на много секунд подряд, тогда как честные матчи даже в тёмных сценах держат . Механика: скользящая медиана присвоенных кадров (окно ~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 на подзадаче и вписываем вернувшиеся матчи (где EDGE_COS_MIN=0.5).
<a name="6-укладка-ломаной"></a>
6. Укладка ломаной: (o,w) → детект ступеней → кусочно-линейная карта
Финал зрения (vision_detect.py) — превратить дискретный pred в гладкую кусочно-линейную карту с явными разрывами на резах. Это перенос сильного аудио-аппарата (anchor/detect.py) на выход зрения: зрение видит структуру сдвига точнее аудио, а прежний «скат» (maximum.accumulate) её подавлял.
6.1. Из pred в узлы сетки (o, w) (vision_ow)
Для сопоставленных кадров считаем время в рефе и сдвиг в кадрах:
На каждом узле сетки в окне VIS_WIN (0.3 с) берём взвешенную медиану сдвига (вес = ), и оцениваем уверенность как медиану , умноженную на долю согласных (anchor-«agree») — кадров, чей сдвиг в пределах tol_fr=2 кадра от медианы:
Взвешенная медиана — робастная статистика без порогов: сортируем по значению, идём по кумулятивному весу до половины. Пустые узлы интерполируются (), веса нормируются на медиану положительных.
6.2. Детект ступеней: DP-сегментация ломаными (detect.warp_curve)
Кривую o(t) режем на сегменты, в каждом — одна взвешенная прямая; стыки сегментов = кандидаты в резы. Это DP по префиксным суммам.
Отступление: робастная прямая (IRLS). Обычная прямая по методу наименьших квадратов легко уводится одной выбросной точкой. IRLS (итеративно перевзвешенный МНК) лечит это: проводим прямую, смотрим, кто далеко отклонился, уменьшаем таким точкам вес и проводим заново. Через пару итераций прямая опирается на основную массу точек и игнорирует выбросы.
Взвешенная прямая (wfit, IRLS). Для точек с весами оценка наклона и сдвига — обычная взвешенная регрессия, но в 2 итерации с даунвейтом выбросов (Iteratively Reweighted Least Squares):
где — остаток, — взвешенный СКО. Второй проход гасит выбросы → прямая не ведётся за единичными ложными узлами.
Сегментация (_wsegment). Стоимость сегмента — взвешенная SSE наилучшей прямой (берётся за из шести префиксных сумм ). DP минимизирует суммарную ошибку плюс штраф PEN за каждый сегмент (чем больше PEN, тем меньше резов), при минимальной длине сегмента msize:
Наклон каждого сегмента ограничен потолком SMAX (физика дрейфа ≤~2%). Рез ставится на стыке, если разрыв прямых MIN_FR (3 кадра ≈ 120 мс).
6.3. Фильтр резов: устойчивость уровня на большом окне (video_cut_filter)
DP-сегментация склонна давать избыточные стыки; настоящий рез отличается тем, что уровень offset устойчиво разный слева и справа на большом окне VC_WIN_S (80 с), и считать его надо по надёжным узлам. Берём взвешенную медиану уровня слева/справа (робастна к «холмику» в окне — холмик у слепой зоны зрения возвращается, и медиана его игнорирует):
Невозможно разобрать выражение (синтаксическая ошибка): {\displaystyle \text{рез реален}\iff \big|\operatorname{wmedian}(o_R,w^3)-\operatorname{wmedian}(o_L,w^3)\big|\ge\text{VC\_THR}. }
Два важных нюанса:
- вес-гейт
VC_GATE=0.55: в окне берутся только узлы с весом выше квантиля 0.55 (_gmask) — всплески у слепых зон отбрасываются; - окно ограничено соседними резами (как
tail_filterв аудио): на узком сегменте между двумя близкими резами окно с не должно перехлёстывать через соседний рез, иначе поймает чужой уровень и ложно подтвердит рез. Регрессия 0/36 дорожек.
6.4. Кусочно-линейная укладка (piecewise_lines)
Между резами кладём одну робастную прямую (wfit, наклон VIS_SMAX=0.45 к/с), изломы только на резах. Это усредняет весь кусок и потому нечувствительно к мелкой ряби o. Итог — кривая сдвига на сетке T.
6.5. Сборка карты (build_map)
На резах в shift ступень (разрыв) — её перекрывает тишина (вызывающий разносит контент на монтажном стыке). Между резами карта монотонна (наклон VIS_SMAX). Выход: (grid, tg_s, cuts, o, w, curve) — tg_s идёт в ресэмпл аудио, o/w/curve — в графики, cuts — в тишину.
Итог Части I. Зрение превращает два видеофайла в карту «время рефа → время дубля» — монотонную кусочно-линейную, с явными монтажными резами, устойчивую к двойкам, лого и слепым зонам. По этой карте аудио дубля ресэмплится на таймлайн рефа; в резах ставится тишина. Дальше — слой звука.
Часть II. Звук — доводка band/muq поверх зрения
Файлы: anchor/apply.py (оркестровка), anchor/maps/band.py (метрика), anchor/detect.py (общий аппарат детекта, тот же, что у зрения), anchor/assemble.py. Метод по умолчанию — band (DSP, 48 полос, без модели/лицензии); альтернатива muq (GPU-эмбеддинги, та же сборка).
<a name="7-зачем-аудио-после-видео"></a>
7. Зачем аудио после видео
Зрение положило дубль по картинке. Но между видео и аудио внутри самого файла бывает сдвиг, которого на картинке не видно:
- AV-sync delta — разный AAC priming / encoder delay (в JoJo измерено стабильные +89 мс между файлами,
BREAKTHROUGH_VIDEO_SYNC.md §4.4); - студийный сдвиг M&E — студия подвинула фон/музыку в миксе относительно своего видео;
- двойки — видео физически не различает соседние одинаковые кадры (предел ±83 мс), аудио снимает это ограничение.
Поэтому второй слой меряет остаточный сдвиг аудио дубля (уже лежащего по видео-карте) относительно аудио рефа и доводит его. Важно: аудио-слой работает после простановки тишины в монтажных резах — он должен видеть реф/тишину в вырезе (как боевой эталон), а не сырой несинхронный дубль, иначе на стыке зрение↔аудио появляется ложный краевой рез.
<a name="8-принцип-зрячих-свидетелей"></a>
8. Принцип зрячих свидетелей
Отчёт METHODOLOGY_audio_matching.md фиксирует намертво: наивная корреляция звука слепа, и это легко принять за «аудио измерить нельзя». Озвучка = чужой голос поверх той же музыки и эффектов (M&E). Прямой NCC сырого сигнала ловит несовпадение голоса и возвращает шум. Лечится не другим инструментом, а изоляцией инварианта ПЕРЕД корреляцией. У каждого искажения дубляжа есть то, что оно не трогает:
| Искажение дубляжа | Что губит | Инвариант (что не трогает) | Как изолируем |
|---|---|---|---|
| Чужой голос диктора | часть частотных полос | чистые полосы M&E | 48 лог-полос + взвеш. медиана |
| Другой мастеринг/тембр | тонкую структуру волны | динамику громкости | лог-огибающая RMS |
| Повтор музыкального такта | однозначность пика | окна с одиночным пиком | comb-veto |
Свидетели разной физики теряют чувствительность в разных местах → их сумма покрывает всю длину. Ниже подробно про основной свидетель боевого band — многополосную метрику (она же несёт оба первых инварианта: полосы против голоса, лог-энергия против мастеринга).
🖼️ ИЗОБРАЖЕНИЕ 8.1 — «Почему полосы видят сквозь голос». Спектрограмма окна озвучки: горизонтальными лентами выделены 48 лог-полос; полосы, занятые голосом диктора (≈300–3000 Гц), подсвечены красным («низкий контраст, малый вес»), полосы чистой музыки/эффектов — зелёным («высокий контраст, голосуют»). Снизу — та же сцена в рефе. Подпись: «Взвешенная медиана по полосам игнорирует испорченные голосом полосы — без знания, где голос».
<a name="9-band-метрика-48-полос"></a>
9. band-метрика: 48 полос
Файл anchor/maps/band.py. Рецепт NB48: , лог-полос в диапазоне Гц, окно win=5 с, STFT nfft=2048, hop=256 (⇒ кадровый темп огибающей Гц), агрегация wmedian, качество agree.
9.1. Полосовые огибающие (_benv)
Отступление: STFT и полосы. STFT (кратковременное преобразование Фурье) режет звук на короткие кусочки и для каждого показывает, сколько в нём энергии на каждой частоте, — получается спектрограмма «время × частота». Мы группируем частоты в 48 логарифмических полос (как деления на эквалайзере) и следим за громкостью каждой полосы во времени — это и есть «огибающая полосы». Лог-шкала частот ближе к слуху: низкие частоты дробятся мельче, высокие — крупнее.
Для окна сигнала берём STFT и мощность . Лог-частотная полосовая маска (границы — logspace(50,14000,49)) суммирует мощность по полосам:
Каждая полоса z-нормируется по времени (убираем общий уровень и масштаб — это инвариант к мастерингу):
log1p + z-score — это и есть «лог-огибающая громкости полосы»: динамика, общая у дубля и рефа, остаётся; тембр выкидывается.
9.2. По-полосный лаг через кросс-корреляцию (_om_core)
Для каждой полосы независимо считаем кросс-корреляцию огибающих рефа и дубля по времени (через FFT, знак даёт «правее=+»):
Выраженность пика полосы (насколько он торчит над фоном):
9.3. Агрегация: взвешенная медиана по полосам (agg="wmedian")
Вместо суммы корреляций (её подавляет голос) — по-полосный аргмакс-лаг и взвешенная медиана этих лагов по 48 полосам, вес = ():
Медиана робастна: даже если голос испортил треть полос и они показывают «не туда», медиана держится за большинство чистых. Финальный сдвиг уточняется параболической интерполяцией вокруг по агрегированной поверхности и переводится в кадры:
9.4. Качество = согласие полос (quality="agree")
Уверенность узла — доля полос, чей собственный аргмакс-лаг согласен с итоговым сдвигом (в пределах tol=2 кадра), взвешенная по выраженности:
Это самопроверка без внешней истины: если полосы единодушны — узлу можно верить; если разбрелись (повтор/тишина/голос везде) — мал, и детект/укладка такой узел не слушают.
muq как альтернатива. Метод muq заменяет полосовую огибающую на GPU-эмбеддинги музыкальной модели, но дальше всё то же — та же поверхность (o,w), тот же детект detect.py, та же сборка assemble.py. band не требует новых зависимостей (только torch/numpy) и потому выбран дефолтом.
<a name="10-грубая-off0--следящий-band"></a>
10. Грубая off0 + следящий band
Рабочее окно band узкое — MAXLAG=0.7 с (точность ценой диапазона). Но сдвиг опенинга бывает крупнее. Поэтому, как и в зрении, две стадии: грубая «тропа» + точное слежение (anchor/apply.py).
10.1. Грубая off0 (_coarse_off0)
Та же band-метрика, но с широким окном лага COARSE_LAG_S=2.5 с — она видит большой сдвиг. Робастность только статистикой, без порогов под контент:
- страж края — если аргмакс на границе с, это «нет пика», вес обнуляется;
- скользящая взвешенная медиана (окно
med_win_s=5с) сдвига; - гейт согласия соседей — узел стабилен, если
stab_tol=4кадра; - повторная медиана только по стабильным + интерполяция → непрерывная
off0(t).
10.2. Слежение (band.build_arr ±0.7с)
Дубль предварительно варпится по off0 (грубая коррекция), и band с узким окном с меряет остаток на пред-варпленном дубле. Полный сдвиг — сумма:
Так узкое точное окно никогда не «теряет» крупный сдвиг — он уже снят грубой тропой, а band доводит лишь малый остаток. (Урок EXPERIMENTS_coarse_band_follow.md: GCC в грубом проходе подавляется голосом, band-грубый с медианой по полосам — нет.)
<a name="11-детект-ступеней-и-кривая-дрейфа"></a>
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)
Простыми словами: сглаживание Надарая–Уотсона. Это «скользящее среднее с приоритетом надёжным точкам»: значение кривой в каждый момент — среднее соседних измерений, но уверенные точки (большой вес ) тянут сильнее, а шумные почти не влияют. Ширина «гауссова окна» задаёт, насколько далёких соседей ещё учитывать.
Внутри куска между резами строим гладкую кривую, которая следит за горками (плавный дрейф) и стоит на шуме. Это ядерное сглаживание Надарая–Уотсона с весом (, гауссово ядро с):
Поверх — потолок скорости изменения сдвига: между соседними узлами не больше lim, что соответствует ускорению max_pct_s (1.25 %/с = SMAX):
Невозможно разобрать выражение (синтаксическая ошибка): {\displaystyle \text{lim}=\frac{\text{max\_pct\_s}}{100}\cdot\text{STEP}\cdot\frac{1000}{\text{FRAME}}\approx0.15\ \text{кадра/узел}. }
Потолок не даёт кривой прыгнуть за единичными яркими выбросами в слепой зоне.
11.3. Опора после реза (фикс 2026-06-18)
Тонкость: в куске, который начинается с реза, band теряет чувствительность на самом монтажном стыке (тусклые выбросы сразу за резом). Если вести потолок скорости от левого края куска, он занижает старт, и кривая ~35 с медленно вытягивается к плато. Идея пользователя: расходиться в обе стороны от самого уверенного якоря в первых anchor_lookback_s=30 с куска (= начало плато):
сдвиг
▲ ● ● ● ● ● ● ● ← плато (уверенные якоря)
│ опора ↑ argmax w в первых 30с
│ ╱ ╲ clamp влево к резу + вправо вдоль плато
│ ✗ ╱ ╲
│ слепой
│ старт
┼──┊─────────────────────────────► t
рез
Слепой старт подтягивается к плато, а не плато к старту. Первый кусок (без реза слева) — по-старому: левый край = опора. Приёмка: 47/55 дорожек не тронуты, изменения = только исправления (память project_band_postcut_anchor_fix).
<a name="12-варп-тишина-в-резах-заполнение-рефом"></a>
12. Варп, тишина в резах, заполнение рефом
12.1. Кусочный варп (_warp_piecewise)
Дубль ресэмплится по кускам между резами: внутри куска — гладкая wcurve (горки), на резе — резкий стык. Для выходного отсчёта источник в дубле:
12.2. Тишина в резах
Непрерывный варп на монтажном стыке «переиграл» бы звук дважды, поэтому в резах ставится тишина (контент разносится):
- (нехватка дубля) → тишина по центру реза;
- (лишнее вырезано) → узкий шов с.
12.3. Заполнение тишины синхронным рефом (_fill_silence_from_ref)
Финальный проход — только после полного band/muq, когда дорожка уже синхронна рефу. Там, где озвучка молчит, а реф звучит, подставляем реф с кроссфейдом. Порог тишины измерен:
Порог дБ — в центре измеренного плато настоящих дыр (ниже ), далеко от обрыва тихого контента (), окно 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-окном ( с, полоса 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. Беглый взгляд на любой график даёт чёткий ответ «всё в порядке или нет». Поэтому график — не украшение, а главный инструмент приёмки и отладки.
<a name="13-зачем-графики"></a>
13. Зачем графики и что на них видно
Единый график строится на каждую пару и показывает оба слоя сразу: верхняя панель — зрение (видео-укладка, всегда), нижняя — звук (доводка band/muq, если включена), на общей оси времени с синхронным зумом и ховером. Один взгляд отвечает на четыре вопроса:
| Вопрос | Где смотреть |
|---|---|
| Держится ли укладка за надёжные данные? | цвет точек (уверенность): линия должна идти по ярким, не по тусклым |
| Есть ли необъяснённый перекос/увод? | отклонение линии от облака точек |
| Верно ли найдены монтажные резы? | вертикальные пунктиры и их подписи (мс) |
| Согласны ли зрение и звук? | сравнение двух панелей в одной зоне |
<a name="14-дрейф-форма"></a>
14. Дрейф-форма и робастная база Тейла–Сена
Сырой сдвиг по всей серии может быть сотни кадров (крупный масштаб оси), и на нём не видно ни плато, ни ступеней в десяток кадров. Поэтому обе панели рисуются в дрейф-форме — вычитается робастная линейная база, и всё разворачивается вокруг нуля.
Отступление: база Тейла–Сена. Чтобы провести «среднюю линию» через облако точек и не дать единичным выбросам её перекосить, берут наклон между каждой парой точек и выбирают медианный — половина пар «за», половина «против». Это устойчивее обычной прямой и не требует никаких порогов отбраковки.
База оценивается методом Тейла–Сена (_baseline) — он устойчив к выбросам, в отличие от МНК: на выборке точек (до 400) берутся все попарные наклоны, и базовый наклон — их медиана; сдвиг — медиана остатков:
База считается по телу дорожки (исключая голову/заставку: с). Затем:
а предел оси берётся как кадров — авто-масштаб под реальный разброс. На резах в линию вставляется NaN (_breaks), чтобы она не соединяла куски через разрыв — ступень видна как обрыв, а не как вертикаль.
🖼️ ИЗОБРАЖЕНИЕ 14.1 — «Сырой сдвиг vs дрейф-форма». Два графика друг над другом для одной дорожки: сверху сырой shift(t) (линия уходит на −600 кадров, мелкие детали не видны); снизу та же дорожка в дрейф-форме (база вычтена, всё колеблется в ±15 кадров вокруг 0, видны и плато, и ступенька реза). Подпись: «Дрейф-форма вытаскивает структуру, которую крупный масштаб прячет».
<a name="15-слои-и-разбор-кейсов"></a>
15. Слои, цветокодирование, разбор реальных кейсов
15.1. Слои единого графика
Верхняя панель (ЗРЕНИЕ):
- точки якорей , цвет = cos (viridis, 0…1) — надёжность каждого видео-якоря;
- оранжевая линия — карта зрения в дрейф-форме, с разрывами на резах;
- crimson-пунктиры — резы зрения с подписью величины (мс);
- заголовок несёт абсолютную медиану сдвига.
Нижняя панель (ЗВУК):
- точки , цвет = w (cividis, 0…1.2) — уверенность band;
- серые точки — gcc-свидетель ( с), справочно;
- зелёная линия — кривая 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 (ловит вызывающий).
Приложения
<a name="приложение-а-константы"></a>
Приложение А. Константы и их обоснования
Все ключевые числа калиброваны не «на глаз», а по плато нечувствительности: на свипе по диапазону результат (резы по датасету) не меняется — значит подстройки под контент нет.
Зрение
| Константа | Значение | Файл | Смысл / обоснование |
|---|---|---|---|
GW×GH
|
128×72 | features | размер серого кадра под SRM |
clip T
|
±3 | features | обрезка выбросов отклика фильтров |
| dim | 18432 | features | , два канала |
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 мин — только с явного согласия пользователя, оценку времени — заранее.
<a name="приложение-б-карта-файлов"></a>
Приложение Б. Карта файлов
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/**. При расхождении кода и документа — приоритет у кода, документ актуализировать.