calesthio/OpenMontage · error · ValueError

seconds must be one of: 4, 8, 12

Error message

seconds must be one of: 4, 8, 12

What it means

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.

Source

Thrown at tools/video/sora_video.py:256

        if model not in _ALLOWED_MODELS:
            raise ValueError("model must be one of: sora-2, sora-2-pro")
        return model

    @staticmethod
    def _normalize_size(inputs: dict[str, Any], model: str) -> str:
        default_size = "1280x720" if inputs.get("aspect_ratio") == "16:9" else _DEFAULT_SIZE
        size = str(inputs.get("size", default_size)).strip().lower()
        allowed = {"1280x720", "720x1280"} if model == "sora-2" else set(_ALLOWED_SIZES)
        if size not in allowed:
            raise ValueError(f"size must be one of: {', '.join(sorted(allowed))} for model {model}")
        return size

    @staticmethod
    def _normalize_seconds(inputs: dict[str, Any]) -> str:
        seconds = str(inputs.get("seconds") or inputs.get("duration") or _DEFAULT_SECONDS).strip().lower()
        seconds = seconds[:-1] if seconds.endswith("s") else seconds
        if seconds not in _ALLOWED_SECONDS:
            raise ValueError("seconds must be one of: 4, 8, 12")
        return seconds

    @staticmethod
    def _get_status_value(video: Any) -> str:
        if isinstance(video, dict):
            return str(video.get("status") or video.get("state") or "unknown")
        return str(getattr(video, "status", None) or getattr(video, "state", None) or "unknown")

    @staticmethod
    def _get_video_id(video: Any) -> str | None:
        if isinstance(video, dict):
            value = video.get("id")
            return value if isinstance(value, str) else None
        value = getattr(video, "id", None)
        return value if isinstance(value, str) else None

    @staticmethod
    def _file_to_data_uri(path: Path) -> str:

View on GitHub (pinned to 95e1c3d0ab)

Solutions

  1. Snap the desired duration to the nearest allowed value of 4, 8, or 12 seconds.
  2. For non-standard lengths, generate at 8 or 12 and trim with ffmpeg afterwards.
  3. Pass plain integer strings ('4', '8', '12') — avoid decimals and unit suffixes other than a bare 's'.

Example fix

# before
inputs = {"seconds": "6"}

# after
inputs = {"seconds": "8"}  # trim to 6s in post if exact length matters
Defensive patterns

Strategy: validation

Validate before calling

raw = str(inputs.get("seconds") or inputs.get("duration") or "8").strip().lower().rstrip("s")
if raw not in {"4", "8", "12"}:
    target = float(raw)
    inputs["seconds"] = min(("4", "8", "12"), key=lambda s: abs(int(s) - target))

Type guard

def is_valid_sora_seconds(v) -> bool:
    raw = str(v).strip().lower()
    raw = raw[:-1] if raw.endswith("s") else raw
    return raw in {"4", "8", "12"}

Prevention

When it happens

Trigger: 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.

Common situations: Storyboards computed in arbitrary seconds (e.g. one 6-second scene per beat); unit mismatches passing milliseconds; LLM-generated inputs carrying decimal durations.

Related errors


AI-assisted analysis of calesthio/OpenMontage@95e1c3d0ab (2026-08-15). Data as JSON: /api/errors/dffcec5aef33c378. Report an issue: GitHub.