ingest.json должны доехать до таймлайна Premiere без потерь. Проверено на живых сборках (YTCH10, YTCH13, 16–17.08.2026): у clip-API Premiere нет субкадрового рычага — всё, что не на кадровой сетке, он молча округляет, и миллисекундная точность Fine Sync умирает на последнем шаге. Этот этап решает задачу системно: всё на сетку точными тиками, миллисекунды — в сами WAV, а после сборки — проверка числом. Итог: синхрон петлички с видео ≤ 1 сэмпла (0.02 мс), и ни одна поломка сборки не проходит молча.Предпосылка: пройдены 3.2 и 3.3 — в ingest лежат выверенные wall_offset. Дальше:
P="/Volumes/DISK/YTXXNN_Project"
cd ~/YTAI/scripts/999_extra/wordsync_multicam
# 1. Рендер петличек в координаты таймлайна + патч ingest (идемпотентно)
python3 0113_frame_align.py --project "$P" --dry-run # посмотреть
python3 0113_frame_align.py --project "$P" # → "VERIFY OK: all placements on exact frame ticks"
# 2. Premiere: перезагрузить панель → вкладка Ingest → Build Ingest
# 3. Панель → Debug Dump (пишет 99_Pipeline/logs/{slug}_dump_*/)
# 4. Гейт числом — обязательный
python3 verify_build_sync.py --dump "$P/99_Pipeline/logs/<свежий дамп>"
# → PASS = таймлайн совпал с планом тик-в-тик. FAIL = не отдавать дальше.
После ЛЮБОЙ правки ingest (finesync, ручные offset'ы, пере-нарезка сцен) — заново 0113 → Rebuild → verify. Гварды устаревания (ниже) не дадут забыть.
Всё ниже — из тик-дампов реальных сборок (254 016 000 000 тиков/с; 54 точки данных YTCH13 + YTCH10), а не из документации Adobe (там квантование не описано вообще):
| операция | что делает Premiere со временем | следствие |
|---|---|---|
setSourceInOut (трим источника) | FLOOR к видеокадровой сетке — даже для аудио-only WAV | субкадровая in-точка невозможна |
createInsertProjectItemAction | ОКРУГЛЯЕТ позицию к БЛИЖАЙШЕМУ кадру (28/28 замеров: дробь <20 мс → вниз, >20 мс → вверх @25p) | субкадровая позиция невозможна; компенсация «дробь в позицию» не работает |
createOverwriteItemAction | on-grid тики проходят точно; float-секунды опасны (double 109.44 лёг на +1 кадр) | позиции слать только целыми тиками кадра |
| compound-транзакция | исполняет свои действия В ОБРАТНОМ порядке | батч overwrite'ов: хвост раннего клипа съедает голову позднего → видео класть по одному, по возрастанию времени |
| фабрики действий (25.6) | работают только внутри lockedAccess — иначе «Requires locked access» | создавать action внутри колбэка транзакции, никогда до |
| «вставлено ok» | ≠ «элемент на таймлайне» (90_CTA: «Placed 3 TX, 0 skipped» — на дорожке 1) | после сборки — обязательный пересчёт фактических элементов |
Вывод, на котором стоит весь этап: субкадрового рычага у clip-API нет. Поправки fine-sync субкадровые и у каждого видеоклипа свои → остаток на каждом слайсе РАЗНЫЙ (−31.6…+19.9 мс на YTCH13 до фикса), одним сдвигом файла не лечится. Единственный сэмпл-точный носитель — сама медиа.
sceneLayout.gridAlignPlan: позиции видео и слайсов, in/out-точки — целые кадры, отправляются через TickTime.createWithTicks (float-секунды типа 109.44 сидят на пол-ULP мимо сетки и ловят ±1 кадр). Блок «видео + петля» якорится к кадру ЦЕЛИКОМ — ступенек на границах V/A нет по построению. Первый блок сцены якорится в 0 — мёртвый воздух до первого видео схлопывается (петличка часто стартует раньше камеры). Видео кладётся последовательными транзакциями по возрастанию времени.
0113_frame_align.py рендерит на каждую (сцену × петличку) один WAV, где время файла = финальное время таймлайна: контент каждого слайса лежит ровно на том сэмпле, где слайс встанет (пер-слайсная компенсация из content_src_sec плана). Билдер кладёт identity-слайсы (offset == source-in, оба на тиках) — Premiere нечего округлять. Синхрон ≤ 1 сэмпла. Пути оригиналов петличек сохраняются в tx_strips_source — локально на SSD они остаются в 01_Source/{сцена}/, пересборка читает их оттуда; на Drive оригиналы архивируются в 99_Pipeline/DJI_Audio/{сцена}/, понадобилась пересборка — вернуть их в 01_Source локально и перегнать 0113. Бэкап ingest пишется рядом, повторный запуск идемпотентен. Хэндлы работают: потянуть край слайса = открыть соседний контент таймлайна.
Три уровня. READBACK в билдере: после сборки панель пересчитывает фактические элементы по дорожкам против плана — «items VANISHED» / «stray items» видно прямо в логе сборки. Гварды устаревания в плане: рендер несёт отпечаток раскладки (block spans + wall_offset источников) — любое изменение после 0113 даёт tx_td_render_stale / tx_td_sources_changed. verify_build_sync.py: дамп секвенций против плана тик-в-тик — счётчики по каждой дорожке (включая камера-аудио), позиции, in-точки, длительности; допуск 2 мс; exit 1 = не отдавать проект дальше.
Единый источник правды: tools/plan_cli.js гоняет ТОТ ЖЕ чистый код раскладки, что и панель — 0113 и verify считают ровно то, что построит Build. Мок в тестах моделирует реальное квантование (floor/nearest/реверс/lock) — регрессии этого класса ловятся до Premiere.
Сначала --dry-run: таблица «сцена × петличка × слайсы», длины рендеров и coverage gaps (файл рекордера кончился раньше блока видео — там будет тишина; десятки мс на стыках 30-мин файлов — норма). Затем боевой запуск: рендеры {TX}_{сцена}_timeline.wav ложатся в 01_Source/{сцена}/ рядом с оригиналами, ingest патчится. Так — локально на SSD; на Drive и в прокси-комплекте монтажёра в 01_Source живут только *_timeline.wav — оригиналы рекордера (*_orig_*.wav) уезжают в архив 99_Pipeline/DJI_Audio/{сцена}/. Обязателен финал «VERIFY OK: all placements on exact frame ticks» — иначе не продолжать.
Перезагрузить панель (если код обновлялся) → вкладка Ingest → Build Ingest. В логе смотреть: warnings плана (не должно быть tx_td_*), «Placed N video + M TX», и READBACK-строки — любой «VANISHED»/«stray» = сборка плохая, в Premiere дальше делать нечего.
Панель → Debug Dump → verify_build_sync.py --dump …. PASS = этап закрыт, можно резать. FAIL печатает точный элемент и величину в мс — это и есть тикет на разбор, гадать не нужно.
| сигнал | что значит | что делать |
|---|---|---|
tx_unaligned_media | петличка кладётся из оригинальных WAV — 0113 не прогнан; синхрон деградирует до ≤½ кадра на слайс | прогнать 0113 → Rebuild |
tx_td_render_stale | раскладка видео изменилась ПОСЛЕ рендера (finesync, offset'ы, пере-нарезка) — звук в рендере устарел, хотя позиции выглядят идеально | 0113 заново → Rebuild |
tx_td_sources_changed | wall_offset источника петлички в tx_strips_source отличается от запечённого в рендер | 0113 заново → Rebuild |
tx_td_render_unverifiable | рендер без отпечатка (старый 0113) — проверить свежесть невозможно | 0113 заново → Rebuild |
| READBACK: «items VANISHED» | транзакция отчиталась ok, но элементов на дорожке нет (класс 90_CTA) | лог сборки → повторный Build; если стабильно — разбор с дампом |
| READBACK: «stray items present» | лишний элемент — обычно огрызок seed-плейсхолдера 1 кадр в нуле верхней V/A (зачистка в 25.6 нестабильна) | удалить руками (верх V + его аудио); гейт продолжит напоминать |
| «Requires locked access» в логе | код панели создаёт action вне lockedAccess — регресс билдера | панель обновить/чинить; тесты мока этот класс ловят |
| coverage gap в 0113 | рекордер выключился раньше видео — в рендере тишина, величина указана | прочитать и принять (или знать, где нет петли) |
| verify: position/source_in off | Premiere положил не туда, куда просили | сверить fps секвенции с планом (seed!), см. лог «sequence timebase» |
Годно: verify_build_sync.py → PASS (все элементы всех дорожек всех сцен в пределах 2 мс от плана; фактически — 0 тиков) и ни одного tx_td_*-warning'а в плане. Проверять ВСЕ сцены — чинить по одной и сломать соседнюю здесь уже нельзя, но правило остаётся.
| значение | |
|---|---|
| обещание fine-sync в числах (ingest) | невязка ≤ 0.4 мс (24 пары, YTCH13) |
| на таймлайне ДО этапа | остаток −31.6 … +19.9 мс, у каждого слайса свой; ступеньки V/A; +1 кадр на overwrite; пропавшие слайсы |
| на таймлайне ПОСЛЕ (живая сборка 17.08.2026) | 26/26 identity-слайсов тик-в-тик (0 расхождений по A-дорожкам петличек); контент ≤ 1 сэмпла (0.02 мс) |
| рендер + патч ingest (шаг 1) | 999_extra/wordsync_multicam/0113_frame_align.py |
| гейт после сборки (шаг 3) | 999_extra/wordsync_multicam/verify_build_sync.py |
| офлайн-план = код панели | 0500_uxp/tools/plan_cli.js |
| grid-раскладка (чистая логика + тесты) | 0500_uxp/src/ingest/layout/sceneLayout.js · gridAlignPlan · tests/ingest/wallclock/ |
| размещение (тики, lock, порядок) | 0500_uxp/src/ingest/placement/clipPlacer.js · wallClockBuilder.js |
| мок с реальным квантованием Premiere | 0500_uxp/tests/mocks/premierepro.js |
| шаги в конвейере | docs/wordsync/WORDSYNC_RUNBOOK.md — Шаг 4.6 + ГЕЙТ 6.5 |
| полная история вопроса (постановка → решение) | {YTCH13}/HANDOFF_frame_shift.md |
| предыдущий слой (звуковая корреляция) | /kb/fine-sync/ |