1.9 KiB
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.<wav|mp3|m4a|flac|ogg|opus>, meta.json, reference.txt, picture.<jpg|jpeg|png|webp>, consent.<audio ext>. Manifest: format (1), app_version (from VERSION), voice_id, files {member: sha256}. One voice per bundle; multi-select exports one bundle per voice. meta.json is the voice sidecar plus audio_problems; provenance.recorded_consent is only kept when a consent member exists.
Export: streamed through a non-seekable ZIP writer (no temp files); manifest is written last so hashes are computed while streaming.
Import threat model (bundle is hostile): upload cap 150 MB (_copy_limited); member names must be in the fixed set, so absolute paths, .., subdirectories and unknown extensions are rejected before anything is read; names are never used as paths (staged under fixed names in a hidden .import-* dir inside the voices dir, removed in finally); declared sizes are checked and real bytes are re-capped while reading; total uncompressed 200 MB, ratio cap 200 for members over 1 MB; manifest must list exactly the members and every sha256 must match; voice id must match [A-Za-z0-9_-][A-Za-z0-9_.-]{0,99}; audio must decode; meta.json goes through _validate_voice_meta and validate_provenance (set_at is reset). Nothing is executed.
Name handling: never overwrite. Same id with identical audio hash -> unchanged (idempotent). Otherwise _2, _3, ... (max 99) -> renamed. Installed into the active voices dir and added to the voice index.
Not done: no signature (hashes detect corruption/tampering of members but not a re-built manifest); explicit overwrite/skip choice; one request per bundle for multi-file import.