fix(dub): classify EINVAL transcribe failures so they stop dead-ending (#763) (#936)

A per-chunk temp-WAV write that fails with OSError EINVAL ("[Errno 22]
Invalid argument") — a missing/read-only/full temp dir, a removed drive,
or antivirus — collapsed into "Transcription produced no segments.
[Errno 22] Invalid argument" with no next step. classify() now names the
class (OS_INVALID_ARGUMENT) so build_failure attaches an actionable
temp-dir/disk/AV hint at the exact surface the streaming dub path already
feeds it (dub_core.py:672) — same treatment the ffmpeg and compute-type
classes get. Fail-before/pass-after regression added; the errno-22 token
keeps it from colliding with the errno-2 transformers-import class.

Also stamps the [0.3.9] CHANGELOG section with today's release date
(2026-07-04) ahead of tagging.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-04 05:35:51 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 86f23326ff
commit eb188931b5
3 changed files with 37 additions and 1 deletions
+2 -1
View File
@@ -6,7 +6,7 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
Versions track the desktop app (`tauri.conf.json` + `frontend/src-tauri/Cargo.toml`).
The bundled TTS model package (`pyproject.toml`) is versioned independently.
## [0.3.9] — 2026-07-02
## [0.3.9] — 2026-07-04
The dictation release — and a deep reliability pass driven by live-testing the entire app. **Dictation is rebuilt end-to-end**: instant feedback with a live waveform, words that commit about half a second after you stop speaking, clean punctuation, and text insertion that never lies about success. **LLM providers get one-click connection testing** with real diagnostics and model discovery, in all 21 languages. The app now **always opens maximized**, bottom buttons **can't hide under the footer** at small window sizes, and a wave of "out of memory / can't reach the backend / stuck at preparing" reports were traced to their real causes and fixed — including the silent VRAM crash on 8 GB cards, dead-IPC startup hangs after a Windows BSOD, and misleading error labels. Intel-Mac support status is now stated honestly, Confucius4-TTS is validated end-to-end, and Parakeet — roughly 20× faster than the default transcriber on CPU — is unlocked for every machine.
@@ -51,6 +51,7 @@ The dictation release — and a deep reliability pass driven by live-testing the
- **No more infinite "preparing" after an unclean shutdown.** If Windows corrupts the WebView cache (e.g. after a BSOD), the splash detects the dead IPC channel, proceeds via a direct backend health check, and — if truly stuck — offers a one-click "Repair and restart". (#879, #892)
- **"Out of memory" is no longer the default excuse.** A failed model download mid-generation was mislabeled as OOM with useless "flush VRAM" advice; network failures are now classified honestly, only real OOM signatures get the OOM treatment, and first-use engine downloads retry once with a fresh connection. (#880, #893)
- **Hung transcriptions recover the same way everywhere.** Chunked dub transcription now shares the same guarded-timeout + GPU-pool reset as the rest of the app, and repeated timeouts recommend the crash-isolated ASR engine — now properly selectable in Settings. (#730, #895)
- **A raw `[Errno 22]` transcribe error now tells you what to fix.** When the OS rejects the temporary WAV write during dub transcription (a missing, read-only, or full temp directory, or antivirus interference), the stream used to dead-end as *"Transcription produced no segments. [Errno 22] Invalid argument"* with no next step; it now classifies the EINVAL and appends an actionable temp-dir/disk/AV hint — the same treatment the ffmpeg and compute-type failure classes already get. (#763)
- **Buttons can no longer hide under the logs footer on small windows.** The bottom status/logs bar was a fixed overlay that pages had to compensate for with padding — any view that missed it (voice-card grids in Gallery and Community, bottom action rows) clipped under the bar at small window sizes, a class previously patched one page at a time (#476, #504). The footer is now a real row of the app shell, so content physically ends at its top edge at every window size, collapsed or expanded — guarded by a new layout test plus a 900×600 Playwright check at the app's minimum window size.
+14
View File
@@ -40,6 +40,7 @@ _HINTS: dict[str, str] = {
"PYANNOTE_LICENSE_REQUIRED": "Accept the pyannote model licenses on Hugging Face, then retry.",
"COMPUTE_TYPE_UNSUPPORTED": "Your GPU doesn't support float16 — OmniVoice retried on int8. If transcription still fails, set OMNIVOICE/ASR_COMPUTE_TYPE=int8 or use CPU.",
"TRANSFORMERS_IMPORT": "Your transformers install is incomplete. Reinstall it (`uv pip install --reinstall transformers`) or switch ASR to faster-whisper (Settings → Models).",
"OS_INVALID_ARGUMENT": "The OS rejected a file operation (Errno 22 / invalid argument) — in the transcribe path this is the temporary WAV write before ASR. It's almost always the temp directory: missing, read-only, on a full or removed drive, or blocked by antivirus. Check that your system TEMP/TMP folder exists and is writable and the drive has free space (add an OmniVoice antivirus exclusion if you use one), then retry.",
"UNSUPPORTED_VIDEO_URL": "This link isn't a directly downloadable video. Paste a direct video page (e.g. a youtube.com/watch?v=… or douyin.com/video/<id> link), not a share/profile/feed link — or download the file and drop it in directly.",
"VIDEO_DOWNLOAD_NETWORK": "The connection to the video server dropped mid-download (often a transient CDN/network blip or a regional rate-limit). Just retry — OmniVoice already cleaned up the partial download. If it keeps failing, check your network/VPN.",
"BROKEN_VENV": "The Python backend environment was moved or damaged. OmniVoice rebuilds it automatically on the next launch; if it keeps failing, use Clean & Retry on the setup screen.",
@@ -187,6 +188,19 @@ def classify(reason: str) -> str:
# failure gets its hint rather than falling through to "".
if "compute type" in low or "efficient float16" in low:
return "COMPUTE_TYPE_UNSUPPORTED"
# #763: a bare OS-level EINVAL ("[Errno 22] Invalid argument") while writing
# the per-chunk temp WAV for transcription (tempfile.NamedTemporaryFile /
# soundfile.write on the system temp dir) used to collapse into a dead-end
# "produced no segments. [Errno 22] Invalid argument" toast with no next
# step. errno 22 is EINVAL on every platform; in this path it's almost always
# a temp dir that's missing, read-only, on a full/removed drive, or blocked
# by antivirus. Name the class so build_failure attaches an actionable hint
# instead of a raw errno. Matching the errno (not the generic "invalid
# argument" wording) keeps this from mislabelling unrelated failures; the
# transformers "errno 2" rule below is unaffected — it also requires the
# transformers + site-packages markers, which this signature lacks.
if "errno 22" in low:
return "OS_INVALID_ARGUMENT"
if (
"could not import module" in low
or "autofeatureextractor" in low
+21
View File
@@ -56,6 +56,27 @@ def test_classify_corrupted_transformers_file():
assert failure.classify("[Errno 2] No such file or directory: '/x/site-packages/numpy/core/foo.py'") == ""
def test_classify_os_invalid_argument_einval():
# #763: a per-chunk temp-WAV write failing with EINVAL surfaced as the
# dead-end "produced no segments. [Errno 22] Invalid argument" toast. It must
# now classify so build_failure attaches a temp-dir/disk/AV hint. This is the
# exact string the streaming dub path aggregates and feeds build_failure.
reason = "[Errno 22] Invalid argument"
assert failure.classify(reason) == "OS_INVALID_ARGUMENT"
evt = failure.build_failure(reason, stage="transcribe", include_diagnostic=False)
assert evt["docs_topic"] == "OS_INVALID_ARGUMENT"
assert evt["hint"], "an EINVAL transcribe failure must carry an actionable hint"
assert "temp" in evt["hint"].lower()
# The errno-22 rule must NOT swallow the errno-2 transformers class (its
# markers still win) or fire on an unrelated errno.
tf = (
"[Errno 2] No such file or directory: "
"'/x/site-packages/transformers/models/qwen3/modeling_qwen3.py'"
)
assert failure.classify(tf) == "TRANSFORMERS_IMPORT"
assert failure.classify("[Errno 13] Permission denied") == ""
def test_classify_video_download_classes():
# #554: a non-downloadable link shape → actionable "paste a direct video URL".
assert failure.classify("Unsupported URL: https://www.douyin.com/discover") == (