fix: preserve startup and media failure evidence (#1722)

* fix: preserve startup and media failure evidence

* fix(dictation): bind readiness to installed revision

* test(dictation): align cache resolution contract

* fix: keep retry failures actionable
This commit is contained in:
Palash Debnath
2026-08-30 20:49:53 +05:30
committed by GitHub
parent 4e61c2782d
commit 80aafa3a53
7 changed files with 237 additions and 42 deletions
+4
View File
@@ -22,6 +22,10 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- Repair-sync failures now retain uv's final dependency error instead of reporting only an opaque exit status (#1705)
- YouTube ingest now retries yt-dlp's transient “page needs to be reloaded” response (#1706)
- Dictation model readiness now follows the live Hugging Face cache selected in Settings (#1707)
## [0.5.1] — 2026-08-28
**Highlights**
+1
View File
@@ -557,6 +557,7 @@ def classify(reason: str) -> str:
or "unable to download video" in low
or "remote end closed" in low
or "timed out" in low
or "the page needs to be reloaded" in low
):
return "VIDEO_DOWNLOAD_NETWORK"
# #1227: Windows Smart App Control / WDAC / AppLocker refused to load a
+43 -18
View File
@@ -281,41 +281,66 @@ def _resolve_model_dir(spec: SherpaModelSpec, *, download: bool = True) -> str:
Restricts the fetch to the exact int8 assets we pin via ``allow_patterns``
so we never pull the bundled fp32 weights or test wavs.
"""
from huggingface_hub import constants as hf_constants
from huggingface_hub import snapshot_download
from services.hf_revisions import installed_revision, revision_for
from services.hf_revisions import revision_for
wanted = list(spec.files.values())
# Probe the revision an existing installation actually resolved. Older
# releases followed ``main`` and may therefore have a different snapshot;
# retaining it preserves offline upgrades. Any network fetch still uses
# the reviewed immutable pin.
installed = installed_revision(spec.repo_id, hf_constants.HF_HUB_CACHE)
try:
return snapshot_download(
repo_id=spec.repo_id,
revision=installed,
local_files_only=True,
allow_patterns=wanted,
)
except Exception:
installed = _installed_snapshot(spec)
if installed:
return installed
if not download:
raise
raise FileNotFoundError(f"No complete cached snapshot for {spec.repo_id}")
logger.info("sherpa dictation: downloading %s on first use", spec.repo_id)
return snapshot_download(
repo_id=spec.repo_id,
revision=revision_for(spec.repo_id),
allow_patterns=wanted,
cache_dir=_live_hub_cache_dir(),
)
def _live_hub_cache_dir() -> str:
"""The effective hub root, evaluated after Settings restores the env."""
direct = os.environ.get("HF_HUB_CACHE") or os.environ.get("HUGGINGFACE_HUB_CACHE")
if direct:
return os.path.expanduser(direct)
home = os.environ.get("HF_HOME") or os.path.expanduser("~/.cache/huggingface")
return os.path.join(os.path.expanduser(home), "hub")
def _installed_snapshot(spec: SherpaModelSpec) -> str | None:
"""Complete snapshot for the recorded revision in the live cache."""
from services.hf_revisions import installed_revision
cache_dir = _live_hub_cache_dir()
revision = installed_revision(spec.repo_id, cache_dir)
snapshot = os.path.join(
cache_dir,
"models--" + spec.repo_id.replace("/", "--"),
"snapshots",
revision,
)
if all(os.path.isfile(os.path.join(snapshot, filename))
for filename in spec.files.values()):
return snapshot
return None
def is_installed(spec: SherpaModelSpec) -> bool:
"""True if every pinned asset is already present in the HF cache."""
try:
d = _resolve_model_dir(spec, download=False)
except Exception:
return False
return all(os.path.isfile(os.path.join(d, f)) for f in spec.files.values())
"""True if the recorded cached snapshot contains every pinned asset.
Do not use ``snapshot_download(local_files_only=True)`` for this probe.
``huggingface_hub.constants.HF_HUB_CACHE`` is fixed when that module is
first imported, while VoiceStudio can restore its cache directory later
from the durable user settings. Resolve the live root and the recorded
revision ourselves so readiness and loading cannot disagree after a cache
move, desktop relaunch, or stale snapshot (#1707).
"""
return _installed_snapshot(spec) is not None
# ── Recognizers ──────────────────────────────────────────────────────────────
+75 -1
View File
@@ -219,6 +219,37 @@ pub fn get_bootstrap_logs(state: tauri::State<'_, BootstrapState>) -> Vec<LogPay
.unwrap_or_default()
}
fn buffered_log_tail(logs: &[LogPayload], stage: &str, max_lines: usize) -> String {
let mut lines = logs
.iter()
.rev()
.filter(|entry| entry.stage == stage && !entry.line.trim().is_empty())
.take(max_lines)
.map(|entry| entry.line.trim().to_string())
.collect::<Vec<_>>();
lines.reverse();
lines.join("\n")
}
fn command_failure_message(
prefix: &str,
result: &io::Result<std::process::ExitStatus>,
output_tail: &str,
) -> String {
let outcome = match result {
Ok(status) => status
.code()
.map(|code| format!("exit code {code}"))
.unwrap_or_else(|| status.to_string()),
Err(error) => format!("command error: {error}"),
};
if output_tail.is_empty() {
format!("{prefix}: {outcome} — no command output was captured")
} else {
format!("{prefix}: {outcome}\n\nLast output:\n{output_tail}")
}
}
#[tauri::command]
pub fn retry_bootstrap(app: tauri::AppHandle, state: tauri::State<'_, BootstrapState>) {
respawn_backend(app, state.stage.clone(), state.logs.clone());
@@ -2359,7 +2390,18 @@ the existing venv; newly added dependencies may be missing (#307)",
ensure_cudnn8_compat(app, &uv_path, &venv_py, &venv_dir, &project_dir);
return Some((venv_py, backend_dir));
}
fail(progress, &format!("Repair uv sync failed: {:?}", repair_status));
let output_tail = app
.try_state::<BootstrapState>()
.and_then(|state| {
state.logs.lock().ok().map(|logs| {
buffered_log_tail(&logs, "installing_deps", 12)
})
})
.unwrap_or_default();
fail(
progress,
&command_failure_message("Repair uv sync failed", &repair_status, &output_tail),
);
return None;
}
@@ -3295,6 +3337,38 @@ mod tests {
assert!(!marker.is_file());
let _ = fs::remove_dir_all(&venv_dir);
}
#[test]
fn failed_command_message_carries_the_newest_relevant_output() {
let logs = vec![
LogPayload {
stage: "downloading_uv".into(),
line: "unrelated".into(),
},
LogPayload {
stage: "installing_deps".into(),
line: "resolver context".into(),
},
LogPayload {
stage: "installing_deps".into(),
line: "actual dependency conflict".into(),
},
];
let tail = buffered_log_tail(&logs, "installing_deps", 1);
let failure = command_failure_message(
"Repair uv sync failed",
&Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"access denied",
)),
&tail,
);
assert!(failure.contains("command error: access denied"));
assert!(failure.contains("Last output:\nactual dependency conflict"));
assert!(!failure.contains("resolver context"));
assert!(!failure.contains("unrelated"));
}
}
#[cfg(test)]
+22
View File
@@ -41,6 +41,14 @@ def test_broken_pipe_is_transient():
assert dp._is_transient_download_error(ConnectionResetError()) is True
def test_youtube_reload_request_is_transient():
reported = RuntimeError(
"ERROR: [youtube] qQjZNdURxzg: The page needs to be reloaded."
)
assert failure.classify(str(reported)) == "VIDEO_DOWNLOAD_NETWORK"
assert dp._is_transient_download_error(reported) is True
def test_unsupported_url_is_not_transient():
# Must classify as UNSUPPORTED (more specific) and therefore NOT retry —
# otherwise we'd waste 3 attempts on a link that can never download.
@@ -137,6 +145,20 @@ def test_gives_up_after_bounded_retries(_patched_ytdlp):
assert evt["hint"]
def test_reload_request_exhaustion_keeps_network_guidance(_patched_ytdlp):
job_dir = str(_patched_ytdlp)
message = "ERROR: [youtube] qQjZNdURxzg: The page needs to be reloaded."
_FakeYDL.behaviors = [RuntimeError(message)] * 10
with pytest.raises(RuntimeError) as ei:
dp.yt_download_sync("https://youtube.com/watch?v=qQjZNdURxzg", job_dir)
assert len(_FakeYDL.calls) == dp._YT_DOWNLOAD_RETRIES + 1
evt = failure.build_failure(ei.value, stage="download", include_diagnostic=False)
assert evt["docs_topic"] == "VIDEO_DOWNLOAD_NETWORK"
assert evt["hint"]
def test_does_not_retry_unsupported_url(_patched_ytdlp):
job_dir = str(_patched_ytdlp)
# A non-downloadable link must fail fast (no wasted retries).
+19 -22
View File
@@ -203,40 +203,42 @@ def test_sherpa_available_degrades_native_loader_failures(monkeypatch, native_er
def test_model_resolution_pins_offline_probe_and_download(monkeypatch, tmp_path):
from services import hf_revisions, sherpa_dictation as sd
import huggingface_hub
from huggingface_hub import constants as hf_constants
spec = sd.get_spec("sherpa-whisper-tiny")
monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path))
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
calls = []
def fake_snapshot(**kwargs):
calls.append(kwargs)
if kwargs.get("local_files_only"):
raise FileNotFoundError("not cached")
return "/cache/pinned"
monkeypatch.setattr(huggingface_hub, "snapshot_download", fake_snapshot)
assert sd._resolve_model_dir(spec) == "/cache/pinned"
assert len(calls) == 2
assert all(call["revision"] == hf_revisions.revision_for(spec.repo_id) for call in calls)
assert calls == [{
"repo_id": spec.repo_id,
"revision": hf_revisions.revision_for(spec.repo_id),
"allow_patterns": list(spec.files.values()),
"cache_dir": str(tmp_path),
}]
def test_model_resolution_probes_preserved_legacy_snapshot(monkeypatch, tmp_path):
from services import hf_revisions, sherpa_dictation as sd
import huggingface_hub
from huggingface_hub import constants as hf_constants
spec = sd.get_spec("sherpa-whisper-tiny")
legacy_revision = "e" * 40
ref = (
tmp_path
/ "models--csukuangfj--sherpa-onnx-whisper-tiny"
/ "refs"
/ "main"
)
repo = tmp_path / "models--csukuangfj--sherpa-onnx-whisper-tiny"
ref = repo / "refs" / "main"
ref.parent.mkdir(parents=True)
ref.write_text(legacy_revision + "\n", encoding="ascii")
monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path))
snapshot = repo / "snapshots" / legacy_revision
snapshot.mkdir(parents=True)
for filename in spec.files.values():
target = snapshot / filename
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"model")
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
calls = []
def fake_snapshot(**kwargs):
@@ -244,14 +246,9 @@ def test_model_resolution_probes_preserved_legacy_snapshot(monkeypatch, tmp_path
return "/cache/legacy"
monkeypatch.setattr(huggingface_hub, "snapshot_download", fake_snapshot)
assert sd._resolve_model_dir(spec) == "/cache/legacy"
assert calls == [{
"repo_id": spec.repo_id,
"revision": legacy_revision,
"local_files_only": True,
"allow_patterns": list(spec.files.values()),
}]
assert calls[0]["revision"] != hf_revisions.revision_for(spec.repo_id)
assert sd._resolve_model_dir(spec) == str(snapshot)
assert calls == []
assert legacy_revision != hf_revisions.revision_for(spec.repo_id)
# ── The 4 recognizer kinds construct + transcribe ───────────────────────────
+72
View File
@@ -0,0 +1,72 @@
"""Sherpa installed-state must follow the catalogue's live cache roots (#1707)."""
from __future__ import annotations
from services import sherpa_dictation
from services.hf_revisions import revision_for
def test_installed_probe_uses_live_hf_cache_root(tmp_path, monkeypatch):
spec = sherpa_dictation.get_spec("sherpa-whisper-tiny")
assert spec is not None
snapshot = (
tmp_path
/ ("models--" + spec.repo_id.replace("/", "--"))
/ "snapshots"
/ revision_for(spec.repo_id)
)
snapshot.mkdir(parents=True)
for filename in spec.files.values():
target = snapshot / filename
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"model")
# Simulate huggingface_hub having been imported before Settings restored a
# different cache. The old implementation asked snapshot_download(),
# which could keep consulting its import-time constant instead of this
# live value.
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
monkeypatch.setattr(
sherpa_dictation,
"_resolve_model_dir",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError(
"installed-state must not call snapshot_download"
)),
)
assert sherpa_dictation.is_installed(spec) is True
def test_installed_probe_requires_all_pinned_assets(tmp_path, monkeypatch):
spec = sherpa_dictation.get_spec("sherpa-whisper-tiny")
assert spec is not None
snapshot = (
tmp_path
/ ("models--" + spec.repo_id.replace("/", "--"))
/ "snapshots"
/ revision_for(spec.repo_id)
)
snapshot.mkdir(parents=True)
first = next(iter(spec.files.values()))
target = snapshot / first
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"partial")
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
assert sherpa_dictation.is_installed(spec) is False
def test_complete_stale_snapshot_does_not_mask_missing_recorded_revision(tmp_path, monkeypatch):
spec = sherpa_dictation.get_spec("sherpa-whisper-tiny")
assert spec is not None
repo = tmp_path / ("models--" + spec.repo_id.replace("/", "--"))
stale = repo / "snapshots" / ("a" * 40)
stale.mkdir(parents=True)
for filename in spec.files.values():
target = stale / filename
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"stale model")
(repo / "voicestudio-revision").write_text("b" * 40, encoding="ascii")
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
assert sherpa_dictation.is_installed(spec) is False