mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 15:50:25 +02:00
* 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>
192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
"""Offline style-pack model: load, inspect, validate.
|
|
|
|
A pack is a directory (canonical layout from the recovered implementation)::
|
|
|
|
<name>/pack.json manifest: refs, artifact inventory, version
|
|
<name>/grade.json color statistics incl. per-zone chroma
|
|
<name>/cadence.json shot-length distribution
|
|
<name>/spec.json distilled style specification
|
|
<name>/grounding.txt measured-ground-truth preamble for a VLM
|
|
<name>/look.cube 33^3 LUT baked against canonical neutral
|
|
<name>/stills/ full-res keyframes - the primary style carrier
|
|
<name>/props/ GLB meshes minted from hero frames
|
|
<name>/plates/ grain / overlay plates
|
|
|
|
This module never opens media decoders and never touches a provider: pack
|
|
metadata is plain JSON, and validation is schema-driven and offline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from . import schema
|
|
|
|
__all__ = ["StylePack", "load"]
|
|
|
|
|
|
class StylePack:
|
|
"""A loaded, validatable style pack directory."""
|
|
|
|
def __init__(self, dir: Path):
|
|
self.dir = Path(dir)
|
|
self.manifest: dict[str, Any] = {}
|
|
self.problems: list[str] = []
|
|
|
|
# ---- paths -----------------------------------------------------------
|
|
@property
|
|
def name(self) -> str:
|
|
return str(self.manifest.get("name") or self.dir.name)
|
|
|
|
@property
|
|
def manifest_path(self) -> Path:
|
|
return self.dir / "pack.json"
|
|
|
|
@property
|
|
def grade_path(self) -> Path:
|
|
return self.dir / "grade.json"
|
|
|
|
@property
|
|
def cadence_path(self) -> Path:
|
|
return self.dir / "cadence.json"
|
|
|
|
@property
|
|
def spec_path(self) -> Path:
|
|
return self.dir / "spec.json"
|
|
|
|
@property
|
|
def grounding_path(self) -> Path:
|
|
return self.dir / "grounding.txt"
|
|
|
|
@property
|
|
def lut_path(self) -> Path:
|
|
return self.dir / "look.cube"
|
|
|
|
@property
|
|
def stills_dir(self) -> Path:
|
|
return self.dir / "stills"
|
|
|
|
@property
|
|
def props_dir(self) -> Path:
|
|
return self.dir / "props"
|
|
|
|
@property
|
|
def plates_dir(self) -> Path:
|
|
return self.dir / "plates"
|
|
|
|
# ---- io --------------------------------------------------------------
|
|
def read_json(self, path: Path) -> dict:
|
|
if not path.exists():
|
|
return {}
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
def stills(self) -> list[Path]:
|
|
return sorted(self.stills_dir.glob("*.png")) if self.stills_dir.exists() else []
|
|
|
|
def props(self) -> list[Path]:
|
|
return sorted(self.props_dir.glob("*.glb")) if self.props_dir.exists() else []
|
|
|
|
def plates(self) -> list[Path]:
|
|
return sorted(p for p in self.plates_dir.glob("*") if p.is_file()) \
|
|
if self.plates_dir.exists() else []
|
|
|
|
# ---- inspect / validate ----------------------------------------------
|
|
def inspect(self) -> dict[str, Any]:
|
|
"""Validate every artifact against its schema; return a full report."""
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
self._check("pack.json (manifest)", self.manifest,
|
|
schema.PACK_MANIFEST_SCHEMA, errors)
|
|
|
|
grade = self.read_json(self.grade_path)
|
|
if grade:
|
|
self._check("grade.json", grade, schema.GRADE_SCHEMA, errors)
|
|
elif self.grade_path.exists():
|
|
errors.append("grade.json: unreadable JSON")
|
|
else:
|
|
warnings.append("grade.json: missing (pack has no measured grade)")
|
|
|
|
cadence = self.read_json(self.cadence_path)
|
|
if cadence:
|
|
self._check("cadence.json", cadence, schema.CADENCE_SCHEMA, errors)
|
|
elif self.cadence_path.exists():
|
|
errors.append("cadence.json: unreadable JSON")
|
|
else:
|
|
warnings.append("cadence.json: missing (pack has no measured cadence)")
|
|
|
|
spec = self.read_json(self.spec_path)
|
|
if spec:
|
|
self._check("spec.json", spec, schema.SPEC_SCHEMA, errors)
|
|
elif self.spec_path.exists():
|
|
errors.append("spec.json: unreadable JSON")
|
|
else:
|
|
warnings.append("spec.json: missing (pack has no distilled spec)")
|
|
|
|
if not self.grounding_path.exists():
|
|
warnings.append("grounding.txt: missing (no measured ground truth)")
|
|
|
|
stills, props, plates = self.stills(), self.props(), self.plates()
|
|
if not stills:
|
|
warnings.append(
|
|
"stills: none present - a full pack carries keyframe stills; "
|
|
"the shipped fixture is metadata-only by design"
|
|
)
|
|
if not props:
|
|
warnings.append("props: none present")
|
|
lut_present = self.lut_path.exists()
|
|
|
|
status = "valid" if not errors else "invalid"
|
|
return {
|
|
"name": self.name,
|
|
"dir": str(self.dir),
|
|
"manifest_version": self.manifest.get("version"),
|
|
"refs": self.manifest.get("refs", []),
|
|
"artifacts": {
|
|
"lut": self.manifest.get("artifacts", {}).get("lut") if lut_present else None,
|
|
"lut_present": lut_present,
|
|
"grade": bool(grade),
|
|
"cadence": bool(cadence),
|
|
"spec": bool(spec),
|
|
"grounding": self.grounding_path.exists(),
|
|
"stills": len(stills),
|
|
"props": len(props),
|
|
"plates": len(plates),
|
|
},
|
|
"grade": {
|
|
k: grade.get(k)
|
|
for k in ("black_point", "white_point", "contrast",
|
|
"saturation", "warmth", "tint", "noise_sigma")
|
|
} if grade else {},
|
|
"cadence": {
|
|
k: cadence.get(k)
|
|
for k in ("mean_shot", "median_shot", "cuts_per_min",
|
|
"rhythm_variance", "n_shots", "fps",
|
|
"total_duration")
|
|
} if cadence else {},
|
|
"validation": {"status": status, "errors": errors, "warnings": warnings},
|
|
}
|
|
|
|
@staticmethod
|
|
def _check(label: str, payload: dict, schem: dict, errors: list[str]) -> None:
|
|
problems = schema.validate(payload, schem)
|
|
for p in problems:
|
|
errors.append(f"{label}: {p}")
|
|
|
|
|
|
def load(path: str | Path) -> StylePack:
|
|
"""Load a pack directory; raises if no manifest exists."""
|
|
sp = StylePack(Path(path))
|
|
if not sp.manifest_path.exists():
|
|
raise FileNotFoundError(
|
|
f"no style pack at {sp.dir} - expected a pack.json manifest"
|
|
)
|
|
try:
|
|
sp.manifest = json.loads(sp.manifest_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
sp.manifest = {}
|
|
sp.problems.append(f"pack.json: {exc}")
|
|
return sp
|