ScriptSpec несёт арт-дирекшн (акценты, грейд, Ken Burns) с Гейта-1, но мост в рендер не существовал. Рендер выдаёт скелет. Этот документ — единственный контракт по которому строятся remotion_props.py и DichVideo.tsx.
Все акценты (text_accent), грейд (concept.look), Ken Burns (kenburns) жили только на Гейте-1 и терялись. Props-билдер ScriptSpec→DichVideoProps собирался вручную и нёс только framed-src + words.
Без remotion_props.py каждый запуск рендера = пустой фрейм + голые субтитры. Noir-грейд — нет. Золотые цифры поверх кадра — нет. Ken Burns — всегда «in» по дефолту. Это был скелет, не продукт.
Три зоны не пересекаются. Accent живёт над фреймом, субтитры — ниже. Главный панч (крупная цифра) всегда читается.
Контент — ВО ФРЕЙМЕ поверх крутящейся фирменной заставки. Только добавляем AD-слои поверх существующей структуры Bg / Framed / Subs / Meme. Ничего не переписываем.
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;
};
| Слой | Компонент | Статус | Что делает |
|---|---|---|---|
| 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 | вуш-переходы + аудио-микс | есть | Без изменений |
Сигнатура:
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 | Источник | Правило |
|---|---|---|
bg | spec.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 |
grade | spec.concept.look | {look:"noir"} если contains noir/documentary/investigative/true-crime, иначе {look:"none"} |
shots | voice.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 |
words | voice.words | w/s/e поля без изменений |
accentWords | все beat.text_accent | Нормализованные + числа авто-акцентируются в Subs |
audio | параметры + voice | voice = 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.
Фикстура: outputs/mars-orbiter-metric-mixup/{script.json, voice/voice.json, assets.json}
# 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
Эти четыре пункта — табу. Нарушение = сломанный рендер, потеря фирменного стиля.
После реализации контракта каждый новый сценарий автоматически получает полный AD: noir-грейд, Ken Burns в нужную сторону, золотые цифры-акценты поверх каждого бита. Ноль ручной сборки props.
text_accent.pipeline/remotion_props.py, TDD-first (8 тестов → реализация)accent? + kb? в Shot, добавить grade? в DichVideoProps, создать <GradeOverlay> компонент, accent-оверлей в Framed#FFC23D цифр (кандидат: Impact / Black Condensed)Контракт написан под Mars-Orbiter-Metric-Mixup — единственный тестовый сценарий. Все биты framed, caption/meme-шоты не покрыты тестами.
musicVolume ≈ 0.2 — эмпирика одного сценария. Если новый бед громкий — нужна нормализация на уровне assets.py.
GradeOverlay — описан в контракте концептуально. Точные значения CSS (opacity, mix-blend-mode) требуют визуальной приёмки на реальном рендере.