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:
@@ -17,3 +17,7 @@ out/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
|
||||
# Tool caches
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user