broll, плашка — чем человек важен зрителю). Рез — в паузе между словами с точностью до кадра, с автопроверкой (edges, edges.strict). Новых сущностей не создавать: шортсы в 04_Shorts, новая версия номером в имени, прежняя рядом.Фиолетовая рамка — агент Draper; пунктир — человек. Из кода этапа ни LLM, ни VLM не вызываются: отбор — агент по заданию, проверка — код.
shorts_master.py words · map · build · verify --head N --name CODE_vN: чистый мастер 4K на времени фильма — камера во весь кадр, речь и воздух без музыки и графикиsrc_master.py plan · video · audio · encode · words в _session_tools проекта: 7 отрезков двух камер, цвет и чистка речи фильма, −14 LUFS, без музыкиfind4k по Drive проекта (видео горизонтальное, длина ±2 с, меньшая сторона ≥ 2160) → иначе 4K-экспорт того же монтажа у монтажёраcheck сверяет новый мастер по звуку с эталоном master_ref.m4a: ✅ тот же · ⚠ сдвиг → master_offset · ⛔ другой монтажkeep_out считается из листа экранов тем же временем, что ставило их в фильм (±0,12 с, 58 мест), scope broll: кусок со звуком идёт через экран, а его картинку закрывает перебивка — проверка check_cover.py «всё закрыто»edl_vN.json: каждый кадр между склейками, что в нём, части «можно брать» вне keep_out — по нему Draper сам ставит broll (дети, малыш, вещи, дом)captions.burn: false) + дорожки RU/EN у каждого шортса; Инстаграм — render --insta в 04_Shorts/Instagram; реплики — из вычитанных субтитров фильма (ru + en)probe на финальном мастере: цвет новой версии развернул оттенки (у героини тёплый серый, у куратора холодный), допуск ±9; два лица на диване, где крупнее молчащая, — рамка на говорящей руками, перебивками «губы в такт» (тот же кадр фильма)edges --fix + edges.strict: рез в паузе, до кадраКаждая версия оставила проверку: мелькнувший кадр (v1), поле broll с проверками (v2), край в паузе (v3). Счёт кадров v1 — README этапа.
| Движок | Где в коде | Где работает · замок | Веса, ключи |
|---|---|---|---|
| mlx-whisper large-v3 (word_timestamps, condition_on_previous_text=False; --language ru) | wordrole_transcribe.py | мак; замок сам не берёт — запускать под mlx_queue.py | HF-кэш |
| pyannote speaker-diarization-3.1 | wordrole_transcribe.py | мак, MPS | ~/.cache/huggingface/token |
| torchaudio MMS_FA (CTC, with_star) | edges.py | мак, процессор; flock mlx.lock, ждёт до 1800 с | ~/.cache/torch/hub/checkpoints/model.pt — 1,26 ГБ, вне git |
| YuNet cv2.FaceDetectorYN (score 0,6, NMS 0,3, top_k 50) | render.py · scout.py | мак | environment/models/face/yunet.onnx — вне git |
| Агент Draper (rya-draper) по PICKS_PROMPT.md | transcript → picks.json | Claude Code; модель сессии | из кода LLM не вызывается |
| headless Chrome (--headless=new, virtual-time-budget 8000, прозрачный фон) | gfx.py | мак | шрифт с Google Fonts — без сети подменится |
| HyperFrames 0.8.106 + GSAP 3.14.2 (lint → render --format mov --workers 1) | viz.py | мак | ProRes 4444 с альфой 1080×1920 |
| Epidemic Sound MCP (JSON-RPC без Claude: SearchRecordings, EditRecording, Poll, Download) | music.py | мак, работает и из launchd | EPIDEMIC_SOUND_API_KEY или ~/.config/ytai/epidemic_sound.env (600) |
| ffmpeg libx264 -preset slow -crf 12 -maxrate 40M, AAC 320k 48 кГц, +faststart; склейки -fps_mode passthrough | render.py · job.py | мак; без videotoolbox и без очереди рендера | субтитры — поток PNG (libass нет); эталон звука — моно 8 кГц AAC 24k |
| YouTube Data API v3: videos.insert (resumable, 8 МиБ, дедуп по названию) · list(status) · update → public | upload_youtube.py · shorts.py · scheduled_publish.py | мак; --sni-bypass через www.googleapis.com, IPv4 | токен канала в channels.json: youtube.upload + force-ssl |
| Telethon: SaveBigFilePart 512 КБ × 8 потоков, до 2 ГБ | tg_send.py | /opt/homebrew/bin/python3 | ~/.config/ytai/tg_user.session |
| rclone copy --bind en0 --drive-chunk-size 64M; Drive API v3; Sheets v4; Trello REST | shorts.py drive · ledger.py | мак | gdrive_rscore · token.json · trello.env |
Memex этап не использует. render_gate.py — только у src_master/shorts_master (сборка мастера), не у render.
Каждый шаг пропускает шортс дальше только через свой порог; подробно — в таблице «Проверки» ниже.
edges --fix: CTC + огибающая + кадр✓ ≤ 0,30 с после · ≤ 0,15 доrender --version N → 04_Shorts⇒ ⚠ мелькнул кадр, растяжение > ×1,25✓ edges.strict: рез в слове — стопsheet — кадр из каждого куска рамки; черновики — файламиupload, --sni-bypass✓ forbid · ~10 в сутки → pending.pyschedule → launchd раз в 5 мин → public⇒ сбой → ⛔ в TelegramКод пускает 20 ≤ dur < 60 с, отборщику задано 25–59 (job.py: duration 20…60).
Не ближе 0,05 с к чужому слову; смычка согласной 40–110 мс — не пауза; рез на границе кадра.
Держится plate.seconds 2,8 с (YTFP 3,6); место, где простоит ≥ 1,5 с; гаснет со сменой кадра.
Мастер 1080p → find4k по Drive проекта, check по звуку; невышедшие — новой версией.
| Проверка | Когда | Норма | Откуда взялась |
|---|---|---|---|
| Длина куска | picks · render (гейт ok) | 20 ≤ dur < 60 с (отборщику задано 25–59) | YouTube считает шортсом ≤ 60 с |
| Запретные участки | picks · render | кусок не задевает keep_out (all | pieces); перебивка — не из keep_out (all | broll) | красная линия канала, мультфильм и песня — чужой материал (YTFP01) |
| Упаковка | picks · upload | title ≤ 70 знаков; slug 2–6 слов; нет forbid в title/description/tags (подстрока без регистра) | ссылки и стоп-слова канала («Сколково» у YTTRBM) |
| Уточнения и выноски | picks | id внутри кусков; текст без скобок совпадает с репликой ≥ 0,7; выносок ≤ 2 | выноска вставала на чужую реплику |
| Перебивки broll | picks · render | начало ≥ broll.from; длина ≥ 1,0 с; src внутри мастера; x 0…1, zoom 1…2; без наложений; зазор либо встык, либо ≥ 1 с; край ближе 0,3 с к смене рамки прилипает к ней; короче 2 кадров — не ставится | YTFP01 v2: поле broll новое, каждая ошибка — отдельная проверка |
| Край в паузе | edges · render | после последнего слова ≤ 0,30 с тишины, перед первым ≤ 0,15; не ближе 0,05 с к чужому слову; порог тишины 5-й перцентиль + 0,35·(95-й − 5-й); пауза < 0,12 с — рез посередине; рез на границе кадра; допуск сверки 0,011 с | YTFP01 v2: «хоро|шо», «молод|цы», хвост «-тей» |
| Буква не чужая | edges | буква, отделённая тишиной > 0,15 с, отбрасывается; слова слиты (склейка выпуска) — рез на склейке в ±0,15 с | крайняя буква цеплялась за похожий звук соседа |
| edges.strict | render | ⛔ край → не собирать (обход --no-edges) | решение Романа 02.10: автопроверка реза |
| Мелькнувший кадр | render → warn → status | полоса кадра crop=iw:ih*0,375; всплеск > 12 в ≥ 60 % блоков и > 4× 75-го перцентиля соседей; две склейки ближе max(2 кадра, 0,1 с) | YTFP01 v1: 12 кадров нового ракурса со старой рамкой |
| Склейка | render | разность кадров 192×108 > max(14, 5×медиана), не чаще 0,2 с; склейки < 0,1 с вливаются в соседнюю | короткая склейка выбрасывалась — картинка короче звука на 62 мс |
| Рамка | render | лицо ≥ close_face 120 px → крупный план; наезд, если лицо видно ≥ 60 % речи; дорожка ≥ 12 % замеров; имя по одежде ≥ 30 % и ≥ 2 замера; держится в пределах 70/50 px, не чаще раза в 1,2 с | рука и тень давали «лицо»; ведущая затылком |
| Плашка | render | только крупный план и речь ≥ 1,0 с; держится plate.seconds (2,8; YTFP 3,6), ищется место, где простоит ≥ 1,5 с; гаснет со сменой кадра; не раньше plate_from | YTFP01 v1: плашка героини легла на педагога из перебивки |
| Растяжение кадра | render → warn | ≤ ×1,25 (⚠ выше) | YTTRBM01: из 1080p ×1,78–2,37, мягко |
| Фейды | render | не длиннее тишины у края, минимум 5 мс (по умолчанию 0,08 / 0,15) | фейд 40 мс съедал хвост гласной |
| Та же версия уже залита · файл версии уже лежит | render | отказ (обход --force) | нельзя затереть прежнюю версию в 04_Shorts |
| Мастер | render · check · find4k | 16:9; check: окна 12 с, сходство ≥ 0,3, FRAME 45 мс → ✅ same / ⚠ offset / ⛔ other; find4k: длина ±2 с, сторона ≥ 2160 | тишина давала «сходство 124» — тихие окна не сравниваются |
| Заливка | upload | пропуск при forbid, ok=false, нет файла; 300 с без вывода — стоп; uploadLimitExceeded → стоп сразу + команда pending.py add; прочие сбои — до 40 повторов | 40 повторов упирались в лимит |
| График | schedule | --start с поясом; шаг --every или cadence_hours; replaced снимаются; ⚠ приватный; ⛔ задача launchd не загружена; без --start нужно прежнее время в будущем | publishAt делал ролик приватным |
| Публикатор и доливка | launchd | 3 повтора запроса; удалён → gone + ⛔; сбой → ⛔ сразу и не чаще раза в час; доливка не позже чем за 20 мин до срока; залито, но не в графике → ⛔ | первые боевые срабатывания — 03.10 |
| Что на YouTube | status | вышел / по ссылке / ⚠ приватный / ⚠ нет на YouTube / ⚠ не в графике; прежняя версия в графике; все warn | после каждой заливки — запросом к YouTube, не по журналу |
| Звук = видео по длине | — | отдельной проверки пока нет | хэндофф «Что дальше»: одна команда приёмки |
| Файл | Что хранит | Кто и когда пишет |
|---|---|---|
| YTs/{CH}/shorts_profile.json (ytai-shorts-profile-v1) | channel · accent · gfx · captions.{font,size,max_width,bottom,…} · plate.{x,y,seconds,min_speech} · brand · hosts.<Имя>.{plate,role,seat} · youtube.{slug,category,language,tags} · cadence_hours · forbid[] · rules[] · hashtags[] · sheet · trello · broll.from · edges.strict | сессия по решению Романа, причина в *_note (YTFP: broll_note, edges_note, plate_note); новый канал — копия у YTTRBM |
| YTs/{CH}/shorts_gfx.html | #brand, .plate, .plate.lead, <!--PLATES--> | руками по бренду; .plate.lead есть у YTFP — другим каналам добавить до использования |
| {CODE}_shorts_job.json (ytai-shorts-job-v1) | master · cues · roles · picks · version · master_offset · people.<Имя>.{wear, seat, plate | {lines, sub}, role, big_face} · wear_order · tags · drive · masters · drive_work · keep_out[{from,to,why,scope}] · crop.* · edges · music.below_db · captions.max_width · pad · audio_fade | человек или агент на выпуск; факты дня съёмки (одежда) — только сюда, не в профиль |
| picks.json | candidates[{rank, take, pieces[{from_id,to_id} | {start,end}], scores, why, title, description, slug, clarifications, viz, broll, plate_from}] | Draper; broll — человек по каталогу scout; edges --fix переписывает куски (3 знака), сдвигает broll.at и plate_from |
| PICKS_PROMPT.md | задание отборщику: {PROJECT} {TITLE} {CHANNEL} {TRANSCRIPT} {SPEAKERS} {CONTEXT} {HASHTAGS} {RULES} {PICKS} {CHECK} | сессия при новом правиле отбора |
| edges.json · out/manifest.json · master_ref.m4a | кэш краёв; n, name, dur, ok, pieces, cuts, broll, warn, upscale, viz, music, video_id, replaced, publish_at; эталон звука мастера | код; манифест после каждого шортса |
| broll/README.md · broll/cuts.json | занятые кадры выпуска (один кадр не повторять в двух шортсах); склейки мастера | человек; scout cuts |
| ~/.config/ytai/publish_schedule.json · shorts_pending.json | [{channel, video_id, publish_at, done, error_at, gone}] · [{job, only, not_before, told_at}] | schedule, pending.py add; читают задачи launchd |
| README этапа «Каноны», «Грабли»; 07_shorts/README.md; CLAUDE.md «Shorts» | каноны этапа и имена {CODE}_S{NN}_v{N}_{slug} | сессия после ошибки, тем же коммитом |
| память: reference_shorts_finished_pipeline · feedback_shorts_show_subject_broll · feedback_shorts_from_sources · reference_whisper_word_edges_unreliable | решения Романа с «почему» | сессия в конце работы |
Каждая — из настоящего прогона; где не указан проект, грабля общая для этапа.
V=~/YTAI/environment/.venv_transcribe/bin/python; D=~/YTAI/scripts/07_shorts/0701_finished
S=$D/shorts.py; C=$D/scout.py; JOB={проект}/00_Setup/07_Shorts/{CODE}_shorts_job.json
python3 ~/YTAI/scripts/999_extra/mlx_queue.py wordrole -- $V ~/YTAI/scripts/999_extra/wordrole_transcribe.py --media <мастер> --language ru --plain --num-speakers 2 --speaker-names 'Имя,Имя' --out-dir $(dirname $JOB) # слова с ролями
$V $S transcript --job $JOB # задание отборщику → агент Draper пишет picks.json
$V $S picks --job $JOB # проверка отбора: длина, запреты, keep_out, перебивки
$V $C cuts --job $JOB && $V $C sheets --job $JOB --ranges 0:51-1:00 && $V $C faces --job $JOB 'src,dur' # каталог кадров для перебивок
$V $S probe --job $JOB --t 4:45 8:25 && $V $S gfx --job $JOB # одежда и места; знак и плашки
$V $S edges --job $JOB --fix # края кусков в паузы между словами
$V $S render --job $JOB --dry # план по секундам: рамка, перебивки, плашки
$V $S render --job $JOB --version 1 # шортсы → 04_Shorts; сам сверит края и мелькания
$V $S sheet --job $JOB # лист кадров — смотреть глазами
$V $S upload --job $JOB # YouTube по ссылке; при лимите печатает команду pending.py add
$V $S telegram --job $JOB && $V $S drive --job $JOB && $V $S ledger --job $JOB # сдача
$V $S schedule --job $JOB --start 2026-10-04T10:00:00+03:00 --every 24 # только по слову Романа
$V $S status --job $JOB # что на YouTube и что осталось$V $S viz --init --job $JOB && $V $S viz --render --job $JOB # выноски HyperFrames $V $S music --find 'calm piano' --job $JOB && $V $S music --track <id> --only S02 --job $JOB # музыка под длину $V $S find4k --job $JOB && $V $S check --job $JOB # 4K-мастер и сверка по звуку $V $S render --job $JOB --version 2 --only S01,S03 && $V $S upload --job $JOB && $V $S schedule --job $JOB # новая версия: три шага $V $D/pending.py add --job $JOB --only S04,S05 && $V $D/pending.py list # очередь доливки $V ~/YTAI/scripts/05_editing/0510_autocut/shorts_master.py --project $P words · map · build · verify --head N --name <CODE>_v6 # чистый мастер из собранного фильма