Spaces:
Running
Running
liuxin commited on
Commit ·
c947fe9
1
Parent(s): 466abca
fix: fail fast on slow backend requests
Browse files
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() ->
|
| 99 |
global _active_generation_requests
|
| 100 |
with _active_generation_lock:
|
| 101 |
_active_generation_requests += 1
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
-
def _end_generation_request() ->
|
| 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 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 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 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 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
|
| 348 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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,
|