From 2f16b074f6eb043c35522c26bb3ce869ab01c01b Mon Sep 17 00:00:00 2001 From: "bbudaedu@gmail.com" Date: Mon, 3 Aug 2026 20:51:05 +0800 Subject: [PATCH] fix(styles): make the anime-ghibli playbook loadable again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `load_playbook("anime-ghibli")` raised a ValidationError, and since `lib.checkpoint._validate_style_playbook` fails closed, that took down every run using the style: init_project(..., style_playbook="anime-ghibli") CheckpointValidationError: Unknown or invalid style_playbook 'anime-ghibli'. Available playbooks: [..., 'anime-ghibli', ...] The playbook is listed by `list_playbooks()` and offered by name in `pipeline_defs/animation.yaml`, so the error even names it as an available alternative to itself. Four schema violations, repaired on whichever side the evidence pointed to — widen the schema where a shipped renderer feature backs the key, conform the playbook where it is only vocabulary drift: - `overlays.section_title` — the schema allowed stat_card/key_term/code_block and none of the four overlay types `Overlay` in Explainer.tsx actually renders. Added section_title/stat_reveal/hero_title/provider_chip, plus `accent` on overlay_style to match the renderer's accentColor prop. - `asset_generation` anime_scene knobs — scene_type, multi_image_per_scene, images_per_scene, image_variation_guidance, default_particles, default_particle_color, default_vignette all map onto real AnimeScene props. Added to the schema. - `identity.category: anime-illustration` and `pace: gentle` — no code reads either field for behavior, so the playbook now uses the canonical `custom` and `slow` rather than expanding the enums for one style. - `color_palette.spirit_glow` / `golden_hour` — ad-hoc named colors. Folded into the `accent` array; both hexes are preserved and still referenced by name in consistency_anchors. With this, `_build_theme_from_playbook("anime-ghibli")` returns the Ghibli palette instead of `{}`, so the style no longer silently falls back to the default dark theme at render time. Regression coverage iterates the catalog instead of a hardcoded list, which is how this slipped: tests/qa/test_07_playbook_intelligence.py names the three playbooks that existed when it was written, so anime-ghibli and premium-minimalist never had load coverage. The new tests assert that every playbook `list_playbooks()` advertises both validates and is accepted by `init_project`, and that the schema's overlay vocabulary tracks the renderer's. Verified: 6 of the new tests fail on the unfixed tree; full suite goes 964 -> 979 passed with no regressions. --- schemas/styles/playbook.schema.json | 42 +++++++++++++- styles/anime-ghibli.yaml | 10 ++-- tests/styles/test_playbook_catalog.py | 80 +++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 tests/styles/test_playbook_catalog.py 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