Export filenames were derived from only the first 30 characters of the
generation text. Generations with similar wording (a common workflow when
iterating on the same line) produced identical filenames, so exports
collided on disk — the browser appended " (1)"/" (2)" and users ended up
opening audio that didn't match the expected filename.
Append the first 8 chars of the generation id to the .wav and .voicebox.zip
export filenames, in both the backend Content-Disposition headers and the
frontend save-file hooks.
Co-authored-by: Claude Opus 4.8 <[email protected]>
The suite hadn't run green since the routes refactor:
- test_profile_duplicate_names.py imported the pre-refactor module
layout and broke collection; now imports backend.services.profiles
- tests/conftest.py puts the repo root and backend dir on sys.path so
files collect standalone instead of depending on run order
- test_cors.py tested a hand-copied mirror of the origin list that had
drifted from app.py (missing http://tauri.localhost); it now builds
the app via the real create_app() factory
- test_progress.py simulated a 1KB download, below the tracker's 1MB
reporting threshold; simulation raised to 5MB
- slow/timeout markers registered in pyproject
Ruff: ~900 violations auto-fixed (typing modernization, import
sorting, unused imports, whitespace). The remaining rules are baselined
in pyproject.toml with per-rule counts to burn down, plus per-file
carve-outs for deliberate env-before-import ordering. ruff check is
now clean; suite is 134 passed, 2 skipped.
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]>
* 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]>
Critical:
- Remove dead backend.utils.validation PyInstaller hidden import
- Fix story_items table rebuild to preserve track/trim/version columns
- Guard cache migration against same source/destination path
- Fix regeneration audio overwrite (use random uuid suffix per take)
Major:
- Engine selector: validate language on Qwen switch, clear stale modelSize
- Sync language validation regex between profile create and generate (22 langs)
- Guard CUDA download against duplicate concurrent requests
- Only set model_size for engines that support multiple sizes
- Fix 404 swallowed by generic except in history export
- Validate audio_path before FileResponse in export-audio
- Transcription: stream uploads in 1MB chunks, use robust cache check,
call complete_download() on Whisper download success
- Set clean version as default when effects chain validation fails
- Return explicit error when Windows port occupied by non-voicebox process
Split the 2,578-line main.py (90 routes) into 12 domain-specific router
modules under routes/. main.py is now a 45-line entry point.
New structure:
- app.py: FastAPI instance, CORS, startup/shutdown, safe_content_disposition
- routes/: health, profiles, channels, generations, history, transcription,
stories, effects, audio, models, tasks, cuda
- services/cuda.py: moved from cuda_download.py
Also includes Phase 5 database/ package (from parallel agent):
- database/__init__.py re-exports all symbols for backward compat
- database/models.py, session.py, migrations.py, seed.py
All 90 routes verified registered and app imports cleanly.