8-gen bench (4 emotions × 2 corpora) at seed=42 against firdhokk
Whisper-LV3:
target RAVDESS CREMA-D
happy happy (0.999) ✓ happy (0.999) ✓
angry neutral (0.92) sad (0.99)
fearful happy (0.998) fearful (0.984) ✓
sad angry (0.99) fearful (0.99)
CREMA-D 2/4 vs RAVDESS 1/4. Larger / more naturalistic corpus
produces more class-pure fearful direction. Neither corpus solves
angry or sad — recipe shifts into 'vague expressivity' rather than
class-specific corners.
Practical: prefer CREMA-D when available; A/B both per emotion if
class precision matters.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the gap where converse_server (Phase 12.4) supported LoRA but
the simpler HTTP-only tts_server didn't. Same flag set
(--lora / --lora-rank / --lora-alpha / --extended-lora) and same
apply_lora_adapter shared helper. Combines with --quantized-gguf
(Phase 12.6) for Q8 + voice clone over plain HTTP.
Verified end-to-end on Metal: tts_server --quantized-gguf … --lora …
boots, injects LoRA into the quantized backbone (q=16 k=16 v=16 o=16
+ MLP w1/w2/w3 = 224 tensors), listens. Single HTTP POST /v1/tts
returned 200 OK with a 146KB 24kHz mono WAV. Lib suite 110/110.
tts_server is now the simplest production deploy for a personalized
voice: HTTP-only, no STT/LLM overhead, LoRA + Q8 + AudioSeal/SilentCipher
+ WavLM-SV all available behind one binary.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Watermarker trait gains embed_with_message(audio, message) with a
default impl forwarding to embed (no-op for watermarkers without a
payload). AudioSealWatermarker overrides to use the requested message
instead of self.message; ResampledWatermarker forwards through the
resample dance.
TtsRequest gains optional watermark_message: Option<String> (decimal or
0xHEX). Useful for clawsample to tag each generation with a unique ID
(e.g. job_id mod 0x10000) for audit trails. When omitted, falls back
to the server-startup --audioseal-message default.
Verified end-to-end: override "0xBEEF" -> detect 0xBEEF (mean_presence
0.9995, 16/16 bits). Default fallback also decodes correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Streams 16-bit little-endian PCM (24 kHz mono) as Mimi produces chunks.
Wraps Generator::generate_streaming via spawn_blocking + tokio::sync::mpsc
bridge into an axum Body::from_stream response.
Same JSON request format as /v1/tts; Content-Type is
audio/L16; rate=24000; channels=1 per RFC 2586.
First-byte (first audio chunk) latency on Metal: ~880 ms vs ~6 s wall
for the non-streaming /v1/tts path — 6.8x faster perceived UX, the
difference between "the app froze" and "the app started speaking."
Caveat: streaming endpoint does NOT apply post-processing or the inline
watermarker (those operate on the full utterance). For watermarked
output use /v1/tts. A chunked AudioSeal port is the natural follow-up
for streaming watermarking.
Adds futures-util as a dev-dependency for the Stream trait.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Extends the HTTP service with three new endpoints exposing AudioSeal
detection and WavLM-SV speaker scoring alongside the existing TTS:
GET /health
POST /v1/tts audio/wav (24 kHz mono)
POST /v1/detect [audio] JSON { mean_presence, message_hex }
POST /v1/speaker_embed [audio] JSON { embedding: [512 floats] }
POST /v1/speaker_compare [a+b] JSON { cosine }
Wires the inline watermarker into /v1/tts when --audioseal-* flags are
set: every TTS response is auto-watermarked through the
ResampledWatermarker (24 kHz <-> 16 kHz) adapter.
Verified end-to-end on Metal:
/health -> ok
/v1/tts -> 200, 145964 bytes (3s @ 24kHz)
/v1/detect -> mean_presence=0.998 on watermarked output
/v1/speaker_embed -> 512-d float vector
/v1/speaker_compare a==b -> cosine 1.0000001
axum gains the "multipart" feature for audio uploads.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>