{"record":{"id":"0e5e3ce9eaddc8e1","repo":"affaan-m/ECC","slug":"asset-fps-differs-from-timeline-asset","errorCode":null,"errorMessage":"asset fps differs from timeline: {asset}","messagePattern":"asset fps differs from timeline: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/resolve.py","lineNumber":127,"sourceCode":"            or not 0 <= opacity <= 100\n        ):\n            raise ValueError(\"explicit opacity must be finite within 0..100\")\n        composite = _integer(event.get(\"composite\"), \"composite\")\n        raw_asset = event.get(\"asset\")\n        if not isinstance(raw_asset, (str, Path)) or not str(raw_asset):\n            raise ValueError(\"asset must be a local regular file\")\n        asset = Path(raw_asset).expanduser().resolve()\n        if not asset.is_file():\n            raise ValueError(f\"asset must be a local regular file: {asset}\")\n        alpha = event.get(\"requires_alpha\", False)\n        if not isinstance(alpha, bool):\n            raise ValueError(\"requires_alpha must be boolean\")\n        key = str(asset)\n        if key not in metadata:\n            metadata[key] = probe(asset)\n        info = metadata[key]\n        if _fps(info.get(\"fps\")) != rate:\n            raise ValueError(f\"asset fps differs from timeline: {asset}\")\n        if _integer(info.get(\"frames\"), \"asset frames\", 1) < frames:\n            raise ValueError(f\"asset contains too few frames: {asset}\")\n        if alpha and info.get(\"has_alpha\") is not True:\n            raise ValueError(f\"asset requires verified alpha: {asset}\")\n        checked.append(\n            {\n                \"id\": identifier,\n                \"asset\": key,\n                \"record_frame\": start,\n                \"frames\": frames,\n                \"opacity\": opacity,\n                \"composite\": composite,\n            }\n        )\n    if not checked:\n        raise ValueError(\"at least one placement is required\")\n    ends, allocated = [], {}\n    for event in sorted(checked, key=lambda entry: entry[\"record_frame\"]):","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/resolve.py#L109-L145","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-encode the asset to the timeline fps: `ffmpeg -i asset.mp4 -vf fps=30 -c:v libx264 -crf 18 asset_30fps.mp4`","Or set the timeline fps to match the asset's probed fps when all assets share one rate","Check the probed fps (ffprobe avg_frame_rate) for each asset and pick a timeline fps all assets can be conformed to","If using a metadata cache, invalidate and re-probe assets that were re-encoded"],"exampleFix":"# before\nplan = resolve.plan(placements, fps=30)  # asset is 25 fps\n# after\nsubprocess.run(['ffmpeg', '-i', 'overlay.mp4', '-vf', 'fps=30', 'overlay_30.mp4'])\nplan = resolve.plan(placements, fps=30)  # asset now 30 fps","handlingStrategy":"validation","validationCode":"import json, subprocess\nfrom fractions import Fraction\n\ndef asset_fps(path):\n    r = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0',\n                        '-show_entries', 'stream=avg_frame_rate', '-of', 'json',\n                        str(path)], capture_output=True, text=True)\n    s = json.loads(r.stdout)['streams'][0]['avg_frame_rate']\n    return float(Fraction(s))\n\ntimeline = 30.0\nfor ev in placements:\n    assert asset_fps(ev['asset']) == timeline, f\"{ev['asset']} not {timeline} fps\"","typeGuard":null,"tryCatchPattern":"try:\n    resolve.plan(placements, fps=fps)\nexcept ValueError as e:\n    if 'differs from timeline' in str(e):\n        asset = e.args[0].rsplit(': ', 1)[1]\n        raise AssetError(f'{asset} must be re-encoded to {fps} fps') from e\n    raise","preventionTips":["Standardize a project fps and re-encode all overlays to it at ingest","Probe fps for every asset during ingest, not at render time","Invalidate metadata caches after re-encoding assets"],"tags":["fps","video","frame-rate-mismatch"],"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"}