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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -178
app.py CHANGED
@@ -1,31 +1,21 @@
1
- import os, io, json, time, tarfile, subprocess, urllib.request
2
  from pathlib import Path
3
 
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) =====
11
- LLAMA_TAG = os.environ.get("LLAMA_TAG", "") # pin e.g. "b8407"; "" = latest
12
- MODEL_REPO = os.environ.get("MODEL_REPO", "Abiray/MiniCPM5-1B-GGUF")
13
- MODEL_FILE = os.environ.get("MODEL_FILE", "minicpm5-1b-Q6_K.gguf")
14
- CTX_SIZE = int(os.environ.get("CTX_SIZE", "8192"))
15
- PARALLEL = int(os.environ.get("PARALLEL", "2"))
16
- LLAMA_PORT = int(os.environ.get("LLAMA_PORT", "8080")) # internal only
17
- API_KEY = os.environ.get("LLAMA_API_KEY", "") # SET THIS as a Space secret!
18
- SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "You are a helpful AI assistant.")
19
 
20
- # ===== 1. HOLD THE ZEROGPU HARDWARE (~5s of quota, once, at boot) =====
21
  @spaces.GPU(duration=5)
22
  def _hold_zerogpu():
23
- print("[zerogpu] host claimed; GPU slice released, CPU+RAM stay ours.")
24
  return True
25
 
26
  _hold_zerogpu()
27
 
28
- # ===== 2. READ THE "DYNAMIC" CPU/RAM WE ACTUALLY GOT (cgroup truth) =====
29
  def effective_cpus() -> int:
30
  try:
31
  quota, period = Path("/sys/fs/cgroup/cpu.max").read_text().split()[:2]
@@ -50,130 +40,8 @@ def memory_limit_gb():
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:
57
- if LLAMA_TAG:
58
- return LLAMA_TAG
59
- try:
60
- req = urllib.request.Request(
61
- "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest",
62
- headers={"User-Agent": "hf-space"})
63
- with urllib.request.urlopen(req, timeout=10) as r:
64
- return json.load(r)["tag_name"]
65
- except Exception as e:
66
- print(f"[llama.cpp] latest-tag lookup failed ({e}); pinned fallback {FALLBACK_TAG}")
67
- return FALLBACK_TAG
68
-
69
- def ensure_llama_server() -> Path:
70
- tag = resolve_tag()
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}")
77
- req = urllib.request.Request(url, headers={"User-Agent": "hf-space"})
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():
84
- f.chmod(0o755)
85
- return server
86
-
87
- # ===== 4. MODEL =====
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
-
95
- env = os.environ.copy()
96
- env["LD_LIBRARY_PATH"] = f"{server_bin.parent}:{env.get('LD_LIBRARY_PATH', '')}"
97
- env.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
98
-
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]
110
-
111
- proc = subprocess.Popen(cmd, env=env)
112
-
113
- def wait_ready(timeout=600):
114
- t0 = time.time()
115
- while time.time() - t0 < timeout:
116
- if proc.poll() is not None:
117
- raise RuntimeError(f"llama-server died early (code {proc.returncode})")
118
- try:
119
- if httpx.get(f"{UPSTREAM}/health", timeout=2).status_code == 200:
120
- print("[llama.cpp] server ready")
121
- return
122
- except Exception:
123
- pass
124
- time.sleep(1)
125
- raise TimeoutError("llama-server not ready in time")
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
-
133
- def _normalize(history):
134
- msgs = [{"role": "system", "content": SYSTEM_PROMPT}]
135
- for item in history:
136
- if isinstance(item, dict):
137
- msgs.append({"role": item["role"], "content": item["content"]})
138
- else:
139
- user, bot = item
140
- msgs.append({"role": "user", "content": user})
141
- if bot:
142
- msgs.append({"role": "assistant", "content": bot})
143
- return msgs
144
-
145
- def chat(message, history):
146
- stream = oai.chat.completions.create(
147
- model="local",
148
- messages=_normalize(history) + [{"role": "user", "content": message}],
149
- stream=True, temperature=0.7, max_tokens=1024,
150
- )
151
- out = ""
152
- for chunk in stream:
153
- delta = chunk.choices[0].delta.content
154
- if delta:
155
- out += delta
156
- yield out
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=["*"],
@@ -181,9 +49,8 @@ app.add_middleware(
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%"
@@ -209,15 +76,17 @@ def format_pitch(pitch_input):
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:
@@ -262,7 +131,6 @@ async def openai_speech(request: Request):
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():
@@ -279,37 +147,56 @@ 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()
286
- headers = {k: request.headers[k] for k in ("authorization", "content-type")
287
- if k in request.headers}
288
- wants_stream = False
289
- if body:
290
- try:
291
- wants_stream = bool(json.loads(body).get("stream"))
292
- except Exception:
293
- pass
294
-
295
- url = f"{UPSTREAM}/v1/{path}"
296
- if request.method == "POST" and wants_stream:
297
- req = _http.build_request("POST", url, content=body, headers=headers)
298
- resp = await _http.send(req, stream=True)
299
- async def gen():
300
- try:
301
- async for chunk in resp.aiter_raw():
302
- yield chunk
303
- finally:
304
- await resp.aclose()
305
- return StreamingResponse(gen(), status_code=resp.status_code,
306
- media_type="text/event-stream")
 
 
307
 
308
- resp = await _http.request(request.method, url, content=body or None, headers=headers)
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__":
 
1
+ import os, io, json
2
  from pathlib import Path
3
 
4
  import gradio as gr
5
  import spaces
 
6
  import edge_tts
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import Response, StreamingResponse, JSONResponse
 
 
 
 
 
 
 
 
10
 
11
+ # ===== 1. ZEROGPU CPU/RAM MAXIMIZER =====
12
  @spaces.GPU(duration=5)
13
  def _hold_zerogpu():
14
+ print("[zerogpu] claimed host; GPU slice released, CPU+RAM stay ours.")
15
  return True
16
 
17
  _hold_zerogpu()
18
 
 
19
  def effective_cpus() -> int:
20
  try:
21
  quota, period = Path("/sys/fs/cgroup/cpu.max").read_text().split()[:2]
 
40
  CORES = effective_cpus()
41
  print(f"[resources] effective cores: {CORES} | RAM limit: {memory_limit_gb() or '?'} GB")
42
 
43
+ # ===== 2. FASTAPI SERVER & CORS SETUP =====
44
+ app = FastAPI(title="Edge-TTS-Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  app.add_middleware(
46
  CORSMiddleware,
47
  allow_origins=["*"],
 
49
  allow_methods=["*"],
50
  allow_headers=["*"]
51
  )
 
52
 
53
+ # ===== 3. PARAMETER NORMALIZERS =====
54
  def format_rate(rate_input):
55
  if rate_input is None:
56
  return "+0%"
 
76
  except ValueError:
77
  return "+0Hz"
78
 
79
+ # ===== 4. EXTERNAL STREAMING ENDPOINTS =====
80
+
81
+ # --- Direct Audio Stream (GET & POST) ---
82
  @app.api_route("/tts", methods=["GET", "POST"])
83
  async def tts_stream(request: Request):
84
  """
85
+ Accepts:
86
+ - text (string, required)
87
+ - voice (string, default: en-US-AriaNeural)
88
+ - speed / rate (float or string, e.g. 1.2 or '+20%')
89
+ - pitch (string or int, e.g. '+5Hz' or '+10%')
90
  """
91
  if request.method == "POST":
92
  try:
 
131
  text = data.get("input", "")
132
  voice = data.get("voice", "en-US-AriaNeural")
133
  speed = data.get("speed", 1.0)
 
134
  rate = format_rate(speed)
135
 
136
  async def generate_audio():
 
147
  voices = await edge_tts.list_voices()
148
  return JSONResponse(voices)
149
 
150
+ # ===== 5. GRADIO TEST INTERFACE =====
151
+ async def gradio_tts(text, voice, speed, pitch):
152
+ if not text:
153
+ return None
154
+ rate_str = format_rate(speed)
155
+ pitch_str = format_pitch(pitch)
156
+ communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate_str, pitch=pitch_str)
157
+
158
+ buf = io.BytesIO()
159
+ async for chunk in communicate.stream():
160
+ if chunk["type"] == "audio":
161
+ buf.write(chunk["data"])
162
+ buf.seek(0)
163
+ return buf.getvalue()
164
+
165
+ DEFAULT_VOICES = [
166
+ "en-US-AriaNeural",
167
+ "en-US-ChristopherNeural",
168
+ "en-US-GuyNeural",
169
+ "en-US-JennyNeural",
170
+ "en-GB-SoniaNeural",
171
+ "en-GB-RyanNeural",
172
+ "es-ES-AlvaroNeural",
173
+ "fr-FR-DeniseNeural",
174
+ "de-DE-KatjaNeural",
175
+ "zh-CN-XiaoxiaoNeural"
176
+ ]
177
 
178
+ with gr.Blocks(title="High-Speed Edge-TTS API") as demo:
179
+ gr.Markdown(
180
+ f"# ⚡ High-Speed Edge-TTS Server\n"
181
+ f"Running on ZeroGPU Max-CPU ({CORES} Cores Allocated)\n\n"
182
+ f"**External API Endpoints:**\n"
183
+ f"- `GET / POST /tts?text=...&voice=...&speed=1.0&pitch=+0Hz`\n"
184
+ f"- `POST /v1/audio/speech` (OpenAI Compatible)\n"
185
+ f"- `GET /tts/voices` (List All Available Edge-TTS Voices)"
186
+ )
187
+ with gr.Row():
188
+ with gr.Column():
189
+ text_input = gr.Textbox(label="Text", value="Hello! This is a real-time streaming test of Edge TTS.", lines=3)
190
+ voice_dropdown = gr.Dropdown(choices=DEFAULT_VOICES, value="en-US-AriaNeural", label="Voice")
191
+ speed_slider = gr.Slider(minimum=0.5, maximum=2.0, value=1.0, step=0.1, label="Speed / Rate")
192
+ pitch_input = gr.Textbox(value="+0Hz", label="Pitch (e.g. +0Hz, +5Hz, -5Hz)")
193
+ btn = gr.Button("Generate Speech", variant="primary")
194
+ with gr.Column():
195
+ audio_output = gr.Audio(label="Audio Output", autoplay=True)
196
+
197
+ btn.click(fn=gradio_tts, inputs=[text_input, voice_dropdown, speed_slider, pitch_input], outputs=audio_output)
198
 
199
+ demo = demo.queue()
200
  app = gr.mount_gradio_app(app, demo, path="/")
201
 
202
  if __name__ == "__main__":