{"record":{"id":"505271a43896c03c","repo":"affaan-m/ECC","slug":"clip-i-has-no-path-timeline","errorCode":null,"errorMessage":"clip {i} has no 'path'","messagePattern":"clip (.+?) has no 'path'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-distillation/scripts/taste/timeline.py","lineNumber":218,"sourceCode":"\n# ---------------------------------------------------------------------------\n# clip normalisation\n# ---------------------------------------------------------------------------\n\n\ndef _normalise(clips: Iterable[dict], fps: float | Fraction) -> list[dict]:\n    \"\"\"Validate clips and pre-compute integer frame counts and offsets.\n\n    Returns dicts with ``path``, ``name``, ``frames`` (int, >= 1) and\n    ``offset_frames`` (int). Working in frames from here down is what makes the\n    sequence duration exactly the sum of the clip durations.\n    \"\"\"\n    out: list[dict] = []\n    offset = 0\n    for i, c in enumerate(clips):\n        path = str(c.get(\"path\") or \"\")\n        if not path:\n            raise ValueError(f\"clip {i} has no 'path'\")\n        dur = float(c.get(\"duration\") or 0.0)\n        if dur <= 0:\n            raise ValueError(f\"clip {i} ({path}) has non-positive duration {dur!r}\")\n        frames = max(1, seconds_to_frames(dur, fps))  # never emit a zero-length event\n        name = str(c.get(\"name\") or Path(path).stem)\n        out.append(\n            {\n                \"path\": path,\n                \"name\": name,\n                \"frames\": frames,\n                \"offset_frames\": offset,\n                \"seconds\": dur,\n            }\n        )\n        offset += frames\n    if not out:\n        raise ValueError(\"no clips to write - a timeline needs at least one event\")\n    return out","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-distillation/scripts/taste/timeline.py#L200-L236","documentation":"_normalise() requires every clip dict to have a non-empty 'path' key; the path is the media file each timeline event points to. A clip without one cannot be represented in FCPXML or EDL, so a ValueError naming the clip index is raised.","triggerScenarios":"Passing build_fcpxml/build_edl a clips list containing a dict like {\"duration\": 2.0} with no 'path', or a clip where path is None or empty string.","commonSituations":"Downstream tooling emitting partial clip records; JSON clip manifests with missing fields; constructing clips programmatically and forgetting the key.","solutions":["Add a valid media 'path' to each clip dict before building the timeline","Filter out incomplete clips: clips = [c for c in clips if c.get('path')]","Validate the clip manifest schema before calling the builder","Check the producer of the clip list for keys like 'file' vs 'path' naming mismatches"],"exampleFix":"// before\nclips = [{\"duration\": 2.0}]\n// after\nclips = [{\"path\": \"/media/clip1.mp4\", \"duration\": 2.0}]","handlingStrategy":"validation","validationCode":"invalid = [i for i, c in enumerate(clips) if not str(c.get('path') or '').strip()]\nif invalid:\n    raise ValueError(f'clips missing path: {invalid}')","typeGuard":"def is_valid_clip(c: object) -> bool:\n    return isinstance(c, dict) and bool(str(c.get('path') or '').strip())","tryCatchPattern":"try:\n    write_timeline(clips, out, fmt='fcpxml')\nexcept ValueError as e:\n    if \"has no 'path'\" in str(e):\n        logger.error('clip manifest incomplete: %s', e)\n        raise","preventionTips":["Validate the clip manifest schema (all required keys) at ingest time","Standardize clip producers to emit a 'path' key","Use a dataclass/TypedDict for clips instead of loose dicts"],"tags":["value-error","validation","missing-field","timeline"],"backgroundTag":"empty-required-field","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"}