{"record":{"id":"2da7966335d15b5c","repo":"affaan-m/ECC","slug":"merge-videos-needs-at-least-one-video-url-falapi","errorCode":null,"errorMessage":"merge_videos() needs at least one video URL","messagePattern":"merge_videos\\(\\) needs at least one video URL","errorType":"validation","errorClass":"FalError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/taste/falapi.py","lineNumber":647,"sourceCode":"    result = submit(ENDPOINTS[\"extract_frame\"], payload, timeout)\n    return first_url(result, ENDPOINTS[\"extract_frame\"])\n\n\ndef compose(tracks: list[dict], *, timeout: int = 900) -> str:\n    \"\"\"Composite timeline tracks into one video. Returns the output URL.\n\n    ``tracks`` is passed straight through so the caller owns the timeline\n    shape; the ffmpeg-api track schema is another default worth verifying\n    before a live run.\n    \"\"\"\n    result = submit(ENDPOINTS[\"compose\"], {\"tracks\": tracks}, timeout)\n    return first_url(result, ENDPOINTS[\"compose\"])\n\n\ndef merge_videos(video_urls: list[str], *, timeout: int = 900) -> str:\n    \"\"\"Concatenate videos end to end. Returns the merged URL.\"\"\"\n    if not video_urls:\n        raise FalError(\"merge_videos() needs at least one video URL\")\n    payload = {\"video_urls\": list(video_urls)}\n    result = submit(ENDPOINTS[\"merge_videos\"], payload, timeout)\n    return first_url(result, ENDPOINTS[\"merge_videos\"])\n\n\n# ---------------------------------------------------------------------------\n# download\n# ---------------------------------------------------------------------------\n\n\nMAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024  # bounded large video/GLB downloads\n\n\ndef _validate_download_url(url: str) -> None:\n    try:\n        parsed = urllib.parse.urlsplit(url)\n        host = parsed.hostname or \"\"\n        valid = (parsed.scheme == \"https\" and not parsed.username","sourceCodeStart":629,"sourceCodeEnd":665,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/taste/falapi.py#L629-L665","documentation":"merge_videos concatenates a list of hosted video URLs via the fal.ai compose/merge endpoint. Because concatenating nothing is meaningless, the library raises FalError up front when the list is empty, before any network request is made.","triggerScenarios":"merge_videos([]) or merge_videos(videos) where videos is an empty list — typically because earlier generation steps produced nothing or were filtered out.","commonSituations":"Pipeline stage collected video URLs into a list that ended up empty after filtering failed generations; forgetting to pass clip URLs generated in a previous step; variable shadowing or a failed upload leaving the list empty.","solutions":["Pass a non-empty list of video URLs to merge_videos","Check that the list is populated before calling (assert video_urls)","Trace why the upstream generation steps returned no URLs if the list should have content"],"exampleFix":"# before\nmerge_videos(clips)\n# after\nassert clips, 'no clips to merge'\nmerged = merge_videos(clips)","handlingStrategy":"validation","validationCode":"if not video_urls:\n    raise ValueError('merge_videos requires at least one URL')","typeGuard":"def has_videos(video_urls) -> bool:\n    return bool(video_urls)","tryCatchPattern":null,"preventionTips":["Check the clips list is non-empty before merging","Propagate errors from earlier clip-generation steps rather than filtering silently","Assert len(video_urls) > 0 at the top of merge pipelines","Keep generated URLs in a list you can inspect before the merge step"],"tags":["validation","argument","empty-input","video"],"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"}