affaan-m/ECC · error · ValueError
fps must be finite and positive
Error message
fps must be finite and positive
What it means
forge() accepts an optional fps override; when provided it must be a finite number greater than zero. NaN, infinity, zero, or negative values would produce invalid ffmpeg output, so it raises ValueError immediately.
Solutions
- Pass a positive finite fps, e.g. fps=24.0.
- Omit fps entirely to let forge use the first take's probed fps.
- Sanitize parsed values: reject non-finite and non-positive numbers before calling forge.
- Fix the config/CLI source so it provides a numeric frame rate.
Example fix
# before
fps = float(cfg.get('fps', 'nan'))
forge('noir', takes, fps=fps, out='edit.mp4') # ValueError
# after
fps = cfg.get('fps')
fps = float(fps) if fps and math.isfinite(float(fps)) and float(fps) > 0 else None
forge('noir', takes, fps=fps, out='edit.mp4') # None -> probe take fps Defensive patterns
Strategy: validation
Validate before calling
import math
def fps_ok(fps):
return fps is None or (isinstance(fps, (int, float)) and math.isfinite(fps) and fps > 0) Type guard
def is_valid_fps(v):
return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and v > 0 Try / catch
try:
forge(genre, takes, fps=fps, out=out)
except ValueError as e:
if 'fps' in str(e):
forge(genre, takes, out=out) # drop override, probe from take Prevention
- Validate numeric overrides with math.isfinite and > 0 before calling.
- Omit fps to let forge probe the first take's frame rate.
- Guard config parsing against 'auto'/empty values producing NaN.
When it happens
Trigger: Calling forge(..., fps=0) or fps=-30; passing float('nan') or float('inf') (e.g. from a failed parse of a config/CLI value); passing a string that was never converted to float.
Common situations: Parsing fps from a config file where the value is 'auto' or empty and a float() call yields NaN via data paths; dividing by zero when computing fps from frame counts; hand-editing a YAML/JSON pipeline with fps: 0.
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
- fps must be finite and positive
- all overlays must be readable local files
- all takes must be readable local files
- Arguments must not contain NUL bytes.
- asset name must be a simple filename stem (letters, digits…
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/bfc3b46473fd46ac.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/forge.py:77
duration: float | None = None,
strength: float = 1.0,
grade_base: bool = True,
width: int | None = None,
height: int | None = None,
work: str = "out/forge_work",
plan: list[dict] | None = None,
fps: float | None = None,
) -> Path:
out_path = Path(out)
validate_output(out_path)
if not takes or any(not Path(t).is_file() for t in takes):
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)View on GitHub (pinned to 8321021c54)