{"record":{"id":"dffcec5aef33c378","repo":"calesthio/OpenMontage","slug":"seconds-must-be-one-of-4-8-12","errorCode":null,"errorMessage":"seconds must be one of: 4, 8, 12","messagePattern":"seconds must be one of: 4, 8, 12","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/video/sora_video.py","lineNumber":256,"sourceCode":"        if model not in _ALLOWED_MODELS:\n            raise ValueError(\"model must be one of: sora-2, sora-2-pro\")\n        return model\n\n    @staticmethod\n    def _normalize_size(inputs: dict[str, Any], model: str) -> str:\n        default_size = \"1280x720\" if inputs.get(\"aspect_ratio\") == \"16:9\" else _DEFAULT_SIZE\n        size = str(inputs.get(\"size\", default_size)).strip().lower()\n        allowed = {\"1280x720\", \"720x1280\"} if model == \"sora-2\" else set(_ALLOWED_SIZES)\n        if size not in allowed:\n            raise ValueError(f\"size must be one of: {', '.join(sorted(allowed))} for model {model}\")\n        return size\n\n    @staticmethod\n    def _normalize_seconds(inputs: dict[str, Any]) -> str:\n        seconds = str(inputs.get(\"seconds\") or inputs.get(\"duration\") or _DEFAULT_SECONDS).strip().lower()\n        seconds = seconds[:-1] if seconds.endswith(\"s\") else seconds\n        if seconds not in _ALLOWED_SECONDS:\n            raise ValueError(\"seconds must be one of: 4, 8, 12\")\n        return seconds\n\n    @staticmethod\n    def _get_status_value(video: Any) -> str:\n        if isinstance(video, dict):\n            return str(video.get(\"status\") or video.get(\"state\") or \"unknown\")\n        return str(getattr(video, \"status\", None) or getattr(video, \"state\", None) or \"unknown\")\n\n    @staticmethod\n    def _get_video_id(video: Any) -> str | None:\n        if isinstance(video, dict):\n            value = video.get(\"id\")\n            return value if isinstance(value, str) else None\n        value = getattr(video, \"id\", None)\n        return value if isinstance(value, str) else None\n\n    @staticmethod\n    def _file_to_data_uri(path: Path) -> str:","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/calesthio/OpenMontage/blob/95e1c3d0ab93482159818560f6a8c8e866b9139f/tools/video/sora_video.py#L238-L274","documentation":"ValueError raised by SoraVideo._normalize_seconds when the duration value — taken from 'seconds' or fallback 'duration', stripped, lowercased, and trailing 's' removed — is not in _ALLOWED_SECONDS {4, 8, 12}. OpenAI's video API only generates clips at these discrete lengths; arbitrary durations like 6 or 15 are rejected client-side.","triggerScenarios":"Passing seconds='6', seconds='15s', duration=10, or '4.0' (string '4.0' does not match '4' after normalization). A single trailing 's' is tolerated ('8s' -> '8') but '8sec' or '4.5s' are not.","commonSituations":"Storyboards computed in arbitrary seconds (e.g. one 6-second scene per beat); unit mismatches passing milliseconds; LLM-generated inputs carrying decimal durations.","solutions":["Snap the desired duration to the nearest allowed value of 4, 8, or 12 seconds.","For non-standard lengths, generate at 8 or 12 and trim with ffmpeg afterwards.","Pass plain integer strings ('4', '8', '12') — avoid decimals and unit suffixes other than a bare 's'."],"exampleFix":"# before\ninputs = {\"seconds\": \"6\"}\n\n# after\ninputs = {\"seconds\": \"8\"}  # trim to 6s in post if exact length matters","handlingStrategy":"validation","validationCode":"raw = str(inputs.get(\"seconds\") or inputs.get(\"duration\") or \"8\").strip().lower().rstrip(\"s\")\nif raw not in {\"4\", \"8\", \"12\"}:\n    target = float(raw)\n    inputs[\"seconds\"] = min((\"4\", \"8\", \"12\"), key=lambda s: abs(int(s) - target))","typeGuard":"def is_valid_sora_seconds(v) -> bool:\n    raw = str(v).strip().lower()\n    raw = raw[:-1] if raw.endswith(\"s\") else raw\n    return raw in {\"4\", \"8\", \"12\"}","tryCatchPattern":null,"preventionTips":["Snap storyboard beat durations to 4/8/12 before calling Sora.","Never pass decimal durations; trim to exact length in post instead."],"tags":["validation","sora","duration"],"backgroundTag":null,"analyzedSha":"95e1c3d0ab93482159818560f6a8c8e866b9139f","analyzedAt":"2026-08-15T06:31:20.014Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}