Releases: loudreader/loudkit
Release list
v0.1.1
The fingerprint moves: 79f71f5821477353 -> 7cd75498ad4e7531. Changes
to sampling, postprocessing, chunking and text preparation are audible, so the same text, voice and seed render
differently than they did on 0.1.0. That is what a moved fingerprint means and
it is the only way this project is allowed to change a reading:
the identity contract lists what
loudkit-1 now includes, and every amendment below carries the measurement
that bought it.
Both model bundles now include all 28 voices: ten English voices and two for
each of the other nine languages. Henry, Oliver, Miles, Oscar, Clara, Emma,
Lucy and Sophie load by name without a separate profile download.
Python voice-profile saving now packs strided tensors into contiguous storage
before passing them to safetensors. Cropped enrollment mels could otherwise be
silently saved with incorrect values, changing the voice after reload in both
models. Already affected profiles must be regenerated from their source audio;
loading and resaving a corrupted file cannot restore the missing values.
Why a patch release moves it: before 1.0 the patch component tracks the API
surface and the fingerprint carries the audible contract, a clause
COMPATIBILITY.md states. This entry meets it.
All five SDKs read both checkpoint formats. Turbo uses format_version 2;
version-1 checkpoints retain their existing interpretation.
Source-breaking in Swift: TokenGenerator.generate now throws
(LoudKitError.cancelled at the poll that fires), so code that reaches the
generator through Engine.tokenGenerator must try. No compatibility shim.
Source-breaking for manually constructed voice profiles: Rust voice::Profile
now stores enrolment; add enrolment: "first-10s".into() to a struct literal
(or preserve the actual method if known). Go callers using positional voice.Profile
literals must add the new Enrolment field; keyed literals can omit it. Loading
and enrolling profiles use the same API and now preserve the enrollment method
when saved across all five SDKs.
Added
-
mp3andopusfrom every door:/v1/synthesize,/v1/audio/speech,
gRPC and MCP. Both come from the codecs of the libsndfile thatsoundfile
bundles, so neither adds a package. Hermes Agent and OpenClaw ask an
OpenAI-compatible server for mp3, or for Opus when the reply is a voice note,
and now get it fromloudkit serve.aacstays refused: libsndfile cannot
write it. -
The long-form conformance case is now driven through
engine.streamand
engine.synthesizeas well as chunk by chunk, so Python consumes the flat
long_form.cases[].tokenslist the four ports consume; the parity page's
seven weighted rows are compared by aslowtest that regenerates with
--checkpoint(theparityjob runs it); enrollment from a WAV file is held
to the enrollment fixture;lk.enrollandloudkit speakon a turbo release
directory have goldens;Result.save()is pinned to the shared quantise
rule; the production fingerprint is pinned weight-free; and the Swift
offline-receipt test asserts the stderr line the other four ports assert. -
loudkit textpreviews what will be spoken without loading a model: the
prepared text on stdout, a word diff of what the funnel changed on stderr.
loudkit --helplists eight commands. -
loudkit speak --playplays the WAV it just saved through the system
player (afplayon macOS,aplayorpaplayon Linux). -
Speed measured on 0.1.1 for both models: eight local paths on an Apple M3
Pro (PyTorch split CPU/MPS, native CoreML, ONNX Runtime CPU, the CPU
reference, and the Swift, Rust, Go and TypeScript ports) and six NVIDIA
parts (A100, L4, RTX 3090, T4, GTX 1080 Ti, Jetson Orin Nano), eager and
with CUDA graphs, plus batched throughput on four of them. loudr-1-turbo
runs 1.4x to 2.1x faster than loudr-1 end to end and 2.6x to 2.8x faster in
aggregate throughput (Benchmarks). -
A second model,
loudreader/loudr-1-turbo, behind the same one line.
lk.load("loudreader/loudr-1-turbo")is the whole difference: same voices,
same API, same seeds. It is thefusion_mtp2generator below beside a
renderer distilled to one Euler step, published as a second repository with
the same layout. Python, Swift, Go, Rust and TypeScript support both models,
using the matching ONNX graphs or CoreML packages. One enrolled voice
profile works with both models.
Choosing a model says which is which. -
A second decode mode,
fusion_mtp2, two speech tokens per transformer
forward: one head reads the hidden state, a second reads that state beside
the first token's embedding, and a fusion MLP folds both into the one KV
slot the pair occupies. Checkpoints that carry it declaredecode.modein
their manifest and ship asformat_version 2. The mode is a property of
the weights, not a runtime option: it is fingerprinted, and a mode/weights
disagreement is a load error. Measured with CUDA graphs against 0.1.0 on
the same lineage, RTF on the short / medium / long benchmark samples:0.1.0 0.1.1 RTX 3090 1.22 / 6.03 / 7.73 1.43 / 7.47 / 8.45 Jetson Orin Nano 0.53 / 1.60 / 1.84 0.63 / 1.82 / 2.16 Beside it, none fingerprinted: sampling runs on the device inside the
captured graph, graphs are kept across calls, the excitation noise is drawn
on the GPU, and the vocoder no longer vocodes padding it throws away
(ExecutionConfig.vocoder_ragged, on by default on CUDA renderers, in the
identity contract'sequivalentclass;--no-vocoder-raggedfor byte
agreement with a 0.1.0 build). End-to-end figures:docs/benchmarks.md. -
New release bundles use a 20 ms raised-cosine ramp on both window edges
to soften onset taps. The tap is a hot first mel frame from the flow decoder
on three of the 28 voices, not a vocoder edge. Measured on oscar, the hottest:
the residual burst in the first 12 ms of a window falls from 2.2 dB below the
window RMS with the old 5 ms ramp to 14.7 dB below with 20 ms, at a cost of
0.7 dB over the first 50 ms of a window that opens on speech; 50 ms would
cost 3 dB there.edge_fade_secondsis part of the algorithm and its
fingerprint, including the new 0.02 s default. Only the historical 0.005 s
spelling stays omitted for identity compatibility. -
loudkit cloneends the reference prompt in silence by default: the clip
is cut at its last pause before the ten-second limit, or at 9.6 s when no
pause fits, and padded with 0.4 s of silence, because the model speaks from
where the prompt ends. On one cloned voice the onset moved from 0 to 50 ms
and the first 60 ms from -32 to -51 dBFS.--no-end-in-silencekeeps the
recording as given, which is what the library'senrolldoes unless asked.
Profiles made with the cut carry the enrolment labelfirst-10s-pause. -
Every download directory carries a receipt,
.loudkit-release.json:
the repo, the revision asked for, the commit it resolved to, the digest of
SHA256SUMSand when it was fetched. A load whose revision still resolves
to that commit hashes nothing; any other receipt, or none, fetches and
verifies again, keeping every file that already hashes to the new manifest;
a load that cannot reach the hub uses the receipt and says so on stderr.
The same rules in Python (loudkit download --local-dir), Go, Rust, JS
and Swift, pinned by one fixture. One hub call per load; a hub answer
naming no commit is an error in all five. -
The JS binding no longer needs Python.
download(repo, dir)fetches
the ONNX release overfetchwith no new dependency, on the same plan and
rules asloudkit download --for onnx;examples/hello.mjsis the
quickstart.Enroller.enrolltakes a WAV path or bytes as well as samples. -
Go:
safetensors.Write,voice.Profile.Save,engine.Result.SaveWav.
Rust:safetensors::write,voice::Profile::save,hub::rejected_name,
hub::parse_sums,hub::is_official,Bundle::can_enroll,
execution::locate_runtime. JS:saveVoice,profileFrom,
writeSafetensors,hub.rejectedName,hub.parseSums,hub.canEnroll,
Enroller.close. Swift:Safetensors.write,VoiceProfile.save,
Engine.voiceNames,Engine.voice(named:),Engine.enroll(contentsOf:). -
export.jsonbeside every exported graph set, ONNX and CoreML, naming
what each file was traced from. The backends refuse a set whose members
disagree with each other or with the checkpoint; a release requires one.
Changed
-
Turbo CoreML uses an fp32 estimator: waveform correlation with ONNX now
exceeds 0.999 on both conformance cases. The stricter gate is shared. -
Python downloads the native generator and fusion graphs required by the
chosen backend. Legacy renderer-only CoreML bundles remain readable.
Complete CoreML bundles run without PyTorch; explicit fp16 token generation
retains the faster PyTorch/CoreML path. See the measured tradeoff in
benchmarks. -
Go, Rust, JS, Swift:
Enroll/enroll_wav/enrollon an engine loaded
by repo id fetches the three enrollment graphs (Swift: packages) into the
engine's own cache directory the first time, through the same verified
download, and reads them from there afterwards. An engine loaded from a
directory of your own still says how to fetch them (Cloning: true/
cloning: true/{ cloning: true }). -
Go, Rust, JS, Swift: one cache layout,
<user cache>/loudkit/<org>--<name>
(~/Library/Cacheson macOS,$XDG_CACHE_HOMEor...
v0.1.0
Full Changelog: https://github.com/loudreader/loudkit/commits/v0.1.0