{"record":{"id":"8456a3346d280f06","repo":"affaan-m/ECC","slug":"native-fps-differs","errorCode":null,"errorMessage":"native fps differs","messagePattern":"native fps differs","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/integration.py","lineNumber":208,"sourceCode":"    _rate(baseline[\"fps\"])\n    bounds = _range(baseline[\"timeline_range\"])\n    _artifact(baseline[\"project_file\"])\n    snapshot = _artifact(baseline[\"snapshot_file\"], parse_json=True)\n    if not isinstance(snapshot, dict):\n        raise ValueError(\"native snapshot must be an object\")\n    settings = snapshot.get(\"settings\")\n    native_fps = settings.get(\"timelineFrameRate\") if isinstance(settings, dict) else None\n    # Resolve's conventional decimal NTSC labels represent these exact rates.\n    ntsc = {\"23.976\": \"24000/1001\", \"29.97\": \"30000/1001\", \"59.94\": \"60000/1001\"}\n    try:\n        if type(native_fps) not in (str, int, float):\n            raise ValueError(\"native fps missing\")\n        label = str(native_fps)\n        if not re.fullmatch(r\"[0-9]{1,9}(?:\\.[0-9]{1,12}|/[1-9][0-9]{0,8})?\", label):\n            raise ValueError(\"native fps must use a bounded decimal or rational label\")\n        native_rate = Fraction(ntsc.get(label, label))\n        if native_rate != Fraction(*_rate(baseline[\"fps\"])):\n            raise ValueError(\"native fps differs\")\n    except (ValueError, ZeroDivisionError) as exc:\n        raise ValueError(\"native snapshot fps is missing, invalid or contradictory\") from exc\n    for cfg, key in [(\"project_name\", \"project\"), (\"timeline_name\", \"timeline\")]:\n        if _text(baseline[cfg]) != snapshot.get(key):\n            raise ValueError(\"native snapshot identity mismatch\")\n    tracks = snapshot.get(\"timeline_readback\")\n    if not isinstance(tracks, dict) or not tracks:\n        raise ValueError(\"native clip snapshot required\")\n    for track, clips in tracks.items():\n        if not re.fullmatch(r\"(?:video|audio)[1-9][0-9]*\", track) or not isinstance(clips, list):\n            raise ValueError(\"invalid native snapshot track\")\n        for clip in clips:\n            _check_clip(clip, bounds)\n    return tracks\n\n\ndef _check_clip(clip: Any, bounds: list[int]) -> None:\n    required = {\"path\", \"name\", \"start\", \"end\", \"left_offset\", \"right_offset\", \"enabled\", \"properties\"}","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/integration.py#L190-L226","documentation":"After parsing the snapshot's fps label (with Resolve's decimal NTSC aliases mapped to exact rationals), _snapshot compares it to baseline.fps. If the native rate differs from the request's baseline rate, the timeline would be retimed, so the library throws \"native fps differs\".","triggerScenarios":"baseline.fps = 24000/1001 but the snapshot says \"24\"; or snapshot says \"29.97\" while baseline says 30/1; any exact-rate mismatch between baseline fps and settings.timelineFrameRate.","commonSituations":"Mixing a 23.976 timeline with a 24 fps baseline (classic NTSC vs true-24 confusion); the request was authored for a different timeline than the snapshot; a template copied from another project with a different frame rate.","solutions":["Align baseline.fps with the snapshot's native rate — e.g. set baseline fps to {\"numerator\": 24000, \"denominator\": 1001} when the snapshot says \"23.976\".","Re-export the snapshot from the timeline the request actually targets.","Check for the NTSC trap: use the decimal aliases (23.976/29.97/59.94) or exact rationals consistently, never mix 24 with 23.976.","Regenerate the request config against the current project's real frame rate."],"exampleFix":"// before\nbaseline.fps = {\"numerator\": 24, \"denominator\": 1}, snapshot says \"23.976\"\n\n// after\nbaseline.fps = {\"numerator\": 24000, \"denominator\": 1001}","handlingStrategy":"validation","validationCode":"from fractions import Fraction\ndef fps_matches(baseline_fps, snapshot_label):\n    ntsc = {\"23.976\": \"24000/1001\", \"29.97\": \"30000/1001\", \"59.94\": \"60000/1001\"}\n    return Fraction(ntsc.get(str(snapshot_label), str(snapshot_label))) == Fraction(baseline_fps[\"numerator\"], baseline_fps[\"denominator\"])","typeGuard":null,"tryCatchPattern":"try:\n    bundle = build_application_bundle(config, None, local_only=True)\nexcept ValueError as exc:\n    if \"fps differs\" in str(exc) or \"fps is missing, invalid or contradictory\" in str(exc):\n        print(\"baseline.fps does not equal the snapshot's native frame rate\")\n    raise","preventionTips":["Derive baseline.fps from the snapshot, not the other way around.","Remember 23.976/29.97/59.94 are aliases for /1001 rationals.","Never mix 24 fps with 23.976 timelines.","Validate fps parity in CI with Fraction comparison."],"tags":["fps","mismatch","validation"],"backgroundTag":"invalid-argument-value","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"}