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

  1. 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".
  2. Re-export the snapshot from the timeline the request actually targets.
  3. 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.
  4. 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

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


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)