Ctrl K
ComunidadGuías…

internal/autogen/audiocppaligner.go

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

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// Qwen3-ASR word timestamps come from a second model: after transcribing, the
// session runs Qwen3 ForcedAligner over the transcript and audio, which is what
// fills each word's start/end sample. audio.cpp loads the aligner INSIDE the ASR
// session (qwen3_asr.forced_aligner_path), so it is a companion, not a served
// model: the aligner gguf stays class 0 and only ever rides along with an ASR.
//
// Unlike audioCppCompanions, the aligner ships as its own repo and lands in a
// different folder, so the option carries an absolute path rather than a name
// relative to the ASR's package directory.

const (
	qwen3AsrFamily       = "qwen3_asr"
	qwen3AlignerFamily   = "qwen3_forced_aligner"
	qwen3AlignerOption   = "qwen3_asr.forced_aligner_path"
	qwen3AsrVadOption    = "qwen3_asr.vad_model_path"
	audioCppSileroVadRel = "assets/framework/models/silero_vad"
)

// audioCppAligner is the forced-aligner gguf discovery found, if any.
type audioCppAligner struct {
	path   string // forward-slashed absolute path
	sizeGB float64
}

// detectQwen3Aligner finds a Qwen3 ForcedAligner gguf among the discovered rows.
// Only rows whose file name says "align" have their header read: a header read
// parses the whole metadata block (an LLM's 150k-entry tokenizer included), and
// Generate already reads every header once in emitModel. The header then has the
// final word, so a file merely named like an aligner is not taken for one. Rows
// arrive sorted, so the same tree always picks the same file.
func detectQwen3Aligner(rows []GgufRow) *audioCppAligner {
	for _, row := range rows {
		if row.IsSam || !strings.Contains(strings.ToLower(row.FileName), "align") {
			continue
		}
		meta, err := ReadGgufMetadata(row.FullPath)
		if err != nil || !strings.EqualFold(strings.TrimSpace(meta.AudioFamily), qwen3AlignerFamily) {
			continue
		}
		return &audioCppAligner{path: filepath.ToSlash(row.FullPath), sizeGB: row.SizeGB}
	}
	return nil
}

// qwen3AsrSessionOptions adds the timestamp companions to a qwen3_asr entry and
// returns its options and admission estimate. The aligner is resident for the
// session's whole life (it is loaded with the ASR, not per request), so its
// weights are counted in estGB whether or not a request asks for timestamps.
//
// The VAD path is pinned absolute when the install has it: audio.cpp resolves
// its relative default against the process cwd, and quartermaster spawns every
// backend from its OWN directory (process.spawnDir), where no assets/ exists.
// With timestamps on, audio_chunk_mode=auto chunks on VAD, so a missing VAD
// would fail exactly the requests the aligner was added for.
func qwen3AsrSessionOptions(b *strings.Builder, s Settings, ov *Override, base map[string]string, estGB float64) (map[string]string, float64) {
	al := s.qwen3Aligner
	if al == nil {
		b.WriteString("\n  # NOTE: no Qwen3 ForcedAligner gguf found, so this ASR serves text only (no word timestamps)")
		return base, estGB
	}
	opts := make(map[string]string, len(base)+2)
	for k, v := range base {
		opts[k] = v
	}
	opts[qwen3AlignerOption] = al.path
	if vad := audioCppAssetPath(audioCppBackend(s, ov, audioCppClass(qwen3AsrFamily)).Exe, audioCppSileroVadRel); vad != "" {
		opts[qwen3AsrVadOption] = vad
	} else {
		fmt.Fprintf(b, "\n  # NOTE: %s not found beside the audio.cpp install; timestamp requests chunk on VAD and may fail", audioCppSileroVadRel)
	}
	return opts, estGB + al.sizeGB
}

// audioCppAssetPath finds rel in the nearest ancestor of the audio.cpp binary's
// directory that holds it, the same walk audiocpp.SpecsDir makes for
// model_specs: a release ships assets beside the binary, a source build keeps
// them at the checkout root above build/<preset>/bin. "" when none has it.
func audioCppAssetPath(exe, rel string) string {
	exe = strings.TrimSpace(exe)
	if exe == "" {
		return ""
	}
	dir := filepath.Dir(exe)
	for i := 0; i <= 3; i++ {
		cand := filepath.Join(dir, filepath.FromSlash(rel))
		if _, err := os.Stat(cand); err == nil {
			return filepath.ToSlash(cand)
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			break
		}
		dir = parent
	}
	return ""
}