Spaces:
Paused
Paused
Download server/main.py from Simzy/world-room: direct link, hf CLI and curl.
- Browser
- Download file 8.11 kB
-
https://huggingface.co/spaces/Simzy/world-room/resolve/main/server/main.py
- Command line
-
hf download hf://spaces/Simzy/world-room/server/main.py
-
curl -L -o main.py https://huggingface.co/spaces/Simzy/world-room/resolve/main/server/main.py
8.11 kB
| """ | |
| World Room — Backend server | |
| ============================ | |
| FastAPI application that: | |
| 1. Mints ephemeral Realtime API tokens (POST /api/session) | |
| 2. Proxies WebRTC SDP offers to OpenAI (POST /api/calls) | |
| 3. Serves the frontend (GET /) | |
| 4. Health check (GET /api/health) | |
| All OpenAI API calls happen server-side. The browser only ever sees | |
| the ephemeral key and the proxied SDP answer. | |
| """ | |
| import os | |
| import json | |
| import logging | |
| from pathlib import Path | |
| import httpx | |
| from fastapi import FastAPI, Request, Response, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| # --------------------------------------------------------------------------- # | |
| # Configuration | |
| # --------------------------------------------------------------------------- # | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | |
| OPENAI_BASE = "https://api.openai.com/v1/realtime" | |
| MODEL = "gpt-realtime-2.1" | |
| VOICE = "alloy" | |
| WORLDBUILDER_INSTRUCTIONS = ( | |
| "You are World Room, a creative worldbuilding companion. " | |
| "You help users invent fictional settings, characters, conflicts, and scene hooks " | |
| "through natural conversation. " | |
| "Be playful, imaginative, and collaborative. Ask questions. Build on the user's ideas. " | |
| "When they describe a setting, suggest characters that would live there. " | |
| "When they describe a character, suggest conflicts. " | |
| "When they describe a conflict, suggest scene hooks. " | |
| "Keep your responses concise and conversational — this is a voice conversation, not a text essay. " | |
| "Use sound effects and emotional expression in your voice." | |
| ) | |
| FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(message)s") | |
| logger = logging.getLogger("world-room") | |
| # --------------------------------------------------------------------------- # | |
| # App | |
| # --------------------------------------------------------------------------- # | |
| app = FastAPI(title="World Room", version="1.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def _startup() -> None: | |
| if not OPENAI_API_KEY: | |
| logger.warning("OPENAI_API_KEY is not set — /api/session and /api/calls will fail.") | |
| else: | |
| logger.info("OPENAI_API_KEY loaded (key=%s...)", OPENAI_API_KEY[:7]) | |
| logger.info("Frontend directory: %s", FRONTEND_DIR) | |
| # --------------------------------------------------------------------------- # | |
| # Health | |
| # --------------------------------------------------------------------------- # | |
| async def health() -> dict: | |
| return {"status": "ok"} | |
| # --------------------------------------------------------------------------- # | |
| # Ephemeral session token | |
| # --------------------------------------------------------------------------- # | |
| async def create_session() -> JSONResponse: | |
| """Create an ephemeral Realtime session token via OpenAI.""" | |
| if not OPENAI_API_KEY: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Server is not configured: OPENAI_API_KEY is missing. " | |
| "Set it and restart the server.", | |
| ) | |
| payload = { | |
| "session": { | |
| "type": "realtime", | |
| "model": MODEL, | |
| "audio": {"output": {"voice": VOICE}}, | |
| "instructions": WORLDBUILDER_INSTRUCTIONS, | |
| } | |
| } | |
| headers = { | |
| "Authorization": f"Bearer {OPENAI_API_KEY}", | |
| "Content-Type": "application/json", | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=15.0) as client: | |
| resp = await client.post( | |
| f"{OPENAI_BASE}/client_secrets", | |
| json=payload, | |
| headers=headers, | |
| ) | |
| except httpx.HTTPError as exc: | |
| logger.error("Network error contacting OpenAI: %s", exc) | |
| raise HTTPException(status_code=502, detail=f"Could not reach OpenAI: {exc}") | |
| if resp.status_code != 200: | |
| detail = _safe_error_detail(resp) | |
| logger.error("OpenAI /client_secrets returned %s: %s", resp.status_code, detail) | |
| raise HTTPException( | |
| status_code=resp.status_code, | |
| detail=f"OpenAI rejected session creation: {detail}", | |
| ) | |
| data = resp.json() | |
| # The ephemeral key lives in the "value" field; expires_at is top-level. | |
| ephemeral_key = data.get("value") | |
| expires_at = data.get("expires_at") | |
| if not ephemeral_key: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="OpenAI response did not include an ephemeral key ('value' field).", | |
| ) | |
| logger.info("Ephemeral key minted (expires_at=%s)", expires_at) | |
| return JSONResponse({"ephemeral_key": ephemeral_key, "expires_at": expires_at}) | |
| # --------------------------------------------------------------------------- # | |
| # WebRTC SDP proxy | |
| # --------------------------------------------------------------------------- # | |
| async def proxy_call(request: Request) -> Response: | |
| """Proxy a WebRTC SDP offer to OpenAI and return the SDP answer.""" | |
| ephemeral_key = request.query_params.get("key", "").strip() | |
| if not ephemeral_key: | |
| raise HTTPException(status_code=400, detail="Missing 'key' query parameter.") | |
| # Read the raw SDP offer body | |
| sdp_offer = (await request.body()).decode("utf-8", errors="replace") | |
| if not sdp_offer.strip(): | |
| raise HTTPException(status_code=400, detail="Empty SDP offer body.") | |
| headers = { | |
| "Authorization": f"Bearer {ephemeral_key}", | |
| "Content-Type": "application/sdp", | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=15.0) as client: | |
| resp = await client.post( | |
| f"{OPENAI_BASE}/calls", | |
| content=sdp_offer, | |
| headers=headers, | |
| ) | |
| except httpx.HTTPError as exc: | |
| logger.error("Network error proxying SDP to OpenAI: %s", exc) | |
| raise HTTPException(status_code=502, detail=f"Could not reach OpenAI: {exc}") | |
| if resp.status_code != 200: | |
| detail = _safe_error_detail(resp) | |
| logger.error("OpenAI /calls returned %s: %s", resp.status_code, detail) | |
| raise HTTPException( | |
| status_code=resp.status_code, | |
| detail=f"OpenAI rejected SDP offer: {detail}", | |
| ) | |
| sdp_answer = resp.text | |
| logger.info("SDP answer received (%d bytes)", len(sdp_answer)) | |
| return Response(content=sdp_answer, media_type="application/sdp") | |
| # --------------------------------------------------------------------------- # | |
| # Frontend | |
| # --------------------------------------------------------------------------- # | |
| async def serve_frontend() -> HTMLResponse: | |
| index_path = FRONTEND_DIR / "index.html" | |
| if not index_path.exists(): | |
| raise HTTPException(status_code=404, detail="frontend/index.html not found.") | |
| html = index_path.read_text(encoding="utf-8") | |
| return HTMLResponse(content=html) | |
| # --------------------------------------------------------------------------- # | |
| # Helpers | |
| # --------------------------------------------------------------------------- # | |
| def _safe_error_detail(resp: httpx.Response) -> str: | |
| """Try to extract a human-readable error message from an OpenAI error response.""" | |
| try: | |
| body = resp.json() | |
| err = body.get("error") or body | |
| if isinstance(err, dict): | |
| return err.get("message") or json.dumps(err) | |
| return str(err) | |
| except Exception: | |
| return resp.text[:500] | |
| # --------------------------------------------------------------------------- # | |
| # Entrypoint (so `python -m server.main` also works) | |
| # --------------------------------------------------------------------------- # | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "server.main:app", | |
| host="0.0.0.0", | |
| port=8002, | |
| reload=False, | |
| ) | |