Files
ECC/skills/taste-application/scripts/tasteforge/schema.py
T
928c1dea72 feat(tasteforge): package reusable workflows and preserve native edits (#3033)
* feat: bundle standalone taste distillation and application workflows

* docs: fix imported taste skill markdown lint

* docs: align Turkish agent catalog with taste skills

* refactor: make ECC the canonical reusable video engine

* fix: preserve video duration when applying image overlays

* fix: preserve background colors in image compositing

* fix: report best-effort duration targets and shortfalls

* feat: ship verified Fusion presets with compatibility provenance

* feat(tasteforge): preserve native edits in application bundles

* feat(tasteforge): compile local preservation without hosted input

* fix: update js-yaml to patched 4.3.2

* test: report bounded Stop wrapper failure diagnostics

* fix(tasteforge): fail closed on unsafe output names, missing overlays and cadence

- cli: default report and spec paths are derived from pack name and profile
  genre; require the manifest's name pattern before using either as a
  filename part so a traversal string cannot write outside cwd/out.
- apply_local: a pack without cadence.json, or with no measured shots and
  no explicit mean_shot, raises instead of silently planning 1.0s shots and
  reporting a measured cadence.
- legacy apply: a missing overlay aborts before any paid upload; forge()
  would have rejected it after every take was generated.
- requirements-live: pin fal-client>=0.13.0, the first release whose
  subscribe() accepts client_timeout.

Addresses the five P1 findings from the independent review of #3033.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fxHRsydPqEcYngGbqkgt1

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 15:31:36 +01:00

405 lines
15 KiB
Python

"""Deterministic schemas and a dependency-free validator.
Every artifact the TasteForge workflow reads or writes has one schema here.
The validator implements the JSON-Schema subset this package needs:
* ``type`` (``object``, ``array``, ``string``, ``integer``, ``number``,
``boolean``, ``null``; ``integer`` accepts ``bool``-exclusive ints)
* ``required``, ``properties``, ``items``, ``additionalProperties: false``
* ``enum``, ``minimum``, ``minItems``, ``pattern``
Validation returns a list of human-readable problems; an empty list means the
instance conforms. Schemas are plain data so they can be emitted as JSON for
documentation or cross-checking against the canonical implementation.
"""
from __future__ import annotations
import re
from typing import Any
# ---------------------------------------------------------------------------
# validator
# ---------------------------------------------------------------------------
_TYPE_CHECKS = {
"object": lambda v: isinstance(v, dict),
"array": lambda v: isinstance(v, list),
"string": lambda v: isinstance(v, str),
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
"number": lambda v: (isinstance(v, (int, float)) and not isinstance(v, bool)),
"boolean": lambda v: isinstance(v, bool),
"null": lambda v: v is None,
}
def validate(instance: Any, schema: dict, path: str = "$") -> list[str]:
"""Validate ``instance`` against ``schema``; return a list of problems."""
problems: list[str] = []
if not isinstance(schema, dict):
return [f"{path}: schema itself is not an object"]
expected_type = schema.get("type")
if expected_type is not None:
allowed = expected_type if isinstance(expected_type, list) else [expected_type]
checks = []
unknown = []
for t in allowed:
check = _TYPE_CHECKS.get(t)
if check is None:
unknown.append(t)
else:
checks.append(check)
if unknown:
problems.append(f"{path}: schema has unknown type(s) {unknown!r}")
if checks and not any(check(instance) for check in checks):
problems.append(
f"{path}: expected type {expected_type!r}, got {_typename(instance)}"
)
return problems # deeper checks are meaningless on a type mismatch
if "enum" in schema and instance not in schema["enum"]:
problems.append(
f"{path}: {instance!r} not in enum {schema['enum']!r}"
)
if expected_type == "object" and isinstance(instance, dict):
for key in schema.get("required", []):
if key not in instance:
problems.append(f"{path}: missing required property {key!r}")
props = schema.get("properties", {})
additional = schema.get("additionalProperties", True)
for key, value in instance.items():
child = f"{path}.{key}"
if key in props:
problems.extend(validate(value, props[key], child))
elif additional is False:
problems.append(f"{child}: unexpected property (additionalProperties false)")
elif isinstance(additional, dict):
problems.extend(validate(value, additional, child))
if expected_type == "array" and isinstance(instance, list):
if "minItems" in schema and len(instance) < schema["minItems"]:
problems.append(
f"{path}: minItems {schema['minItems']} not met "
f"(has {len(instance)})"
)
item_schema = schema.get("items")
if isinstance(item_schema, dict):
for i, item in enumerate(instance):
problems.extend(validate(item, item_schema, f"{path}[{i}]"))
if "minimum" in schema and isinstance(instance, (int, float)) \
and not isinstance(instance, bool) and instance < schema["minimum"]:
problems.append(f"{path}: {instance} below minimum {schema['minimum']}")
if "pattern" in schema and isinstance(instance, str):
if re.search(schema["pattern"], instance) is None:
problems.append(f"{path}: {instance!r} does not match pattern {schema['pattern']!r}")
return problems
def _typename(value: Any) -> str:
return type(value).__name__
# ---------------------------------------------------------------------------
# schemas
# ---------------------------------------------------------------------------
nonempty_str = {"type": "string", "pattern": r"\S"}
# --- taste interview / profile ------------------------------------------
LOOK_FIELDS: dict[str, dict[str, Any]] = {
"palette_description": {"type": "string"},
"grain": {"type": "string"},
"lighting": {"type": "string"},
"focal_length": {"type": "string"},
"camera_motion": {"type": "string"},
"subject_framing": {"type": "string"},
"grade_description": {"type": "string"},
"mood_adjectives": {"type": "array", "items": {"type": "string"}},
"avoid": {"type": "array", "items": {"type": "string"}},
}
TASTE_PROFILE_SCHEMA = {
"type": "object",
"required": ["schema_version", "genre", "answers", "constraints"],
"properties": {
"schema_version": {"type": "integer", "enum": [1]},
"genre": {"type": "string", "pattern": r"^[a-z0-9][a-z0-9_-]*$"},
"created": {"type": "string"},
"answers": {"type": "object"},
"unanswered": {"type": "array", "items": {"type": "string"}},
"constraints": {
"type": "object",
"required": ["look", "content"],
"properties": {
"look": {
"type": "object",
"required": ["mood_adjectives", "avoid"],
"properties": dict(LOOK_FIELDS),
},
"content": {
"type": "object",
"required": ["brief"],
"properties": {"brief": {"type": "string"}},
},
},
},
},
}
# --- style pack manifest (pack.json) ------------------------------------
PACK_MANIFEST_SCHEMA = {
"type": "object",
"required": ["name", "version", "created", "updated", "refs", "artifacts"],
"properties": {
"name": {"type": "string", "pattern": r"^[a-z0-9][a-z0-9_-]*$"},
"version": {"type": "integer", "enum": [1]},
"created": {"type": "string"},
"updated": {"type": "string"},
"refs": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "src", "duration", "n_shots"],
"properties": {
"id": {"type": "string"},
"src": {"type": "string"},
"duration": {"type": "number", "minimum": 0},
"n_shots": {"type": "integer", "minimum": 0},
},
},
},
"artifacts": {
"type": "object",
"required": ["grade", "cadence", "spec", "stills", "props", "plates"],
"properties": {
"lut": {"type": ["string", "null"]},
"grade": {"type": "boolean"},
"cadence": {"type": "boolean"},
"spec": {"type": "boolean"},
"stills": {"type": "integer", "minimum": 0},
"props": {"type": "integer", "minimum": 0},
"plates": {"type": "integer", "minimum": 0},
},
},
"mint": {
"type": "object",
"properties": {
"lut_size": {"type": "integer", "minimum": 2},
"strength": {"type": "number"},
"pixels_analyzed": {"type": "integer", "minimum": 0},
"ui_masked": {"type": "boolean"},
},
},
"distill": {
"type": "object",
"properties": {
"generated": {"type": "string"},
"stills_used": {"type": "array", "items": {"type": "string"}},
"vlm_endpoint": {"type": "string"},
"vlm_model": {"type": "string"},
"dry_run": {"type": "boolean"},
"prop": {"type": "object"},
},
},
},
}
# --- measured color statistics (grade.json) ----------------------------
GRADE_SCHEMA = {
"type": "object",
"required": [
"l_cdf", "black_point", "white_point", "contrast", "palette",
"zones", "noise_sigma",
],
"properties": {
"lab_mean": {"type": "array", "items": {"type": "number"}},
"lab_std": {"type": "array", "items": {"type": "number"}},
"l_cdf": {"type": "array", "items": {"type": "number"}, "minItems": 2},
"black_point": {"type": "number", "minimum": 0},
"white_point": {"type": "number", "minimum": 0},
"contrast": {"type": "number"},
"saturation": {"type": "number"},
"warmth": {"type": "number"},
"tint": {"type": "number"},
"noise_sigma": {"type": "number", "minimum": 0},
"palette": {
"type": "array",
"items": {
"type": "array",
"items": {"type": ["string", "number"]},
"minItems": 2,
},
},
"zones": {
"type": "array",
"items": {"type": "array", "items": {"type": "number"}},
},
"n_frames": {"type": "integer", "minimum": 0},
},
}
# --- cut rhythm (cadence.json) -------------------------------------------
SHOT_SCHEMA = {
"type": "object",
"required": ["index", "start", "end", "duration"],
"properties": {
"index": {"type": "integer", "minimum": 0},
"start": {"type": "number", "minimum": 0},
"end": {"type": "number", "minimum": 0},
"duration": {"type": "number", "minimum": 0},
},
}
CADENCE_SCHEMA = {
"type": "object",
"required": [
"shots", "mean_shot", "median_shot", "p25_shot", "p75_shot",
"min_shot", "max_shot", "cuts_per_min", "rhythm_variance",
"total_duration", "fps", "n_shots",
],
"properties": {
"shots": {"type": "array", "items": SHOT_SCHEMA},
"mean_shot": {"type": "number", "minimum": 0},
"median_shot": {"type": "number", "minimum": 0},
"p25_shot": {"type": "number", "minimum": 0},
"p75_shot": {"type": "number", "minimum": 0},
"min_shot": {"type": "number", "minimum": 0},
"max_shot": {"type": "number", "minimum": 0},
"cuts_per_min": {"type": "number", "minimum": 0},
"rhythm_variance": {"type": "number", "minimum": 0},
"total_duration": {"type": "number", "minimum": 0},
"fps": {"type": "number", "minimum": 0},
"n_shots": {"type": "integer", "minimum": 0},
},
}
# --- distilled style specification (spec.json) ---------------------------
SPEC_SCHEMA = {
"type": "object",
"required": [
"palette_description", "grain", "lighting", "focal_length",
"camera_motion", "subject_framing", "grade_description",
"mood_adjectives", "avoid",
],
"properties": {
**{k: dict(v) for k, v in LOOK_FIELDS.items()},
"source": {
"type": "object",
"required": ["dry_run"],
"properties": {
"pack": {"type": "string"},
"generated": {"type": "string"},
"stills": {"type": "array", "items": {"type": "string"}},
"dry_run": {"type": "boolean"},
"provider": {"type": "string"},
"endpoint": {"type": "string"},
"attempts": {"type": "array"},
},
},
"extra": {"type": "object"},
},
}
# --- timeline events (input to EDL / FCPXML export) ----------------------
TIMELINE_EVENT_SCHEMA = {
"type": "object",
"required": ["path", "duration", "frames"],
"properties": {
"path": {"type": "string", "pattern": r"\S"},
"name": {"type": "string"},
"duration": {"type": "number", "minimum": 0},
"frames": {"type": "integer", "minimum": 1},
"offset_frames": {"type": "integer", "minimum": 0},
"fps": {"type": "number", "minimum": 0},
},
}
# --- application report (apply run manifest) ------------------------------
APPLICATION_REPORT_SCHEMA = {
"type": "object",
"required": [
"schema_version", "pack", "generated", "mode", "dry_run", "provider",
"planned_shots", "timeline_events", "cadence",
],
"properties": {
"schema_version": {"type": "integer", "enum": [1]},
"pack": {"type": "string"},
"generated": {"type": "string"},
"mode": {"type": "string", "enum": ["local-deterministic"]},
# This lane can only ever produce offline reports; the enums make a
# false provider claim structurally invalid.
"dry_run": {"type": "boolean", "enum": [True]},
"provider": {"type": "string", "enum": ["none"]},
"target_duration": {"type": "number", "minimum": 0},
"media": {"type": "array", "items": {"type": "object"}},
"planned_shots": {"type": "array", "items": SHOT_SCHEMA},
"timeline_events": {"type": "array", "items": TIMELINE_EVENT_SCHEMA},
"cadence": {
"type": "object",
"required": ["mean_shot", "rhythm_variance", "cuts_per_min"],
"properties": {
"mean_shot": {"type": "number"},
"rhythm_variance": {"type": "number"},
"cuts_per_min": {"type": "number"},
},
},
"notes": {"type": "array", "items": {"type": "string"}},
},
}
# --- provenance records ----------------------------------------------------
PROVENANCE_SCHEMA = {
"type": "object",
"required": ["canonical_source", "generations", "claude_session"],
"properties": {
"canonical_source": {
"type": "object",
"required": ["path", "read_only"],
"properties": {
"path": {"type": "string"},
"read_only": {"type": "boolean"},
"copy_verification_sha256": {"type": "string"},
"note": {"type": "string"},
},
},
"generations": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["archive", "sha256", "status", "delta"],
"properties": {
"archive": {"type": "string"},
"sha256": {"type": "string", "pattern": r"^[0-9a-f]{64}$"},
"status": {"type": "string", "enum": ["prior", "latest"]},
"delta": {"type": "string"},
},
},
},
"claude_session": {
"type": "object",
"required": ["id", "transcript_available", "selection_evidence_local"],
"properties": {
"id": {"type": "string"},
"transcript_available": {"type": "boolean"},
"selection_evidence_local": {"type": "boolean"},
"note": {"type": "string"},
},
},
"fixture": {"type": "object"},
},
}