feat(parakeet-cpp): load bundle GGUF files, bundle gallery entries, pin bump (#12479)

* feat(parakeet-cpp): load bundle GGUF files and use their components by role

A bundle GGUF holds several models (ASR, VAD, diarization, sound events,
speaker encoder) in one file, each with its own licence. Detect a bundle
at load through parakeet_capi_bundle_components_json and open components
with parakeet_capi_load_component. The three symbols are probed together,
so an older libparakeet.so still loads plain files as before.

The only ASR component is the primary model; bundle_asr:<name> picks one
when there are several. A Silero VAD component of the primary bundle is
loaded without an option and serves /v1/vad and vad:true. The diar, ced
and voice components load on request: diar_component, sound_component and
speaker_component, or a companion option (diarization_model, sound_model,
speaker_model, vad_model) that names a bundle, even the model file itself.
vad_component picks a VAD component and implies vad:true.

A role the bundle cannot fill fails the load with the component list, and
a diarization or sound request on a model without that role names the
bundle components. Every existing option and single-file model behaves as
before.

Assisted-by: Claude:claude-sonnet-5-5 [Claude Code]

* chore(parakeet-cpp): bump parakeet.cpp to 781a973

Brings in the bundle GGUF format and its C-API (parakeet_capi_load_component,
parakeet_capi_bundle_components_json, parakeet_capi_load_error).

Assisted-by: Claude:claude-sonnet-5-5 [Claude Code]

* feat(parakeet-cpp): gallery entries for the bundle GGUF files, docs

Add parakeet-cpp-bundle-small (338 MB: Parakeet TDT+CTC 110M, Nemotron-3-
Diarization, CED-Small, WeSpeaker ResNet34-LM, Silero VAD), -standard
(1.1 GB, Parakeet TDT 0.6B v3 instead of the 110M model) and
-moondream-redux (215 MB: packed Redux and Silero VAD, CPU only). One
install serves transcription, VAD, diarization, sound events and speaker
naming through the component options. The existing single-purpose entries
stay.

A bundle has no single licence, so the entries use license: other and
state the licence and credit of each component in the description, with
the upstream inconsistency of the CED licence. The docs get a section on
bundles in audio-to-text with the entries, the roles, the options and the
licence notice, and pointers from the VAD, diarization and sound
classification pages. A gallery test checks the file names, checksums,
usecases and options.

Assisted-by: Claude:claude-sonnet-5-5 [Claude Code]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-10-04 20:33:13 +02:00
1 parent d3724eef50
commit 3185b6fcf8
14 files changed
+1095 -44

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
# parakeet-cpp backend Makefile.
#
# Upstream pin lives below as PARAKEET_VERSION?=e53a2539bd7fc3290951037696b953b34a9c4b9c
# Upstream pin lives below as PARAKEET_VERSION?=781a973e755bec5562ac2a0b04770ddffa36cad2
# (.github/bump_deps.sh) can find and update it - matches the
# whisper.cpp / ds4 / vibevoice-cpp convention.
#
@@ -15,7 +15,7 @@
# That's what the L0 smoke test uses. The default target below does the
# proper clone-at-pin + cmake build so CI doesn't need a side-checkout.
PARAKEET_VERSION?=e53a2539bd7fc3290951037696b953b34a9c4b9c
PARAKEET_VERSION?=781a973e755bec5562ac2a0b04770ddffa36cad2
PARAKEET_REPO?=https://github.com/mudler/parakeet.cpp
GOCMD?=go
+189
View File
@@ -0,0 +1,189 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Bundle GGUF support. A bundle is one GGUF file holding several models
// ("components"), each with its own kind and licence (docs/bundle.md in
// parakeet.cpp). The library opens one component per context; this file works
// out which component each role of a loaded model uses.
//
// Model options (key:value, all optional):
//
// bundle_asr:<name> the ASR component of the primary file (or of the
// asr_model: file), when the bundle has several
// vad_component:<name> the VAD component; with no vad_model: the VAD of the
// primary file
// diar_component:<name> the diarization component; with no diarization_model:
// the one of the primary file
// sound_component:<name> the sound (CED) component; likewise for sound_model:
// speaker_component:<name> the speaker encoder component; likewise for speaker_model:
//
// A companion option (diarization_model: and the others) may name a bundle: the
// only component of the wanted kind is used, and a *_component option picks one
// when there are several. A Silero VAD component of the primary bundle is loaded
// without any option, because it costs little and serves the VAD endpoint.
// Component kinds as the C-API reports them in the bundle component list.
const (
componentASR = "asr"
componentVAD = "vad"
componentDiar = "diar"
componentSound = "ced"
componentVoice = "voice"
)
// bundleComponent is one entry of parakeet_capi_bundle_components_json. The
// library reports more fields (sources, hashes); only the ones this backend
// reads are decoded.
type bundleComponent struct {
Name string `json:"name"`
Kind string `json:"kind"`
License string `json:"license"`
Bytes int64 `json:"bytes"`
}
// componentKindModel maps a component kind to the model kind the loaded context
// must report.
func componentKindModel(kind string) (int32, bool) {
switch kind {
case componentASR:
return modelKindASR, true
case componentVAD:
return modelKindVAD, true
case componentDiar:
return modelKindDiarization, true
case componentSound:
return modelKindSound, true
case componentVoice:
return modelKindSpeaker, true
}
return 0, false
}
// kindForModel is the reverse of componentKindModel.
func kindForModel(model int32) string {
switch model {
case modelKindASR:
return componentASR
case modelKindVAD:
return componentVAD
case modelKindDiarization:
return componentDiar
case modelKindSound:
return componentSound
case modelKindSpeaker:
return componentVoice
}
return ""
}
// bundleComponents lists the components of the file at path. It returns nil
// when the file is not a bundle, and also when the library has no bundle
// support (the three bundle symbols are probed together in main.go), so callers
// treat both as a plain file. Only the header of the file is read.
func bundleComponents(path string) ([]bundleComponent, error) {
if CppBundleComponentsJSON == nil {
return nil, nil
}
cstr := CppBundleComponentsJSON(path)
if cstr == 0 {
return nil, nil
}
raw := goStringFromCPtr(cstr)
CppFreeString(cstr)
var comps []bundleComponent
if err := json.Unmarshal([]byte(raw), &comps); err != nil {
return nil, fmt.Errorf("parakeet-cpp: decode bundle component list of %q: %w", path, err)
}
return comps, nil
}
// describeComponents renders a component list for error messages, for example
// "asr (asr), vad (vad)": name, then kind.
func describeComponents(comps []bundleComponent) string {
parts := make([]string, 0, len(comps))
for _, c := range comps {
parts = append(parts, fmt.Sprintf("%s (%s)", c.Name, c.Kind))
}
return strings.Join(parts, ", ")
}
// componentsOfKind returns the names of the components of one kind.
func componentsOfKind(comps []bundleComponent, kind string) []string {
var names []string
for _, c := range comps {
if c.Kind == kind {
names = append(names, c.Name)
}
}
return names
}
// pickComponent chooses the component of kind wantKind in a bundle. A name set
// by the user must exist and have that kind; with no name the bundle must hold
// exactly one component of the kind. what names the role for the messages and
// nameOpt the option that picks a component.
func pickComponent(path string, comps []bundleComponent, name, wantKind, what, nameOpt string) (string, error) {
if name != "" {
for _, c := range comps {
if c.Name != name {
continue
}
if c.Kind != wantKind {
return "", fmt.Errorf("parakeet-cpp: component %q of %q has kind %q, but %s needs kind %q",
name, path, c.Kind, what, wantKind)
}
return name, nil
}
return "", fmt.Errorf("parakeet-cpp: %q has no component %q (components: %s)",
path, name, describeComponents(comps))
}
names := componentsOfKind(comps, wantKind)
switch len(names) {
case 1:
return names[0], nil
case 0:
return "", fmt.Errorf("parakeet-cpp: %s needs a %q component, but the bundle %q has none (components: %s)",
what, wantKind, path, describeComponents(comps))
default:
return "", fmt.Errorf("parakeet-cpp: the bundle %q has several %q components (%s); set %s to pick one",
path, wantKind, strings.Join(names, ", "), nameOpt)
}
}
// loadComponent opens one component of a bundle and returns the library's
// reason on failure.
func loadComponent(path, name string) (uintptr, error) {
if CppLoadComponent == nil {
return 0, errors.New("parakeet-cpp: bundle components need a libparakeet.so with parakeet_capi_load_component; rebuild the backend against a newer parakeet.cpp")
}
ctx := CppLoadComponent(path, name)
if ctx == 0 {
reason := ""
if CppLoadError != nil {
reason = CppLoadError()
}
return 0, fmt.Errorf("parakeet-cpp: failed to load component %q of %q: %s", name, path, reason)
}
return ctx, nil
}
// roleHint ends the error of an RPC that needs a role the loaded model does not
// have. For a bundle it says whether the bundle has a component of that kind
// (and which option loads it) or lacks one; for any other model it is empty.
func (p *ParakeetCpp) roleHint(kind, option string) string {
if len(p.bundle) == 0 {
return ""
}
if names := componentsOfKind(p.bundle, kind); len(names) > 0 {
return fmt.Sprintf(" (the model file is a bundle with a %q component %s: set %s:%s to load it)",
kind, strings.Join(names, ", "), option, names[0])
}
return fmt.Sprintf(" (the model file is a bundle without a %q component; components: %s)",
kind, describeComponents(p.bundle))
}
+349
View File
@@ -0,0 +1,349 @@
package main
import (
"context"
"encoding/json"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// bundleLib extends fakeLib with bundle files. Paths registered with withBundle
// answer the component list and open components by name; the plain
// CppLoad on a bundle behaves like the library (the only ASR component, else
// the only component).
type bundleLib struct {
*fakeLib
bundles map[string][]bundleComponent
pool liveCstrPool
loadedComp []string
loadErr string
}
func newBundleLib() *bundleLib {
return &bundleLib{fakeLib: newFakeLib(), bundles: map[string][]bundleComponent{}}
}
func (b *bundleLib) withBundle(path string, comps ...bundleComponent) *bundleLib {
b.bundles[path] = comps
return b
}
func (b *bundleLib) withPlain(path string, kind int32) *bundleLib {
b.fakeLib.withModel(path, kind)
return b
}
func (b *bundleLib) open(kind string) uintptr {
mk, _ := componentKindModel(kind)
ctx := b.next
b.next++
b.ctxKind[ctx] = mk
return ctx
}
func (b *bundleLib) install() (restore func()) {
restoreBase := b.fakeLib.install()
savedJSON, savedComp, savedErr, savedFreeStr, savedLoad := CppBundleComponentsJSON, CppLoadComponent, CppLoadError, CppFreeString, CppLoad
baseLoad := CppLoad
CppFreeString = func(uintptr) {}
CppBundleComponentsJSON = func(path string) uintptr {
comps, ok := b.bundles[path]
if !ok {
return 0
}
raw, _ := json.Marshal(comps)
return b.pool.cstr(string(raw))
}
CppLoadError = func() string { return b.loadErr }
CppLoadComponent = func(path, name string) uintptr {
for _, c := range b.bundles[path] {
if c.Name == name {
b.loadedComp = append(b.loadedComp, path+"#"+name)
return b.open(c.Kind)
}
}
b.loadErr = "no such component " + name
return 0
}
CppLoad = func(path string) uintptr {
comps, ok := b.bundles[path]
if !ok {
return baseLoad(path)
}
asr := componentsOfKind(comps, componentASR)
switch {
case len(asr) == 1:
return CppLoadComponent(path, asr[0])
case len(comps) == 1:
return CppLoadComponent(path, comps[0].Name)
}
b.loadErr = "bundle has several candidates"
return 0
}
return func() {
restoreBase()
CppBundleComponentsJSON, CppLoadComponent, CppLoadError, CppFreeString, CppLoad = savedJSON, savedComp, savedErr, savedFreeStr, savedLoad
}
}
var smallBundle = []bundleComponent{
{Name: "asr", Kind: "asr", License: "CC-BY-4.0"},
{Name: "diar", Kind: "diar", License: "OpenMDW-1.1"},
{Name: "ced", Kind: "ced", License: "Apache-2.0"},
{Name: "voice", Kind: "voice", License: "CC-BY-4.0"},
{Name: "vad", Kind: "vad", License: "MIT"},
}
var _ = Describe("bundle GGUF files (stubbed C API)", func() {
var restore func()
var savedAdd func(uintptr, string, *float32, int32) int32
var savedDim func(uintptr) int32
var savedBegin func(asr, diar, tagger, speaker, reg uintptr, o *cSceneOpts) uintptr
BeforeEach(func() {
savedAdd, savedDim, savedBegin = CppSpeakerRegistryAddEmbedding, CppSpeakerDim, CppSceneStreamBeginSpeaker
CppSpeakerRegistryAddEmbedding = func(uintptr, string, *float32, int32) int32 { return 0 }
CppSpeakerDim = func(uintptr) int32 { return 256 }
CppSceneStreamBeginSpeaker = func(_, _, _, _, _ uintptr, _ *cSceneOpts) uintptr { return 0 }
})
AfterEach(func() {
if restore != nil {
restore()
restore = nil
}
CppSpeakerRegistryAddEmbedding, CppSpeakerDim, CppSceneStreamBeginSpeaker = savedAdd, savedDim, savedBegin
})
load := func(b *bundleLib, opts ...string) (*ParakeetCpp, error) {
restore = b.install()
p := &ParakeetCpp{}
return p, p.Load(&pb.ModelOptions{ModelFile: "small.gguf", Options: opts})
}
It("opens the ASR component and the Silero component of a bundle with no options", func() {
b := newBundleLib().withBundle("small.gguf", smallBundle...)
p, err := load(b)
Expect(err).ToNot(HaveOccurred())
Expect(b.loadedComp).To(Equal([]string{"small.gguf#asr", "small.gguf#vad"}))
Expect(p.ctxPtr).ToNot(BeZero())
Expect(p.vadCtx).ToNot(BeZero())
Expect(p.diarCtx).To(BeZero(), "diarization is opt-in")
Expect(p.tagCtx).To(BeZero())
Expect(p.bundle).To(HaveLen(5))
Expect(p.vad).To(BeFalse(), "vad:true stays opt-in")
})
It("loads every role of a bundle from the component options and frees each context once", func() {
CppTranscribePathJSONVad = func(uintptr, string, int32) uintptr { return 0 }
DeferCleanup(func() { CppTranscribePathJSONVad = nil })
b := newBundleLib().withBundle("small.gguf", smallBundle...)
p, err := load(b, "diar_component:diar", "sound_component:ced", "speaker_component:voice", "vad:true")
Expect(err).ToNot(HaveOccurred())
Expect(p.ctxPtr).ToNot(BeZero())
Expect(p.diarCtx).ToNot(BeZero())
Expect(p.tagCtx).ToNot(BeZero())
Expect(p.spkCtx).ToNot(BeZero())
Expect(p.vadCtx).ToNot(BeZero())
Expect(p.vad).To(BeTrue())
Expect(p.Free()).To(Succeed())
Expect(b.freed).To(HaveLen(5))
})
It("takes the same file for a role through the companion option", func() {
b := newBundleLib().withBundle("small.gguf", smallBundle...)
p, err := load(b, "diarization_model:small.gguf", "sound_model:small.gguf")
Expect(err).ToNot(HaveOccurred())
Expect(p.diarCtx).ToNot(BeZero())
Expect(p.tagCtx).ToNot(BeZero())
Expect(b.loadedComp).To(ContainElements("small.gguf#diar", "small.gguf#ced"))
})
It("uses a bundle as the companion of a plain primary", func() {
b := newBundleLib().
withBundle("small.gguf", smallBundle...).
withPlain("asr.gguf", modelKindASR)
restore = b.install()
p := &ParakeetCpp{}
err := p.Load(&pb.ModelOptions{ModelFile: "asr.gguf", Options: []string{"diarization_model:small.gguf"}})
Expect(err).ToNot(HaveOccurred())
Expect(p.diarCtx).ToNot(BeZero())
Expect(p.bundle).To(BeNil(), "the primary is a plain file")
Expect(p.vadCtx).To(BeZero(), "the bundle VAD is only picked up from the primary file")
})
It("chooses an ASR component by bundle_asr", func() {
two := append([]bundleComponent{{Name: "fast", Kind: "asr"}, {Name: "big", Kind: "asr"}}, smallBundle[4])
b := newBundleLib().withBundle("small.gguf", two...)
_, err := load(b)
Expect(err).To(MatchError(And(ContainSubstring("several"), ContainSubstring("fast, big"), ContainSubstring("bundle_asr"))))
Expect(b.loadedComp).To(BeEmpty())
b2 := newBundleLib().withBundle("small.gguf", two...)
p, err := load(b2, "bundle_asr:big")
Expect(err).ToNot(HaveOccurred())
Expect(p.ctxPtr).ToNot(BeZero())
Expect(b2.loadedComp).To(ContainElement("small.gguf#big"))
})
It("rejects an unknown or wrong-kind component name and frees what it opened", func() {
b := newBundleLib().withBundle("small.gguf", smallBundle...)
_, err := load(b, "bundle_asr:nope")
Expect(err).To(MatchError(And(ContainSubstring(`no component "nope"`), ContainSubstring("vad (vad)"))))
b2 := newBundleLib().withBundle("small.gguf", smallBundle...)
_, err = load(b2, "diar_component:vad")
Expect(err).To(MatchError(ContainSubstring(`kind "vad"`)))
Expect(b2.freed).To(HaveLen(len(b2.loadedComp)), "every opened context is freed")
})
It("gives a clear error for a role the bundle has no component for", func() {
noDiar := []bundleComponent{smallBundle[0], smallBundle[4]}
b := newBundleLib().withBundle("small.gguf", noDiar...)
_, err := load(b, "diar_component:diar")
Expect(err).To(MatchError(ContainSubstring(`no component "diar"`)))
b2 := newBundleLib().withBundle("small.gguf", noDiar...)
_, err = load(b2, "diarization_model:small.gguf")
Expect(err).To(MatchError(And(ContainSubstring(`needs a "diar" component`), ContainSubstring("asr (asr), vad (vad)"))))
})
It("names the role mismatch in the RPC errors of a bundle", func() {
noDiar := []bundleComponent{smallBundle[0], smallBundle[4]}
b := newBundleLib().withBundle("small.gguf", noDiar...)
p, err := load(b)
Expect(err).ToNot(HaveOccurred())
_, err = p.Diarize(&pb.DiarizeRequest{Dst: "x.wav"})
Expect(err).To(MatchError(And(ContainSubstring("not a diarization model"), ContainSubstring(`without a "diar" component`))))
_, err = p.SoundDetection(context.Background(), &pb.SoundDetectionRequest{Src: "x.wav"})
Expect(err).To(MatchError(ContainSubstring(`without a "ced" component`)))
full := newBundleLib().withBundle("small.gguf", smallBundle...)
restore()
p, err = load(full)
Expect(err).ToNot(HaveOccurred())
_, err = p.Diarize(&pb.DiarizeRequest{Dst: "x.wav"})
Expect(err).To(MatchError(ContainSubstring("diar_component:diar")))
})
It("does not load the VAD component when vad_model or vad_component is given", func() {
b := newBundleLib().
withBundle("small.gguf", smallBundle...).
withPlain("silero.gguf", modelKindVAD)
restore = b.install()
p := &ParakeetCpp{}
CppTranscribePathJSONVadWith = func(ctx, vadCtx uintptr, wav string, dec int32, o string) uintptr { return 0 }
DeferCleanup(func() { CppTranscribePathJSONVadWith = nil })
Expect(p.Load(&pb.ModelOptions{ModelFile: "small.gguf", Options: []string{"vad_model:silero.gguf"}})).To(Succeed())
Expect(b.loadedComp).To(Equal([]string{"small.gguf#asr"}))
Expect(p.vad).To(BeTrue())
})
It("skips an ambiguous bundle VAD silently, and refuses it when vad_component is set without a name match", func() {
two := append(append([]bundleComponent{}, smallBundle...), bundleComponent{Name: "vad2", Kind: "vad"})
b := newBundleLib().withBundle("small.gguf", two...)
p, err := load(b)
Expect(err).ToNot(HaveOccurred())
Expect(p.vadCtx).To(BeZero())
CppTranscribePathJSONVadWith = func(ctx, vadCtx uintptr, wav string, dec int32, o string) uintptr { return 0 }
DeferCleanup(func() { CppTranscribePathJSONVadWith = nil })
restore()
b2 := newBundleLib().withBundle("small.gguf", two...)
p, err = load(b2, "vad_component:vad2")
Expect(err).ToNot(HaveOccurred())
Expect(p.vadCtx).ToNot(BeZero())
Expect(b2.loadedComp).To(ContainElement("small.gguf#vad2"))
})
It("rejects a component option on a file that is not a bundle", func() {
b := newBundleLib().withPlain("small.gguf", modelKindASR)
_, err := load(b, "diar_component:diar")
Expect(err).To(MatchError(ContainSubstring("not one")))
Expect(b.freed).To(HaveLen(1), "the primary is freed")
b2 := newBundleLib().withPlain("small.gguf", modelKindASR)
_, err = load(b2, "bundle_asr:asr")
Expect(err).To(MatchError(ContainSubstring("bundle_asr needs a bundle")))
})
It("loads a plain file exactly as before when the library has the bundle symbols", func() {
b := newBundleLib().withPlain("small.gguf", modelKindASR)
p, err := load(b)
Expect(err).ToNot(HaveOccurred())
Expect(p.ctxPtr).ToNot(BeZero())
Expect(p.bundle).To(BeNil())
Expect(b.loadedComp).To(BeEmpty())
})
It("loads a bundle without an ASR component through the library default", func() {
b := newBundleLib().withBundle("small.gguf", bundleComponent{Name: "vad", Kind: "vad"})
p, err := load(b)
Expect(err).ToNot(HaveOccurred())
Expect(p.vadCtx).ToNot(BeZero())
Expect(p.ctxPtr).To(BeZero())
})
It("reports the library reason when a bundle component does not open", func() {
b := newBundleLib().withBundle("small.gguf", smallBundle...)
restore = b.install()
saved := CppLoadComponent
CppLoadComponent = func(string, string) uintptr { b.loadErr = "component asr is damaged"; return 0 }
DeferCleanup(func() { CppLoadComponent = saved })
p := &ParakeetCpp{}
err := p.Load(&pb.ModelOptions{ModelFile: "small.gguf"})
Expect(err).To(MatchError(ContainSubstring("component asr is damaged")))
})
Describe("a libparakeet.so without the bundle symbols", func() {
It("loads a plain file as before, treating the file as ASR", func() {
f := newFakeLib().withModel("a.gguf", modelKindASR)
restore = f.install()
savedJSON, savedComp := CppBundleComponentsJSON, CppLoadComponent
CppBundleComponentsJSON, CppLoadComponent = nil, nil
DeferCleanup(func() { CppBundleComponentsJSON, CppLoadComponent = savedJSON, savedComp })
p := &ParakeetCpp{}
Expect(p.Load(&pb.ModelOptions{ModelFile: "a.gguf"})).To(Succeed())
Expect(p.bundle).To(BeNil())
})
It("refuses the component options", func() {
f := newFakeLib().withModel("a.gguf", modelKindASR)
restore = f.install()
savedJSON, savedComp := CppBundleComponentsJSON, CppLoadComponent
CppBundleComponentsJSON, CppLoadComponent = nil, nil
DeferCleanup(func() { CppBundleComponentsJSON, CppLoadComponent = savedJSON, savedComp })
for _, opt := range []string{"bundle_asr:asr", "diar_component:diar", "sound_component:ced", "vad_component:vad"} {
p := &ParakeetCpp{}
err := p.Load(&pb.ModelOptions{ModelFile: "a.gguf", Options: []string{opt}})
Expect(err).To(HaveOccurred(), opt)
Expect(err.Error()).To(Or(ContainSubstring("not one"), ContainSubstring("needs a bundle"), ContainSubstring("need")), opt)
}
})
})
})
var _ = Describe("bundle helpers", func() {
comps := []bundleComponent{{Name: "a", Kind: "asr"}, {Name: "v", Kind: "vad"}}
It("maps kinds both ways", func() {
for _, k := range []string{"asr", "vad", "diar", "ced", "voice"} {
m, ok := componentKindModel(k)
Expect(ok).To(BeTrue(), k)
Expect(kindForModel(m)).To(Equal(k))
}
_, ok := componentKindModel("future")
Expect(ok).To(BeFalse())
})
It("picks the only component of a kind and names the alternatives otherwise", func() {
name, err := pickComponent("f", comps, "", "vad", "vad_model", "vad_component")
Expect(err).ToNot(HaveOccurred())
Expect(name).To(Equal("v"))
_, err = pickComponent("f", comps, "", "ced", "sound_model", "sound_component")
Expect(err).To(MatchError(ContainSubstring("a (asr), v (vad)")))
})
})
+1 -1
View File
@@ -116,7 +116,7 @@ func (p *ParakeetCpp) Diarize(req *pb.DiarizeRequest) (pb.DiarizeResponse, error
}
if p.diarCtx == 0 {
return pb.DiarizeResponse{}, status.Error(codes.FailedPrecondition,
"parakeet-cpp: model is not a diarization model")
"parakeet-cpp: model is not a diarization model"+p.roleHint(componentDiar, "diar_component"))
}
if CppDiarizePCM == nil {
return pb.DiarizeResponse{}, status.Error(codes.Unimplemented,
+21 -6
View File
@@ -39,6 +39,18 @@ var (
CppFreeString func(s uintptr)
CppLastError func(ctx uintptr) string
// Bundle GGUF (additive in the C-API, no ABI bump; see bundle.go). All three
// are registered together and nil on an older libparakeet.so, where a
// bundle file is loaded like any other file (the library then refuses it)
// and the *_component options are rejected.
// CppLoadComponent opens one named component of a bundle.
// CppBundleComponentsJSON returns the component list as a malloc'd JSON
// array (uintptr, freed via CppFreeString), or 0 when the file is not a bundle.
// CppLoadError is the reason of the last failed load on the calling thread.
CppLoadComponent func(ggufPath, component string) uintptr
CppBundleComponentsJSON func(ggufPath string) uintptr
CppLoadError func() string
// CppTranscribePathJSONVad is CppTranscribePathJSON with long audio cut at
// pauses by the model's own VAD head (segments of at most 30 s; the document
// has the same shape, times are relative to the whole file). Returns 0 and
@@ -254,9 +266,12 @@ type ParakeetCpp struct {
// primary (asr_model:/diarization_model:/sound_model: options), so Free
// can release them after the primary.
companions []uintptr
engineMu sync.Mutex // sole guard of the one C engine (dispatcher + streaming)
bat *batcher
batStop chan struct{}
// bundle lists the components of the primary model file when it is a bundle
// GGUF (bundle.go); nil for a plain file.
bundle []bundleComponent
engineMu sync.Mutex // sole guard of the one C engine (dispatcher + streaming)
bat *batcher
batStop chan struct{}
// segmentGapFrames is NeMo's segment_gap_threshold in ENCODER FRAMES (model
// YAML option, default 0=off). When >0 it adds NeMo's silence-gap split on
// top of the punctuation split; converted to seconds via the JSON frame_sec.
@@ -295,11 +310,11 @@ func (p *ParakeetCpp) Load(opts *pb.ModelOptions) error {
return err
}
p.vadOptions = vadOpts
if optString(opts, "vad_model") != "" {
if optString(opts, "vad_model") != "" || optString(opts, "vad_component") != "" {
if CppTranscribePathJSONVadWith == nil {
return errors.New("parakeet-cpp: vad_model needs a libparakeet.so with parakeet_capi_transcribe_path_json_vad_with; rebuild the backend against a newer parakeet.cpp")
return errors.New("parakeet-cpp: vad_model and vad_component need a libparakeet.so with parakeet_capi_transcribe_path_json_vad_with; rebuild the backend against a newer parakeet.cpp")
}
// vad_model implies vad: a Silero model is only useful to cut audio.
// vad_model and vad_component imply vad: a Silero model is only useful to cut audio.
p.vad = true
}
+13
View File
@@ -89,6 +89,19 @@ func main() {
purego.RegisterLibFunc(&CppVadPcmJSON, lib, "parakeet_capi_vad_pcm_json")
}
// Bundle GGUF (one file with several models; docs/bundle.md in parakeet.cpp).
// Additive in the C-API. The three entry points come together, so one probe
// decides; without them a bundle file is handled like any other file.
if sym, err := purego.Dlsym(lib, "parakeet_capi_load_component"); err == nil && sym != 0 {
if sym2, err2 := purego.Dlsym(lib, "parakeet_capi_bundle_components_json"); err2 == nil && sym2 != 0 {
if sym3, err3 := purego.Dlsym(lib, "parakeet_capi_load_error"); err3 == nil && sym3 != 0 {
purego.RegisterLibFunc(&CppLoadComponent, lib, "parakeet_capi_load_component")
purego.RegisterLibFunc(&CppBundleComponentsJSON, lib, "parakeet_capi_bundle_components_json")
purego.RegisterLibFunc(&CppLoadError, lib, "parakeet_capi_load_error")
}
}
}
// Per-request language variants (multilingual nemotron). Same probe pattern:
// present only in libparakeet.so built with multilingual support, so the
// backend still loads against an older library and falls back to the
+177 -34
View File
@@ -103,6 +103,11 @@ type companionSpec struct {
wantKind int32
assign func(*ParakeetCpp, uintptr)
current func(*ParakeetCpp) uintptr
// component and compOpt are the bundle component name for this role and the
// option that sets it (bundle.go). With a component set and no value, the
// role is loaded from the primary model file, which must be a bundle.
component string
compOpt string
}
// indefiniteArticle returns "an" for a word starting with a vowel sound and
@@ -137,17 +142,23 @@ func (p *ParakeetCpp) loadRoles(opts *pb.ModelOptions) error {
soundModelOpt := optString(opts, "sound_model")
speakerModelOpt := optString(opts, "speaker_model")
vadModelOpt := optString(opts, "vad_model")
hasCompanionOpts := diarModelOpt != "" || asrModelOpt != "" || soundModelOpt != "" || speakerModelOpt != "" || vadModelOpt != ""
bundleASR := optString(opts, "bundle_asr")
vadComp := optString(opts, "vad_component")
diarComp := optString(opts, "diar_component")
soundComp := optString(opts, "sound_component")
speakerComp := optString(opts, "speaker_component")
hasCompanionOpts := diarModelOpt != "" || asrModelOpt != "" || soundModelOpt != "" || speakerModelOpt != "" || vadModelOpt != "" ||
bundleASR != "" || vadComp != "" || diarComp != "" || soundComp != "" || speakerComp != ""
if hasCompanionOpts && CppModelKind == nil {
return errors.New("parakeet-cpp: asr_model/diarization_model/sound_model/speaker_model/vad_model options need " +
return errors.New("parakeet-cpp: asr_model/diarization_model/sound_model/speaker_model/vad_model and the bundle component options need " +
"parakeet_capi_model_kind (ABI v8) to verify what they load; the loaded libparakeet.so " +
"is too old to report companion model roles")
}
if speakerModelOpt != "" {
if speakerModelOpt != "" || speakerComp != "" {
if CppSpeakerRegistryAddEmbedding == nil || CppSpeakerDim == nil || CppSceneStreamBeginSpeaker == nil {
return errors.New("parakeet-cpp: speaker_model needs libparakeet.so ABI 10 " +
return errors.New("parakeet-cpp: speaker_model and speaker_component need libparakeet.so ABI 10 " +
"(parakeet_capi_speaker_registry_add_embedding); the loaded library is older")
}
}
@@ -165,18 +176,11 @@ func (p *ParakeetCpp) loadRoles(opts *pb.ModelOptions) error {
return err
}
primary := CppLoad(opts.ModelFile)
if primary == 0 {
// No ctx to ask for last_error (the C-API's last-error buffer lives on
// the ctx that was never returned). Surface the path so the operator
// at least knows which load failed.
//
// A packed ternary Redux GGUF (redux-packed) is the likeliest cause on a
// GPU build: the library refuses it with a message that goes to its own
// log, not through the C-API, so name the cause here.
return fmt.Errorf("parakeet-cpp: parakeet_capi_load failed for %q (see the backend log for the library message; "+
"a packed ternary Redux model is CPU only and is refused on a GPU backend, use the redux-f16 or redux-q8_0 file there)", opts.ModelFile)
primary, comps, err := loadPrimary(opts.ModelFile, bundleASR)
if err != nil {
return err
}
p.bundle = comps
loaded := []uintptr{primary}
// freeLoaded undoes everything loadRoles opened this call: every context
// it freed AND every ParakeetCpp field it may have assigned (the primary
@@ -189,6 +193,7 @@ func (p *ParakeetCpp) loadRoles(opts *pb.ModelOptions) error {
CppFree(c)
}
p.ctxPtr, p.diarCtx, p.tagCtx, p.spkCtx, p.vadCtx = 0, 0, 0, 0, 0
p.bundle = nil
p.companions = nil
}
@@ -218,23 +223,42 @@ func (p *ParakeetCpp) loadRoles(opts *pb.ModelOptions) error {
specs := []companionSpec{
{"diarization_model", diarModelOpt, modelKindDiarization,
func(pp *ParakeetCpp, c uintptr) { pp.diarCtx = c },
func(pp *ParakeetCpp) uintptr { return pp.diarCtx }},
func(pp *ParakeetCpp) uintptr { return pp.diarCtx },
diarComp, "diar_component"},
{"asr_model", asrModelOpt, modelKindASR,
func(pp *ParakeetCpp, c uintptr) { pp.ctxPtr = c },
func(pp *ParakeetCpp) uintptr { return pp.ctxPtr }},
func(pp *ParakeetCpp) uintptr { return pp.ctxPtr },
bundleASR, "bundle_asr"},
{"sound_model", soundModelOpt, modelKindSound,
func(pp *ParakeetCpp, c uintptr) { pp.tagCtx = c },
func(pp *ParakeetCpp) uintptr { return pp.tagCtx }},
func(pp *ParakeetCpp) uintptr { return pp.tagCtx },
soundComp, "sound_component"},
{"speaker_model", speakerModelOpt, modelKindSpeaker,
func(pp *ParakeetCpp, c uintptr) { pp.spkCtx = c },
func(pp *ParakeetCpp) uintptr { return pp.spkCtx }},
func(pp *ParakeetCpp) uintptr { return pp.spkCtx },
speakerComp, "speaker_component"},
{"vad_model", vadModelOpt, modelKindVAD,
func(pp *ParakeetCpp, c uintptr) { pp.vadCtx = c },
func(pp *ParakeetCpp) uintptr { return pp.vadCtx }},
func(pp *ParakeetCpp) uintptr { return pp.vadCtx },
vadComp, "vad_component"},
}
for _, spec := range specs {
path := resolveModelPath(opts.ModelPath, spec.value)
fromPrimary := false
if spec.value == "" {
continue
// bundle_asr names the ASR of the primary file, so on its own it is
// not a request for an asr_model companion.
if spec.component == "" || spec.optName == "asr_model" {
continue
}
// A component option with no companion file: the role comes from the
// primary file, which must be a bundle.
if comps == nil {
freeLoaded()
return fmt.Errorf("parakeet-cpp: %s needs the model file or a %s to be a bundle GGUF; %q is not one",
spec.compOpt, spec.optName, opts.ModelFile)
}
path, fromPrimary = opts.ModelFile, true
}
// A companion whose role the primary already occupies (e.g. asr_model:
// on an already-ASR primary) would overwrite that role field below,
@@ -245,30 +269,46 @@ func (p *ParakeetCpp) loadRoles(opts *pb.ModelOptions) error {
return fmt.Errorf("parakeet-cpp: %s is not allowed on %s %s model",
spec.optName, indefiniteArticle(modelKindName(spec.wantKind)), modelKindName(spec.wantKind))
}
resolved := resolveModelPath(opts.ModelPath, spec.value)
cctx := CppLoad(resolved)
if cctx == 0 {
freeLoaded()
return fmt.Errorf("parakeet-cpp: failed to load %s %q", spec.optName, resolved)
cctx, err := loadCompanion(spec, path, fromPrimary, comps)
if cctx != 0 {
loaded = append(loaded, cctx)
}
loaded = append(loaded, cctx)
if gotKind := CppModelKind(cctx); gotKind != spec.wantKind {
if err != nil {
freeLoaded()
return fmt.Errorf("parakeet-cpp: %s %q is %s %s model, expected %s %s model",
spec.optName, resolved, indefiniteArticle(modelKindName(gotKind)), modelKindName(gotKind),
indefiniteArticle(modelKindName(spec.wantKind)), modelKindName(spec.wantKind))
return err
}
spec.assign(p, cctx)
p.companions = append(p.companions, cctx)
}
if vadModelOpt != "" && p.ctxPtr == 0 {
// A Silero VAD component of the primary bundle is the VAD source without any
// option, as in the parakeet-cli rules: it is small and serves the VAD
// endpoint and vad:true. Not finding one, or not being able to load it, is
// not an error here: the ASR head (if any) stays the fallback.
if vadModelOpt == "" && vadComp == "" && p.vadCtx == 0 && p.ctxPtr != 0 && comps != nil && CppLoadComponent != nil {
if names := componentsOfKind(comps, componentVAD); len(names) == 1 {
vctx, verr := loadComponent(opts.ModelFile, names[0])
switch {
case verr != nil:
xlog.Warn("parakeet-cpp: bundle VAD component not loaded", "error", verr)
case CppModelKind(vctx) != modelKindVAD:
CppFree(vctx)
xlog.Warn("parakeet-cpp: bundle VAD component is not a Silero model; using the ASR VAD head if any", "component", names[0])
default:
loaded = append(loaded, vctx)
p.vadCtx = vctx
p.companions = append(p.companions, vctx)
}
}
}
if (vadModelOpt != "" || vadComp != "") && p.ctxPtr == 0 {
freeLoaded()
return errors.New("parakeet-cpp: vad_model cuts audio for transcription and needs an ASR model (the primary or asr_model:)")
return errors.New("parakeet-cpp: vad_model and vad_component cut audio for transcription and needs an ASR model (the primary or asr_model:)")
}
if p.spkCtx != 0 && p.diarCtx == 0 {
freeLoaded()
return errors.New("parakeet-cpp: speaker_model needs a diarization model (the primary or diarization_model:)")
return errors.New("parakeet-cpp: speaker_model needs a diarization model (the primary, diarization_model: or diar_component:)")
}
p.speakerAccept, p.speakerMargin = accept, margin
p.diarLatency = latency
@@ -296,3 +336,106 @@ func (p *ParakeetCpp) notASRError() error {
return nil
}
}
// loadPrimary opens the primary model file. A bundle is opened by component: the
// bundle_asr one, else its only ASR component. A bundle with several ASR
// components and no bundle_asr is refused with their names. A bundle with no ASR
// component is left to the library, which opens its only loadable component.
// Any other file is opened as before. comps lists the components of a bundle
// (nil for a plain file); nothing is loaded when it returns an error.
func loadPrimary(path, bundleASR string) (ctx uintptr, comps []bundleComponent, err error) {
comps, err = bundleComponents(path)
if err != nil {
return 0, nil, err
}
if comps == nil {
if bundleASR != "" {
return 0, nil, fmt.Errorf("parakeet-cpp: bundle_asr needs a bundle GGUF; %q is not one (or libparakeet.so has no bundle support)", path)
}
ctx = CppLoad(path)
if ctx == 0 {
// No ctx to ask for last_error (the C-API's last-error buffer lives on
// the ctx that was never returned). Surface the path so the operator
// at least knows which load failed.
//
// A packed ternary Redux GGUF (redux-packed) is the likeliest cause on a
// GPU build: the library refuses it with a message that goes to its own
// log, not through the C-API, so name the cause here.
return 0, nil, fmt.Errorf("parakeet-cpp: parakeet_capi_load failed for %q (see the backend log for the library message; "+
"a packed ternary Redux model is CPU only and is refused on a GPU backend, use the redux-f16 or redux-q8_0 file there)", path)
}
return ctx, nil, nil
}
if bundleASR == "" && len(componentsOfKind(comps, componentASR)) == 0 {
ctx = CppLoad(path)
if ctx == 0 {
return 0, nil, fmt.Errorf("parakeet-cpp: failed to load the bundle %q: %s", path, loadErrorText())
}
return ctx, comps, nil
}
name, err := pickComponent(path, comps, bundleASR, componentASR, "the primary model", "bundle_asr")
if err != nil {
return 0, nil, err
}
ctx, err = loadComponent(path, name)
if err != nil {
return 0, nil, withRedux(err)
}
return ctx, comps, nil
}
// withRedux adds the CPU-only hint of a packed Redux component to a failed
// component load: the library refuses it on a GPU backend.
func withRedux(err error) error {
return fmt.Errorf("%w (a packed ternary Redux model is CPU only and is refused on a GPU backend)", err)
}
func loadErrorText() string {
if CppLoadError == nil {
return ""
}
return CppLoadError()
}
// loadCompanion loads the model of one companion role from path. A bundle gives
// its component of the wanted kind (named by the role's component option, else
// the only one); a plain file is loaded as before and refuses a component
// option. It returns the context even with an error when the context was
// opened but is not usable, so the caller can free it. fromPrimary tells that
// path is the primary file, already known to be a bundle with components comps.
func loadCompanion(spec companionSpec, path string, fromPrimary bool, comps []bundleComponent) (uintptr, error) {
if !fromPrimary {
var err error
comps, err = bundleComponents(path)
if err != nil {
return 0, err
}
}
var cctx uintptr
if comps == nil {
if spec.component != "" {
return 0, fmt.Errorf("parakeet-cpp: %s needs a bundle GGUF; %q is not one (or libparakeet.so has no bundle support)",
spec.compOpt, path)
}
cctx = CppLoad(path)
if cctx == 0 {
return 0, fmt.Errorf("parakeet-cpp: failed to load %s %q", spec.optName, path)
}
} else {
wantKind := kindForModel(spec.wantKind)
name, err := pickComponent(path, comps, spec.component, wantKind, spec.optName, spec.compOpt)
if err != nil {
return 0, err
}
cctx, err = loadComponent(path, name)
if err != nil {
return 0, err
}
}
if gotKind := CppModelKind(cctx); gotKind != spec.wantKind {
return cctx, fmt.Errorf("parakeet-cpp: %s %q is %s %s model, expected %s %s model",
spec.optName, path, indefiniteArticle(modelKindName(gotKind)), modelKindName(gotKind),
indefiniteArticle(modelKindName(spec.wantKind)), modelKindName(spec.wantKind))
}
return cctx, nil
}
+1 -1
View File
@@ -51,7 +51,7 @@ type classAvg struct {
func (p *ParakeetCpp) SoundDetection(ctx context.Context, req *pb.SoundDetectionRequest) (*pb.SoundDetectionResponse, error) {
if p.tagCtx == 0 {
return nil, status.Error(codes.FailedPrecondition,
"parakeet-cpp: model is not a sound (CED) model")
"parakeet-cpp: model is not a sound (CED) model"+p.roleHint(componentSound, "sound_component"))
}
if CppSoundStreamBegin == nil || CppSoundStreamFeed == nil || CppSoundStreamDrainScoresJSON == nil ||
CppSoundStreamFree == nil || CppSoundOptsDefault == nil || CppNumClasses == nil {
@@ -0,0 +1,113 @@
package gallery_test
import (
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/gallery"
)
// The parakeet-cpp bundle entries install one GGUF file that holds several
// models. The component options name components inside that file, so a typo
// installs cleanly and fails when the model loads. The sizes and checksums are
// those of the files published in mudler/parakeet-cpp-gguf.
var _ = Describe("gallery/index.yaml parakeet-cpp bundle entries", func() {
type bundle struct {
file string
sha256 string
usecases []string
options []string
}
full := []string{"vad:true", "diar_component:diar", "sound_component:ced", "speaker_component:voice"}
want := map[string]bundle{
"parakeet-cpp-bundle-small": {
"parakeet-cpp/parakeet-bundle-small.gguf",
"5f2c6697eb08d4d67e858692d4223a9d911aacc51ae16cf2aee9e760f8a2269a",
[]string{"transcript", "vad", "diarization", "sound_classification"}, full,
},
"parakeet-cpp-bundle-standard": {
"parakeet-cpp/parakeet-bundle-standard.gguf",
"85304e3bb49de04b84a7d0c76baaa7dedd1a0c02c0920078a99f92c00cffbdf6",
[]string{"transcript", "vad", "diarization", "sound_classification"}, full,
},
"parakeet-cpp-bundle-moondream-redux": {
"parakeet-cpp/parakeet-bundle-moondream-redux.gguf",
"d7c5bff66b1dbc02f4c7a83148b0836b57805676c844193c214c751249853d7b",
[]string{"transcript", "vad"}, []string{"vad:true"},
},
}
entries := func() map[string]gallery.GalleryModel {
list, err := loadGalleryIndex()
Expect(err).ToNot(HaveOccurred())
m := map[string]gallery.GalleryModel{}
for _, e := range list {
m[e.Name] = e
}
return m
}
It("installs one bundle file with the published checksum and serves it as the model", func() {
all := entries()
for name, w := range want {
e, ok := all[name]
Expect(ok).To(BeTrue(), name)
Expect(e.Overrides["backend"]).To(Equal("parakeet-cpp"), name)
Expect(e.Overrides["name"]).To(Equal(name), name)
Expect(e.Overrides["parameters"]).To(HaveKeyWithValue("model", w.file), name)
Expect(e.AdditionalFiles).To(HaveLen(1), name)
Expect(e.AdditionalFiles[0].Filename).To(Equal(w.file), name)
Expect(e.AdditionalFiles[0].SHA256).To(Equal(w.sha256), name)
Expect(e.AdditionalFiles[0].URI).To(Equal("huggingface://mudler/parakeet-cpp-gguf/"+strings.TrimPrefix(w.file, "parakeet-cpp/")), name)
Expect(e.Variants).To(BeEmpty(), name)
}
})
It("declares the usecases and options of the roles each bundle covers", func() {
all := entries()
for name, w := range want {
e := all[name]
Expect(e.Overrides["known_usecases"]).To(ConsistOf(toAny(w.usecases)...), name)
Expect(e.Overrides["options"]).To(ConsistOf(toAny(w.options)...), name)
}
})
It("states that the licence is per component and names each one", func() {
all := entries()
for name := range want {
e := all[name]
Expect(e.License).To(Equal("other"), name)
Expect(e.Description).To(ContainSubstring("no single license"), name)
Expect(e.Description).To(ContainSubstring("CC-BY-4.0"), name)
Expect(e.Description).To(ContainSubstring("MIT"), name)
}
for _, name := range []string{"parakeet-cpp-bundle-small", "parakeet-cpp-bundle-standard"} {
d := all[name].Description
Expect(d).To(ContainSubstring("OpenMDW-1.1"), name)
Expect(d).To(ContainSubstring("Apache-2.0"), name)
Expect(d).To(ContainSubstring("not consistent upstream"), name)
Expect(d).To(ContainSubstring("converted, not trained"), name)
}
Expect(all["parakeet-cpp-bundle-moondream-redux"].Description).To(ContainSubstring("CPU only"))
})
It("keeps the single-purpose parakeet-cpp entries", func() {
all := entries()
for _, name := range []string{
"parakeet-cpp-tdt_ctc-110m", "parakeet-cpp-tdt-0.6b-v3", "parakeet-cpp-moondream-redux-packed",
"parakeet-cpp-silero-vad-f16", "parakeet-cpp-nemotron-3-diarization", "parakeet-cpp-ced-tiny",
} {
Expect(all).To(HaveKey(name))
}
})
})
func toAny(in []string) []any {
out := make([]any, len(in))
for i, s := range in {
out[i] = s
}
return out
}
@@ -80,6 +80,8 @@ curl http://localhost:8080/v1/audio/classification \
-F top_k=10
```
The bundle entries `parakeet-cpp-bundle-small` and `parakeet-cpp-bundle-standard` also serve this endpoint: they hold CED-Small next to the transcription, VAD and diarization models (`sound_component:ced`). See [Bundle GGUF files]({{% relref "audio-to-text" %}}#bundle-gguf-files-several-models-in-one-file).
## See also
- [Audio to Text]({{% relref "audio-to-text" %}}) - speech transcription
@@ -196,6 +196,8 @@ The complete `-asr-speakers` entry downloads Nemotron-3-Diarization, Parakeet TD
It configures both `asr_model` and `speaker_model`; no custom gallery configuration is needed.
See [Remember speakers in the Web UI](#remember-speakers-in-the-web-ui) for installation and enrollment.
The entries `parakeet-cpp-bundle-small` and `parakeet-cpp-bundle-standard` hold Nemotron-3-Diarization, an ASR model and the WeSpeaker speaker encoder in one file (`diar_component:diar` and `speaker_component:voice`), so one install serves the transcript and the identification options above. See [Bundle GGUF files]({{% relref "audio-to-text" %}}#bundle-gguf-files-several-models-in-one-file).
For manual configuration, this example pairs Sortformer with ASR:
```yaml
+51
View File
@@ -308,6 +308,57 @@ The segmenter options below apply to both `vad:true` and `vad_model`. Each is op
`vad_speech_pad` (seconds) pads each region and only affects the [VAD endpoint]({{%relref "features/voice-activity-detection" %}}). `vad_model` needs a `libparakeet.so` that exports `parakeet_capi_transcribe_path_json_vad_with`; an older library fails the load with a message that names it.
### Bundle GGUF files (several models in one file)
A bundle is one GGUF file that holds several models, called components. Each component keeps its own licence. The backend opens the components it needs from the one file, so a single model YAML can serve transcription, VAD, diarization, speaker naming and sound events. A bundle needs a `libparakeet.so` from parakeet.cpp with bundle support (pin `781a973` or newer); the format is described in the [parakeet.cpp bundle documentation](https://github.com/mudler/parakeet.cpp/blob/master/docs/bundle.md). Single-model files and every existing option work as before.
The gallery has three bundles, built from [`mudler/parakeet-cpp-gguf`](https://huggingface.co/mudler/parakeet-cpp-gguf):
| Gallery entry | Size | Components | Serves |
|---|---|---|---|
| `parakeet-cpp-bundle-small` | 338 MB | Parakeet TDT+CTC 110M (Q8_0), Nemotron-3-Diarization (Q8_0), CED-Small (Q8_0), WeSpeaker ResNet34-LM (F32), Silero VAD (F16) | transcription, VAD, diarization, speaker naming, sound events |
| `parakeet-cpp-bundle-standard` | 1.1 GB | Parakeet TDT 0.6B v3 (Q8_0), plus the same four components | the same, with the multilingual 0.6B model |
| `parakeet-cpp-bundle-moondream-redux` | 215 MB | Moondream Redux (packed ternary), Silero VAD (F16) | transcription and VAD; CPU only and offline only |
The component that each role uses, and the option that picks another one:
| Role | Component used | Option |
|---|---|---|
| Transcription | the only `asr` component | `bundle_asr:<name>` picks one when the bundle has several |
| VAD (`/v1/vad`, and `vad:true` for long audio) | the `vad` (Silero) component, loaded with no option; without one the VAD head of the ASR model | `vad_component:<name>` picks one, and implies `vad:true` |
| Diarization | the `diar` component, only when asked for | `diar_component:<name>` |
| Sound events | the `ced` component, only when asked for | `sound_component:<name>` |
| Speaker naming | the `voice` component, only when asked for | `speaker_component:<name>` (needs a diarization component) |
A `*_component` option without the matching companion option takes the component from the model file itself. The companion options (`diarization_model:`, `sound_model:`, `speaker_model:`, `vad_model:`, `asr_model:`) can also name a bundle file, even the same file as the model: the only component of the wanted kind is used, and the `*_component` option picks one when there are several. This YAML loads the same file for four roles:
```yaml
name: parakeet-bundle
backend: parakeet-cpp
parameters:
model: parakeet-cpp/parakeet-bundle-small.gguf
options:
- vad:true # cut long audio at pauses, with the Silero component
- diar_component:diar # same as diarization_model:parakeet-cpp/parakeet-bundle-small.gguf
- sound_component:ced
- speaker_component:voice
```
A role that the bundle cannot fill fails with a message that lists the components, for example `parakeet-cpp: diarization_model needs a "diar" component, but the bundle "<path>" has none (components: asr (asr), vad (vad))`. A request for a role the loaded model does not have (diarization from a bundle without a `diar` component) returns `parakeet-cpp: model is not a diarization model (the model file is a bundle without a "diar" component; ...)`. A `*_component` option on a file that is not a bundle, or on a library without bundle support, fails the load.
Licences: a bundle has no single licence, so the gallery entries use `license: other`. The licence, source and credit of every component are in the file header, and the NOTICE file next to each bundle in the repository (`NOTICE-<bundle>.txt`) has the credits and the full licence texts. Keep it with any copy of the file you pass on.
| Component | Licence | Credit |
|---|---|---|
| Parakeet TDT+CTC 110M, Parakeet TDT 0.6B v3 | CC-BY-4.0 | NVIDIA |
| Moondream Redux | CC-BY-4.0 | Moondream, derived from Parakeet TDT 0.6B v3 by NVIDIA |
| Nemotron-3-Diarization | OpenMDW-1.1 | NVIDIA |
| CED-Small | Apache-2.0, as stated on the model card | Heinrich Dinkel et al., Xiaomi (mispeech) |
| WeSpeaker ResNet34-LM | CC-BY-4.0 | the WeSpeaker project |
| Silero VAD | MIT | Silero Team |
The licence of the CED weights is not consistent upstream: the model card says Apache-2.0, the upstream code repository is GPL-3.0 and the original checkpoint records say CC-BY-4.0. The file here is converted, not trained, and follows the model card. The weights of all components were converted to GGUF, and quantised where the table shows it; nothing was retrained.
## See also
- [Audio Transform]({{< relref "audio-transform.md" >}}) - clean up the audio (echo cancellation, noise suppression, dereverberation) before passing it to a transcription model.
@@ -125,6 +125,8 @@ The `parakeet-cpp` backend serves the same endpoint. It runs one of two detector
- **The VAD head** of a full Moondream Ultra or Redux model (gallery entries `parakeet-cpp-vad-moondream-ultra-q8_0` and `parakeet-cpp-vad-moondream-redux-packed`). One probability per 80 ms. The packed Redux file runs on CPU only.
- **A VAD-only slice** of that head (gallery entries `parakeet-cpp-vad-moondream-redux`, 9.9 MB, and `parakeet-cpp-vad-moondream-ultra`, 6.0 MB). The slice is cut out of the full model without retraining, so the segments are byte-identical to the full model's head, and the speed is the same. Compared with loading the whole model (213 MB to 1.4 GB), the file is 6 to 10 MB, loads in a few milliseconds instead of 0.1 to 0.7 s, and needs about 245 MiB of peak memory for a 33 s clip instead of 0.6 to 1.6 GiB. A slice cannot transcribe, and it needs a parakeet.cpp build with VAD-only GGUF support (pin e53a253 or newer).
- **A bundle GGUF** (gallery entries `parakeet-cpp-bundle-small`, `parakeet-cpp-bundle-standard` and `parakeet-cpp-bundle-moondream-redux`). The bundle holds a Silero component next to the ASR model, and the backend uses it for this endpoint with no option. See [Bundle GGUF files]({{%relref "features/audio-to-text" %}}#bundle-gguf-files-several-models-in-one-file).
The entry `parakeet-cpp-vad` installs Silero. The detectors differ and are not variants of one model, so install the entry of the VAD head by name if you want it (`parakeet-cpp-vad-moondream-redux` or `parakeet-cpp-vad-moondream-ultra` for the small files). The request is the same as above: `audio` is 16 kHz mono float32 PCM, and the response lists `segments` with `start` and `end` in seconds. An ASR model that has no VAD head fails the request with `model has no VAD head`.
```yaml
+172
View File
@@ -54685,6 +54685,178 @@
- filename: parakeet-cpp/ced-base-q8_0.gguf
uri: huggingface://mudler/ced-gguf/ced-base-q8_0.gguf
sha256: bd34a7710169f0047fea17267965d211f967828ab25ba6fb9d3768481393f6e2
- name: parakeet-cpp-bundle-small
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
- https://huggingface.co/mudler/parakeet-cpp-gguf
- https://github.com/mudler/parakeet.cpp
- https://huggingface.co/nvidia/parakeet-tdt_ctc-110m
- https://huggingface.co/nvidia/Nemotron-3-Diarization
- https://huggingface.co/mispeech/ced-small
- https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet34-LM
- https://github.com/snakers4/silero-vad
description: |
Parakeet TDT+CTC 110M with diarization, sound events, speaker naming and VAD. One bundle GGUF file (about 338 MB) that holds five models for the parakeet-cpp backend
(C++/ggml port of NVIDIA NeMo Parakeet): Parakeet TDT+CTC 110M (English, Q8_0), Nemotron-3-Diarization (Q8_0), CED-Small sound
events (Q8_0), WeSpeaker ResNet34-LM speaker encoder (F32) and Silero VAD (F16). One install serves
transcription (/v1/audio/transcriptions), voice activity detection (/v1/vad), speaker diarization with
text and named speakers (/v1/audio/diarization) and sound events (/v1/audio/classification). The vad:true
option cuts long audio at pauses found by Silero before it transcribes. The bundle options
(diar_component, sound_component, speaker_component) load each model from the same file; see the
audio-to-text docs for the option list.
A bundle has no single license: each model keeps its own, listed in the file header and in the NOTICE file
next to the file. Parakeet TDT+CTC 110M by NVIDIA is CC-BY-4.0, Nemotron-3-Diarization by NVIDIA is OpenMDW-1.1, CED-Small is Apache-2.0
(as stated on the model card; the upstream code repository is GPL-3.0 and the original checkpoint records say
CC-BY-4.0, so the licence of the weights is not consistent upstream; the model here is converted, not trained),
WeSpeaker ResNet34-LM by the WeSpeaker project is CC-BY-4.0, Silero VAD by the Silero Team is MIT. The
weights were converted to GGUF and quantised where stated; nothing was retrained.
license: other
tags:
- parakeet
- parakeet-cpp
- nemotron
- sortformer
- ced
- silero
- bundle
- asr
- speech-recognition
- stt
- vad
- diarization
- speaker-diarization
- sound-classification
- gguf
- ggml
- quantized
overrides:
backend: parakeet-cpp
known_usecases:
- transcript
- vad
- diarization
- sound_classification
name: parakeet-cpp-bundle-small
options:
- vad:true
- diar_component:diar
- sound_component:ced
- speaker_component:voice
parameters:
model: parakeet-cpp/parakeet-bundle-small.gguf
files:
- filename: parakeet-cpp/parakeet-bundle-small.gguf
uri: huggingface://mudler/parakeet-cpp-gguf/parakeet-bundle-small.gguf
sha256: 5f2c6697eb08d4d67e858692d4223a9d911aacc51ae16cf2aee9e760f8a2269a
- name: parakeet-cpp-bundle-standard
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
- https://huggingface.co/mudler/parakeet-cpp-gguf
- https://github.com/mudler/parakeet.cpp
- https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3
- https://huggingface.co/nvidia/Nemotron-3-Diarization
- https://huggingface.co/mispeech/ced-small
- https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet34-LM
- https://github.com/snakers4/silero-vad
description: |
Parakeet TDT 0.6B v3 (multilingual) with diarization, sound events, speaker naming and VAD. One bundle GGUF file (about 1101 MB) that holds five models for the parakeet-cpp backend
(C++/ggml port of NVIDIA NeMo Parakeet): Parakeet TDT 0.6B v3 (multilingual, Q8_0), Nemotron-3-Diarization (Q8_0), CED-Small sound
events (Q8_0), WeSpeaker ResNet34-LM speaker encoder (F32) and Silero VAD (F16). One install serves
transcription (/v1/audio/transcriptions), voice activity detection (/v1/vad), speaker diarization with
text and named speakers (/v1/audio/diarization) and sound events (/v1/audio/classification). The vad:true
option cuts long audio at pauses found by Silero before it transcribes. The bundle options
(diar_component, sound_component, speaker_component) load each model from the same file; see the
audio-to-text docs for the option list.
A bundle has no single license: each model keeps its own, listed in the file header and in the NOTICE file
next to the file. Parakeet TDT 0.6B v3 by NVIDIA is CC-BY-4.0, Nemotron-3-Diarization by NVIDIA is OpenMDW-1.1, CED-Small is Apache-2.0
(as stated on the model card; the upstream code repository is GPL-3.0 and the original checkpoint records say
CC-BY-4.0, so the licence of the weights is not consistent upstream; the model here is converted, not trained),
WeSpeaker ResNet34-LM by the WeSpeaker project is CC-BY-4.0, Silero VAD by the Silero Team is MIT. The
weights were converted to GGUF and quantised where stated; nothing was retrained.
license: other
tags:
- parakeet
- parakeet-cpp
- nemotron
- sortformer
- ced
- silero
- bundle
- asr
- speech-recognition
- stt
- vad
- diarization
- speaker-diarization
- sound-classification
- gguf
- ggml
- quantized
overrides:
backend: parakeet-cpp
known_usecases:
- transcript
- vad
- diarization
- sound_classification
name: parakeet-cpp-bundle-standard
options:
- vad:true
- diar_component:diar
- sound_component:ced
- speaker_component:voice
parameters:
model: parakeet-cpp/parakeet-bundle-standard.gguf
files:
- filename: parakeet-cpp/parakeet-bundle-standard.gguf
uri: huggingface://mudler/parakeet-cpp-gguf/parakeet-bundle-standard.gguf
sha256: 85304e3bb49de04b84a7d0c76baaa7dedd1a0c02c0920078a99f92c00cffbdf6
- name: parakeet-cpp-bundle-moondream-redux
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
- https://huggingface.co/mudler/parakeet-cpp-gguf
- https://github.com/mudler/parakeet.cpp
- https://huggingface.co/moondream/parakeet-redux
- https://github.com/snakers4/silero-vad
description: |
Moondream Redux with Silero VAD in one bundle GGUF file (about 215 MB) for the parakeet-cpp backend
(C++/ggml port of NVIDIA NeMo Parakeet). Redux is Moondream's ternary-encoder derivative of NVIDIA
parakeet-tdt-0.6b-v3 (TDT, 0.6B), kept as published with the packed ternary weights. The bundle also holds
Silero VAD (F16). One install serves transcription (/v1/audio/transcriptions) and voice activity detection
(/v1/vad). The vad:true option cuts long audio at pauses before it transcribes, with Silero as the VAD source.
CPU only and offline only: the library refuses the packed Redux model on a GPU backend and does not stream it.
For a GPU or for streaming, use the redux-f16 or redux-q8_0 entry instead.
A bundle has no single license: each model keeps its own, listed in the file header and in the NOTICE file
next to the file. Parakeet Redux by Moondream, derived from Parakeet TDT 0.6B v3 by NVIDIA, is CC-BY-4.0
(credit Moondream and NVIDIA); Silero VAD by the Silero Team is MIT. The weights were converted to GGUF;
nothing was retrained.
license: other
tags:
- parakeet
- parakeet-cpp
- moondream
- silero
- bundle
- asr
- speech-recognition
- stt
- vad
- gguf
- ggml
overrides:
backend: parakeet-cpp
known_usecases:
- transcript
- vad
name: parakeet-cpp-bundle-moondream-redux
options:
- vad:true
parameters:
model: parakeet-cpp/parakeet-bundle-moondream-redux.gguf
files:
- filename: parakeet-cpp/parakeet-bundle-moondream-redux.gguf
uri: huggingface://mudler/parakeet-cpp-gguf/parakeet-bundle-moondream-redux.gguf
sha256: d7c5bff66b1dbc02f4c7a83148b0836b57805676c844193c214c751249853d7b
- name: moss-transcribe-cpp-0.9b
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls: