fix(styles): make the anime-ghibli playbook loadable again

`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.
This commit is contained in:
bbudaedu@gmail.com
2026-08-03 20:51:05 +08:00
parent 4eab34c5cf
commit 2f16b074f6
3 changed files with 125 additions and 7 deletions

View File

@@ -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
},

View File

@@ -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

View File

@@ -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