Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions include/parakeet_capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ typedef struct parakeet_ctx parakeet_ctx;
// Added parakeet_capi_stream_drain_events (typed per-event records with
// is_eob + timestamps, freed with parakeet_capi_free_events) and an
// "events" array in the stream_feed_json / stream_finalize_json documents.
//
// v6: added parakeet_capi_transcribe_pcm_logits, exposing the CTC head's
// log-prob matrix (row-major [T, vocab+1], already log-softmaxed) instead
// of decoded text — for external LM/decoder stacks (e.g. pyctcdecode +
// KenLM) that need the raw distribution rather than this library's own
// greedy/beam decode. Freed with the new parakeet_capi_free_logits. The
// original entry points are unchanged.
int parakeet_capi_abi_version(void);

// Load a GGUF model. Returns an owning context, or NULL on failure.
Expand Down Expand Up @@ -180,6 +187,35 @@ char* parakeet_capi_transcribe_pcm_nbest_json(
parakeet_ctx* ctx, const float* samples, int n_samples, int sample_rate,
int beam_size, int nbest, int score_norm, const char* target_lang);

// Run mel + encoder + CTC head on in-memory mono float PCM and return the
// log-prob matrix instead of decoded text, for callers that run their own
// external decoder (e.g. pyctcdecode + a KenLM n-gram LM + hotwords) on top of
// this library's CTC output rather than using parakeet.cpp's own greedy/beam
// decode. If `sample_rate != 16000` the audio is linearly resampled to 16 kHz
// first. Always runs the CTC head regardless of the model's preferred
// decoder — `decoder` is not a parameter here, unlike parakeet_capi_transcribe_pcm.
//
// On success returns 0, mallocs `*out_logits` to `(*out_T) * (*out_vocab_plus_1)`
// floats — row-major [T, vocab+1], i.e. out_logits[t*(*out_vocab_plus_1) + v],
// already log-softmaxed over the vocab axis — and sets `*out_T` /
// `*out_vocab_plus_1`. Free `*out_logits` with parakeet_capi_free_logits.
//
// On error returns nonzero. A NULL `ctx` or any NULL out-param pointer
// returns nonzero without writing through any pointer (nothing to zero
// safely). Otherwise (ctx and all three out-params valid, but e.g. no model,
// invalid samples buffer, the model has no CTC head, or OOM) sets the
// context's last error (see parakeet_capi_last_error) and leaves `*out_logits`
// NULL and `*out_T`/`*out_vocab_plus_1` 0 — the caller owns nothing and has
// nothing to free.
int parakeet_capi_transcribe_pcm_logits(parakeet_ctx* ctx, const float* samples,
int n_samples, int sample_rate,
float** out_logits, int* out_T,
int* out_vocab_plus_1);

// Free a logits buffer previously returned by
// parakeet_capi_transcribe_pcm_logits. Safe on NULL.
void parakeet_capi_free_logits(float* logits);

// ---------------------------------------------------------------------------
// Streaming API (cache-aware streaming RNN-T, e.g. the EOU model
// nvidia/parakeet_realtime_eou_120m-v1). The stream session buffers incoming
Expand Down
66 changes: 66 additions & 0 deletions src/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,57 @@ std::string Model::transcribe_16k(const std::vector<float>& pcm16k,
encoded.d_model, encoded.frames, use_tdt);
}

void Model::transcribe_16k_ctc_logits(const std::vector<float>& pcm16k,
std::vector<float>& logits, int& T,
int& vocab_plus_1,
const std::string& target_lang) const {
const ParakeetConfig& cfg = loader_.config();
const int prompt_index = resolve_prompt_index(target_lang);

// 1. Log-mel front end -> feats [n_mels, T]. Mirrors transcribe_16k exactly.
std::vector<float> feats;
int n_mels = 0, Tmel = 0;
if (std::string(pk::global_backend().device_name()) != "cpu") {
GpuMel gmel(loader_);
gmel.compute(pcm16k, feats, n_mels, Tmel);
} else {
MelFrontend mel(loader_);
mel.compute(pcm16k, feats, n_mels, Tmel);
}

// 2. FastConformer encoder -> enc_out [d_model, Tout] (channels-first).
// Long audio: tile the subsampling stage exactly as transcribe_16k does.
Encoder encoder(loader_);
std::vector<float> enc_out;
int d_model = 0, Tout = 0;
const int sub_tile = subsampling_tile_for(cfg, loader_, Tmel);
if (sub_tile > 0) {
MelBatch mb1;
mb1.B = 1; mb1.n_mels = n_mels; mb1.T_max = Tmel; mb1.valid_T = { Tmel };
mb1.data = feats;
std::vector<std::vector<float>> eo; std::vector<int> vT;
int dm1 = 0, To1 = 0;
encoder.forward_batch_tiled(mb1, eo, dm1, To1, vT, sub_tile);
enc_out = std::move(eo[0]);
d_model = dm1;
Tout = vT[0];
} else {
encoder.forward(feats, n_mels, Tmel, enc_out, d_model, Tout);
}

// 2b. Prompt conditioning (multilingual nemotron): project the encoder
// output with the selected language one-hot before decoding. No-op
// for other models (prompt.present == false).
maybe_apply_prompt(loader_, enc_out, d_model, Tout, prompt_index);

// 3. CTC head only — always, regardless of the model's preferred decoder.
// Throws std::runtime_error (from ctc_head_tensor, via CTCDecoder::forward)
// if the model has no CTC head, e.g. a TDT/RNNT-only streaming model.
CTCDecoder ctc(loader_);
ctc.forward(enc_out, d_model, Tout, logits, vocab_plus_1);
T = Tout;
}

// Max mel frames per encoder pass before the first subsampling conv output
// (n_mels/2 * T/2 * conv_channels) approaches INT_MAX. ggml's CUDA unary (relu)
// kernel indexes elements with int32, so a tensor > 2^31 elements crashes
Expand Down Expand Up @@ -555,6 +606,21 @@ std::string Model::transcribe_pcm(const std::vector<float>& pcm, int sample_rate
return transcribe_16k(pcm16k, decoder, target_lang);
}

void Model::transcribe_pcm_ctc_logits(const std::vector<float>& pcm, int sample_rate,
std::vector<float>& logits, int& T,
int& vocab_plus_1,
const std::string& target_lang) const {
if (sample_rate <= 0) {
throw std::runtime_error("parakeet: invalid sample_rate");
}
if (sample_rate == 16000) {
transcribe_16k_ctc_logits(pcm, logits, T, vocab_plus_1, target_lang);
return;
}
std::vector<float> pcm16k = resample_linear(pcm, sample_rate, 16000);
transcribe_16k_ctc_logits(pcm16k, logits, T, vocab_plus_1, target_lang);
}

std::string Model::transcribe_path(const std::string& wav_path,
Decoder decoder, const std::string& target_lang) const {
Audio audio;
Expand Down
21 changes: 21 additions & 0 deletions src/model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ class Model {
Decoder decoder = Decoder::kDefault,
const std::string& target_lang = "") const;

// Run mel + encoder + CTC head only, returning the log-prob matrix
// (row-major [T, vocab+1], already log-softmaxed) instead of decoded text —
// the seam external decoder stacks (e.g. pyctcdecode + KenLM) need. If
// `sample_rate != 16000` the audio is linearly resampled to 16 kHz first.
// `target_lang` as in transcribe_pcm (ignored by non-prompt models). Always
// runs the CTC head regardless of the model's preferred decoder; throws
// std::runtime_error if the model has no CTC head (e.g. a TDT/RNNT-only
// streaming model).
void transcribe_pcm_ctc_logits(const std::vector<float>& pcm, int sample_rate,
std::vector<float>& logits, int& T,
int& vocab_plus_1,
const std::string& target_lang = "") const;

// Transcribe raw mono float PCM, returning the flat text plus per-word and
// per-token timestamps + confidence (matching NeMo timestamps=True +
// 'max_prob' confidence). If `sample_rate != 16000` the audio is linearly
Expand Down Expand Up @@ -141,6 +154,14 @@ class Model {
const std::vector<float>& pcm16k, int beam_size, int nbest,
bool score_norm, const std::string& target_lang) const;

// Core orchestration for transcribe_pcm_ctc_logits: 16 kHz mono PCM -> CTC
// log-prob matrix. Mirrors transcribe_16k through the encoder, then runs
// the CTC head directly instead of decode_enc_out.
void transcribe_16k_ctc_logits(const std::vector<float>& pcm16k,
std::vector<float>& logits, int& T,
int& vocab_plus_1,
const std::string& target_lang = "") const;

ModelLoader loader_;
};

Expand Down
47 changes: 46 additions & 1 deletion src/parakeet_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
// Added stream_drain_events / free_events (typed per-event records) and
// the "events" array in the stream_feed_json / stream_finalize_json
// documents.
#define PARAKEET_CAPI_ABI_VERSION 5
// v6: transcribe_pcm_logits, exposing the CTC head's log-prob matrix (row-major
// [T, vocab+1], already log-softmaxed) instead of decoded text, freed with
// the new free_logits. Original entry points unchanged.
#define PARAKEET_CAPI_ABI_VERSION 6

// The opaque context: a loaded model plus a buffer for the last error message.
struct parakeet_ctx {
Expand Down Expand Up @@ -205,6 +208,48 @@ extern "C" char* parakeet_capi_transcribe_pcm(parakeet_ctx* ctx, const float* sa
decoder, nullptr);
}

extern "C" int parakeet_capi_transcribe_pcm_logits(parakeet_ctx* ctx,
const float* samples, int n_samples,
int sample_rate, float** out_logits,
int* out_T, int* out_vocab_plus_1) {
if (!ctx) return 1;
if (!out_logits || !out_T || !out_vocab_plus_1) {
ctx->last_error = "invalid output pointer(s)";
return 1;
}
*out_logits = nullptr;
*out_T = 0;
*out_vocab_plus_1 = 0;
if (!ctx->model) { ctx->last_error = "context has no loaded model"; return 1; }
if (!samples || n_samples < 0) { ctx->last_error = "invalid samples buffer"; return 1; }
try {
std::vector<float> pcm(samples, samples + n_samples);
std::vector<float> logits;
int T = 0, vocab_plus_1 = 0;
ctx->model->transcribe_pcm_ctc_logits(pcm, sample_rate, logits, T, vocab_plus_1);

float* buf = static_cast<float*>(std::malloc(logits.size() * sizeof(float)));
if (!buf) { ctx->last_error = "out of memory"; return 1; }
std::memcpy(buf, logits.data(), logits.size() * sizeof(float));

ctx->last_error.clear();
*out_logits = buf;
*out_T = T;
*out_vocab_plus_1 = vocab_plus_1;
return 0;
} catch (const std::exception& e) {
ctx->last_error = e.what();
return 1;
} catch (...) {
ctx->last_error = "unknown error";
return 1;
}
}

extern "C" void parakeet_capi_free_logits(float* logits) {
std::free(logits);
}

extern "C" int parakeet_capi_transcribe_pcm_batch_lang(parakeet_ctx* ctx,
const float* const* samples,
const int* n_samples, int n_clips,
Expand Down
5 changes: 3 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pk_add_test(test_capi_stream)
pk_add_test(test_capi_stream_json)
pk_add_test(test_capi_timestamps)
pk_add_test(test_capi_batch_json)
pk_add_test(test_capi_ctc_logits)

if(TARGET parakeet-cli)
add_test(NAME cli_version_long COMMAND $<TARGET_FILE:parakeet-cli> --version)
Expand Down Expand Up @@ -125,7 +126,7 @@ set_tests_properties(test_model_loader test_mel test_mel_gpu test_subsampling te
test_transcribe_speech test_transcribe_tiled test_transcribe_tdt test_transcribe_0_6b
test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron
test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json
test_capi_timestamps test_capi_batch_json
test_capi_timestamps test_capi_batch_json test_capi_ctc_logits
PROPERTIES LABELS "model")
# These tests read fixtures/baselines via paths relative to the project root.
set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_batch test_subsampling_batch_causal test_relpos_attention test_relpos_attention_batch test_conformer test_conformer_batch
Expand All @@ -141,7 +142,7 @@ set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_bat
test_transcribe_speech test_transcribe_tiled test_transcribe_tdt test_transcribe_0_6b
test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron
test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json
test_capi_timestamps test_capi_batch_json
test_capi_timestamps test_capi_batch_json test_capi_ctc_logits
PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})

# Python converter check (skips with exit 77 when the venv/model are absent).
Expand Down
Loading
Loading