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>
165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
"""Style pack: the durable artifact that makes taste reusable.
|
|
|
|
A pack is a directory, not a database row, so it can be copied, versioned in
|
|
git, zipped, and handed to someone else. Genres partition the library:
|
|
``stylepacks/flashethereal/``, ``stylepacks/<next-genre>/``, and so on.
|
|
|
|
Layout::
|
|
|
|
stylepacks/flashethereal/
|
|
pack.json manifest: refs, artifact inventory, version
|
|
grade.json GradeStats - color statistics incl. per-zone chroma
|
|
cadence.json Cadence - shot-length distribution
|
|
spec.json VLM style spec (written by distill.py)
|
|
look.cube 33^3 LUT baked against canonical neutral
|
|
stills/ full-res keyframes - the primary style carrier
|
|
props/ GLB meshes minted from hero frames
|
|
plates/ grain / overlay plates
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
DEFAULT_ROOT = Path("stylepacks")
|
|
PACK_VERSION = 1
|
|
|
|
|
|
def _utc_now() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
@dataclass
|
|
class StylePack:
|
|
name: str
|
|
root: Path = DEFAULT_ROOT
|
|
manifest: dict = field(default_factory=dict)
|
|
|
|
# ---- paths -----------------------------------------------------------
|
|
@property
|
|
def dir(self) -> Path:
|
|
return Path(self.root) / self.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 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"
|
|
|
|
# ---- lifecycle -------------------------------------------------------
|
|
def ensure(self) -> "StylePack":
|
|
for d in (self.dir, self.stills_dir, self.props_dir, self.plates_dir):
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
if not self.manifest:
|
|
self.manifest = {
|
|
"name": self.name,
|
|
"version": PACK_VERSION,
|
|
"created": _utc_now(),
|
|
"updated": _utc_now(),
|
|
"refs": [],
|
|
"artifacts": {},
|
|
}
|
|
return self
|
|
|
|
def add_ref(self, ref_id: str, src: str, duration: float, n_shots: int) -> None:
|
|
self.manifest.setdefault("refs", []).append(
|
|
{
|
|
"id": ref_id,
|
|
"src": str(src),
|
|
"duration": round(float(duration), 3),
|
|
"n_shots": int(n_shots),
|
|
}
|
|
)
|
|
|
|
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 refresh_inventory(self) -> None:
|
|
self.manifest["artifacts"] = {
|
|
"lut": self.lut_path.name if self.lut_path.exists() else None,
|
|
"grade": self.grade_path.exists(),
|
|
"cadence": self.cadence_path.exists(),
|
|
"spec": self.spec_path.exists(),
|
|
"stills": len(self.stills()),
|
|
"props": len(self.props()),
|
|
"plates": len(list(self.plates_dir.glob("*"))) if self.plates_dir.exists() else 0,
|
|
}
|
|
self.manifest["updated"] = _utc_now()
|
|
|
|
def save(self) -> Path:
|
|
self.ensure()
|
|
self.refresh_inventory()
|
|
self.manifest_path.write_text(json.dumps(self.manifest, indent=2), encoding="utf-8")
|
|
return self.manifest_path
|
|
|
|
def write_json(self, path: Path, payload: dict) -> Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
return path
|
|
|
|
def read_json(self, path: Path) -> dict:
|
|
if not path.exists():
|
|
return {}
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
def archive(self, dest_dir: str | Path = "out") -> Path:
|
|
"""Zip the pack so a whole taste can be handed off as one file."""
|
|
dest_dir = Path(dest_dir)
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
base = dest_dir / f"{self.name}-stylepack"
|
|
return Path(shutil.make_archive(str(base), "zip", root_dir=self.dir))
|
|
|
|
|
|
def load(name: str, root: str | Path = DEFAULT_ROOT) -> StylePack:
|
|
p = StylePack(name=name, root=Path(root))
|
|
if not p.manifest_path.exists():
|
|
raise FileNotFoundError(
|
|
f"no style pack '{name}' under {root} - run mint.py first"
|
|
)
|
|
p.manifest = json.loads(p.manifest_path.read_text(encoding="utf-8"))
|
|
return p
|
|
|
|
|
|
def create(name: str, root: str | Path = DEFAULT_ROOT) -> StylePack:
|
|
return StylePack(name=name, root=Path(root)).ensure()
|
|
|
|
|
|
def list_packs(root: str | Path = DEFAULT_ROOT) -> list[str]:
|
|
root = Path(root)
|
|
if not root.exists():
|
|
return []
|
|
return sorted(d.name for d in root.iterdir() if (d / "pack.json").exists())
|