From f88ca913cee986b975e9458f0fddec2d2156f550 Mon Sep 17 00:00:00 2001 From: jadssss Date: Tue, 27 Oct 2026 23:03:29 +0300 Subject: [PATCH] make long-file CUDA OOM recoverable per file - replace permanent _CUDA_BROKEN flag with CudaOOMError and retry helper - retry WhisperX with configured settings, then batch=1 without VAD, then once more - keep the loaded model cached between retries instead of reloading it - actually pass vad_filter through transcribe() and use it in aligned ASR - skip only the OOM file in sequential, staged and LRC pipelines - mark OOM files with oom_skipped / lrc+oom for the summary - fix staged vocal separation writing input_dir/vocals.wav when convert is off - route original inputs and resumed files through safe per-file separation - validate ffmpeg before starting the LRC task --- core/lrc_pipeline.py | 6 +++- core/pipeline.py | 33 +++++++++++++++-- core/transcribe.py | 86 ++++++++++++++++++++++---------------------- gui/main_window.py | 8 +++++ 4 files changed, 86 insertions(+), 47 deletions(-) diff --git a/core/lrc_pipeline.py b/core/lrc_pipeline.py index cc6d406..0e75d4c 100644 --- a/core/lrc_pipeline.py +++ b/core/lrc_pipeline.py @@ -14,7 +14,7 @@ from .metadata import read_metadata from .pipeline import ( FileResult, _artifact_belongs_to, _save_unrecognized, collect_audio_files, ) -from .transcribe import transcribe_aligned +from .transcribe import CudaOOMError, transcribe_aligned LogFn = Callable[[str], None] @@ -155,6 +155,10 @@ def run_lrc_pipeline( metadata=read_metadata(f), ) results.append(FileResult(f, None, track_out, None, "lrc")) + except CudaOOMError as e: + error = f"CudaOOMError: {e}" + log(f" ⊘ {f.name}: OOM — пропущен, идём дальше") + results.append(FileResult(f, None, None, error, "lrc+oom")) except Exception as e: # noqa: BLE001 error = f"{type(e).__name__}: {e}" log(f" ✗ Ошибка: {error}") diff --git a/core/pipeline.py b/core/pipeline.py index 0ac9209..641892b 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -18,7 +18,7 @@ from .lyrics import LyricsMatch, search_by_identity, search_by_text from .metadata import TrackMetadata, read_metadata from .recognize import ShazamResult, recognize from .separate import separate_vocals, separate_vocals_batch -from .transcribe import transcribe +from .transcribe import CudaOOMError, transcribe from .lrc import is_lrc, read_embedded_lyrics @@ -729,6 +729,10 @@ def _run_sequential( log("⏹ Отменено") was_cancelled = True break + except CudaOOMError as e: + error = f"CudaOOMError: {e}" + source = "oom_skipped" + log(f" ⊘ {f.name}: OOM после 3 попыток — пропущен, идём дальше") except Exception as e: # noqa: BLE001 error = f"{type(e).__name__}: {e}" log(f" ✗ Ошибка: {error}") @@ -1010,7 +1014,22 @@ def _run_staged_batch( def separate_single(f: Path) -> None: try: src = state[f]["audio_path"] - target = src.parent / "vocals.wav" + # Never write next to the original input: with + # convert.enabled=False src == f, so every track would + # overwrite input_dir/vocals.wav. Only reuse the track + # output folder when the source already lives there. + track_dir = output_root / f.stem + if src.parent == track_dir: + # Already in the persistent track folder (resume). + target = src.parent / "vocals.wav" + elif src.parent == f.parent: + # src is the original input file (convert disabled). + # Never write next to it: all tracks share input_dir + # and would overwrite input_dir/vocals.wav. + target = _mkdtemp("at_sep_") / "vocals.wav" + else: + # Freshly converted file in its own unique temp dir. + target = src.parent / "vocals.wav" with tempfile.TemporaryDirectory() as tmp: raw = separate_vocals( src, Path(tmp), model=config.separate.model, @@ -1027,11 +1046,15 @@ def _run_staged_batch( # Resumed convert.wav files already live in output_root/, # while freshly converted files live in unique temp dirs. + # Original inputs (convert disabled) must also go through the + # one-by-one path: batching them shares input_dir and can + # collide on vocals.wav. # unblend's `{parent}` batch layout would mix those roots, so - # separate resumed files one by one and batch the rest. + # separate resumed/original files one by one and batch the rest. resumed = [ f for f in todo if state[f]["audio_path"].parent == output_root / f.stem + or state[f]["audio_path"].parent == f.parent ] resumed_set = set(resumed) batchable = [f for f in todo if f not in resumed_set] @@ -1111,6 +1134,10 @@ def _run_staged_batch( state[f]["text"] = text state[f]["source"] = config.transcribe.engine log(f" ✓ {f.name} ({len(text)} символов)") + except CudaOOMError as e: + state[f]["error"] = f"CudaOOMError: {e}" + state[f]["source"] = "oom_skipped" + log(f" ⊘ {f.name}: OOM — пропущен") except Exception as e: # noqa: BLE001 state[f]["error"] = f"{type(e).__name__}: {e}" log(f" ✗ {f.name}: {e}") diff --git a/core/transcribe.py b/core/transcribe.py index 33bf795..ded5ea2 100644 --- a/core/transcribe.py +++ b/core/transcribe.py @@ -11,10 +11,8 @@ from pathlib import Path from typing import Optional -# Set permanently once the CUDA context has died (e.g. after an unrecoverable -# OOM). Every later CUDA transcription fails instantly instead of re-loading -# the model forever and hanging the pipeline. -_CUDA_BROKEN = False +class CudaOOMError(RuntimeError): + """CUDA OOM, переживший все стратегии понижения нагрузки.""" def _is_cuda_error(exc: BaseException) -> bool: @@ -28,14 +26,6 @@ def _is_cuda_error(exc: BaseException) -> bool: ) -def _check_cuda_usable(device: str) -> None: - if device == "cuda" and _CUDA_BROKEN: - raise RuntimeError( - "CUDA context is broken (previous OOM was unrecoverable). " - "Restart the app or switch device to CPU." - ) - - def _resolve_device(device: str) -> str: if device in ("cuda", "cpu"): return device @@ -68,6 +58,36 @@ def _free_vram() -> None: pass +def _model_transcribe_with_retry( + model_obj, audio, language, batch_size, vad_filter, name="