diff --git a/schemas/styles/playbook.schema.json b/schemas/styles/playbook.schema.json index 7f6aa27c..e602b87e 100644 --- a/schemas/styles/playbook.schema.json +++ b/schemas/styles/playbook.schema.json @@ -141,16 +141,50 @@ "type": "array", "items": { "type": "string" }, "minItems": 1 + }, + "scene_type": { + "type": "string", + "description": "Default Remotion cut.type this style composes scenes as, e.g. 'anime_scene'. See remotion-composer/SCENE_TYPES.md." + }, + "multi_image_per_scene": { + "type": "boolean", + "description": "Whether a scene is built from several stills that crossfade (the anime_scene grammar) rather than a single image." + }, + "images_per_scene": { + "type": "integer", + "minimum": 1, + "description": "How many stills to generate per scene when multi_image_per_scene is set." + }, + "image_variation_guidance": { + "type": "string", + "description": "How the per-scene stills should differ from each other so the crossfade reads as motion." + }, + "default_particles": { + "type": "string", + "description": "Default particle overlay for scenes in this style (maps to the renderer's particles prop)." + }, + "default_particle_color": { + "type": "string", + "description": "Default particle color (maps to the renderer's particleColor prop)." + }, + "default_vignette": { + "type": "boolean", + "description": "Whether scenes in this style get a vignette by default (maps to the renderer's vignette prop)." } }, "additionalProperties": false }, "overlays": { + "description": "Per-overlay styling. Covers the in-scene overlay boxes plus the timed overlay types the Explainer composition renders (see remotion-composer/src/Explainer.tsx, `Overlay.type`).", "type": "object", "properties": { "stat_card": { "$ref": "#/$defs/overlay_style" }, "key_term": { "$ref": "#/$defs/overlay_style" }, - "code_block": { "$ref": "#/$defs/overlay_style" } + "code_block": { "$ref": "#/$defs/overlay_style" }, + "section_title": { "$ref": "#/$defs/overlay_style" }, + "stat_reveal": { "$ref": "#/$defs/overlay_style" }, + "hero_title": { "$ref": "#/$defs/overlay_style" }, + "provider_chip": { "$ref": "#/$defs/overlay_style" } }, "additionalProperties": false }, @@ -236,7 +270,11 @@ "text": { "type": "string" }, "radius": { "type": "number" }, "shadow": { "type": "string" }, - "highlight": { "type": "string" } + "highlight": { "type": "string" }, + "accent": { + "type": "string", + "description": "Accent color for overlay types that render one (maps to the renderer's accentColor prop)." + } }, "additionalProperties": false }, diff --git a/styles/anime-ghibli.yaml b/styles/anime-ghibli.yaml index b2079ca5..daa3102a 100644 --- a/styles/anime-ghibli.yaml +++ b/styles/anime-ghibli.yaml @@ -1,19 +1,19 @@ identity: name: "Anime Ghibli" - category: anime-illustration + category: custom mood: warm, whimsical, contemplative, magical - pace: gentle + pace: slow best_for: "Narrative animations, nature-themed stories, emotional storytelling, fantasy visuals, educational content with wonder" visual_language: color_palette: primary: ["#2D5016", "#1B4332"] # Deep forest greens - accent: ["#FFB347", "#FF6B9D"] # Warm golden, soft cherry blossom pink + # Warm golden, soft cherry blossom pink, spirit glow (magical elements), + # golden-hour light. The last two are referenced by name in consistency_anchors. + accent: ["#FFB347", "#FF6B9D", "#A8E6CF", "#FFF3B0"] background: "#0A0A1A" # Deep night sky text: "#F5F0E8" # Warm parchment white muted: "#8B9A7E" # Mossy sage - spirit_glow: "#A8E6CF" # Soft teal glow for magical elements - golden_hour: "#FFF3B0" # Warm golden hour light composition: centered subjects, rule of thirds for landscapes, generous negative space, layered depth texture: soft watercolor edges, painterly brushstrokes, organic shapes, no hard geometric lines diff --git a/tests/styles/test_playbook_catalog.py b/tests/styles/test_playbook_catalog.py new file mode 100644 index 00000000..a8bbcd09 --- /dev/null +++ b/tests/styles/test_playbook_catalog.py @@ -0,0 +1,80 @@ +"""Every shipped style playbook must actually load. + +`list_playbooks()` advertises a playbook as selectable, `pipeline_defs/*.yaml` +offer them by name, and `lib.checkpoint._validate_style_playbook` fails closed +on anything `load_playbook()` rejects. So a playbook that is listed but does not +validate is not a cosmetic problem — it blocks `init_project()`/`write_checkpoint()` +outright, taking the whole pipeline down for that style. + +These tests iterate the catalog rather than a hardcoded list, which is how +`anime-ghibli` and `premium-minimalist` slipped through: the pre-existing +coverage in tests/qa/test_07_playbook_intelligence.py names three playbooks +that were the only ones in the repo when it was written. +""" + +import pytest + +from lib.checkpoint import init_project +from styles.playbook_loader import list_playbooks, load_playbook + +PLAYBOOK_NAMES = sorted(list_playbooks()) + +# The overlay types remotion-composer actually renders, from the `Overlay` +# union in remotion-composer/src/Explainer.tsx. A playbook must be able to +# style any of them. +RENDERER_OVERLAY_TYPES = ( + "section_title", + "stat_reveal", + "hero_title", + "provider_chip", +) + + +def test_catalog_is_not_empty() -> None: + assert PLAYBOOK_NAMES, "list_playbooks() found no playbooks at all" + + +@pytest.mark.parametrize("name", PLAYBOOK_NAMES) +def test_listed_playbook_loads_and_validates(name: str) -> None: + """A playbook offered by the catalog must pass its own schema.""" + playbook = load_playbook(name) + assert playbook["identity"]["name"] + + +@pytest.mark.parametrize("name", PLAYBOOK_NAMES) +def test_listed_playbook_is_accepted_by_init_project(name: str, tmp_path) -> None: + """The checkpoint writer validates style_playbook fail-closed. + + Regression: `anime-ghibli` was listed and offered by pipeline_defs/animation.yaml + but raised CheckpointValidationError here, so no run using that style could + write a checkpoint. + """ + init_project( + f"catalog-{name}", + title="Catalog probe", + pipeline_type="animation", + pipeline_dir=tmp_path, + style_playbook=name, + ) + + +@pytest.mark.parametrize("overlay_type", RENDERER_OVERLAY_TYPES) +def test_schema_allows_every_renderer_overlay_type(overlay_type: str) -> None: + """The playbook schema's overlay vocabulary must track the renderer's. + + Regression: the schema allowed only stat_card/key_term/code_block — none of + the four overlay types Explainer renders — so styling `section_title` made + the whole playbook invalid. + """ + import json + from pathlib import Path + + schema_path = ( + Path(__file__).resolve().parents[2] + / "schemas" + / "styles" + / "playbook.schema.json" + ) + schema = json.loads(schema_path.read_text(encoding="utf-8")) + allowed = schema["properties"]["overlays"]["properties"] + assert overlay_type in allowed