Spaces:
Paused
Paused
File size: 7,821 Bytes
96540b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Streaming protocol for the optional WebSocket backend.
The Gradio Space itself streams through generator functions; this module defines
the wire format and a modality-agnostic dispatcher so the same playground can be
re-deployed as a Docker Space (FastAPI + uvicorn) without touching the loaders.
Wire format (JSON frames)
-------------------------
client -> server
{"type": "request", "id": "<uuid>", "modality": "text|image|audio|video",
"repo_id": "...", "task": "auto|<task>", "params": {...}}
{"type": "cancel", "id": "<uuid>"}
server -> client
{"type": "ack", "id": "..."}
{"type": "chunk", "id": "...", "seq": 0, "data": "...", "encoding": "text|base64"}
{"type": "done", "id": "...", "meta": {...}}
{"type": "error", "id": "...", "message": "..."}
Endpoints: /ws/text, /ws/audio, /ws/video (one dispatcher, three routes).
Run standalone: uvicorn ws_protocol:app --host 0.0.0.0 --port 7861
"""
from __future__ import annotations
import base64
import json
import os
from dataclasses import asdict, dataclass, field
from typing import Any, Dict, Iterator, Optional
import model_registry
from loaders.common import PlaygroundError
WS_ROUTES = ("/ws/text", "/ws/audio", "/ws/video")
FRAME_TYPES = ("request", "cancel", "ack", "chunk", "done", "error")
# --------------------------------------------------------------------------- #
# Frames
# --------------------------------------------------------------------------- #
@dataclass
class Frame:
type: str
id: str
seq: Optional[int] = None
data: Optional[str] = None
encoding: Optional[str] = None
message: Optional[str] = None
meta: Dict[str, Any] = field(default_factory=dict)
def to_json(self) -> str:
payload = {k: v for k, v in asdict(self).items() if v not in (None, {}, [])}
payload["type"] = self.type
payload["id"] = self.id
return json.dumps(payload, ensure_ascii=False)
def ack(request_id: str) -> Frame:
return Frame(type="ack", id=request_id)
def chunk(request_id: str, seq: int, data: str, encoding: str = "text") -> Frame:
return Frame(type="chunk", id=request_id, seq=seq, data=data, encoding=encoding)
def done(request_id: str, **meta: Any) -> Frame:
return Frame(type="done", id=request_id, meta=meta)
def error(request_id: str, message: str) -> Frame:
return Frame(type="error", id=request_id, message=message)
def parse_request(raw: str) -> Dict[str, Any]:
"""Validate an inbound frame and return it as a dict."""
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise PlaygroundError(f"Malformed frame: {exc}") from exc
if not isinstance(payload, dict):
raise PlaygroundError("Frame must be a JSON object.")
frame_type = payload.get("type")
if frame_type not in FRAME_TYPES:
raise PlaygroundError(f"Unknown frame type {frame_type!r}.")
if frame_type == "request":
for required in ("id", "repo_id"):
if not payload.get(required):
raise PlaygroundError(f"Frame is missing '{required}'.")
return payload
# --------------------------------------------------------------------------- #
# Dispatcher
# --------------------------------------------------------------------------- #
def handle_request(payload: Dict[str, Any]) -> Iterator[Frame]:
"""Turn one `request` frame into a stream of response frames.
Synchronous generator on purpose: the transport (FastAPI, Gradio, a queue
worker) decides how to pump it, and the loaders stay transport-agnostic.
"""
request_id = payload.get("id", "0")
repo_id = payload.get("repo_id", "")
task = payload.get("task", "auto")
params: Dict[str, Any] = payload.get("params", {}) or {}
yield ack(request_id)
try:
spec = model_registry.resolve(repo_id, task)
loader = model_registry.get_loader(spec.modality)
handle = loader.load_model(
spec.repo_id,
spec.task,
dtype=params.get("dtype", "auto"),
uncensored=bool(params.get("uncensored", False)),
)
if spec.modality == "text":
prompt = params.pop("prompt", "")
seq = 0
for partial in loader.stream_inference(handle, prompt, **params):
yield chunk(request_id, seq, partial)
seq += 1
yield done(request_id, task=spec.task, chunks=seq)
return
if spec.modality == "image":
images, info = loader.run_inference(handle, **params)
for seq, image in enumerate(images):
yield chunk(request_id, seq, _encode_image(image), encoding="base64")
yield done(request_id, task=spec.task, info=info)
return
if spec.modality == "audio":
audio, text = loader.run_inference(handle, **params)
if text:
yield chunk(request_id, 0, text)
if audio is not None:
rate, array = audio
yield chunk(request_id, 1, _encode_array(array), encoding="base64")
yield done(request_id, task=spec.task, sampling_rate=rate)
return
yield done(request_id, task=spec.task)
return
path, info = loader.run_inference(handle, **params)
if path:
yield chunk(request_id, 0, _encode_file(path), encoding="base64")
yield done(request_id, task=spec.task, info=info, filename=os.path.basename(path or ""))
except PlaygroundError as exc:
yield error(request_id, str(exc))
except Exception as exc: # never let the socket die on a model-specific bug
yield error(request_id, f"{type(exc).__name__}: {exc}")
def _encode_file(path: str) -> str:
with open(path, "rb") as handle:
return base64.b64encode(handle.read()).decode("ascii")
def _encode_image(image: Any) -> str:
import io
buffer = io.BytesIO()
image.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("ascii")
def _encode_array(array: Any) -> str:
try:
import numpy as np
return base64.b64encode(np.asarray(array, dtype="float32").tobytes()).decode("ascii")
except ImportError:
return base64.b64encode(bytes(array)).decode("ascii")
# --------------------------------------------------------------------------- #
# Optional FastAPI app (only built when fastapi is installed)
# --------------------------------------------------------------------------- #
def build_app():
"""Return a FastAPI app exposing WS_ROUTES. Raises if fastapi is missing."""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
api = FastAPI(title="Model Playground WS")
async def endpoint(websocket: WebSocket) -> None:
await websocket.accept()
try:
while True:
raw = await websocket.receive_text()
try:
payload = parse_request(raw)
except PlaygroundError as exc:
await websocket.send_text(error("0", str(exc)).to_json())
continue
if payload["type"] != "request":
continue
for frame in handle_request(payload):
await websocket.send_text(frame.to_json())
except WebSocketDisconnect:
return
for route in WS_ROUTES:
api.add_api_websocket_route(route, endpoint)
@api.get("/health")
async def health() -> Dict[str, Any]:
return {"status": "ok", "routes": list(WS_ROUTES)}
return api
try: # convenience for `uvicorn ws_protocol:app`
app = build_app()
except Exception: # fastapi absent in a plain Gradio Space — expected
app = None
|