Skip to content

Commit 03c84cf

Browse files
localai-botmudler
andauthored
feat(parakeet-cpp): nemotron-3.5-asr multilingual streaming model + request language support (#10199)
* feat(parakeet-cpp): honor request language (multilingual nemotron) on batched + streaming paths Reads opts.GetLanguage() and threads it through to the new parakeet_capi_transcribe_pcm_batch_json_lang and parakeet_capi_stream_begin_lang C-API entry points, both probed with Dlsym so the backend still loads against an older libparakeet.so (falling back to the non-lang paths, i.e. model default). parakeet.cpp's batched C-API takes a single target_lang for the whole batch, so the dispatcher only coalesces same-language requests: a request whose language differs from the batch leader is held as a single carry-over and becomes the leader of the next batch, never dropped and never left waiting (including on shutdown). A new batcher test asserts no dispatched batch is ever mixed-language and that every submitted request still receives a reply. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gallery): add parakeet-cpp-nemotron-3.5-asr-streaming-0.6b; bump parakeet.cpp pin Adds the multilingual prompt-conditioned streaming model to the gallery (q8_0 default, OpenMDW-1.1) and bumps the parakeet-cpp backend pin to the parakeet.cpp commit that ships nemotron support plus batched causal subsampling and the batched target_lang C-API. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 9bc69c9 commit 03c84cf

6 files changed

Lines changed: 170 additions & 13 deletions

File tree

backend/go/parakeet-cpp/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# parakeet-cpp backend Makefile.
22
#
3-
# Upstream pin lives below as PARAKEET_VERSION?=843600590f96a31467a5199f827c253f34c110f7
3+
# Upstream pin lives below as PARAKEET_VERSION?=50dfc24b4faa4ee23a1f59401f1d0c87fc4042b0
44
# (.github/bump_deps.sh) can find and update it - matches the
55
# whisper.cpp / ds4 / vibevoice-cpp convention.
66
#
@@ -15,7 +15,7 @@
1515
# That's what the L0 smoke test uses. The default target below does the
1616
# proper clone-at-pin + cmake build so CI doesn't need a side-checkout.
1717

18-
PARAKEET_VERSION?=843600590f96a31467a5199f827c253f34c110f7
18+
PARAKEET_VERSION?=50dfc24b4faa4ee23a1f59401f1d0c87fc4042b0
1919
PARAKEET_REPO?=https://github.com/mudler/parakeet.cpp
2020

2121
GOCMD?=go

backend/go/parakeet-cpp/batcher.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import "time"
77
type batchRequest struct {
88
pcm []float32
99
decoder int32
10-
tag string
11-
reply chan batchReply
10+
// language is the per-request target locale ("" means the model default).
11+
// parakeet.cpp's batched C-API takes ONE target_lang for the whole batch,
12+
// so the dispatcher only coalesces requests that share a language.
13+
language string
14+
tag string
15+
reply chan batchReply
1216
}
1317

1418
// batchReply carries one per-item JSON object string (an element of the C-API's
@@ -43,13 +47,25 @@ func newBatcher(maxSize int, maxWait time.Duration, runBatch func([]*batchReques
4347
// run is the dispatcher loop: accumulate submitted requests until either maxSize
4448
// is reached or maxWait elapses since the first queued request, then dispatch.
4549
// Exits when stop is closed (draining any partially-filled batch first).
50+
//
51+
// A batch carries ONE language (parakeet.cpp's batched C-API takes a single
52+
// target_lang), so a request whose language differs from the batch leader is
53+
// not coalesced: it is held in carry and becomes the leader of the next batch.
54+
// carry is therefore never dropped and its caller never deadlocks: every batch
55+
// (including a lone carry on stop) is dispatched, and runBatch replies to all.
4656
func (b *batcher) run(stop <-chan struct{}) {
57+
var carry *batchRequest
4758
for {
4859
var first *batchRequest
49-
select {
50-
case first = <-b.submit:
51-
case <-stop:
52-
return
60+
if carry != nil {
61+
// A mismatched request from the previous fill leads this batch.
62+
first, carry = carry, nil
63+
} else {
64+
select {
65+
case first = <-b.submit:
66+
case <-stop:
67+
return
68+
}
5369
}
5470
batch := []*batchRequest{first}
5571

@@ -64,12 +80,22 @@ func (b *batcher) run(stop <-chan struct{}) {
6480
for len(batch) < b.maxSize {
6581
select {
6682
case r := <-b.submit:
83+
if r.language != first.language {
84+
// Different language: carry it to the next batch so this
85+
// batch stays single-language, then dispatch what we have.
86+
carry = r
87+
break fill
88+
}
6789
batch = append(batch, r)
6890
case <-timer.C:
6991
break fill
7092
case <-stop:
7193
timer.Stop()
7294
b.runBatch(batch)
95+
// Don't strand a carried request's caller on shutdown.
96+
if carry != nil {
97+
b.runBatch([]*batchRequest{carry})
98+
}
7399
return
74100
}
75101
}

backend/go/parakeet-cpp/batcher_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,60 @@ var _ = Describe("batcher", func() {
105105
go func() { <-rep }()
106106
Eventually(dispatched, "2s").Should(Receive(Equal(1)))
107107
})
108+
109+
It("never coalesces requests with different languages into one batch", func() {
110+
// parakeet.cpp's batched C-API takes ONE target_lang per batch, so the
111+
// dispatcher must keep every dispatched batch single-language. Submit a
112+
// mix of languages and assert (a) no batch ever carries more than one
113+
// distinct language and (b) every submitted request still gets a reply
114+
// (the mismatched carry-over is never dropped).
115+
var mu sync.Mutex
116+
var langsPerBatch [][]string
117+
run := func(reqs []*batchRequest) {
118+
seen := map[string]struct{}{}
119+
var distinct []string
120+
for _, r := range reqs {
121+
if _, ok := seen[r.language]; !ok {
122+
seen[r.language] = struct{}{}
123+
distinct = append(distinct, r.language)
124+
}
125+
}
126+
mu.Lock()
127+
langsPerBatch = append(langsPerBatch, distinct)
128+
mu.Unlock()
129+
echoReply(reqs)
130+
}
131+
// Large window + size so the fill loop stays open across submits and the
132+
// language constraint (not the timer) is what splits the batches.
133+
b := newBatcher(16, 200*time.Millisecond, run)
134+
stop := make(chan struct{})
135+
go b.run(stop)
136+
defer close(stop)
137+
138+
langs := []string{"en", "en", "de", "de", "en", "fr", "fr"}
139+
const N = 7
140+
var wg sync.WaitGroup
141+
got := make([]string, N)
142+
for i := 0; i < N; i++ {
143+
wg.Add(1)
144+
go func(i int) {
145+
defer wg.Done()
146+
rep := make(chan batchReply, 1)
147+
b.submit <- &batchRequest{tag: string(rune('a' + i)), language: langs[i], reply: rep}
148+
got[i] = (<-rep).json
149+
}(i)
150+
}
151+
wg.Wait()
152+
153+
mu.Lock()
154+
defer mu.Unlock()
155+
// Invariant: every dispatched batch is single-language.
156+
for _, distinct := range langsPerBatch {
157+
Expect(len(distinct)).To(Equal(1), "a batch coalesced more than one language: %v", distinct)
158+
}
159+
// Liveness: every request got a reply (carry-over never stranded).
160+
for i := 0; i < N; i++ {
161+
Expect(got[i]).To(Equal(string(rune('a' + i))))
162+
}
163+
})
108164
})

backend/go/parakeet-cpp/goparakeetcpp.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,25 @@ var (
4848
// side reads them as const float*/const int*.
4949
CppTranscribePcmBatchJSON func(ctx uintptr, samplesConcat []float32, nSamples []int32, nClips int32, sampleRate int32, decoder int32) uintptr
5050

51+
// CppTranscribePcmBatchJSONLang is the multilingual variant of the batched
52+
// JSON entry point: identical, plus a trailing target_lang. "" (the model
53+
// default, "auto") is passed for non-prompt models, which ignore it; an
54+
// unknown locale on a prompt model returns 0 and sets last_error. Present
55+
// only in newer libparakeet.so; nil falls back to CppTranscribePcmBatchJSON.
56+
CppTranscribePcmBatchJSONLang func(ctx uintptr, samplesConcat []float32, nSamples []int32, nClips int32, sampleRate int32, decoder int32, targetLang string) uintptr
57+
5158
// Cache-aware streaming (RNN-T) entry points. stream_begin returns 0 for
5259
// non-streaming models. feed/finalize return a malloc'd char* (uintptr,
5360
// freed via CppFreeString); feed writes 1 to *eouOut on an <EOU>/<EOB>.
5461
CppStreamBegin func(ctx uintptr) uintptr
5562
CppStreamFeed func(s uintptr, pcm []float32, nSamples int32, eouOut unsafe.Pointer) uintptr
5663
CppStreamFinalize func(s uintptr) uintptr
5764
CppStreamFree func(s uintptr)
65+
66+
// CppStreamBeginLang is the multilingual variant of stream_begin: identical,
67+
// plus a trailing target_lang ("" means the model default). Present only in
68+
// newer libparakeet.so; nil falls back to CppStreamBegin.
69+
CppStreamBeginLang func(ctx uintptr, targetLang string) uintptr
5870
)
5971

6072
// streamChunkSamples is how much 16 kHz mono PCM we hand to stream_feed per
@@ -187,8 +199,19 @@ func (p *ParakeetCpp) runBatch(reqs []*batchRequest) {
187199
if len(reqs) > 0 {
188200
dec = reqs[0].decoder
189201
}
202+
// All requests in a batch share one language (the batcher coalesces only
203+
// same-language requests), so any element's language describes the batch.
204+
lang := ""
205+
if len(reqs) > 0 {
206+
lang = reqs[0].language
207+
}
190208
p.engineMu.Lock()
191-
cstr := CppTranscribePcmBatchJSON(p.ctxPtr, concat, nSamples, int32(len(reqs)), 16000, dec)
209+
var cstr uintptr
210+
if CppTranscribePcmBatchJSONLang != nil {
211+
cstr = CppTranscribePcmBatchJSONLang(p.ctxPtr, concat, nSamples, int32(len(reqs)), 16000, dec, lang)
212+
} else {
213+
cstr = CppTranscribePcmBatchJSON(p.ctxPtr, concat, nSamples, int32(len(reqs)), 16000, dec)
214+
}
192215
p.engineMu.Unlock()
193216
if cstr == 0 {
194217
err := fmt.Errorf("parakeet-cpp: batch transcribe failed: %s", CppLastError(p.ctxPtr))
@@ -226,8 +249,9 @@ func (p *ParakeetCpp) runBatch(reqs []*batchRequest) {
226249
// OpenAI API, whose default is segment-level); token ids always populate
227250
// Segment.Tokens.
228251
//
229-
// translate/diarize/prompt/temperature/language/threads are not applicable to
230-
// parakeet and are ignored; streaming is handled by AudioTranscriptionStream
252+
// translate/diarize/prompt/temperature/threads are not applicable to parakeet
253+
// and are ignored; language is honored on the batched + streaming paths (see
254+
// opts.GetLanguage() below); streaming is handled by AudioTranscriptionStream
231255
// (L2).
232256
func (p *ParakeetCpp) AudioTranscription(ctx context.Context, opts *pb.TranscriptRequest) (pb.TranscriptResult, error) {
233257
if p.ctxPtr == 0 {
@@ -271,7 +295,7 @@ func (p *ParakeetCpp) AudioTranscription(ctx context.Context, opts *pb.Transcrip
271295
}
272296
rep := make(chan batchReply, 1)
273297
select {
274-
case p.bat.submit <- &batchRequest{pcm: pcm, decoder: 0, reply: rep}:
298+
case p.bat.submit <- &batchRequest{pcm: pcm, decoder: 0, language: opts.GetLanguage(), reply: rep}:
275299
case <-ctx.Done():
276300
return pb.TranscriptResult{}, status.Error(codes.Canceled, "transcription cancelled")
277301
}
@@ -361,7 +385,12 @@ func (p *ParakeetCpp) AudioTranscriptionStream(ctx context.Context, opts *pb.Tra
361385
return status.Error(codes.Canceled, "transcription cancelled")
362386
}
363387

364-
stream := CppStreamBegin(p.ctxPtr)
388+
var stream uintptr
389+
if CppStreamBeginLang != nil {
390+
stream = CppStreamBeginLang(p.ctxPtr, opts.GetLanguage())
391+
} else {
392+
stream = CppStreamBegin(p.ctxPtr)
393+
}
365394
if stream == 0 {
366395
// Not a cache-aware streaming model: run a normal offline
367396
// transcription and emit it as one delta + a closing final result.

backend/go/parakeet-cpp/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ func main() {
6565
purego.RegisterLibFunc(&CppTranscribePcmBatchJSON, lib, "parakeet_capi_transcribe_pcm_batch_json")
6666
}
6767

68+
// Per-request language variants (multilingual nemotron). Same probe pattern:
69+
// present only in libparakeet.so built with multilingual support, so the
70+
// backend still loads against an older library and falls back to the
71+
// non-lang batched + streaming entry points (model default / "auto").
72+
if sym, err := purego.Dlsym(lib, "parakeet_capi_transcribe_pcm_batch_json_lang"); err == nil && sym != 0 {
73+
purego.RegisterLibFunc(&CppTranscribePcmBatchJSONLang, lib, "parakeet_capi_transcribe_pcm_batch_json_lang")
74+
}
75+
if sym, err := purego.Dlsym(lib, "parakeet_capi_stream_begin_lang"); err == nil && sym != 0 {
76+
purego.RegisterLibFunc(&CppStreamBeginLang, lib, "parakeet_capi_stream_begin_lang")
77+
}
78+
6879
fmt.Fprintf(os.Stderr, "[parakeet-cpp] ABI=%d\n", CppAbiVersion())
6980

7081
flag.Parse()

gallery/index.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31940,6 +31940,41 @@
3194031940
- filename: parakeet-cpp/tdt_ctc-1.1b-f16.gguf
3194131941
uri: huggingface://mudler/parakeet-cpp-gguf/tdt_ctc-1.1b-f16.gguf
3194231942
sha256: cd53f64eefac2623a12f2f118ef50b56622dc3012f42c815c6adf0d08292f387
31943+
- name: parakeet-cpp-nemotron-3.5-asr-streaming-0.6b
31944+
url: github:mudler/LocalAI/gallery/virtual.yaml@master
31945+
urls:
31946+
- https://huggingface.co/mudler/parakeet-cpp-gguf
31947+
- https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b
31948+
- https://github.com/mudler/parakeet.cpp
31949+
description: |
31950+
Multilingual (40+ locales), prompt-conditioned, cache-aware streaming FastConformer RNN-T, 0.6B.
31951+
Q8_0 GGUF for the parakeet-cpp backend (C++/ggml port of NVIDIA NeMo). Byte-identical to NeMo at
31952+
WER 0 offline and streaming, about 2.5x faster than NeMo on CPU with no GPU. Select a language with
31953+
the request "language" field (for example en, de, es, ja-JP), or leave it empty for automatic
31954+
detection. License OpenMDW-1.1.
31955+
license: other
31956+
tags:
31957+
- parakeet
31958+
- parakeet-cpp
31959+
- nemotron
31960+
- asr
31961+
- speech-recognition
31962+
- stt
31963+
- multilingual
31964+
- streaming
31965+
- gguf
31966+
- ggml
31967+
overrides:
31968+
backend: parakeet-cpp
31969+
known_usecases:
31970+
- transcript
31971+
name: parakeet-cpp-nemotron-3.5-asr-streaming-0.6b
31972+
parameters:
31973+
model: parakeet-cpp/nemotron-3.5-asr-streaming-0.6b-q8_0.gguf
31974+
files:
31975+
- filename: parakeet-cpp/nemotron-3.5-asr-streaming-0.6b-q8_0.gguf
31976+
uri: huggingface://mudler/parakeet-cpp-gguf/nemotron-3.5-asr-streaming-0.6b-q8_0.gguf
31977+
sha256: ba2f13eccd4a5245be728f77e6149bd6a4fdcdd133ff2e08ac6005bcef7a99f1
3194331978
- name: parakeet-crispasr
3194431979
url: github:mudler/LocalAI/gallery/virtual.yaml@master
3194531980
urls:

0 commit comments

Comments
 (0)