world-room / server /main.py
Simzy's picture
Upload folder using huggingface_hub
40e8f00 verified
Raw History Blame Contribute Delete
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=["*"],
)
@app.on_event("startup")
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
# --------------------------------------------------------------------------- #
@app.get("/api/health")
async def health() -> dict:
return {"status": "ok"}
# --------------------------------------------------------------------------- #
# Ephemeral session token
# --------------------------------------------------------------------------- #
@app.post("/api/session")
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
# --------------------------------------------------------------------------- #
@app.post("/api/calls")
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
# --------------------------------------------------------------------------- #
@app.get("/", response_class=HTMLResponse)
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,
)