Skip to content
Merged
25 changes: 25 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,23 @@ def _env_path_opt(name: str) -> Path | None:
return Path(raw).expanduser().resolve() if raw else None


# Why torch could not be loaded at the last probe, or None if it loaded (#730).
# Refreshed on every probe rather than decided once, so a repaired install
# clears it without a restart.
_torch_load_error: str | None = None


def torch_load_error() -> str | None:
"""The last probe's reason torch is installed but unusable, if any."""
return _torch_load_error


def available_torch_devices() -> list[str]:
"""Compute devices this machine can actually use, best-first. CPU is always
present; cuda/mps depend on the hardware + installed torch build. The
Settings UI uses this to disable options that aren't available/detected so
a user can't pick an impossible device."""
global _torch_load_error
devices: list[str] = []
try:
import torch
Expand All @@ -41,8 +53,21 @@ def available_torch_devices() -> list[str]:
devices.append("cuda")
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
devices.append("mps")
_torch_load_error = None
except ImportError:
pass
except Exception as exc: # noqa: BLE001 -- any failure here must not stop the server
# Installed but unloadable: on Windows a CUDA DLL left behind by a
# half-reverted install raises OSError (WinError 127) from torch's own
# DLL loader (#723). Catching only ImportError let that kill startup
# and every /api/settings call. The server has to come up so it can
# say what is wrong. Logged loudly once per distinct cause, because
# separation cannot run on any device until torch is repaired, CPU
# included, and a quiet fallback would hide exactly that.
reason = f"{type(exc).__name__}: {exc}"
if reason != _torch_load_error:
logger.error("torch is installed but could not be loaded: %s", reason, exc_info=True)
_torch_load_error = reason
devices.append("cpu")
return devices

Expand Down
4 changes: 4 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
available_torch_devices,
configure_portable_environment,
ensure_runtime_dirs,
torch_load_error,
)
from app.core.logging_setup import configure_logging
from app.core.process import process_exists as _process_exists
Expand Down Expand Up @@ -393,6 +394,9 @@ def _settings_payload() -> dict[str, object]:
"demucs_device": get_demucs_device_choice(),
"demucs_device_resolved": get_demucs_device(),
"demucs_devices_available": available_torch_devices(),
# Set when torch is installed but cannot load, which stops separation
# on every device (#730). Read after the probe above refreshed it.
"torch_error": torch_load_error(),
}


Expand Down
352 changes: 292 additions & 60 deletions app/pipeline/analyze.py

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion app/pipeline/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from app.core.redact import redact
from app.core.registry import is_upload, set_proc
from app.core.registry import persist as persist_registry
from app.pipeline.analyze import analyze
from app.pipeline.analyze import analyze, refine_key_from_stems
from app.pipeline.beatgrid import compute_beat_grid
from app.pipeline.collect import (
cleanup_source,
Expand Down Expand Up @@ -222,6 +222,12 @@ def _run_common(job: Job, source: Path, job_dir: Path) -> None:
cleanup_source(job_dir)
job.stems = [{"name": name, "url": f"/api/jobs/{job.id}/stems/{name}.wav"} for name in found]
_check_cancel(job)
# The key was first estimated from the mix, before separation. The stems
# are better evidence, the bass line above all (#726). Keeps the first
# estimate on any failure. Counted in "post", not as a stage of its own:
# the timings' order is a contract (tests/test_identify_regressions.py).
refine_key_from_stems(job, stems_dir)
_check_cancel(job)
_set(job, stage="Mixing tracks...")
original_path = make_original_track(job, job_dir, stems_dir)
if original_path is not None:
Expand Down
Loading
Loading