Merge branch 'fix/review-2138' into fix/community-integration

# Conflicts:
#	docs/dubbing/translation-engines.md
This commit is contained in:
Palash Debnath
2026-09-17 15:40:36 +05:30
5 changed files with 75 additions and 2 deletions
+1 -1
View File
@@ -1918,7 +1918,7 @@ async def dub_transcribe_stream(
"heuristic",
)
fut_diar = loop.run_in_executor(_gpu_pool, _diarize)
fut_diar = loop.run_in_executor(_gpu_pool, lambda: _asr_work.run(_diarize))
async for _ping in _ping_while(fut_diar):
yield _ping
final_segs, diar_warning, labels_source = fut_diar.result()
+4 -1
View File
@@ -291,7 +291,10 @@ async def stream_task(task_id: str, after_seq: int = 0):
finally:
await task_manager.remove_listener(task_id, q)
return StreamingResponse(_reader(), media_type="text/event-stream")
return StreamingResponse(
_reader(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"},
)
@router.get("/jobs")
+2
View File
@@ -263,3 +263,5 @@ Mixed or malformed SRT files can make a bare number indistinguishable from spoke
If the Argos native runtime cannot load, both desktop and browser clients show localized recovery guidance: reinstall the backend or select NLLB. The API returns the stable `argos_runtime_unavailable` error code without exposing native library paths.
WebVTT import separates metadata blocks from cue identifiers using the [WebVTT block-parsing rules](https://www.w3.org/TR/webvtt1/#file-parsing): a timing line immediately after an identifier makes a cue, even when that identifier is NOTE, STYLE, or REGION. Later timing examples inside metadata are ignored, and empty cues never borrow the next cues identifier as dialogue.
Dubbing transcription emits keepalives during quiet diarization, reference-refinement, and cleanup steps. Disconnecting stops queued model work; native calls already running retain their model until they finish, then cleanup restores TTS. Task streams also request that proxies disable buffering so keepalives reach the client promptly.
+66
View File
@@ -1024,3 +1024,69 @@ def test_stream_unload_is_single_shot_during_disconnect():
normal.result(timeout=5)
disconnected.result(timeout=5)
assert calls == ["unload"]
def test_disconnect_during_diarization_waits_before_unload_and_restore(tmp_path, monkeypatch):
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
from api.routers import dub_core as dc
from services import asr_backend
started, release, cleanup_started, restored = [threading.Event() for _ in range(4)]
events, guarded = [], []
original_cleanup = dc._ASRWorkLifetime.cleanup
def cleanup(lifetime, fn):
guarded.append(lifetime._lock.locked())
cleanup_started.set()
return original_cleanup(lifetime, fn)
monkeypatch.setattr(dc._ASRWorkLifetime, "cleanup", cleanup)
def diarize(**kwargs):
started.set()
assert release.wait(5)
events.append("diarization finished")
return None, None
class Backend:
id = "fake"
def ensure_loaded(self): pass
def transcribe(self, *a, **kw):
return {"chunks": [{"text": "hi", "timestamp": (0., .5)}], "segments": [], "language": "en"}
def unload(self): events.append("unloaded")
async def model():
result = MagicMock()
result._asr_pipe = MagicMock()
return result
def restore():
events.append("restored")
restored.set()
monkeypatch.setattr(dc, "get_model", model)
monkeypatch.setattr(asr_backend, "get_active_asr_backend", lambda *a, **kw: Backend())
monkeypatch.setattr(dc, "get_diarization_pipeline", diarize)
monkeypatch.setattr(dc, "offload_tts_for_asr", lambda: None)
monkeypatch.setattr(dc, "restore_tts_after_asr", restore)
monkeypatch.setattr(dc, "POST_ASR_PING_S", .01)
audio = tmp_path / "diar.wav"
_make_wav(audio, seconds=1.)
job_id = "diar_disconnect"
dc._dub_jobs[job_id] = {"audio_path": str(audio), "vocals_path": None, "scene_cuts": []}
async def scenario():
response = await dc.dub_transcribe_stream(job_id)
try:
async for _ in response.body_iterator:
if started.is_set():
break
await response.body_iterator.aclose()
assert await asyncio.to_thread(cleanup_started.wait, 5)
assert guarded == [True]
assert not restored.is_set()
finally:
release.set()
await response.body_iterator.aclose()
assert await asyncio.to_thread(restored.wait, 5)
try:
with ThreadPoolExecutor(max_workers=2) as pool:
monkeypatch.setattr(dc, "_gpu_pool", pool)
asyncio.run(asyncio.wait_for(scenario(), timeout=10))
finally:
dc._dub_jobs.pop(job_id, None)
assert events == ["diarization finished", "unloaded", "restored"]
+2
View File
@@ -28,6 +28,8 @@ def test_quiet_task_stream_emits_keepalive_comments(monkeypatch):
"error": None, "cancelled": False,
}
resp = await de.stream_task(task_id)
assert resp.headers["cache-control"] == "no-cache, no-transform"
assert resp.headers["x-accel-buffering"] == "no"
frames = []
async def _read():