From 761505237d8bb42e59deb59537a215fa057c6b12 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 28 Sep 2026 06:57:51 +0000 Subject: [PATCH] feat(importers): detect the parakeet.cpp diarization GGUF The Nemotron-3-Diarization GGUFs are now published in mudler/parakeet-cpp-gguf as nemotron-3-diarization-.gguf. The parakeet-cpp importer did not recognise that name, so a direct import fell through to another importer. A direct URL to the file now imports with the diarization usecase. A repo import still picks ASR weights when the repo also ships the diarization model, and falls back to the diarization weights only when there are no others. The docs import the published file instead of converting the checkpoint. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- core/gallery/importers/parakeet-cpp.go | 31 +++++++++++++-- core/gallery/importers/parakeet-cpp_test.go | 42 +++++++++++++++++++++ docs/content/features/audio-diarization.md | 8 ++-- docs/content/features/audio-to-text.md | 2 +- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/core/gallery/importers/parakeet-cpp.go b/core/gallery/importers/parakeet-cpp.go index aa732aa3e..5b758167b 100644 --- a/core/gallery/importers/parakeet-cpp.go +++ b/core/gallery/importers/parakeet-cpp.go @@ -108,6 +108,11 @@ func (i *ParakeetCppImporter) Import(details Details) (gallery.ModelConfig, erro uri := downloader.URI(details.URI) directGGUF := isParakeetGGUF(filepath.Base(details.URI)) + // A speaker diarization GGUF is served by the same backend but answers + // /v1/audio/diarization, not transcription. + if directGGUF && isParakeetDiarGGUF(filepath.Base(details.URI)) { + modelConfig.KnownUsecaseStrings = []string{"diarization"} + } switch { case uri.LooksLikeURL() && directGGUF: // Direct file URL (e.g. .../resolve/main/tdt_ctc-110m-f16.gguf). The @@ -128,12 +133,23 @@ func (i *ParakeetCppImporter) Import(details Details) (gallery.ModelConfig, erro // HF repo: collect every parakeet GGUF, pick the preferred quant, and // nest under parakeet-cpp/models// so a multi-quant repo doesn't // collide on disk. - var ggufFiles []hfapi.ModelFile + // Prefer ASR weights: a repo that also ships the diarization model + // (mudler/parakeet-cpp-gguf) imports as a transcription model, and the + // diarization GGUF is imported by its direct URL. A repo with only + // diarization weights imports as a diarization model. + var ggufFiles, diarFiles []hfapi.ModelFile for _, f := range details.HuggingFace.Files { - if isParakeetGGUF(filepath.Base(f.Path)) { + switch base := filepath.Base(f.Path); { + case isParakeetDiarGGUF(base): + diarFiles = append(diarFiles, f) + case isParakeetGGUF(base): ggufFiles = append(ggufFiles, f) } } + if len(ggufFiles) == 0 && len(diarFiles) > 0 { + ggufFiles = diarFiles + modelConfig.KnownUsecaseStrings = []string{"diarization"} + } if chosen, ok := pickPreferredGGMLFile(ggufFiles, quants); ok { target := filepath.Join("parakeet-cpp", "models", name, filepath.Base(chosen.Path)) cfg.Files = append(cfg.Files, gallery.File{ @@ -176,5 +192,14 @@ func isParakeetGGUF(name string) bool { return true } } - return false + return isParakeetDiarGGUF(name) +} + +// isParakeetDiarGGUF reports whether name is the parakeet.cpp speaker +// diarization GGUF (nemotron-3-diarization-.gguf). Matched by its +// published name only, so diarization weights for other backends are not +// claimed. +func isParakeetDiarGGUF(name string) bool { + lower := strings.ToLower(name) + return strings.HasSuffix(lower, ".gguf") && strings.Contains(lower, "nemotron-3-diarization") } diff --git a/core/gallery/importers/parakeet-cpp_test.go b/core/gallery/importers/parakeet-cpp_test.go index 4aa87c411..8636c929d 100644 --- a/core/gallery/importers/parakeet-cpp_test.go +++ b/core/gallery/importers/parakeet-cpp_test.go @@ -50,6 +50,11 @@ var _ = Describe("ParakeetCppImporter", func() { Expect(imp.Match(d)).To(BeTrue()) }) + It("matches a direct URL to the diarization GGUF", func() { + d := parakeetDetails("https://huggingface.co/mudler/parakeet-cpp-gguf/resolve/main/nemotron-3-diarization-q8_0.gguf", `{}`) + Expect(imp.Match(d)).To(BeTrue()) + }) + It("does NOT claim a generic llama-style GGUF", func() { d := parakeetDetails("huggingface://someorg/some-llm-gguf", `{}`, hfapi.ModelFile{Path: "llama-3-8b-instruct-q4_k_m.gguf"}, @@ -66,6 +71,43 @@ var _ = Describe("ParakeetCppImporter", func() { }) Context("import (Import)", func() { + It("imports the diarization GGUF as a diarization model", func() { + d := parakeetDetails("https://huggingface.co/mudler/parakeet-cpp-gguf/resolve/main/nemotron-3-diarization-q8_0.gguf", + `{"name":"nemotron-diarization"}`) + cfg, err := imp.Import(d) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg.ConfigFile).To(ContainSubstring("backend: parakeet-cpp")) + Expect(cfg.ConfigFile).To(ContainSubstring("diarization")) + Expect(cfg.ConfigFile).ToNot(ContainSubstring("transcript")) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].Filename).To(HaveSuffix("nemotron-3-diarization-q8_0.gguf")) + }) + + It("keeps picking ASR weights from a repo that also ships the diarization model", func() { + d := parakeetDetails("huggingface://mudler/parakeet-cpp-gguf", `{"name":"parakeet-110m"}`, + hfapi.ModelFile{Path: "nemotron-3-diarization-f16.gguf", URL: "https://hf/diar-f16", SHA256: "ddd"}, + hfapi.ModelFile{Path: "tdt_ctc-110m-f16.gguf", URL: "https://hf/f16", SHA256: "aaa"}, + hfapi.ModelFile{Path: "nemotron-3-diarization-q8_0.gguf", URL: "https://hf/diar-q8", SHA256: "eee"}, + ) + cfg, err := imp.Import(d) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].URI).To(Equal("https://hf/f16")) + Expect(cfg.ConfigFile).To(ContainSubstring("transcript")) + }) + + It("imports a diarization-only repo as a diarization model", func() { + d := parakeetDetails("huggingface://someone/diar-gguf", `{"name":"diar"}`, + hfapi.ModelFile{Path: "nemotron-3-diarization-f16.gguf", URL: "https://hf/diar-f16", SHA256: "ddd"}, + hfapi.ModelFile{Path: "nemotron-3-diarization-q8_0.gguf", URL: "https://hf/diar-q8", SHA256: "eee"}, + ) + cfg, err := imp.Import(d) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].URI).To(Equal("https://hf/diar-q8"), "default quant ladder picks q8_0 before f16") + Expect(cfg.ConfigFile).To(ContainSubstring("diarization")) + }) + It("picks the default quant (q4_k) from a multi-quant HF repo", func() { d := parakeetDetails("huggingface://mudler/parakeet-cpp-gguf", `{"name":"parakeet-110m"}`, hfapi.ModelFile{Path: "tdt_ctc-110m-f16.gguf", URL: "https://hf/f16", SHA256: "aaa"}, diff --git a/docs/content/features/audio-diarization.md b/docs/content/features/audio-diarization.md index cb8f866dd..3bfde1aef 100644 --- a/docs/content/features/audio-diarization.md +++ b/docs/content/features/audio-diarization.md @@ -160,15 +160,13 @@ curl http://localhost:8080/v1/audio/diarization \ ## Backend setup - parakeet.cpp (Nemotron-3-Diarization) -The [parakeet-cpp backend]({{%relref "features/audio-to-text#using-the-parakeet-cpp-backend" %}}) runs [nvidia/Nemotron-3-Diarization](https://huggingface.co/nvidia/Nemotron-3-Diarization). Convert the checkpoint to GGUF with parakeet.cpp's converter (Q8_0 is 109 MB and gives the same segments as F32): +The [parakeet-cpp backend]({{%relref "features/audio-to-text#using-the-parakeet-cpp-backend" %}}) runs [nvidia/Nemotron-3-Diarization](https://huggingface.co/nvidia/Nemotron-3-Diarization). GGUF weights are published in [`mudler/parakeet-cpp-gguf`](https://huggingface.co/mudler/parakeet-cpp-gguf): F16 (191 MB) and Q8_0 (104 MB) both give the same segments as NeMo. Import one directly: ```bash -python scripts/convert_parakeet_to_gguf.py \ - --model nvidia/Nemotron-3-Diarization --dtype q8_0 \ - --output nemotron-3-diarization-q8_0.gguf +local-ai models import https://huggingface.co/mudler/parakeet-cpp-gguf/resolve/main/nemotron-3-diarization-q8_0.gguf ``` -A diarization-only model: +Or configure it by hand, as a diarization-only model: ```yaml name: nemotron-diarization diff --git a/docs/content/features/audio-to-text.md b/docs/content/features/audio-to-text.md index 5b3937344..f927cc121 100644 --- a/docs/content/features/audio-to-text.md +++ b/docs/content/features/audio-to-text.md @@ -207,7 +207,7 @@ options: ### Speaker labels -Attach a speaker diarization model with the `diar_model` option and each transcript segment carries a `speaker` (`"0"`, `"1"`, ... in order of first appearance). Segments split wherever the speaker changes, on top of the punctuation split. The model is NVIDIA Nemotron-3-Diarization, converted with parakeet.cpp's converter; see [Speaker Diarization]({{< relref "audio-diarization.md" >}}) for the conversion and for the `/v1/audio/diarization` endpoint the same model serves. +Attach a speaker diarization model with the `diar_model` option and each transcript segment carries a `speaker` (`"0"`, `"1"`, ... in order of first appearance). Segments split wherever the speaker changes, on top of the punctuation split. The model is NVIDIA Nemotron-3-Diarization, published as `nemotron-3-diarization-f16.gguf` and `nemotron-3-diarization-q8_0.gguf` in [`mudler/parakeet-cpp-gguf`](https://huggingface.co/mudler/parakeet-cpp-gguf); see [Speaker Diarization]({{< relref "audio-diarization.md" >}}) for the conversion and for the `/v1/audio/diarization` endpoint the same model serves. ```yaml name: parakeet-speakers