fix(backends): explain why an explicitly requested device cannot be used

--device nvidia on a machine without one reported "no usable backend remains",
which says nothing about the cause and repeated the same message once per URL.
Asking for a device that cannot run is a configuration mistake, so it now fails
once, fatally, naming the reason: "nvidia/cuda: no nvidia hardware detected".

Found by end-to-end testing; the unit tests only covered the runtime-failure
path, where TranscribeError remains correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-13 15:44:43 -05:00
co-authored by Claude Opus 5
parent fab20e1d32
commit c844781d1e
3 changed files with 53 additions and 0 deletions
+4
View File
@@ -17,3 +17,7 @@ out/
.coverage
htmlcov/
.pytest_cache/
# Tool caches
.mypy_cache/
.ruff_cache/
+21
View File
@@ -55,6 +55,20 @@ def _select_requested(
return matches
def explain_unavailable(requested: str, chain: tuple[type[Backend], ...] | None = None) -> str:
"""Why an explicitly requested backend cannot be used."""
chain = chain if chain is not None else DEFAULT_CHAIN
reasons: list[str] = []
for backend in _select_requested(requested, chain):
label = key(backend)
if not backend.probe_hardware():
reasons.append(f"{label}: no {backend.name} hardware detected")
continue
status = backend.probe_toolchain()
reasons.append(f"{label}: {status.reason or 'unavailable'}")
return "; ".join(reasons)
def candidates(
requested: str | None = None, chain: tuple[type[Backend], ...] | None = None
) -> Iterator[type[Backend]]:
@@ -149,6 +163,13 @@ def transcribe_with_fallback(
log.warning("backend %s failed (%s); demoting it and falling back", label, exc)
last = exc
if explicit and requested is not None and last is None:
# Asking for a backend that cannot run is a configuration mistake, not a
# per-job failure; say why once and stop rather than repeating it.
raise errors.ConfigError(
f"--device {requested} was requested but cannot be used",
hint=explain_unavailable(requested, chain),
)
raise errors.TranscribeError(
"no usable backend remains",
hint=str(last) if last else "Run `ccn-transcribe doctor` to see what was detected.",
+28
View File
@@ -225,3 +225,31 @@ class TestRealChain:
sys.modules.pop(module, None)
list(registry.candidates())
assert not {"torch", "faster_whisper", "ctranslate2"} & set(sys.modules)
class TestExplicitDeviceDiagnostics:
def test_requesting_absent_hardware_is_a_config_error(self) -> None:
# A device that cannot run is a configuration mistake, not a per-job
# failure to repeat once per URL.
a = make_backend("a", hardware=False)
with pytest.raises(errors.ConfigError, match="cannot be used"):
registry.transcribe_with_fallback(PCM, REQ, requested="a", chain=(a,))
def test_the_hint_says_the_hardware_is_absent(self) -> None:
a = make_backend("a", hardware=False)
try:
registry.transcribe_with_fallback(PCM, REQ, requested="a", chain=(a,))
except errors.ConfigError as exc:
assert "no a hardware detected" in (exc.hint or "")
def test_the_hint_relays_a_toolchain_reason(self) -> None:
a = make_backend("a", toolchain=ToolchainStatus(ok=False, reason="not implemented"))
assert "not implemented" in registry.explain_unavailable("a", (a,))
def test_a_runtime_failure_still_reports_as_a_transcribe_error(self) -> None:
a = make_backend("a", fail_on_transcribe=True)
with pytest.raises(errors.TranscribeError):
registry.transcribe_with_fallback(PCM, REQ, requested="a", chain=(a,))
def test_real_nvidia_request_explains_the_absence(self) -> None:
assert "no nvidia hardware" in registry.explain_unavailable("nvidia")