Ctrl K
ComunidadGuías…

internal/autogen/audiocpp_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
)

// audioCppSettings is a fleet with one installed audio.cpp build of the given
// variant, plus whatever other backend rows the case needs.
func audioCppSettings(variant string, extra ...BackendEntry) Settings {
	rows := []BackendEntry{{
		ID: "managed-audiocpp-server", Kind: "audiocpp", Name: "audio.cpp",
		Path: "/backends/audiocpp_server", Managed: true, Variant: variant, Default: true,
	}}
	return Settings{Backends: append(rows, extra...), TtlSec: 600}
}

func audioCppRow(path string) GgufRow {
	return GgufRow{FullPath: path, FileName: path[strings.LastIndexByte(path, '/')+1:], SizeGB: 0.2}
}

func TestIsAudioCppModel(t *testing.T) {
	cases := []struct {
		name string
		meta Metadata
		want bool
	}{
		{"arch", Metadata{Architecture: "audiocpp"}, true},
		{"family only", Metadata{AudioFamily: "moss_tts_nano"}, true},
		{"chat llm", Metadata{Architecture: "qwen35"}, false},
		{"qwentts talker", Metadata{Architecture: "qwen3-tts"}, false},
		{"kokoro", Metadata{Architecture: "kokoro"}, false},
	}
	for _, c := range cases {
		if got := IsAudioCppModel(c.meta); got != c.want {
			t.Errorf("%s: IsAudioCppModel = %v, want %v", c.name, got, c.want)
		}
	}
}

func TestAudioCppTask(t *testing.T) {
	cases := map[string]string{
		"moss_tts_nano": "tts",
		"qwen3_asr":     "asr",
		"nemotron_asr":  "asr",
		"echo_tts":      "tts",  // clone-only is still speech synthesis
		"firered_audio": "tts",  // dual-task: one config entry, tts wins
		"ace_step":      "gen",  // music
		"htdemucs":      "sep",  // separation
		"sheetsage2":    "midi", // score transcription
		"seed_vc":       "",     // voice conversion
		"not_a_family":  "",
	}
	for family, want := range cases {
		if got := audioCppTask(family); got != want {
			t.Errorf("audioCppTask(%q) = %q, want %q", family, got, want)
		}
	}
	if audioCppKnownFamily("not_a_family") {
		t.Error("audioCppKnownFamily(not_a_family) = true")
	}
	if !audioCppKnownFamily("ace_step") {
		t.Error("audioCppKnownFamily(ace_step) = false, want true")
	}
}

func TestEmitAudioCppModel_TTS(t *testing.T) {
	s := audioCppSettings("vulkan")
	row := audioCppRow("/models/speech/moss-tts-nano-100m-q8_0.gguf")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "moss_tts_nano"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "moss-tts-nano-100m-q8_0", meta, &emitted)
	out := b.String()

	for _, want := range []string{
		"/backends/audiocpp_server",
		"--port ${PORT}",
		"--backend vulkan",
		"--max-loaded-models 1",
		"--voice-dir /models/speech/voices",
		"    audiocpp:",
		`      family: "moss_tts_nano"`,
		`      path: "/models/speech/moss-tts-nano-100m-q8_0.gguf"`,
		`      task: "tts"`,
		"concurrencyLimit: 1",
		"checkEndpoint: /health",
		"in: [text]",
		"out: [audio]",
		"voiceClone: true",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted YAML missing %q:\n%s", want, out)
		}
	}
	// There is no --model flag on audiocpp_server; naming the gguf on argv would
	// be rejected outright.
	if strings.Contains(out, "--model ") {
		t.Errorf("emitted a --model flag, which audiocpp_server does not accept:\n%s", out)
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want 1 entry", emitted)
	}
}

func TestEmitAudioCppModel_ASR(t *testing.T) {
	s := audioCppSettings("cuda")
	row := audioCppRow("/models/speech/qwen3-asr-1.7b-q8_0.gguf")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "qwen3_asr"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "qwen3-asr-1.7b-q8_0", meta, &emitted)
	out := b.String()

	for _, want := range []string{"--backend cuda", `task: "asr"`, "in: [audio]", "out: [text]"} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted YAML missing %q:\n%s", want, out)
		}
	}
	// An ASR model has no speech surface to clone a voice for.
	if strings.Contains(out, "voiceClone") {
		t.Errorf("ASR entry advertises voiceClone:\n%s", out)
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want 1 entry", emitted)
	}
}

// A family we know about but do not serve (voice conversion, codecs, alignment)
// must not reach the config at all: no other engine here could serve it either,
// and an entry in the Speech tab would fail every request.
func TestEmitAudioCppModel_UnservedFamiliesAreSkipped(t *testing.T) {
	cases := []struct {
		name   string
		family string
		want   string
	}{
		{"voice conversion", "seed_vc", "voice-conversion"},
		{"unknown", "brand_new_tts", "unknown audio.cpp family"},
		{"no family id", "", "no audiocpp.model_spec.family"},
	}
	for _, c := range cases {
		s := audioCppSettings("vulkan")
		row := audioCppRow("/models/speech/x-q8_0.gguf")
		var b strings.Builder
		var emitted []string
		emitAudioCppModel(&b, s, row, &Override{}, "x-q8_0", Metadata{Architecture: "audiocpp", AudioFamily: c.family}, &emitted)
		out := b.String()
		if len(emitted) != 0 {
			t.Errorf("%s: emitted = %v, want none", c.name, emitted)
		}
		if !strings.Contains(out, "# SKIPPED") || !strings.Contains(out, c.want) {
			t.Errorf("%s: want a SKIPPED comment mentioning %q, got:\n%s", c.name, c.want, out)
		}
		if strings.Contains(out, "cmd:") {
			t.Errorf("%s: emitted a launch command for an unserved family:\n%s", c.name, out)
		}
	}
}

// A hand-entered row records no build variant, so no --backend can be derived.
// audio.cpp's own default is CUDA, which is the wrong guess on most machines, so
// the config has to say so rather than silently emit nothing.
func TestEmitAudioCppModel_NoVariantWarns(t *testing.T) {
	s := Settings{TtlSec: 600, Backends: []BackendEntry{{
		ID: "hand", Kind: "audiocpp", Name: "audio.cpp", Path: "/opt/audiocpp_server", Default: true,
	}}}
	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, audioCppRow("/models/speech/supertonic-3-q8_0.gguf"), &Override{},
		"supertonic-3-q8_0", Metadata{Architecture: "audiocpp", AudioFamily: "supertonic"}, &emitted)
	out := b.String()
	if strings.Contains(out, "\n      --backend ") {
		t.Errorf("guessed a --backend for a row with no variant:\n%s", out)
	}
	if !strings.Contains(out, "# NOTE:") {
		t.Errorf("no note about the cuda default:\n%s", out)
	}
}

// With no audio.cpp build registered the entry is still emitted (so the model is
// visible and starts working the moment one is installed), but it says so.
func TestEmitAudioCppModel_NoBackendRegistered(t *testing.T) {
	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, Settings{TtlSec: 600}, audioCppRow("/models/speech/vibevoice-1.5b-q8_0.gguf"),
		&Override{}, "vibevoice-1.5b-q8_0", Metadata{Architecture: "audiocpp", AudioFamily: "vibevoice"}, &emitted)
	out := b.String()
	if !strings.Contains(out, "# WARNING: no audio.cpp backend registered") {
		t.Errorf("no warning about the missing backend:\n%s", out)
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want the model to still be listed", emitted)
	}
}

// audio.cpp serves the tts class, so it can be the class default -- but it reads
// neither legacy engine's weights. A Kokoro gguf on a box where audio.cpp owns
// the tts default must still resolve to TTS.cpp, not to an audiocpp_server
// command built around a --model-path it does not accept.
func TestTTSBackend_IgnoresAudioCppRow(t *testing.T) {
	s := Settings{TtsServerExe: "tts-server", Backends: []BackendEntry{{
		ID: "managed-audiocpp-server", Kind: "audiocpp", Path: "/backends/audiocpp_server", Default: true,
	}}}
	row := GgufRow{FullPath: "/models/tts/Kokoro_espeak_f16.gguf", FileName: "Kokoro_espeak_f16.gguf"}
	kind, exe := ttsBackend(s, row, &Override{}, Metadata{Architecture: "kokoro"})
	if kind != ttsKindTTSCpp {
		t.Errorf("kind = %q, want %q", kind, ttsKindTTSCpp)
	}
	if strings.Contains(exe, "audiocpp") {
		t.Errorf("exe = %q, want the legacy tts exe", exe)
	}
}

// audio.cpp is the first kind serving two classes at once, which is what the
// registry's "an install never steals a populated class" rule has to reason
// about: arriving on a box that already has an ASR engine must not hand it the
// tts auto-pick either, because the two travel together in one row.
func TestAudioCppIsMultiClass(t *testing.T) {
	for _, class := range []string{"tts", "asr"} {
		if !KindServesClass("audiocpp", class) {
			t.Errorf("KindServesClass(audiocpp, %q) = false", class)
		}
	}
	if got := KindClasses("audiocpp"); len(got) != 2 {
		t.Errorf("KindClasses(audiocpp) = %v, want two classes", got)
	}
	if !ClassTaken([]BackendEntry{{ID: "parakeet", Kind: "asr"}}, "audiocpp") {
		t.Error("ClassTaken = false with an asr row present, want true")
	}
	if ClassTaken([]BackendEntry{{ID: "llama", Kind: "llama"}}, "audiocpp") {
		t.Error("ClassTaken = true against an llm-only registry, want false")
	}
}

// The same rule on the other class this kind serves: a Parakeet gguf on a box
// where audio.cpp owns the asr default must still launch parakeet-server, not
// audiocpp_server wearing parakeet's argv.
func TestASRExe_IgnoresAudioCppRow(t *testing.T) {
	s := Settings{AsrServerExe: "parakeet-server", Backends: []BackendEntry{{
		ID: "managed-audiocpp-server", Kind: "audiocpp", Path: "/backends/audiocpp_server", Default: true,
	}}}
	if exe := asrExe(s, &Override{}); exe != "parakeet-server" {
		t.Errorf("exe = %q, want the legacy asr exe", exe)
	}
	// A real ASR backend row still wins, as it does for every other class.
	s.Backends = append(s.Backends, BackendEntry{ID: "pk", Kind: "parakeet", Path: "/backends/parakeet", Default: true})
	if exe := asrExe(s, &Override{}); exe != "/backends/parakeet" {
		t.Errorf("exe = %q, want the registered parakeet row", exe)
	}
}

// A pin at another engine's row must be a no-op, not a broken launch: audio.cpp
// weights are its own format, so there is nothing for TTS.cpp to run here, and
// falling back to the bare "audiocpp_server" name (not on PATH) while warning
// that no backend is installed would be the worst of both.
func TestAudioCppBackend_IgnoresCrossEnginePin(t *testing.T) {
	s := audioCppSettings("vulkan", BackendEntry{
		ID: "ttscpp-row", Kind: "ttscpp", Name: "tts.cpp", Path: "/backends/tts-server",
	})
	be := audioCppBackend(s, &Override{Backend: "ttscpp-row"}, "tts")
	if be.Exe != "/backends/audiocpp_server" {
		t.Errorf("exe = %q, want the installed audio.cpp build", be.Exe)
	}

	// A pin at a specific audio.cpp BUILD still wins, which is what pinning is for.
	s.Backends = append(s.Backends, BackendEntry{
		ID: "build-audiocpp-cuda", Kind: "audiocpp", Path: "/backends/cuda/audiocpp_server",
		Managed: true, Build: true, Variant: "cuda",
	})
	be = audioCppBackend(s, &Override{Backend: "build-audiocpp-cuda"}, "tts")
	if be.Exe != "/backends/cuda/audiocpp_server" {
		t.Errorf("exe = %q, want the pinned cuda build", be.Exe)
	}
	if got := audioCppFlavour(s, be.ID); got != "cuda" {
		t.Errorf("flavour = %q, want cuda from the pinned build row", got)
	}
}

func TestEmitAudioCppModel_MusicGen(t *testing.T) {
	s := audioCppSettings("vulkan")
	row := audioCppRow("/models/music/acestep-v15-turbo-bf16.gguf")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "ace_step"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "acestep-v15-turbo-bf16", meta, &emitted)
	out := b.String()

	for _, want := range []string{`task: "gen"`, `family: "ace_step"`, "music: true", "musicEdit: true",
		// Without mem_saver a seed stops reproducing after a longer render.
		"sessionOptions:", `ace_step.mem_saver: "true"`} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted YAML missing %q:\n%s", want, out)
		}
	}
	if strings.Contains(out, "musicScore") {
		t.Errorf("ACE-Step entry advertises a score:\n%s", out)
	}
	// A music generator has no speech surface: text->audio would advertise
	// audio_speech, and voice cloning does not apply.
	for _, bad := range []string{"in: [text]", "out: [audio]", "voiceClone"} {
		if strings.Contains(out, bad) {
			t.Errorf("music entry advertises %q:\n%s", bad, out)
		}
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want 1 entry", emitted)
	}
}

func TestEmitAudioCppModel_Separation(t *testing.T) {
	s := audioCppSettings("vulkan")
	row := audioCppRow("/models/music/htdemucs-f16.gguf")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "htdemucs"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "htdemucs-f16", meta, &emitted)
	out := b.String()

	if !strings.Contains(out, `task: "sep"`) || !strings.Contains(out, "separation: true") {
		t.Errorf("separation entry missing task/capability:\n%s", out)
	}
	if strings.Contains(out, "mem_saver") {
		t.Errorf("separation entry carries an ace_step option:\n%s", out)
	}
	if strings.Contains(out, "music: true") {
		t.Errorf("separation entry advertises music:\n%s", out)
	}
	if strings.Contains(out, "in: [") {
		t.Errorf("separation entry declares speech in/out:\n%s", out)
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want 1 entry", emitted)
	}
}

// writeMiniMaxPackage lays out a MiniMax Music 3 package directory: the shared
// vocoder carries the family id in its header, the other components carry none.
// Each file in files is written as a non-empty placeholder, except the shared
// vocoder and condition encoder, which are real GGUF headers.
func writeMiniMaxPackage(t *testing.T, dir string, files ...string) {
	t.Helper()
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	family := [][2]any{
		{"general.architecture", "audiocpp"},
		{"audiocpp.model_spec.family", "minimax_music3"},
	}
	for _, f := range files {
		p := filepath.Join(dir, f)
		if f == "vocoder.gguf" || f == "condition_encoder.gguf" {
			writeGgufKVs(t, p, family)
			continue
		}
		if err := os.WriteFile(p, []byte("placeholder-weights"), 0o644); err != nil {
			t.Fatal(err)
		}
	}
}

// miniMaxRow is a GgufRow for a file inside a package directory, with FileName
// set to the base name (audioCppRow splits on "/" only, which a Windows
// filepath.Join path does not contain).
func miniMaxRow(path string) GgufRow {
	row := audioCppRow(filepath.ToSlash(path))
	row.FileName = filepath.Base(path)
	return row
}

func TestEmitAudioCppModel_MiniMaxDirectory(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "MiniMax-Music-3-GGUF")
	writeMiniMaxPackage(t, dir,
		"vocoder.gguf", "condition_encoder.gguf",
		"language_model_q8_0.gguf", "transformer_q8_0.gguf", "rvq_depth_decoder_q8_0.gguf",
		"language_model_q4_0.gguf", // no transformer_q4_0: q4_0 must not be emitted
	)
	s := audioCppSettings("vulkan")
	row := miniMaxRow(filepath.Join(dir, "vocoder.gguf"))
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "minimax_music3"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "vocoder", meta, &emitted)
	out := b.String()

	for _, want := range []string{
		`"minimax-music-3-q8_0":`,
		`minimax_music3.language_model_gguf: "language_model_q8_0.gguf"`,
		`minimax_music3.flow_transformer_gguf: "transformer_q8_0.gguf"`,
		`minimax_music3.rvq_depth_decoder_gguf: "rvq_depth_decoder_q8_0.gguf"`,
		// Vulkan renders the q8_0 components as hiss unless they load as f16.
		`minimax_music3.weight_type: "f16"`,
		"sessionOptions:",
		"music: true",
		`path: "` + filepath.ToSlash(dir) + `"`,
	} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted YAML missing %q:\n%s", want, out)
		}
	}
	if strings.Contains(out, "minimax-music-3-q4_0") {
		t.Errorf("emitted q4_0 with no transformer_q4_0 on disk:\n%s", out)
	}
	// MiniMax has no edit routes.
	if strings.Contains(out, "musicEdit") {
		t.Errorf("MiniMax entry advertises musicEdit:\n%s", out)
	}
	if want := []string{"minimax-music-3-q8_0"}; !reflect.DeepEqual(emitted, want) {
		t.Fatalf("emitted = %v, want %v", emitted, want)
	}
}

// The f16 conversion is a Vulkan workaround: other backends, and the package's
// own bf16 quant (stored as f16 on Vulkan already), load native.
func TestEmitAudioCppModel_MiniMaxWeightTypeOnlyOnVulkanQuants(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "MiniMax-Music-3-GGUF")
	writeMiniMaxPackage(t, dir,
		"vocoder.gguf", "condition_encoder.gguf",
		"language_model_bf16.gguf", "transformer_bf16.gguf", "rvq_depth_decoder_bf16.gguf",
		"language_model_q8_0.gguf", "transformer_q8_0.gguf", "rvq_depth_decoder_q8_0.gguf",
	)
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "minimax_music3"}
	row := miniMaxRow(filepath.Join(dir, "vocoder.gguf"))

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, audioCppSettings("cuda"), row, &Override{}, "vocoder", meta, &emitted)
	if out := b.String(); strings.Contains(out, "weight_type") {
		t.Errorf("CUDA entry carries weight_type:\n%s", out)
	}

	b.Reset()
	emitted = nil
	emitAudioCppModel(&b, audioCppSettings("vulkan"), row, &Override{}, "vocoder", meta, &emitted)
	out := b.String()
	if n := strings.Count(out, "weight_type"); n != 1 {
		t.Errorf("weight_type appears %d times on Vulkan, want once (q8_0 only):\n%s", n, out)
	}
	bf16 := out[strings.Index(out, `"minimax-music-3-bf16":`):strings.Index(out, `"minimax-music-3-q8_0":`)]
	if strings.Contains(bf16, "weight_type") {
		t.Errorf("bf16 entry carries weight_type:\n%s", bf16)
	}
}

// A pinned weight type beats the Vulkan rule both ways, and the editor's
// "Auto" label reports the pin-free rule.
func TestEmitAudioCppModel_MiniMaxWeightTypePin(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "MiniMax-Music-3-GGUF")
	writeMiniMaxPackage(t, dir,
		"vocoder.gguf", "condition_encoder.gguf",
		"language_model_q8_0.gguf", "transformer_q8_0.gguf", "rvq_depth_decoder_q8_0.gguf",
	)
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "minimax_music3"}
	row := miniMaxRow(filepath.Join(dir, "vocoder.gguf"))
	emit := func(flavour, pin string) string {
		var b strings.Builder
		var emitted []string
		emitAudioCppModel(&b, audioCppSettings(flavour), row, &Override{AudioWeightType: pin}, "vocoder", meta, &emitted)
		return b.String()
	}
	if out := emit("vulkan", AudioWeightNative); strings.Contains(out, "weight_type") {
		t.Errorf("native pin on Vulkan still emits weight_type:\n%s", out)
	}
	if out := emit("cuda", AudioWeightF16); !strings.Contains(out, `minimax_music3.weight_type: "f16"`) {
		t.Errorf("f16 pin on CUDA emits no weight_type:\n%s", out)
	}

	opts := map[string]string{"minimax_music3.language_model_gguf": "language_model_q8_0.gguf"}
	pinned := &Override{AudioWeightType: AudioWeightNative}
	if got := AudioCppWeightTypeAuto(audioCppSettings("vulkan"), pinned, "minimax_music3", opts); got != AudioWeightF16 {
		t.Errorf("auto on Vulkan q8_0 = %q, want f16 (the pin must not leak into it)", got)
	}
	if got := AudioCppWeightTypeAuto(audioCppSettings("cuda"), nil, "minimax_music3", opts); got != AudioWeightNative {
		t.Errorf("auto on CUDA = %q, want native", got)
	}
	if got := AudioCppWeightTypeAuto(audioCppSettings("vulkan"), nil, "ace_step", opts); got != "" {
		t.Errorf("auto for a family with no choice = %q, want empty", got)
	}
}

func TestEmitAudioCppModel_MiniMaxComponents(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "MiniMax-Music-3-GGUF")
	writeMiniMaxPackage(t, dir,
		"vocoder.gguf", "condition_encoder.gguf",
		"language_model_q8_0.gguf", "transformer_q8_0.gguf", "rvq_depth_decoder_q8_0.gguf",
	)
	s := audioCppSettings("vulkan")

	// A family-less per-quant component is explained by its package, not skipped.
	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, miniMaxRow(filepath.Join(dir, "transformer_q8_0.gguf")), &Override{},
		"transformer_q8_0", Metadata{Architecture: "audiocpp"}, &emitted)
	out := b.String()
	if !strings.Contains(out, "component of the minimax_music3 package") {
		t.Errorf("no package note for a family-less component:\n%s", out)
	}
	if strings.Contains(out, "SKIPPED") {
		t.Errorf("family-less component of a known package was SKIPPED:\n%s", out)
	}
	if len(emitted) != 0 {
		t.Errorf("emitted = %v for a component, want none", emitted)
	}

	// The shared condition encoder carries the family but is not the anchor.
	b.Reset()
	emitAudioCppModel(&b, s, miniMaxRow(filepath.Join(dir, "condition_encoder.gguf")), &Override{},
		"condition_encoder", Metadata{Architecture: "audiocpp", AudioFamily: "minimax_music3"}, &emitted)
	out = b.String()
	if !strings.Contains(out, "shared component") {
		t.Errorf("no shared-component note for condition_encoder.gguf:\n%s", out)
	}
	if len(emitted) != 0 {
		t.Errorf("emitted = %v for the shared encoder, want none", emitted)
	}
}

func TestEmitAudioCppModel_MiniMaxIncomplete(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "MiniMax-Music-3-GGUF")
	writeMiniMaxPackage(t, dir, "vocoder.gguf", "language_model_q8_0.gguf")
	s := audioCppSettings("vulkan")

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, miniMaxRow(filepath.Join(dir, "vocoder.gguf")), &Override{},
		"vocoder", Metadata{Architecture: "audiocpp", AudioFamily: "minimax_music3"}, &emitted)
	out := b.String()
	if !strings.Contains(out, "SKIPPED minimax_music3: no complete quant") {
		t.Errorf("no skip note for a package with no complete quant:\n%s", out)
	}
	if len(emitted) != 0 {
		t.Errorf("emitted = %v for an incomplete package, want none", emitted)
	}
}

// YuE2 is a folder model: the main gguf carries the family and anchors the
// emit, the VAE beside it carries none and is a component, not a broken export.
func TestEmitAudioCppModel_Yue2Companion(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "Yue2-3B-GGUF")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	writeGgufKVs(t, filepath.Join(dir, "yue2-3b-q8_0.gguf"), [][2]any{
		{"general.architecture", "audiocpp"},
		{"audiocpp.model_spec.family", "yue2"},
	})
	s := audioCppSettings("vulkan")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "yue2"}
	main := miniMaxRow(filepath.Join(dir, "yue2-3b-q8_0.gguf"))

	// No VAE yet: nothing loadable, and the YAML names the missing file.
	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, main, &Override{}, "yue2-3b-q8_0", meta, &emitted)
	if out := b.String(); !strings.Contains(out, "SKIPPED yue2-3b-q8_0: yue2 needs its companion yue2-vae-f16.gguf") || len(emitted) != 0 {
		t.Fatalf("emitted = %v without a VAE:\n%s", emitted, out)
	}

	for _, f := range []string{"yue2-vae-f16.gguf", "yue2-vae-f32.gguf"} {
		if err := os.WriteFile(filepath.Join(dir, f), []byte("placeholder-weights"), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	b.Reset()
	emitAudioCppModel(&b, s, main, &Override{}, "yue2-3b-q8_0", meta, &emitted)
	out := b.String()
	for _, want := range []string{
		`"yue2-3b-q8_0":`,
		`family: "yue2"`,
		`task: "gen"`,
		`path: "` + filepath.ToSlash(dir) + `"`,
		`yue2.model_gguf: "yue2-3b-q8_0.gguf"`,
		`yue2.vae_gguf: "yue2-vae-f16.gguf"`,
		"music: true",
		"musicScore: abc",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted YAML missing %q:\n%s", want, out)
		}
	}
	if strings.Contains(out, "musicEdit") {
		t.Errorf("YuE2 entry advertises musicEdit:\n%s", out)
	}
	if want := []string{"yue2-3b-q8_0"}; !reflect.DeepEqual(emitted, want) {
		t.Fatalf("emitted = %v, want %v", emitted, want)
	}
}

// A full-precision YuE2 pairs the f32 VAE first, except on a Vulkan build,
// where the f32 decode needs one allocation over the device limit.
func TestEmitAudioCppModel_Yue2FullPrecisionVAE(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "Yue2-3B-GGUF")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	for _, f := range []string{"yue2-3b-bf16.gguf", "yue2-3b-q8_0.gguf", "yue2-vae-f16.gguf", "yue2-vae-f32.gguf"} {
		writeGgufKVs(t, filepath.Join(dir, f), [][2]any{
			{"general.architecture", "audiocpp"},
			{"audiocpp.model_spec.family", "yue2"},
		})
	}
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "yue2"}
	cases := []struct{ variant, main, vae string }{
		{"rocm", "yue2-3b-bf16.gguf", "yue2-vae-f32.gguf"},
		{"cuda", "yue2-3b-bf16.gguf", "yue2-vae-f32.gguf"},
		{"vulkan", "yue2-3b-bf16.gguf", "yue2-vae-f16.gguf"},
		{"rocm", "yue2-3b-q8_0.gguf", "yue2-vae-f16.gguf"},
	}
	for _, c := range cases {
		var b strings.Builder
		var emitted []string
		emitAudioCppModel(&b, audioCppSettings(c.variant), miniMaxRow(filepath.Join(dir, c.main)), &Override{}, "yue2", meta, &emitted)
		if want := `yue2.vae_gguf: "` + c.vae + `"`; !strings.Contains(b.String(), want) {
			t.Errorf("%s %s: want %s in:\n%s", c.variant, c.main, want, b.String())
		}
	}
}

func TestEmitAudioCppModel_Transcription(t *testing.T) {
	s := audioCppSettings("vulkan")
	row := audioCppRow("/models/music/sheetsage2-orig.gguf")
	meta := Metadata{Architecture: "audiocpp", AudioFamily: "sheetsage2"}

	var b strings.Builder
	var emitted []string
	emitAudioCppModel(&b, s, row, &Override{}, "sheetsage2-orig", meta, &emitted)
	out := b.String()

	if !strings.Contains(out, `task: "midi"`) || !strings.Contains(out, "musicTranscribe: abc") {
		t.Errorf("transcription entry missing task/capability:\n%s", out)
	}
	for _, bad := range []string{"music: true", "separation: true", "in: ["} {
		if strings.Contains(out, bad) {
			t.Errorf("transcription entry carries %q:\n%s", bad, out)
		}
	}
	if len(emitted) != 1 {
		t.Fatalf("emitted = %v, want 1 entry", emitted)
	}
}

// A ROCm build must launch with --backend hip: without a flag the server
// defaults to cuda, which an AMD build cannot open.
func TestAudioCppFlavour_Rocm(t *testing.T) {
	for _, v := range []string{"rocm", "hip", "ROCm"} {
		s := Settings{Backends: []BackendEntry{{ID: "a", Kind: "audiocpp", Variant: v}}}
		if got := audioCppFlavour(s, "a"); got != "hip" {
			t.Errorf("variant %q: flavour = %q, want hip", v, got)
		}
	}
}