affaan-m/ECC · error · ValueError
native fps differs
Error message
native fps differs
What it means
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".
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.
Example fix
// before
baseline.fps = {"numerator": 24, "denominator": 1}, snapshot says "23.976"
// after
baseline.fps = {"numerator": 24000, "denominator": 1001} Defensive patterns
Strategy: validation
Validate before calling
from fractions import Fraction
def fps_matches(baseline_fps, snapshot_label):
ntsc = {"23.976": "24000/1001", "29.97": "30000/1001", "59.94": "60000/1001"}
return Fraction(ntsc.get(str(snapshot_label), str(snapshot_label))) == Fraction(baseline_fps["numerator"], baseline_fps["denominator"]) Try / catch
try:
bundle = build_application_bundle(config, None, local_only=True)
except ValueError as exc:
if "fps differs" in str(exc) or "fps is missing, invalid or contradictory" in str(exc):
print("baseline.fps does not equal the snapshot's native frame rate")
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- candidate fps/duration/retime ambiguity
- fps must be a reduced positive rational
- fps must be finite and positive
- fps must be finite and positive
- fps must be positive, got
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/8456a3346d280f06.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/integration.py:208
_rate(baseline["fps"])
bounds = _range(baseline["timeline_range"])
_artifact(baseline["project_file"])
snapshot = _artifact(baseline["snapshot_file"], parse_json=True)
if not isinstance(snapshot, dict):
raise ValueError("native snapshot must be an object")
settings = snapshot.get("settings")
native_fps = settings.get("timelineFrameRate") if isinstance(settings, dict) else None
# Resolve's conventional decimal NTSC labels represent these exact rates.
ntsc = {"23.976": "24000/1001", "29.97": "30000/1001", "59.94": "60000/1001"}
try:
if type(native_fps) not in (str, int, float):
raise ValueError("native fps missing")
label = str(native_fps)
if not re.fullmatch(r"[0-9]{1,9}(?:\.[0-9]{1,12}|/[1-9][0-9]{0,8})?", label):
raise ValueError("native fps must use a bounded decimal or rational label")
native_rate = Fraction(ntsc.get(label, label))
if native_rate != Fraction(*_rate(baseline["fps"])):
raise ValueError("native fps differs")
except (ValueError, ZeroDivisionError) as exc:
raise ValueError("native snapshot fps is missing, invalid or contradictory") from exc
for cfg, key in [("project_name", "project"), ("timeline_name", "timeline")]:
if _text(baseline[cfg]) != snapshot.get(key):
raise ValueError("native snapshot identity mismatch")
tracks = snapshot.get("timeline_readback")
if not isinstance(tracks, dict) or not tracks:
raise ValueError("native clip snapshot required")
for track, clips in tracks.items():
if not re.fullmatch(r"(?:video|audio)[1-9][0-9]*", track) or not isinstance(clips, list):
raise ValueError("invalid native snapshot track")
for clip in clips:
_check_clip(clip, bounds)
return tracks
def _check_clip(clip: Any, bounds: list[int]) -> None:
required = {"path", "name", "start", "end", "left_offset", "right_offset", "enabled", "properties"}View on GitHub (pinned to 8321021c54)