liuxin commited on
Commit
c947fe9
·
1 Parent(s): 466abca

fix: fail fast on slow backend requests

Browse files
Files changed (2) hide show
  1. README.md +4 -0
  2. app.py +108 -24
README.md CHANGED
@@ -62,5 +62,9 @@ Recommended environment variables:
62
  - `REQUEST_LOG_DIR`: optional persistent request log directory. Defaults to `/data/logs` when `/data` exists
63
  - `GRADIO_QUEUE_MAX_SIZE`: defaults to `30`
64
  - `GRADIO_DEFAULT_CONCURRENCY_LIMIT`: defaults to `6` (uses async server pool bridge for thread-safe concurrency)
 
 
 
 
65
  - `DENOISE_MAX_CONCURRENT`: defaults to `1` (limits concurrent ZipEnhancer denoise requests to avoid GPU OOM)
66
  - `GRADIO_SSR_MODE`: defaults to `false`
 
62
  - `REQUEST_LOG_DIR`: optional persistent request log directory. Defaults to `/data/logs` when `/data` exists
63
  - `GRADIO_QUEUE_MAX_SIZE`: defaults to `30`
64
  - `GRADIO_DEFAULT_CONCURRENCY_LIMIT`: defaults to `6` (uses async server pool bridge for thread-safe concurrency)
65
+ - `NANOVLLM_API_CONNECT_TIMEOUT`: defaults to `5`
66
+ - `NANOVLLM_API_ASR_TIMEOUT`: defaults to `30`
67
+ - `NANOVLLM_API_DENOISE_TIMEOUT`: defaults to `60`
68
+ - `NANOVLLM_API_GENERATE_TIMEOUT`: defaults to `120`
69
  - `DENOISE_MAX_CONCURRENT`: defaults to `1` (limits concurrent ZipEnhancer denoise requests to avoid GPU OOM)
70
  - `GRADIO_SSR_MODE`: defaults to `false`
app.py CHANGED
@@ -5,6 +5,7 @@ import os
5
  import re
6
  import sys
7
  import tempfile
 
8
  from datetime import datetime, timezone
9
  from pathlib import Path
10
  from threading import Lock
@@ -62,6 +63,21 @@ def _get_bool_env(name: str, default: bool) -> bool:
62
  raise ValueError(f"Invalid boolean env: {name}={value!r}")
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # ---------- Request Logging ----------
66
 
67
 
@@ -95,16 +111,18 @@ def _append_request_log(payload: dict) -> None:
95
  fp.write(json.dumps(record, ensure_ascii=False) + "\n")
96
 
97
 
98
- def _begin_generation_request() -> None:
99
  global _active_generation_requests
100
  with _active_generation_lock:
101
  _active_generation_requests += 1
 
102
 
103
 
104
- def _end_generation_request() -> None:
105
  global _active_generation_requests
106
  with _active_generation_lock:
107
  _active_generation_requests = max(0, _active_generation_requests - 1)
 
108
 
109
 
110
  def _get_active_generation_requests() -> int:
@@ -123,11 +141,16 @@ def _api_asr(audio_path: str) -> str:
123
  wav_b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
124
  wav_fmt = path.suffix.lstrip(".").lower() or "wav"
125
 
126
- resp = requests.post(
127
- f"{NANOVLLM_API_BASE}/asr",
128
- json={"wav_base64": wav_b64, "wav_format": wav_fmt},
129
- timeout=60,
130
- )
 
 
 
 
 
131
  resp.raise_for_status()
132
  return resp.json().get("text", "")
133
 
@@ -140,11 +163,16 @@ def _api_denoise(audio_path: str) -> str:
140
  wav_b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
141
  wav_fmt = path.suffix.lstrip(".").lower() or "wav"
142
 
143
- resp = requests.post(
144
- f"{NANOVLLM_API_BASE}/denoise",
145
- json={"wav_base64": wav_b64, "wav_format": wav_fmt},
146
- timeout=120,
147
- )
 
 
 
 
 
148
  resp.raise_for_status()
149
 
150
  denoised_b64 = resp.json()["wav_base64"]
@@ -333,19 +361,29 @@ def _api_generate(payload: dict) -> str:
333
 
334
  url = f"{NANOVLLM_API_BASE}/generate"
335
  logger.info(f"Calling {url} ...")
336
-
337
- resp = requests.post(url, json=payload, stream=True, timeout=300)
338
- resp.raise_for_status()
339
-
340
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
341
  try:
 
 
 
 
 
 
 
 
 
 
 
342
  for chunk in resp.iter_content(chunk_size=64 * 1024):
343
  tmp.write(chunk)
344
  tmp.close()
345
  return tmp.name
 
 
346
  except Exception:
347
- tmp.close()
348
- if os.path.exists(tmp.name):
 
349
  os.unlink(tmp.name)
350
  raise
351
 
@@ -372,7 +410,8 @@ def generate_tts_audio(
372
  denoise: bool = True,
373
  request: Optional[gr.Request] = None,
374
  ) -> str:
375
- _begin_generation_request()
 
376
  request_payload = {
377
  "event": "tts_request",
378
  "ui_language": _resolve_ui_language(request),
@@ -384,7 +423,14 @@ def generate_tts_audio(
384
  "do_normalize": bool(do_normalize),
385
  "denoise": bool(denoise),
386
  "has_reference_audio": bool(reference_wav_path_input and reference_wav_path_input.strip()),
 
387
  }
 
 
 
 
 
 
388
  if request_payload["has_reference_audio"]:
389
  try:
390
  request_payload["reference_audio_duration_seconds"] = round(
@@ -471,29 +517,63 @@ def generate_tts_audio(
471
  pass
472
 
473
  try:
474
- _append_request_log({**request_payload, "status": "success"})
 
 
 
475
  except Exception as exc:
476
  logger.warning(f"Failed to append request log: {exc}")
477
 
478
  return mp3_path
479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  except (ValueError, gr.Error) as exc:
 
481
  try:
482
- _append_request_log({**request_payload, "status": "rejected", "error": str(exc)})
 
 
483
  except Exception:
484
  pass
485
  if isinstance(exc, gr.Error):
486
  raise
487
  raise gr.Error(str(exc)) from exc
488
  except Exception as exc:
 
489
  logger.exception("Generation failed")
490
  try:
491
- _append_request_log({**request_payload, "status": "error", "error": str(exc)})
 
 
492
  except Exception:
493
  pass
494
  raise gr.Error(_get_i18n_text("backend_retry_error", request)) from exc
495
  finally:
496
- _end_generation_request()
 
 
 
 
 
497
 
498
 
499
  # ---------- Inline i18n (en + zh-CN) ----------
@@ -596,6 +676,8 @@ _I18N_TRANSLATIONS = {
596
  "denoise_busy_error": "Too many reference-audio enhancement requests are running. Please try again in a moment.",
597
  "denoise_failed_error": "Reference audio enhancement failed. Please try disabling denoise or use a cleaner clip.",
598
  "backend_retry_error": "The backend is temporarily unstable. Please try again in a moment.",
 
 
599
  "asr_failed_error": "ASR failed. Please fill the transcript manually or try another reference audio.",
600
  "usage_instructions": _USAGE_INSTRUCTIONS_EN,
601
  "examples_footer": _EXAMPLES_FOOTER_EN,
@@ -622,6 +704,8 @@ _I18N_TRANSLATIONS = {
622
  "denoise_busy_error": "当前参考音频降噪请求过多,请稍后再试。",
623
  "denoise_failed_error": "参考音频降噪失败,请尝试关闭降噪或更换更干净的音频。",
624
  "backend_retry_error": "后端暂时不稳定,请稍后再试。",
 
 
625
  "asr_failed_error": "ASR 识别失败,请手动填写参考音频文本,或更换一段参考音频后重试。",
626
  "usage_instructions": _USAGE_INSTRUCTIONS_ZH,
627
  "examples_footer": _EXAMPLES_FOOTER_ZH,
 
5
  import re
6
  import sys
7
  import tempfile
8
+ import time
9
  from datetime import datetime, timezone
10
  from pathlib import Path
11
  from threading import Lock
 
63
  raise ValueError(f"Invalid boolean env: {name}={value!r}")
64
 
65
 
66
+ def _api_timeout(read_timeout_env: str, default_read_timeout: float) -> tuple[float, float]:
67
+ return (
68
+ _get_float_env("NANOVLLM_API_CONNECT_TIMEOUT", 5.0),
69
+ _get_float_env(read_timeout_env, default_read_timeout),
70
+ )
71
+
72
+
73
+ class BackendBusyError(RuntimeError):
74
+ pass
75
+
76
+
77
+ class BackendTimeoutError(TimeoutError):
78
+ pass
79
+
80
+
81
  # ---------- Request Logging ----------
82
 
83
 
 
111
  fp.write(json.dumps(record, ensure_ascii=False) + "\n")
112
 
113
 
114
+ def _begin_generation_request() -> int:
115
  global _active_generation_requests
116
  with _active_generation_lock:
117
  _active_generation_requests += 1
118
+ return _active_generation_requests
119
 
120
 
121
+ def _end_generation_request() -> int:
122
  global _active_generation_requests
123
  with _active_generation_lock:
124
  _active_generation_requests = max(0, _active_generation_requests - 1)
125
+ return _active_generation_requests
126
 
127
 
128
  def _get_active_generation_requests() -> int:
 
141
  wav_b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
142
  wav_fmt = path.suffix.lstrip(".").lower() or "wav"
143
 
144
+ try:
145
+ resp = requests.post(
146
+ f"{NANOVLLM_API_BASE}/asr",
147
+ json={"wav_base64": wav_b64, "wav_format": wav_fmt},
148
+ timeout=_api_timeout("NANOVLLM_API_ASR_TIMEOUT", 30.0),
149
+ )
150
+ except requests.Timeout as exc:
151
+ raise BackendTimeoutError("ASR request timed out") from exc
152
+ if resp.status_code == 429:
153
+ raise BackendBusyError("ASR backend is busy")
154
  resp.raise_for_status()
155
  return resp.json().get("text", "")
156
 
 
163
  wav_b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
164
  wav_fmt = path.suffix.lstrip(".").lower() or "wav"
165
 
166
+ try:
167
+ resp = requests.post(
168
+ f"{NANOVLLM_API_BASE}/denoise",
169
+ json={"wav_base64": wav_b64, "wav_format": wav_fmt},
170
+ timeout=_api_timeout("NANOVLLM_API_DENOISE_TIMEOUT", 60.0),
171
+ )
172
+ except requests.Timeout as exc:
173
+ raise BackendTimeoutError("Denoise request timed out") from exc
174
+ if resp.status_code == 429:
175
+ raise BackendBusyError("Denoise backend is busy")
176
  resp.raise_for_status()
177
 
178
  denoised_b64 = resp.json()["wav_base64"]
 
361
 
362
  url = f"{NANOVLLM_API_BASE}/generate"
363
  logger.info(f"Calling {url} ...")
364
+ tmp = None
 
 
 
 
365
  try:
366
+ resp = requests.post(
367
+ url,
368
+ json=payload,
369
+ stream=True,
370
+ timeout=_api_timeout("NANOVLLM_API_GENERATE_TIMEOUT", 120.0),
371
+ )
372
+ if resp.status_code == 429:
373
+ raise BackendBusyError("Generate backend is busy")
374
+ resp.raise_for_status()
375
+
376
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
377
  for chunk in resp.iter_content(chunk_size=64 * 1024):
378
  tmp.write(chunk)
379
  tmp.close()
380
  return tmp.name
381
+ except requests.Timeout as exc:
382
+ raise BackendTimeoutError("Generate request timed out") from exc
383
  except Exception:
384
+ if tmp is not None:
385
+ tmp.close()
386
+ if tmp is not None and os.path.exists(tmp.name):
387
  os.unlink(tmp.name)
388
  raise
389
 
 
410
  denoise: bool = True,
411
  request: Optional[gr.Request] = None,
412
  ) -> str:
413
+ started_at = time.monotonic()
414
+ active_at_start = _begin_generation_request()
415
  request_payload = {
416
  "event": "tts_request",
417
  "ui_language": _resolve_ui_language(request),
 
423
  "do_normalize": bool(do_normalize),
424
  "denoise": bool(denoise),
425
  "has_reference_audio": bool(reference_wav_path_input and reference_wav_path_input.strip()),
426
+ "active_generation_requests_at_start": active_at_start,
427
  }
428
+ logger.info(
429
+ "TTS request started: active=%s, has_ref=%s, use_prompt_text=%s",
430
+ active_at_start,
431
+ request_payload["has_reference_audio"],
432
+ request_payload["use_prompt_text"],
433
+ )
434
  if request_payload["has_reference_audio"]:
435
  try:
436
  request_payload["reference_audio_duration_seconds"] = round(
 
517
  pass
518
 
519
  try:
520
+ duration_seconds = round(time.monotonic() - started_at, 3)
521
+ _append_request_log(
522
+ {**request_payload, "status": "success", "duration_seconds": duration_seconds}
523
+ )
524
  except Exception as exc:
525
  logger.warning(f"Failed to append request log: {exc}")
526
 
527
  return mp3_path
528
 
529
+ except BackendBusyError as exc:
530
+ duration_seconds = round(time.monotonic() - started_at, 3)
531
+ logger.warning("TTS backend busy after %.3fs: %s", duration_seconds, exc)
532
+ try:
533
+ _append_request_log(
534
+ {**request_payload, "status": "backend_busy", "error": str(exc), "duration_seconds": duration_seconds}
535
+ )
536
+ except Exception:
537
+ pass
538
+ raise gr.Error(_get_i18n_text("backend_busy_error", request)) from exc
539
+ except BackendTimeoutError as exc:
540
+ duration_seconds = round(time.monotonic() - started_at, 3)
541
+ logger.warning("TTS backend timeout after %.3fs: %s", duration_seconds, exc)
542
+ try:
543
+ _append_request_log(
544
+ {**request_payload, "status": "backend_timeout", "error": str(exc), "duration_seconds": duration_seconds}
545
+ )
546
+ except Exception:
547
+ pass
548
+ raise gr.Error(_get_i18n_text("backend_timeout_error", request)) from exc
549
  except (ValueError, gr.Error) as exc:
550
+ duration_seconds = round(time.monotonic() - started_at, 3)
551
  try:
552
+ _append_request_log(
553
+ {**request_payload, "status": "rejected", "error": str(exc), "duration_seconds": duration_seconds}
554
+ )
555
  except Exception:
556
  pass
557
  if isinstance(exc, gr.Error):
558
  raise
559
  raise gr.Error(str(exc)) from exc
560
  except Exception as exc:
561
+ duration_seconds = round(time.monotonic() - started_at, 3)
562
  logger.exception("Generation failed")
563
  try:
564
+ _append_request_log(
565
+ {**request_payload, "status": "error", "error": str(exc), "duration_seconds": duration_seconds}
566
+ )
567
  except Exception:
568
  pass
569
  raise gr.Error(_get_i18n_text("backend_retry_error", request)) from exc
570
  finally:
571
+ remaining = _end_generation_request()
572
+ logger.info(
573
+ "TTS request finished: duration=%.3fs, remaining_active=%s",
574
+ time.monotonic() - started_at,
575
+ remaining,
576
+ )
577
 
578
 
579
  # ---------- Inline i18n (en + zh-CN) ----------
 
676
  "denoise_busy_error": "Too many reference-audio enhancement requests are running. Please try again in a moment.",
677
  "denoise_failed_error": "Reference audio enhancement failed. Please try disabling denoise or use a cleaner clip.",
678
  "backend_retry_error": "The backend is temporarily unstable. Please try again in a moment.",
679
+ "backend_busy_error": "The backend is busy. Please try again in a moment.",
680
+ "backend_timeout_error": "The backend request timed out. Please try shorter text or reference audio.",
681
  "asr_failed_error": "ASR failed. Please fill the transcript manually or try another reference audio.",
682
  "usage_instructions": _USAGE_INSTRUCTIONS_EN,
683
  "examples_footer": _EXAMPLES_FOOTER_EN,
 
704
  "denoise_busy_error": "当前参考音频降噪请求过多,请稍后再试。",
705
  "denoise_failed_error": "参考音频降噪失败,请尝试关闭降噪或更换更干净的音频。",
706
  "backend_retry_error": "后端暂时不稳定,请稍后再试。",
707
+ "backend_busy_error": "后端当前繁忙,请稍后再试。",
708
+ "backend_timeout_error": "后端请求超时,请尝试缩短文本或参考音频后重试。",
709
  "asr_failed_error": "ASR 识别失败,请手动填写参考音频文本,或更换一段参考音频后重试。",
710
  "usage_instructions": _USAGE_INSTRUCTIONS_ZH,
711
  "examples_footer": _EXAMPLES_FOOTER_ZH,