Download demo/server.py from BBBBCHAN/DeltaStreamer: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/BBBBCHAN/DeltaStreamer/resolve/main/demo/server.py
- Command line
-
hf download hf://BBBBCHAN/DeltaStreamer/demo/server.py
-
curl -L -o server.py https://huggingface.co/BBBBCHAN/DeltaStreamer/resolve/main/demo/server.py
17 kB
| """Local upload-and-ask server. Run: python -m demo.server --port 7861.""" | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import base64 | |
| from contextlib import asynccontextmanager | |
| from datetime import datetime, timezone | |
| import io | |
| import json | |
| import math | |
| import os | |
| from pathlib import Path | |
| import re | |
| import shutil | |
| import signal | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| from urllib.parse import urlsplit | |
| import uuid | |
| from fastapi import FastAPI, File, HTTPException, Request, UploadFile | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| import httpx | |
| from PIL import Image | |
| from pydantic import BaseModel, Field | |
| from .config import REPO, ROOT, Settings | |
| from .media import EXTENSIONS, prepare_video | |
| TERMINAL = {"completed", "failed", "cancelled"} | |
| def now(): | |
| return datetime.now(timezone.utc).isoformat() | |
| def atomic_json(path, value): | |
| temporary = path.with_name(path.name + "." + uuid.uuid4().hex + ".tmp") | |
| temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") | |
| temporary.replace(path) | |
| def checked_id(value): | |
| if not re.fullmatch(r"[a-f0-9]{32}", value): | |
| raise HTTPException(404, "Item not found.") | |
| return value | |
| class RunRequest(BaseModel): | |
| videoId: str | |
| question: str = Field(min_length=1, max_length=2000) | |
| queryTime: float | None = None | |
| class RunManager: | |
| def __init__(self, settings, runner_command=None): | |
| self.settings = settings | |
| self.root = settings.data_dir / "runs" | |
| self.root.mkdir(parents=True, exist_ok=True) | |
| self.lock = threading.RLock() | |
| self.processes = {} | |
| self.runner_command = runner_command or [sys.executable, "-m", "demo.runner"] | |
| def directory(self, run_id): | |
| directory = self.root / checked_id(run_id) | |
| if not (directory / "status.json").is_file(): | |
| raise HTTPException(404, "Inference task not found.") | |
| return directory | |
| def _external_pid(self, directory): | |
| """Only recognize a surviving worker whose command names this exact job.""" | |
| try: | |
| pid = int(json.loads((directory / "process.json").read_text())["pid"]) | |
| command = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\x00") | |
| if b"demo.runner" in command and str(directory).encode() in command: | |
| return pid | |
| except (OSError, ValueError, KeyError): | |
| pass | |
| return None | |
| def read(self, run_id): | |
| directory = self.directory(run_id) | |
| with self.lock: | |
| status = json.loads((directory / "status.json").read_text()) | |
| process = self.processes.get(run_id) | |
| alive = process.poll() is None if process else bool(self._external_pid(directory)) | |
| if status.get("status") not in TERMINAL and not alive: | |
| cancelled = (directory / "cancel.requested").exists() | |
| status.update(status="cancelled" if cancelled else "failed", | |
| phase="cancelled" if cancelled else "error", | |
| error=None if cancelled else "The inference worker stopped. See this task's worker.log on the server.", | |
| finishedAt=now()) | |
| atomic_json(directory / "status.json", status) | |
| return status | |
| def active_id(self): | |
| with self.lock: | |
| for path in sorted(self.root.glob("*/status.json")): | |
| if self.read(path.parent.name).get("status") not in TERMINAL: | |
| return path.parent.name | |
| return None | |
| def start(self, video, question, query_time): | |
| with self.lock: | |
| active = self.active_id() | |
| if active: | |
| raise HTTPException(409, {"message": "Another inference task is active. View or cancel it before starting a new question.", "activeRunId": active}) | |
| run_id = uuid.uuid4().hex | |
| directory = self.root / run_id | |
| directory.mkdir(mode=0o700) | |
| job = {"id": run_id, "videoId": video["id"], "video_path": video["original_path"], | |
| "question": question, "query_time": query_time, "duration": video["duration"], | |
| "max_steps": self.settings.max_model_calls * 2, | |
| "max_model_calls": self.settings.max_model_calls, | |
| "enable_audio": False, "enable_tracking": False} | |
| atomic_json(directory / "job.json", job) | |
| state = {"id": run_id, "videoId": video["id"], "status": "queued", "phase": "preparing", | |
| "question": question, "queryTime": query_time, "createdAt": now(), | |
| "progress": {"current": 0, "total": video["duration"], "message": "Preparing video frames."}, | |
| "answer": None, "error": None, "observations": [], | |
| "memory": {"working": [], "episodes": [], "pins": []}, "plan": {}, "events": [], "modelCalls": 0} | |
| atomic_json(directory / "status.json", state) | |
| with (directory / "worker.log").open("wb") as log: | |
| try: | |
| process = subprocess.Popen(self.runner_command + ["--job-dir", str(directory)], | |
| cwd=REPO, env=self.settings.worker_env(), stdout=log, | |
| stderr=subprocess.STDOUT, start_new_session=True) | |
| except OSError as exc: | |
| state.update(status="failed", phase="error", error="The inference worker could not start.") | |
| atomic_json(directory / "status.json", state) | |
| raise HTTPException(500, state["error"]) from exc | |
| self.processes[run_id] = process | |
| atomic_json(directory / "process.json", {"pid": process.pid}) | |
| threading.Thread(target=self._watch, args=(run_id, process), daemon=True).start() | |
| return state | |
| def _watch(self, run_id, process): | |
| process.wait() | |
| self.read(run_id) | |
| def cancel(self, run_id): | |
| directory = self.directory(run_id) | |
| state = self.read(run_id) | |
| if state.get("status") in TERMINAL: | |
| return state | |
| (directory / "cancel.requested").touch() | |
| process = self.processes.get(run_id) | |
| # Allow a cooperative cancellation at the next model/tool boundary. | |
| if process: | |
| try: | |
| process.wait(timeout=2) | |
| except subprocess.TimeoutExpired: | |
| try: | |
| os.killpg(process.pid, signal.SIGTERM) | |
| except ProcessLookupError: | |
| pass | |
| try: | |
| process.wait(timeout=3) | |
| except subprocess.TimeoutExpired: | |
| try: | |
| os.killpg(process.pid, signal.SIGKILL) | |
| except ProcessLookupError: | |
| pass | |
| process.wait(timeout=3) | |
| else: | |
| pid = self._external_pid(directory) | |
| if pid: | |
| try: | |
| os.killpg(pid, signal.SIGTERM) | |
| except ProcessLookupError: | |
| pass | |
| with self.lock: | |
| state = json.loads((directory / "status.json").read_text()) | |
| if state.get("status") not in TERMINAL: | |
| state.update(status="cancelled", phase="cancelled", error=None, finishedAt=now()) | |
| atomic_json(directory / "status.json", state) | |
| return state | |
| def shutdown(self): | |
| for run_id, process in list(self.processes.items()): | |
| if process.poll() is None: | |
| self.cancel(run_id) | |
| def create_app(settings=None, runner_command=None): | |
| settings = settings or Settings.from_env() | |
| videos = settings.data_dir / "videos" | |
| videos.mkdir(parents=True, exist_ok=True) | |
| runs = RunManager(settings, runner_command) | |
| health_cache = {"time": 0, "value": None, "verified_until": 0} | |
| upload_lock = asyncio.Semaphore(1) | |
| async def lifespan(_app): | |
| yield | |
| await asyncio.to_thread(runs.shutdown) | |
| app = FastAPI(title="DeltaStreamer Upload Demo", lifespan=lifespan) | |
| app.state.settings = settings | |
| app.state.runs = runs | |
| async def same_origin(request: Request, call_next): | |
| if request.method in {"POST", "PUT", "DELETE", "PATCH"}: | |
| origin = request.headers.get("origin") | |
| if origin and urlsplit(origin).netloc != request.headers.get("host"): | |
| return JSONResponse({"detail": "Use this application's own origin."}, status_code=403) | |
| return await call_next(request) | |
| def read_video(video_id, private=False): | |
| directory = videos / checked_id(video_id) | |
| path = directory / "metadata.json" | |
| if not path.is_file(): | |
| raise HTTPException(404, "Uploaded video not found.") | |
| result = json.loads(path.read_text()) | |
| return result if private else {k: v for k, v in result.items() if k != "original_path"} | |
| def headers(): | |
| return {"Authorization": f"Bearer {settings.api_key}"} if settings.api_key else {} | |
| async def health(): | |
| if (time.monotonic() > health_cache["verified_until"] and time.monotonic() - health_cache["time"] > 5) or health_cache["value"] is None: | |
| result = {"ready": False, "connectionStatus": "error"} | |
| try: | |
| async with httpx.AsyncClient(timeout=min(8, settings.api_timeout)) as client: | |
| response = await client.get(settings.api_base_url + "/models", headers=headers()) | |
| if response.status_code in {404, 405}: | |
| result.update(ready=True, connectionStatus="unverified", message="This provider does not list models. Use Check connection to verify two-image input.") | |
| elif response.is_success: | |
| models = [row.get("id") for row in response.json().get("data", [])] | |
| found = settings.model in models | |
| result.update(ready=found, connectionStatus="reachable" if found else "model_missing", | |
| message="Model endpoint is reachable." if found else "DEMO_MODEL was not found in the server's model list.") | |
| else: | |
| result["message"] = f"Model endpoint returned HTTP {response.status_code}. Check the API URL and server-side key." | |
| except (httpx.HTTPError, ValueError, TypeError, AttributeError): | |
| result["message"] = "Cannot reach the model endpoint. Check that the model server is running and DEMO_API_BASE_URL is correct." | |
| health_cache.update(time=time.monotonic(), value=result) | |
| return {"ok": True, "model": settings.model, "backend": settings.backend, | |
| "limits": {"maxUploadMB": settings.max_upload_mb, "maxDurationSeconds": settings.max_duration_seconds}, | |
| "activeRunId": runs.active_id(), **health_cache["value"]} | |
| async def check_connection(): | |
| content = [{"type": "text", "text": "Return exactly a JSON list with the dominant color of the first image, then the second image. Use English color names only."}] | |
| for color in ("red", "blue"): | |
| buffer = io.BytesIO() | |
| Image.new("RGB", (64, 64), color).save(buffer, format="PNG") | |
| data = base64.b64encode(buffer.getvalue()).decode() | |
| content.append({"type": "image_url", "image_url": {"url": "data:image/png;base64," + data}}) | |
| body = {"model": settings.model, "messages": [{"role": "user", "content": content}], | |
| "max_tokens": 128, "temperature": 0, "stream": False, **settings.extra_body} | |
| try: | |
| async with httpx.AsyncClient(timeout=settings.api_timeout) as client: | |
| response = await client.post(settings.api_base_url + "/chat/completions", headers=headers(), json=body) | |
| if not response.is_success: | |
| raise HTTPException(502, f"Vision check returned HTTP {response.status_code}. Check the model name, credentials, and provider options.") | |
| text = response.json()["choices"][0]["message"]["content"] | |
| match = re.search(r"\[[^\]]+\]", text or "") | |
| colors = json.loads(match.group()) if match else None | |
| correct = isinstance(colors, list) and [str(x).lower().strip() for x in colors] == ["red", "blue"] | |
| except httpx.HTTPError as exc: | |
| raise HTTPException(502, "The model endpoint could not complete the two-image check.") from exc | |
| except (KeyError, IndexError, TypeError, ValueError) as exc: | |
| raise HTTPException(502, "The model returned no usable text for the two-image check. Check thinking-mode and output-token settings.") from exc | |
| if correct: | |
| health_cache.update(time=time.monotonic(), verified_until=time.monotonic() + 300, | |
| value={"ready": True, "connectionStatus": "verified", "message": "Two-image input verified."}) | |
| return {"ok": correct, "model": settings.model, "message": "Two-image input verified." if correct else "The endpoint responded, but the two-image color check did not pass. Verify vision support and provider options.", "response": settings.safe_error(text or "")} | |
| async def upload_video(video: UploadFile = File(...)): | |
| name = Path((video.filename or "video.mp4").replace("\\", "/")).name[:240] | |
| extension = Path(name).suffix.lower() | |
| if extension not in EXTENSIONS: | |
| await video.close() | |
| raise HTTPException(415, "Use an MP4, MOV, MKV, WebM, AVI, or M4V video.") | |
| video_id = uuid.uuid4().hex | |
| directory = videos / video_id | |
| directory.mkdir(mode=0o700) | |
| original = directory / ("original" + extension) | |
| try: | |
| size = 0 | |
| with original.open("wb") as target: | |
| while chunk := await video.read(1024 * 1024): | |
| size += len(chunk) | |
| if size > settings.max_upload_mb * 1024 * 1024: | |
| raise HTTPException(413, f"Video exceeds the {settings.max_upload_mb} MB upload limit.") | |
| target.write(chunk) | |
| if not size: | |
| raise HTTPException(400, "The uploaded file is empty.") | |
| async with upload_lock: | |
| details = await asyncio.to_thread(prepare_video, original, directory / "preview.mp4", settings.max_duration_seconds) | |
| metadata = {"id": video_id, "name": name, "url": f"/media/{video_id}/video.mp4", | |
| **details, "size": size, "createdAt": now(), "original_path": str(original)} | |
| atomic_json(directory / "metadata.json", metadata) | |
| return read_video(video_id) | |
| except (HTTPException, ValueError) as exc: | |
| shutil.rmtree(directory, ignore_errors=True) | |
| if isinstance(exc, HTTPException): | |
| raise | |
| raise HTTPException(422, str(exc)) from exc | |
| except BaseException: | |
| shutil.rmtree(directory, ignore_errors=True) | |
| raise | |
| finally: | |
| await video.close() | |
| async def get_video(video_id: str): | |
| return read_video(video_id) | |
| async def video_media(video_id: str): | |
| read_video(video_id) | |
| return FileResponse(videos / video_id / "preview.mp4", media_type="video/mp4") | |
| async def start_run(request: RunRequest): | |
| video = read_video(request.videoId, private=True) | |
| question = request.question.strip() | |
| if not question: | |
| raise HTTPException(422, "Enter a question.") | |
| query_time = video["duration"] if request.queryTime is None else request.queryTime | |
| if not math.isfinite(query_time) or not 0 <= query_time <= video["duration"]: | |
| raise HTTPException(422, "Query time must be within the uploaded video's duration.") | |
| return runs.start(video, question, query_time) | |
| async def get_run(run_id: str): | |
| return runs.read(run_id) | |
| async def cancel_run(run_id: str): | |
| return await asyncio.to_thread(runs.cancel, run_id) | |
| async def index(): | |
| return FileResponse(ROOT / "static" / "index.html", headers={"Cache-Control": "no-cache"}) | |
| app.mount("/static", StaticFiles(directory=ROOT / "static"), name="static") | |
| return app | |
| app = create_app() | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--host", default="127.0.0.1") | |
| parser.add_argument("--port", default=7861, type=int) | |
| args = parser.parse_args() | |
| import uvicorn | |
| uvicorn.run(app, host=args.host, port=args.port) | |