Files
ECC/skills/taste-application/scripts/tasteforge/workflow.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

834 lines
34 KiB
Python

"""File-driven, multimodal TasteForge dry-run orchestration.
The workflow reads one JSON contract, hashes and probes every local reference,
keeps each numbered genre separate, and writes provider request manifests. It
never imports or calls a provider SDK.
"""
from __future__ import annotations
import hashlib
import json
import math
import os
import random
import re
import secrets
import shutil
import stat
import statistics
import subprocess
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any, cast
Probe = Callable[[Path], dict[str, Any]]
_MODALITIES = ("image", "video", "3d_asset")
class MediaToolUnavailable(RuntimeError):
"""A required local media executable is unavailable."""
def _canonical_bytes(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _hash_descriptor(descriptor: int) -> tuple[int, str]:
os.lseek(descriptor, 0, os.SEEK_SET)
digest = hashlib.sha256()
total = 0
while True:
chunk = os.read(descriptor, 1024 * 1024)
if not chunk:
break
total += len(chunk)
digest.update(chunk)
return total, digest.hexdigest()
def _stable_probe(path: Path, probe: Probe) -> tuple[dict[str, Any], int, str]:
"""Probe a private snapshot while binding the digest to one stable source object."""
if not hasattr(os, "O_NOFOLLOW"):
raise ValueError("stable source probing requires O_NOFOLLOW")
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
try:
before = os.fstat(descriptor)
if not stat.S_ISREG(before.st_mode):
raise ValueError("reference source must be a regular file")
with tempfile.TemporaryDirectory(prefix="tasteforge-source-") as temporary:
snapshot = Path(temporary) / f"source{path.suffix}"
snapshot_fd = os.open(
snapshot, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600
)
try:
os.lseek(descriptor, 0, os.SEEK_SET)
digest = hashlib.sha256()
total = 0
while True:
chunk = os.read(descriptor, 1024 * 1024)
if not chunk:
break
total += len(chunk)
digest.update(chunk)
view = memoryview(chunk)
while view:
written = os.write(snapshot_fd, view)
if written <= 0:
raise ValueError("stable source snapshot write made no progress")
view = view[written:]
os.fsync(snapshot_fd)
finally:
os.close(snapshot_fd)
source_digest = digest.hexdigest()
copied = os.fstat(descriptor)
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
copied.st_dev, copied.st_ino, copied.st_size, copied.st_mtime_ns
):
raise ValueError("reference source mutated while creating stable snapshot")
verified_size, verified_digest = _hash_descriptor(descriptor)
if verified_size != total or verified_digest != source_digest:
raise ValueError("reference source mutated while creating stable snapshot")
measured = probe(snapshot)
after = os.fstat(descriptor)
final_size, final_digest = _hash_descriptor(descriptor)
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns
) or final_size != total or final_digest != source_digest:
raise ValueError("reference source mutated during media probing")
rebound = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
try:
rebound_stat = os.fstat(rebound)
if (rebound_stat.st_dev, rebound_stat.st_ino) != (before.st_dev, before.st_ino):
raise ValueError("reference source identity changed during media probing")
finally:
os.close(rebound)
return measured, total, source_digest
finally:
os.close(descriptor)
def _finite_real(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
def _validate_probe(measured: dict[str, Any]) -> float:
def require_finite_evidence(value: Any) -> None:
if isinstance(value, bool):
raise ValueError( # noqa: TRY004 - one bounded invalid-media error family
"reference probe numeric evidence must be finite real values"
)
if isinstance(value, (int, float)):
if not math.isfinite(value):
raise ValueError("reference probe numeric evidence must be finite real values")
elif isinstance(value, dict):
for nested in value.values():
require_finite_evidence(nested)
elif isinstance(value, list):
for nested in value:
require_finite_evidence(nested)
require_finite_evidence(measured)
duration = measured.get("duration")
if not _finite_real(duration):
raise ValueError("reference probe duration must be finite and positive")
duration = cast(float, duration)
if float(duration) <= 0:
raise ValueError("reference probe duration must be finite and positive")
for field in ("sample_times", "scene_changes"):
values = measured.get(field, [])
if not isinstance(values, list) or any(
not _finite_real(value) or float(value) < 0 or float(value) > float(duration)
for value in values
):
raise ValueError(f"reference probe {field} must contain finite in-duration times")
samples = measured.get("style_samples", [])
if not isinstance(samples, list) or any(
not isinstance(sample, dict)
or not _finite_real(sample.get("time"))
or float(cast(float, sample["time"])) < 0
or float(cast(float, sample["time"])) > float(duration)
for sample in samples
):
raise ValueError("reference style evidence times must be finite and within duration")
return float(duration)
class _SafeOutput:
"""Descriptor-bound output tree with no-follow traversal and atomic writes."""
def __init__(self, root: Path) -> None:
self._root_fd = -1
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
raise RuntimeError("secure output requires O_NOFOLLOW and O_DIRECTORY")
if root.exists() or root.is_symlink():
metadata = root.lstat()
if stat.S_ISLNK(metadata.st_mode):
raise ValueError("output root must not be a symlink")
if not stat.S_ISDIR(metadata.st_mode):
raise ValueError("output root must be a directory")
else:
if not root.parent.is_dir():
raise ValueError("output parent directory must already exist")
root.mkdir(mode=0o700)
self.root = root
self._root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
self._written: list[str] = []
def close(self) -> None:
if self._root_fd >= 0:
os.close(self._root_fd)
self._root_fd = -1
def __del__(self) -> None:
self.close()
def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:
current = os.dup(self._root_fd)
try:
for part in parts:
if not part or part in {".", ".."} or "/" in part:
raise ValueError("output path contains an invalid component")
try:
metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
except FileNotFoundError:
if not create:
raise ValueError(f"missing output directory: {part}") from None
os.mkdir(part, mode=0o700, dir_fd=current)
metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
if stat.S_ISLNK(metadata.st_mode):
raise ValueError(f"output directory must not be a symlink: {part}")
if not stat.S_ISDIR(metadata.st_mode):
raise ValueError(f"output intermediate must be a directory: {part}")
child = os.open(
part,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current,
)
os.close(current)
current = child
return current
except Exception:
os.close(current)
raise
def prepare(self, directories: tuple[str, ...]) -> None:
"""Validate every known intermediate before the first artifact write."""
opened: list[int] = []
try:
for directory in directories:
opened.append(self._open_dir((directory,), create=True))
finally:
for descriptor in opened:
os.close(descriptor)
def write_json(self, relative: str, payload: Any) -> None:
path = Path(relative)
if path.is_absolute() or not path.name or any(part in {".", ".."} for part in path.parts):
raise ValueError("artifact path must stay beneath output root")
parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)
temporary = f".{path.name}.tmp-{secrets.token_hex(8)}"
descriptor = -1
try:
try:
existing = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)
except FileNotFoundError:
existing = None
if existing is not None and not stat.S_ISREG(existing.st_mode):
raise ValueError(f"output artifact must be a regular file: {relative}")
data = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + b"\n"
descriptor = os.open(
temporary,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600,
dir_fd=parent_fd,
)
view = memoryview(data)
while view:
written = os.write(descriptor, view)
view = view[written:]
os.fsync(descriptor)
os.close(descriptor)
descriptor = -1
os.replace(temporary, path.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
os.fsync(parent_fd)
if relative not in self._written:
self._written.append(relative)
finally:
if descriptor >= 0:
os.close(descriptor)
try:
os.unlink(temporary, dir_fd=parent_fd)
except FileNotFoundError:
pass
os.close(parent_fd)
def artifact_paths(self) -> list[str]:
return sorted(self._written)
def artifact_metadata(self, relative: str) -> tuple[int, str]:
path = Path(relative)
parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)
descriptor = -1
try:
descriptor = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=parent_fd)
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
raise ValueError(f"output artifact must be a regular file: {relative}")
digest = hashlib.sha256()
total = 0
while True:
chunk = os.read(descriptor, 1024 * 1024)
if not chunk:
break
total += len(chunk)
digest.update(chunk)
return total, digest.hexdigest()
finally:
if descriptor >= 0:
os.close(descriptor)
os.close(parent_fd)
def parse_feature_output(output: str, *, scene_threshold: float = 0.30) -> dict[str, Any]:
"""Parse ffmpeg ``metadata=print`` output into timestamped measurements."""
records: list[dict[str, float]] = []
current: dict[str, float] | None = None
for raw_line in output.splitlines():
line = raw_line.strip()
match = re.search(r"pts_time:([-+0-9.eE]+)", line)
if line.startswith("frame:") and match:
if current:
records.append(current)
current = {"time": float(match.group(1))}
continue
if current is None or "=" not in line:
continue
key, value = line.rsplit("=", 1)
mapped = {
"lavfi.signalstats.YAVG": "luma_raw",
"lavfi.signalstats.SATAVG": "saturation_raw",
"lavfi.signalstats.HUEAVG": "hue",
"lavfi.scene_score": "scene_score",
}.get(key)
if mapped:
try:
current[mapped] = float(value)
except ValueError:
pass
if current:
records.append(current)
style_samples = []
scene_changes = []
for record in records:
if "luma_raw" in record:
style_samples.append({
"time": round(record["time"], 6),
"luma": round(record["luma_raw"] / 255.0, 6),
"saturation": round(record.get("saturation_raw", 0.0) / 100.0, 6),
"hue": record.get("hue"),
})
if record.get("scene_score", 0.0) >= scene_threshold:
scene_changes.append(round(record["time"], 6))
return {"style_samples": style_samples, "scene_changes": scene_changes}
def _run_ffmpeg_features(path: Path) -> dict[str, Any]:
ffmpeg = shutil.which("ffmpeg")
if not ffmpeg:
raise MediaToolUnavailable("ffmpeg is required for temporal/style feature extraction")
filters = (
"scale=320:-2,"
"select='not(mod(n\\,12))+gt(scene\\,0.30)',"
"signalstats,metadata=print:file=-"
)
result = subprocess.run(
[ffmpeg, "-v", "error", "-i", str(path), "-vf", filters,
"-an", "-vsync", "0", "-f", "null", "-"],
check=True,
capture_output=True,
text=True,
)
return parse_feature_output(result.stderr + "\n" + result.stdout)
def probe_media(path: Path) -> dict[str, Any]:
"""Probe local media and extract timestamped style/temporal features."""
ffprobe = shutil.which("ffprobe")
if not ffprobe:
raise MediaToolUnavailable("ffprobe is required for reference probing")
command = [
ffprobe, "-v", "error", "-show_streams", "-show_format",
"-of", "json", str(path),
]
result = subprocess.run(command, check=True, capture_output=True, text=True)
payload = json.loads(result.stdout)
video: dict[str, Any] = next(
(stream for stream in payload.get("streams", []) if stream.get("codec_type") == "video"),
{},
)
duration = float(video.get("duration") or payload.get("format", {}).get("duration") or 0)
rate = str(video.get("avg_frame_rate") or video.get("r_frame_rate") or "0/1")
num, _, den = rate.partition("/")
fps = float(num) / float(den or 1) if float(den or 1) else 0.0
feature_data = _run_ffmpeg_features(path)
measured_times = sorted({
float(sample["time"]) for sample in feature_data["style_samples"]
} | set(feature_data["scene_changes"]))
sample_times = measured_times or [
round(duration * fraction, 6) for fraction in (0.125, 0.375, 0.625, 0.875)
]
return {
"duration": duration,
"width": int(video.get("width") or 0),
"height": int(video.get("height") or 0),
"fps": fps,
"codec": str(video.get("codec_name") or "unknown"),
"pixel_format": str(video.get("pix_fmt") or "unknown"),
"color_space": str(video.get("color_space") or "unknown"),
"sample_times": sample_times,
"style_samples": feature_data["style_samples"],
"scene_changes": feature_data["scene_changes"],
}
def _prompt(modality: str, genre: dict[str, Any], features: dict[str, Any]) -> str:
signature = genre["signature"]
base = (
f"Genre {genre['number']}: {genre['label']}. "
f"Materials: {', '.join(signature['materials'])}. "
f"Motion: {', '.join(signature['motion'])}. "
f"Composition: {', '.join(signature['composition'])}. "
f"Avoid: {', '.join(signature['avoid'])}. "
f"Reference evidence: {features['reference_count']} file(s), "
f"{features['total_duration']:.3f}s total."
)
suffix = {
"image": " Create one still image with explicit subject placement and no temporal language.",
"video": " Create a moving shot with camera motion and non-looping temporal progression.",
"3d_asset": " Create a watertight textured 3D asset with front, side, and material consistency.",
}[modality]
return base + suffix
def _build_effect_recipe(
config: dict[str, Any], specs: list[dict[str, Any]], references: list[dict[str, Any]]
) -> dict[str, Any]:
"""Build a deterministic, seeded, non-periodic Resolve placement plan."""
seed = int(config["seed"])
rng = random.Random(seed)
by_genre = {
spec["number"]: [ref for ref in references if ref["genre_number"] == spec["number"]]
for spec in specs
}
configured_duration = config.get("resolve_duration")
if configured_duration is not None and (
not _finite_real(configured_duration) or float(configured_duration) <= 0
):
raise ValueError("resolve_duration must be finite and positive")
duration = float(configured_duration or sum(
spec["measured_features"]["total_duration"] for spec in specs
))
duration = max(duration, 6.0)
effect_names = {
"flash-ethereal": "bloom_flash",
"3d-cyber-glitch": "cv_wireframe_lock",
"fluid-sketch": "fluid_contour_bleed",
}
event_times: list[float] = []
clock = round(rng.uniform(0.35, 0.75), 6)
while clock <= duration - 0.08 and len(event_times) < 18:
event_times.append(clock)
clock = round(clock + rng.uniform(0.61, 2.17), 6)
# Short timelines use deterministic, aperiodic fallback positions rather
# than forcing later random draws beyond the declared duration.
if len(event_times) < 4:
event_times.extend(round(duration * fraction, 6) for fraction in (0.10, 0.28, 0.53, 0.82))
event_times = sorted({time for time in event_times if 0 <= time <= duration - 0.08})[:18]
if len(event_times) < 4:
raise ValueError("timeline is too short for a fail-closed aperiodic effect schedule")
events: list[dict[str, Any]] = []
for index, clock in enumerate(event_times):
spec = specs[index % len(specs)]
ref = by_genre[spec["number"]][index % len(by_genre[spec["number"]])]
sample_times = ref["probe"].get("sample_times", [])
evidence_time = float(sample_times[index % len(sample_times)]) if sample_times else 0.0
source_duration = ref["source_duration"]
requires_anchor = spec["slug"] == "3d-cyber-glitch"
event_duration = round(min(rng.uniform(0.08, 0.42), duration - clock), 6)
event: dict[str, Any] = {
"event_id": f"fx-{index:03d}",
"time": clock,
"duration": event_duration,
"genre_number": spec["number"],
"effect": effect_names.get(spec["slug"], "reference_accent"),
"requires_subject_anchor": requires_anchor,
"placement": {
"safe_area": 0.08,
"max_coverage": 0.30 if requires_anchor else 0.35,
"occlusion_policy": "preserve_subject_face_and_readable_type",
"track_space": "source_normalized",
},
"evidence": {
"reference_sha256": ref["sha256"],
"time": evidence_time,
"source_duration": source_duration,
"style_fingerprint": spec["style_fingerprint"],
},
}
if requires_anchor:
event["subject_anchor"] = {
"mode": "segmentation_track",
"target": "primary_subject",
"source_ref_sha256": ref["sha256"],
"evidence_time": evidence_time,
"source_duration": source_duration,
"lost_policy": "disable_effect_until_track_recovers",
}
events.append(event)
return {
"schema_version": 1,
"dry_run": True,
"provider_calls": 0,
"provider_execution": False,
"seed": seed,
"rng_algorithm": "python.random.Random/v1",
"periodic": False,
"timeline_duration": duration,
"events": events,
"placement_constraints": {
"subject_anchored_cv_only": True,
"full_frame_3d_not_corner_overlay": True,
"preserve_titles_and_faces": True,
"disable_on_track_loss": True,
},
}
def run_workflow(config_path: str | Path, out_dir: str | Path, *, probe: Probe | None = None) -> dict[str, Any]:
"""Execute the deterministic offline contract and return its receipt."""
config_path = Path(config_path)
out_dir = Path(out_dir)
config = json.loads(config_path.read_text(encoding="utf-8"))
probe = probe or probe_media
def resolve_input(raw_path: str) -> Path:
candidate = Path(raw_path).expanduser()
if not candidate.is_absolute():
candidate = config_path.parent / candidate
return Path(os.path.abspath(candidate))
if config.get("schema_version") != 1:
raise ValueError("workflow schema_version must be 1")
if config.get("dry_run", True) is not True:
raise ValueError("workflow requires dry_run=true; provider execution is disabled")
source_policy = config.get("source_availability_policy", "allow_unavailable")
if source_policy not in {"allow_unavailable", "require_available"}:
raise ValueError("source_availability_policy must be allow_unavailable or require_available")
genres = sorted(config.get("genres", []), key=lambda item: item["number"])
if not genres:
raise ValueError("workflow needs at least one numbered genre")
output = _SafeOutput(out_dir)
output.prepare(("genres", "manifests", "resolve"))
references: list[dict[str, Any]] = []
specs: list[dict[str, Any]] = []
manifests: dict[str, dict[str, Any]] = {
modality: {
"schema_version": 1,
"modality": modality,
"dry_run": True,
"submit": False,
"provider_calls": 0,
"provider_execution": False,
"requests": [],
}
for modality in _MODALITIES
}
provenance_rules: list[dict[str, Any]] = []
evidence_files: list[dict[str, Any]] = []
for raw_path in config.get("evidence_files", []):
path = resolve_input(raw_path)
if not path.is_file():
raise FileNotFoundError(path)
suffix = path.suffix.lower()
evidence_files.append({
"path": str(path),
"bytes": path.stat().st_size,
"sha256": _sha256(path),
"kind": (
"editorial" if suffix in {".edl", ".fcpxml"}
else "archive" if suffix == ".zip"
else "workflow_record"
),
})
for genre in genres:
genre_refs: list[dict[str, Any]] = []
for raw_path in genre.get("references", []):
path = resolve_input(raw_path)
if not path.is_file():
raise FileNotFoundError(path)
measured, source_bytes, source_digest = _stable_probe(path, probe)
source_duration = _validate_probe(measured)
ref = {
"genre_number": genre["number"],
"genre_slug": genre["slug"],
"path": str(path),
"bytes": source_bytes,
"sha256": source_digest,
"source_duration": source_duration,
"probe": measured,
}
references.append(ref)
genre_refs.append(ref)
if not genre_refs:
raise ValueError(f"genre {genre['slug']} has no references")
total_duration = round(sum(float(ref["probe"].get("duration") or 0) for ref in genre_refs), 6)
scene_change_evidence = [{
"sha256": ref["sha256"],
"source_duration": ref["source_duration"],
"times": [float(time) for time in ref["probe"].get("scene_changes", [])],
} for ref in genre_refs]
scene_changes = [time for item in scene_change_evidence for time in item["times"]]
scene_intervals = [
later - earlier
for earlier, later in zip(scene_changes, scene_changes[1:]) # noqa: RUF007
]
style_samples = [
sample
for ref in genre_refs
for sample in ref["probe"].get("style_samples", [])
]
luma = [float(sample["luma"]) for sample in style_samples if "luma" in sample]
saturation = [
float(sample["saturation"])
for sample in style_samples
if "saturation" in sample
]
features = {
"reference_count": len(genre_refs),
"total_duration": total_duration,
"sample_times": [
{
"sha256": ref["sha256"],
"source_duration": ref["source_duration"],
"times": ref["probe"].get("sample_times", []),
}
for ref in genre_refs
],
"temporal": {
"scene_change_count": len(scene_changes),
"scene_change_evidence": scene_change_evidence,
"scene_interval_mean": (
round(statistics.fmean(scene_intervals), 6) if scene_intervals else 0.0
),
"scene_interval_variance": (
round(statistics.pvariance(scene_intervals), 6)
if len(scene_intervals) > 1 else 0.0
),
},
"style": {
"sample_count": len(style_samples),
"luma_mean": round(statistics.fmean(luma), 6) if luma else None,
"saturation_mean": (
round(statistics.fmean(saturation), 6) if saturation else None
),
},
}
fingerprint_payload = {"signature": genre["signature"], "features": features}
fingerprint = hashlib.sha256(_canonical_bytes(fingerprint_payload)).hexdigest()
spec = {
"schema_version": 1,
"number": genre["number"],
"slug": genre["slug"],
"label": genre["label"],
"signature": genre["signature"],
"measured_features": features,
"style_fingerprint": fingerprint,
"dry_run": True,
}
specs.append(spec)
output.write_json(f"genres/{genre['number']:02d}-{genre['slug']}.json", spec)
evidence = [
{
"reference_sha256": ref["sha256"],
"times": ref["probe"].get("sample_times", []),
"source_duration": ref["source_duration"],
"feature_keys": ["duration", "fps", "style_samples", "scene_changes"],
}
for ref in genre_refs
]
for axis, values in genre["signature"].items():
provenance_rules.append({
"rule_id": f"genre-{genre['number']}-{axis}",
"genre_number": genre["number"],
"axis": axis,
"rule": values,
"evidence": evidence,
})
for modality in _MODALITIES:
prompt = _prompt(modality, genre, features)
modality_index = _MODALITIES.index(modality)
request_seed = int(config["seed"]) + genre["number"] * 100 + modality_index
endpoint = {
"image": "fal-ai/flux/dev",
"video": "fal-ai/kling-video/v2.1/master/text-to-video",
"3d_asset": "fal-ai/hunyuan3d/v2",
}[modality]
body: dict[str, Any] = {"prompt": prompt, "seed": request_seed}
if modality == "image":
body.update({"image_size": "landscape_16_9", "num_images": 1})
elif modality == "video":
body.update({"aspect_ratio": "16:9", "duration": "5", "generate_audio": False})
else:
body.update({
"output_format": "glb",
"generate_texture": True,
"input_image_artifact": (
f"manifest://image/{config['run_id']}-{genre['number']}-image"
),
})
manifests[modality]["requests"].append({
"request_id": f"{config['run_id']}-{genre['number']}-{modality}",
"genre_number": genre["number"],
"genre_slug": genre["slug"],
"style_fingerprint": fingerprint,
"prompt": prompt,
"provider": "fal",
"endpoint_candidate": endpoint,
"endpoint_status": "historical_candidate_unverified_no_network_lookup",
"provider_call_mode": "disabled",
"provider_calls": 0,
"provider_execution": False,
"request_body": body,
"submit": False,
"dry_run": True,
"reference_sha256": [ref["sha256"] for ref in genre_refs],
})
for modality, manifest in manifests.items():
output.write_json(f"manifests/{modality}.json", manifest)
output.write_json("references.json", references)
output.write_json("evidence_files.json", evidence_files)
output.write_json("provenance.json", {
"schema_version": 1,
"run_id": config["run_id"],
"rules": provenance_rules,
})
effect_recipe = _build_effect_recipe(config, specs, references)
output.write_json("resolve/effect_recipe.json", effect_recipe)
def all_probe_times(ref: dict[str, Any]) -> list[float]:
probe_payload = ref["probe"]
return sorted({
*[float(time) for time in probe_payload.get("sample_times", [])],
*[float(time) for time in probe_payload.get("scene_changes", [])],
*[float(sample["time"]) for sample in probe_payload.get("style_samples", [])],
})
def reference_provenance(selected: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [{
"reference_path": ref["path"],
"reference_sha256": ref["sha256"],
"reference_times": all_probe_times(ref),
"source_duration": ref["source_duration"],
"time_basis": "media_seconds",
} for ref in selected]
whole_file_provenance = [{
"reference_path": item["path"],
"reference_sha256": item["sha256"],
"reference_times": [],
"time_basis": "whole_file",
} for item in evidence_files]
evidence_artifacts: list[dict[str, Any]] = []
all_genres = [spec["number"] for spec in specs]
all_modalities = list(_MODALITIES)
for relative in output.artifact_paths():
artifact_path = Path(relative)
genre_numbers = list(all_genres)
modalities = list(all_modalities)
sources = reference_provenance(references)
if relative.startswith("genres/"):
genre_number = int(artifact_path.name.split("-", 1)[0])
genre_numbers = [genre_number]
sources = reference_provenance([
ref for ref in references if ref["genre_number"] == genre_number
])
elif relative.startswith("manifests/"):
modality = artifact_path.stem
modalities = [modality]
elif relative == "evidence_files.json" and whole_file_provenance:
genre_numbers = []
modalities = []
sources = whole_file_provenance
elif relative == "resolve/effect_recipe.json":
modalities = ["video"]
source_by_digest = {ref["sha256"]: ref for ref in references}
exact: dict[str, set[float]] = {}
for event in effect_recipe["events"]:
evidence = event["evidence"]
exact.setdefault(evidence["reference_sha256"], set()).add(float(evidence["time"]))
sources = [{
"reference_path": source_by_digest[digest]["path"],
"reference_sha256": digest,
"reference_times": sorted(times),
"source_duration": source_by_digest[digest]["source_duration"],
"time_basis": "media_seconds",
} for digest, times in sorted(exact.items())]
artifact_bytes, artifact_sha256 = output.artifact_metadata(relative)
evidence_artifacts.append({
"path": relative,
"bytes": artifact_bytes,
"sha256": artifact_sha256,
"genre_numbers": genre_numbers,
"modalities": modalities,
"provider_execution": False,
"provenance": sources,
})
receipt = {
"schema_version": 1,
"run_id": config["run_id"],
"seed": int(config["seed"]),
"dry_run": True,
"provider_calls": 0,
"provider_execution": False,
"source_availability_policy": source_policy,
"references": references,
"evidence_files": evidence_files,
"evidence_artifacts": evidence_artifacts,
"genre_fingerprints": [spec["style_fingerprint"] for spec in specs],
"artifacts": {
"genres": len(specs),
"manifests": list(_MODALITIES),
"provenance_rules": len(provenance_rules),
"evidence_artifacts": len(evidence_artifacts),
},
}
receipt["receipt_sha256"] = hashlib.sha256(_canonical_bytes(receipt)).hexdigest()
output.write_json("receipt.json", receipt)
output.close()
return receipt