diff --git a/skills/meta/reviewer.md b/skills/meta/reviewer.md index b440ca8..6d1d647 100644 --- a/skills/meta/reviewer.md +++ b/skills/meta/reviewer.md @@ -305,3 +305,33 @@ Run at **compose** and **publish** stages. Ensures the agent reviewed the actual 1. Verify that `final_review` was passed through as a required artifact 2. If `final_review.status` is not `pass`: **CRITICAL** — "Cannot publish with a non-passing self-review" 3. If `final_review.issues_found` is non-empty and `recommended_action` is not `present_to_user`: **SUGGESTION** — "Self-review found issues; verify they were resolved before publishing" + +## Composition Authoring Mode Review + +The templated→atelier inversion (`AGENT_GUIDE.md` → "Composition Authoring Mode" + `skills/meta/bespoke-composition.md`) is governance, not a suggestion. The reviewer is the enforcement point: without these checks, the next agent quietly defaults back to the stock cut-schema and every video starts looking the same again. + +### At proposal stage: +1. `decision_log` must contain a `composition_mode` decision with `options_considered: ["templated","atelier"]` and a `selected` value with a real reason tied to the brief. + - Missing `composition_mode` decision entirely: **CRITICAL** — "Proposal missing composition_mode choice. Atelier vs templated is a mandatory presented decision (see AGENT_GUIDE.md → Composition Authoring Mode)." + - Decision logged with only one option considered: **CRITICAL** — "composition_mode decision logged without presenting both templated and atelier alternatives." +2. For **hero work** (brief tagged marketing / launch / brand piece / explainer-with-quality-bar / any single-deliverable where quality is the point) where `selected == "templated"`: **CRITICAL** — "Hero brief locked composition_mode='templated'. Default is atelier per doctrine; templated requires an explicit reason in `decision_log..reason` (e.g. localization variant, batch, time-boxed draft)." Only suppress if the reason field names a sanctioned exception. +3. If `composition_mode == "atelier"` and `proposal_packet` lacks an `art_direction` declaration (palette, type, motion, signature device): **CRITICAL** — "Atelier proposal missing art-direction commitment. Per `skills/meta/bespoke-composition.md` step 1, art direction must be written down *before* authoring scenes." + +### At scene_plan / edit stage (when composition_mode == "atelier"): +1. `edit_decisions.composition_mode` must equal `"atelier"` and `edit_decisions.bespoke.{entry, composition_id, art_direction}` must all be set. + - Missing any of `entry`/`composition_id`: **CRITICAL** — "Atelier compose contract incomplete; render will be rejected by `_render_via_atelier`." + - Missing `art_direction`: **CRITICAL** — "Atelier without an art-direction declaration; reviewer cannot evaluate distinctness." +2. Any presence of stock `cut.type` scene-types (`text_card`, `stat_card`, `bar_chart`, `kpi_grid`, `callout`, `comparison`, `hero_title`, `terminal_scene`, `anime_scene`, `progress_bar`, `pie_chart`, `line_chart`) in `edit_decisions.cuts`: **CRITICAL** — "Atelier piece reaches for stock cut.type {name}. Hand-author the scene; the stock registry is a mechanics codex, not a parts bin (`skills/meta/bespoke-composition.md`)." + +### At compose stage (when composition_mode == "atelier"): +1. The compose stage's `final_review.checks.atelier` block must exist. If absent: **CRITICAL** — "Atelier render skipped doctrine checks — `_render_via_atelier` returned without `atelier` checks; investigate tool wiring." +2. If `final_review.checks.atelier.stock_reuse_detected == true`: **CRITICAL** — "Stock-registry import inside bespoke project ({offending_imports[0].file} → {offending_imports[0].import}). Hand-author the scene; do not import from the stock src/." +3. If `final_review.checks.atelier.art_direction_declared == false`: **CRITICAL** — "Atelier render with no art-direction declaration. Set `edit_decisions.bespoke.art_direction` before re-render." +4. **Distinctness review (human-judged, mandatory).** Before approving the render, the reviewer must explicitly answer in the review record: + - *"Could this video be any other product's video?"* (yes ⇒ CRITICAL, re-author art direction) + - *"Does its visual language reuse a look from a prior piece I've made?"* (yes ⇒ CRITICAL, re-author) + - *"Is the signature device named in `art_direction` actually present in the rendered frames?"* (no ⇒ CRITICAL, re-author or update the declaration to match what was actually built) + Distinctness is taste-call territory the tool can't automate; reviewer absence on this question is itself a **CRITICAL** finding ("distinctness review not recorded"). + +### At publish stage (when composition_mode == "atelier"): +1. All four atelier compose-stage findings above must show `resolved` in the review record. Any unresolved: **CRITICAL** — "Cannot publish atelier piece with unresolved doctrine or distinctness findings." diff --git a/tools/video/video_compose.py b/tools/video/video_compose.py index 2692b52..dc8bc67 100644 --- a/tools/video/video_compose.py +++ b/tools/video/video_compose.py @@ -789,18 +789,130 @@ class VideoCompose(BaseTool): error=f"Atelier render completed but output file missing: {output_path}", ) - return ToolResult( - success=True, - data={ - "operation": "render", - "composition_mode": "atelier", - "entry": str(entry_path), - "composition_id": comp_id, - "output": str(output_path), - }, - artifacts=[str(output_path)], + # --- Atelier post-render review ------------------------------------- + # The cut-schema paths run _run_final_review (technical/visual/audio + # probes + transcript-vs-script). Atelier MUST do the same so hero + # renders aren't shipped without the safety net — and additionally + # enforce the bespoke doctrine: no stock-registry imports, an + # art-direction declaration must exist. The distinctness review + # ("could this be any other product's video?") stays human; what we + # automate here is the *doctrine bypass*, not the taste call. + final_review = self._run_final_review( + output_path=output_path, + edit_decisions=edit_decisions, + proposal_packet=inputs.get("proposal_packet"), + narration_transcript_path=inputs.get("narration_transcript_path"), + script_text=inputs.get("script_text"), ) + atelier_checks = self._run_atelier_checks(entry_path, bespoke) + final_review.setdefault("checks", {})["atelier"] = atelier_checks + final_review["issues_found"] = list(final_review.get("issues_found", [])) + atelier_checks.get("issues", []) + + # Escalate atelier-critical issues (stock reuse) to the overall status. + # Missing art-direction is a warning, not a fail — it shows in issues_found. + if atelier_checks.get("stock_reuse_detected"): + final_review["status"] = "fail" + final_review["recommended_action"] = "re_author" + + data: dict[str, Any] = { + "operation": "render", + "composition_mode": "atelier", + "entry": str(entry_path), + "composition_id": comp_id, + "output": str(output_path), + "final_review": final_review, + "final_review_status": final_review.get("status"), + } + + if final_review.get("status") == "fail": + return ToolResult( + success=False, + error=( + "Atelier render produced an invalid output:\n" + + "\n".join(f" • {i}" for i in final_review.get("issues_found", [])) + ), + data=data, + artifacts=[str(output_path)], + ) + + return ToolResult(success=True, data=data, artifacts=[str(output_path)]) + + # Stock-registry import patterns that violate the atelier doctrine. + # Any of these inside a bespoke project tree means a creative component + # was reused instead of hand-stitched. Engine knowledge (the `remotion` + # package, `@remotion/*`, project-local files) is fine. + _ATELIER_STOCK_IMPORT_RE = ( + r"""from\s+["'](""" + # parent-traversed paths into the stock src/ + r"""(?:\.\./)+src/(?:components|Explainer|CinematicRenderer|""" + r"""TitledVideo|TalkingHead|CollageBurst|LyricOverlay|cinematic|crucix|phantom)""" + # or absolute-ish paths into the same + r"""|remotion-composer/src/(?:components|Explainer|CinematicRenderer|""" + r"""TitledVideo|TalkingHead|CollageBurst|LyricOverlay|cinematic|crucix|phantom)""" + r""")""" + ) + + def _run_atelier_checks(self, entry_path: Path, bespoke: dict[str, Any]) -> dict[str, Any]: + """Doctrine-enforcement checks specific to atelier mode. + + Returns a dict with two checks: + - stock_reuse_detected (bool) + offending_imports (list) — CRITICAL, + fails the render. Catches `import X from "../../src/components/..."` + and similar reuse of stock creative components. + - art_direction_declared (bool) + art_direction (str|None) — WARNING. + Forces step 1 of the bespoke-composition skill (commit to a fresh + art direction per video) to be written down rather than skipped. + """ + import re as _re + + issues: list[str] = [] + offending: list[dict[str, str]] = [] + project_dir = entry_path.parent + pat = _re.compile(self._ATELIER_STOCK_IMPORT_RE) + + try: + for f in project_dir.rglob("*"): + if not f.is_file() or f.suffix.lower() not in {".tsx", ".ts", ".jsx", ".js"}: + continue + try: + txt = f.read_text(encoding="utf-8", errors="replace") + except Exception: + continue + for m in pat.finditer(txt): + offending.append({"file": str(f.relative_to(project_dir)), "import": m.group(1)}) + except Exception as e: # pragma: no cover — never let the check itself break a render + issues.append(f"atelier stock-reuse scan errored: {e}") + + stock_reuse_detected = bool(offending) + if stock_reuse_detected: + issues.append( + "atelier doctrine violation: bespoke project imports from the stock " + "creative registry. Hand-author the scene instead — the registry is " + "a mechanics codex, not a parts bin. Offending imports: " + + ", ".join(f"{o['file']} → {o['import']}" for o in offending[:5]) + + ("…" if len(offending) > 5 else "") + ) + + art_direction = bespoke.get("art_direction") or bespoke.get("art_direction_note") + art_direction_declared = bool(art_direction and str(art_direction).strip()) + if not art_direction_declared: + issues.append( + "atelier warning: no bespoke.art_direction declared. Per " + "skills/meta/bespoke-composition.md step 1, every atelier piece must " + "commit to a fresh art direction (palette, type, motion, signature " + "device) before authoring. Pass edit_decisions.bespoke.art_direction " + "as a short note or a path to art-direction.md." + ) + + return { + "stock_reuse_detected": stock_reuse_detected, + "offending_imports": offending, + "art_direction_declared": art_direction_declared, + "art_direction": str(art_direction) if art_direction else None, + "issues": issues, + } + @staticmethod def _build_theme_from_playbook( playbook_name: str | None,