.words.json, тайминги до миллисекунд), эти транскрипты и становятся якорем синхрона.creation_time в контейнерах. Не нужно: для одной камеры (линейная сборка) и когда есть общий таймкод (jam-sync).истина = creation_time + Δ_cam. Одно Δ на камеру на день — за смену дрейф кварца незаметен (<1с).T_k (имя файла — только начальное приближение).r_ik — позицию начала клипа на таймлайне куска: creation_time_i + Δ_cam(i) ≈ T_k + r_ik.ct + Δ_cam — она уже откалибрована остальными клипами.
t_chunk − t_clip. Плотный кластер голосов = кандидат-совпадение; медиана кластера = измерение. ASR-ошибки не мешают: голосуют только совпавшие триграммы, а их тысячи.
--ref-cam): её клипы становятся якорным пулом, рамка = её часы, T фиксированы. Остальные камеры пришиваются рёбрами. Важно: в якорный пул идут все транскрипты опорной камеры (даже клипы, не попавшие в выборку) — больше рёбер, точнее Δ. Так собрано интервью YTUVI-706: ZVE1 пришит к FX3A (кварц ZVE1 спешил на +3м15с).Синхрон, который нельзя проверить, не существует. Каждый уровень ловит свой класс ошибок; все пять зелёные → можно строить таймлайн.
wordsync.py --selftest — сгенерированный «день» с врущим именем куска и настоящим эхом дубля. Проверяет и решатель, и защиту от эха (echo_rejections ≥ 1). Гоняется после любого изменения кода.ct + Δ_cam. Норма < 1с (тип. 0.2–0.6с). Большой резидуал = клип пришит к чужому дублю или у камеры нестабильные часы — смотреть руками.gap ≈ 0. На YTUVI-704 сплит-пары сошлись в +0.006с / −0.054с / +0.014с — независимое подтверждение всей системы. Зазор в секунды = решение неверно.wall_offset до/после — идентичны до мс. Проверяется diff'ом двух прогонов.validate_ingest.py — все требования UXP-билдера, включая ТИХИЕ отказы (fallback в linear, выпадение петли, «unknown_cam», коллизии имён — билдер резолвит клипы по имени глобально, полосы одной метки петлички внахлёст больше кадра). qc_html.py — тёмная витрина: таймлайн дня, часы камер, поправки имён, рёбра/эхо/резидуалы каждого клипа, первые слова матча.Все пять уровней проверяют внутреннюю связность: решение против самого себя, ингест против контракта, сборку против плана. Вопрос «а звучит ли готовый таймлайн синхронно» не задаёт никто — и дефект, сидящий внутри кольца, проходит все пять.
ЧТО СПРАШИВАЛИ ОТВЕТ
validate_ingest.py «ингест по контракту?» VALIDATION PASSED
prproj_verify.py «таймлайн по плану?» сошлось 21 из 21 ✅
pair_check «камеры друг другу?» 37 из 37, 14.2 мс
ЧЕГО НЕ СПРАШИВАЛ НИКТО
«а звучит синхронно?» −28.7 с
YTUVIE01, 25.09.2026: всё зелёное, петличка стоит на 28,7 с мимо. Обязательный шестой уровень — замер по самим медиафайлам, снаружи кольца: 4.4 Sync Verification.
~/YTAI/scripts/999_extra/wordsync_multicam/ — README там = полный контракт билдера и грабли. Подробный ранбук для пошагового исполнения (в т.ч. слабыми моделями): scripts/05_editing/0500_uxp/docs/wordsync/WORDSYNC_RUNBOOK.md.
# 1. решить день (петля) python3 wordsync.py --day /Volumes/DISK/DAY --cam-map "CAM1=CAM1_Name,..." # 1'. решить день (без петли) python3 wordsync.py --day /Volumes/DISK/DAY --ref-cam CAM1-FX3A --only-file selected.txt # 1''. две системы часов на ±1 ч (спаренные DJI + Sony против TX00/Pocket) — # сдвиг группы в решении, пишется в _reframed; повтор с той же меткой отказывает python3 reframe_result.py --project "$P" --sec -3600 --chunk-re '^TX0[1-9]_' \ --cams CAM-A_FX3,CAM-B_ZVE1 --tag dubai_clock --why "…" --dry-run
make_ingest.py собирает одну сцену, а у съёмочного дня их десяток и больше; ingest из одной сцены панель вдобавок уводит в линейный сборщик. Мост зависит от того, чем разобран день (3.1):
cd ~/YTAI/scripts/999_extra/wordsync_multicam python3 retarget_result.py --project "$P" # пути решения → файлы ВНУТРИ проекта # (видео 01_Source_Proxy/{сцена}/{камера}/, звук 01_Source/{сцена}/Sound/) python3 scene_stems.py --project "$P" # списки клипов по сценам + прогноз коллизий имён петличек python3 build_day_ingest.py --project "$P" --dry-run python3 build_day_ingest.py --project "$P" # все сцены: первая начисто, остальные --add
Порядок камер — из паспорта дня (00_Setup/06_Day/*_day_passport.json), ничего руками не набирается. Драйвер зовёт make_ingest.py --tx-label device --tx-scene-suffix: дорожка петлички = физический передатчик DJI-N, а полоса, попавшая в несколько сцен, получает суффикс сцены (…_orig__S08.wav). Сцены другого дня — 00_Setup/01_Ingest/extra_scenes.json, драйвер доливает их после паспорта.
cd ~/YTAI/scripts/01_prepare/0121_studio_day python3 ingest_src.py --block 1 # сухой прогон python3 ingest_src.py --block 1 --apply # камеры + петличка срезами уроков # … finesync (4.2) … python3 ingest_src.py --block 1 --screens --apply # дорожка экрана — строго ПОСЛЕ finesync, ДО 0113
Сцена = урок ({NN}_{слаг}), симлинков нет (--no-symlink): файлы уже лежат деревом курса. Петличка — срезы уроков lavcut.py (wall_offset 0). Запись экрана кладётся с нуля урока отдельным проходом: в графе finesync она держится одной слабой связью и получила бы отрицательный wall_offset.
python3 make_ingest.py --project "$P" --scene 05_Interview --add \
--result .../wordsync_result.json --cam-order CAM1,CAM2 # мультикам-сцена
python3 make_ingest.py --project "$P" --scene 04_Stones --add \
--linear --files-from stones.txt --folder STONES # линейная (selects/b-roll)make_ingest.py пишет на клип audio_streams и probe.fps/width/height/rotation, на сцену — scene_fps (50p, только если вся сцена 50p, иначе 25p). Только такой ingest панель строит из .sqpreset с точным числом дорожек, без посева и огрызков. Старый ingest дозамерить без пересборки: make_ingest.py --probe-audio --probe-fps --ingest <ingest.json> (бэкап *.bak_probe_*.json сам).
# проверки + витрина python3 validate_ingest.py "$P/00_Setup/01_Ingest/{CODE}_ingest.json" python3 qc_html.py --result .../wordsync_result.json --tr .../Transcription --out qc.html # дальше по порядку — каждый шаг меняет то, что читает следующий 4.2 finesync_run.py --apply → [экран] → 4.3 0113_frame_align.py → ingest_markers.py --project "$P" --apply # маркеры говорящих — последними (время через block_spans плана) → Premiere: Build Ingest → Verify Sync
Маркеры: название — мысль реплики, цвет — кто говорит (marker_titles.py --titles <json> --apply). После повторного ingest_markers.py заголовки накладываются заново.
Ingest готов и провалидирован → дальше всё делает UXP-панель «YTAI Assembly». Дорожки раскладывает не Premiere и не ты — их на 100% задаёт ingest. Ниже — как загрузить, собрать и убедиться, что легло ровно.
{PROJECT}.prproj. Панель грузится через Adobe UXP Developer Tool: Add Plugin → выбрать ~/YTAI/scripts/05_editing/0500_uxp/manifest.json → Load. В Premiere появится панель «YTAI Assembly» (id com.ytai.assembly.panel). Если панель уже грузилась раньше — в UDT просто Load/Reload.{PROJECT}/00_Setup/01_Ingest/{CODE}_ingest.json (код проекта берётся из имени папки по ^(YT[A-Z]{2,4}\d+)_). Кнопка Build Ingest должна стать активной.files:{} или битый JSON. Прогони validate_ingest.py — он назовёт точную причину.layout_mode:"wallclock", хоть у одного клипа нет числового wall_offset или в ingest всего одна сцена). Мосты дня это исключают, но если ingest правили руками — validate_ingest поймает.{CODE}_{scene} (напр. YTEVO03_04_…); готовые секвенции панель убирает в бин 00_Source_Timelines. Какие сцены строить — галочки «Sequences to build» (new · all · none). У измеренного ingest секвенция рождается из .sqpreset сразу с частотой scene_fps и точным числом дорожек; без замеров — прежний путь от посевного клипа, с однокадровыми огрызками (лог называет, каким путём собрана каждая сцена). Клипы камеры лягут на свою V-дорожку, паузы между блоками видео билдер вырезает и ставит на стыках Comment-маркеры с величиной пропуска.*_old не нужно. Остальные сцены не трогаются.cam_layers[scene] в ingest, слева направо: первый элемент → V1, второй → V2… Задаётся флагом --cam-order у make_ingest.py. Имена в массиве обязаны байт-в-байт совпадать с именами папок-родителей клипов (симлинк-папки камер) — иначе клипы камеры молча выпадут (warning unknown_cam в логе панели).audio_streams из ingest): у FX3/ZV-E1 один поток — одна дорожка, у DJI Pocket 4 Pro два — две. Поэтому номер A-дорожки совпадает с номером V только когда у всех камер по одному потоку.--tx-label device → физический передатчик), нарезанными полосами строго под видео (там, где видео нет, петли нет — это норма). Источник петли в секвенции — timeline-рендер {TX}_{scene}_timeline.wav от 0113_frame_align.py: один на пару сцена × передатчик, лежит рядом с исходной полосой (полевой день — 01_Source/{сцена}/Sound/, студийный — Sound/ урока), формат 1:1 с источником — Float32 mono, паузы = тишина. Ingest (tx_strips) и секвенции ссылаются только на рендеры; исходные полосы нужны лишь пересборке (пути в tx_strips_source).node tools/plan_cli.js --ingest … (cam_audio, tx_aIdx_base, a_tracks_needed).SYNC / DESYNC / INSTRUMENT (контроль петличка ↔ петличка не ноль — прибор сломан) / NO_DATA. Подробно — 4.4 Sync Verification.verify_build_sync.py — легло ли по плану тик-в-тик (4.3).Sync Audio: она двигает элементы таймлайна мимо ingest. Правильный цикл:--cam-order (для дня — поправить паспорт и перегнать build_day_ingest.py; wall_offset'ы не меняются) → Rebuild отмеченных сцен;wall_offset (finesync или Spread → Synchronize → Collect, 4.2) → 0113_frame_align.py заново → Rebuild → Verify Sync;TX01…TX04 берут время у приёмника → телефона (UTC+4), неспаренный TX00 и Pocket — МСК; Sony по речи ушли к спаренным. Внутри групп всё сходилось, поэтому валидатор и finesync молчали; признак — correction_sec полосы ≈ ±3600. Лечение — reframe_result.py, затем retarget → scene_stems → build_day_ingest. Там же: слот TX00 — не устройство (под ним писали разные передатчики, билдер обрезал полосу на 112 мин) → --tx-label device; 14 полос из 44 попали в несколько сцен → --tx-scene-suffix; короткие дубли Pocket по 5–60 с — полоса < 30 с берётся, если она ≥ 3 с и хотя бы наполовину лежит под клипами.
--screens. Все сверки с планом дали PASS, а петличка стояла на 28,7 с мимо — дефект сидел внутри кольца «планировщик → план → билдер» (4.4). Отсюда Verify Sync как обязательный шаг после Build.