ДИЧЬ · sandbox/oko · инженерный контракт

AD не доходит до рендера — контракт props v2

ScriptSpec несёт арт-дирекшн (акценты, грейд, Ken Burns) с Гейта-1, но мост в рендер не существовал. Рендер выдаёт скелет. Этот документ — единственный контракт по которому строятся remotion_props.py и DichVideo.tsx.

2026-06-17 · проект ДИЧЬ · sandbox/oko/docs/superpowers/specs/ · аудитория: Тимур + агенты-исполнители
пропасть, которую закрывает этот контракт
0

AD-слоёв доходило до рендера до сегодня

Все акценты (text_accent), грейд (concept.look), Ken Burns (kenburns) жили только на Гейте-1 и терялись. Props-билдер ScriptSpec→DichVideoProps собирался вручную и нёс только framed-src + words.

3 новых поля в Shot
1 новый компонент GradeOverlay
1 новый файл remotion_props.py
TDD: 8 тестов по Mars-фикстуре
корень проблемы
→ /dev/null

Вся AD-работа Гейта-1 уходила в никуда

Без remotion_props.py каждый запуск рендера = пустой фрейм + голые субтитры. Noir-грейд — нет. Золотые цифры поверх кадра — нет. Ken Burns — всегда «in» по дефолту. Это был скелет, не продукт.

Безопасные зоны кадра (1080×1920)

accent — ВЕРХНЯЯ ПОЛОСА y: 140–430 px · число-акцент #FFC23D · НЕ перекрывает фрейм
Framed — ЦЕНТР КАДРА y: 470–998 px · видеоконтент во фрейме · не трогаем геометрию
Subs — НИЗ bottom 200 px · субтитры word-by-word · поверх всего

Три зоны не пересекаются. Accent живёт над фреймом, субтитры — ниже. Главный панч (крупная цифра) всегда читается.

1 · DichVideoProps v2 (TypeScript)

HARD-паттерн (не менять)

Контент — ВО ФРЕЙМЕ поверх крутящейся фирменной заставки. Только добавляем AD-слои поверх существующей структуры Bg / Framed / Subs / Meme. Ничего не переписываем.

TypeScript · расширение существующего
export type KB = "in" | "out" | "left" | "right" | "up" | "down";

export type Shot = {
  from:  number;        // старт бита, сек
  dur:   number;        // длина бита, сек
  kind:  "framed" | "caption" | "meme";
  src?:  string;        // framed/meme: файл в public/
  // ── НОВОЕ (AD-пропагация на framed) ──────────────────
  accent?: string;     // beat.text_accent — КРУПНОЕ число/слово поверх кадра (верхняя треть)
  kb?:   KB;           // beat.kenburns — направление движения кадра (было всегда "in")
  // ── существующее (caption/meme) ──────────────────────
  big?:  string;  sub?:   string;  color?: string;
  side?: "left" | "right";
};

export type Grade = {
  look: "noir" | "none";
  // noir: desaturate + contrast + cool-tint + тяжёлая виньетка
  // чистый CSS, без видео-оверлея
};

export type DichVideoProps = {
  bg:         string;    // bg_haze.mp4 (noir, дефолт) | bg_dots.mp4 | bg_flow.mp4 — НЕ bg_digital
  grade?:     Grade;    // НОВОЕ: грейд-слой по concept.look
  shots:      Shot[];
  words:      WordT[];
  audio:      AudioCfg;
  accentWords?: string[]; // авто-акцентируются в Subs
  transition?: string;
};

2 · Порядок слоёв рендера (снизу → вверх)

СлойКомпонентСтатусЧто делает
1 <Bg> есть Крутящаяся фирменная заставка (bg props)
2 shots.map → Framed расширяем Получает kb (Ken Burns), грейд кадра внутри если grade.look==="noir": saturate(.72) contrast(1.14) brightness(.96) + холодный sepia-blue нюанс
3 <GradeOverlay> новый Full-length холодная цвето-вуаль + виньетка если noir. Чистый CSS (radial-gradient + полупрозрачный сине-стальной слой, mix-blend). Под текстом, без видео-грейна
4 accent-оверлей новый Для каждого framed-шота с accent: крупная цифра/слово в верхней полосе (y≈140–430). Pop-in spring, держится бит, лёгкий дрейф. Цвет #FFC23D на чёрной обводке
5 <Subs> есть Субтитры word-by-word. Поверх всего
6 вуш-переходы + аудио-микс есть Без изменений

3 · Props-билдер remotion_props.py (НОВЫЙ, TDD)

Сигнатура:

Python
def build_props(
    spec: ScriptSpec,
    voice: VoiceResult,
    asset_refs: list[AssetRef]
) -> dict: ...

def write_props(props: dict, slug: str) -> Path:
    # → outputs/<slug>/remotion_props.json
    ...

Маппинг полей:

Поле propsИсточникПравило
bgspec.concept.look"noir"/"haze"/"investigative"/"true-crime" → bg_haze.mp4
"dots"/"grid"/"tech"/"data" → bg_dots.mp4
"flow"/"organic" → bg_flow.mp4
дефолт → bg_haze.mp4. НИКОГДА bg_digital
gradespec.concept.look{look:"noir"} если contains noir/documentary/investigative/true-crime, иначе {look:"none"}
shotsvoice.beats + asset_refs + beat.*По 1 framed-шоту на бит: from/dur из voice.beats (start/end), src = basename(asset_refs[id].path), accent = beat.text_accent, kb = beat.kenburns
wordsvoice.wordsw/s/e поля без изменений
accentWordsвсе beat.text_accentНормализованные + числа авто-акцентируются в Subs
audioпараметры + voicevoice = basename(voice.audio_path); music = выбранный бед (дефолт bed_dich.mp3); musicVolume ≈ 0.2; click = sfx_click.mp3; impact = sfx_impact.mp3
transitionконстантаtr_film.mp4

Побочное действие: копирует ассеты (b*, bg, music, voice, sfx, transition) в remotion-style/public/ и пишет в outputs/<slug>/remotion_props.json.

Тесты (pytest · фикстура Mars)

Фикстура: outputs/mars-orbiter-metric-mixup/{script.json, voice/voice.json, assets.json}

Python · pytest
# 8 акцентов не теряются
def test_accent_propagated(props):
    for i, beat in enumerate(spec.beats):
        assert props["shots"][i]["accent"] == beat.text_accent

# bg = bg_haze (look noir), НЕ bg_digital
def test_bg_is_haze_not_digital(props):
    assert props["bg"] == "bg_haze.mp4"

# grade.look == "noir"
def test_grade_noir(props):
    assert props["grade"]["look"] == "noir"

# kb пробрасывается
def test_kb_propagated(props):
    for i, beat in enumerate(spec.beats):
        assert props["shots"][i]["kb"] == beat.kenburns

# from/dur соответствуют voice.beats
def test_timing_from_voice_beats(props):
    for i, vbeat in enumerate(voice.beats):
        assert props["shots"][i]["from"]  == vbeat.start
        assert props["shots"][i]["dur"]   == vbeat.end - vbeat.start

# accentWords непустой
def test_accent_words_nonempty(props):
    assert len(props["accentWords"]) > 0

# music/voice basenames корректны
def test_audio_basenames(props):
    assert props["audio"]["voice"].endswith(".mp3")
    assert props["audio"]["music"] == "bed_dich.mp3"

# musicVolume voice>>music
def test_music_volume_quiet(props):
    assert props["audio"]["musicVolume"] <= 0.25

4 · Что НЕ трогаем — жёсткий стоп

Эти четыре пункта — табу. Нарушение = сломанный рендер, потеря фирменного стиля.

Что это даёт

После реализации контракта каждый новый сценарий автоматически получает полный AD: noir-грейд, Ken Burns в нужную сторону, золотые цифры-акценты поверх каждого бита. Ноль ручной сборки props.

Что дальше

Честные оговорки

Контракт написан под Mars-Orbiter-Metric-Mixup — единственный тестовый сценарий. Все биты framed, caption/meme-шоты не покрыты тестами.

musicVolume ≈ 0.2 — эмпирика одного сценария. Если новый бед громкий — нужна нормализация на уровне assets.py.

GradeOverlay — описан в контракте концептуально. Точные значения CSS (opacity, mix-blend-mode) требуют визуальной приёмки на реальном рендере.

ДИЧЬ · sandbox/oko · инженерный контракт props v2
Данные: ScriptSpec · VoiceResult · Mars-Orbiter фикстура · feedback-oko-framed-on-dynamic-bg