Go
The same engine as a Go module over onnxruntime. No Python, no torch.
Go 1.25 or newer, and one shared library that cannot be vendored: brew install onnxruntime on macOS, apt install libonnxruntime-dev on Linux, the
onnxruntime-win-x64 archive
on Windows. loudkit.Load finds it; set LOUDKIT_ONNXRUNTIME_LIB if yours is
somewhere unusual.
mkdir hello && cd hellogo mod init hellogo get github.com/loudreader/loudkit/go@v0.1.1package main
import ( "log"
loudkit "github.com/loudreader/loudkit/go")
func main() { eng, err := loudkit.Load("loudreader/loudr-1") if err != nil { log.Fatal(err) } defer eng.Close()
v, err := eng.Voice("joe") if err != nil { log.Fatal(err) }
out, err := eng.Synthesize("Hello from loudkit.", v, loudkit.Options{Seed: 7}) if err != nil { log.Fatal(err) } if err := out.SaveWav("hello.wav"); err != nil { log.Fatal(err) }}The first run downloads the model files into
~/Library/Caches/loudkit/loudreader--loudr-1 on macOS and
~/.cache/loudkit/loudreader--loudr-1 on Linux ($LOUDKIT_CACHE moves it),
the directory the Rust, JS and Swift ports share, and checks every file
against the release’s own SHA256SUMS; later runs read what is there.
eng.Voices() names the 28 voices. The snippets need loudkit 0.1.1; from
a checkout, go run ./examples/hello in go/.
Both loudr-1 and loudr-1-turbo use this API in 0.1.1. Change the model
name to switch; keep the same voice profile. A local release directory works
as well as a published model name.
A directory of your own
Section titled “A directory of your own”dir, err := loudkit.DownloadWith("loudreader/loudr-1", "loudr-1", loudkit.Fetch{Revision: "v0.1.1"})eng, err := loudkit.Load(dir)Download writes a receipt, .loudkit-release.json; a later call whose
revision still resolves to the same commit fetches and hashes nothing, a moved
revision keeps every file that still hashes to the new SHA256SUMS and fetches
the rest, and an interrupted fetch resumes. Pin Revision for anything reproducible. Under
loudreader/, release.json must say the bundle passed the builder’s gate,
and it is checked before any weight moves.
Synthesize
Section titled “Synthesize”Synthesize takes text of any length: it splits at sentence boundaries,
gives each chunk its own seed, carries the pitch contour across the joins and
returns one Result. The zero Options is every default.
out, err := eng.Synthesize(text, v, loudkit.Options{ Seed: 7, // 0 when omitted Language: "pl", // the voice's own when empty Speed: 1.25, // [0.5, 2.0], pitch preserved; 0 or 1.0 is an exact bypass PreviousTokens: earlier.Tokens, // continue an earlier result's pitch contour})out.Audio // []float32 at out.SampleRateout.Tokens // the speech tokensout.Chunks // where each chunk lands, and an estimate of each wordout.HitTokenCap // generation stopped at the token cap: probably cut offout.SaveWav(path); out.WriteWav(w)SynthesizeWindow renders exactly one model window and returns an error on
longer text; it is for the conformance harness.
Streaming and barge-in
Section titled “Streaming and barge-in”err := eng.Stream(text, v, loudkit.Options{Seed: 7, ShouldCancel: stopped}, func(c loudkit.Chunk) bool { play(c.Audio) // c.Timing starts at zero; c.Timing.Shifted adds your offset return true // false stops at the next chunk})Stream hands out chunks as they are made, so playback starts before the
passage is finished. ShouldCancel is polled on every decode step, and the
chunk being generated is discarded.
Timestamps and speed
Section titled “Timestamps and speed”out.Chunks is exact at the chunk level and an estimate at the word level;
read timestamps.md before building on the word
times. Speed is refused outside [0.5, 2.0]; see
speed.md.
Cloning a voice
Section titled “Cloning a voice”mine, err := eng.Enroll("me.wav", "mine", "en")if err != nil { log.Fatal(err) }if err := mine.Save("mine.safetensors"); err != nil { log.Fatal(err) }The first Enroll on an engine loaded by repo id fetches the three
enrollment graphs into the same cache directory,
~/Library/Caches/loudkit/loudreader--loudr-1 on macOS and
~/.cache/loudkit/loudreader--loudr-1 on Linux; later calls read them from
there. An engine loaded from a directory of your own needs them fetched with
loudkit.DownloadWith(repo, dir, loudkit.Fetch{Cloning: true}). Five to ten
seconds of clean speech is the input this was tuned for. Enroll reads 8,
16, 24 and 32-bit PCM and 32-bit float WAVs at any rate; EnrollPCM takes
samples. voice.Load(path) reads the profile back.
Execution provider
Section titled “Execution provider”Load takes the best provider the shared library offers. To name one, build
the engine yourself:
eng, err := engine.LoadWith(ckpt, onnxDir, tokPath, config.ExecutionConfig{ ONNXProvider: config.ProviderCPU, // auto, cpu, cuda, coreml, directml})fmt.Println(eng.Provider()) // the provider that ran, never "auto"auto takes cuda where the shared library carries it and cpu otherwise; it
reaches neither coreml nor directml. A named provider the library does not
carry is an error, never a quiet fall back to cpu. The shared library alone
decides which providers exist.
coreml runs the renderer on CoreML and keeps the generator on CPU, so the
speech tokens are identical to a cpu run and the waveform is not
bit-identical. The first run compiles the graphs, about two minutes, cached
under ~/Library/Caches/loudkit/coreml ($LOUDKIT_COREML_CACHE moves it).
CUDA measured 2.68x on an RTX 3090 (measured on 0.1.0), against 0.67x for the CPU provider on the same host. On an Apple M3 Pro the CPU provider runs the shared passage at 1.18x with loudr-1 and 1.70x with loudr-1-turbo, measured on 0.1.1. See the benchmark page for the passage and the runtime details.
Your own layout
Section titled “Your own layout”loudkit.LoadPaths(checkpoint, onnxDir, tokenizer) opens three paths you
assembled yourself, and loudkit.Open(dir) reads a release’s paths without
loading it.
Verify against the shared fixture
Section titled “Verify against the shared fixture”cd go && go test ./conformance/ # weight-free vectorsLOUDKIT_CKPT=… LOUDKIT_ONNX_DIR=… LOUDKIT_VOICE=… LOUDKIT_ONNXRUNTIME_LIB=… go test ./conformance/The second needs the checkpoint, the graphs, the reference voice and the
shared library, and holds Synthesize and SynthesizeWindow to the fixture’s
tokens, chunk by chunk.
A little feedback helps us build.
With your permission, Google Analytics helps us understand which pages and features people use. Analytics is optional. We never send your voice recordings or prompt contents.
What gets measured?
Page visits and basic interactions, such as playing a sample, copying the setup prompt and opening a setup link. Google receives technical information such as your browser and IP address. We do not enable advertising features. Your cookie choice is saved on this device for six months; you can change it here anytime.
