affaan-m/ECC · error · ValueError

asset fps differs from timeline

Error message

asset fps differs from timeline: {asset}

What it means

For each placement, allocate_placements compares the probed asset fps (via metadata[asset]) with the timeline fps and raises ValueError('asset fps differs from timeline: {asset}') when they are not exactly equal after normalization. Compositing assumes every overlay shares the timeline's frame rate, or frame indexing would drift.

Solutions

  1. Re-encode the asset to the timeline fps: `ffmpeg -i asset.mp4 -vf fps=30 -c:v libx264 -crf 18 asset_30fps.mp4`
  2. Or set the timeline fps to match the asset's probed fps when all assets share one rate
  3. Check the probed fps (ffprobe avg_frame_rate) for each asset and pick a timeline fps all assets can be conformed to
  4. If using a metadata cache, invalidate and re-probe assets that were re-encoded

Example fix

# before
plan = resolve.plan(placements, fps=30)  # asset is 25 fps
# after
subprocess.run(['ffmpeg', '-i', 'overlay.mp4', '-vf', 'fps=30', 'overlay_30.mp4'])
plan = resolve.plan(placements, fps=30)  # asset now 30 fps
Defensive patterns

Strategy: validation

Validate before calling

import json, subprocess
from fractions import Fraction

def asset_fps(path):
    r = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0',
                        '-show_entries', 'stream=avg_frame_rate', '-of', 'json',
                        str(path)], capture_output=True, text=True)
    s = json.loads(r.stdout)['streams'][0]['avg_frame_rate']
    return float(Fraction(s))

timeline = 30.0
for ev in placements:
    assert asset_fps(ev['asset']) == timeline, f"{ev['asset']} not {timeline} fps"

Try / catch

try:
    resolve.plan(placements, fps=fps)
except ValueError as e:
    if 'differs from timeline' in str(e):
        asset = e.args[0].rsplit(': ', 1)[1]
        raise AssetError(f'{asset} must be re-encoded to {fps} fps') from e
    raise

Prevention

When it happens

Trigger: Timeline at 30 fps but overlay asset encoded at 24/25/29.97/60 fps; rational representations that normalize differently (e.g. '30000/1001' vs '30'); metadata supplied via probe() containing a stale or wrong fps field.

Common situations: Mixing stock footage recorded at 25 fps (PAL) into a 30 fps project; phone footage at 29.97; templates assuming a single fps while assets were exported at another; reused metadata cache from an older version of the asset.

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/0e5e3ce9eaddc8e1. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/resolve.py:127

            or not 0 <= opacity <= 100
        ):
            raise ValueError("explicit opacity must be finite within 0..100")
        composite = _integer(event.get("composite"), "composite")
        raw_asset = event.get("asset")
        if not isinstance(raw_asset, (str, Path)) or not str(raw_asset):
            raise ValueError("asset must be a local regular file")
        asset = Path(raw_asset).expanduser().resolve()
        if not asset.is_file():
            raise ValueError(f"asset must be a local regular file: {asset}")
        alpha = event.get("requires_alpha", False)
        if not isinstance(alpha, bool):
            raise ValueError("requires_alpha must be boolean")
        key = str(asset)
        if key not in metadata:
            metadata[key] = probe(asset)
        info = metadata[key]
        if _fps(info.get("fps")) != rate:
            raise ValueError(f"asset fps differs from timeline: {asset}")
        if _integer(info.get("frames"), "asset frames", 1) < frames:
            raise ValueError(f"asset contains too few frames: {asset}")
        if alpha and info.get("has_alpha") is not True:
            raise ValueError(f"asset requires verified alpha: {asset}")
        checked.append(
            {
                "id": identifier,
                "asset": key,
                "record_frame": start,
                "frames": frames,
                "opacity": opacity,
                "composite": composite,
            }
        )
    if not checked:
        raise ValueError("at least one placement is required")
    ends, allocated = [], {}
    for event in sorted(checked, key=lambda entry: entry["record_frame"]):

View on GitHub (pinned to 8321021c54)