From e0daafd4e1a8129d9928e9408c03b4928edf0927 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 4 Oct 2026 16:42:28 +0200 Subject: [PATCH] feat(voices): export and import voice bundles Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + Manual.md | 10 ++ architecture/voice-bundle.md | 13 ++ core/voice_bundle.py | 231 ++++++++++++++++++++++++++++++++++ routes/library.py | 35 +++++- static/dist/main.min.js | 2 +- static/js/voice-library.js | 50 ++++++++ static/sections/s-voices.html | 3 + static/style.css | 1 + tests/test_voice_bundle.py | 156 +++++++++++++++++++++++ tests/voice-bundle.spec.js | 50 ++++++++ 11 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 architecture/voice-bundle.md create mode 100644 core/voice_bundle.py create mode 100644 tests/test_voice_bundle.py create mode 100644 tests/voice-bundle.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 08aa616..3050b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Added +- **Voice export/import bundles.** `GET /api/voices/{id}/export` streams a `.voice.zip` (audio, `reference.txt`, `meta.json` with seed, design prompt, rating/tags, `provenance`, `audio_problems`; optional picture and consent recording; `manifest.json` with format version, app version and sha256 per file; no temp files). `POST /api/voices/import` validates at the boundary: 150 MB upload cap, fixed flat member names only (zip-slip/path traversal rejected), per-file, total and compression-ratio caps, hashes verified, audio decoded, meta and provenance re-validated; a taken name gets a `_2` suffix, an identical voice is a no-op, nothing is overwritten. Voice library toolbar: "Export selected" and "Import bundle" (44px). Tests: `tests/test_voice_bundle.py`, `tests/voice-bundle.spec.js`. See `architecture/voice-bundle.md`. - **Consent/provenance field per voice.** Each voice gets `provenance` in its `.meta.json` (`basis`: own_voice, consented, licensed, public_domain or unknown; `source`, `licence`, `note`, optional `recorded_consent`, `set_at`). Existing voices count as `unknown`; `quick-import-voice` prefills the source URL (basis stays `unknown`). New `PUT /api/voices/{id}/provenance` (enum, length caps, control characters stripped, http(s) URLs only, no credentials) and `POST`/`GET /api/voices/{id}/consent-recording` (audio only, 10 MB cap, stored as `.consent`, moves/deletes with the voice). The voice inspector has a "Source & consent" section (typed note or MediaRecorder recording) and voices without a recorded basis show a "Source unknown" badge. Generation is not blocked. Data stays local. Tests: `tests/test_voice_provenance.py`, `tests/voice-provenance.spec.js`. - **Seed Finder: Auto-pick best.** `POST /api/seed-autopick {voice, text, seeds|count}` (default 8, max 16) generates candidates sequentially through the existing temporary-voice path (voices.json is always restored), scores each 0-100 (ASR word accuracy, loudness/clipping, `audio_problems`) and returns all sub-scores plus the winner. Degrades to loudness/problems when STT is unavailable. The UI highlights the winner; saving stays a click on "Use seed". Tests: `tests/test_seed_autopick.py`, `tests/seed-autopick.spec.js`. - **Voice Design: attribute chips.** Toggle chips (gender, age, pitch, pace, accent/region, tone/timbre, emotion; German + English labels) above the design prompt add or remove only their own phrase in the free-text description. The textarea stays fully free-form. Test: `tests/design-chips.spec.js`. diff --git a/Manual.md b/Manual.md index c4fa1ab..bbc6bc1 100644 --- a/Manual.md +++ b/Manual.md @@ -14,6 +14,16 @@ Living manual. Every feature updates this file in the same commit as the code. --- +## Voice export and import bundles + +Purpose: back up a voice or move it between machines (DGX and NUC) as one `.voice.zip` file containing the reference audio, transcript, settings (seed, design prompt, rating, tags, source & consent, audio problems), and optionally the picture and the consent recording. +Prerequisites: Voice library open. Export needs at least one checked voice. +Steps: tick the checkboxes of the voices, press "Export selected"; one `.voice.zip` is downloaded per voice. On the other machine press "Import bundle" and choose one or more `.voice.zip` files. +Expected result: the voice appears in the library. If the name is already taken the import gets a suffix (`_2`, `_3`); an identical voice that is already there is reported as "already present". Existing voices are never overwritten. +Errors: damaged, tampered, oversized or unexpected files are rejected with a message (for example "Hash mismatch"); nothing is imported then. Messages appear as toasts, which screen readers announce. +Mobile / accessibility: both buttons are at least 44 px high and keyboard operable; the file chooser is the browser's own. +Offline: works fully offline. Privacy: a bundle contains the voice, its source/consent notes and any consent recording; share it only with people you trust. + ## Voice source & consent Purpose: record where a voice comes from and why you may use it (own voice, speaker consented, licensed, public domain, unknown). diff --git a/architecture/voice-bundle.md b/architecture/voice-bundle.md new file mode 100644 index 0000000..37284f0 --- /dev/null +++ b/architecture/voice-bundle.md @@ -0,0 +1,13 @@ +# Voice export/import bundles + +Code: `core/voice_bundle.py`; routes in `routes/library.py` (`GET /api/voices/{id}/export`, `POST /api/voices/import`); UI in `static/js/voice-library.js` + toolbar in `static/sections/s-voices.html`. + +Format (v1), flat ZIP, fixed member names only: `manifest.json`, `audio.`, `meta.json`, `reference.txt`, `picture.`, `consent.