Files
OpenMontage/lib/variation_checker.py
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

170 lines
6.5 KiB
Python

"""Scene plan variation checker.
Analyzes a scene plan for repetitive patterns that make videos feel
like slideshows. Catches problems before asset generation begins.
This is a structural check, not a creative judgment — it flags concrete
patterns that reliably produce generic-feeling output.
"""
from __future__ import annotations
from collections import Counter
from typing import Any
# Generic language patterns that signal lazy scene descriptions
GENERIC_PHRASES = {
"a person", "a beautiful", "modern", "futuristic", "cutting-edge",
"in today's world", "sleek design", "innovative", "state-of-the-art",
"next-generation", "revolutionary", "a professional", "dynamic",
"vibrant", "stunning", "breathtaking", "amazing", "incredible",
"powerful", "seamless", "elegant solution",
}
def check_scene_variation(scenes: list[dict[str, Any]]) -> dict[str, Any]:
"""Analyze a scene plan for repetitive patterns.
Returns:
{
"score": float (0-5, lower is better),
"verdict": "strong" | "acceptable" | "revise" | "fail",
"violations": list of specific issues,
"suggestions": list of improvement suggestions,
}
"""
if not scenes:
return {"score": 5.0, "verdict": "fail", "violations": ["No scenes to check"], "suggestions": []}
violations: list[str] = []
suggestions: list[str] = []
# --- Check 1: Shot size variety ---
shot_sizes = [
s.get("shot_language", {}).get("shot_size", "unspecified")
for s in scenes
]
size_counts = Counter(shot_sizes)
if len(scenes) >= 4:
most_common_size, most_common_count = size_counts.most_common(1)[0]
if most_common_count / len(scenes) > 0.5:
violations.append(
f"Shot size '{most_common_size}' used in {most_common_count}/{len(scenes)} scenes "
f"({most_common_count/len(scenes):.0%}). Vary shot sizes for visual interest."
)
suggestions.append("Mix wide establishing shots with close-ups for visual rhythm.")
# --- Check 2: Consecutive same-size shots ---
consecutive_same = 0
for i in range(1, len(shot_sizes)):
if shot_sizes[i] == shot_sizes[i-1] and shot_sizes[i] != "unspecified":
consecutive_same += 1
if consecutive_same >= 3:
violations.append(
f"{consecutive_same} consecutive same-size shots. "
f"Vary shot sizes between scenes for editorial rhythm."
)
# --- Check 3: Static shot overuse ---
movements = [
s.get("shot_language", {}).get("camera_movement", "unspecified")
for s in scenes
]
static_count = sum(1 for m in movements if m in ("static", "unspecified"))
if len(scenes) >= 4 and static_count / len(scenes) > 0.6:
violations.append(
f"{static_count}/{len(scenes)} scenes are static or unspecified movement. "
f"Add intentional camera movement to at least 40% of scenes."
)
suggestions.append("Consider dolly_in for emphasis, tracking for energy, or crane for scale.")
# --- Check 4: Lighting variety ---
lightings = {
s.get("shot_language", {}).get("lighting_key")
for s in scenes
if s.get("shot_language", {}).get("lighting_key")
}
if len(scenes) >= 4 and len(lightings) <= 1:
violations.append(
f"Only {len(lightings)} unique lighting setup(s) across {len(scenes)} scenes. "
f"Vary lighting to create mood shifts."
)
# --- Check 5: Hero moment exists and is visually distinct ---
hero_scenes = [s for s in scenes if s.get("hero_moment")]
if len(scenes) >= 4 and not hero_scenes:
violations.append(
"No hero_moment flagged. Every video should have at least one visual peak."
)
suggestions.append("Mark the most impactful scene as hero_moment=true.")
if hero_scenes:
for hero in hero_scenes:
hero_idx = scenes.index(hero)
hero_size = hero.get("shot_language", {}).get("shot_size")
# Check neighbors
for offset in (-1, 1):
neighbor_idx = hero_idx + offset
if 0 <= neighbor_idx < len(scenes):
neighbor_size = scenes[neighbor_idx].get("shot_language", {}).get("shot_size")
if hero_size and neighbor_size and hero_size == neighbor_size:
violations.append(
f"Hero scene '{hero.get('id')}' has same shot size as neighbor. "
f"Hero moments should be visually distinct from surrounding scenes."
)
# --- Check 6: Description specificity ---
generic_count = 0
for scene in scenes:
desc = scene.get("description", "").lower()
for phrase in GENERIC_PHRASES:
if phrase in desc:
generic_count += 1
break
if generic_count >= len(scenes) * 0.3:
violations.append(
f"{generic_count}/{len(scenes)} scenes use generic language. "
f"Replace vague descriptions with specific visual details."
)
suggestions.append(
"Instead of 'a beautiful cityscape', try 'rain-slicked Tokyo intersection "
"at night, neon reflections in puddles, pedestrians with translucent umbrellas'."
)
# --- Check 7: Texture keywords presence ---
textured = sum(1 for s in scenes if s.get("texture_keywords"))
if len(scenes) >= 4 and textured < len(scenes) * 0.3:
violations.append(
f"Only {textured}/{len(scenes)} scenes have texture_keywords. "
f"Add texture descriptors to visual scenes for richer generation prompts."
)
# --- Check 8: Shot intent completeness ---
intented = sum(1 for s in scenes if s.get("shot_intent"))
if len(scenes) >= 4 and intented < len(scenes) * 0.5:
violations.append(
f"Only {intented}/{len(scenes)} scenes have shot_intent. "
f"Every scene should explain WHY it exists in the video."
)
# --- Score ---
# Each violation category adds ~0.6 to score
score = min(5.0, len(violations) * 0.6)
if score < 2.0:
verdict = "strong"
elif score < 3.0:
verdict = "acceptable"
elif score < 4.0:
verdict = "revise"
else:
verdict = "fail"
return {
"score": round(score, 1),
"verdict": verdict,
"violations": violations,
"suggestions": suggestions,
}