{"record":{"id":"3415f14fe33dbc4d","repo":"affaan-m/ECC","slug":"fps-must-be-positive-got-fps-r-3415f1","errorCode":null,"errorMessage":"fps must be positive, got {fps!r}","messagePattern":"fps must be positive, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-distillation/scripts/taste/timeline.py","lineNumber":90,"sourceCode":"# CMX3600 signals drop-frame with the `FCM:` header line rather than with the\n# timecode separator; some houses also swap ':' for ';'. We emit the spec form\n# (FCM header, ':' separators) because that is what Resolve's EDL parser keys on.\nEDL_DROP_SEPARATOR = \":\"\n\n\ndef fps_fraction(fps: float | Fraction) -> Fraction:\n    \"\"\"Exact frame rate as a :class:`Fraction`, snapping NTSC decimals.\n\n    >>> fps_fraction(29.97)\n    Fraction(30000, 1001)\n    >>> fps_fraction(24)\n    Fraction(24, 1)\n    \"\"\"\n    if isinstance(fps, Fraction):\n        return fps\n    fps = float(fps)\n    if fps <= 0:\n        raise ValueError(f\"fps must be positive, got {fps!r}\")\n    for nominal, exact in _NTSC.items():\n        if abs(fps - nominal) < _NTSC_TOL:\n            return exact\n    if abs(fps - round(fps)) < 1e-9:\n        return Fraction(int(round(fps)), 1)\n    return Fraction(fps).limit_denominator(100000)\n\n\ndef frame_duration(fps: float | Fraction) -> Fraction:\n    \"\"\"Duration of one frame, in seconds, as an exact fraction.\"\"\"\n    return 1 / fps_fraction(fps)\n\n\ndef seconds_to_frames(seconds: float, fps: float | Fraction) -> int:\n    \"\"\"Quantise ``seconds`` to the nearest whole frame at ``fps``.\n\n    Rounds half away from zero rather than using banker's rounding, so a clip\n    asked for at exactly half a frame does not silently vanish.","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-distillation/scripts/taste/timeline.py#L72-L108","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nwrite_timeline(clips, out, fps=0)\n// after\nfps = fps or 24.0\nwrite_timeline(clips, out, fps=fps)","handlingStrategy":"validation","validationCode":"if fps is None or float(fps) <= 0:\n    fps = 24.0  # or probe media with ffprobe","typeGuard":"def has_valid_fps(v) -> bool:\n    try:\n        return v is not None and float(v) > 0\n    except (TypeError, ValueError):\n        return False","tryCatchPattern":null,"preventionTips":["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"],"tags":["value-error","video","fps","validation"],"backgroundTag":"value-out-of-range","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}