Unmid commited on
Commit
ffec431
·
verified ·
1 Parent(s): b335c06

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -20
app.py CHANGED
@@ -4,6 +4,7 @@ from pathlib import Path
4
  import gradio as gr
5
  import spaces
6
  import httpx
 
7
  from huggingface_hub import hf_hub_download
8
 
9
  # ===== 0. CONFIG (override in Space Settings → Variables and secrets) =====
@@ -49,7 +50,7 @@ def memory_limit_gb():
49
  CORES = effective_cpus()
50
  print(f"[resources] effective cores: {CORES} | RAM limit: {memory_limit_gb() or '?'} GB")
51
 
52
- # ===== 3. FETCH PREBUILT llama.cpp (seconds, zero compilation) =====
53
  FALLBACK_TAG = "b8407"
54
 
55
  def resolve_tag() -> str:
@@ -70,7 +71,6 @@ def ensure_llama_server() -> Path:
70
  root = Path.home() / ".cache" / "llamacpp" / tag
71
  server = root / "build" / "bin" / "llama-server"
72
  if not server.exists():
73
- # llama.cpp Linux binaries are now distributed as .tar.gz (not .zip)
74
  url = (f"https://github.com/ggml-org/llama.cpp/releases/download/"
75
  f"{tag}/llama-{tag}-bin-ubuntu-x64.tar.gz")
76
  print(f"[llama.cpp] downloading {url}")
@@ -78,7 +78,6 @@ def ensure_llama_server() -> Path:
78
  with urllib.request.urlopen(req, timeout=180) as r:
79
  blob = r.read()
80
  root.mkdir(parents=True, exist_ok=True)
81
- # Extract tar.gz archive
82
  with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as t:
83
  t.extractall(root)
84
  for f in (root / "build" / "bin").iterdir():
@@ -89,7 +88,7 @@ def ensure_llama_server() -> Path:
89
  print(f"[model] downloading {MODEL_REPO}/{MODEL_FILE}")
90
  model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
91
 
92
- # ===== 5. LAUNCH llama-server, SIZED TO THE DETECTED HARDWARE =====
93
  server_bin = ensure_llama_server()
94
  UPSTREAM = f"http://127.0.0.1:{LLAMA_PORT}"
95
 
@@ -100,11 +99,11 @@ env.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
100
  cmd = [
101
  str(server_bin), "-m", model_path,
102
  "--host", "127.0.0.1", "--port", str(LLAMA_PORT),
103
- "-t", str(CORES), "-tb", str(CORES), # every core the cgroup gives us
104
  "-c", str(CTX_SIZE),
105
- "-np", str(PARALLEL), # real concurrent slots, no global lock
106
- "-fa", # flash attention → much smaller KV cache
107
- "--cache-reuse", "256", # KV reuse across chat turns
108
  ]
109
  if API_KEY:
110
  cmd += ["--api-key", API_KEY]
@@ -127,7 +126,7 @@ def wait_ready(timeout=600):
127
 
128
  wait_ready()
129
 
130
- # ===== 6. GRADIO UI (same backend as the API) =====
131
  from openai import OpenAI
132
  oai = OpenAI(base_url=f"{UPSTREAM}/v1", api_key=API_KEY or "not-needed")
133
 
@@ -158,25 +157,129 @@ def chat(message, history):
158
 
159
  with gr.Blocks() as demo:
160
  gr.Markdown(
161
- f"# ⚡ llama.cpp on ZeroGPU's free CPU\n"
162
- f"`{MODEL_REPO}` · {CORES} threads · ctx {CTX_SIZE} · {PARALLEL} parallel slots\n\n"
163
- f"**External API:** `POST /v1/chat/completions` on this Space's `*.hf.space` URL."
 
 
 
164
  )
165
- gr.ChatInterface(fn=chat, examples=["Who are you?",
166
- "Write a python script to reverse a string.", "Explain quantum computing."])
167
 
168
  demo = demo.queue()
169
 
170
- # ===== 7. PUBLIC /v1 PROXY the supported way, no monkey-patch =====
171
  from fastapi import FastAPI, Request
172
  from fastapi.middleware.cors import CORSMiddleware
173
- from fastapi.responses import Response, StreamingResponse
174
 
175
- app = FastAPI(title="llama-cpp-proxy")
176
- app.add_middleware(CORSMiddleware, allow_origins=["*"],
177
- allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
178
  _http = httpx.AsyncClient(timeout=httpx.Timeout(None))
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  @app.api_route("/v1/{path:path}", methods=["GET", "POST"])
181
  async def v1_proxy(path: str, request: Request):
182
  body = await request.body()
@@ -206,7 +309,8 @@ async def v1_proxy(path: str, request: Request):
206
  return Response(content=resp.content, status_code=resp.status_code,
207
  media_type=resp.headers.get("content-type", "application/json"))
208
 
209
- app = gr.mount_gradio_app(app, demo, path="/") # mounted LAST so /v1/* wins
 
210
 
211
  if __name__ == "__main__":
212
  import uvicorn
 
4
  import gradio as gr
5
  import spaces
6
  import httpx
7
+ import edge_tts
8
  from huggingface_hub import hf_hub_download
9
 
10
  # ===== 0. CONFIG (override in Space Settings → Variables and secrets) =====
 
50
  CORES = effective_cpus()
51
  print(f"[resources] effective cores: {CORES} | RAM limit: {memory_limit_gb() or '?'} GB")
52
 
53
+ # ===== 3. FETCH PREBUILT llama.cpp =====
54
  FALLBACK_TAG = "b8407"
55
 
56
  def resolve_tag() -> str:
 
71
  root = Path.home() / ".cache" / "llamacpp" / tag
72
  server = root / "build" / "bin" / "llama-server"
73
  if not server.exists():
 
74
  url = (f"https://github.com/ggml-org/llama.cpp/releases/download/"
75
  f"{tag}/llama-{tag}-bin-ubuntu-x64.tar.gz")
76
  print(f"[llama.cpp] downloading {url}")
 
78
  with urllib.request.urlopen(req, timeout=180) as r:
79
  blob = r.read()
80
  root.mkdir(parents=True, exist_ok=True)
 
81
  with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as t:
82
  t.extractall(root)
83
  for f in (root / "build" / "bin").iterdir():
 
88
  print(f"[model] downloading {MODEL_REPO}/{MODEL_FILE}")
89
  model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
90
 
91
+ # ===== 5. LAUNCH llama-server =====
92
  server_bin = ensure_llama_server()
93
  UPSTREAM = f"http://127.0.0.1:{LLAMA_PORT}"
94
 
 
99
  cmd = [
100
  str(server_bin), "-m", model_path,
101
  "--host", "127.0.0.1", "--port", str(LLAMA_PORT),
102
+ "-t", str(CORES), "-tb", str(CORES),
103
  "-c", str(CTX_SIZE),
104
+ "-np", str(PARALLEL),
105
+ "-fa",
106
+ "--cache-reuse", "256",
107
  ]
108
  if API_KEY:
109
  cmd += ["--api-key", API_KEY]
 
126
 
127
  wait_ready()
128
 
129
+ # ===== 6. GRADIO UI =====
130
  from openai import OpenAI
131
  oai = OpenAI(base_url=f"{UPSTREAM}/v1", api_key=API_KEY or "not-needed")
132
 
 
157
 
158
  with gr.Blocks() as demo:
159
  gr.Markdown(
160
+ f"# ⚡ llama.cpp + Edge-TTS API on ZeroGPU's CPU\n"
161
+ f"`{MODEL_REPO}` · {CORES} threads · ctx {CTX_SIZE}\n\n"
162
+ f"**Endpoints Available:**\n"
163
+ f"- `POST /v1/chat/completions` (LLM Chat)\n"
164
+ f"- `GET/POST /tts` (Microsoft Edge Text-to-Speech Stream)\n"
165
+ f"- `GET /tts/voices` (List Edge TTS Voices)"
166
  )
167
+ gr.ChatInterface(fn=chat, examples=["Who are you?", "Tell me a fast joke."])
 
168
 
169
  demo = demo.queue()
170
 
171
+ # ===== 7. FASTAPI PROXY & EDGE-TTS ENDPOINTS =====
172
  from fastapi import FastAPI, Request
173
  from fastapi.middleware.cors import CORSMiddleware
174
+ from fastapi.responses import Response, StreamingResponse, JSONResponse
175
 
176
+ app = FastAPI(title="llama-cpp-edgetts-proxy")
177
+ app.add_middleware(
178
+ CORSMiddleware,
179
+ allow_origins=["*"],
180
+ allow_credentials=True,
181
+ allow_methods=["*"],
182
+ allow_headers=["*"]
183
+ )
184
  _http = httpx.AsyncClient(timeout=httpx.Timeout(None))
185
 
186
+ # Helper functions to normalize speed and pitch for EdgeTTS
187
+ def format_rate(rate_input):
188
+ if rate_input is None:
189
+ return "+0%"
190
+ val = str(rate_input).strip()
191
+ if val.endswith("%"):
192
+ return val if val.startswith(("+", "-")) else f"+{val}"
193
+ try:
194
+ f = float(val)
195
+ pct = int(round((f - 1.0) * 100))
196
+ return f"+{pct}%" if pct >= 0 else f"{pct}%"
197
+ except ValueError:
198
+ return "+0%"
199
+
200
+ def format_pitch(pitch_input):
201
+ if pitch_input is None:
202
+ return "+0Hz"
203
+ val = str(pitch_input).strip()
204
+ if val.endswith("Hz") or val.endswith("%"):
205
+ return val if val.startswith(("+", "-")) else f"+{val}"
206
+ try:
207
+ val_int = int(val)
208
+ return f"+{val_int}Hz" if val_int >= 0 else f"{val_int}Hz"
209
+ except ValueError:
210
+ return "+0Hz"
211
+
212
+ # --- Edge-TTS Stream Endpoint ---
213
+ @app.api_route("/tts", methods=["GET", "POST"])
214
+ async def tts_stream(request: Request):
215
+ """
216
+ Query or JSON Parameters:
217
+ - text: text to read
218
+ - voice: e.g., 'en-US-AriaNeural' or 'en-US-ChristopherNeural'
219
+ - speed / rate: e.g. 1.2 or '+20%'
220
+ - pitch: e.g. '+0Hz' or '+5%'
221
+ """
222
+ if request.method == "POST":
223
+ try:
224
+ data = await request.json()
225
+ except Exception:
226
+ data = {}
227
+ else:
228
+ data = dict(request.query_params)
229
+
230
+ text = data.get("text", "")
231
+ if not text:
232
+ return JSONResponse({"error": "Missing 'text' parameter"}, status_code=400)
233
+
234
+ voice = data.get("voice", "en-US-AriaNeural")
235
+ raw_rate = data.get("rate") or data.get("speed")
236
+ rate = format_rate(raw_rate)
237
+ pitch = format_pitch(data.get("pitch"))
238
+
239
+ async def generate_audio():
240
+ communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate, pitch=pitch)
241
+ async for chunk in communicate.stream():
242
+ if chunk["type"] == "audio":
243
+ yield chunk["data"]
244
+
245
+ return StreamingResponse(
246
+ generate_audio(),
247
+ media_type="audio/mpeg",
248
+ headers={
249
+ "Cache-Control": "no-cache",
250
+ "Content-Disposition": "inline; filename=tts.mp3"
251
+ }
252
+ )
253
+
254
+ # --- OpenAI Audio Speech Compatible Endpoint ---
255
+ @app.api_route("/v1/audio/speech", methods=["POST"])
256
+ async def openai_speech(request: Request):
257
+ try:
258
+ data = await request.json()
259
+ except Exception:
260
+ data = {}
261
+
262
+ text = data.get("input", "")
263
+ voice = data.get("voice", "en-US-AriaNeural")
264
+ speed = data.get("speed", 1.0)
265
+
266
+ rate = format_rate(speed)
267
+
268
+ async def generate_audio():
269
+ communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate)
270
+ async for chunk in communicate.stream():
271
+ if chunk["type"] == "audio":
272
+ yield chunk["data"]
273
+
274
+ return StreamingResponse(generate_audio(), media_type="audio/mpeg")
275
+
276
+ # --- Get All Available Voices ---
277
+ @app.get("/tts/voices")
278
+ async def list_voices():
279
+ voices = await edge_tts.list_voices()
280
+ return JSONResponse(voices)
281
+
282
+ # --- Proxy LLM Chat Completions ---
283
  @app.api_route("/v1/{path:path}", methods=["GET", "POST"])
284
  async def v1_proxy(path: str, request: Request):
285
  body = await request.body()
 
309
  return Response(content=resp.content, status_code=resp.status_code,
310
  media_type=resp.headers.get("content-type", "application/json"))
311
 
312
+ # Mount Gradio UI
313
+ app = gr.mount_gradio_app(app, demo, path="/")
314
 
315
  if __name__ == "__main__":
316
  import uvicorn