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>
243 lines
9.5 KiB
Python
243 lines
9.5 KiB
Python
"""Apply a style pack to local media - deterministically, offline.
|
|
|
|
The recovered pipeline's provider stage generated each shot against a hosted
|
|
model. In this lane, application is *local and deterministic*: the pack's
|
|
measured cadence plans the shot rhythm, local media clips fill the slots, and
|
|
the result is a schema-valid application report plus a timeline ready for
|
|
EDL/FCPXML export. Provider generation fails closed (see :func:`apply_generate).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import math
|
|
from fractions import Fraction
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from . import pack as pack_mod
|
|
from . import schema, timeline
|
|
|
|
__all__ = ["ProviderDisabledError", "apply_local", "apply_generate", "plan_shots"]
|
|
|
|
_DEFAULT_FPS = 24.0
|
|
_MIN_SHOT = 0.05 # matches the recovered cadence floor
|
|
|
|
|
|
class ProviderDisabledError(RuntimeError):
|
|
"""Provider generation was requested but is not authorized."""
|
|
|
|
|
|
_FAIL_CLOSED = (
|
|
"provider generation requires explicit separately authorized execution; "
|
|
"this package ships no provider adapters and performs no network calls. "
|
|
"Use apply_local() (deterministic, offline) instead."
|
|
)
|
|
|
|
|
|
def _utc_now() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def _positive(value: Any, label: str) -> float:
|
|
if isinstance(value, bool):
|
|
raise ValueError(f"{label} must be finite and positive")
|
|
try:
|
|
number = float(value)
|
|
except (TypeError, ValueError, OverflowError) as exc:
|
|
raise ValueError(f"{label} must be finite and positive") from exc
|
|
if not math.isfinite(number) or number <= 0:
|
|
raise ValueError(f"{label} must be finite and positive")
|
|
return number
|
|
|
|
|
|
def _strict_assign(
|
|
planned: list[float], media: list[dict[str, Any]], target: float, fps: float
|
|
) -> list[tuple[dict[str, Any], int]]:
|
|
"""Fill the target frame count, then match whole shots to unique sources."""
|
|
target_frames = timeline.seconds_to_frames(target, fps)
|
|
if target_frames < 1:
|
|
raise ValueError("target duration must contain at least one frame")
|
|
frame_counts = []
|
|
elapsed = 0.0
|
|
assigned = 0
|
|
for duration in planned:
|
|
if assigned == target_frames:
|
|
break
|
|
elapsed += duration
|
|
boundary = min(target_frames, timeline.seconds_to_frames(elapsed, fps))
|
|
if boundary <= assigned:
|
|
raise ValueError("cadence shot cannot occupy a whole frame")
|
|
frame_counts.append(boundary - assigned)
|
|
assigned = boundary
|
|
if assigned < target_frames:
|
|
frame_counts.append(target_frames - assigned)
|
|
|
|
rate = timeline.fps_fraction(fps)
|
|
sources: dict[str, tuple[dict[str, Any], int]] = {}
|
|
for clip in media:
|
|
path = str(Path(clip["path"]).expanduser().resolve())
|
|
# Floor rational capacity: rounding up could read past the source end.
|
|
capacity = math.floor(Fraction(str(clip["duration"])) * rate)
|
|
# Accept a boundary serialized as a float only when the frame duration
|
|
# itself compares within the supplied duration; no broad epsilon.
|
|
if float((capacity + 1) / rate) <= clip["duration"]:
|
|
capacity += 1
|
|
if path in sources:
|
|
raise ValueError("no-repeat media must contain unique normalized source paths")
|
|
sources[path] = ({**clip, "path": path}, capacity)
|
|
if len(sources) < len(frame_counts):
|
|
raise ValueError("no-repeat plan requires more unique source clips")
|
|
assignments = []
|
|
for (clip, capacity), count in zip(sources.values(), frame_counts):
|
|
if capacity < count:
|
|
raise ValueError("source clip is too short for its no-repeat cadence slot")
|
|
assignments.append((clip, count))
|
|
return assignments
|
|
|
|
|
|
|
|
def plan_shots(cadence: dict[str, Any], target_duration: float) -> list[float]:
|
|
"""Propose shot durations filling ``target_duration`` at this cadence.
|
|
|
|
Samples from the reference's own shot-length distribution (seeded, like
|
|
the recovered ``Cadence.plan_shots``) so the plan inherits rhythm
|
|
variance instead of flattening into evenly spaced clips.
|
|
"""
|
|
target_duration = _positive(target_duration, "target duration")
|
|
durations = [
|
|
_positive(s["duration"], "cadence shot duration")
|
|
for s in cadence.get("shots", [])
|
|
if isinstance(s, dict) and _positive(s.get("duration"), "cadence shot duration") > _MIN_SHOT
|
|
]
|
|
if not durations:
|
|
if "mean_shot" not in cadence:
|
|
raise ValueError("cadence has no measured shot durations to plan from")
|
|
durations = [max(_positive(cadence.get("mean_shot"), "mean shot duration"), 1.0)]
|
|
|
|
rng = random.Random(7) # deterministic, mirrors numpy default_rng(7)
|
|
out: list[float] = []
|
|
acc = 0.0
|
|
while acc < target_duration:
|
|
d = rng.choice(durations)
|
|
remaining = target_duration - acc
|
|
if remaining < d * 0.5:
|
|
break
|
|
d = min(d, remaining)
|
|
out.append(round(d, 3))
|
|
acc += d
|
|
if not out:
|
|
out = [round(target_duration, 3)]
|
|
return out
|
|
|
|
|
|
def apply_local(
|
|
sp: pack_mod.StylePack,
|
|
media: list[dict[str, Any]],
|
|
duration: float | None = None,
|
|
fps: float | None = None,
|
|
no_repeat: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Plan a cut from the pack's cadence over local media clips.
|
|
|
|
With ``no_repeat=True``, normalized source paths are used at most once;
|
|
insufficient sources or source durations fail instead of repeating clips.
|
|
Strict plans fill the nearest whole-frame target and never exceed source
|
|
capacity. Media duration metadata must describe the available source.
|
|
|
|
Returns an application report validated against
|
|
``schema.APPLICATION_REPORT_SCHEMA``. The report structurally cannot
|
|
claim a provider run: ``provider`` is enum-locked to ``"none"`` and
|
|
``dry_run`` to ``true``.
|
|
"""
|
|
if not media:
|
|
raise ValueError("apply_local needs at least one media clip")
|
|
|
|
if not sp.cadence_path.exists():
|
|
raise ValueError("pack has no measured cadence (cadence.json is missing)")
|
|
cadence = sp.read_json(sp.cadence_path)
|
|
if not isinstance(cadence, dict) or not (cadence.get("shots") or "mean_shot" in cadence):
|
|
raise ValueError("cadence.json has no measured shots to plan from")
|
|
seq_fps = _positive(fps if fps is not None else cadence.get("fps", _DEFAULT_FPS), "fps")
|
|
validated_media = []
|
|
for clip in media:
|
|
if not isinstance(clip, dict) or not isinstance(clip.get("path"), (str, Path)):
|
|
raise ValueError("media clips require a local source path")
|
|
if not str(clip["path"]).strip():
|
|
raise ValueError("media clips require a local source path")
|
|
validated_media.append({**clip, "duration": _positive(clip.get("duration"), "media duration")})
|
|
target = _positive(duration if duration is not None else sum(
|
|
c["duration"] for c in validated_media
|
|
), "target duration")
|
|
planned = plan_shots(cadence, target)
|
|
assignments = _strict_assign(planned, validated_media, target, seq_fps) if no_repeat else [
|
|
(validated_media[i % len(validated_media)], max(1, timeline.seconds_to_frames(d, seq_fps)))
|
|
for i, d in enumerate(planned)
|
|
]
|
|
|
|
shots: list[dict[str, Any]] = []
|
|
events: list[dict[str, Any]] = []
|
|
clock = 0.0
|
|
offset_frames = 0
|
|
for i, (clip, frames) in enumerate(assignments):
|
|
d = float(frames / timeline.fps_fraction(seq_fps)) if no_repeat else planned[i]
|
|
clock = float(offset_frames / timeline.fps_fraction(seq_fps)) if no_repeat else clock
|
|
events.append(
|
|
{
|
|
"path": str(clip["path"]),
|
|
"name": str(clip.get("name") or clip["path"]),
|
|
"duration": d if no_repeat else round(d, 3),
|
|
"frames": frames,
|
|
"offset_frames": offset_frames,
|
|
"fps": seq_fps,
|
|
}
|
|
)
|
|
shots.append(
|
|
{
|
|
"index": i,
|
|
"start": clock if no_repeat else round(clock, 3),
|
|
"end": (float((offset_frames + frames) / timeline.fps_fraction(seq_fps))
|
|
if no_repeat else round(clock + d, 3)),
|
|
"duration": d if no_repeat else round(d, 3),
|
|
}
|
|
)
|
|
clock += d
|
|
offset_frames += frames
|
|
|
|
report = {
|
|
"schema_version": 1,
|
|
"pack": sp.name,
|
|
"generated": _utc_now(),
|
|
"mode": "local-deterministic",
|
|
"dry_run": True,
|
|
"provider": "none",
|
|
"target_duration": target if no_repeat else round(target, 3),
|
|
"media": [
|
|
{"path": str(c.get("path")), "duration": float(c.get("duration") or 0)}
|
|
for c in validated_media
|
|
],
|
|
"planned_shots": shots,
|
|
"timeline_events": events,
|
|
"cadence": {
|
|
"mean_shot": cadence.get("mean_shot", 0.0),
|
|
"rhythm_variance": cadence.get("rhythm_variance", 0.0),
|
|
"cuts_per_min": cadence.get("cuts_per_min", 0.0),
|
|
},
|
|
"notes": [
|
|
"shot durations drawn from the pack's measured cadence (seeded, "
|
|
"deterministic); no provider generation was requested or run",
|
|
],
|
|
}
|
|
problems = schema.validate(report, schema.APPLICATION_REPORT_SCHEMA)
|
|
if problems:
|
|
raise ValueError(f"apply_local produced an invalid report: {problems}")
|
|
return report
|
|
|
|
|
|
def apply_generate(
|
|
sp: pack_mod.StylePack, brief: str, **_: Any
|
|
) -> dict[str, Any]:
|
|
"""Refuse provider generation. Fails closed, always."""
|
|
raise ProviderDisabledError(_FAIL_CLOSED)
|