Merge pull request #1970 from debpalash/fix/1879-clone-reference

fix(errors): say 'no reference clip' instead of naming library parameters
This commit is contained in:
Palash Debnath
2026-09-09 16:10:58 -07:00
committed by GitHub
4 changed files with 95 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains.
## [Unreleased]
**Highlights**
- Cloning without a reference clip now says so, instead of naming library parameters you cannot set (#1879)
- Upgrading torch for an RTX 50-series card no longer trades one startup crash for another, and the upgrade is documented (#1931)
- A generation timeout now points at the compute-time budget in Settings rather than an environment variable (#1808)
- An engine you have not installed now says so, instead of reporting a failed check (#1866)
+10 -1
View File
@@ -2412,7 +2412,16 @@ async def generate_speech(
raise HTTPException(status_code=503, detail=str(e)) from e
except ValueError as e:
logger.error("Validation failed: %s", e)
raise HTTPException(status_code=400, detail=str(e)) from e
# Most ValueErrors here are VoiceStudio's own validation messages and
# are exactly what the user should read. A few are raw library text
# naming parameters and files the user cannot act on — those get the
# owned remedy for their class instead (#1879). Unclassified ones keep
# passing through, so this cannot swallow a good message.
from core.failure import classify, public_hint_for_topic
_topic = classify(str(e))
_owned = public_hint_for_topic(_topic) if _topic else ""
raise HTTPException(status_code=400, detail=_owned or str(e)) from e
except Exception as e:
tb = traceback.format_exc()
logger.error("Inference failed: %s\n%s", e, tb)
+16
View File
@@ -98,6 +98,7 @@ _HINTS: dict[str, str] = {
"WINDOWS_APP_CONTROL_BLOCKED": "Windows refused to load a file VoiceStudio needs — an Application Control policy (Smart App Control, WDAC, or AppLocker) blocked it. On a personal PC: Windows Security → App & browser control → Smart App Control → Off (Windows only lets you turn it off once — re-enabling requires a Windows reset), then restart VoiceStudio. On a managed/work PC, ask IT to allow the VoiceStudio install folder.",
"WINDOWS_PAGING_FILE_TOO_SMALL": "Windows ran out of virtual memory while mapping the model into memory — its paging file is smaller than the model needs. This is not the same as your RAM being full, and closing other apps usually won't fix it: Windows has to be allowed to back the mapping. Set a bigger paging file — Settings → System → About → Advanced system settings → Performance → Settings → Advanced → Virtual memory → Change: untick \"Automatically manage\", pick your system drive, choose \"Custom size\" and set both Initial and Maximum to at least 32768 MB (more than the model's size), then OK and restart Windows. A smaller/quantized engine (OmniVoice GGUF, Supertonic-3) also avoids the large mapping entirely.",
"WINDOWS_UNTRUSTED_MOUNT": "Windows refused to walk a folder on the way to this file because the path crosses a mount point it does not trust (WinError 448). That is a Windows rule about the VOLUME, not about VoiceStudio or the file itself — it turns up on Dev Drives, on mounted VHD/ReFS volumes, and on junctions pointing into another user profile, so retrying the same link cannot help. Point VoiceStudio at a folder on an ordinary local drive instead: Settings → Storage → data directory, or the download/output folder named in the message. If that folder has to stay where it is, trust the volume with `fsutil devdrv trust <drive>:` from an elevated prompt and restart.",
"CLONE_REFERENCE_MISSING": "This engine was asked to clone a voice but got no reference audio to clone FROM, and the model folder carries no built-in voice either. Pick a voice profile that has a saved reference clip, or record/upload a few seconds of clean speech as the reference, then generate again. A designed voice with no saved reference cannot be cloned from — synthesize with it directly instead.",
"MEDIA_TOOL_MISSING": "VoiceStudio's media engine (ffmpeg/ffprobe) wasn't on the system path when a component went looking for it. Open Settings → Audio tools and use Download/Repair to fetch the bundled copy, then retry — a restart picks it up for everything. If you'd rather use a system install, install ffmpeg (macOS: `brew install ffmpeg`; Windows: `winget install Gyan.FFmpeg`; Linux: your package manager) and restart VoiceStudio, or point FFMPEG_PATH / OMNIVOICE_FFPROBE_PATH at the binaries in Settings.",
"AUDIO_IO_FAILED": "An audio file couldn't be read or written at the OS level. Check the drive isn't full, that the output and temp folders exist and are writable, and that antivirus or OneDrive isn't locking them (add a VoiceStudio exclusion if you use one).",
"VIDEO_DOWNLOAD_OS_ERROR": "The OS refused a file operation while saving the downloaded video — this is a disk/folder problem, not a network one, so retrying the same link won't help. The download is written to a job folder under your VoiceStudio data directory (Settings → Storage shows the path): check that drive isn't full, that the folder exists and is writable, and that antivirus or a cloud-sync client (OneDrive, Dropbox) isn't locking it — add a VoiceStudio exclusion if you use one. If your data directory sits on a synced or network drive, move it to a local one.",
@@ -313,6 +314,9 @@ _CONTEXT_FREE_HINT_CLASSES = frozenset({
# point" — both unmistakable, and it reaches the user as a bare
# download failure with only the OS sentence attached.
"WINDOWS_UNTRUSTED_MOUNT",
# #1879: matched on wording no other failure produces, and it reaches the
# user as a bare 400 carrying only the library sentence.
"CLONE_REFERENCE_MISSING",
# Its trigger is a VoiceStudio-authored sentence — "the TTS model cache
# for … is incomplete" plus "could not be auto-repaired" / "weights
# missing" — so it cannot be produced by an unrelated library. The 500
@@ -587,6 +591,18 @@ def classify(reason: str) -> str:
# translates the sentence, with the English phrase as a fallback.
if "[winerror 448]" in low or "untrusted mount point" in low:
return "WINDOWS_UNTRUSTED_MOUNT"
# #1879: mlx-audio (and the Chatterbox-family models under it) raise a
# bare ValueError naming their own parameters — "No conditionals
# available. Either provide audio_prompt/audio_prompt_sr ... or ensure
# conds.safetensors is in the model directory." The generate route passed
# that straight through as the 400 detail, so the user was told to supply
# an argument they have no way to name and to check for a file they have
# never heard of. What actually happened is "you asked to clone without a
# reference clip".
if "no conditionals available" in low or (
"audio_prompt" in low and "conds.safetensors" in low
):
return "CLONE_REFERENCE_MISSING"
# #1221: libsndfile failed an OS-level audio read/write. Its own wording is
# a bare "System error.", so match the library name — audio_io already
# prefixes the target path and free space onto the write-path failures.
@@ -0,0 +1,68 @@
"""#1879 — "clone without a reference clip" must not be reported in library terms.
mlx-audio raises a bare ValueError naming its own parameters:
No conditionals available. Either provide audio_prompt/audio_prompt_sr for
voice cloning, or ensure conds.safetensors is in the model directory.
The generate route passed that straight through as the 400 detail, so the user
was told to supply an argument they have no way to name and to check for a file
they have never heard of. What actually happened is simply "you asked to clone
without giving anything to clone from".
The route still passes through every ValueError it cannot classify — most are
VoiceStudio's own validation messages and are exactly what the user should
read, so the fix must not swallow them.
"""
import pytest
from core.failure import _CONTEXT_FREE_HINT_CLASSES, classify, public_hint_for_topic
_RAW = (
"No conditionals available. Either provide audio_prompt/audio_prompt_sr "
"for voice cloning, or ensure conds.safetensors is in the model directory."
)
def test_the_library_wording_is_classified():
assert classify(_RAW) == "CLONE_REFERENCE_MISSING"
def test_the_remedy_talks_about_a_reference_clip():
hint = public_hint_for_topic("CLONE_REFERENCE_MISSING")
assert "reference" in hint.lower()
# And never in the library's own vocabulary.
assert "audio_prompt" not in hint
assert "conds.safetensors" not in hint
def test_it_survives_the_context_free_filter():
assert "CLONE_REFERENCE_MISSING" in _CONTEXT_FREE_HINT_CLASSES
@pytest.mark.parametrize(
"message",
[
"Text is required.",
"Unknown TTS engine: 'nope'. See GET /engines/tts for the list of valid engine ids.",
"speed must be between 0.5 and 2.0",
"reference audio is shorter than 1s",
],
)
def test_our_own_validation_messages_are_left_alone(message):
# The route falls back to str(e) whenever classify() finds nothing, so a
# good message must not classify to something else and get replaced.
topic = classify(message)
assert topic != "CLONE_REFERENCE_MISSING"
def test_the_route_replaces_only_the_classified_case():
# Mirrors the handler: owned remedy when classified, raw text otherwise.
def detail_for(text):
topic = classify(text)
owned = public_hint_for_topic(topic) if topic else ""
return owned or text
assert detail_for(_RAW) != _RAW
assert "reference" in detail_for(_RAW).lower()
assert detail_for("Text is required.") == "Text is required."