{"record":{"id":"d0f50ab2d816020f","repo":"affaan-m/ECC","slug":"fps-must-be-positive-got-fps-r","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-application/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-application/scripts/taste/timeline.py#L72-L108","documentation":"timeline.py raises ValueError('fps must be positive, got {fps!r}') in fps_fraction when the supplied frame rate is zero or negative after conversion to float. The function normalizes any fps into an exact Fraction (with NTSC 29.97/23.976-style snap), so a non-positive value is meaningless.","triggerScenarios":"Calling fps_fraction (directly or via frame_duration, seconds_to_frames, frames_to_timecode) with 0, a negative number, NaN, or an fps field parsed as 0 from missing config.","commonSituations":"fps loaded from a config file defaulting to 0; division result used as fps becoming 0; typos like fps=-30; JSON config where 'fps': 0 was copied as a placeholder.","solutions":["Pass a positive fps value (e.g. 24, 30, 29.97).","Validate the fps field before building the timeline: `if not fps or fps <= 0: raise/skip`.","Fix the source config/data where fps was 0 or negative.","Coerce numeric strings ('30') to float before calling; non-numeric will fail earlier in float() conversion."],"exampleFix":"// before\nframes_to_timecode(90, fps=0)\n// after\nassert fps and fps > 0, f'fps must be positive, got {fps!r}'\nframes_to_timecode(90, fps=24)","handlingStrategy":"validation","validationCode":"from fractions import Fraction\ndef validate_fps(fps):\n    if fps is None:\n        raise ValueError('fps is required')\n    if isinstance(fps, Fraction):\n        return fps\n    f = float(fps)\n    if f <= 0 or f != f:  # also reject NaN\n        raise ValueError(f'fps must be positive, got {fps!r}')\n    return f","typeGuard":"def is_valid_fps(fps) -> bool:\n    try:\n        f = float(fps)\n    except (TypeError, ValueError):\n        return False\n    return f > 0 and f == f","tryCatchPattern":"try:\n    duration = frame_duration(frames, fps)\nexcept ValueError as e:\n    if 'fps must be positive' in str(e):\n        logger.error('bad fps from config: %r', fps)\n        fps = 30.0  # documented default","preventionTips":["Never default fps to 0 in config files; use None to mean 'unset'","Validate fps at config load time","Coerce numeric strings to float before passing","Be aware NaN passes float() but fails positivity — check explicitly"],"tags":["validation","timeline","fps"],"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"}