fix(dub): keep SSE streams alive through byte-silent steps (#2108)

After transcription, several minutes of backend work could produce no
SSE bytes, causing the desktop webview to close the idle connection while
the job continued and eventually completed. This also led to a misleading
reverse-proxy error on local connections.

- dub_core: keep post-transcript awaits alive with `_ping_while` (5s pings)
- dub_export: send SSE comments after 15s of stream silence
- backendCrash.ts: show deployment-aware connection-loss guidance
- Add regression tests for post-transcript pings, task-stream keepalive,
  and local-mode error messaging

Fixes #2108
This commit is contained in:
denemon
2026-09-16 02:08:54 +09:00
parent 4e55180f70
commit 22e52c64a4
28 changed files with 291 additions and 45 deletions
+3
View File
@@ -31,6 +31,9 @@ the frozen-backend fallback mirror it for their toolchains.
- Transcribing an M4A file with PyTorch Whisper works, instead of failing with "Format not recognised" (#2042, #2039)
- PyTorch Whisper runs on 6 GB NVIDIA cards instead of falling back to CPU, because its memory check now fits the model it loads (#2044, #2041)
- MCP tools wait as long as the backend does, so a long transcription no longer fails at 120 s with an empty error (#2043, #2040)
- Dub transcription no longer drops its connection while voice references are refined after the transcript: the backend keeps the stream alive through that step, which ran silent for 19 minutes on an M1 Pro CPU (#2108)
- Task streams (dub prep, generate, audiobook) send a keepalive while a step is busy but quiet, so a long ffmpeg extract or a slow segment no longer gets the connection severed (#2108)
- A stream that ends while the backend is still running no longer blames a reverse proxy in the desktop app or dev server, where there is none; it says the connection was lost, that the job usually finishes anyway, and where to look (#2108)
### CI
+49 -22
View File
@@ -869,9 +869,30 @@ _CHUNK_TRANSCRIBE_ATTEMPTS = max(1, int(os.environ.get("OMNIVOICE_TRANSCRIBE_CHU
#: by Chrome's ~5 min no-response cap and by reverse-proxy idle timeouts,
#: which the UI can only report as the generic "stream dropped" guess.
ASR_LOAD_KEEPALIVE_S = float(os.environ.get("OMNIVOICE_ASR_LOAD_KEEPALIVE_S", "15.0"))
#: Seconds between `ping` events while a post-transcript step runs on an
#: executor (diarization, clone extraction, reference-text refinement, the
#: ASR unload / TTS restore). #2108: the per-segment refinement re-runs ASR
#: once per segment — 113 passes, ~19 min on an M1 Pro CPU — and was awaited
#: bare, so the stream went byte-silent for the whole stretch, the webview
#: severed it, and the UI could only say "stream ended early".
POST_ASR_PING_S = 5.0
_sse_event = dub_pipeline.sse_event
async def _ping_while(fut):
"""Yield `ping` events every POST_ASR_PING_S until ``fut`` settles.
Every await in the transcribe stream body that can outlast a few seconds
goes through here so the connection never goes byte-silent. The result
(or exception) stays on ``fut`` for the caller to read.
"""
while True:
done, _ = await asyncio.wait({fut}, timeout=POST_ASR_PING_S)
if done:
return
yield _sse_event("ping", {})
_prep_event_helper = dub_pipeline.prep_event # alias; we keep the module-local _prep_event below for the inline one-liner shape
#: User-facing warning emitted when auto voice cloning is skipped because the
@@ -1856,15 +1877,9 @@ async def dub_transcribe_stream(
)
fut_diar = loop.run_in_executor(_gpu_pool, _diarize)
final_segs = None
diar_warning = None
labels_source = "heuristic"
while True:
done, pending = await asyncio.wait([fut_diar], timeout=5.0)
if done:
final_segs, diar_warning, labels_source = done.pop().result()
break
yield _sse_event("ping", {})
async for _ping in _ping_while(fut_diar):
yield _ping
final_segs, diar_warning, labels_source = fut_diar.result()
if job.get("aborted") or task_manager.is_cancelled(job_id):
yield _sse_event("aborted", {})
return
@@ -1929,12 +1944,9 @@ async def dub_transcribe_stream(
labels_source=labels_source,
),
)
while True:
done, pending = await asyncio.wait([fut_clones], timeout=5.0)
if done:
clones = done.pop().result()
break
yield _sse_event("ping", {})
async for _ping in _ping_while(fut_clones):
yield _ping
clones = fut_clones.result()
if clones:
from services.speaker_clone import refine_ref_texts
# Bound the re-transcribe like every other ASR dispatch in
@@ -1944,11 +1956,14 @@ async def dub_transcribe_stream(
# and raises — keep the original (unrefined) clones, matching
# refine_ref_text's own "failure is a strict no-op" fallback.
try:
clones = await run_transcribe_guarded(
fut_refine = asyncio.ensure_future(run_transcribe_guarded(
_gpu_pool,
lambda: refine_ref_texts(clones, _asr_backend),
what="Dub clone ref-text refine",
)
))
async for _ping in _ping_while(fut_refine):
yield _ping
clones = fut_refine.result()
except ASRTimeoutError as e:
logger.warning(
"clone ref-text refine timed out; keeping original ref_text: %s", e
@@ -1971,23 +1986,29 @@ async def dub_transcribe_stream(
# reviewers, on the first version of this fix).
_seg_clone_dir = _safe_job_dir(job_id) or os.path.dirname(vocals_for_clone)
os.makedirs(_seg_clone_dir, exist_ok=True)
seg_clones = await loop.run_in_executor(
fut_seg_refs = loop.run_in_executor(
_cpu_pool, lambda: extract_segment_refs(
vocals_for_clone, final_segs,
_seg_clone_dir,
seg_ids=seg_ids_for_clone,
),
)
async for _ping in _ping_while(fut_seg_refs):
yield _ping
seg_clones = fut_seg_refs.result()
if seg_clones:
from services.speaker_clone import refine_ref_texts
# Same guard as the per-speaker refine above (#730):
# keep the original seg_clones on a wedge/timeout.
try:
seg_clones = await run_transcribe_guarded(
fut_refine = asyncio.ensure_future(run_transcribe_guarded(
_gpu_pool,
lambda: refine_ref_texts(seg_clones, _asr_backend),
what="Dub segment ref-text refine",
)
))
async for _ping in _ping_while(fut_refine):
yield _ping
seg_clones = fut_refine.result()
except ASRTimeoutError as e:
logger.warning(
"segment ref-text refine timed out; keeping original ref_text: %s", e
@@ -2042,13 +2063,19 @@ async def dub_transcribe_stream(
# (CodeRabbit review, #1198 — normal-completion half).
if _asr_backend:
try:
await loop.run_in_executor(_gpu_pool, _asr_backend.unload)
fut_unload = loop.run_in_executor(_gpu_pool, _asr_backend.unload)
async for _ping in _ping_while(fut_unload):
yield _ping
fut_unload.result()
except Exception as e:
logger.warning("Failed to unload ASR backend: %s", e)
# Unload attempted once — don't retry from gen()'s finally.
_loaded_asr["backend"] = None
await loop.run_in_executor(_cpu_pool, restore_tts_after_asr)
fut_restore = loop.run_in_executor(_cpu_pool, restore_tts_after_asr)
async for _ping in _ping_while(fut_restore):
yield _ping
fut_restore.result()
# Debt paid — don't make gen()'s finally repeat it.
_tts_offloaded["v"] = False
+12 -1
View File
@@ -45,6 +45,13 @@ def _unique_stamp() -> str:
_SAFE_LANG = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
#: Seconds of silence on a `/tasks/stream` before a keepalive comment goes out.
#: A task that is busy but quiet — ffmpeg on a long video, a slow TTS segment,
#: a job queued behind another — leaves the stream byte-silent, and byte-silent
#: SSE gets severed by the desktop webview, Chrome's ~5 min cap or a proxy's
#: idle timeout (#1196, #2108). Comments are invisible to every consumer.
TASK_STREAM_KEEPALIVE_S = 15.0
def _job_dir_or_400(job_id: str) -> str:
if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", job_id or ""):
@@ -248,7 +255,11 @@ async def stream_task(task_id: str, after_seq: int = 0):
await task_manager.add_listener(task_id, q)
try:
while True:
evt = await q.get()
try:
evt = await asyncio.wait_for(q.get(), timeout=TASK_STREAM_KEEPALIVE_S)
except asyncio.TimeoutError:
yield ": keepalive\n\n"
continue
if evt is None:
break
yield evt
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "يجري إغلاق VoiceStudio. أعد فتح التطبيق وحاول مرة أخرى.",
"crash_broken_env": "توقّف أثناء تحميل اعتمادات Python الخاصة به، فالمشكلة ليست في الذاكرة ولا في كرت الرسوميات — البيئة ناقصة أو بقيت نصف محدَّثة. استخدم «تنظيف وإعادة المحاولة» في الإعدادات ← السجلات ← الخادم الخلفي، فهو يعيد بناءها من الصفر ويصلحها في مكانها دون المساس بأصواتك أو مشاريعك. إذا استمر الفشل، فإن تفاصيل الانهيار تذكر اسم الحزمة التي تعذّر استيرادها.",
"crash_vram_default": "على وحدات معالجة الرسومات الأصغر، السبب المعتاد هو نفاد ذاكرة VRAM أثناء تحميل نموذج ASR فوق نموذج TTS: أفرغ نموذج TTS أولاً، أو اختر نموذج ASR أصغر من كتالوج النماذج ← النماذج.",
"stream_cut_backend_alive": "انتهى البث مبكرًا، لكن الخادم الخلفي ما يزال يعمل — أي أنه لم ينهر. في بيئة مقدَّمة عبر خادم أو حاويات، يكون السبب عادةً وكيلًا عكسيًا أو موزع حمل يخزّن الاتصال مؤقتًا أو ينهي مهلته: عطّل التخزين المؤقت للاستجابة على هذا المسار (nginx: proxy_buffering off; X-Accel-Buffering: no) وارفع مهلة القراءة لديه. تشغيل تطبيق سطح المكتب مباشرة، أو على localhost دون وكيل، سيؤكد ذلك."
"stream_cut_backend_alive": "انتهى البث مبكرًا، لكن الخادم الخلفي ما يزال يعمل — أي أنه لم ينهر. في بيئة مقدَّمة عبر خادم أو حاويات، يكون السبب عادةً وكيلًا عكسيًا أو موزع حمل يخزّن الاتصال مؤقتًا أو ينهي مهلته: عطّل التخزين المؤقت للاستجابة على هذا المسار (nginx: proxy_buffering off; X-Accel-Buffering: no) وارفع مهلة القراءة لديه. تشغيل تطبيق سطح المكتب مباشرة، أو على localhost دون وكيل، سيؤكد ذلك.",
"stream_cut_backend_alive_local": "انتهى البث مبكرًا، لكن الخادم الخلفي لا يزال يعمل — أي أنه لم يتعطل. فقد التطبيق اتصاله بالخادم الخلفي أثناء المهمة؛ يحدث ذلك عندما تظل خطوة طويلة صامتة لدقائق، وعادةً ما يُكمل الخادم الخلفي المهمة رغم ذلك. انتظر بضع دقائق ثم أعد فتح المهمة من السجل، أو راجع سجل الخادم الخلفي (الإعدادات → السجلات → الخادم الخلفي) لمعرفة ما كان يقوم به."
},
"keyboard": {
"title": "اختصارات لوحة المفاتيح",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio wird beendet. Öffnen Sie die App erneut und versuchen Sie es noch einmal.",
"crash_broken_env": "Er ist beim Laden seiner eigenen Python-Abhängigkeiten gestorben — es geht also weder um Speicher noch um Ihre GPU, sondern um eine unvollständige oder halb aktualisierte Umgebung. Nutzen Sie „Bereinigen & Wiederholen“ unter Einstellungen → Logs → Backend: Das baut sie von Grund auf neu und repariert sie an Ort und Stelle, ohne Ihre Stimmen oder Projekte anzurühren. Schlägt es danach weiter fehl, nennen die Absturzdetails das Paket, das sich nicht importieren ließ.",
"crash_vram_default": "Auf kleineren GPUs ist die übliche Ursache, dass beim Laden des ASR-Modells zusätzlich zum TTS-Modell der VRAM ausgeht: Entladen Sie zuerst das TTS-Modell, oder wählen Sie unter Modellkatalog → Modelle ein kleineres ASR-Modell.",
"stream_cut_backend_alive": "Der Stream endete vorzeitig, aber das Backend läuft noch — es ist also nicht abgestürzt. In einem Server- oder Container-Setup liegt das meist an einem Reverse-Proxy oder Load-Balancer, der die Verbindung puffert oder per Timeout beendet: Deaktivieren Sie das Response-Buffering für diese Route (nginx: proxy_buffering off; X-Accel-Buffering: no) und erhöhen Sie das Lese-Timeout. Wenn Sie die Desktop-App direkt oder auf localhost ohne Proxy ausführen, lässt sich das bestätigen."
"stream_cut_backend_alive": "Der Stream endete vorzeitig, aber das Backend läuft noch — es ist also nicht abgestürzt. In einem Server- oder Container-Setup liegt das meist an einem Reverse-Proxy oder Load-Balancer, der die Verbindung puffert oder per Timeout beendet: Deaktivieren Sie das Response-Buffering für diese Route (nginx: proxy_buffering off; X-Accel-Buffering: no) und erhöhen Sie das Lese-Timeout. Wenn Sie die Desktop-App direkt oder auf localhost ohne Proxy ausführen, lässt sich das bestätigen.",
"stream_cut_backend_alive_local": "Der Stream endete vorzeitig, aber das Backend läuft noch — es ist also nicht abgestürzt. Die App hat mitten im Auftrag die Verbindung zum Backend verloren; das passiert, wenn ein langer Schritt minutenlang keine Daten sendet, und meist beendet das Backend den Auftrag trotzdem. Warte ein paar Minuten und öffne den Auftrag erneut aus dem Verlauf, oder sieh im Backend-Log (Einstellungen → Logs → Backend) nach, was zuletzt lief."
},
"keyboard": {
"title": "Tastaturkürzel",
+2 -1
View File
@@ -2252,7 +2252,8 @@
"backend_shutting_down": "VoiceStudio is shutting down. Reopen the app and try again.",
"crash_broken_env": "It died while loading its own Python dependencies, so this is not about memory or your GPU — the environment is incomplete or was left half-updated. Use \"Clean & Retry\" in Settings → Logs → Backend, which rebuilds it from scratch; that repairs it in place, without touching your voices or projects. If it still fails afterwards, the crash details name the exact package that would not import.",
"crash_vram_default": "On smaller GPUs the usual cause is running out of VRAM while loading the ASR model on top of the TTS model: flush the TTS model first, or pick a smaller ASR model in the engine's Weights list in Model Catalogue.",
"stream_cut_backend_alive": "The stream ended early, but the backend is still running — so it did not crash. In a served or containerised setup this is usually a reverse proxy or load balancer buffering or timing out the connection: disable response buffering for this route (nginx: proxy_buffering off; X-Accel-Buffering: no) and raise its read timeout. Running the desktop app directly, or on localhost without a proxy, will confirm it."
"stream_cut_backend_alive": "The stream ended early, but the backend is still running — so it did not crash. In a served or containerised setup this is usually a reverse proxy or load balancer buffering or timing out the connection: disable response buffering for this route (nginx: proxy_buffering off; X-Accel-Buffering: no) and raise its read timeout. Running the desktop app directly, or on localhost without a proxy, will confirm it.",
"stream_cut_backend_alive_local": "The stream ended early, but the backend is still running — so it did not crash. The app lost its connection to the backend mid-job; that happens when a long step goes quiet for minutes, and the backend usually finishes the job anyway. Give it a few minutes and reopen the job from its history, or check the backend log (Settings → Logs → Backend) for what it was doing."
},
"crash": {
"notice": "The voice backend crashed ({{exit}}) {{ago}} ago and is being restarted automatically.",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio se está cerrando. Vuelve a abrir la aplicación e inténtalo de nuevo.",
"crash_broken_env": "Murió mientras cargaba sus propias dependencias de Python, así que no es cuestión de memoria ni de tu GPU: el entorno está incompleto o quedó a medio actualizar. Usa «Limpiar y reintentar» en Configuración → Registros → Backend, que lo reconstruye desde cero y lo repara sin tocar tus voces ni tus proyectos. Si sigue fallando, los detalles del fallo indican el paquete exacto que no se pudo importar.",
"crash_vram_default": "En GPU más pequeñas, la causa habitual es quedarse sin VRAM al cargar el modelo ASR junto con el modelo TTS: libera primero el modelo TTS de la memoria, o elige un modelo ASR más pequeño en Catálogo de modelos → Modelos.",
"stream_cut_backend_alive": "El stream terminó antes de tiempo, pero el backend sigue en ejecución — así que no se bloqueó. En una instalación servida o en contenedor, esto suele deberse a un proxy inverso o balanceador de carga que almacena en búfer la conexión o la corta por tiempo de espera: desactiva el almacenamiento en búfer de la respuesta para esta ruta (nginx: proxy_buffering off; X-Accel-Buffering: no) y aumenta su tiempo de espera de lectura. Ejecutar la aplicación de escritorio directamente, o en localhost sin proxy, lo confirmará."
"stream_cut_backend_alive": "El stream terminó antes de tiempo, pero el backend sigue en ejecución — así que no se bloqueó. En una instalación servida o en contenedor, esto suele deberse a un proxy inverso o balanceador de carga que almacena en búfer la conexión o la corta por tiempo de espera: desactiva el almacenamiento en búfer de la respuesta para esta ruta (nginx: proxy_buffering off; X-Accel-Buffering: no) y aumenta su tiempo de espera de lectura. Ejecutar la aplicación de escritorio directamente, o en localhost sin proxy, lo confirmará.",
"stream_cut_backend_alive_local": "La transmisión terminó antes de tiempo, pero el backend sigue en ejecución, así que no se bloqueó. La app perdió la conexión con el backend a mitad del trabajo; ocurre cuando un paso largo pasa minutos sin enviar datos, y normalmente el backend termina el trabajo de todos modos. Espera unos minutos y vuelve a abrir el trabajo desde el historial, o revisa el registro del backend (Ajustes → Registros → Backend) para ver qué estaba haciendo."
},
"keyboard": {
"title": "Atajos de teclado",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio est en cours de fermeture. Rouvrez lapplication et réessayez.",
"crash_broken_env": "Il est mort en chargeant ses propres dépendances Python : ce n'est donc ni la mémoire ni votre GPU, mais un environnement incomplet ou à moitié mis à jour. Utilisez « Nettoyer et réessayer » dans Paramètres → Journaux → Backend, qui le reconstruit de zéro et le répare sur place, sans toucher à vos voix ni à vos projets. Si l'échec persiste, les détails du plantage nomment le paquet qui refusait de s'importer.",
"crash_vram_default": "Sur les GPU plus modestes, la cause habituelle est un manque de VRAM lors du chargement du modèle ASR en plus du modèle TTS : déchargez d'abord le modèle TTS, ou choisissez un modèle ASR plus petit dans Catalogue de modèles → Modèles.",
"stream_cut_backend_alive": "Le flux s'est terminé prématurément, mais le backend tourne toujours — il n'a donc pas planté. Dans une installation servie ou conteneurisée, c'est généralement un reverse proxy ou un répartiteur de charge qui met la connexion en tampon ou la coupe par timeout : désactivez la mise en tampon des réponses pour cette route (nginx: proxy_buffering off; X-Accel-Buffering: no) et augmentez son délai de lecture. Lancer l'application de bureau directement, ou sur localhost sans proxy, permettra de le confirmer."
"stream_cut_backend_alive": "Le flux s'est terminé prématurément, mais le backend tourne toujours — il n'a donc pas planté. Dans une installation servie ou conteneurisée, c'est généralement un reverse proxy ou un répartiteur de charge qui met la connexion en tampon ou la coupe par timeout : désactivez la mise en tampon des réponses pour cette route (nginx: proxy_buffering off; X-Accel-Buffering: no) et augmentez son délai de lecture. Lancer l'application de bureau directement, ou sur localhost sans proxy, permettra de le confirmer.",
"stream_cut_backend_alive_local": "Le flux s'est terminé prématurément, mais le backend fonctionne toujours — il n'a donc pas planté. L'application a perdu sa connexion au backend en cours de tâche ; cela arrive quand une étape longue reste silencieuse plusieurs minutes, et le backend termine généralement la tâche malgré tout. Patientez quelques minutes puis rouvrez la tâche depuis l'historique, ou consultez le journal du backend (Paramètres → Journaux → Backend) pour voir ce qu'il faisait."
},
"keyboard": {
"title": "Raccourcis clavier",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio बंद हो रहा है। ऐप दोबारा खोलें और फिर कोशिश करें।",
"crash_broken_env": "यह अपनी ही Python निर्भरताएँ लोड करते समय बंद हो गया, इसलिए मामला मेमोरी या GPU का नहीं है — एनवायरनमेंट अधूरा है या आधा-अधूरा अपडेट रह गया। सेटिंग्स → लॉग → बैकएंड में \"साफ़ करें और पुनः प्रयास करें\" चलाएँ; यह उसे नए सिरे से बनाकर वहीं ठीक कर देता है, आपकी आवाज़ों या प्रोजेक्ट्स को छुए बिना। फिर भी विफल हो तो क्रैश विवरण उस पैकेज का नाम बताता है जो इम्पोर्ट नहीं हो पाया।",
"crash_vram_default": "छोटे GPU पर आम कारण है TTS मॉडल के ऊपर ASR मॉडल लोड करते समय VRAM का ख़त्म हो जाना: पहले TTS मॉडल को हटाएँ, या मॉडल कैटलॉग → मॉडल में कोई छोटा ASR मॉडल चुनें।",
"stream_cut_backend_alive": "स्ट्रीम जल्दी ख़त्म हो गई, लेकिन बैकएंड अभी भी चल रहा है — यानी वह क्रैश नहीं हुआ। सर्वर या कंटेनर सेटअप में इसका कारण आमतौर पर कोई रिवर्स प्रॉक्सी या लोड बैलेंसर होता है जो कनेक्शन को बफ़र करता है या टाइमआउट पर काट देता है: इस रूट के लिए रिस्पॉन्स बफ़रिंग बंद करें (nginx: proxy_buffering off; X-Accel-Buffering: no) और उसका रीड टाइमआउट बढ़ाएँ। डेस्कटॉप ऐप को सीधे चलाना, या बिना प्रॉक्सी के localhost पर चलाना, इसकी पुष्टि कर देगा।"
"stream_cut_backend_alive": "स्ट्रीम जल्दी ख़त्म हो गई, लेकिन बैकएंड अभी भी चल रहा है — यानी वह क्रैश नहीं हुआ। सर्वर या कंटेनर सेटअप में इसका कारण आमतौर पर कोई रिवर्स प्रॉक्सी या लोड बैलेंसर होता है जो कनेक्शन को बफ़र करता है या टाइमआउट पर काट देता है: इस रूट के लिए रिस्पॉन्स बफ़रिंग बंद करें (nginx: proxy_buffering off; X-Accel-Buffering: no) और उसका रीड टाइमआउट बढ़ाएँ। डेस्कटॉप ऐप को सीधे चलाना, या बिना प्रॉक्सी के localhost पर चलाना, इसकी पुष्टि कर देगा।",
"stream_cut_backend_alive_local": "स्ट्रीम समय से पहले समाप्त हो गई, लेकिन बैकएंड अभी भी चल रहा है — यानी यह क्रैश नहीं हुआ। काम के बीच में ऐप का बैकएंड से कनेक्शन टूट गया; ऐसा तब होता है जब कोई लंबा चरण कई मिनट तक कुछ नहीं भेजता, और बैकएंड आम तौर पर काम फिर भी पूरा कर देता है। कुछ मिनट रुकें और इतिहास से जॉब दोबारा खोलें, या बैकएंड लॉग (सेटिंग्स → लॉग → बैकएंड) में देखें कि वह क्या कर रहा था।"
},
"keyboard": {
"title": "कीबोर्ड शॉर्टकट",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio sedang ditutup. Buka kembali aplikasinya lalu coba lagi.",
"crash_broken_env": "Ia mati saat memuat dependensi Python-nya sendiri, jadi ini bukan soal memori atau GPU Anda — lingkungannya tidak lengkap atau tertinggal setengah diperbarui. Gunakan \"Bersihkan & Coba Lagi\" di Pengaturan → Log → Backend, yang membangunnya ulang dari nol dan memperbaikinya di tempat, tanpa menyentuh suara atau proyek Anda. Jika masih gagal, detail crash menyebutkan paket persis yang gagal diimpor.",
"crash_vram_default": "Pada GPU yang lebih kecil, penyebab umumnya adalah kehabisan VRAM saat memuat model ASR di atas model TTS: kosongkan model TTS terlebih dahulu, atau pilih model ASR yang lebih kecil di Katalog model → Model.",
"stream_cut_backend_alive": "Stream berakhir lebih awal, tetapi backend masih berjalan — jadi backend tidak mogok. Pada penyiapan server atau kontainer, ini biasanya karena reverse proxy atau load balancer yang mem-buffer atau memutus koneksi karena batas waktu: nonaktifkan buffering respons untuk rute ini (nginx: proxy_buffering off; X-Accel-Buffering: no) dan naikkan batas waktu bacanya. Menjalankan aplikasi desktop secara langsung, atau di localhost tanpa proxy, akan memastikannya."
"stream_cut_backend_alive": "Stream berakhir lebih awal, tetapi backend masih berjalan — jadi backend tidak mogok. Pada penyiapan server atau kontainer, ini biasanya karena reverse proxy atau load balancer yang mem-buffer atau memutus koneksi karena batas waktu: nonaktifkan buffering respons untuk rute ini (nginx: proxy_buffering off; X-Accel-Buffering: no) dan naikkan batas waktu bacanya. Menjalankan aplikasi desktop secara langsung, atau di localhost tanpa proxy, akan memastikannya.",
"stream_cut_backend_alive_local": "Stream berakhir lebih awal, tetapi backend masih berjalan — jadi tidak crash. Aplikasi kehilangan koneksi ke backend di tengah pekerjaan; ini terjadi saat langkah yang lama tidak mengirim apa pun selama beberapa menit, dan biasanya backend tetap menyelesaikan pekerjaannya. Tunggu beberapa menit lalu buka kembali pekerjaan dari riwayat, atau periksa log backend (Pengaturan → Log → Backend) untuk melihat apa yang sedang dikerjakannya."
},
"keyboard": {
"title": "Pintasan keyboard",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio si sta chiudendo. Riapri lapp e riprova.",
"crash_broken_env": "È morto mentre caricava le proprie dipendenze Python, quindi non c'entrano né la memoria né la GPU: l'ambiente è incompleto o è rimasto aggiornato a metà. Usa «Pulisci e riprova» in Impostazioni → Log → Backend, che lo ricostruisce da zero e lo ripara sul posto, senza toccare le tue voci o i tuoi progetti. Se continua a fallire, i dettagli del crash indicano il pacchetto che non si importava.",
"crash_vram_default": "Sulle GPU più piccole la causa più comune è l'esaurimento della VRAM quando il modello ASR viene caricato insieme al modello TTS: scarica prima il modello TTS, oppure scegli un modello ASR più piccolo in Catalogo modelli → Modelli.",
"stream_cut_backend_alive": "Lo stream è terminato in anticipo, ma il backend è ancora in esecuzione — quindi non è andato in crash. In una configurazione servita o containerizzata di solito è un reverse proxy o un load balancer che bufferizza la connessione o la interrompe per timeout: disattiva il buffering delle risposte per questa route (nginx: proxy_buffering off; X-Accel-Buffering: no) e aumenta il suo timeout di lettura. Eseguire l'app desktop direttamente, o su localhost senza proxy, lo confermerà."
"stream_cut_backend_alive": "Lo stream è terminato in anticipo, ma il backend è ancora in esecuzione — quindi non è andato in crash. In una configurazione servita o containerizzata di solito è un reverse proxy o un load balancer che bufferizza la connessione o la interrompe per timeout: disattiva il buffering delle risposte per questa route (nginx: proxy_buffering off; X-Accel-Buffering: no) e aumenta il suo timeout di lettura. Eseguire l'app desktop direttamente, o su localhost senza proxy, lo confermerà.",
"stream_cut_backend_alive_local": "Lo stream è terminato in anticipo, ma il backend è ancora in esecuzione — quindi non si è bloccato. L'app ha perso la connessione al backend a metà lavoro; succede quando un passaggio lungo resta in silenzio per minuti, e di solito il backend porta comunque a termine il lavoro. Attendi qualche minuto e riapri il lavoro dalla cronologia, oppure controlla il log del backend (Impostazioni → Log → Backend) per vedere cosa stava facendo."
},
"keyboard": {
"title": "Scorciatoie da tastiera",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio を終了しています。アプリを開き直してからもう一度お試しください。",
"crash_broken_env": "自身の Python 依存関係を読み込んでいる最中に停止しました。メモリや GPU の問題ではなく、環境が不完全か、更新が中途半端なまま残っています。設定 → ログ → バックエンド の「クリーンアップして再試行」を実行してください。環境をゼロから作り直してその場で修復し、音声やプロジェクトには手を触れません。それでも失敗する場合は、クラッシュ詳細に読み込めなかったパッケージ名が出ています。",
"crash_vram_default": "小さめの GPU では、TTS モデルを読み込んだまま ASR モデルを読み込む際に VRAM が不足するのがよくある原因です。先に TTS モデルをアンロードするか、モデルカタログ → モデル でより小さい ASR モデルを選んでください。",
"stream_cut_backend_alive": "ストリームは途中で終了しましたが、バックエンドはまだ動作しています。つまりクラッシュではありません。サーバー経由やコンテナ環境では、リバースプロキシやロードバランサーが接続をバッファリングまたはタイムアウトさせているのがよくある原因です。このルートのレスポンスバッファリングを無効にし(nginx: proxy_buffering off; X-Accel-Buffering: no)、読み取りタイムアウトを延ばしてください。デスクトップアプリを直接実行するか、プロキシなしの localhost で実行すれば確認できます。"
"stream_cut_backend_alive": "ストリームは途中で終了しましたが、バックエンドはまだ動作しています。つまりクラッシュではありません。サーバー経由やコンテナ環境では、リバースプロキシやロードバランサーが接続をバッファリングまたはタイムアウトさせているのがよくある原因です。このルートのレスポンスバッファリングを無効にし(nginx: proxy_buffering off; X-Accel-Buffering: no)、読み取りタイムアウトを延ばしてください。デスクトップアプリを直接実行するか、プロキシなしの localhost で実行すれば確認できます。",
"stream_cut_backend_alive_local": "ストリームは途中で終了しましたが、バックエンドはまだ動作しています。つまりクラッシュではありません。処理の途中でアプリとバックエンドの接続が切れました。時間のかかる工程が数分間無応答になると起こることがあり、多くの場合バックエンドは処理を最後まで続けています。数分待ってから履歴からジョブを開き直すか、バックエンドのログ(設定 → ログ → バックエンド)で直前の処理内容を確認してください。"
},
"keyboard": {
"title": "キーボードショートカット",
+2 -1
View File
@@ -2112,7 +2112,8 @@
"backend_shutting_down": "VoiceStudio를 종료하는 중입니다. 앱을 다시 열고 시도하세요.",
"crash_broken_env": "자체 Python 의존성을 불러오는 도중에 종료됐습니다. 메모리나 GPU 문제가 아니라 환경이 불완전하거나 업데이트가 중간에 멈춘 상태입니다. 설정 → 로그 → 백엔드 의 \"정리 후 재시도\"를 사용하세요. 환경을 처음부터 다시 만들어 그 자리에서 복구하며, 음성이나 프로젝트는 건드리지 않습니다. 그래도 실패하면 크래시 세부 정보에 가져오지 못한 패키지 이름이 나옵니다.",
"crash_vram_default": "작은 GPU에서는 TTS 모델이 로드된 상태에서 ASR 모델을 불러오는 동안 VRAM이 부족한 것이 흔한 원인입니다. 먼저 TTS 모델을 언로드하거나, 모델 카탈로그 → 모델에서 더 작은 ASR 모델을 선택하세요.",
"stream_cut_backend_alive": "스트림이 일찍 끝났지만 백엔드는 계속 실행 중입니다. 즉, 크래시는 아닙니다. 서버 또는 컨테이너 환경에서는 보통 리버스 프록시나 로드 밸런서가 연결을 버퍼링하거나 시간 초과시키는 것이 원인입니다. 이 경로의 응답 버퍼링을 끄고(nginx: proxy_buffering off; X-Accel-Buffering: no) 읽기 시간 제한을 늘리세요. 데스크톱 앱을 직접 실행하거나 프록시 없이 localhost에서 실행해 보면 확인할 수 있습니다."
"stream_cut_backend_alive": "스트림이 일찍 끝났지만 백엔드는 계속 실행 중입니다. 즉, 크래시는 아닙니다. 서버 또는 컨테이너 환경에서는 보통 리버스 프록시나 로드 밸런서가 연결을 버퍼링하거나 시간 초과시키는 것이 원인입니다. 이 경로의 응답 버퍼링을 끄고(nginx: proxy_buffering off; X-Accel-Buffering: no) 읽기 시간 제한을 늘리세요. 데스크톱 앱을 직접 실행하거나 프록시 없이 localhost에서 실행해 보면 확인할 수 있습니다.",
"stream_cut_backend_alive_local": "스트림이 도중에 끊겼지만 백엔드는 계속 실행 중입니다. 즉, 충돌은 아닙니다. 작업 중에 앱과 백엔드의 연결이 끊어졌습니다. 오래 걸리는 단계가 몇 분간 아무 응답도 보내지 않을 때 발생할 수 있으며, 대개 백엔드는 작업을 끝까지 마칩니다. 몇 분 기다린 뒤 기록에서 작업을 다시 열거나, 백엔드 로그(설정 → 로그 → 백엔드)에서 마지막으로 무엇을 하고 있었는지 확인하세요."
},
"keyboard": {
"title": "키보드 단축키",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio wordt afgesloten. Open de app opnieuw en probeer het nog eens.",
"crash_broken_env": "Hij stierf tijdens het laden van zijn eigen Python-afhankelijkheden, dus dit gaat niet over geheugen of je GPU — de omgeving is onvolledig of half bijgewerkt blijven staan. Gebruik \"Wissen & Opnieuw proberen\" bij Instellingen → Logs → Backend: dat bouwt hem helemaal opnieuw op en repareert hem ter plekke, zonder je stemmen of projecten aan te raken. Blijft het misgaan, dan noemen de crashdetails het pakket dat niet te importeren was.",
"crash_vram_default": "Op kleinere GPU's is de gebruikelijke oorzaak dat het VRAM-geheugen opraakt bij het laden van het ASR-model bovenop het TTS-model: ontlaad eerst het TTS-model, of kies een kleiner ASR-model in Modelcatalogus → Modellen.",
"stream_cut_backend_alive": "De stream stopte te vroeg, maar de backend draait nog — hij is dus niet gecrasht. In een server- of containeropstelling is dit meestal een reverse proxy of load balancer die de verbinding buffert of door een time-out afbreekt: schakel responsbuffering voor deze route uit (nginx: proxy_buffering off; X-Accel-Buffering: no) en verhoog de leestime-out ervan. Draai je de desktopapp direct, of op localhost zonder proxy, dan bevestigt dat het."
"stream_cut_backend_alive": "De stream stopte te vroeg, maar de backend draait nog — hij is dus niet gecrasht. In een server- of containeropstelling is dit meestal een reverse proxy of load balancer die de verbinding buffert of door een time-out afbreekt: schakel responsbuffering voor deze route uit (nginx: proxy_buffering off; X-Accel-Buffering: no) en verhoog de leestime-out ervan. Draai je de desktopapp direct, of op localhost zonder proxy, dan bevestigt dat het.",
"stream_cut_backend_alive_local": "De stream is voortijdig beëindigd, maar de backend draait nog — hij is dus niet gecrasht. De app verloor halverwege de taak de verbinding met de backend; dat gebeurt als een lange stap minutenlang niets verstuurt, en meestal rondt de backend de taak toch af. Wacht een paar minuten en open de taak opnieuw vanuit de geschiedenis, of bekijk het backend-log (Instellingen → Logs → Backend) om te zien wat hij aan het doen was."
},
"keyboard": {
"title": "Sneltoetsen",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio się zamyka. Otwórz aplikację ponownie i spróbuj jeszcze raz.",
"crash_broken_env": "Zakończył się podczas ładowania własnych zależności Pythona, więc nie chodzi o pamięć ani o kartę graficzną — środowisko jest niekompletne albo zostało zaktualizowane w połowie. Użyj „Wyczyść i ponów” w Ustawienia → Logi → Backend: odbudowuje je od zera i naprawia w miejscu, nie ruszając twoich głosów ani projektów. Jeśli nadal się nie udaje, szczegóły awarii wskazują pakiet, którego nie dało się zaimportować.",
"crash_vram_default": "Na mniejszych GPU zwykłą przyczyną jest brak pamięci VRAM podczas ładowania modelu ASR obok już załadowanego modelu TTS: najpierw zwolnij model TTS albo wybierz mniejszy model ASR w Katalogu modeli → Modele.",
"stream_cut_backend_alive": "Strumień urwał się przedwcześnie, ale backend nadal działa — a więc nie uległ awarii. W konfiguracji serwerowej lub kontenerowej zwykle oznacza to, że odwrotne proxy albo load balancer buforuje połączenie lub przerywa je po limicie czasu: wyłącz buforowanie odpowiedzi dla tej trasy (nginx: proxy_buffering off; X-Accel-Buffering: no) i zwiększ jego limit czasu odczytu. Uruchomienie aplikacji desktopowej bezpośrednio albo na localhost bez proxy pozwoli to potwierdzić."
"stream_cut_backend_alive": "Strumień urwał się przedwcześnie, ale backend nadal działa — a więc nie uległ awarii. W konfiguracji serwerowej lub kontenerowej zwykle oznacza to, że odwrotne proxy albo load balancer buforuje połączenie lub przerywa je po limicie czasu: wyłącz buforowanie odpowiedzi dla tej trasy (nginx: proxy_buffering off; X-Accel-Buffering: no) i zwiększ jego limit czasu odczytu. Uruchomienie aplikacji desktopowej bezpośrednio albo na localhost bez proxy pozwoli to potwierdzić.",
"stream_cut_backend_alive_local": "Strumień zakończył się przedwcześnie, ale backend nadal działa — więc się nie zawiesił. Aplikacja utraciła połączenie z backendem w trakcie zadania; zdarza się to, gdy długi krok przez kilka minut nic nie wysyła, a backend zwykle i tak kończy zadanie. Odczekaj kilka minut i otwórz zadanie ponownie z historii albo sprawdź w logu backendu (Ustawienia → Logi → Backend), co wtedy robił."
},
"keyboard": {
"title": "Skróty klawiaturowe",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "O VoiceStudio está sendo encerrado. Reabra o aplicativo e tente novamente.",
"crash_broken_env": "Ele morreu enquanto carregava as próprias dependências de Python, então não é questão de memória nem da sua GPU — o ambiente está incompleto ou ficou atualizado pela metade. Use \"Limpar e Repetir\" em Configurações → Logs → Backend, que o reconstrói do zero e o repara no lugar, sem tocar nas suas vozes ou projetos. Se continuar falhando, os detalhes da falha nomeiam o pacote exato que não importava.",
"crash_vram_default": "Em GPUs menores, a causa habitual é ficar sem VRAM ao carregar o modelo ASR junto com o modelo TTS: descarregue primeiro o modelo TTS, ou escolha um modelo ASR menor em Catálogo de modelos → Modelos.",
"stream_cut_backend_alive": "O stream terminou mais cedo, mas o backend continua em execução — portanto, não travou. Em uma instalação servida ou em contêiner, isso geralmente é um proxy reverso ou balanceador de carga fazendo buffer da conexão ou encerrando-a por tempo limite: desative o buffering de resposta para esta rota (nginx: proxy_buffering off; X-Accel-Buffering: no) e aumente o tempo limite de leitura. Executar o aplicativo desktop diretamente, ou em localhost sem proxy, confirmará isso."
"stream_cut_backend_alive": "O stream terminou mais cedo, mas o backend continua em execução — portanto, não travou. Em uma instalação servida ou em contêiner, isso geralmente é um proxy reverso ou balanceador de carga fazendo buffer da conexão ou encerrando-a por tempo limite: desative o buffering de resposta para esta rota (nginx: proxy_buffering off; X-Accel-Buffering: no) e aumente o tempo limite de leitura. Executar o aplicativo desktop diretamente, ou em localhost sem proxy, confirmará isso.",
"stream_cut_backend_alive_local": "A transmissão terminou antes do fim, mas o backend continua em execução — portanto não travou. O app perdeu a conexão com o backend no meio da tarefa; isso acontece quando uma etapa longa fica minutos sem enviar nada, e normalmente o backend conclui a tarefa mesmo assim. Aguarde alguns minutos e reabra a tarefa pelo histórico, ou verifique o log do backend (Configurações → Logs → Backend) para ver o que ele estava fazendo."
},
"keyboard": {
"title": "Atalhos de teclado",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio завершает работу. Откройте приложение заново и повторите попытку.",
"crash_broken_env": "Он завершился при загрузке собственных зависимостей Python, так что дело не в памяти и не в видеокарте — окружение неполное или обновилось наполовину. Используйте «Очистить и повторить» в Настройки → Логи → Бэкенд: это пересоберёт окружение с нуля и починит его на месте, не трогая ваши голоса и проекты. Если ошибка останется, в подробностях сбоя указан пакет, который не импортировался.",
"crash_vram_default": "На небольших видеокартах обычная причина — нехватка видеопамяти (VRAM) при загрузке модели ASR поверх модели TTS: сначала выгрузите модель TTS или выберите меньшую модель ASR в Каталоге моделей → Модели.",
"stream_cut_backend_alive": "Поток оборвался раньше времени, но бэкенд всё ещё работает — значит, он не падал. В серверной или контейнерной установке причиной обычно является обратный прокси или балансировщик нагрузки, который буферизует соединение или обрывает его по тайм-ауту: отключите буферизацию ответов для этого маршрута (nginx: proxy_buffering off; X-Accel-Buffering: no) и увеличьте его тайм-аут чтения. Запуск настольного приложения напрямую или на localhost без прокси подтвердит это."
"stream_cut_backend_alive": "Поток оборвался раньше времени, но бэкенд всё ещё работает — значит, он не падал. В серверной или контейнерной установке причиной обычно является обратный прокси или балансировщик нагрузки, который буферизует соединение или обрывает его по тайм-ауту: отключите буферизацию ответов для этого маршрута (nginx: proxy_buffering off; X-Accel-Buffering: no) и увеличьте его тайм-аут чтения. Запуск настольного приложения напрямую или на localhost без прокси подтвердит это.",
"stream_cut_backend_alive_local": "Поток завершился раньше времени, но бэкенд всё ещё работает — значит, он не упал. Приложение потеряло соединение с бэкендом посреди задачи; это случается, когда долгий шаг несколько минут ничего не передаёт, и обычно бэкенд всё равно доводит задачу до конца. Подождите несколько минут и снова откройте задачу из истории или посмотрите в журнале бэкенда (Настройки → Журналы → Бэкенд), что он делал."
},
"keyboard": {
"title": "Сочетания клавиш",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio stängs av. Öppna appen igen och försök på nytt.",
"crash_broken_env": "Den dog när den läste in sina egna Python-beroenden, så det handlar varken om minne eller om din GPU — miljön är ofullständig eller halvuppdaterad. Använd ”Rensa & Försök igen” under Inställningar → Loggar → Backend, som bygger om den från grunden och reparerar den på plats utan att röra dina röster eller projekt. Misslyckas det ändå anger kraschdetaljerna exakt vilket paket som inte gick att importera.",
"crash_vram_default": "På mindre GPU:er är den vanliga orsaken att VRAM-minnet tar slut när ASR-modellen läses in ovanpå TTS-modellen: ladda ur TTS-modellen först, eller välj en mindre ASR-modell under Modellkatalog → Modeller.",
"stream_cut_backend_alive": "Strömmen avbröts i förtid, men backend körs fortfarande — den kraschade alltså inte. I en serverad eller containerbaserad miljö beror det oftast på en omvänd proxy eller lastbalanserare som buffrar anslutningen eller bryter den efter en tidsgräns: stäng av svarsbuffring för den här rutten (nginx: proxy_buffering off; X-Accel-Buffering: no) och höj dess tidsgräns för läsning. Att köra skrivbordsappen direkt, eller på localhost utan proxy, bekräftar det."
"stream_cut_backend_alive": "Strömmen avbröts i förtid, men backend körs fortfarande — den kraschade alltså inte. I en serverad eller containerbaserad miljö beror det oftast på en omvänd proxy eller lastbalanserare som buffrar anslutningen eller bryter den efter en tidsgräns: stäng av svarsbuffring för den här rutten (nginx: proxy_buffering off; X-Accel-Buffering: no) och höj dess tidsgräns för läsning. Att köra skrivbordsappen direkt, eller på localhost utan proxy, bekräftar det.",
"stream_cut_backend_alive_local": "Strömmen avslutades i förtid, men backend körs fortfarande — så den kraschade inte. Appen tappade anslutningen till backend mitt i jobbet; det händer när ett långt steg är tyst i flera minuter, och backend slutför oftast jobbet ändå. Vänta några minuter och öppna jobbet igen från historiken, eller titta i backend-loggen (Inställningar → Loggar → Backend) för att se vad den höll på med."
},
"keyboard": {
"title": "Kortkommandon",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio กำลังปิดอยู่ เปิดแอปอีกครั้งแล้วลองใหม่",
"crash_broken_env": "มันหยุดทำงานขณะโหลดไลบรารี Python ของตัวเอง จึงไม่ใช่เรื่องหน่วยความจำหรือ GPU ของคุณ — สภาพแวดล้อมไม่สมบูรณ์หรืออัปเดตค้างอยู่ครึ่งทาง ใช้ \"ล้างข้อมูลและลองใหม่\" ใน การตั้งค่า → บันทึก → แบ็กเอนด์ ซึ่งจะสร้างใหม่ทั้งหมดและซ่อมให้ในที่เดิม โดยไม่แตะต้องเสียงหรือโปรเจกต์ของคุณ หากยังล้มเหลว รายละเอียดข้อขัดข้องจะระบุแพ็กเกจที่นำเข้าไม่ได้",
"crash_vram_default": "บน GPU ขนาดเล็ก สาเหตุที่พบบ่อยคือ VRAM หมดขณะโหลดโมเดล ASR ซ้อนบนโมเดล TTS ให้ล้างโมเดล TTS ออกก่อน หรือเลือกโมเดล ASR ที่เล็กกว่าใน แค็ตตาล็อกโมเดล → โมเดล",
"stream_cut_backend_alive": "สตรีมจบก่อนเวลา แต่แบ็กเอนด์ยังทำงานอยู่ จึงไม่ได้ล่ม ในการติดตั้งแบบเซิร์ฟเวอร์หรือคอนเทนเนอร์ สาเหตุมักเป็น reverse proxy หรือ load balancer ที่บัฟเฟอร์หรือตัดการเชื่อมต่อเมื่อหมดเวลา ให้ปิดการบัฟเฟอร์การตอบสนองของเส้นทางนี้ (nginx: proxy_buffering off; X-Accel-Buffering: no) และเพิ่ม read timeout ของมัน การรันแอปเดสก์ท็อปโดยตรง หรือรันบน localhost โดยไม่มีพร็อกซี จะช่วยยืนยันได้"
"stream_cut_backend_alive": "สตรีมจบก่อนเวลา แต่แบ็กเอนด์ยังทำงานอยู่ จึงไม่ได้ล่ม ในการติดตั้งแบบเซิร์ฟเวอร์หรือคอนเทนเนอร์ สาเหตุมักเป็น reverse proxy หรือ load balancer ที่บัฟเฟอร์หรือตัดการเชื่อมต่อเมื่อหมดเวลา ให้ปิดการบัฟเฟอร์การตอบสนองของเส้นทางนี้ (nginx: proxy_buffering off; X-Accel-Buffering: no) และเพิ่ม read timeout ของมัน การรันแอปเดสก์ท็อปโดยตรง หรือรันบน localhost โดยไม่มีพร็อกซี จะช่วยยืนยันได้",
"stream_cut_backend_alive_local": "สตรีมสิ้นสุดก่อนกำหนด แต่แบ็กเอนด์ยังทำงานอยู่ จึงไม่ใช่การล่ม แอปขาดการเชื่อมต่อกับแบ็กเอนด์ระหว่างงาน ซึ่งเกิดขึ้นได้เมื่อขั้นตอนที่ใช้เวลานานไม่ส่งข้อมูลใด ๆ เป็นเวลาหลายนาที และโดยปกติแบ็กเอนด์จะทำงานต่อจนเสร็จ รอสักครู่แล้วเปิดงานอีกครั้งจากประวัติ หรือดูบันทึกของแบ็กเอนด์ (การตั้งค่า → บันทึก → แบ็กเอนด์) ว่ากำลังทำอะไรอยู่"
},
"keyboard": {
"title": "แป้นพิมพ์ลัด",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio kapanıyor. Uygulamayı yeniden açıp tekrar dene.",
"crash_broken_env": "Kendi Python bağımlılıklarını yüklerken sonlandı; yani sorun bellek ya da ekran kartınız değil — ortam eksik veya yarım güncellenmiş durumda. Ayarlar → Günlükler → Arka uç bölümündeki \"Temizle ve Yeniden Dene\" seçeneğini kullanın: ortamı sıfırdan yeniden kurar ve seslerinize ya da projelerinize dokunmadan yerinde onarır. Yine de başarısız olursa, çökme ayrıntıları içe aktarılamayan paketin adını verir.",
"crash_vram_default": "Küçük GPU'larda en yaygın neden, TTS modeli yüklüyken ASR modelini de yüklerken VRAM'in tükenmesidir: önce TTS modelini bellekten kaldırın veya Model kataloğu → Modeller bölümünden daha küçük bir ASR modeli seçin.",
"stream_cut_backend_alive": "Akış erken sona erdi ama arka uç hâlâ çalışıyor — yani çökmedi. Sunucu veya konteyner kurulumlarında bunun nedeni genellikle bir ters vekilin (reverse proxy) ya da yük dengeleyicinin bağlantıyı arabelleğe alması veya zaman aşımına uğratmasıdır: bu yol için yanıt arabelleğe almayı kapatın (nginx: proxy_buffering off; X-Accel-Buffering: no) ve okuma zaman aşımını artırın. Masaüstü uygulamasını doğrudan ya da vekil olmadan localhost üzerinde çalıştırmak bunu doğrular."
"stream_cut_backend_alive": "Akış erken sona erdi ama arka uç hâlâ çalışıyor — yani çökmedi. Sunucu veya konteyner kurulumlarında bunun nedeni genellikle bir ters vekilin (reverse proxy) ya da yük dengeleyicinin bağlantıyı arabelleğe alması veya zaman aşımına uğratmasıdır: bu yol için yanıt arabelleğe almayı kapatın (nginx: proxy_buffering off; X-Accel-Buffering: no) ve okuma zaman aşımını artırın. Masaüstü uygulamasını doğrudan ya da vekil olmadan localhost üzerinde çalıştırmak bunu doğrular.",
"stream_cut_backend_alive_local": "Akış erken sona erdi ancak arka uç hâlâ çalışıyor — yani çökmedi. Uygulama, iş sürerken arka uçla bağlantısını kaybetti; bu, uzun bir adım dakikalarca hiçbir şey göndermediğinde olur ve arka uç genellikle işi yine de tamamlar. Birkaç dakika bekleyip işi geçmişten yeniden açın veya arka ucun ne yaptığını görmek için arka uç günlüğüne (Ayarlar → Günlükler → Arka Uç) bakın."
},
"keyboard": {
"title": "Klavye kısayolları",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio завершує роботу. Відкрийте застосунок знову та спробуйте ще раз.",
"crash_broken_env": "Він завершився під час завантаження власних залежностей Python, тож річ не в пам'яті й не у відеокарті — середовище неповне або оновилося наполовину. Скористайтеся «Очистити і повторити» у Налаштування → Журнали → Бекенд: це перезбирає середовище з нуля й лагодить його на місці, не торкаючись ваших голосів і проєктів. Якщо помилка лишиться, у подробицях збою вказано пакет, який не імпортувався.",
"crash_vram_default": "На невеликих відеокартах звичайна причина — брак відеопам'яті (VRAM) під час завантаження моделі ASR поверх моделі TTS: спершу вивантажте модель TTS або виберіть меншу модель ASR у Каталозі моделей → Моделі.",
"stream_cut_backend_alive": "Потік обірвався завчасно, але бекенд усе ще працює — отже, він не впав. У серверному або контейнерному розгортанні причиною зазвичай є зворотний проксі чи балансувальник навантаження, який буферизує з'єднання або розриває його за тайм-аутом: вимкніть буферизацію відповідей для цього маршруту (nginx: proxy_buffering off; X-Accel-Buffering: no) і збільште його тайм-аут читання. Запуск десктопного застосунку напряму або на localhost без проксі підтвердить це."
"stream_cut_backend_alive": "Потік обірвався завчасно, але бекенд усе ще працює — отже, він не впав. У серверному або контейнерному розгортанні причиною зазвичай є зворотний проксі чи балансувальник навантаження, який буферизує з'єднання або розриває його за тайм-аутом: вимкніть буферизацію відповідей для цього маршруту (nginx: proxy_buffering off; X-Accel-Buffering: no) і збільште його тайм-аут читання. Запуск десктопного застосунку напряму або на localhost без проксі підтвердить це.",
"stream_cut_backend_alive_local": "Потік завершився раніше, але бекенд усе ще працює — отже, він не впав. Застосунок втратив з'єднання з бекендом посеред завдання; це трапляється, коли довгий крок кілька хвилин нічого не передає, і зазвичай бекенд усе одно доводить завдання до кінця. Зачекайте кілька хвилин і знову відкрийте завдання з історії або перегляньте журнал бекенду (Налаштування → Журнали → Бекенд), щоб побачити, що він робив."
},
"keyboard": {
"title": "Комбінації клавіш",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio đang tắt. Mở lại ứng dụng rồi thử lại.",
"crash_broken_env": "Nó dừng khi đang nạp các phụ thuộc Python của chính mình, nên đây không phải chuyện bộ nhớ hay GPU — môi trường bị thiếu hoặc cập nhật dở dang. Hãy dùng \"Dọn dẹp & Thử lại\" trong Cài đặt → Nhật ký → Backend: nó dựng lại từ đầu và sửa ngay tại chỗ, không đụng đến giọng nói hay dự án của bạn. Nếu vẫn lỗi, phần chi tiết sự cố sẽ nêu đúng gói không nạp được.",
"crash_vram_default": "Trên các GPU nhỏ hơn, nguyên nhân thường gặp là hết VRAM khi tải mô hình ASR chồng lên mô hình TTS: hãy giải phóng mô hình TTS trước, hoặc chọn mô hình ASR nhỏ hơn trong Danh mục mô hình → Mô hình.",
"stream_cut_backend_alive": "Luồng kết thúc sớm, nhưng backend vẫn đang chạy — nên nó không hề sập. Trong môi trường chạy qua server hoặc container, nguyên nhân thường là reverse proxy hoặc load balancer đang đệm hoặc ngắt kết nối do hết thời gian chờ: hãy tắt đệm phản hồi cho tuyến này (nginx: proxy_buffering off; X-Accel-Buffering: no) và tăng thời gian chờ đọc của nó. Chạy trực tiếp ứng dụng desktop, hoặc chạy trên localhost không qua proxy, sẽ xác nhận điều đó."
"stream_cut_backend_alive": "Luồng kết thúc sớm, nhưng backend vẫn đang chạy — nên nó không hề sập. Trong môi trường chạy qua server hoặc container, nguyên nhân thường là reverse proxy hoặc load balancer đang đệm hoặc ngắt kết nối do hết thời gian chờ: hãy tắt đệm phản hồi cho tuyến này (nginx: proxy_buffering off; X-Accel-Buffering: no) và tăng thời gian chờ đọc của nó. Chạy trực tiếp ứng dụng desktop, hoặc chạy trên localhost không qua proxy, sẽ xác nhận điều đó.",
"stream_cut_backend_alive_local": "Luồng đã kết thúc sớm, nhưng backend vẫn đang chạy — nên đây không phải là sự cố treo. Ứng dụng đã mất kết nối với backend giữa lúc xử lý; điều này xảy ra khi một bước dài không gửi gì trong nhiều phút, và backend thường vẫn hoàn tất công việc. Hãy chờ vài phút rồi mở lại công việc từ lịch sử, hoặc xem nhật ký backend (Cài đặt → Nhật ký → Backend) để biết nó đang làm gì."
},
"keyboard": {
"title": "Phím tắt",
+2 -1
View File
@@ -2117,7 +2117,8 @@
"backend_shutting_down": "VoiceStudio 正在关闭。请重新打开应用后再试。",
"crash_broken_env": "它在加载自身的 Python 依赖时退出,因此与内存或显卡无关——运行环境不完整,或更新到一半就停下了。请在 设置 → 日志 → 后端 中使用“清理并重试”,它会从头重建环境并就地修复,不会动你的声音或项目。如果之后仍然失败,崩溃详情会指出无法导入的具体软件包。",
"crash_vram_default": "在较小的 GPU 上,常见原因是在 TTS 模型仍占用显存时加载 ASR 模型,导致显存(VRAM)不足:请先卸载 TTS 模型,或在 模型库 → 模型 中选择更小的 ASR 模型。",
"stream_cut_backend_alive": "音频流提前结束,但后端仍在运行——因此它并没有崩溃。在服务器或容器化部署中,这通常是反向代理或负载均衡器在缓冲连接或使其超时:请为此路由禁用响应缓冲(nginx: proxy_buffering off; X-Accel-Buffering: no)并调高读取超时。直接运行桌面应用,或在没有代理的 localhost 上运行,即可确认。"
"stream_cut_backend_alive": "音频流提前结束,但后端仍在运行——因此它并没有崩溃。在服务器或容器化部署中,这通常是反向代理或负载均衡器在缓冲连接或使其超时:请为此路由禁用响应缓冲(nginx: proxy_buffering off; X-Accel-Buffering: no)并调高读取超时。直接运行桌面应用,或在没有代理的 localhost 上运行,即可确认。",
"stream_cut_backend_alive_local": "串流提前结束,但后端仍在运行——因此并未崩溃。应用在任务进行中与后端断开了连接;当某个耗时步骤连续数分钟没有输出时就可能发生,而后端通常仍会把任务做完。请等几分钟后从历史记录中重新打开该任务,或查看后端日志(设置 → 日志 → 后端)了解它当时在做什么。"
},
"keyboard": {
"title": "键盘快捷键",
+2 -1
View File
@@ -1797,7 +1797,8 @@
"backend_shutting_down": "VoiceStudio 正在關閉。請重新開啟應用程式後再試。",
"crash_broken_env": "它在載入自身的 Python 相依套件時結束,因此與記憶體或顯示卡無關——執行環境不完整,或更新到一半就停住了。請在 設定 → 系統日誌 → 後端 中使用「清理並重試」,它會從頭重建環境並就地修復,不會動到你的聲音或專案。若之後仍然失敗,當機詳情會指出無法匯入的確切套件。",
"crash_vram_default": "在較小的 GPU 上,常見原因是在 TTS 模型仍載入時再載入 ASR 模型,導致顯示記憶體(VRAM)不足:請先卸載 TTS 模型,或在 模型庫 → 模型 中選擇較小的 ASR 模型。",
"stream_cut_backend_alive": "串流提前結束,但後端仍在執行——因此它並沒有當機。在伺服器或容器化部署中,這通常是反向代理或負載平衡器在緩衝連線或使其逾時:請為此路由停用回應緩衝(nginx: proxy_buffering off; X-Accel-Buffering: no)並調高讀取逾時。直接執行桌面應用程式,或在沒有代理的 localhost 上執行,即可確認。"
"stream_cut_backend_alive": "串流提前結束,但後端仍在執行——因此它並沒有當機。在伺服器或容器化部署中,這通常是反向代理或負載平衡器在緩衝連線或使其逾時:請為此路由停用回應緩衝(nginx: proxy_buffering off; X-Accel-Buffering: no)並調高讀取逾時。直接執行桌面應用程式,或在沒有代理的 localhost 上執行,即可確認。",
"stream_cut_backend_alive_local": "串流提前結束,但後端仍在執行——因此並未當機。應用程式在任務進行中與後端斷線;當某個耗時步驟連續數分鐘沒有輸出時就可能發生,而後端通常仍會把任務完成。請等幾分鐘後從歷史記錄重新開啟該任務,或查看後端日誌(設定 → 日誌 → 後端)了解它當時在做什麼。"
},
"keyboard": {
"title": "鍵盤快速鍵",
+38 -1
View File
@@ -62,7 +62,10 @@ describe('streamDropError (#1062)', () => {
});
it('says a live backend was not the crash it looked like (#1242)', async () => {
const err = await streamDropError(FALLBACK, async () => null, { probeAlive: ALIVE });
const err = await streamDropError(FALLBACK, async () => null, {
probeAlive: ALIVE,
mode: 'server',
});
// The caller's guess is dropped: the process answered, so it did not die.
expect(err.message).not.toContain(FALLBACK);
expect(err.message).toMatch(/still running/i);
@@ -328,6 +331,7 @@ describe('stream drop with no crash marker (#1242)', () => {
const err = await streamDropError(FB, async () => null, {
probeAlive: async () => true,
waitMs: 0,
mode: 'server',
});
expect(err.message).toMatch(/proxy|buffering/i);
});
@@ -360,3 +364,36 @@ describe('stream drop with no crash marker (#1242)', () => {
expect(err.message).toMatch(/memory \(RAM\)/);
});
});
// #2108 — a desktop (Tauri) user got the reverse-proxy diagnosis for a dub
// transcribe stream that died on 127.0.0.1, where no proxy can exist. The
// backend had gone byte-silent for minutes refining voice references, the
// webview severed the idle connection, and the job itself finished fine.
describe('stream drop with the backend alive, by deployment mode (#2108)', () => {
const FB = 'fallback with no cause asserted';
const alive = { probeAlive: async () => true, waitMs: 0 };
it('does not blame a proxy in the desktop shell — there is none to blame', async () => {
const err = await streamDropError(FB, async () => null, { ...alive, mode: 'desktop' });
expect(err.message).toMatch(/still running/i);
expect(err.message).not.toMatch(/proxy|buffering|nginx/i);
// Says where the evidence is, and that the job usually went on without the UI.
expect(err.message).toMatch(/Logs/);
expect(err.message).toMatch(/finishes/);
});
it('treats the dev server the same — it also talks to 127.0.0.1 directly', async () => {
const err = await streamDropError(FB, async () => null, { ...alive, mode: 'dev' });
expect(err.message).not.toMatch(/proxy|buffering/i);
});
it('detects the mode itself when the caller does not say (vitest runs as dev)', async () => {
const err = await streamDropError(FB, async () => null, alive);
expect(err.message).not.toMatch(/proxy|buffering/i);
});
it('keeps the proxy diagnosis for a served deployment', async () => {
const err = await streamDropError(FB, async () => null, { ...alive, mode: 'server' });
expect(err.message).toMatch(/proxy|buffering/i);
});
});
+20
View File
@@ -1,4 +1,5 @@
import { abortableDelay } from './abortableDelay.ts';
import { deploymentMode, type DeploymentMode } from './deploymentMode.ts';
/**
* backendCrash frontend bridge to the desktop shell's crash forensics
* (#941, src-tauri/src/crash.rs).
@@ -499,6 +500,7 @@ export async function streamDropError(
intervalMs?: number;
sleep?: (ms: number) => Promise<void>;
probeAlive?: () => Promise<boolean>;
mode?: DeploymentMode;
} = {},
): Promise<Error> {
// #1119: the shell learns the backend died from a ~2 s POLL — it must notice
@@ -526,6 +528,24 @@ export async function streamDropError(
// out the SSE connection.
const probeAlive = opts.probeAlive ?? _probeBackendAlive;
if (await probeAlive()) {
// #2108: the proxy diagnosis below is only possible where a proxy can
// exist. The desktop webview and `bun run dev` talk to 127.0.0.1
// directly; a drop there is the local connection dying under a step
// that went byte-silent for minutes — and the backend kept going (the
// reporter's transcript was saved 20 min after the UI gave up). Say
// that, and where to look, instead of handing out nginx advice.
if ((opts.mode ?? deploymentMode()) !== 'server') {
return new Error(
i18next.t('errors.stream_cut_backend_alive_local', {
defaultValue:
'The stream ended early, but the backend is still running — so it did not crash. ' +
'The app lost its connection to the backend mid-job; that happens when a long step ' +
'goes quiet for minutes, and the backend usually finishes the job anyway. Give it a ' +
'few minutes and reopen the job from its history, or check the backend log ' +
'(Settings → Logs → Backend) for what it was doing.',
}),
);
}
return new Error(
i18next.t('errors.stream_cut_backend_alive', {
defaultValue:
+75
View File
@@ -835,3 +835,78 @@ class TestTranscribeRoute:
# At least one segment boundary should land at/near the scene cut.
near_cut = [s for s in segs if abs(s["end"] - 5.5) < 0.2 or abs(s["start"] - 5.5) < 0.2]
assert near_cut, f"no segment boundary near scene cut 5.5; got {[(s['start'], s['end']) for s in segs]}"
def test_transcribe_stream_pings_while_reference_texts_refine(tmp_path, monkeypatch):
"""#2108: the work after the last chunk — diarization, clone extraction,
one ASR pass per segment to refine its reference text ran for 19 minutes
on an M1 Pro CPU with nothing on the wire. The desktop webview severed the
idle stream, the UI reported a drop (blaming a reverse proxy), and the
backend went on to finish the job unseen. Every long await in that stretch
must keep `ping`ing, at the interval POST_ASR_PING_S."""
import asyncio
import time
from api.routers import dub_core as dc
from services import speaker_clone as sc
job_id = "t_refine_ping"
audio = tmp_path / "a.wav"
_make_wav(audio, seconds=1.0)
dc._dub_jobs[job_id] = {
"audio_path": str(audio), "vocals_path": None, "scene_cuts": [],
}
fake_model = MagicMock()
fake_model._asr_pipe = MagicMock()
async def _ok_model():
return fake_model
class _FakeASR:
id = "fake"
def ensure_loaded(self):
pass
def transcribe(self, *a, **k):
return {"chunks": [{"text": "hi", "timestamp": (0.0, 0.5)}],
"segments": [], "language": "en"}
def unload(self):
pass
monkeypatch.setattr(dc, "get_model", _ok_model)
monkeypatch.setattr(
"services.asr_backend.get_active_asr_backend",
lambda *a, **k: _FakeASR(),
)
monkeypatch.setattr(dc, "offload_tts_for_asr", lambda *a, **k: None)
# raising=False: without the fix the constant does not exist, and the test
# must then fail on the assertion below, not on this line.
monkeypatch.setattr(dc, "POST_ASR_PING_S", 0.02, raising=False)
monkeypatch.setattr(
sc, "extract_segment_refs",
lambda *a, **k: {"0": {"ref_audio_path": "ref.wav", "ref_text": "hi"}},
)
def _slow_refine(refs, _backend):
time.sleep(0.3) # many pings' worth, on the executor thread like the real one
return refs
monkeypatch.setattr(sc, "refine_ref_texts", _slow_refine)
async def _collect():
resp = await dc.dub_transcribe_stream(job_id)
parts = []
async for chunk in resp.body_iterator:
parts.append(chunk.decode() if isinstance(chunk, (bytes, bytearray)) else str(chunk))
return "".join(parts)
try:
body = asyncio.run(_collect())
finally:
dc._dub_jobs.pop(job_id, None)
last_segments = body.rfind("event: segments")
final = body.rfind("event: final")
assert final > last_segments >= 0, body
quiet_stretch = body[last_segments:final]
assert "event: ping" in quiet_stretch, quiet_stretch
assert body.rfind("event: done") > final, body
+52
View File
@@ -0,0 +1,52 @@
"""#2108 class: a `/tasks/stream` that is busy but quiet must not go byte-silent.
ffmpeg on a long video, a slow TTS segment or a job queued behind another one
all leave the task stream with nothing to say for minutes, and byte-silent SSE
gets severed by the desktop webview, Chrome's ~5 min cap or a proxy's idle
timeout (#1196). SSE comment frames keep it alive and are invisible to every
consumer: EventSource, the fetch-based generate reader (`data: ` lines only),
the CLI tailer and the bench script.
"""
import asyncio
def test_quiet_task_stream_emits_keepalive_comments(monkeypatch):
from api.routers import dub_export as de
from core.tasks import task_manager
task_id = "prep_quiet"
# raising=False: without the fix the constant does not exist, and the test
# must then fail on the assertion below, not here.
monkeypatch.setattr(de, "TASK_STREAM_KEEPALIVE_S", 0.02, raising=False)
monkeypatch.setattr("core.job_store.get", lambda _id: None)
monkeypatch.setattr("core.job_store.events_since", lambda *a, **k: [])
async def _first_frames(n, budget_s=1.0):
task_manager.active_tasks[task_id] = {
"status": "running", "type": "prep", "created_at": 0.0, "history": [],
"listeners": [], "listeners_lock": asyncio.Lock(),
"error": None, "cancelled": False,
}
resp = await de.stream_task(task_id)
frames = []
async def _read():
async for chunk in resp.body_iterator:
frames.append(chunk)
if len(frames) >= n:
break
# Pre-fix the reader blocks on an empty queue forever; bound the wait
# so the failure is a clean assertion rather than a hung test.
try:
await asyncio.wait_for(_read(), timeout=budget_s)
except asyncio.TimeoutError:
pass
return frames
try:
frames = asyncio.run(_first_frames(2))
finally:
task_manager.active_tasks.pop(task_id, None)
assert frames == [": keepalive\n\n"] * 2, frames