calesthio/OpenMontage · error · HTTPException

path escapes project

Error message

path escapes project

What it means

The final range check in _normalize_duration: the value parsed to an integer, but that integer is not -1 and not within 4..max_seconds (default 15). Note a separate earlier check also rejects non-canonical forms like "4.5"; this specific message fires only for genuinely out-of-range integers such as 3, 16, or 30.

Source

Thrown at backlot/server.py:251

                    yield _sse({"type": "change", "project_id": changed})
            finally:
                hub.unsubscribe(q)

        return StreamingResponse(stream(), media_type="text/event-stream", headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        })

    # ---- Thumbnails (downscaled, cached on disk) ------------------------

    @app.get("/thumb/{project_id}/{file_path:path}")
    async def thumb(project_id: str, file_path: str, w: int = 640) -> FileResponse:
        project_dir = _safe_project_dir(project_id)
        target = (project_dir / file_path).resolve()
        try:
            target.relative_to(project_dir.resolve())
        except ValueError:
            raise HTTPException(status_code=403, detail="path escapes project")
        if not target.is_file():
            raise HTTPException(status_code=404, detail="media not found")
        width = min(THUMB_WIDTHS, key=lambda x: abs(x - w))
        cached = await asyncio.to_thread(_thumbnail_for, target, width)
        if cached is None:
            # Never fall back to raw video bytes for an <img> consumer (F-03);
            # non-thumbable images are safe to serve as-is.
            if target.suffix.lower() in {".mp4", ".webm", ".mov"}:
                raise HTTPException(status_code=404, detail="no poster frame available")
            return FileResponse(target)
        return FileResponse(cached, media_type="image/jpeg")

    # ---- Media (range requests handled by FileResponse) ---------------

    @app.get("/media/{project_id}/{file_path:path}")
    async def media(project_id: str, file_path: str) -> FileResponse:
        project_dir = _safe_project_dir(project_id)
        target = (project_dir / file_path).resolve()

View on GitHub (pinned to 95e1c3d0ab)

Solutions

  1. Clamp the requested duration into [4, 15] or send -1 to let Ark pick.
  2. Check which model variant you target — max_seconds is a parameter (default 15); if the model supports longer clips, pass the correct max through the tool's configuration rather than guessing.
  3. Split a long clip request into multiple 15s generations and concatenate.

Example fix

# before
inputs = {"duration": 30}
# after
MAX = 15
inputs = {"duration": min(max(4, requested), MAX) if requested > 0 else -1}
Defensive patterns

Strategy: validation

Validate before calling

def clamp_duration(requested, lo=4, hi=15):
    if requested in ("auto", -1, None):
        return -1
    return max(lo, min(hi, int(requested)))

Try / catch

try:
    tool.run(inputs)
except ValueError as e:
    if "duration must be between" in str(e):
        inputs["duration"] = -1
        tool.run(inputs)
    else:
        raise

Prevention

When it happens

Trigger: duration=3, duration=16, duration=0, or duration=-2 passed to seedance_ark. Also passing a longer duration (e.g. 20) intended for a different provider whose max is higher.

Common situations: Porting prompts/configs from another video model that allows 5-10s or up to 60s; assuming -1 means 'unlimited' and then also trying values like 60; off-by-one attempts at the boundary (duration=15 works, 16 does not).

Related errors


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