1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
// transcribe-session.h - internal base class for the public opaque
// transcribe_session handle.
//
// Mirrors transcribe-model.h: a small base owned by the central dispatch,
// derived per-family contexts owning everything else, with a virtual
// destructor so transcribe_session_free() can delete polymorphically.
// Result storage lives on the base because it's family-agnostic (segments
// / words / tokens / full text); per-family run() drivers populate the
// vectors and the public accessors read them directly.
#pragma once
#include "transcribe.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>
struct transcribe_model;
// ggml handle types for the base-owned compute scratch. Forward-declared so
// this internal header does not pull the ggml headers into every includer
// (the public transcribe.h never sees them); transcribe-backend.h uses the
// same pattern for ggml_backend_t.
struct ggml_context;
struct ggml_backend_sched;
typedef struct ggml_backend_sched * ggml_backend_sched_t;
// Read transcribe_session_params::n_ctx with a struct_size guard. n_ctx is
// a trailing field appended after kv_type, so an older caller's smaller
// struct may not include it; in that case (or a NULL params) the default 0
// = "use the model's true max" is returned. Per-family init_context() that
// honors n_ctx should cache the result onto the base session's n_ctx field.
// The negative-value rejection happens once, generically, in
// transcribe_session_init().
inline int32_t transcribe_session_params_n_ctx(const struct transcribe_session_params * params) {
if (params == nullptr) {
return 0;
}
const size_t field_end = offsetof(struct transcribe_session_params, n_ctx) + sizeof(params->n_ctx);
if (params->struct_size < static_cast<uint64_t>(field_end)) {
return 0;
}
return params->n_ctx;
}
struct transcribe_session {
// The model this session was constructed from. Borrowed pointer:
// the caller is required (per the public threading contract) to keep
// the model alive for the lifetime of every derived session.
transcribe_model * model = nullptr;
// True only for sessions created via transcribe_open(), which loads
// and therefore owns its model. Both transcribe_session_free() and
// transcribe_close() (now an alias) read this flag and free the
// owned model after destroying the session. Sessions created via
// transcribe_session_init() leave this false and their model is
// freed independently by the caller via transcribe_model_free().
bool owns_model = false;
// Cached n_threads value the caller passed at init time. 0 means
// "library picks a sensible default" (matches the factory).
int n_threads = 0;
// Cached n_ctx value the caller passed at init time (decoder context
// cap in tokens). 0 means "use the model's true maximum from GGUF".
// A positive value lowers the ceiling to bound KV-cache memory and is
// clamped down to the model maximum by the family that honors it.
// Only hard-context-cap families read this; chunked / unbounded
// families ignore it. See include/transcribe.h
// (transcribe_session_params::n_ctx).
int32_t n_ctx = 0;
// Cached kv_type the caller passed at init time. Hoisted to the base so
// generic code (e.g. transcribe_session_get_limits's KV-byte estimate)
// can read it without a per-family hook. Every family's init_context sets
// this from params; the families resolve AUTO to f16 for the KV cache, so
// for byte accounting only F32 differs (4 bytes/elem vs 2).
transcribe_kv_type kv_type = TRANSCRIBE_KV_TYPE_AUTO;
// Per-call timings, populated by the most recent transcribe_run.
// Surfaced via the public transcribe_get_timings accessor; reset
// by transcribe_reset_timings.
int64_t t_mel_us = 0;
int64_t t_encode_us = 0;
int64_t t_decode_us = 0;
// Result storage (family-agnostic; populated by per-family run()).
// A flat backing array per level (segments / words / tokens) with
// forward/backward indices between levels. result_kind is the
// timestamp granularity the family populated; has_result is false
// until a run populates, in which case accessors return safe
// sentinels ("", 0, NAN).
struct TokenEntry {
int id = 0;
std::string text; // decoded fragment (▁ → space)
float p = 0.0f;
int64_t t0_ms = 0;
int64_t t1_ms = 0;
int seg_index = 0;
int word_index = -1;
};
struct WordEntry {
std::string text;
int64_t t0_ms = 0;
int64_t t1_ms = 0;
int seg_index = 0;
int first_token = 0;
int n_tokens = 0;
};
struct SegmentEntry {
std::string text;
int64_t t0_ms = 0;
int64_t t1_ms = 0;
int first_word = 0;
int n_words = 0;
int first_token = 0;
int n_tokens = 0;
int32_t speaker_id = 0; // 1-based; 0 = no attribution
};
// "Who spoke when" rows (diarization). Populated only when a run
// resolves diarize ON for a supporting family; may overlap in time.
// t0_ms == t1_ms == 0 means the family attributes text but has no
// timing information for the turn.
struct SpeakerSegmentEntry {
int64_t t0_ms = 0;
int64_t t1_ms = 0;
int32_t speaker_id = 0; // 1-based
float p = NAN;
};
std::vector<TokenEntry> tokens;
std::vector<WordEntry> words;
std::vector<SegmentEntry> segments;
std::vector<SpeakerSegmentEntry> speaker_segments;
std::string full_text;
// The model's decoded output before family post-processing (marker
// parsing, tag filtering, envelope stripping, whitespace trims).
// Equals full_text modulo whitespace for families that do none.
std::string raw_text;
// Offline batch results (transcribe_run_batch). The scratch fields
// above are the single "current result" slot every run() writes into
// and the single-shot accessors read; batch_results is a separate
// vector so the serial-fallback path can snapshot each utterance
// without per-family changes.
//
// Source-of-truth rule for the public accessors:
// - batch_results non-empty -> the batch accessors index it, and
// the scratch slot mirrors batch_results[0] so the single
// accessors stay coherent (they show utterance 0).
// - batch_results empty -> single-shot mode; the batch
// accessors synthesize index 0 from the scratch slot.
// Cleared at the top of every transcribe_run / transcribe_run_batch,
// NOT by clear_result (which only wipes the scratch slot + streaming
// snapshot, so a run() inside the fallback loop does not erase
// already-accumulated entries).
struct ResultSet {
std::vector<TokenEntry> tokens;
std::vector<WordEntry> words;
std::vector<SegmentEntry> segments;
std::vector<SpeakerSegmentEntry> speaker_segments;
std::string full_text;
std::string raw_text;
std::string detected_language;
transcribe_timestamp_kind result_kind = TRANSCRIBE_TIMESTAMPS_NONE;
bool has_result = false;
transcribe_status status = TRANSCRIBE_OK;
// Per-utterance timings (us). For a batched run the encoder is one
// shared dispatch, so a family amortizes its total encode time across
// the batch (sum over utterances == the real batch encode time);
// decode is genuinely per-utterance. Lets transcribe_batch_get_timings
// expose where time goes (encoder vs host decode) per utterance.
int64_t t_mel_us = 0;
int64_t t_encode_us = 0;
int64_t t_decode_us = 0;
};
std::vector<ResultSet> batch_results;
// Snapshot the current scratch result slot (the fields above that a
// per-family run() populates) into a standalone ResultSet. Used by the
// batch dispatcher's serial fallback and by family run_batch() hooks to
// capture each utterance's result. `st` records the per-utterance
// terminal status.
ResultSet capture_result(transcribe_status st = TRANSCRIBE_OK) const {
ResultSet rs;
rs.tokens = tokens;
rs.words = words;
rs.segments = segments;
rs.speaker_segments = speaker_segments;
rs.full_text = full_text;
rs.raw_text = raw_text;
rs.detected_language = detected_language;
rs.result_kind = result_kind;
rs.has_result = has_result;
rs.status = st;
rs.t_mel_us = t_mel_us;
rs.t_encode_us = t_encode_us;
rs.t_decode_us = t_decode_us;
return rs;
}
// ISO short code the model itself predicted on this run, populated
// only when the caller did NOT pass a language hint (auto/null) and
// the family actually ran a detection step. Empty string means
// "unknown" — either no detection ran (English-only model, user
// supplied a hint, family doesn't support LID) or detection produced
// a non-language sentinel (e.g. SenseVoice's <|nospeech|>).
std::string detected_language;
transcribe_timestamp_kind result_kind = TRANSCRIBE_TIMESTAMPS_NONE;
bool has_result = false;
// Abort / cancellation (set via transcribe_set_abort_callback).
// run() drivers call poll_abort() at chunk / decode-step boundaries;
// a callback returning true sets was_aborted and the run returns
// TRANSCRIBE_ERR_ABORTED with partial segments preserved. was_aborted
// is cleared at the top of every transcribe_run, NOT by clear_result
// (the partial result may be deliberately retained).
transcribe_abort_callback abort_cb = nullptr;
void * abort_userdata = nullptr;
bool was_aborted = false;
bool poll_abort() {
if (abort_cb != nullptr && abort_cb(abort_userdata)) {
was_aborted = true;
return true;
}
return false;
}
// Set by a run() driver when decode stops at the model's context /
// position cap before end-of-stream (output truncated; partial result
// retained). Surfaced via transcribe_was_truncated(); cleared at the
// top of every transcribe_run, NOT by clear_result. Distinct from the
// up-front TRANSCRIBE_ERR_INPUT_TOO_LONG rejection (couldn't finish vs
// couldn't start). See docs/input-limits.md.
bool was_truncated = false;
// Streaming state. Lifecycle (stream_state) is separated from the
// result snapshot so clear_result() can wipe per-call data without
// churning the IDLE/ACTIVE/FINISHED/FAILED machine, which the
// dispatcher manages explicitly. The snapshot fields below (revision,
// committed counts, last_status, audio cursors) ARE cleared by
// clear_result. Audio cursors are us-precision; the public
// stream_update struct exposes them as ms.
transcribe_stream_state stream_state = TRANSCRIBE_STREAM_IDLE;
int32_t stream_revision = 0;
int n_committed_segments = 0;
int n_committed_words = 0;
int n_committed_tokens = 0;
transcribe_status stream_last_status = TRANSCRIBE_OK;
int64_t stream_audio_input_us = 0;
int64_t stream_audio_committed_us = 0;
transcribe_stream_commit_policy stream_commit_policy = TRANSCRIBE_STREAM_COMMIT_AUTO;
uint32_t stream_stable_prefix_agreement_n = 0;
// Session-owned copies of the caller's run-params strings, refreshed
// on every transcribe_stream_begin. The dispatcher hands the family
// hooks a params view whose language/target_language point HERE, so a
// family that captures *run_params holds pointers into library-owned
// storage (the public contract lets the caller free its params pointers
// the moment begin returns). Stable for the stream's lifetime; only the
// next begin mutates them.
std::string stream_language_owned;
std::string stream_target_language_owned;
// UI-facing streaming text state. `full_text` above remains the raw
// model hypothesis. `stream_committed_text` is the append-only public
// display/input prefix; `stream_tentative_text` is the current raw
// suffix after `stream_raw_tentative_start_bytes`. The raw history is
// used by the generic STABLE_PREFIX policy.
std::string stream_committed_text;
std::string stream_tentative_text;
uint64_t stream_raw_tentative_start_bytes = 0;
std::deque<std::string> stream_raw_history;
void clear_result();
// Per-run ggml compute scratch, owned by the base so every family
// releases it the same way and none can forget to: the backend
// scheduler (whose graph allocator only ever grows) and the no_alloc
// graph context. Families create both lazily inside their run / stream
// hooks and use them directly; the base frees them in release_scratch
// and in its destructor (scheduler first, then context).
ggml_backend_sched_t sched = nullptr;
ggml_context * compute_ctx = nullptr;
// Release the per-run ggml compute scratch (sched, then compute_ctx;
// see transcribe::release_compute_scratch), then let the family drop
// pointers that lived in them (on_scratch_released).
// The dispatcher calls this after every offline transcribe_run or
// transcribe_run_batch that passes pre-clear validation and reaches its
// commit point, whether family execution succeeds, fails, or throws.
// Without this, a single long utterance would pin the scheduler's
// high-water mark in backend compute memory for the session's lifetime
// (Handy #2000). Families re-create the scheduler lazily on the next run.
// The measured recreation cost is about 1 ms per run on Metal and up to
// about 10 ms on CPU for families that reserve a worst-case decoder
// workspace each run, such as Canary and Cohere.
//
// Host-side vectors that scale with input length, such as mel buffers,
// encoder host copies, and positional banks, deliberately retain their
// capacity. Family KV caches sized by the last batch are also retained;
// this hook releases only the ggml scheduler and compute context.
//
// Streaming entry points do not invoke this hook. A streaming session
// keeps its scheduler until a later offline run or session destruction.
// Parakeet and Voxtral Realtime use per-chunk bounded stream workspaces;
// Moonshine Streaming's decode graph cross-attends over the complete
// committed stream, so its workspace grows with total stream length.
// Must not throw. Non-virtual: a family cannot opt out of the release.
void release_scratch() noexcept;
transcribe_session() = default;
// Frees sched / compute_ctx after the derived destructor has run. The
// scheduler owns only its own allocator buffers and references
// model-owned backends, so freeing it after the family's KV caches and
// stream buffers is order-independent.
virtual ~transcribe_session();
transcribe_session(const transcribe_session &) = delete;
transcribe_session & operator=(const transcribe_session &) = delete;
transcribe_session(transcribe_session &&) = delete;
transcribe_session & operator=(transcribe_session &&) = delete;
protected:
// Family hook, called by release_scratch after sched / compute_ctx are
// freed: null out tensors borrowed from the freed context (encoder_out)
// or reset capacity bookkeeping (whisper compute_ctx_size). Most
// families need nothing. Not called from the base destructor because
// derived members are already destroyed by then. Must not throw.
virtual void on_scratch_released() noexcept {}
};