Files
video/lib/pipeline_loader.py
T
calesthio 2cd36fa8e0 Implementation spec: governance, decision intelligence, theme system, and E2E bug fixes
Implements the 2026-04-02 transformation spec (Phases 1-8) and fixes all
critical bugs found during 5-pipeline E2E testing.

Governance & Decision Intelligence:
- Pipeline-specific stage order in checkpoint (replaces global STAGES list)
- Provider scoring engine (lib/scoring.py) with 7-dimension weighted ranking
- Decision log artifact enforced at proposal/idea stage across all 10 pipelines
- Delivery promise classifier prevents silent motion-to-still downgrades
- Structured shot language in scene_plan schema (camera, lens, lighting, DOF)
- Variation checker and slideshow risk scorer block samey output before render
- Creative intake, capability extension, and creative-intake meta skills
- Final self-review artifact with 5 mandatory checks before presenting output
- Source media review contract for user-supplied footage

Render & Theme System:
- Remotion AnimatedBackground now derives colors from playbook (no more hardcoded
  dark blue fintech gradient on every video)
- video_compose builds custom ThemeConfig from playbook YAML colors/fonts —
  custom playbooks flow through to Remotion automatically
- Explainer component wires theme to all child components (charts, cards, etc.)
- resolveAsset() handles absolute paths on Windows/Unix via file:// URIs
- RENDERER_FAMILY_MAP synced with actual Remotion compositions

Critical Bug Fixes:
- Windows npx subprocess: run_command() resolves .cmd wrappers via shutil.which()
- Silent renderer downgrade: Remotion failure now returns explicit error with
  options instead of silently falling back to FFmpeg
- .env inline comment parsing strips trailing # comments from API keys
- concat_path UnboundLocalError in video_compose finally block
- audio_mixer and showcase_card capture=True kwarg bug
- Selector estimate_cost() calls fixed (_select_tool -> _select_best_tool)
- asset_manifest schema expanded with provider, license, subtype fields
- screen-demo subtitle_gen moved from required to optional tools
- Duration drift detection in post-render final review (>25% warns)
2026-04-03 09:35:09 -07:00

138 lines
4.3 KiB
Python

"""Pipeline manifest loader.
Loads and validates pipeline YAML manifests from pipeline_defs/.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Optional
import yaml
import jsonschema
PIPELINE_DEFS_DIR = Path(__file__).resolve().parent.parent / "pipeline_defs"
SCHEMA_PATH = (
Path(__file__).resolve().parent.parent
/ "schemas"
/ "pipelines"
/ "pipeline_manifest.schema.json"
)
def _load_manifest_schema() -> dict:
with open(SCHEMA_PATH) as f:
return json.load(f)
def load_pipeline(name: str, defs_dir: Optional[Path] = None) -> dict[str, Any]:
"""Load and validate a pipeline manifest by name.
Args:
name: Pipeline name (without .yaml extension).
defs_dir: Override directory for pipeline definitions.
Returns:
Validated pipeline manifest dict.
"""
defs_dir = defs_dir or PIPELINE_DEFS_DIR
path = defs_dir / f"{name}.yaml"
if not path.exists():
raise FileNotFoundError(f"Pipeline manifest not found: {path}")
with open(path) as f:
manifest = yaml.safe_load(f)
schema = _load_manifest_schema()
jsonschema.validate(instance=manifest, schema=schema)
return manifest
def list_pipelines(defs_dir: Optional[Path] = None) -> list[str]:
"""List all available pipeline manifest names."""
defs_dir = defs_dir or PIPELINE_DEFS_DIR
return [p.stem for p in defs_dir.glob("*.yaml")]
def get_stage_order(manifest: dict) -> list[str]:
"""Extract the ordered list of stage names from a manifest."""
return [stage["name"] for stage in manifest["stages"]]
def get_required_tools(manifest: dict) -> set[str]:
"""Collect all preferred + fallback + available tools across all stages."""
tools: set[str] = set()
for stage in manifest["stages"]:
tools.update(stage.get("preferred_tools", []))
tools.update(stage.get("fallback_tools", []))
tools.update(stage.get("tools_available", []))
return tools
def get_stage_skill(manifest: dict, stage_name: str) -> Optional[str]:
"""Get the skill path for an instruction-driven stage."""
for stage in manifest["stages"]:
if stage["name"] == stage_name:
return stage.get("skill")
return None
def get_stage_review_focus(manifest: dict, stage_name: str) -> list[str]:
"""Get the review focus items for a stage."""
for stage in manifest["stages"]:
if stage["name"] == stage_name:
return stage.get("review_focus", [])
return []
# ---------------------------------------------------------------------------
# Capability-Extension Enforcement
# ---------------------------------------------------------------------------
class ExtensionNotPermitted(PermissionError):
"""Raised when a capability extension is used but not permitted by the pipeline."""
def check_extension_permitted(
manifest: dict,
extension_type: str,
) -> None:
"""Enforce that a capability extension is permitted by the pipeline manifest.
Args:
manifest: Loaded pipeline manifest dict.
extension_type: One of 'custom_scripts', 'custom_playbooks',
'custom_skills', 'custom_tools'.
Raises:
ExtensionNotPermitted: If the extension is not allowed.
"""
valid_extensions = {"custom_scripts", "custom_playbooks", "custom_skills", "custom_tools"}
if extension_type not in valid_extensions:
raise ValueError(
f"Unknown extension type {extension_type!r}. "
f"Valid types: {sorted(valid_extensions)}"
)
extensions = manifest.get("extensions", {})
if not extensions.get(extension_type, False):
raise ExtensionNotPermitted(
f"Pipeline {manifest.get('name', 'unknown')!r} does not permit "
f"{extension_type}. Set extensions.{extension_type}: true in the "
f"pipeline manifest to allow this."
)
def get_permitted_extensions(manifest: dict) -> dict[str, bool]:
"""Return the extension permission flags for a pipeline."""
defaults = {
"custom_scripts": False,
"custom_playbooks": False,
"custom_skills": False,
"custom_tools": False,
}
extensions = manifest.get("extensions", {})
return {k: extensions.get(k, v) for k, v in defaults.items()}