package autogen // audio.cpp (0xShug0/audio.cpp) is one ggml runtime for ~72 speech and audio // model families: text-to-speech, voice cloning, speech recognition, music // generation, stem separation and (not wired here yet) voice conversion. It // speaks the same // OpenAI surface the existing engines do - /v1/audio/speech, // /v1/audio/transcriptions, /v1/audio/voices, /health - so it lands in the // existing tts and asr classes rather than needing new ones, and it is the first // backend kind to serve TWO classes at once (see kindClasses in vllm.go). // // Two things make its emitter unlike every other class here: // // 1. audiocpp_server has NO --model flag. A model can only be named in a JSON // config file passed as --config. Every other knob is an argv flag, and argv // wins over the file. Rather than litter the disk with one checked-in JSON // per model, the emitted YAML carries a typed `audiocpp:` block and the // spawn hook materializes a single-model config from it at launch (see // internal/server/audiocppconfig.go), deleting it on stop. // // 2. It ships a model manager of its own - lazy loading, an LRU residency cap, // idle unloads, a free-memory admission guard. That is a SECOND scheduler // sitting behind ours, making residency decisions the router cannot see. It // is contained rather than used: one model per generated config (structural, // so there is nothing for an LRU to choose between), --max-loaded-models 1, // idle_unload_ms and min_free_memory_mb left at 0, lazy_load left at its // false default so the listen socket opening really does mean the weights // are resident, and concurrencyLimit 1 in the YAML so requests queue in our // scheduler instead of on audio.cpp's busy mutex. The /v1/models/load, // /v1/models/unload and /v1/tasks/unload_models routes are deliberately not // proxied anywhere. import ( "fmt" "os" "path/filepath" "regexp" "slices" "sort" "strings" ) // audioCppKind is the registry kind for an audio.cpp build. The aliases are the // ones kindClasses accepts, so anything that can reach the registry can be // tested with isAudioCppKind. const audioCppKind = "audiocpp" // isAudioCppKind reports whether a registry kind names an audio.cpp build. The // other speech emitters use it as a negative guard: audio.cpp serves their class // but reads neither of their weight formats, so a resolved audio.cpp row must // read to them as "nothing matched" rather than as a usable engine. func isAudioCppKind(kind string) bool { switch strings.ToLower(strings.TrimSpace(kind)) { case "audiocpp", "audio.cpp", "audiocpp-server": return true } return false } // The classes an audio.cpp family serves, as a set: a handful of families (see // firered_audio, vevo2) do several jobs, and the emitted capabilities block has // to describe the one task the generated config actually configures. const ( audioClsTTS = 1 << iota audioClsASR audioClsMusic // task "gen", answered on /v1/music/* audioClsSep // task "sep", answered on /v1/audio/separations audioClsMidi // task "midi", answered on /v1/music/transcriptions ) // audioCppFamilies maps audio.cpp's family id - the value its converter stamps // into the gguf as audiocpp.model_spec.family - onto the classes we serve it // under. Derived from upstream's model_specs/*.json, one entry per spec file, // with that spec's own task list and support status in the trailing comment. // // A family at 0 is KNOWN and deliberately not emitted: voice conversion, // codecs, alignment, diarization and speech-to-speech have no class, route or // UI here yet. Music and separation ARE served, but only for the families whose // request options the typed routes in internal/server/musicapi.go know // (ace_step, minimax_music3, yue2, htdemucs, sheetsage2): each family reads a different option // set, and one the route has no whitelist for would 400 every request. // // A family MISSING from this table is one upstream added after the table was // written; emitAudioCppModel says so in the YAML rather than guessing a class // from the name. // // "clone" counts as tts: a clone-only family (echo_tts, confucius4_tts) is a // speech synthesizer whose voice must come from a reference clip rather than a // preset, which is a request-shape difference, not a different class. var audioCppFamilies = map[string]int{ "ace_step": audioClsMusic, // music,edit (supported) "apollo": 0, // s2s (supported) "audio8_asr": audioClsASR, // asr (community) "audio8_tts": audioClsTTS, // tts,clone (community) "audiosr": 0, // s2s (experimental) "auk": 0, // tts,edit (experimental): a folder of three components (generator + Qwen2.5-Omni conditioner + VAE), not wired "breeze_tts": audioClsTTS, // tts,clone,design (supported) "bs_roformer": 0, // sep (supported) "builtin_audio_utils": 0, // s2s (experimental) "canary_asr": audioClsASR, // asr (supported) "chatterbox": audioClsTTS, // tts,clone,vc (supported) "chatterbox_turbo": audioClsTTS, // tts (testing) "citrinet_asr": audioClsASR, // asr (supported) "cohere_asr": audioClsASR, // asr (supported) "confucius4_r2t2": audioClsASR, // asr (community) "confucius4_tts": audioClsTTS, // clone (experimental) "controlfoley": 0, // sfx (experimental) "cosyvoice3": audioClsTTS, // tts,clone (supported) "dots_tts": audioClsTTS, // tts,clone (supported) "dramabox": audioClsTTS, // tts,clone (experimental) "echo_tts": audioClsTTS, // clone (experimental) "f5_tts": audioClsTTS, // tts,clone (community) "firered_audio": audioClsTTS | audioClsASR, // asr,tts,clone,design,edit (experimental) "fireredtts3": audioClsTTS, // tts,clone,design (experimental) "fish_audio": audioClsTTS, // tts,clone (supported) "fun_asr_nano": audioClsASR, // asr (wip) "glm_tts": audioClsTTS, // tts,clone (community) "granite5asr": audioClsASR, // asr (supported) "heartmula": 0, // music (supported) "higgs_audio_stt": audioClsASR, // asr (supported) "higgs_audio_tts": audioClsTTS, // tts,clone (supported) "htdemucs": audioClsSep, // sep (supported) "hviske_asr": audioClsASR, // asr (supported) "index_tts2": audioClsTTS, // tts,clone (supported) "inflect_v2": audioClsTTS, // tts (community) "irodori_tts": audioClsTTS, // tts,clone,design (supported) "kitten_tts": audioClsTTS, // tts (community) "kokoro_tts": audioClsTTS, // tts (community) "kroko_asr": audioClsASR, // asr (community) "liveavatar": 0, // sfx (experimental) "magpie_tts": audioClsTTS, // tts (supported) "meanvc2": 0, // vc (supported) "mel_band_roformer": 0, // sep (supported) "midashenglm_gen": 0, // music (experimental) "minimax_h3": 0, // tts,music,sfx (experimental) "minimax_music3": audioClsMusic, // music (experimental) "miocodec": 0, // codec,vc,s2s (supported) "miotts": audioClsTTS, // tts,clone (supported) "mira_tts": audioClsTTS, // tts,clone (experimental) "mms_forced_aligner": 0, // align (community) "moonshine_asr": audioClsASR, // asr (experimental) "moss_transcribe_diarize": audioClsASR, // asr (supported) "moss_tts_local": audioClsTTS, // tts,clone (supported) "moss_tts_nano": audioClsTTS, // tts,clone (supported) "moss_tts_v15": audioClsTTS, // tts,clone (experimental) "moss_ttsd": audioClsTTS, // tts,clone (experimental) "moss_voicegen": 0, // vdes (community) "muscriptor": 0, // midi (supported) "nemotron_3_diar": 0, // diar (supported) "nemotron_asr": audioClsASR, // asr (supported) "neutts": audioClsTTS, // tts (supported) "niagara_asr": audioClsASR, // asr (supported) "omnivoice": audioClsTTS, // tts,clone,design (supported) "outetts": audioClsTTS, // tts,clone (community) "parakeet_tdt": audioClsASR, // asr (community) "personaplex": 0, // s2s (supported) "piper_tts": audioClsTTS, // tts (community) "pocket_tts": audioClsTTS, // tts,clone (supported) "pulsevad": 0, // vad (supported) "qwen3_asr": audioClsASR, // asr (supported) "qwen3_forced_aligner": 0, // align (supported) "qwen3_tts": audioClsTTS, // tts,clone,design (supported) "rvc": 0, // vc (experimental) "sanotts": audioClsTTS, // tts (community) "seed_vc": 0, // vc,svc (supported) "sense_asr": audioClsASR, // asr (community) "sheetsage2": audioClsMidi, // midi (supported) "soprano_tts": audioClsTTS, // tts (community) "sopro_tts": audioClsTTS, // tts,clone (community) "sortformer_diar": 0, // diar (supported) "sortformer_diar_v2": 0, // diar (community) "stable_audio": 0, // music,sfx,edit (supported) "supertonic": audioClsTTS, // tts (supported) "universr": 0, // s2s (supported) "vevo2": audioClsTTS, // tts,music,vc,edit,svc,s2s (supported) "vibeasr": audioClsASR, // asr (community) "vibevoice": audioClsTTS, // tts (supported) "vibevoice_asr": audioClsASR, // asr (supported) "vibevoice_asr_streaming": audioClsASR, // asr (supported) "vieneu_v3_turbo": audioClsTTS, // tts,clone (community) "vietneu_tts": audioClsTTS, // tts,clone (community) "voxcpm1": audioClsTTS, // tts,clone (supported) "voxcpm2": audioClsTTS, // tts,clone,design (supported) "voxtral_realtime": audioClsASR, // asr (supported) "yue2": audioClsMusic, // music (supported) "zipvoice": audioClsTTS, // tts,clone (community) } // audioCppClones lists the families that can register a NEW voice from a // reference clip, which is what gates the playground's cloning UI. Same source // as audioCppFamilies: the spec's task list carries "clone". var audioCppClones = map[string]bool{ "audio8_tts": true, "breeze_tts": true, "chatterbox": true, "confucius4_tts": true, "cosyvoice3": true, "dots_tts": true, "dramabox": true, "echo_tts": true, "f5_tts": true, "firered_audio": true, "fireredtts3": true, "fish_audio": true, "glm_tts": true, "higgs_audio_tts": true, "index_tts2": true, "irodori_tts": true, "miotts": true, "mira_tts": true, "moss_tts_local": true, "moss_tts_nano": true, "omnivoice": true, "outetts": true, "pocket_tts": true, "qwen3_tts": true, "sopro_tts": true, "vietneu_tts": true, "voxcpm1": true, "voxcpm2": true, "moss_tts_v15": true, "moss_ttsd": true, "vieneu_v3_turbo": true, "zipvoice": true, } // audioCppMusicEdits lists the music families that implement the edit routes // (repaint / complete / cover), which gates /v1/music/edits. var audioCppMusicEdits = map[string]bool{"ace_step": true} // AudioCppScore describes a music family that can be conditioned on a written // score. It is what turns on the playground's score box (the model's // capabilities.musicScore names the notation) and what the generation route // maps its typed `score` field through, so a new family is one row here. type AudioCppScore struct { // Format is the notation the family reads ("abc"), advertised as // capabilities.musicScore. Format string // Option is the request option the score text is sent in. Always an // INLINE option: the path-valued twins (YuE2's abc_file) are a server-side // file read and never reach audio.cpp from a request. Option string // ModeOption/ModeOff name a planning option whose ModeOff value skips the // score stage, so a score sent with it would be ignored without a word. // Blank when the family has no such switch. ModeOption, ModeOff string } // audioCppMusicScores lists the music families that take a score. var audioCppMusicScores = map[string]AudioCppScore{ "yue2": {Format: "abc", Option: "abc", ModeOption: "cot", ModeOff: "off"}, } // AudioCppMusicScore reports whether an audio.cpp family takes a score, and how. func AudioCppMusicScore(family string) (AudioCppScore, bool) { sc, ok := audioCppMusicScores[family] return sc, ok } // audioCppFamilySessionOptions are session options a single-file family always // gets. ace_step.mem_saver frees the per-request graph buffers instead of // keeping the largest ones warm: without it a render that follows a LONGER one // in the same process comes out different (corr 0.9999, not byte-equal) from // the same seed rendered fresh, so a seed stopped reproducing a result. With it // every render matches audiocpp_cli byte for byte, at no measurable cost on // warm requests (Vulkan, 20 s @ 8 steps: ~40 s either way). var audioCppFamilySessionOptions = map[string]map[string]string{ "ace_step": {"ace_step.mem_saver": "true"}, } // audioCppDirFamilies are the families whose model is a DIRECTORY of component // ggufs rather than one file, keyed to the component that anchors the emit. // MiniMax Music 3 ships a language model, an RVQ depth decoder and a flow // transformer per quant beside one shared vocoder and condition encoder, and // audio.cpp loads the folder, told which quant of each component to use through // session_options. Only the shared files carry the family id in their header; // the per-quant components carry none. var audioCppDirFamilies = map[string]string{"minimax_music3": "vocoder.gguf"} // audioCppDirQuants are the component quants a MiniMax Music 3 package is // published in, best first. A model is emitted per quant whose language model // and flow transformer are both on disk; the RVQ depth decoder falls back to // q8_0, which the q4_0 package ships in place of a q4_0 one. var audioCppDirQuants = []string{"bf16", "q8_0", "q4_0"} // miniMaxF16OnVulkan reports whether a MiniMax Music 3 quant must be loaded // with weight_type f16. On the Vulkan build the q8_0 components render a hiss // instead of music unless the weights are converted to f16 on load (seen in // use on an RX 7900 XTX). It is not the bf16 tensors those files also carry: // with weight_type native, audio.cpp already stores bf16 as f16 on Vulkan // (backend_weight_store.h). What f16 adds is DEQUANTIZING the q8_0 tensors, // so the package's own bf16 quant needs nothing and gets nothing. Upstream // says not to combine weight_type with component selection; on this backend // that advice is what produces the noise. func miniMaxF16OnVulkan(s Settings, ov *Override, q string) bool { if q == "bf16" { return false } be := audioCppBackend(s, ov, audioCppClass("minimax_music3")) return be.Exe != "" && audioCppFlavour(s, be.ID) == "vulkan" } // AudioCppWeightTypeFamilies are the audio.cpp families whose weight storage // the model editor offers as a choice (Override.AudioWeightType). var AudioCppWeightTypeFamilies = []string{"minimax_music3"} // Override.AudioWeightType values. Blank is auto. const ( AudioWeightNative = "native" AudioWeightF16 = "f16" ) // miniMaxF16 resolves whether a MiniMax Music 3 quant loads as f16: an // explicit Override.AudioWeightType wins, blank falls back to the Vulkan rule. // The user pin exists because the auto rule is a trade, not a fix: f16 costs a // q4_0 quant the VRAM of a q8_0 one, and a backend whose native path renders // music (or a future audio.cpp that fixes Vulkan) should be able to opt out. func miniMaxF16(s Settings, ov *Override, q string) bool { if ov != nil { switch ov.AudioWeightType { case AudioWeightF16: return true case AudioWeightNative: return false } } return miniMaxF16OnVulkan(s, ov, q) } // AudioCppWeightTypeAuto is what a blank Override.AudioWeightType resolves to // for an emitted model, given its family and its generated session_options // (which name the quant through the language model file). "" for a family // that offers no choice. The editor labels its Auto option with this, so it // reads the pin-free rule rather than the launched config, which already // reflects any pin. func AudioCppWeightTypeAuto(s Settings, ov *Override, family string, opts map[string]string) string { if !slices.Contains(AudioCppWeightTypeFamilies, family) { return "" } var auto Override if ov != nil { auto = *ov } auto.AudioWeightType = "" q := strings.TrimSuffix(strings.TrimPrefix(opts[family+".language_model_gguf"], "language_model_"), ".gguf") if miniMaxF16(s, &auto, q) { return AudioWeightF16 } return AudioWeightNative } // ggufF16GB is a gguf's weights once converted to f16 on load, or the file // size when the header cannot be read. func ggufF16GB(path string, fileGB float64) float64 { meta, err := ReadGgufMetadataCached(path) if err != nil || meta.ParamCount <= 0 { return fileGB } return float64(meta.ParamCount) * 2 / (1 << 30) } // audioCppCompanion describes a family whose model is one main gguf plus a // companion gguf beside it in the same folder. audio.cpp loads the FOLDER and is // told both file names through session options, which are relative to it. type audioCppCompanion struct { mainOption, companionOption string // files are the companion files to look for, best first. files []string // fullFiles replaces files for an unquantized main file (bf16/f16/f32 in its // name): a full-precision model is asked for when quality is the point, so it // gets the full-precision companion first. fullFiles []string } // audioCppCompanions are the main-plus-companion families. YuE2 publishes its // 3B AR/NAR model per quant and its Oobleck VAE per precision as separate // packages into one folder; only the main file carries family=yue2. The VAE // never reaches this emitter as a row of its own: discovery drops any "-vae" // file as a diffusion component (encoderFileRe), and the main model finds it // by name. var audioCppCompanions = map[string]audioCppCompanion{ "yue2": { mainOption: "yue2.model_gguf", companionOption: "yue2.vae_gguf", files: []string{"yue2-vae-f16.gguf", "yue2-vae-f32.gguf"}, fullFiles: []string{"yue2-vae-f32.gguf", "yue2-vae-f16.gguf"}, }, } // audioCppMusicOverheadGB is audioCppOverheadGB for a music generator: a song // is minutes of audio latent plus a vocoder decode over all of it, which is not // the working set of a sentence of speech. A first estimate, to be replaced by // a measured one. const audioCppMusicOverheadGB = 1.0 // audioCppMidiOverheadGB is the pad for a score transcriber (SheetSage2): its // session reserves fixed arenas regardless of the song, weight_context_mb 1024 // plus decoder_graph_arena_mb 1536 by default, on top of the encoder's own // working set. Upstream's defaults, not a measurement. const audioCppMidiOverheadGB = 2.75 // audioCppTranscribeFormat is the score notation a "midi" family returns, as // emitted in the musicTranscribe capability. SheetSage2 is the only one, and // it writes ABC. const audioCppTranscribeFormat = "abc" // audioCppOverheadGB pads a family's weights into a VRAM admission estimate: the // session buffers, the vocoder or audio tokenizer that loads alongside the model // and the runtime's own context. A flat pad, for the same reason qwentts gets // one - these are sub-GB models with no KV window to size and no offload to plan. const audioCppOverheadGB = 0.4 // IsAudioCppModel reports whether a GGUF is an audio.cpp conversion. Unlike // every other speech detector here there is no filename heuristic and no arch // guessing: audio.cpp's converter writes general.architecture=audiocpp and its // own family id into the header, so the file states outright both that it is // ours and which family it belongs to. func IsAudioCppModel(meta Metadata) bool { return strings.EqualFold(strings.TrimSpace(meta.Architecture), audioCppKind) || strings.TrimSpace(meta.AudioFamily) != "" } // audioCppTask is the "task" the generated config declares for a family, and // with it the capabilities the model is advertised under. ONE task, even for a // dual-task family: the config entry names a single task, so advertising // transcription on a process configured for speech would route requests at a // route it was not set up to answer. TTS wins the tie, which is what the // dual-task families are primarily published as. Music is "gen", separation // "sep" and score transcription "midi", audio.cpp's own task names. Returns "" for a family with no class here // (voice conversion et al) and for one the table does not know. func audioCppTask(family string) string { switch cls := audioCppFamilies[strings.ToLower(strings.TrimSpace(family))]; { case cls&audioClsTTS != 0: return "tts" case cls&audioClsASR != 0: return "asr" case cls&audioClsMusic != 0: return "gen" case cls&audioClsSep != 0: return "sep" case cls&audioClsMidi != 0: return "midi" } return "" } // audioCppKnownFamily reports whether the family appears in the table at all, // which is what tells "upstream added a family since this table was written" // apart from "this family is a music model we do not serve yet". func audioCppKnownFamily(family string) bool { _, ok := audioCppFamilies[strings.ToLower(strings.TrimSpace(family))] return ok } // IsAudioCppKind reports whether a registry row's kind is the audio.cpp engine. // Exported for the model browser, which has to find the INSTALLED audio.cpp to // read its shipped model catalog; the spelling set stays in one place. func IsAudioCppKind(kind string) bool { return isAudioCppKind(kind) } // AudioCppFamilySupport answers, for one audio.cpp family id, what this build // would do with a model of that family: the task it is emitted as ("tts"/"asr", // empty for a family we do not serve) and whether the family is in the table at // all. Exported for the model browser's audio.cpp catalog, which must offer only // downloads that become a working model row -- and must say WHY when it does // not, since "music, not wired up yet" and "newer than this backend's table" are // different answers for the user. // // One table for both callers on purpose: a catalog that decided this for itself // would start advertising families the emitter then refuses. func AudioCppFamilySupport(family string) (task string, known bool) { return audioCppTask(family), audioCppKnownFamily(family) } // audioCppClass is the backend class the family is served under here. Music and // separation resolve as "tts": the class only picks WHICH audio.cpp build row // launches the model (resolution is filtered to audio.cpp rows anyway), and a // class of their own would add a default-star slot to the backends page that // could only ever hold the same build. func audioCppClass(family string) string { if audioCppTask(family) == "asr" { return "asr" } return "tts" } // audioCppBackend resolves the audio.cpp build serving this model. Kind-checked // rather than class-resolved: audio.cpp shares the tts and asr classes with // qwentts.cpp, TTS.cpp and parakeet.cpp, and falling back to the class default // would launch one of those against weights it cannot read. A zero exe means no // audio.cpp build is registered, which emitAudioCppModel reports in the YAML. // withoutAudioCpp hides the audio.cpp rows from a resolver run on behalf of // another engine. Dropping the row is stronger than checking the answer // afterwards: audio.cpp can hold the class star while a real parakeet or // TTS.cpp row sits further down the registry, and "the star is audio.cpp" must // degrade to the NEXT installed engine of that class, not all the way past the // registry to the legacy derived exe. The audio.cpp emitter never comes through // here -- it resolves its own row by kind (audioCppBackend). func withoutAudioCpp(s Settings) Settings { keep := make([]BackendEntry, 0, len(s.Backends)) for _, e := range s.Backends { if isAudioCppKind(e.Kind) { continue } keep = append(keep, e) } s.Backends = keep return s } // onlyAudioCpp is withoutAudioCpp's mirror: the registry as an audio.cpp model // sees it. Filtering before resolution rather than checking the answer is what // makes a cross-engine pin a NO-OP instead of a broken launch - Override.Backend // is looked up in this list, so a row pinned to TTS.cpp resolves to nothing and // falls through to the installed audio.cpp build, while a pin at a specific // audio.cpp BUILD row (vulkan vs cuda) still wins, which is what pinning is for. // The model editor offers these ggufs no other engine anyway: audio.cpp weights // are its own format, and the header, not the pin, is what routes them here. func onlyAudioCpp(s Settings) Settings { keep := make([]BackendEntry, 0, len(s.Backends)) for _, e := range s.Backends { if isAudioCppKind(e.Kind) { keep = append(keep, e) } } s.Backends = keep return s } func audioCppBackend(s Settings, ov *Override, class string) resolvedBackend { be := resolveBackendPreferring(onlyAudioCpp(s), ov, class, audioCppKind) if !isAudioCppKind(be.Kind) { return resolvedBackend{} } return be } // audioCppFlavour maps the installed variant id onto audio.cpp's --backend // value. This is load-bearing, not cosmetic: the server's built-in default is // CUDA, so a Vulkan or CPU build launched without the flag tries a device it was // not compiled for. Returns "" for a hand-entered path with no recorded variant, // where guessing would be the same mistake in the other direction. func audioCppFlavour(s Settings, id string) string { variant := "" for _, e := range s.Backends { if e.ID == id { variant = strings.ToLower(strings.TrimSpace(e.Variant)) break } } switch variant { case "cuda": return "cuda" case "vulkan": return "vulkan" case "rocm", "hip": // The catalog and the device listing both say rocm; the flag only takes hip. return "hip" case "metal", "metal-x64": return "metal" case "cpu", "cpu-portable": return "cpu" } return "" } // audioCppVoiceDir is where this model's reference voices live: a "voices" // folder beside the gguf, shared by every audio.cpp model in that folder. The // per-model split qwentts.cpp gets is deliberately not copied - those are // learned speaker latents bound to one talker, while these are plain reference // WAVs plus a prompt_text mapping, reusable by any family that clones. func audioCppVoiceDir(row GgufRow) string { return strings.ReplaceAll(filepath.Join(filepath.Dir(row.FullPath), "voices"), "\\", "/") } // audioCppCmdLines builds the audiocpp_server argv (exe first). Shared by // emitAudioCppModel and RenderSoloCmd so the editor preview matches a save. // // Note what is NOT here: --model, because there is no such flag (the model // arrives through the materialized --config, injected at spawn), and any // residency knob beyond the cap, because --max-loaded-models 1 is the whole of // our containment and the defaults for idle unload and the memory guard are // already off. func audioCppCmdLines(s Settings, row GgufRow, ov *Override, class string) []string { be := audioCppBackend(s, ov, class) exe := be.Exe if exe == "" { exe = "audiocpp_server" } lines := []string{ exe, "--host 127.0.0.1", "--port ${PORT}", } flavour := audioCppFlavour(s, be.ID) if flavour != "" { lines = append(lines, "--backend "+flavour) } // Which adapter, not just which runtime: left alone audio.cpp takes device 0, // which is the iGPU on any box that enumerates it first. See audiocppdev.go. if dev, ok := audioCppDeviceArg(exe, flavour, ov); ok { lines = append(lines, dev) } // One model per process, so the LRU this bounds has nothing to choose // between. It is emitted anyway as the belt to the config's braces: a hand // edit that adds a second model to the block must not silently gain a second // set of weights the router never budgeted for. lines = append(lines, "--max-loaded-models 1") lines = append(lines, "--voice-dir "+audioCppVoiceDir(row)) if ov != nil && ov.Threads > 0 { lines = append(lines, fmt.Sprintf("--threads %d", ov.Threads)) } if ov != nil { if extra := strings.TrimSpace(ov.ExtraArgs); extra != "" { lines = append(lines, extra) } } return lines } // emitAudioCppModel writes an audiocpp_server YAML entry for an audio.cpp GGUF. // // The `audiocpp:` block is the part with no precedent in this file: it is not // launch state but the models[] entry of the JSON config the spawn hook writes, // carried in the model config so the materializer needs no second source of // truth and nothing has to be written to disk at generate time. // // checkEndpoint is "/health": with lazy_load left false the server loads weights // before it opens the listen socket, so a 200 there really does mean resident, // and gating readiness on it holds the first request instead of racing a load. func emitAudioCppModel(b *strings.Builder, s Settings, row GgufRow, ov *Override, name string, meta Metadata, emitted *[]string) { family := strings.ToLower(strings.TrimSpace(meta.AudioFamily)) if family == "" { // A per-quant component of a directory package carries no family id of // its own; it is emitted with the package's anchor, not skipped. if fam := audioCppDirPackageFamily(filepath.Dir(row.FullPath)); fam != "" { fmt.Fprintf(b, "\n # %s: component of the %s package in %s, emitted with its %s\n", name, fam, filepath.ToSlash(filepath.Dir(row.FullPath)), audioCppDirFamilies[fam]) return } // Nothing can be done from here: family is a required field of the config // entry, and it is the one thing the server cannot work out for itself // from the weights. A conversion that dropped the spec (a third-party // "-orig" export) has to be re-fetched from audio.cpp's own GGUF repo. fmt.Fprintf(b, "\n # SKIPPED %s: audio.cpp gguf with no audiocpp.model_spec.family in its header - audiocpp_server cannot be configured for it; re-download the audio.cpp GGUF build of this model\n", name) return } task := audioCppTask(family) if anchor, ok := audioCppDirFamilies[family]; ok { if !strings.EqualFold(row.FileName, anchor) { fmt.Fprintf(b, "\n # %s: shared component of the %s package in %s, emitted with its %s\n", name, family, filepath.ToSlash(filepath.Dir(row.FullPath)), anchor) return } emitAudioCppDirModel(b, s, row, ov, family, task, emitted) return } if task == "" { if audioCppKnownFamily(family) { fmt.Fprintf(b, "\n # SKIPPED %s: audio.cpp family %q is a voice-conversion / codec / alignment model, or a music / separation family the typed routes have no options for, which has no class or route here yet\n", name, family) } else { fmt.Fprintf(b, "\n # SKIPPED %s: unknown audio.cpp family %q - add it to audioCppFamilies in internal/autogen/audiocpp.go\n", name, family) } return } overhead := audioCppOverheadGB switch task { case "gen": overhead = audioCppMusicOverheadGB case "midi": overhead = audioCppMidiOverheadGB } if c, ok := audioCppCompanions[family]; ok { emitAudioCppCompanionModel(b, s, row, ov, name, family, task, c, overhead, emitted) return } opts, estGB := audioCppFamilySessionOptions[family], row.SizeGB+overhead if family == qwen3AsrFamily { opts, estGB = qwen3AsrSessionOptions(b, s, ov, opts, estGB) } writeAudioCppEntry(b, s, row, ov, name, family, task, strings.ReplaceAll(row.FullPath, "\\", "/"), opts, row.SizeGB, estGB) *emitted = append(*emitted, name) } // emitAudioCppCompanionModel emits a main-plus-companion family's main gguf as // a folder model, both files pinned through session options. Each main quant // in the folder is its own model sharing the one companion. With no companion // on disk there is nothing to load, so the YAML says which file is missing. func emitAudioCppCompanionModel(b *strings.Builder, s Settings, row GgufRow, ov *Override, name, family, task string, c audioCppCompanion, overhead float64, emitted *[]string) { dir := filepath.Dir(row.FullPath) files := c.files if c.fullFiles != nil && audioCppFullPrecisionRe.MatchString(row.FileName) && !audioCppOnVulkan(s, ov, family) { files = c.fullFiles } for _, f := range files { fi, err := os.Stat(filepath.Join(dir, f)) if err != nil || fi.IsDir() || fi.Size() <= 0 { continue } weights := row.SizeGB + float64(fi.Size())/(1<<30) opts := map[string]string{c.mainOption: row.FileName, c.companionOption: f} writeAudioCppEntry(b, s, row, ov, name, family, task, strings.ReplaceAll(dir, "\\", "/"), opts, round(weights, 2), weights+overhead) *emitted = append(*emitted, name) return } fmt.Fprintf(b, "\n # SKIPPED %s: %s needs its companion %s in %s (a download may still be running)\n", name, family, strings.Join(files, " or "), filepath.ToSlash(dir)) } // audioCppOnVulkan reports whether the family is served by a Vulkan audio.cpp // build. YuE2's f32 VAE cannot decode there: its graph asks for one 3.6 GB // allocation, over the device's per-allocation limit, so Vulkan keeps f16. // HIP and CUDA builds have no such cap. func audioCppOnVulkan(s Settings, ov *Override, family string) bool { be := audioCppBackend(s, ov, audioCppClass(family)) return audioCppFlavour(s, be.ID) == "vulkan" } // audioCppFullPrecisionRe matches an unquantized main file name (yue2-3b-bf16.gguf). var audioCppFullPrecisionRe = regexp.MustCompile(`(?i)[-_.](bf16|f16|f32)\.gguf$`) // writeAudioCppEntry writes one audiocpp_server model entry. path is what the // audio.cpp config names (a gguf, or a package directory), sessionOptions its // session_options, sizeGB the weights on disk and estGB the admission estimate. // A session option key carries its family prefix ("minimax_music3.language_model_gguf"), // unlike a per-request option: model_specs lists the bare name, but the server // looks session options up prefixed and silently ignores a bare key, falling // back to the spec default (MiniMax then asks for language_model_q4_0.gguf). func writeAudioCppEntry(b *strings.Builder, s Settings, row GgufRow, ov *Override, name, family, task, path string, sessionOptions map[string]string, sizeGB, estGB float64) { class := audioCppClass(family) be := audioCppBackend(s, ov, class) fmt.Fprintf(b, "\n # family=%s task=%s size=%gGB (audio.cpp audiocpp_server)\n", family, task, sizeGB) if be.Exe == "" { fmt.Fprintf(b, " # WARNING: no audio.cpp backend registered (Settings -> Backends, \"audio.cpp\"); %s will not start until one is installed\n", name) } else if audioCppFlavour(s, be.ID) == "" { // Worth a line in the config: the failure mode is a CUDA init error on a // machine with no CUDA, which reads as a broken model rather than as a // missing flag. fmt.Fprintf(b, " # NOTE: audio.cpp backend %q records no build variant, so no --backend flag is emitted and the server's own default (cuda) applies; add \"--backend vulkan|cpu|metal\" to this model's extra arguments if that is wrong\n", be.ID) } fmt.Fprintf(b, " %q:\n", name) b.WriteString(" cmd: >\n") for _, line := range audioCppCmdLines(s, row, ov, class) { fmt.Fprintf(b, " %s\n", line) } b.WriteString(" audiocpp:\n") fmt.Fprintf(b, " family: %q\n", family) fmt.Fprintf(b, " path: %q\n", path) fmt.Fprintf(b, " task: %q\n", task) if len(sessionOptions) > 0 { b.WriteString(" sessionOptions:\n") keys := make([]string, 0, len(sessionOptions)) for k := range sessionOptions { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Fprintf(b, " %s: %q\n", k, sessionOptions[k]) } } fmt.Fprintf(b, " ttl: %d\n", s.TtlSec) // audio.cpp serializes inference behind a per-model mutex and 503s a request // that waits out its busy timeout. Queuing here instead keeps that from ever // being reached, and keeps the wait visible to our own scheduler. b.WriteString(" concurrencyLimit: 1\n") writeEstVram(b, estGB) b.WriteString(" checkEndpoint: /health\n") if ov != nil && ov.Unlisted { b.WriteString(" unlisted: true\n") } b.WriteString(" capabilities:\n") switch task { case "asr": b.WriteString(" in: [audio]\n") b.WriteString(" out: [text]\n") case "gen": // No in/out pair: text->audio is what advertises audio_speech, and a // music generator has no /v1/audio/speech route. b.WriteString(" music: true\n") if audioCppMusicEdits[family] { b.WriteString(" musicEdit: true\n") } if sc, ok := audioCppMusicScores[family]; ok { fmt.Fprintf(b, " musicScore: %s\n", sc.Format) } case "sep": b.WriteString(" separation: true\n") case "midi": fmt.Fprintf(b, " musicTranscribe: %s\n", audioCppTranscribeFormat) default: b.WriteString(" in: [text]\n") b.WriteString(" out: [audio]\n") if audioCppClones[family] { b.WriteString(" voiceClone: true\n") } } writeDisplayName(b, s, name) } // audioCppDirPackageFamily reports the directory-model family a folder holds, // read from its anchor component's header, or "" when it holds none. This is // what tells a family-less MiniMax component (whose header says nothing about // what it belongs to) apart from a broken export that genuinely lost its spec. func audioCppDirPackageFamily(dir string) string { for fam, anchor := range audioCppDirFamilies { meta, err := ReadGgufMetadata(filepath.Join(dir, anchor)) if err == nil && strings.EqualFold(strings.TrimSpace(meta.AudioFamily), fam) { return fam } } return "" } // emitAudioCppDirModel emits one model per complete quant of a directory // package, anchored on the package's shared vocoder row. The config names the // directory, and session_options pin every component file: audio.cpp's own // defaults name the q4_0 language model and transformer with the bf16 depth // decoder, a mix no published package contains, so leaving any of them unset // fails at load on a box that downloaded one quant. func emitAudioCppDirModel(b *strings.Builder, s Settings, row GgufRow, ov *Override, family, task string, emitted *[]string) { dir := filepath.Dir(row.FullPath) base := strings.ToLower(filepath.Base(dir)) base = strings.TrimSuffix(strings.TrimSuffix(base, "-gguf"), "_gguf") size := func(file string) (float64, bool) { fi, err := os.Stat(filepath.Join(dir, file)) if err != nil || fi.IsDir() || fi.Size() <= 0 { return 0, false } return float64(fi.Size()) / (1 << 30), true } shared := row.SizeGB if gb, ok := size("condition_encoder.gguf"); ok { shared += gb } emittedAny := false for _, q := range audioCppDirQuants { lm, transformer := "language_model_"+q+".gguf", "transformer_"+q+".gguf" lmGB, ok1 := size(lm) trGB, ok2 := size(transformer) if !ok1 || !ok2 { continue } rvq := "rvq_depth_decoder_" + q + ".gguf" rvqGB, ok := size(rvq) if !ok { rvq = "rvq_depth_decoder_q8_0.gguf" if rvqGB, ok = size(rvq); !ok { continue } } name := base + "-" + q for n := 2; slices.Contains(*emitted, name); n++ { name = fmt.Sprintf("%s-%s-%d", base, q, n) } weights := shared + lmGB + trGB + rvqGB est := weights + audioCppMusicOverheadGB opts := map[string]string{ family + ".language_model_gguf": lm, family + ".rvq_depth_decoder_gguf": rvq, family + ".flow_transformer_gguf": transformer, } if miniMaxF16(s, ov, q) { opts[family+".weight_type"] = "f16" // f16 roughly doubles a q8_0 component, and summing every file the // way the native price does would ask for more than a 24 GB card // has. mem_saver (on by default) holds one stage at a time: the AR // stage (language model + depth decoder), then condition + flow, // then the vocoder. Price the largest of them. ar := ggufF16GB(filepath.Join(dir, lm), lmGB) + ggufF16GB(filepath.Join(dir, rvq), rvqGB) flow := ggufF16GB(filepath.Join(dir, transformer), trGB) + shared est = max(ar, flow) + audioCppMusicOverheadGB } writeAudioCppEntry(b, s, row, ov, name, family, task, strings.ReplaceAll(dir, "\\", "/"), opts, round(weights, 2), est) *emitted = append(*emitted, name) emittedAny = true } if !emittedAny { fmt.Fprintf(b, "\n # SKIPPED %s: no complete quant in %s (needs language_model_.gguf and transformer_.gguf; a download may still be running)\n", family, filepath.ToSlash(dir)) } }