affaan-m/ECC · error · ValueError
source fps must be finite and positive; provide --fps
Error message
source fps must be finite and positive; provide --fps
What it means
After probing the first take, forge() computes FPS as the explicit fps argument if given, else info0.fps from ffprobe. If the resulting frame rate is not finite and positive (probe returned 0, garbage, or the container lacks a usable rate), it raises ValueError telling the user to supply --fps manually.
Solutions
- Pass the frame rate explicitly: forge(..., fps=30) or --fps 30 on the CLI, exactly as the message suggests.
- Check the probe result: ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate take0.mov — if it shows 0/0 the metadata is broken.
- Re-encode/remux the first take (ffmpeg -i in.mov -r 30 -c copy fixed.mov) to embed a sane frame rate.
- Verify the take file is not truncated or corrupt.
Example fix
// before (CLI) $ python forge.py noir take1.mov take2.mov --out edit.mp4 # probe fps = 0 // after $ python forge.py noir take1.mov take2.mov --out edit.mp4 --fps 30
Defensive patterns
Strategy: fallback
Validate before calling
import subprocess, json
def probe_fps(path, default=30.0):
out = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=r_frame_rate', '-of', 'json', path],
capture_output=True, text=True)
try:
num, den = json.loads(out.stdout)['streams'][0]['r_frame_rate'].split('/')
fps = float(num) / float(den)
return fps if fps > 0 else default
except Exception:
return default Try / catch
try:
forge(genre, takes, out=out)
except ValueError as e:
if 'source fps' in str(e):
forge(genre, takes, out=out, fps=30.0) # explicit fallback Prevention
- Inspect takes with ffprobe r_frame_rate before forging; remux files reporting 0/0.
- Keep a known-good first take whose metadata is healthy.
- Prefer passing --fps explicitly for screen recordings or raw streams.
When it happens
Trigger: Calling forge without fps on a take whose container has a missing/zero frame rate (some screen recordings, raw streams, MKVs with unusual headers, or a corrupt file ffprobe misreads).
Common situations: Screen-capture or OBS recordings with variable/absent frame rate metadata; images or raw streams concatenated into containers without timing info; ffprobe version differences returning 0/0 for broken files.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- asset fps differs from timeline
- asset must contain a video stream
- fps must be positive, got
- output width and height must be positive
- variable or ambiguous frame rate
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/6fcaa8dc120d734d.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/forge.py:89
raise ValueError("all takes must be readable local files")
if base_video and not Path(base_video).is_file():
raise ValueError("base video must be a readable local file")
if any(not Path(o).is_file() for o in (overlays or [])):
raise ValueError("all overlays must be readable local files")
if fps is not None and (not math.isfinite(fps) or fps <= 0):
raise ValueError("fps must be finite and positive")
sp = pack_mod.load(genre, root=root)
tgt = grade_mod.load_stats(sp.grade_path)
cad = cad_mod.load(sp.cadence_path)
# Geometry comes from the first take unless overridden; everything else is
# normalized to it so concat does not silently fail on a size mismatch.
info0 = frame_mod.probe(takes[0])
W = width or info0.width
H = height or info0.height
FPS = fps if fps is not None else info0.fps
if not math.isfinite(FPS) or FPS <= 0:
raise ValueError("source fps must be finite and positive; provide --fps")
if W <= 0 or H <= 0:
raise ValueError("output width and height must be positive")
# Prior timelines reference these shot files. Each run owns a fresh child,
# including failed runs, so retries cannot erase an existing edit.
work_root = Path(work)
work_root.mkdir(parents=True, exist_ok=True)
work_dir = Path(tempfile.mkdtemp(prefix="run-", dir=work_root)).resolve()
print(f"forging '{genre}' -> {W}x{H} @ {FPS:g}fps")
print(f" cadence: mean {cad.mean_shot:.2f}s, {cad.cuts_per_min:.0f} cuts/min, "
f"variance {cad.rhythm_variance:.2f}")
total_target = duration or sum(frame_mod.probe(t).duration for t in takes)
# When apply.py generated these takes it already decided where the cuts
# fall, and it told the model so ("cut into 6 pieces of ~0.8s"). Re-planning
# here would silently cut somewhere else, against footage shot for the
# original plan - so the caller's plan wins when there is one.
if plan is None:
# Plan PER TAKE against each take's own length, not by splitting theView on GitHub (pinned to 8321021c54)