diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed4954d..647d507d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/backend/core/failure.py b/backend/core/failure.py index 3daee91f..bea1a570 100644 --- a/backend/core/failure.py +++ b/backend/core/failure.py @@ -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/ 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 diff --git a/tests/test_failure_classify.py b/tests/test_failure_classify.py index 2f6068e9..31681c1d 100644 --- a/tests/test_failure_classify.py +++ b/tests/test_failure_classify.py @@ -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") == (