Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions schemas/styles/playbook.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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
},
Expand Down
10 changes: 5 additions & 5 deletions styles/anime-ghibli.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
80 changes: 80 additions & 0 deletions tests/styles/test_playbook_catalog.py
Original file line number Diff line number Diff line change
@@ -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