SalexAI commited on
Commit
978c3bc
·
verified ·
1 Parent(s): c132b4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +131 -44
app.py CHANGED
@@ -1,81 +1,168 @@
1
  import os
2
  import requests
3
- from fastapi import FastAPI, Request
 
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.responses import JSONResponse
6
 
7
  app = FastAPI()
8
 
9
- # Allow ScratchX / PenguinMod
10
  app.add_middleware(
11
  CORSMiddleware,
12
- allow_origins=["*"], # 🔒 restrict later if desired
13
  allow_credentials=True,
14
- allow_methods=["GET", "POST", "OPTIONS"],
15
  allow_headers=["*"],
 
16
  )
17
 
18
  OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
19
- OPENAI_REALTIME_URL = "https://api.openai.com/v1/realtime/sessions"
 
20
 
 
 
 
21
 
22
- def _mint_ephemeral(model: str, voice: str):
23
- """Helper to call OpenAI and mint ephemeral token."""
24
- if not OPENAI_API_KEY:
25
- return JSONResponse(
26
- status_code=500,
27
- content={"error": "OPENAI_API_KEY not set in environment"},
28
- )
29
 
30
- headers = {
 
 
 
 
 
 
 
 
31
  "Authorization": f"Bearer {OPENAI_API_KEY}",
32
- "Content-Type": "application/json",
33
- "OpenAI-Beta": "realtime=v1",
34
  }
35
- body = {"model": model, "voice": voice}
 
 
 
36
 
37
- try:
38
- r = requests.post(OPENAI_REALTIME_URL, headers=headers, json=body)
39
- r.raise_for_status()
40
- return r.json()
41
- except Exception as e:
42
- return JSONResponse(status_code=500, content={"error": str(e)})
43
 
44
-
45
- # --- Health endpoints ---
46
  @app.get("/health")
47
  @app.get("/health/")
48
- @app.get("/proxy/health")
49
- @app.get("/proxy/health/")
50
  def health():
51
- return {"status": "ok"}
52
 
 
53
 
54
- @app.middleware("http")
55
- async def log_requests(request: Request, call_next):
56
- print(f"[DEBUG] Incoming: {request.method} {request.url.path}")
57
- response = await call_next(request)
58
- return response
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # --- Ephemeral endpoints ---
62
  @app.get("/ephemeral")
63
  @app.get("/ephemeral/")
64
- @app.get("/proxy/ephemeral")
65
- @app.get("/proxy/ephemeral/")
66
- def ephemeral_get(model: str = "gpt-4o-realtime-preview", voice: str = "verse"):
67
- return _mint_ephemeral(model, voice)
68
-
69
 
70
  @app.post("/ephemeral")
71
  @app.post("/ephemeral/")
72
- @app.post("/proxy/ephemeral")
73
- @app.post("/proxy/ephemeral/")
74
  async def ephemeral_post(request: Request):
75
  try:
76
  data = await request.json()
77
- model = data.get("model", "gpt-4o-realtime-preview")
78
- voice = data.get("voice", "verse")
79
  except Exception:
80
- model, voice = "gpt-4o-realtime-preview", "verse"
81
- return _mint_ephemeral(model, voice)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import requests
3
+ from typing import Optional
4
+ from fastapi import FastAPI, Request, UploadFile, File, Form, Response
5
  from fastapi.middleware.cors import CORSMiddleware
6
  from fastapi.responses import JSONResponse
7
 
8
  app = FastAPI()
9
 
10
+ # CORS: keep wide for dev; you can restrict origins later
11
  app.add_middleware(
12
  CORSMiddleware,
13
+ allow_origins=["*"],
14
  allow_credentials=True,
15
+ allow_methods=["GET", "POST", "OPTIONS", "HEAD"],
16
  allow_headers=["*"],
17
+ expose_headers=["*"],
18
  )
19
 
20
  OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
21
+ REALTIME_SESSION_URL = "https://api.openai.com/v1/realtime/sessions"
22
+ AUDIO_TRANSCRIBE_URL = "https://api.openai.com/v1/audio/transcriptions"
23
 
24
+ # defaults (you can tune these)
25
+ DEFAULT_REALTIME_MODEL = "gpt-realtime"
26
+ DEFAULT_VOICE = "verse"
27
 
28
+ # New STT defaults — fast + cheap; switch to gpt-4o-transcribe for peak accuracy
29
+ DEFAULT_STT_MODEL = "gpt-4o-mini-transcribe"
30
+
31
+ # ---------- helpers ----------
 
 
 
32
 
33
+ def _json_err(msg: str, code: int = 500):
34
+ return JSONResponse(
35
+ status_code=code,
36
+ content={"error": msg},
37
+ headers={"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"},
38
+ )
39
+
40
+ def _auth_headers(beta_realtime: bool = False):
41
+ h = {
42
  "Authorization": f"Bearer {OPENAI_API_KEY}",
 
 
43
  }
44
+ if beta_realtime:
45
+ # required for Realtime session creation
46
+ h["OpenAI-Beta"] = "realtime=v1"
47
+ return h
48
 
49
+ # ---------- health ----------
 
 
 
 
 
50
 
 
 
51
  @app.get("/health")
52
  @app.get("/health/")
 
 
53
  def health():
54
+ return JSONResponse({"status": "ok"}, headers={"Access-Control-Allow-Origin": "*"})
55
 
56
+ # ---------- realtime ephemeral ----------
57
 
58
+ def mint_ephemeral(model: str = DEFAULT_REALTIME_MODEL, voice: str = DEFAULT_VOICE):
59
+ if not OPENAI_API_KEY:
60
+ return _json_err("OPENAI_API_KEY not set in environment", 500)
 
 
61
 
62
+ try:
63
+ r = requests.post(
64
+ REALTIME_SESSION_URL,
65
+ headers={**_auth_headers(beta_realtime=True), "Content-Type": "application/json"},
66
+ json={"model": model, "voice": voice},
67
+ timeout=15,
68
+ )
69
+ r.raise_for_status()
70
+ return JSONResponse(
71
+ status_code=200,
72
+ content=r.json(),
73
+ headers={"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"},
74
+ )
75
+ except Exception as e:
76
+ return _json_err(str(e), 500)
77
 
 
78
  @app.get("/ephemeral")
79
  @app.get("/ephemeral/")
80
+ def ephemeral_get(model: str = DEFAULT_REALTIME_MODEL, voice: str = DEFAULT_VOICE):
81
+ return mint_ephemeral(model, voice)
 
 
 
82
 
83
  @app.post("/ephemeral")
84
  @app.post("/ephemeral/")
 
 
85
  async def ephemeral_post(request: Request):
86
  try:
87
  data = await request.json()
88
+ model = data.get("model", DEFAULT_REALTIME_MODEL)
89
+ voice = data.get("voice", DEFAULT_VOICE)
90
  except Exception:
91
+ model, voice = DEFAULT_REALTIME_MODEL, DEFAULT_VOICE
92
+ return mint_ephemeral(model, voice)
93
+
94
+ # Catch-all (helps when callers accidentally hit "/" with signed proxy params)
95
+ @app.api_route("/", methods=["GET", "POST", "OPTIONS", "HEAD"])
96
+ @app.api_route("/{_path:path}", methods=["GET", "POST", "OPTIONS", "HEAD"])
97
+ async def catch_all(request: Request, _path: str = ""):
98
+ # Serve ephemeral token everywhere except the explicit /transcribe path
99
+ if request.url.path.startswith("/transcribe"):
100
+ return JSONResponse(
101
+ {"error": "use POST /transcribe for audio"}, status_code=405,
102
+ headers={"Access-Control-Allow-Origin": "*"}
103
+ )
104
+ if request.method == "OPTIONS":
105
+ return Response(
106
+ status_code=204,
107
+ headers={
108
+ "Access-Control-Allow-Origin": "*",
109
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS, HEAD",
110
+ "Access-Control-Allow-Headers": "*",
111
+ },
112
+ )
113
+ # default: mint realtime token (handy for clients that strip paths)
114
+ return mint_ephemeral(DEFAULT_REALTIME_MODEL, DEFAULT_VOICE)
115
+
116
+ # ---------- NEW: speech-to-text via OpenAI ----------
117
+
118
+ @app.post("/transcribe")
119
+ async def transcribe(
120
+ file: UploadFile = File(..., description="Audio file (wav/mp3/m4a/webm/ogg)"),
121
+ model: str = Form(DEFAULT_STT_MODEL),
122
+ language: Optional[str] = Form(None),
123
+ response_format: str = Form("json"), # "json" | "text" | "srt" | "verbose_json" | "vtt" (model dependent)
124
+ ):
125
+ """
126
+ Proxy to OpenAI audio/transcriptions.
127
+ - Default model: gpt-4o-mini-transcribe (fast). Use gpt-4o-transcribe for max accuracy.
128
+ - Send multipart/form-data with 'file' plus optional fields.
129
+ """
130
+ if not OPENAI_API_KEY:
131
+ return _json_err("OPENAI_API_KEY not set in environment", 500)
132
+
133
+ try:
134
+ # read bytes once
135
+ data_bytes = await file.read()
136
+ files = {
137
+ "file": (file.filename or "audio", data_bytes, file.content_type or "application/octet-stream")
138
+ }
139
+ form = {"model": model}
140
+ if language:
141
+ form["language"] = language
142
+ if response_format:
143
+ form["response_format"] = response_format
144
+
145
+ r = requests.post(
146
+ AUDIO_TRANSCRIBE_URL,
147
+ headers=_auth_headers(),
148
+ files=files,
149
+ data=form, # multipart form fields
150
+ timeout=60,
151
+ )
152
+ # If text/plain was requested, forward as text response
153
+ if response_format == "text":
154
+ return Response(content=r.text, media_type="text/plain", headers={"Access-Control-Allow-Origin": "*"})
155
+ # Otherwise assume JSON or SRT/VTT handled as text but wrapped in JSON for consistency
156
+ try:
157
+ r.raise_for_status()
158
+ ct = r.headers.get("content-type", "")
159
+ if "application/json" in ct:
160
+ return JSONResponse(r.json(), headers={"Access-Control-Allow-Origin": "*"})
161
+ # Non-JSON payloads (srt, vtt) — wrap as {text: "..."}
162
+ return JSONResponse({"text": r.text}, headers={"Access-Control-Allow-Origin": "*"})
163
+ except Exception:
164
+ # bubble up OpenAI's error text
165
+ return _json_err(r.text or "Transcription failed", r.status_code if r.status_code else 500)
166
+
167
+ except Exception as e:
168
+ return _json_err(str(e), 500)