from __future__ import annotations import time from dataclasses import dataclass from pathlib import Path from typing import Any import httpx class ModalUploadError(RuntimeError): """Raised when hosted post-cut inference fails.""" @dataclass class ModalUploadClient: base_url: str token: str http_client: httpx.Client | None = None poll_interval_s: float = 1.0 timeout_s: float = 900.0 def submit_video( self, video_path: str | Path, *, uploader_hf_username: str, style_key: str = "deadpan", scene_hint: str = "", ) -> dict[str, Any]: close = self.http_client is None client = self.http_client or httpx.Client(timeout=30.0, follow_redirects=True) try: job_id = self._submit( client, Path(video_path), uploader_hf_username, style_key, scene_hint, ) return self._poll(client, job_id) finally: if close: client.close() def _submit( self, client: httpx.Client, video_path: Path, uploader_hf_username: str, style_key: str, scene_hint: str, ) -> str: with video_path.open("rb") as handle: response = client.post( f"{self.base_url.rstrip('/')}/v1/cuts", headers={"Authorization": f"Bearer {self.token}"}, data={ "style_key": style_key, "scene_hint": scene_hint, "uploader_hf_username": uploader_hf_username, }, files={"video": (video_path.name, handle, "video/mp4")}, ) response.raise_for_status() job_id = response.json().get("job_id") if not isinstance(job_id, str) or not job_id: raise ModalUploadError("Modal did not return a job_id") return job_id def _poll(self, client: httpx.Client, job_id: str) -> dict[str, Any]: deadline = time.monotonic() + self.timeout_s while time.monotonic() < deadline: response = client.get( f"{self.base_url.rstrip('/')}/v1/cuts/{job_id}", headers={"Authorization": f"Bearer {self.token}"}, ) if response.status_code == 202: time.sleep(self.poll_interval_s) continue response.raise_for_status() payload = response.json() scene = payload.get("scene") if not isinstance(scene, dict): raise ModalUploadError("Modal completed without a scene payload") return scene raise ModalUploadError("Modal upload timed out")