video-frame-regen / src /selfcheck.py
kpranav022's picture
Added audio generator and corrected the image generator.
d98289d
Raw
History Blame Contribute Delete
55.9 kB
"""Assert-based self-check for Gemini frame JSON parsing + timestamp list."""
from __future__ import annotations
from src.extract_frames import clamp_timestamp, extract_frames # noqa: F401
from src.gemini_analyze import parse_scene_analysis, timestamps_from_analysis
def main() -> None:
raw = """
{
"overall_quality": "360p phone UGC, soft and compressed",
"frames": [
{"timestamp_sec": 12.4, "label": "hook", "description": "person talking", "quality_cues": "soft"},
{"timestamp_sec": 12.6, "label": "dup", "description": "almost same", "quality_cues": "soft"},
{"timestamp_sec": 90, "label": "demo", "description": "product closeup", "quality_cues": "blocky"},
{"timestamp_sec": 200.5, "label": "cta", "description": "end screen", "quality_cues": "noisy"}
]
}
"""
analysis = parse_scene_analysis(raw) # auto: no user max — keep all after dedupe
assert analysis.overall_quality.startswith("360p"), analysis.overall_quality
# 12.4 and 12.6 are < 0.5s apart → dedupe keeps one
assert len(analysis.frames) == 3, [f.timestamp_sec for f in analysis.frames]
ts = timestamps_from_analysis(analysis)
assert ts == [12.4, 90.0, 200.5], ts
capped = parse_scene_analysis(raw, max_frames=2)
assert len(capped.frames) == 2
assert timestamps_from_analysis(capped) == [12.4, 90.0]
from src.gemini_analyze import (
SCRIPT_MIN_FRAMES,
_build_prompt,
_build_script_only_prompt,
_frames_cap,
_script_frames_cap,
)
prompt = _build_prompt(duration_sec=30.0, script=None)
assert "DISTINCT" in prompt
assert "fixed count" in prompt.lower() or "YOURSELF" in prompt
assert _frames_cap(None) == 32
assert _frames_cap(5) == 5
assert SCRIPT_MIN_FRAMES == 30
assert _script_frames_cap(None) >= SCRIPT_MIN_FRAMES
assert _script_frames_cap(10) >= SCRIPT_MIN_FRAMES
script_prompt = _build_prompt(duration_sec=30.0, script="Hook.\nBody.\nCTA.")
assert "SCRIPT decides" in script_prompt or "story beats" in script_prompt
assert "DISTINCT shots / images" not in script_prompt
assert "Hook." in script_prompt
assert str(SCRIPT_MIN_FRAMES) in script_prompt
assert "AT LEAST" in script_prompt
only = _build_script_only_prompt("Hook.\nBody.\nCTA.")
assert "No video" in only or "script-only" in only.lower()
assert "Hook." in only and "CTA." in only
assert "Files API" not in only
assert "DISTINCT shots / images" not in only
assert "timestamp_sec" in only
assert str(SCRIPT_MIN_FRAMES) in only
assert "AT LEAST" in only
# Script mode may keep close timestamps (different beats, same source moment)
close_beats = parse_scene_analysis(
{
"overall_quality": "x",
"frames": [
{
"timestamp_sec": 5.0,
"label": "a",
"description": "d",
"quality_cues": "q",
"script_line": "beat one",
"regen_brief": "r1",
},
{
"timestamp_sec": 5.2,
"label": "b",
"description": "d",
"quality_cues": "q",
"script_line": "beat two",
"regen_brief": "r2",
},
],
},
dedupe_close=False,
)
assert len(close_beats.frames) == 2
from src.regenerate import (
ASPECT_RATIOS,
DEFAULT_ASPECT_RATIO,
GROK_MODEL,
_caption_remove_prompt,
_near_copy_prompt,
_script_t2i_prompt,
_normalize_aspect_ratio,
_scrub,
)
assert DEFAULT_ASPECT_RATIO == "auto"
assert "9:16" in ASPECT_RATIOS and "16:9" in ASPECT_RATIOS
assert _normalize_aspect_ratio("9:16") == "9:16"
assert _normalize_aspect_ratio("nope") == "auto"
_check_story_order()
_check_cast_routing()
_check_text_cleanup()
_check_retry_and_rejection()
_check_references()
_check_crop_to_ratio()
_check_fal_payload()
_check_animate_payload()
_check_clip_join()
fenced = parse_scene_analysis(
'```json\n{"overall_quality":"x","frames":[{"timestamp_sec":1,"label":"a","description":"d","quality_cues":"q"}]}\n```',
max_frames=5,
)
assert timestamps_from_analysis(fenced) == [1.0]
# Gemini sometimes returns times past EOF (e.g. 856s on a ~746s video)
past_eof = parse_scene_analysis(
{
"overall_quality": "x",
"frames": [
{"timestamp_sec": 10, "label": "a", "description": "d", "quality_cues": "q"},
{"timestamp_sec": 856, "label": "b", "description": "d", "quality_cues": "q"},
],
},
max_frames=8,
duration_sec=746.173,
)
assert timestamps_from_analysis(past_eof) == [10.0, clamp_timestamp(856, 746.173)]
assert past_eof.frames[-1].timestamp_sec < 746.173
scripted = parse_scene_analysis(
{
"overall_quality": "soft phone",
"frames": [
{
"timestamp_sec": 5,
"label": "hook",
"description": "person talking to camera",
"quality_cues": "soft",
"script_line": "Stop wasting money on bad ads.",
"regen_brief": "Creator pointing at camera, serious expression",
}
],
},
max_frames=4,
duration_sec=100,
)
assert scripted.frames[0].script_line.startswith("Stop wasting")
assert "pointing" in scripted.frames[0].regen_brief
from src.gemini_analyze import FrameSpec
plain = FrameSpec(timestamp_sec=0, description="house with trucks", quality_cues="soft")
caption_prompt = _near_copy_prompt(plain)
assert "caption" in caption_prompt.lower() or "timestamp" in caption_prompt.lower()
assert "police" not in caption_prompt.lower()
assert "dead" not in caption_prompt.lower()
assert GROK_MODEL == "xai/grok-imagine-image"
assert "Erase every letter" in _caption_remove_prompt(0)
assert "Aggressively wipe" in _caption_remove_prompt(1)
near = _near_copy_prompt(scripted.frames[0], "soft phone")
assert "Stop wasting money" in near
assert "police" not in _scrub("Two police officers stand on the porch. Soft light.")
assert "Soft light" in _scrub("Two police officers stand on the porch. Soft light.")
t2i = _script_t2i_prompt(scripted.frames[0], "soft phone")
assert "Stop wasting money" in t2i
assert "pointing" in t2i.lower() or "Creator" in t2i
assert "Edit this phone-video" not in t2i
assert "No captions" in t2i or "no captions" in t2i.lower()
# pipeline accepts script-only (no video) at the API boundary
import inspect
from src.pipeline import run_pipeline
sig = inspect.signature(run_pipeline)
assert "video_path" in sig.parameters
assert sig.parameters["video_path"].default is None
_check_parallel_regen()
_check_edit_approve()
_check_reel()
_check_script_leads_prompt()
_check_short_script_still_runs()
_check_voiceover()
print("selfcheck ok")
def _check_script_leads_prompt() -> None:
"""The beat outranks the source frame, or the edit endpoint returns a near-copy."""
from src.gemini_analyze import FrameSpec
from src.regenerate import _script_match_prompt
frame = FrameSpec(
timestamp_sec=3,
description="woman on a sofa",
script_line="I saved £240 in ten minutes.",
regen_brief="grinning at her phone in a supermarket car park",
)
prompt = _script_match_prompt(frame)
assert "car park" in prompt and "£240" in prompt
# what the old wording promised the model, and why every frame came back as the source
assert "Adjust expression/pose only as needed" not in prompt, prompt
assert "Keep the same people, framing" not in prompt, prompt
assert "do not return a copy of the input" in prompt, prompt
# the source frame is explicitly demoted to people + look, and the beat decides the rest
assert "The beat decides what is in the picture" in prompt, prompt
assert "the still only supplies the people" in prompt, prompt
def _check_short_script_still_runs() -> None:
"""A script with fewer beats than the floor generates them, it does not abort the run."""
import json
from src import gemini_analyze as ga
def frames(n: int) -> str:
return json.dumps(
{
"overall_quality": "soft phone",
"cast": [{"id": "c1", "name": "mum", "description": "30s, brown hair"}],
"frames": [
{
"timestamp_sec": float(i),
"label": f"beat {i}",
"description": "d",
"quality_cues": "q",
"script_line": f"line {i}",
"regen_brief": f"brief {i}",
"character_ids": ["c1"],
}
for i in range(n)
],
}
)
calls: list[int] = []
real_client = ga._gemini_client
real_json = ga._generate_json
ga._gemini_client = lambda **kw: (object(), "fake-model")
ga._generate_json = lambda client, model_id, contents: (
calls.append(len(contents)) or frames(6)
)
try:
analysis = ga.analyze_script("Hook. Body. CTA.")
finally:
ga._gemini_client = real_client
ga._generate_json = real_json
# one corrective retry was still attempted, then the short answer was accepted
assert len(calls) == 2, calls
assert len(analysis.frames) == 6, len(analysis.frames)
assert [f.script_line for f in analysis.frames] == [f"line {i}" for i in range(6)]
def _check_voiceover() -> None:
"""TTS payload, and that the mux keeps the video's length and drops its own audio."""
import json
import subprocess
import tempfile
from pathlib import Path
from src import voice
from src.voice import (
MAX_CHARS,
TTS_MODEL,
clamp_speed,
estimate_cost,
normalize_voice,
script_text_for,
)
assert TTS_MODEL == "fal-ai/elevenlabs/tts/multilingual-v2"
assert clamp_speed(3.0) == 1.2 and clamp_speed(0.1) == 0.7 # endpoint rejects the rest
assert normalize_voice("") == "Rachel"
assert round(estimate_cost("x" * 1000), 4) == 0.10
class _Spec:
def __init__(self, line: str) -> None:
self.script_line = line
class _Res:
def __init__(self, line: str) -> None:
self.spec = _Spec(line)
# one script line spread over several beats must be read once, not three times
assert script_text_for([_Res("a"), _Res("a"), _Res("b")]) == "a\nb"
assert script_text_for(" pasted script ") == "pasted script"
calls: list[tuple[str, dict]] = []
real_sub = voice.fal_client.subscribe
real_get = voice.requests.get
voice.fal_client.subscribe = lambda m, arguments=None, **kw: (
calls.append((m, arguments)) or {"audio": {"url": "https://f/vo.mp3"}}
)
voice.requests.get = lambda url, **kw: type(
"R", (), {"content": b"mp3", "raise_for_status": lambda self: None}
)()
import os
real_key = os.environ.get("FAL_KEY")
os.environ["FAL_KEY"] = "test"
try:
with tempfile.TemporaryDirectory() as tmp:
out = voice.generate_voiceover(
" Save money today. " + "x" * MAX_CHARS,
Path(tmp) / "voiceover.mp3",
voice="George",
speed=2.0,
)
assert out.read_bytes() == b"mp3"
model, args = calls[-1]
assert model == TTS_MODEL, model
assert args["voice"] == "George" and args["speed"] == 1.2, args
assert len(args["text"]) == MAX_CHARS and args["text"].startswith("Save money"), len(
args["text"]
)
assert sorted(args) == ["similarity_boost", "speed", "stability", "text", "voice"], args
finally:
voice.fal_client.subscribe = real_sub
voice.requests.get = real_get
if real_key is None:
os.environ.pop("FAL_KEY", None)
else:
os.environ["FAL_KEY"] = real_key
import shutil
if not (shutil.which("ffmpeg") and shutil.which("ffprobe")):
return
from src.assemble_video import attach_audio, has_audio
with tempfile.TemporaryDirectory() as tmp:
tmpdir = Path(tmp)
silent = tmpdir / "silent.mp4" # 3s video, no audio
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", "color=c=black:s=64x64:d=3", "-pix_fmt", "yuv420p", str(silent)],
check=True,
)
long_audio = tmpdir / "vo.mp3" # 10s of tone — longer than the video on purpose
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", "sine=frequency=440:duration=10", str(long_audio)],
check=True,
)
voiced = attach_audio(silent, long_audio, tmpdir / "voiced.mp4")
assert has_audio(voiced)
probe = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "json",
str(voiced)],
capture_output=True,
text=True,
)
seconds = float(json.loads(probe.stdout)["format"]["duration"])
# the video's length wins: a 10s voiceover on a 3s video must not stretch the video
assert 2.8 < seconds < 3.6, seconds
def _check_reel() -> None:
"""Concat list is exact; the encode itself runs only when ffmpeg is installed."""
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from src.assemble_video import DEFAULT_SECONDS_EACH, _filter_complex, build_reel
assert DEFAULT_SECONDS_EACH == 4.0
fc = _filter_complex(3, 240, 426)
assert fc.count("scale=240:426") == 3 # one chain per image, no more, no less
assert fc.endswith("[v0][v1][v2]concat=n=3:v=1:a=0[out]"), fc
assert "force_original_aspect_ratio=decrease" in fc and "setsar=1" in fc
if not (shutil.which("ffmpeg") and shutil.which("ffprobe")):
print("(skipped reel encode — ffmpeg not on PATH)")
return
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
imgs = []
# deliberately mismatched sizes — the reel must letterbox, not fail
for i, size in enumerate(("240x426", "200x400", "240x426")):
p = root / f"img_{i}.jpg"
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", f"color=c=blue:s={size}", "-frames:v", "1", str(p)],
capture_output=True,
)
assert p.is_file(), p
imgs.append(p)
out = build_reel(imgs, root / "reel.mp4", seconds_each=2.0)
assert out.is_file() and out.stat().st_size > 0
probe = subprocess.run(
["ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=width,height", "-of", "json", str(out)],
capture_output=True, text=True,
)
data = json.loads(probe.stdout)
dur = float(data["format"]["duration"])
# exact, not approximate: every image must get its full slice, including the last
assert abs(dur - 6.0) < 0.1, f"3 images x 2s should be 6s, got {dur}"
stream = data["streams"][0]
assert (stream["width"], stream["height"]) == (240, 426), stream
assert stream["width"] % 2 == 0 and stream["height"] % 2 == 0 # yuv420p needs even
# None / missing entries are skipped rather than crashing the mux
out2 = build_reel([None, imgs[0], root / "gone.jpg"], root / "r2.mp4", seconds_each=1.0)
assert out2.is_file()
# Mixed orientations must land on a real image's shape, never a square mash-up of
# max(width) x max(height).
mixed = []
for name, size in (("wide.jpg", "640x360"), ("tall.jpg", "270x480")):
p = root / name
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", f"color=c=green:s={size}", "-frames:v", "1", str(p)],
capture_output=True,
)
mixed.append(p)
out3 = build_reel(mixed, root / "r3.mp4", seconds_each=1.0)
probe3 = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height", "-of", "json", str(out3)],
capture_output=True, text=True,
)
s3 = json.loads(probe3.stdout)["streams"][0]
assert (s3["width"], s3["height"]) == (640, 360), s3 # the larger-area image wins
try:
build_reel([], root / "empty.mp4")
except ValueError:
pass
else:
raise AssertionError("build_reel must reject an empty image list")
# build_run_reel must never raise — a failed mux cannot sink a paid-for run
from src.gemini_analyze import FrameSpec, SceneAnalysis
from src.pipeline import FrameResult, RunResult, build_run_reel
def result_with(paths):
return RunResult(
run_id="r", run_dir=root, analysis=SceneAnalysis(),
frames=[FrameResult(spec=FrameSpec(timestamp_sec=0), regenerated=p)
for p in paths],
seconds_each=1.0,
)
assert build_run_reel(result_with([])) is None # no frames at all
assert build_run_reel(result_with([root / "nope.jpg"])) is None # frames all missing
logs: list[str] = []
ok = result_with(imgs)
reel = build_run_reel(ok, seconds_each=1.0, log=logs.append)
assert reel is not None and reel.name == "reel.mp4"
assert ok.reel == reel and ok.seconds_each == 1.0
assert any("3 × 1s" in m for m in logs), logs
def _check_cast_routing() -> None:
"""Each shot gets only its own characters' references — the core of the fix."""
import tempfile
from pathlib import Path
from src import pipeline
from src.gemini_analyze import CharacterSpec, FrameSpec, parse_scene_analysis
from src.regenerate import _cast_clause, _identity_lock
# --- Gemini side: cast survives parsing, unknown ids are dropped ---
parsed = parse_scene_analysis(
{
"overall_quality": "soft",
"cast": [
{"id": "c1", "name": "young mum", "description": "30s, brown bob, red coat"},
{"id": "c2", "name": "driver", "description": "50s man, grey beard, blue polo"},
{"id": "c1", "name": "dupe", "description": "ignored duplicate"},
],
"frames": [
{"timestamp_sec": 0, "label": "a", "description": "d", "quality_cues": "q",
"script_line": "one", "regen_brief": "b", "character_ids": ["c1"]},
{"timestamp_sec": 1, "label": "b", "description": "d", "quality_cues": "q",
"script_line": "two", "regen_brief": "b", "character_ids": ["c2"]},
{"timestamp_sec": 2, "label": "c", "description": "d", "quality_cues": "q",
"script_line": "three", "regen_brief": "b", "character_ids": ["c1", "c2"]},
{"timestamp_sec": 3, "label": "pack", "description": "d", "quality_cues": "q",
"script_line": "four", "regen_brief": "b", "character_ids": []},
{"timestamp_sec": 4, "label": "ghost", "description": "d", "quality_cues": "q",
"script_line": "five", "regen_brief": "b", "character_ids": ["c9"]},
],
},
sort_by_time=False,
dedupe_close=False,
)
assert [c.id for c in parsed.cast] == ["c1", "c2"], "duplicate ids must collapse"
assert parsed.frames[3].character_ids == [], "a product shot keeps nobody"
assert parsed.frames[4].character_ids == [], "an undefined id must be dropped"
assert [c.label for c in parsed.characters_in(parsed.frames[2])] == ["young mum", "driver"]
# --- prompt side: who is in the shot, and which reference is whom ---
mum, driver = parsed.cast
clause = _cast_clause([mum])
assert "young mum" in clause and "red coat" in clause
assert "driver" not in clause, "a solo shot must not describe the other character"
lock = _identity_lock([mum, driver], 2)
assert "reference image 1 is young mum" in lock
assert "reference image 2 is driver" in lock
assert "not swap" in lock or "Do not swap" in lock
assert _identity_lock([], 0) == "", "no cast and no refs means no identity clause"
# --- routing side: the reference actually handed to each frame ---
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
sheets = {}
for cid in ("c1", "c2"):
p = root / "cast" / f"{cid}.jpg"
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(b"x")
sheets[cid] = p
uploaded = root / "uploaded.jpg"
uploaded.write_bytes(b"u")
seen: dict[int, tuple[tuple[str, ...], tuple[str, ...]]] = {}
def fake_regen(original, frame, *, out_path, references=None, characters=None, **kw):
idx = int(Path(out_path).stem.split("_")[1])
seen[idx] = (
tuple(Path(r).name for r in (references or [])),
tuple(c.id for c in (characters or [])),
)
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
Path(out_path).write_bytes(b"i")
return Path(out_path)
real, pipeline.regenerate_frame = pipeline.regenerate_frame, fake_regen
try:
pipeline._regen_all(
parsed.frames, [None] * len(parsed.frames), overall_quality="",
dest=root, aspect_ratio="auto", cleanup_pass=False, workers=4,
log=lambda m: None, references=[uploaded], anchor_first=True,
cast=parsed.by_id(), sheets=sheets,
)
assert seen[0] == (("c1.jpg",), ("c1",)), seen[0]
# the whole bug: c1's face must never reach c2's shot
assert seen[1] == (("c2.jpg",), ("c2",)), seen[1]
assert seen[2] == (("c1.jpg", "c2.jpg"), ("c1", "c2")), seen[2]
assert seen[3] == ((), ()), "product shot must get no face at all"
assert seen[4] == ((), ()), "dropped id must not fall back to someone else"
assert all("uploaded.jpg" not in refs for refs, _ in seen.values())
# with no cast at all, the uploaded reference still applies everywhere
seen.clear()
plain = [FrameSpec(timestamp_sec=0, script_line="x") for _ in range(3)]
pipeline._regen_all(
plain, [None] * 3, overall_quality="", dest=root, aspect_ratio="auto",
cleanup_pass=False, workers=3, log=lambda m: None, references=[uploaded],
)
assert all(refs == ("uploaded.jpg",) for refs, _ in seen.values()), seen
finally:
pipeline.regenerate_frame = real
def _check_text_cleanup() -> None:
"""Cleanup passes are driven by the text check, not run blindly."""
import os
import tempfile
from pathlib import Path
from src import regenerate
from src.gemini_analyze import FrameSpec
from src.regenerate import _text_check_enabled, _text_fix_attempts, regenerate_frame
assert _text_fix_attempts() == 1 # one extra pass by default, as before
os.environ["MAX_TEXT_FIX_PASSES"] = "2"
assert _text_fix_attempts() == 2
os.environ["MAX_TEXT_FIX_PASSES"] = "99"
assert _text_fix_attempts() == 3, "must stay bounded — each pass costs money"
del os.environ["MAX_TEXT_FIX_PASSES"]
assert _text_check_enabled()
os.environ["TEXT_CHECK"] = "0"
assert not _text_check_enabled()
del os.environ["TEXT_CHECK"]
verdicts: list[bool | None] = []
calls: list[str] = []
real_run = regenerate._run_fal_resilient
real_detect = regenerate.detect_readable_text
real_write = regenerate._write_image
def fake_run(src, prompt, *, aspect_ratio=None, references=None):
# the video-only path's FIRST prompt is itself a caption remover, so count calls
# rather than classifying them
calls.append("cleanup" if "Aggressively wipe" in prompt else "generate")
return b"img"
regenerate._run_fal_resilient = fake_run
regenerate.detect_readable_text = lambda p, **kw: verdicts.pop(0) if verdicts else False
regenerate._write_image = lambda data, dest, ar: (dest.write_bytes(data), dest)[1]
os.environ["FAL_KEY"] = "test"
try:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
src = root / "src.jpg"
src.write_bytes(b"seed")
plain = FrameSpec(timestamp_sec=0, description="a house")
# clean first result → no second call. This is the saving versus the old code,
# which always paid for two passes.
calls.clear(); verdicts[:] = [False]
regenerate_frame(src, plain, out_path=root / "a.jpg")
assert len(calls) == 1, calls
# text seen → one cleanup pass, then verified clean
calls.clear(); verdicts[:] = [True, False]
notes: list[str] = []
regenerate_frame(src, plain, out_path=root / "b.jpg", on_note=notes.append)
assert len(calls) == 2 and calls[1] == "cleanup", calls
assert notes == [], notes
# still dirty after the allowed passes → flagged for the user, not retried forever
calls.clear(); verdicts[:] = [True, True]
notes = []
regenerate_frame(src, plain, out_path=root / "c.jpg", on_note=notes.append)
assert len(calls) == 2, calls
assert notes and "text" in notes[0], notes
# checker unavailable (None) → behave like the old always-clean-up default
calls.clear(); verdicts[:] = [None, None]
regenerate_frame(src, plain, out_path=root / "d.jpg")
assert len(calls) == 2, calls
# fast mode → single call, no check at all
calls.clear(); verdicts[:] = [True, True]
regenerate_frame(src, plain, out_path=root / "e.jpg", cleanup_pass=False)
assert len(calls) == 1, calls
# script-only frames are now checked too — they never were before
calls.clear(); verdicts[:] = [True, False]
beat = FrameSpec(timestamp_sec=0, script_line="Buy now", regen_brief="a kitchen")
regenerate_frame(None, beat, out_path=root / "f.jpg")
assert len(calls) == 2 and calls[0] == "generate", calls
# no leftover temp files from the cleanup loop
assert not list(root.glob("*_txt*.jpg")), list(root.glob("*_txt*.jpg"))
finally:
regenerate._run_fal_resilient = real_run
regenerate.detect_readable_text = real_detect
regenerate._write_image = real_write
def _check_story_order() -> None:
"""Script mode must keep Gemini's story order, not re-sort beats by timestamp."""
beats = [
{"timestamp_sec": t, "label": f"b{n}", "description": "d", "quality_cues": "q",
"script_line": f"{n}. line", "regen_brief": f"r{n}"}
for n, t in enumerate([2.0, 30.0, 8.0, 45.0, 5.0], start=1)
]
data = {"overall_quality": "x", "frames": beats}
story = [f"{n}. line" for n in range(1, 6)]
kept = parse_scene_analysis(data, dedupe_close=False, sort_by_time=False, duration_sec=60)
assert [f.script_line for f in kept.frames] == story, [f.script_line for f in kept.frames]
# timestamps still travel with their beat, they are just no longer the sort key
assert [f.timestamp_sec for f in kept.frames] == [2.0, 30.0, 8.0, 45.0, 5.0]
# no-script mode still orders by time, where the timeline *is* the running order
timed = parse_scene_analysis(data, duration_sec=60)
assert [f.timestamp_sec for f in timed.frames] == [2.0, 5.0, 8.0, 30.0, 45.0]
import inspect
from src.gemini_analyze import analyze_script, analyze_video
# both script paths must opt out of sorting
assert "sort_by_time=False" in inspect.getsource(analyze_script)
assert "sort_by_time=not has_script" in inspect.getsource(analyze_video)
def _check_retry_and_rejection() -> None:
"""Transient errors retry; refused prompts fall back to a reworded one."""
from src import regenerate
from src.regenerate import _is_transient, is_content_rejection, with_retries
class HTTPish(Exception):
def __init__(self, status):
super().__init__(f"http {status}")
self.status_code = status
class Timeouty(Exception):
pass
Timeouty.__name__ = "FalClientTimeoutError"
assert _is_transient(HTTPish(429)) and _is_transient(HTTPish(503))
assert not _is_transient(HTTPish(400)) and not _is_transient(HTTPish(422))
assert _is_transient(Timeouty())
# a 429 is a rate limit, never a moderation refusal
assert not is_content_rejection(HTTPish(429))
assert is_content_rejection(HTTPish(422))
assert is_content_rejection(Exception("Request blocked by content moderation"))
assert not is_content_rejection(Exception("some unrelated failure"))
real_sleep = regenerate.time.sleep
regenerate.time.sleep = lambda s: None
try:
tries = {"n": 0}
def flaky():
tries["n"] += 1
if tries["n"] < 3:
raise HTTPish(429)
return "ok"
assert with_retries(flaky) == "ok" and tries["n"] == 3
# permanent failures must not be retried — that would just burn time
hard = {"n": 0}
def refused():
hard["n"] += 1
raise HTTPish(422)
try:
with_retries(refused)
except Exception:
pass
assert hard["n"] == 1, hard
# and retries are bounded
always = {"n": 0}
def down():
always["n"] += 1
raise HTTPish(503)
try:
with_retries(down, attempts=3)
except Exception:
pass
assert always["n"] == 3, always
finally:
regenerate.time.sleep = real_sleep
# the reworded fallback strips the sensitive sentence that triggered the refusal
safe = regenerate._safe_variant("A cop holds a gun. A woman smiles at the camera.")
assert "gun" not in safe and "smiles" in safe
assert "brand-safe" in safe
def _check_references() -> None:
"""Identity references ride along in image_urls and change the prompt."""
from pathlib import Path
from src import regenerate
from src.gemini_analyze import FrameSpec
from src.regenerate import (
MAX_IMAGE_INPUTS,
MAX_REFERENCES,
GROK_MODEL,
GROK_MODEL_EDIT,
_run_fal,
_scene_from_reference_prompt,
)
assert MAX_IMAGE_INPUTS == 3 and MAX_REFERENCES == 2
frame = FrameSpec(timestamp_sec=0, script_line="Buy now", regen_brief="woman in a kitchen")
prompt = _scene_from_reference_prompt(frame, "", [], 1)
assert "woman in a kitchen" in prompt and "Buy now" in prompt
assert "same face" in prompt # the identity lock
# The scene must come before the reference is mentioned, and the reference must be
# identity-only: leading with "keep the same person" returned the portrait with a new
# background and lost the beat.
assert prompt.index("woman in a kitchen") < prompt.index("reference image"), prompt
assert "ONLY the person's identity" in prompt, prompt
assert "Ignore its background, framing, pose and expression" in prompt, prompt
here = Path(__file__)
calls: list[tuple[str, dict]] = []
real_sub = regenerate.fal_client.subscribe
real_up = regenerate.upload_image
real_dl = regenerate._download_output
regenerate.fal_client.subscribe = lambda m, arguments=None, **kw: (
calls.append((m, arguments)) or {"images": [{"url": "https://f/o.jpg"}]}
)
regenerate.upload_image = lambda p, **kw: f"https://f/{Path(p).name}"
regenerate._download_output = lambda out: b"img"
try:
# no source frame + a reference → edit that reference instead of text-to-image
_run_fal(None, "p", aspect_ratio="9:16", references=[here])
model, args = calls[-1]
assert model == GROK_MODEL_EDIT, model
assert args["image_urls"] == [f"https://f/{here.name}"], args
# no source, no reference → still plain text-to-image
_run_fal(None, "p", aspect_ratio="9:16")
assert calls[-1][0] == GROK_MODEL, calls[-1][0]
assert "image_urls" not in calls[-1][1]
# source frame + references → frame first, references after, capped at 3 total
_run_fal(here, "p", aspect_ratio="auto", references=[here, here, here, here])
_, args = calls[-1]
assert len(args["image_urls"]) == MAX_IMAGE_INPUTS, args["image_urls"]
# a reference path that does not exist is ignored, not sent as a broken URL
_run_fal(here, "p", aspect_ratio="auto", references=[Path("/nope/x.jpg")])
assert len(calls[-1][1]["image_urls"]) == 1
finally:
regenerate.fal_client.subscribe = real_sub
regenerate.upload_image = real_up
regenerate._download_output = real_dl
def _check_animate_payload() -> None:
"""Arguments sent to the image-to-video endpoint, plus the cost estimate."""
import os
import tempfile
from pathlib import Path
from src import animate
from src.animate import (
DEFAULT_CLIP_SECONDS,
GROK_VIDEO_MODEL,
_extract_video_url,
animate_frame,
clamp_seconds,
estimate_cost,
motion_prompt,
normalize_video_aspect_ratio,
)
from src.gemini_analyze import FrameSpec
assert DEFAULT_CLIP_SECONDS == 4
# fal takes an integer 1..15
assert clamp_seconds(4) == 4 and clamp_seconds(4.4) == 4
assert clamp_seconds(0) == 1 and clamp_seconds(99) == 15
assert clamp_seconds(None) == 4 and clamp_seconds("x") == 4
# the video enum is narrower than the image one — 2:1 is valid for stills, not clips
assert normalize_video_aspect_ratio("9:16") == "9:16"
assert normalize_video_aspect_ratio("2:1") == "auto"
assert normalize_video_aspect_ratio(None) == "auto"
# 720p is ~1.75x 480p; a 4s 720p clip is ~$0.56
assert abs(estimate_cost(1, 4, "720p") - 0.56) < 0.01
assert abs(estimate_cost(10, 4, "480p") - 3.20) < 0.01
assert estimate_cost(0, 4, "720p") == 0
prompt = motion_prompt(FrameSpec(timestamp_sec=0, regen_brief="woman pointing at camera"))
assert "woman pointing" in prompt
assert "No new text" in prompt and "No scene cuts" in prompt
# sensitive wording is scrubbed here too, same as for stills
assert "police" not in motion_prompt(FrameSpec(timestamp_sec=0, regen_brief="a police car."))
assert "zoom slowly" in motion_prompt(None, "zoom slowly")
assert _extract_video_url({"video": {"url": "https://x/v.mp4"}}) == "https://x/v.mp4"
assert _extract_video_url({"video": None}) is None
assert _extract_video_url({}) is None
assert _extract_video_url(None) is None
calls: list[tuple[str, dict]] = []
real_sub = animate.fal_client.subscribe
real_up = animate.upload_image # animate imports it by name, so patch it there
real_get = animate.requests.get
class FakeResp:
content = b"fakemp4bytes"
def raise_for_status(self):
return None
with tempfile.TemporaryDirectory() as tmp:
img = Path(tmp) / "f.jpg"
img.write_bytes(b"jpeg")
animate.fal_client.subscribe = lambda m, arguments=None, **kw: (
calls.append((m, arguments)) or {"video": {"url": "https://fal.media/v.mp4"}}
)
animate.upload_image = lambda p, **kw: "https://fal.media/in.jpg"
animate.requests.get = lambda url, **kw: FakeResp()
os.environ["FAL_KEY"] = "test-key"
try:
out = animate_frame(img, Path(tmp) / "c.mp4", seconds=4, resolution="480p",
aspect_ratio="9:16")
assert out.is_file() and out.read_bytes() == b"fakemp4bytes"
model, args = calls[-1]
assert model == GROK_VIDEO_MODEL, model
assert args["duration"] == 4 and isinstance(args["duration"], int)
assert args["resolution"] == "480p"
assert args["aspect_ratio"] == "9:16"
assert args["image_url"] == "https://fal.media/in.jpg"
assert isinstance(args["prompt"], str) and args["prompt"]
# unsupported values must be corrected before they reach fal
animate_frame(img, Path(tmp) / "c2.mp4", seconds=99, resolution="4k",
aspect_ratio="2:1")
_, args = calls[-1]
assert args["duration"] == 15
assert args["resolution"] == "720p"
assert args["aspect_ratio"] == "auto"
finally:
animate.fal_client.subscribe = real_sub
animate.upload_image = real_up
animate.requests.get = real_get
def _check_clip_join() -> None:
"""Joining real clips: order, total duration, and audio handling."""
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from src.assemble_video import build_clip_reel, has_audio
if not (shutil.which("ffmpeg") and shutil.which("ffprobe")):
print("(skipped clip join — ffmpeg not on PATH)")
return
def make_clip(path: Path, seconds: int, size: str, audio: bool) -> Path:
cmd = ["ffmpeg", "-nostdin", "-y", "-loglevel", "error",
"-f", "lavfi", "-i", f"testsrc=s={size}:rate=30:d={seconds}"]
if audio:
cmd += ["-f", "lavfi", "-i", f"sine=frequency=440:duration={seconds}",
"-c:a", "aac", "-shortest"]
cmd += ["-c:v", "libx264", "-pix_fmt", "yuv420p", "-t", str(seconds), str(path)]
subprocess.run(cmd, capture_output=True)
assert path.is_file() and path.stat().st_size > 0, path
return path
def probe(path: Path) -> dict:
out = subprocess.run(
["ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=width,height,codec_type", "-of", "json", str(path)],
capture_output=True, text=True,
).stdout
return json.loads(out)
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
# three 4s clips, all with audio, one at an odd size
clips = [
make_clip(root / "a.mp4", 4, "480x854", True),
make_clip(root / "b.mp4", 4, "360x640", True),
make_clip(root / "c.mp4", 4, "480x854", True),
]
assert all(has_audio(c) for c in clips)
out = build_clip_reel(clips, root / "final.mp4")
info = probe(out)
dur = float(info["format"]["duration"])
assert abs(dur - 12.0) < 0.3, f"3 x 4s should be ~12s, got {dur}"
kinds = {s["codec_type"] for s in info["streams"]}
assert "audio" in kinds, "audio must survive when every clip has it"
vid = next(s for s in info["streams"] if s["codec_type"] == "video")
assert (vid["width"], vid["height"]) == (480, 854), vid # largest clip's shape
# one silent clip in the mix → video-only output rather than desynced audio
mixed = [clips[0], make_clip(root / "d.mp4", 4, "480x854", False)]
out2 = build_clip_reel(mixed, root / "mixed.mp4")
info2 = probe(out2)
assert {s["codec_type"] for s in info2["streams"]} == {"video"}
assert abs(float(info2["format"]["duration"]) - 8.0) < 0.3
try:
build_clip_reel([], root / "none.mp4")
except ValueError:
pass
else:
raise AssertionError("build_clip_reel must reject an empty list")
def _check_fal_payload() -> None:
"""The exact arguments sent to fal — the API itself is never called in tests.
Guards the two things the OpenAPI schema pins down: the text-to-image endpoint has no
"auto" in its aspect_ratio enum (so the key must be omitted), and the edit endpoint
takes `image_urls` as a list, not a single `image`.
"""
from pathlib import Path
from src import regenerate
from src.gemini_analyze import FrameSpec
from src.regenerate import GROK_MODEL, GROK_MODEL_EDIT, _run_fal, _extract_url
calls: list[tuple[str, dict]] = []
def fake_subscribe(model, arguments=None, **kw):
calls.append((model, arguments))
return {"images": [{"url": "https://fal.media/out.jpg"}], "revised_prompt": "x"}
real_sub = regenerate.fal_client.subscribe
real_up = regenerate.fal_client.upload_file
real_dl = regenerate._download_output
regenerate.fal_client.subscribe = fake_subscribe
regenerate.fal_client.upload_file = lambda p, **kw: f"https://fal.media/in/{Path(p).name}"
regenerate._download_output = lambda out: b"\xff\xd8jpegbytes"
try:
# text-to-image: no image_urls, and "auto" is dropped (not a valid enum value there)
_run_fal(None, "a prompt", aspect_ratio="auto")
model, args = calls[-1]
assert model == GROK_MODEL, model
assert "image_urls" not in args
assert "aspect_ratio" not in args, args
assert args["prompt"] == "a prompt" and args["num_images"] == 1
assert args["output_format"] == "jpeg"
_run_fal(None, "p", aspect_ratio="9:16")
assert calls[-1][1]["aspect_ratio"] == "9:16"
# edit: image_urls is a list, and "auto" IS valid (means keep the input's ratio)
_run_fal(Path(__file__), "edit me", aspect_ratio="auto")
model, args = calls[-1]
assert model == GROK_MODEL_EDIT, model
assert isinstance(args["image_urls"], list) and len(args["image_urls"]) == 1
assert args["image_urls"][0].startswith("https://fal.media/in/")
assert args["aspect_ratio"] == "auto"
_run_fal(Path(__file__), "edit me", aspect_ratio="1:1")
assert calls[-1][1]["aspect_ratio"] == "1:1"
# an unsupported ratio must degrade to auto, never reach fal and 422
_run_fal(Path(__file__), "edit me", aspect_ratio="7:3")
assert calls[-1][1]["aspect_ratio"] == "auto"
finally:
regenerate.fal_client.subscribe = real_sub
regenerate.fal_client.upload_file = real_up
regenerate._download_output = real_dl
# fal response shape: {"images": [{"url": ...}]}
assert _extract_url({"images": [{"url": "https://x/y.jpg"}]}) == "https://x/y.jpg"
assert _extract_url({"images": []}) is None
assert _extract_url({}) is None
assert _extract_url(None) is None
assert _extract_url("https://x/y.jpg") == "https://x/y.jpg"
_check_regenerate_paths()
def _check_regenerate_paths() -> None:
"""All three regenerate_frame branches still produce a file, with fal faked out."""
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from src import regenerate
from src.assemble_video import probe_image_size
from src.gemini_analyze import FrameSpec
from src.regenerate import regenerate_frame
if not (shutil.which("ffmpeg") and shutil.which("ffprobe")):
print("(skipped regenerate path check — ffmpeg not on PATH)")
return
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
src_frame = root / "src.jpg"
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", "testsrc=s=720x1280", "-frames:v", "1", str(src_frame)],
capture_output=True,
)
# whatever fal "returns" is this 9:16 image, so a 1:1 request must be corrected locally
payload = src_frame.read_bytes()
calls: list[str] = []
real_sub = regenerate.fal_client.subscribe
real_up = regenerate.fal_client.upload_file
real_dl = regenerate._download_output
regenerate.fal_client.subscribe = lambda m, arguments=None, **kw: calls.append(m) or {
"images": [{"url": "https://fal.media/out.jpg"}]
}
regenerate.fal_client.upload_file = lambda p, **kw: "https://fal.media/in.jpg"
regenerate._download_output = lambda out: payload
# pretend every result is text-free; the text loop itself is covered separately
real_detect = regenerate.detect_readable_text
regenerate.detect_readable_text = lambda p, **kw: False
os.environ["FAL_KEY"] = "test-key"
try:
spec_plain = FrameSpec(timestamp_sec=0, description="a house")
out = regenerate_frame(src_frame, spec_plain, out_path=root / "a.jpg",
aspect_ratio="auto")
# verified clean → one call, where the old code always paid for two
assert out.is_file() and len(calls) == 1, calls
assert not list(root.glob("a_txt*.jpg")), "cleanup temps must be removed"
calls.clear()
out = regenerate_frame(src_frame, spec_plain, out_path=root / "b.jpg",
aspect_ratio="auto", cleanup_pass=False)
assert out.is_file() and len(calls) == 1, calls # fast mode = single pass
calls.clear()
spec_script = FrameSpec(timestamp_sec=0, script_line="Buy now", regen_brief="smile")
out = regenerate_frame(src_frame, spec_script, out_path=root / "c.jpg",
aspect_ratio="auto")
assert out.is_file() and len(calls) == 1, calls
assert not (root / "c_tmp.jpg").exists(), "scripted temp must be cleaned up"
calls.clear()
out = regenerate_frame(None, spec_script, out_path=root / "d.jpg",
aspect_ratio="auto")
assert out.is_file() and len(calls) == 1, calls # script-only = one T2I call
# the safety net: fal handed back 9:16 while 1:1 was requested
out = regenerate_frame(src_frame, spec_plain, out_path=root / "e.jpg",
aspect_ratio="1:1", cleanup_pass=False)
assert probe_image_size(out) == (720, 720), probe_image_size(out)
finally:
regenerate.fal_client.subscribe = real_sub
regenerate.fal_client.upload_file = real_up
regenerate._download_output = real_dl
regenerate.detect_readable_text = real_detect
def _check_crop_to_ratio() -> None:
"""The requested aspect ratio must reach the model as the input image's real shape."""
import shutil
import subprocess
import tempfile
from pathlib import Path
from src.assemble_video import probe_image_size
from src.regenerate import _crop_to_ratio
if not (shutil.which("ffmpeg") and shutil.which("ffprobe")):
print("(skipped crop check — ffmpeg not on PATH)")
return
with tempfile.TemporaryDirectory() as tmp:
tall = Path(tmp) / "tall.jpg" # 9:16, like a real UGC upload
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-loglevel", "error", "-f", "lavfi",
"-i", "testsrc=s=720x1280", "-frames:v", "1", str(tall)],
capture_output=True,
)
assert probe_image_size(tall) == (720, 1280)
square = _crop_to_ratio(tall, "1:1")
assert square is not None
assert probe_image_size(square) == (720, 720), probe_image_size(square)
square.unlink(missing_ok=True)
wide = _crop_to_ratio(tall, "16:9")
assert wide is not None
w, h = probe_image_size(wide)
assert w == 720 and abs(w / h - 16 / 9) < 0.02, (w, h)
wide.unlink(missing_ok=True)
# already the requested shape → no wasted re-encode
assert _crop_to_ratio(tall, "9:16") is None
# garbage ratio must not blow up a frame
assert _crop_to_ratio(tall, "auto") is None
assert _crop_to_ratio(Path(tmp) / "missing.jpg", "1:1") is None
def _check_edit_approve() -> None:
"""Approving an edit replaces the image in place and keeps the replaced version."""
import tempfile
from pathlib import Path
from src.pipeline import apply_edit, edit_candidate_path
from src.regenerate import _edit_prompt
prompt = _edit_prompt(" make it daytime ")
assert "make it daytime" in prompt
assert "ONLY the requested change" in prompt
assert "No captions" in prompt
# user wording is passed through verbatim, unlike Gemini's scrubbed descriptions
assert "police" in _edit_prompt("add a police car")
with tempfile.TemporaryDirectory() as tmp:
run = Path(tmp)
regen = run / "regen" / "frame_03_regen.jpg"
regen.parent.mkdir(parents=True)
regen.write_bytes(b"v1")
c1 = edit_candidate_path(run, 3)
assert c1.name == "frame_03_edit1.jpg" and c1.parent.name == "edits"
c1.parent.mkdir(parents=True, exist_ok=True)
c1.write_bytes(b"v2")
# a second unapproved attempt must not clobber the first
c2 = edit_candidate_path(run, 3)
assert c2.name == "frame_03_edit2.jpg", c2
final = apply_edit(regen, c1)
assert final == regen
assert regen.read_bytes() == b"v2" # replaced in place
assert not c1.exists() # candidate moved, not copied
assert regen.with_name("frame_03_regen_prev1.jpg").read_bytes() == b"v1"
# approving again keeps a second backup rather than overwriting the first
c2.write_bytes(b"v3")
apply_edit(regen, c2)
assert regen.read_bytes() == b"v3"
assert regen.with_name("frame_03_regen_prev1.jpg").read_bytes() == b"v1"
assert regen.with_name("frame_03_regen_prev2.jpg").read_bytes() == b"v2"
# earlier generation failed → candidate is adopted as-is, nothing to back up
c3 = edit_candidate_path(run, 7)
c3.write_bytes(b"new")
assert apply_edit(None, c3) == c3
try:
apply_edit(regen, run / "edits" / "missing.jpg")
except FileNotFoundError:
pass
else:
raise AssertionError("apply_edit must reject a missing candidate")
def _check_parallel_regen() -> None:
"""Concurrent regen keeps frame order and isolates per-frame failures."""
import tempfile
from pathlib import Path
from src import pipeline
from src.gemini_analyze import FrameSpec
specs = [FrameSpec(timestamp_sec=float(i), label=f"b{i}") for i in range(9)]
calls: list[int] = []
def fake_regen(original, frame, *, out_path, **kwargs):
idx = int(Path(out_path).stem.split("_")[1])
calls.append(idx)
if idx == 4:
raise RuntimeError("boom")
assert kwargs["cleanup_pass"] is False
return Path(out_path)
real, pipeline.regenerate_frame = pipeline.regenerate_frame, fake_regen
logs: list[str] = []
try:
with tempfile.TemporaryDirectory() as tmp:
results = pipeline._regen_all(
specs,
[None] * len(specs),
overall_quality="soft",
dest=Path(tmp),
aspect_ratio="auto",
cleanup_pass=False,
workers=4,
log=logs.append,
)
finally:
pipeline.regenerate_frame = real
assert len(results) == len(specs)
# results stay in submission order even though completion order is arbitrary
assert [r.spec.timestamp_sec for r in results] == [float(i) for i in range(9)]
assert sorted(calls) == list(range(9)), calls
assert results[4].regenerated is None and "boom" in (results[4].error or "")
for i, r in enumerate(results):
if i != 4:
assert r.error is None and r.regenerated is not None
assert r.regenerated.name == f"frame_{i:02d}_regen.jpg"
assert any("9/9" in m for m in logs), logs
_check_anchor_first()
def _check_anchor_first() -> None:
"""Script-only runs anchor on their own first frame so the cast stays consistent."""
import tempfile
from pathlib import Path
from src import pipeline
from src.gemini_analyze import FrameSpec
specs = [FrameSpec(timestamp_sec=0, script_line=f"beat {i}") for i in range(5)]
seen: list[tuple[int, tuple[str, ...]]] = []
def fake_regen(original, frame, *, out_path, references=None, **kwargs):
idx = int(Path(out_path).stem.split("_")[1])
seen.append((idx, tuple(Path(r).name for r in (references or []))))
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
Path(out_path).write_bytes(b"x")
return Path(out_path)
real, pipeline.regenerate_frame = pipeline.regenerate_frame, fake_regen
try:
with tempfile.TemporaryDirectory() as tmp:
logs: list[str] = []
results = pipeline._regen_all(
specs, [None] * 5, overall_quality="", dest=Path(tmp), aspect_ratio="auto",
cleanup_pass=False, workers=4, log=logs.append, anchor_first=True,
)
assert all(r.regenerated for r in results)
by_idx = dict(seen)
assert seen[0][0] == 0, "the anchor frame must be generated first"
assert by_idx[0] == (), "the anchor itself has no reference"
anchor = "frame_00_regen.jpg"
for i in range(1, 5):
assert by_idx[i] == (anchor,), (i, by_idx[i])
assert any("anchor" in m for m in logs), logs
# an explicit reference wins: no serial anchor pass, every frame uses the reference
seen.clear()
with tempfile.TemporaryDirectory() as tmp:
ref = Path(tmp) / "hero.jpg"
ref.write_bytes(b"ref")
pipeline._regen_all(
specs, [None] * 5, overall_quality="", dest=Path(tmp), aspect_ratio="auto",
cleanup_pass=False, workers=4, log=lambda m: None,
references=[ref], anchor_first=True,
)
assert all(refs == ("hero.jpg",) for _, refs in seen), seen
# a failed anchor must not abort the run
seen.clear()
def fail_first(original, frame, *, out_path, references=None, **kwargs):
idx = int(Path(out_path).stem.split("_")[1])
if idx == 0:
raise RuntimeError("anchor boom")
return fake_regen(original, frame, out_path=out_path, references=references)
pipeline.regenerate_frame = fail_first
with tempfile.TemporaryDirectory() as tmp:
logs = []
results = pipeline._regen_all(
specs, [None] * 5, overall_quality="", dest=Path(tmp), aspect_ratio="auto",
cleanup_pass=False, workers=4, log=logs.append, anchor_first=True,
)
assert results[0].regenerated is None and "boom" in (results[0].error or "")
assert all(r.regenerated for r in results[1:]), "other frames must still generate"
assert all(refs == () for _, refs in seen), seen
finally:
pipeline.regenerate_frame = real
if __name__ == "__main__":
main()