Spaces:
Running on Zero
Running on Zero
Declare @spaces.GPU functions so ZeroGPU will start the Space
Browse filesZeroGPU refuses to start a Space with no decorated function - 'No @spaces.GPU
function detected during startup' - which is why hardware.current stayed None
from the first build. Move the model calls into decorated functions, since
that is the only window in which a device is actually attached, and default
to cuda when the spaces package is importable.
app.py
CHANGED
|
@@ -36,9 +36,21 @@ SAMPLE_RATE = 16000
|
|
| 36 |
CHUNK_LENGTH_S = float(os.environ.get("CHUNK_LENGTH_S", "30"))
|
| 37 |
STRIDE_LENGTH_S = float(os.environ.get("STRIDE_LENGTH_S", "5"))
|
| 38 |
MT_BATCH = int(os.environ.get("MT_BATCH", "16"))
|
| 39 |
-
#
|
| 40 |
-
#
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# Gradio owns the process on a Gradio-SDK Space, but it is a FastAPI app
|
| 44 |
# underneath — so the REST routes below are the real interface and the little UI
|
|
@@ -160,6 +172,20 @@ def _words_from_chunks(chunks, audio_secs: float):
|
|
| 160 |
return words
|
| 161 |
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
@app.post("/asr")
|
| 164 |
async def asr(request: Request, x_service_token: str = Header(default=None)):
|
| 165 |
_auth(x_service_token)
|
|
@@ -169,7 +195,6 @@ async def asr(request: Request, x_service_token: str = Header(default=None)):
|
|
| 169 |
|
| 170 |
audio = _decode(raw)
|
| 171 |
audio_secs = len(audio) / float(SAMPLE_RATE)
|
| 172 |
-
pipe = _load("asr")
|
| 173 |
|
| 174 |
# language/task are forced because a fine-tune sometimes ships a generation
|
| 175 |
# config still defaulting to English transcription, which silently produces
|
|
@@ -177,16 +202,7 @@ async def asr(request: Request, x_service_token: str = Header(default=None)):
|
|
| 177 |
gen = {"language": "ha", "task": "transcribe"}
|
| 178 |
|
| 179 |
t0 = time.time()
|
| 180 |
-
word_level =
|
| 181 |
-
try:
|
| 182 |
-
out = pipe(audio.copy(), return_timestamps="word", generate_kwargs=gen)
|
| 183 |
-
except Exception as e:
|
| 184 |
-
# Word timings need alignment_heads in the generation config. Fine-tunes
|
| 185 |
-
# frequently drop them; phrase-level timings still make usable cues.
|
| 186 |
-
print(f"word timestamps unavailable ({type(e).__name__}: {e}) — phrase level",
|
| 187 |
-
flush=True)
|
| 188 |
-
word_level = False
|
| 189 |
-
out = pipe(audio.copy(), return_timestamps=True, generate_kwargs=gen)
|
| 190 |
|
| 191 |
words = _words_from_chunks(out.get("chunks"), audio_secs)
|
| 192 |
print(f"asr {audio_secs:.1f}s audio -> {len(words)} words in {time.time() - t0:.1f}s "
|
|
@@ -205,6 +221,11 @@ class TranslateIn(BaseModel):
|
|
| 205 |
texts: list[str]
|
| 206 |
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
@app.post("/translate")
|
| 209 |
def translate(body: TranslateIn, x_service_token: str = Header(default=None)):
|
| 210 |
_auth(x_service_token)
|
|
@@ -212,13 +233,12 @@ def translate(body: TranslateIn, x_service_token: str = Header(default=None)):
|
|
| 212 |
if not texts:
|
| 213 |
return {"translations": []}
|
| 214 |
|
| 215 |
-
pipe = _load("mt")
|
| 216 |
# Blank inputs are held out and reinserted: MarianMT will happily emit a
|
| 217 |
# hallucinated sentence for an empty string.
|
| 218 |
idx = [i for i, t in enumerate(texts) if (t or "").strip()]
|
| 219 |
out = [""] * len(texts)
|
| 220 |
if idx:
|
| 221 |
-
res =
|
| 222 |
for i, r in zip(idx, res):
|
| 223 |
out[i] = (r.get("translation_text") or "").strip()
|
| 224 |
return {"translations": out}
|
|
|
|
| 36 |
CHUNK_LENGTH_S = float(os.environ.get("CHUNK_LENGTH_S", "30"))
|
| 37 |
STRIDE_LENGTH_S = float(os.environ.get("STRIDE_LENGTH_S", "5"))
|
| 38 |
MT_BATCH = int(os.environ.get("MT_BATCH", "16"))
|
| 39 |
+
# ZeroGPU hands out a GPU only inside @spaces.GPU functions, and refuses to
|
| 40 |
+
# start a Space that declares none ("No @spaces.GPU function detected during
|
| 41 |
+
# startup"). Importing spaces also tells us we are on that hardware, so the
|
| 42 |
+
# device default follows it.
|
| 43 |
+
try:
|
| 44 |
+
import spaces
|
| 45 |
+
_GPU = spaces.GPU(duration=int(os.environ.get("GPU_DURATION", "120")))
|
| 46 |
+
_ON_ZEROGPU = True
|
| 47 |
+
except Exception: # local runs, or a plain CPU host
|
| 48 |
+
_ON_ZEROGPU = False
|
| 49 |
+
|
| 50 |
+
def _GPU(fn):
|
| 51 |
+
return fn
|
| 52 |
+
|
| 53 |
+
DEVICE = os.environ.get("ASR_DEVICE", "cuda" if _ON_ZEROGPU else "cpu")
|
| 54 |
|
| 55 |
# Gradio owns the process on a Gradio-SDK Space, but it is a FastAPI app
|
| 56 |
# underneath — so the REST routes below are the real interface and the little UI
|
|
|
|
| 172 |
return words
|
| 173 |
|
| 174 |
|
| 175 |
+
@_GPU
|
| 176 |
+
def _asr_run(audio, gen):
|
| 177 |
+
"""Decoding, on the GPU when ZeroGPU has granted one."""
|
| 178 |
+
pipe = _load("asr")
|
| 179 |
+
try:
|
| 180 |
+
return pipe(audio.copy(), return_timestamps="word", generate_kwargs=gen), True
|
| 181 |
+
except Exception as e:
|
| 182 |
+
# Word timings need alignment_heads in the generation config. Fine-tunes
|
| 183 |
+
# frequently drop them; phrase-level timings still make usable cues.
|
| 184 |
+
print(f"word timestamps unavailable ({type(e).__name__}: {e}) — phrase level",
|
| 185 |
+
flush=True)
|
| 186 |
+
return pipe(audio.copy(), return_timestamps=True, generate_kwargs=gen), False
|
| 187 |
+
|
| 188 |
+
|
| 189 |
@app.post("/asr")
|
| 190 |
async def asr(request: Request, x_service_token: str = Header(default=None)):
|
| 191 |
_auth(x_service_token)
|
|
|
|
| 195 |
|
| 196 |
audio = _decode(raw)
|
| 197 |
audio_secs = len(audio) / float(SAMPLE_RATE)
|
|
|
|
| 198 |
|
| 199 |
# language/task are forced because a fine-tune sometimes ships a generation
|
| 200 |
# config still defaulting to English transcription, which silently produces
|
|
|
|
| 202 |
gen = {"language": "ha", "task": "transcribe"}
|
| 203 |
|
| 204 |
t0 = time.time()
|
| 205 |
+
out, word_level = _asr_run(audio, gen)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
words = _words_from_chunks(out.get("chunks"), audio_secs)
|
| 208 |
print(f"asr {audio_secs:.1f}s audio -> {len(words)} words in {time.time() - t0:.1f}s "
|
|
|
|
| 221 |
texts: list[str]
|
| 222 |
|
| 223 |
|
| 224 |
+
@_GPU
|
| 225 |
+
def _mt_run(texts):
|
| 226 |
+
return _load("mt")(texts, batch_size=MT_BATCH, truncation=True)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
@app.post("/translate")
|
| 230 |
def translate(body: TranslateIn, x_service_token: str = Header(default=None)):
|
| 231 |
_auth(x_service_token)
|
|
|
|
| 233 |
if not texts:
|
| 234 |
return {"translations": []}
|
| 235 |
|
|
|
|
| 236 |
# Blank inputs are held out and reinserted: MarianMT will happily emit a
|
| 237 |
# hallucinated sentence for an empty string.
|
| 238 |
idx = [i for i, t in enumerate(texts) if (t or "").strip()]
|
| 239 |
out = [""] * len(texts)
|
| 240 |
if idx:
|
| 241 |
+
res = _mt_run([texts[i] for i in idx])
|
| 242 |
for i, r in zip(idx, res):
|
| 243 |
out[i] = (r.get("translation_text") or "").strip()
|
| 244 |
return {"translations": out}
|