SiddhJagani commited on
Commit
7549ac8
·
verified ·
1 Parent(s): 8fb3e8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -50
app.py CHANGED
@@ -3,19 +3,21 @@ import json
3
  import httpx
4
  import gradio as gr
5
  import uvicorn
6
- from fastapi import FastAPI, Request, Header, HTTPException, File, UploadFile
7
- from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
8
  import time
 
 
9
 
10
  # ---------------------------------------------------------------------
11
- # Configuration
12
  # ---------------------------------------------------------------------
13
- BYTEZ_CHAT_URL = "https://api.bytez.com/models/v2/openai/v1/chat/completions "
14
- BYTEZ_MODELS_URL = "https://api.bytez.com/models/v2/list/models "
15
  BYTEZ_TTS_URL = "https://api.bytez.com/models/v2/openai/tts-1-hd"
16
  BYTEZ_STT_URL = "https://api.bytez.com/models/v2/openai/whisper-1"
17
- BYTEZ_AUTH = os.getenv("BYTEZ_API_KEY")
18
- LOCAL_API_KEY = os.getenv("LOCAL_API_KEY")
19
 
20
  # ---------------------------------------------------------------------
21
  # FastAPI backend
@@ -61,7 +63,7 @@ async def models(authorization: str = Header(None)):
61
  )
62
 
63
  # ---------------------------------------------------------------------
64
- # /v1/chat/completions
65
  # ---------------------------------------------------------------------
66
  @api.post("/v1/chat/completions")
67
  async def chat(request: Request, authorization: str = Header(None)):
@@ -74,7 +76,9 @@ async def chat(request: Request, authorization: str = Header(None)):
74
  "Authorization": BYTEZ_AUTH,
75
  "Content-Type": "application/json",
76
  }
 
77
  async def event_stream():
 
78
  async with httpx.AsyncClient(timeout=120) as client:
79
  async with client.stream("POST", BYTEZ_CHAT_URL, headers=headers, json=payload) as upstream:
80
  async for line in upstream.aiter_lines():
@@ -82,38 +86,50 @@ async def chat(request: Request, authorization: str = Header(None)):
82
  if not line:
83
  continue
84
  if line.startswith("data: "):
85
- json_str = line[6:]
86
  else:
87
  json_str = line
 
 
 
 
 
88
  try:
89
  chunk = json.loads(json_str)
90
  except json.JSONDecodeError:
91
  continue
92
- if json_str == "[DONE]":
93
- yield "data: [DONE]\n\n"
94
- break
 
 
 
95
  content = ""
96
- if "token" in chunk:
 
 
97
  content = chunk["token"]
98
- elif "choices" in chunk and len(chunk["choices"]) > 0:
99
- delta = chunk["choices"][0].get("delta", {})
100
- content = delta.get("content", "")
101
  elif "text" in chunk:
102
  content = chunk["text"]
103
  else:
104
- content = str(chunk)
 
 
 
 
 
 
 
105
  openai_chunk = {
106
  "id": "chatcmpl-proxy-stream",
107
  "object": "chat.completion.chunk",
108
  "created": int(time.time()),
109
  "model": payload.get("model", "unknown"),
110
- "choices": [
111
- {
112
- "index": 0,
113
- "delta": {"role": "assistant", "content": content},
114
- "finish_reason": None,
115
- }
116
- ],
117
  }
118
  yield f"data: {json.dumps(openai_chunk)}\n\n"
119
  yield "data: [DONE]\n\n"
@@ -124,12 +140,15 @@ async def chat(request: Request, authorization: str = Header(None)):
124
  media_type="text/event-stream",
125
  headers={"Access-Control-Allow-Origin": "*"}
126
  )
 
 
127
  async with httpx.AsyncClient(timeout=120) as c:
128
  r = await c.post(BYTEZ_CHAT_URL, headers=headers, json=payload)
129
  try:
130
  data = r.json()
131
  except json.JSONDecodeError:
132
  raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
 
133
  if "choices" not in data:
134
  content = (
135
  data.get("output")
@@ -147,57 +166,114 @@ async def chat(request: Request, authorization: str = Header(None)):
147
  return JSONResponse(data, headers={"Access-Control-Allow-Origin": "*"})
148
 
149
  # ---------------------------------------------------------------------
150
- # /v1/tts (Text-to-Speech)
151
  # ---------------------------------------------------------------------
152
- @api.post("/v1/tts/audio/transcriptions")
153
  async def tts(request: Request, authorization: str = Header(None)):
154
  check_key(authorization)
155
  if not BYTEZ_AUTH:
156
  raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
157
- payload = await request.json()
158
- text = payload.get("text", "")
 
 
 
 
 
159
  if not text:
160
- raise HTTPException(status_code=400, detail="Text not provided for TTS")
 
161
  headers = {
162
  "Authorization": BYTEZ_AUTH,
163
  "Content-Type": "application/json",
164
  }
165
- async with httpx.AsyncClient(timeout=120) as c:
166
- r = await c.post(BYTEZ_TTS_URL, headers=headers, json={"text": text})
 
 
 
 
 
 
167
  if r.status_code != 200:
168
- raise HTTPException(status_code=r.status_code, detail="TTS request failed")
169
- audio_data = r.content
170
- return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
 
 
 
 
 
 
171
 
172
  # ---------------------------------------------------------------------
173
- # /v1/stt (Speech-to-Text)
174
  # ---------------------------------------------------------------------
175
- @api.post("/v1/stt")
176
- async def stt(file: UploadFile = File(...), authorization: str = Header(None)):
 
 
 
 
177
  check_key(authorization)
178
  if not BYTEZ_AUTH:
179
  raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
180
- headers = {"Authorization": BYTEZ_AUTH}
181
- file_content = await file.read()
182
- files = {"file": (file.filename, file_content, file.content_type)}
183
-
184
- async with httpx.AsyncClient(timeout=120) as c:
185
- r = await c.post(BYTEZ_STT_URL, headers=headers, files=files)
186
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  try:
188
  data = r.json()
189
  except json.JSONDecodeError:
190
- raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
191
-
192
- transcript = data.get("transcription", "")
193
- return {"transcription": transcript}
 
 
 
 
 
 
 
194
 
195
  # ---------------------------------------------------------------------
196
- # Minimal Gradio UI (to make HF Space start)
197
  # ---------------------------------------------------------------------
198
  with gr.Blocks() as ui:
199
  gr.Markdown("### ✅ Jwero Bytez → OpenAI Proxy\n"
200
- "Endpoints: `/v1/models`, `/v1/chat/completions`, `/v1/tts`, `/v1/stt`")
 
 
 
 
201
 
202
  demo = gr.mount_gradio_app(api, ui, path="/")
203
 
 
3
  import httpx
4
  import gradio as gr
5
  import uvicorn
6
+ from fastapi import FastAPI, Request, Header, HTTPException, File, UploadFile, Form
7
+ from fastapi.responses import JSONResponse, StreamingResponse, Response
8
  import time
9
+ import base64
10
+ from io import BytesIO
11
 
12
  # ---------------------------------------------------------------------
13
+ # Configuration - FIXED TRAILING SPACES
14
  # ---------------------------------------------------------------------
15
+ BYTEZ_CHAT_URL = "https://api.bytez.com/models/v2/openai/v1/chat/completions"
16
+ BYTEZ_MODELS_URL = "https://api.bytez.com/models/v2/list/models"
17
  BYTEZ_TTS_URL = "https://api.bytez.com/models/v2/openai/tts-1-hd"
18
  BYTEZ_STT_URL = "https://api.bytez.com/models/v2/openai/whisper-1"
19
+ BYTEZ_AUTH = os.getenv("BYTEZ_API_KEY")
20
+ LOCAL_API_KEY = os.getenv("LOCAL_API_KEY")
21
 
22
  # ---------------------------------------------------------------------
23
  # FastAPI backend
 
63
  )
64
 
65
  # ---------------------------------------------------------------------
66
+ # /v1/chat/completions - FIXED STREAMING
67
  # ---------------------------------------------------------------------
68
  @api.post("/v1/chat/completions")
69
  async def chat(request: Request, authorization: str = Header(None)):
 
76
  "Authorization": BYTEZ_AUTH,
77
  "Content-Type": "application/json",
78
  }
79
+
80
  async def event_stream():
81
+ first_chunk = True # Track first chunk for role
82
  async with httpx.AsyncClient(timeout=120) as client:
83
  async with client.stream("POST", BYTEZ_CHAT_URL, headers=headers, json=payload) as upstream:
84
  async for line in upstream.aiter_lines():
 
86
  if not line:
87
  continue
88
  if line.startswith("data: "):
89
+ json_str = line[6:].strip()
90
  else:
91
  json_str = line
92
+
93
+ if json_str == "[DONE]":
94
+ yield "data: [DONE]\n\n"
95
+ break
96
+
97
  try:
98
  chunk = json.loads(json_str)
99
  except json.JSONDecodeError:
100
  continue
101
+
102
+ # Skip metadata chunks
103
+ if "usage" in chunk or not chunk.get("choices"):
104
+ continue
105
+
106
+ choice = chunk["choices"][0]
107
  content = ""
108
+ if "delta" in choice and "content" in choice["delta"]:
109
+ content = choice["delta"]["content"]
110
+ elif "token" in chunk:
111
  content = chunk["token"]
 
 
 
112
  elif "text" in chunk:
113
  content = chunk["text"]
114
  else:
115
+ continue
116
+
117
+ # Build delta - role only in first chunk
118
+ delta = {"content": content}
119
+ if first_chunk:
120
+ delta["role"] = "assistant"
121
+ first_chunk = False
122
+
123
  openai_chunk = {
124
  "id": "chatcmpl-proxy-stream",
125
  "object": "chat.completion.chunk",
126
  "created": int(time.time()),
127
  "model": payload.get("model", "unknown"),
128
+ "choices": [{
129
+ "index": 0,
130
+ "delta": delta,
131
+ "finish_reason": None,
132
+ }],
 
 
133
  }
134
  yield f"data: {json.dumps(openai_chunk)}\n\n"
135
  yield "data: [DONE]\n\n"
 
140
  media_type="text/event-stream",
141
  headers={"Access-Control-Allow-Origin": "*"}
142
  )
143
+
144
+ # Non-streaming path
145
  async with httpx.AsyncClient(timeout=120) as c:
146
  r = await c.post(BYTEZ_CHAT_URL, headers=headers, json=payload)
147
  try:
148
  data = r.json()
149
  except json.JSONDecodeError:
150
  raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
151
+
152
  if "choices" not in data:
153
  content = (
154
  data.get("output")
 
166
  return JSONResponse(data, headers={"Access-Control-Allow-Origin": "*"})
167
 
168
  # ---------------------------------------------------------------------
169
+ # /v1/audio/speech → TTS (OpenAI compatible)
170
  # ---------------------------------------------------------------------
171
+ @api.post("/v1/audio/speech")
172
  async def tts(request: Request, authorization: str = Header(None)):
173
  check_key(authorization)
174
  if not BYTEZ_AUTH:
175
  raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
176
+
177
+ try:
178
+ payload = await request.json()
179
+ except Exception:
180
+ raise HTTPException(status_code=400, detail="Invalid JSON payload")
181
+
182
+ text = payload.get("input", "")
183
  if not text:
184
+ raise HTTPException(status_code=400, detail="Missing 'input' text for TTS")
185
+
186
  headers = {
187
  "Authorization": BYTEZ_AUTH,
188
  "Content-Type": "application/json",
189
  }
190
+ bytez_payload = {"text": text}
191
+
192
+ async with httpx.AsyncClient(timeout=30) as c:
193
+ try:
194
+ r = await c.post(BYTEZ_TTS_URL, headers=headers, json=bytez_payload)
195
+ except httpx.RequestError as e:
196
+ raise HTTPException(status_code=502, detail=f"TTS request failed: {str(e)}")
197
+
198
  if r.status_code != 200:
199
+ error_detail = r.text[:200] if r.text else f"Status {r.status_code}"
200
+ raise HTTPException(status_code=r.status_code, detail=f"TTS failed: {error_detail}")
201
+
202
+ # Return raw audio with proper content type
203
+ return Response(
204
+ content=r.content,
205
+ media_type="audio/mpeg",
206
+ headers={"Access-Control-Allow-Origin": "*"}
207
+ )
208
 
209
  # ---------------------------------------------------------------------
210
+ # /v1/audio/transcriptions → STT (OpenAI compatible)
211
  # ---------------------------------------------------------------------
212
+ @api.post("/v1/audio/transcriptions")
213
+ async def stt(
214
+ authorization: str = Header(None),
215
+ file: UploadFile = File(...),
216
+ model: str = Form("whisper-1")
217
+ ):
218
  check_key(authorization)
219
  if not BYTEZ_AUTH:
220
  raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
221
+
222
+ # Read audio file
223
+ try:
224
+ audio_data = await file.read()
225
+ except Exception as e:
226
+ raise HTTPException(status_code=400, detail=f"File read error: {str(e)}")
227
+
228
+ # Convert to base64 data URL
229
+ mime_type = file.content_type or "audio/wav"
230
+ audio_b64 = base64.b64encode(audio_data).decode('utf-8')
231
+ data_url = f"data:{mime_type};base64,{audio_b64}"
232
+
233
+ headers = {
234
+ "Authorization": BYTEZ_AUTH,
235
+ "Content-Type": "application/json",
236
+ }
237
+ bytez_payload = {
238
+ "url": data_url,
239
+ "model": model
240
+ }
241
+
242
+ async with httpx.AsyncClient(timeout=60) as c:
243
+ try:
244
+ r = await c.post(BYTEZ_STT_URL, headers=headers, json=bytez_payload)
245
+ except httpx.RequestError as e:
246
+ raise HTTPException(status_code=502, detail=f"STT request failed: {str(e)}")
247
+
248
+ if r.status_code != 200:
249
+ error_detail = r.text[:200] if r.text else f"Status {r.status_code}"
250
+ raise HTTPException(status_code=r.status_code, detail=f"STT failed: {error_detail}")
251
+
252
  try:
253
  data = r.json()
254
  except json.JSONDecodeError:
255
+ raise HTTPException(status_code=502, detail="Invalid STT response format")
256
+
257
+ # Extract transcript - handle multiple possible formats
258
+ transcript = (
259
+ data.get("text") or
260
+ data.get("transcript") or
261
+ data.get("result", {}).get("text", "") or
262
+ str(data)
263
+ )
264
+
265
+ return JSONResponse({"text": transcript}, headers={"Access-Control-Allow-Origin": "*"})
266
 
267
  # ---------------------------------------------------------------------
268
+ # Minimal Gradio UI
269
  # ---------------------------------------------------------------------
270
  with gr.Blocks() as ui:
271
  gr.Markdown("### ✅ Jwero Bytez → OpenAI Proxy\n"
272
+ "Endpoints:\n"
273
+ "- `/v1/models`\n"
274
+ "- `/v1/chat/completions`\n"
275
+ "- `/v1/audio/speech` (TTS)\n"
276
+ "- `/v1/audio/transcriptions` (STT)")
277
 
278
  demo = gr.mount_gradio_app(api, ui, path="/")
279