affaan-m/ECC · error · ValueError
fps must be positive, got
Error message
fps must be positive, got {fps!r} What it means
fps_fraction() converts any fps input to an exact Fraction. After Fraction passthrough, it casts to float and validates positivity; a zero or negative frame rate cannot define a timeline, so a ValueError is raised.
Solutions
- Inspect the fps value passed to build_fcpxml/build_edl/write_timeline and ensure it is > 0
- Probe actual media fps with ffprobe and pass that value
- Guard before calling: assert fps and fps > 0, or fall back to a known default like 24
- Check upstream pipeline code that computes fps for division-by-zero or None-to-0 coercion
Example fix
// before write_timeline(clips, out, fps=0) // after fps = fps or 24.0 write_timeline(clips, out, fps=fps)
Defensive patterns
Strategy: validation
Validate before calling
if fps is None or float(fps) <= 0:
fps = 24.0 # or probe media with ffprobe Type guard
def has_valid_fps(v) -> bool:
try:
return v is not None and float(v) > 0
except (TypeError, ValueError):
return False Prevention
- Never pass raw probed metadata fps straight into builders; sanitize 0/None first
- Default to a known-good fps (24) when media metadata is unreliable
- Unit-test timeline builders with fps=0 and negative values
When it happens
Trigger: Calling fps_fraction(0), fps_fraction(-24), or passing a clip build/build_fcpxml/build_edl/write_timeline with fps <= 0 (e.g. fps=0 default misuse or a computed fps of 0 from a failed metadata probe).
Common situations: Video metadata extraction returning fps=0 for streams without frame-rate data; sign errors when computing fps from a duration; passing a nullable fps that defaults to 0.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- analyze() needs at least one frame
- asset fps differs from timeline
- candidate fps/duration/retime ambiguity
- clip has no 'path
- clip ( ) has non-positive duration
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3415f14fe33dbc4d.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-distillation/scripts/taste/timeline.py:90
# CMX3600 signals drop-frame with the `FCM:` header line rather than with the
# timecode separator; some houses also swap ':' for ';'. We emit the spec form
# (FCM header, ':' separators) because that is what Resolve's EDL parser keys on.
EDL_DROP_SEPARATOR = ":"
def fps_fraction(fps: float | Fraction) -> Fraction:
"""Exact frame rate as a :class:`Fraction`, snapping NTSC decimals.
>>> fps_fraction(29.97)
Fraction(30000, 1001)
>>> fps_fraction(24)
Fraction(24, 1)
"""
if isinstance(fps, Fraction):
return fps
fps = float(fps)
if fps <= 0:
raise ValueError(f"fps must be positive, got {fps!r}")
for nominal, exact in _NTSC.items():
if abs(fps - nominal) < _NTSC_TOL:
return exact
if abs(fps - round(fps)) < 1e-9:
return Fraction(int(round(fps)), 1)
return Fraction(fps).limit_denominator(100000)
def frame_duration(fps: float | Fraction) -> Fraction:
"""Duration of one frame, in seconds, as an exact fraction."""
return 1 / fps_fraction(fps)
def seconds_to_frames(seconds: float, fps: float | Fraction) -> int:
"""Quantise ``seconds`` to the nearest whole frame at ``fps``.
Rounds half away from zero rather than using banker's rounding, so a clip
asked for at exactly half a frame does not silently vanish.View on GitHub (pinned to 8321021c54)