Review feedback on the preprocessor:
1. ``trim_top_db=30`` was labelled "conservative" in the docstring but is
actually *more* aggressive than librosa's default of 60. Normal
speech dynamic range sits around 30 dB, so 30 dB would eat quiet
trailing syllables and soft consonants. Raise the default to 40 dB —
below normal speech dynamic range but still catching obvious edge
silence — and fix the docstring.
2. Unconditional 100 ms edge padding ran even when ``librosa.effects.trim``
removed nothing. For a well-recorded 29.9 s upload that path would
push the waveform past the 30 s ceiling and trigger a spurious "too
long" rejection. Only pad when trimming actually shortened the
audio, and cap the pad so the output never exceeds the input length.
Adds a regression test for the net-neutral length behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Uploaded/recorded voice samples were rejected outright whenever the peak
exceeded 0.99 ("Audio is clipping (reduce input gain)"). That wasn't
actionable: a recording in the app has no pre-gain control, and an
already-captured file can't be re-taken by the user. The Settings
"Normalize audio" toggle only affects generated TTS output, so users who
enabled it expecting it to help with sample uploads were still blocked.
Replace the hard reject with a small, always-on preprocess step that
runs right after load:
- DC-offset removal
- Conservative edge-silence trim (top_db=30) with 100 ms padding kept
- Peak cap at 0.95 if the input peak exceeds that
Duration and RMS checks now run on the preprocessed waveform, so
samples that were previously rejected for being "hot" are accepted and
stored with safe headroom. True in-waveform clipping artifacts still
can't be repaired — peak scaling only prevents downstream re-clipping
during multi-sample combination and TTS inference.
Adds a unit-test file (previously none existed for audio.py).
Fixes#456.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): use qwen_custom_voice in API example (instruct is CustomVoice-only)
The curl snippet showed engine: "qwen" alongside an instruct field, but base
Qwen3-TTS has no instruct path — that's a Qwen CustomVoice feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): use a realistic UUID for profile_id in API example
Profile IDs are str(uuid.uuid4()), not slugs (see backend/services/profiles.py:175).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(landing): add polished /download page — no more dumping users on GitHub
Users were clicking download, landing on the GitHub releases page, and filing
confused comments along the lines of "I ended up on some blog site called
GitHub." We now route every download CTA through a dedicated /download page
that auto-triggers the platform-specific download and gives users a polished
post-click experience with donate + docs + AI help prompts.
- New /download page:
- Big app logo + "Your download has started" messaging.
- Auto-detects platform from ?platform=X or navigator.userAgent.
- Programmatically clicks a hidden anchor to trigger the file download
without leaving the page.
- Platform-specific buttons as a visible fallback for "download not
working" / manual-pick.
- Personal donate spiel + Buy Me a Coffee button.
- Resources grid: docs, DeepWiki ("got questions? ask AI"), GitHub.
- Landing page download section cards now link to /download?platform=X
instead of the asset URL directly.
- /download/[platform] (used by README/docs links) now redirects to the
/download page rather than straight to the asset or to GitHub on error.
- Drops unused downloadLinks state from the landing page.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): use official platform brand icons via simple-icons
The hand-rolled Linux SVG path wasn't actually Tux — it was a symmetric
placeholder shape. Apple/Windows were close but not canonical either.
- Apple + Linux: pulled from @icons-pack/react-simple-icons (SiApple, SiLinux).
- Windows: simple-icons drops the Microsoft mark over trademark policy, so
the Windows 11 flag is inlined from Microsoft's public brand guidance.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): route Download CTAs to /download page, not the section anchor
Hero CTA, navbar link, and footer link were all scrolling to #download
(the section at the bottom of the page) instead of going to the new
/download page that triggers the actual download.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* chore(landing): run dev server on Node instead of Bun runtime
Bun runtime + Next 16 Turbopack dev server intermittently trips a
JavaScriptCore allocator panic ('pas panic: deallocation did fail ...
Alloc bit not set') after a few requests. Dropping --bun keeps Bun as
the package manager but runs next dev on Node, which is stable.
Build + start keep --bun since one-shot invocations don't exhibit the
allocator drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): route Linux users to /linux-install instead of attempting download
No prebuilt Linux binary exists yet (see /linux-install for build-from-source
instructions). The /download page previously treated Linux like the other
platforms — auto-triggering a non-existent AppImage and offering a dead
manual button.
- /download page: if platform resolves to 'linux' via ?platform or UA detect,
window.location.replace('/linux-install') — never try to auto-download.
- Manual Linux card: label changed to "Build from source" and links to
/linux-install (no download attribute, no asset URL).
- /download/linux pretty URL: 307s straight to /linux-install.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* docs: consolidate troubleshooting into the MDX docs site + status updates
- Delete docs/TROUBLESHOOTING.md; the canonical troubleshooting guide now
lives under docs/content/docs/overview/troubleshooting.mdx so it's served
from docs.voicebox.sh alongside the rest of the docs.
- CONTRIBUTING.md + README.md: repoint "Troubleshooting" references to the
new MDX path. README gets a top-level callout so users hit the guide
before filing an issue.
- PROJECT_STATUS.md: refresh issue/PR counts, document the flash-attn
warning (cosmetic on all platforms; CUDA-only, fallback is PyTorch SDPA
which is near-FA2 on Ampere+) with per-platform context + community
Windows wheels + SageAttention/xformers alternatives, add WebAudio
audio-session bug note (tracked separately in PR #486), and expand the
Qwen 0.6B→1.7B MLX fallback explanation for triage.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(landing): address PR #487 review feedback
- Preserve canonical camelCase platform aliases (macArm, macIntel) in the
/download/[platform] redirect so those URLs don't lose their platform param.
- Add accessible title + role="img" to the inline Windows SVG so it passes
Biome's a11y rule and announces to screen readers.
- On /api/releases fetch failure, show an explicit error state with a single
intentional link to GitHub releases — no more silent GitHub fallback or
disabled-button UX lie. Keeps normies off GitHub unless they opt in.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Keep a silent looping <audio> element mounted at the app root so macOS
never tears down the CoreAudio session. Without this, backgrounding the
app long enough leaves WaveSurfer's AudioContext in a state where play()
resolves and timeupdate fires, but no audio reaches the output — and not
even cmd+R (full JS reload) restores it, only a full app relaunch.
Uses a zero-PCM WAV blob at full volume rather than a muted element,
since WebKit can optimize muted media away and defeat the purpose.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
- Add three tutorial cards (Danish Sofi, StinkyScrublet, mikbes)
- Navbar: switch parent to flex/justify-between on mobile (grid on sm+),
unhide Donate button so both CTAs sit on the right, matching desktop
- Hero CTAs: keep Download and GitHub side-by-side on mobile instead of
stacking vertically
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
The previous fix (#481) capped transformers at 4.57.6 in requirements-mlx.txt,
but pip's clean resolver in CI can't satisfy that alongside mlx-audio>=0.3.1
(declares `transformers==5.0.0rc3` or `>=5.0.0`) — it backtracks through every
transformers and tokenizers version and exits with `ResolutionImpossible`.
The dev install worked only because mlx-audio 0.4.1 was already present, so
pip never tried to re-resolve.
mlx-audio 0.4.1 + mlx-lm 0.31.1 both declare transformers>=5.x but the API
surface we actually use works fine on 4.57.x in practice (verified across all
engines in dev). Install both --no-deps to bypass the resolver; transitive
runtime deps (huggingface_hub, librosa, numpy, numba, pyloudnorm, etc.) are
already pulled in by requirements.txt.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
mlx-audio depends on `transformers` with no upper bound. Installing
requirements-mlx.txt after requirements.txt lets pip upgrade transformers
past the 4.57.x cap to 5.x, which breaks three engines in the frozen MLX
bundle:
- qwen-custom-voice: `check_model_inputs` was rewritten to take `func` as
positional, so `@check_model_inputs()` factory calls fail with
`TypeError: missing 1 required positional argument: 'func'`
- tada-1b: `PretrainedConfig.__init_subclass__` now applies `@dataclass`,
which rejects tada's `strides: list = []` mutable default
- luxtts: Whisper init hits `AssertionError` in `torch._refs.normal_`
Restating the same constraint here keeps mlx-audio's transformers
dependency from quietly winning the resolver.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
cpal 0.15 uses ALSA as its Linux backend, which does not expose
PulseAudio/PipeWire monitor sources. The previous approach searched
for 'monitor' in cpal device names, which never matched on most
Linux systems, silently falling back to the microphone input.
This fix:
- Detects the correct monitor source via 'pactl get-default-sink'
and 'pactl list short sources'
- Sets PULSE_SOURCE env var before cpal initialization so PulseAudio's
ALSA plugin routes the default input through the monitor
- Preserves the original name-based search as fallback when pactl is
unavailable
- No new dependencies added
Tested on PipeWire 1.0.5 with Realtek ALC897 (HD-Audio Generic).
ModelManagement.tsx reads migrationResult.moved (added in #433) but
apiClient.migrateModels() was typed as returning only { source, destination }.
The backend actually returns { moved: int, errors: list[str], source, destination }
(backend/routes/models.py:140, 168). Widen the TS return type so the check
typechecks under the new CI gate from #418.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
When user attempts to change model storage location with no models
downloaded, the migration API returns moved=0 early. Previously the UI
would still call setCustomModelsDir() and restart the server, causing
unexpected behavior (hang/connection lost).
This change checks migrationResult.moved === 0 and shows a helpful
toast message instead of proceeding with the storage change.
Fixes: #426
Co-authored-by: fuleinist <[email protected]>
When migrating model path with an empty cache, backend returned early without emitting migration completion SSE, causing frontend overlay to hang. This patch emits complete status for empty migrations.
* fix(build): bundle kokoro source files for transformers runtime introspection
transformers opens .py source files at runtime to check attention/MoE
implementation via regex (e.g. _can_set_attn_implementation). PyInstaller's
--hidden-import only bundles .pyc bytecode, so kokoro/modules.py was missing
from the bundle causing a FileNotFoundError on Kokoro model load.
Switch from individual --hidden-import entries to --collect-all kokoro in both
build_binary.py and voicebox-server.spec. The kokoro package is 172K so no
meaningful bundle size impact.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(build): use SPECPATH for runtime hook instead of hardcoded absolute path
The linter expanded runtime_hooks=[] to an absolute /Users/... path which
would break CI and other dev machines. Use os.path.join(SPECPATH, ...) to
mirror the relative approach in build_binary.py.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(build): runtime hook to work around PyInstaller + Python 3.12 import breakages
Four distinct bundling-specific crashes blocked Kokoro and Qwen CustomVoice
from loading in the frozen binary:
1. torch._dynamo import triggered via class-body decorators
(@torch._dynamo.allow_in_graph on PreTrainedModel,
@torch.compiler.disable in flex_attention) pulls in torch._numpy._ufuncs
which crashes on module load with NameError: name 'name' is not defined.
2. AlbertModel (Kokoro) triggers @auto_docstring -> modeling_auto ->
GenerationMixin -> candidate_generator -> sklearn -> scipy, which hits
the same class of bug in scipy.stats._distn_infrastructure (NameError:
name 'obj' is not defined).
3. AutoModel (Qwen) pulls the same sklearn -> scipy chain directly.
4. librosa (required by most TTS engines) -> scipy.signal -> scipy.stats
hits the _distn_infrastructure crash regardless of the transformers
stubs above.
The root cause of (1) and (4) is that PyInstaller's frozen importer runs
module-level `for X in [<list-comp using dir()>]:` loops with an empty
iterable, leaving the loop variable unbound. Trailing `del obj` / unrelated
references then crash.
Fix: a single runtime hook (pyi_rth_torch_compiler_disable.py) installs:
- sys.modules stubs for torch._dynamo and torch._dynamo.config, plus a
meta-path finder for torch._dynamo.* submodules — voicebox never uses
torch.compile/dynamo for inference, so a permissive no-op stub (callable
as decorator, falsey as predicate, context-manager-safe for
TransformGetItemToIndex) is drop-in safe.
- meta-path finder stubs for transformers.utils.auto_docstring and
transformers.generation.candidate_generator — both import-chain
short-circuits; docstrings and speculative decoding aren't used for TTS.
- meta-path finder for scipy.stats._distn_infrastructure that reads the
real .py source via the wrapped loader's get_source(), replaces the
bundling-broken `del obj` with `globals().pop('obj', None)`, and
compile+exec's the patched source. This keeps the real scipy module
intact so librosa and everything downstream works normally.
Supporting changes:
- backend/pyi_hooks/hook-scipy.stats._distn_infrastructure.py sets
module_collection_mode = "pyz+py" so the .py source is actually in the
bundle for the runtime patcher to read.
- build_binary.py and voicebox-server.spec register the runtime hook and
the new hooks dir.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(build): force transformers torch<2.6 mask path and bundle spacy_pkuseg
- patch transformers.masking_utils to set _is_torch_greater_or_equal_than_2_6
= False, forcing sdpa_mask_older_torch and avoiding the vmap .item() crash
that breaks Qwen CustomVoice generation (our torch._dynamo stub can't
reproduce TransformGetItemToIndex's graph transform).
- add PyInstaller hook to bundle transformers.masking_utils .py source so the
runtime finder can source-patch it.
- --collect-all spacy_pkuseg so Chatterbox Multilingual can load its Chinese
segmenter (dicts/default.pkl + native .so extensions).
- add per-finder install diagnostics + _HOOK_VERSION marker to make future
bundle-only regressions easier to triage.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(build): pass PyInstaller hook paths relative so .spec is portable
Absolute paths ended up in the auto-regenerated voicebox-server.spec
because build_binary.py prefixed every --runtime-hook and
--additional-hooks-dir with str(backend_dir / ...). That broke builds
on any machine whose checkout wasn't at /Users/jamie/... and anyone
invoking pyinstaller voicebox-server.spec directly.
os.chdir(backend_dir) already runs before PyInstaller (same reason
server.py works as a bare filename), so the backend_dir prefix is
unnecessary. Drop it so the generated spec references pyi_hooks/,
pyi_rth_numpy_compat.py, pyi_rth_torch_compiler_disable.py as repo-
relative paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
0.3.0 sometimes stored relative media paths with the data-dir name baked in
(e.g. "data/profiles/<uuid>/sample.wav"). resolve_storage_path joined those
directly with _data_dir, producing "<data_dir>/data/profiles/..." — a
spurious double nest that breaks file reads after upgrading to 0.4.0.
The 0.4.0 startup migration didn't catch it because resolve_storage_path
produced the buggy double-nested path, to_storage_path saw "data" at the
first (legitimate) index, and the normalized value matched the stored value
so the row was skipped.
Strip any leading "data/" component before joining. This unblocks runtime
reads and lets _normalize_storage_paths rewrite the affected rows on next
startup — no manual migration needed.
Fixes "No such file or directory: '<data_dir>/data/profiles/...'" and
associated 404s on GET /audio/<id> after upgrading from 0.3.0 to 0.4.0.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Catches format drift that accumulated across 13 authored files in
app/src and docs/. Auto-generated artifacts (tauri/src-tauri/gen,
docs/openapi.json, docs/cli.json, app/src/lib/api) were left alone
since the build regenerates them on each run — baking their formatted
state into git just causes churn next build.
No behavioral changes. Trailing commas, line wrapping, and indentation
only.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The in-app changelog viewer rendered each entry's version number at the
same size as body text (text-sm font-medium), so visually there was no
clear anchor for where one release's notes ended and the next began.
Bump the version heading to text-xl font-semibold tracking-tight and
widen the bottom margin so each release reads as a proper section
header.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Before 0.4 every engine was a cloning model, so the instruct UI in the
floating generate box applied the same way everywhere. Commit 3187344
hid the instruct toggle because the mix of new engines landing in 0.4
made it unclear which ones honored the kwarg. With Qwen CustomVoice
now shipping as the only engine actually tuned for instruct-style
control, bring the button back — conditionally, and only for that
engine.
Changes:
• FloatingGenerateBox: SlidersHorizontal toggle button appears left
of Generate when the box is expanded AND engine is
qwen_custom_voice. Clicking it reveals an additive instruct
textarea below the main text field (not a modal swap like the old
version). State persists across engine switches so the toggle
remembers its last position.
• GenerationForm: narrow the instruct FormField's conditional from
`qwen || qwen_custom_voice` to just `qwen_custom_voice`.
• useGenerationForm: narrow supportsInstruct for the same reason.
Base Qwen3-TTS accepts the kwarg but the model itself doesn't honor
it — only CustomVoice was trained for instruction-based style
control.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add cleanup for requestAnimationFrame and setTimeout in scroll effect
to prevent stale DOM writes on unmount or rapid selection changes
- Fix disabled+selected card click: bounce the selection to re-trigger
the engine auto-switch instead of deselecting
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Show all voice profiles with unsupported ones grayed out (opacity) instead of hidden
- Clicking a grayed-out profile selects it and auto-switches the engine to a compatible one
- Sort supported profiles first, with info tip about compatibility at the bottom
- Scroll to selected profile after engine/sort changes with safe margin
- Fix engine desync on tab navigation by initializing form engine from store
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Immortalizes the workflow used to clear the open-PR backlog before
0.4.0: classify every open PR into merge / candidate / supersede /
defer tiers, write a working triage doc, then run the merge loop —
rebasing where needed, merging in batches, applying post-merge
follow-ups, and closing superseded PRs with credit.
Captures the gotchas that matter most:
• Never review a stale branch via `git diff main..HEAD` — it shows
every intermediate main commit as a deletion and makes a 3-line
PR look like a 700-line revert
• Always rebase before squash-merging; GitHub's squash computes
diff(PR-head, merge-base), so a stale branch will revert
in-between work
• Route-ordering, weak-framework linking, dependency floors, !Send
audio types, and why PyTorch nightly isn't shippable
Paired with draft-release-notes and release-bump: triage → draft →
bump.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When the model wasn't loaded, the app was closed mid-run, or a
generation otherwise errored out, the resulting "Failed" rows
accumulate in history and there was no way to remove them in bulk —
individual delete was the only option.
Adds a header row above the history list (only rendered when at
least one failed generation is present) with a "Clear failed" button
that opens a confirmation dialog, then calls a new
DELETE /history/failed endpoint which sweeps all status='failed'
rows (plus their version files / audio files on disk).
Closesjamiepine/voicebox#410
Co-authored-by: Claude Opus 4.6 <[email protected]>
Applies the compatibility-checker portion of #367. Adds a
check_cuda_compatibility() helper that compares the current device's
compute capability against torch.cuda._get_arch_list() and returns a
human-readable warning if the PyTorch build doesn't support it.
Wired into three places:
• HealthResponse gains a gpu_compatibility_warning field so clients
can surface the issue in the UI
• Startup logs the warning as WARN level
• _get_gpu_status() appends "[UNSUPPORTED - see logs]" to the GPU
label shown in settings
Skipped #367's other half — the switch from stable to nightly cu128
wheels across release.yml, build_binary.py, and justfile. That's
redundant with #401's TORCH_CUDA_ARCH_LIST=...12.0+PTX approach and
would introduce non-deterministic builds from shifting nightly
releases.
Co-Authored-By: nyzxor <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Applies the cache_dir portion of #218. On Windows local setups, model
assets can split between .hf-cache/hub and .hf-cache/transformers when
Qwen3TTSModel.from_pretrained doesn't explicitly pin the cache root —
speech_tokenizer and preprocessor_config.json then fail to resolve
during load, causing 500s at generation time.
Routes both HF Hub and Transformers through hf_constants.HF_HUB_CACHE.
Skipped the torch_dtype= → dtype= rename from #218: transformers 4.36
(our minimum) doesn't accept the dtype alias, only 4.46+. Once we bump
the minimum we can make that change.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Follow-up to #402. The sentinel is only removed inside the grace-period
"sentinel found" branch. When the HTTP /watchdog/disable request wins
the race (normal case on macOS/Linux, occasional on Windows), the
_watchdog_disabled=True check returns first and the sentinel is left on
disk indefinitely.
If a later session spawns a fresh server and the user exits without
"keep running", the new watchdog would find that stale sentinel during
its grace period and keep the server alive against user intent.
Wipe any pre-existing sentinel when the watchdog starts so only signals
written during this session's lifetime can influence grace-period
decisions.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The HTTP /watchdog/disable request races with process exit on Windows,
causing the watchdog to kill the server before the request arrives.
Added a .keep-running sentinel file as a reliable fallback:
- Tauri writes the file to data_dir before sending the HTTP request
- The watchdog checks for it during the grace period after detecting
parent death
- The file is removed after being read to avoid stale state
This approach works regardless of HTTP timing because file writes
complete synchronously before the Tauri process exits.
Fixes#372
Co-authored-by: Matt Van Horn <[email protected]>
The cpal Stream was created and play() called but then immediately
dropped when play_to_device() returned. When a cpal Stream is dropped,
audio output stops immediately. This caused silent playback.
Fix: add a spin-wait loop that holds the Stream in scope until all
samples have been consumed (or stop_flag is set).
* fix: add asyncio.Lock to prevent concurrent CUDA downloads
The startup auto-update task and the manual download endpoint can both
invoke download_cuda_binary() concurrently. Without mutual exclusion,
both coroutines write to the same temp file path, corrupting the
download. The progress-manager status check is a TOCTOU race because
the status is not set until after several synchronous checks complete.
Add a module-level asyncio.Lock acquired at the top of
download_cuda_binary() so only one download can proceed at a time.
* fix: fast-reject duplicate CUDA download when lock is held
Address CodeRabbit review feedback: check _download_lock.locked()
before awaiting the lock so concurrent callers return immediately
instead of queueing behind the first download. This prevents the
route handler from returning "started" to multiple callers when only
one download actually proceeds.
Set TORCH_CUDA_ARCH_LIST in the CUDA build step to include 12.0+PTX
for forward compatibility with Blackwell GPUs (RTX 5070 Ti, 5080, etc).
Pre-built PyTorch cu128 wheels only ship native kernels for sm_80/86/89/90.
Without this, Blackwell GPU users get "no kernel image is available for
execution on the device" at runtime.
Fixes#386
Related: #395, #396, #399, #400
Co-authored-by: Matt Van Horn <[email protected]>
Two changes to address the race condition causing "Failed to split clip":
Backend (stories.py): Added with_for_update() to the item query in
split_story_item so concurrent requests for the same clip are
serialized via a row lock instead of racing.
Frontend (StoryTrackEditor.tsx): Guard handleSplit with
splitItem.isPending to prevent rapid double-clicks from firing
multiple mutations before the first completes.
Fixes#366
Co-authored-by: Matt Van Horn <[email protected]>
* fix(history): populate status/error/engine/model_size/is_favorited from DB
GET /history/{generation_id} was constructing HistoryResponse without
passing status, error, engine, model_size, or is_favorited from the
DB row. Since HistoryResponse.status defaults to "completed" in the
Pydantic model (models.py:141), this endpoint returned
status="completed" for every generation regardless of the actual DB
state — including jobs still in "loading_model" or "generating", and
even "failed" jobs.
This breaks any client polling /history/{id} for job completion:
the API lies about the status, so the only trustworthy success
signal becomes `audio_path` being non-empty. All other fields left
at their model defaults were similarly masked.
Fix: pass all fields through from the DB row, matching the pattern
used elsewhere in the codebase. The DB model (Generation in
database/models.py) already has all these columns.
* fix(history): apply NULL fallbacks to match list endpoint
Align the defensive mappings with services/history.py:206-223 so
both the single-item and list history endpoints handle legacy rows
with NULL status/engine/is_favorited identically. Without this,
HistoryResponse's non-Optional str/bool fields would raise a
pydantic ValidationError (500) on any row where these columns are
NULL — possible from direct SQL updates or past migrations.
Addresses review feedback on PR #394.
---------
Co-authored-by: malletfils <[email protected]>
Two small safety improvements:
1. Voice prompt cache (cache.py): add weights_only=True to torch.load()
so cached .prompt files are loaded using the safe unpickler instead of
the unrestricted pickle deserializer. This follows the PyTorch 2.6+
best practice of opting in to safe loading for all torch.load() calls.
2. SPA catch-all (app.py): replace str.startswith() path guard with
Path.is_relative_to(). The string prefix check passes for sibling
paths like /app/frontend_evil/ that share the /app/frontend prefix.
is_relative_to() correctly tests directory containment.
Follow-up to #361. The original fallback silently mapped unknown numpy
dtypes to torch.float32, which would reinterpret the memcpy'd bytes in
the wrong dtype and corrupt data (e.g. fp16 tensors from some TTS
engines) rather than erroring loudly.
- Hoist dtype_map out of the inner function so it's built once
- Add float16, complex64, complex128 mappings
- Raise TypeError on unknown dtype instead of silent float32 fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
torch is compiled against numpy 1.x. numpy 2.x changed the ABI version
returned by PyArray_GetNDArrayCVersion() (0x01000009 → 0x02000000), so
torch's is_numpy_available() always returns False and torch.from_numpy()
raises RuntimeError. This causes TTS generation to fail with:
ValueError: Unable to create tensor, you should probably activate
padding with 'padding=True'
Two fixes:
1. Pin numpy<2.0 in requirements.txt so new builds bundle a compatible
numpy version. (The existing comment already flagged this intention
but the upper bound was never added.)
2. Add a PyInstaller runtime hook (pyi_rth_numpy_compat.py) that installs
a ctypes memmove fallback for torch.from_numpy() at startup. Runtime
hooks run after FrozenImporter is registered so frozen torch is
importable. The fallback catches RuntimeError from the C-level ABI
check and copies the numpy array into a new tensor via raw memory copy,
bypassing the check entirely. This is a belt-and-suspenders fix that
works regardless of the bundled numpy version.
Co-authored-by: aimaaaimaa <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>