Merge remote-tracking branch 'origin/main' into fix/1931-torchaudio-backend

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Palash Debnath
2026-09-09 15:20:05 -07:00
5 changed files with 172 additions and 2 deletions
+1
View File
@@ -10,6 +10,7 @@ the frozen-backend fallback mirror it for their toolchains.
**Highlights**
- Upgrading torch for an RTX 50-series card no longer trades one startup crash for another, and the upgrade is documented (#1931)
- An engine you have not installed now says so, instead of reporting a failed check (#1866)
- The Accessibility prompt no longer floats over first-run setup and every other app until you grant it (#1845, #1886)
- The last onboarding step offers to install a speech-to-text model instead of failing three times when none is installed (#1856)
- A download that fails because the folder sits behind a mount point Windows will not cross now says so, and where to move it (#1957)
+49 -1
View File
@@ -32,6 +32,54 @@ def _public_routing_reason(status: object, diagnostic: object) -> str:
return _ROUTING_BY_STATUS.get(status, _ROUTING_UNAVAILABLE)
# Categories for WHY an engine is unavailable. The probe's own sentence cannot
# cross the boundary — it carries exception text, local paths and sometimes
# credentials — but "Engine unavailable. Check installation and configuration."
# told the user nothing at all, and "Last error: A previous engine check
# failed." reads like a crash rather than "you have not installed this yet"
# (#1866). Classifying the private diagnostic into an owned sentence keeps the
# boundary intact and still names the kind of problem and the place to fix it.
_UNAVAILABLE_NOT_INSTALLED = (
"This engine's package isn't installed yet. Install it from "
"Model Catalogue → Engines."
)
_UNAVAILABLE_NEEDS_CONFIG = (
"This engine needs to be configured before it can run. Open "
"Model Catalogue → Engines to finish setting it up."
)
_UNAVAILABLE_FILE_MISSING = (
"A file this engine needs is missing or unreadable. Reinstall it from "
"Model Catalogue → Engines."
)
# Matched against the lowered probe text. Ordered most specific first: a
# missing file often also says "not installed", and the file case has the more
# useful remedy of the two.
_UNAVAILABLE_SIGNATURES = (
(_UNAVAILABLE_FILE_MISSING, (
"file is missing", "file is empty", "file is unreadable",
"script missing", "binary", "not found at",
)),
(_UNAVAILABLE_NEEDS_CONFIG, (
"environment variable", "configure a server endpoint", "api key",
"unconfigured", "set the", "base url",
)),
(_UNAVAILABLE_NOT_INSTALLED, (
"not installed", "package missing", "not available", "no module named",
"import ", "unavailable:", "failed to load",
)),
)
def _public_unavailable_reason(diagnostic: object) -> str:
"""Map a private availability probe to an accurate stable category."""
private = diagnostic.lower() if isinstance(diagnostic, str) else ""
for public, markers in _UNAVAILABLE_SIGNATURES:
if any(marker in private for marker in markers):
return public
return _UNAVAILABLE
def public_backends(entries: list[dict]) -> list[dict]:
"""Copy registry entries while replacing service diagnostics.
@@ -46,7 +94,7 @@ def public_backends(entries: list[dict]) -> list[dict]:
for entry in entries:
item = dict(entry)
if item.get("reason") is not None:
item["reason"] = _UNAVAILABLE
item["reason"] = _public_unavailable_reason(item["reason"])
if item.get("last_error") is not None:
item["last_error"] = _PREVIOUS_FAILURE
if item.get("routing_reason") is not None:
@@ -521,6 +521,14 @@ def test_is_available_rejects_garbage_binary(monkeypatch, tmp_path):
assert "not a usable executable" in reason
@pytest.mark.skipif(
os.name == "nt",
reason=(
"POSIX exec bits do not exist on Windows: os.access(path, os.X_OK) is "
"true for any file that exists, so the assertion below can never fail "
"there and the whole module errored on a Windows checkout instead."
),
)
def test_is_available_does_not_chmod_placeholder(monkeypatch, tmp_path):
"""The #437 exec-bit self-heal must never bless a placeholder: with no
manifest present, no SHA check confirmed the file, so chmod +x on an
+6 -1
View File
@@ -80,7 +80,12 @@ def test_docs_url_survives_the_public_metadata_scrub():
}
(public,) = public_backends([entry])
assert public["reason"] == "Engine unavailable. Check installation and configuration."
# The reason is still replaced — what matters here is that no private text
# survives it. Since #1866 the replacement names the KIND of problem, so
# pinning the old generic sentence would fight that on purpose.
assert "/home/alice" not in public["reason"]
assert "CosyVoice" not in public["reason"]
assert "isn't installed yet" in public["reason"]
assert public["last_error"] == "A previous engine check failed."
assert "/home/alice" not in public["reason"]
assert public["docs_url"] == entry["docs_url"]
@@ -0,0 +1,108 @@
"""#1866 — an unavailable engine must say what KIND of problem it has.
Model Catalogue → Engines rendered "Engine unavailable. Check installation and
configuration." plus "Last error: A previous engine check failed." for engines
the user had simply never installed. Neither names a missing package, a missing
step, or a next action, and the second reads like a crash or a poisoned cache
rather than "you have not installed this yet".
The probe's own sentence still cannot cross the boundary — it carries exception
text, local paths and sometimes credentials. What changed is that the private
diagnostic is now CLASSIFIED into a VoiceStudio-owned category, the same shape
`_public_routing_reason` already uses for routing.
"""
import pytest
from api.public_engine_metadata import public_backends
_PRIVATE_PATH = "/Users/alice/Library/Caches/secret-token-abc123"
def _reason(diagnostic):
return public_backends([{"id": "e", "reason": diagnostic}])[0]["reason"]
@pytest.mark.parametrize(
"diagnostic",
[
"voxcpm package not installed.",
"transformers not installed",
"funasr not installed. Install with: uv pip install funasr",
"kittentts not installed: No module named 'kittentts'",
"omnivoice package missing: cannot import name",
"mlx-whisper unavailable: not supported on this platform",
],
)
def test_a_missing_package_says_so(diagnostic):
assert "isn't installed yet" in _reason(diagnostic)
@pytest.mark.parametrize(
"diagnostic",
[
"Set ELEVENLABS_API_KEY environment variable.",
"Configure a server endpoint in Model Catalogue → Engines",
"unconfigured",
],
)
def test_a_configuration_gap_says_so(diagnostic):
assert "needs to be configured" in _reason(diagnostic)
@pytest.mark.parametrize(
"diagnostic",
[
"file is empty (0 bytes) — a placeholder, not a real binary",
"file is missing",
"ASR sidecar script missing at /opt/thing/run.py",
],
)
def test_a_missing_file_says_so(diagnostic):
assert "missing or unreadable" in _reason(diagnostic)
def test_an_unrecognised_probe_falls_back_to_the_generic_line():
# The categories must not guess. Anything unclassified keeps the old text
# rather than asserting a cause the probe never gave.
assert _reason("something entirely unexpected") == (
"Engine unavailable. Check installation and configuration."
)
@pytest.mark.parametrize(
"diagnostic",
[
f"kittentts not installed: No module named 'kittentts' at {_PRIVATE_PATH}",
f"file is unreadable ({_PRIVATE_PATH})",
f"Set ELEVENLABS_API_KEY; current value read from {_PRIVATE_PATH}",
],
)
def test_no_private_text_crosses_the_boundary(diagnostic):
# The whole reason the reason was replaced in the first place.
out = _reason(diagnostic)
assert _PRIVATE_PATH not in out
assert "alice" not in out
assert "secret-token-abc123" not in out
def test_registry_authored_fields_still_pass_through():
row = public_backends(
[
{
"id": "e",
"reason": "voxcpm package not installed.",
"install_hint": "uv pip install voxcpm",
"docs_url": "https://example.invalid/docs",
"setup_snippet": "export FOO=1",
}
]
)[0]
assert row["install_hint"] == "uv pip install voxcpm"
assert row["docs_url"] == "https://example.invalid/docs"
assert row["setup_snippet"] == "export FOO=1"
def test_the_input_row_is_not_mutated():
original = {"id": "e", "reason": "voxcpm package not installed."}
public_backends([original])
assert original["reason"] == "voxcpm package not installed."