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
// transcribe-model.h - internal base class for the public opaque
// transcribe_model handle.
//
// This header is INTERNAL. The public C ABI in include/transcribe.h only
// forward-declares `struct transcribe_model;`; the real definition lives
// here so per-family code (src/arch/<family>/...) can derive from it.
//
// Layout: hybrid of "minimal base" and "full polymorphism" — the base
// owns the universal data and per-call dispatch goes through the Arch
// trait callbacks. RESUME.md "Decisions still load-bearing" has the
// rationale and the rejected alternatives.
//
// - The base owns everything that is genuinely invariant across all
// families: the dispatch token (`arch`), the variant string, the
// bound runtime backend string, and the public capabilities struct.
// - The base has a virtual destructor so the central
// transcribe_model_free() can `delete model;` polymorphically without
// a per-family free callback.
// - Per-family subclasses (ParakeetModel, etc.) own everything else:
// the gguf_context, weights, decoder state. RAII frees them in the
// subclass destructor.
//
// The language pointer chain (the only mildly tricky part of
// transcribe_capabilities) is centralized in set_languages() so per-family
// load() code never has to get it right.
#pragma once
#include "transcribe.h"
#include <cstdint>
#include <string>
#include <vector>
namespace transcribe {
struct Arch;
class Tokenizer;
} // namespace transcribe
// The public C ABI forward-declares this as `struct transcribe_model;`,
// so the real definition stays in the global namespace and uses the
// `struct` keyword for ABI compatibility with C callers.
struct transcribe_model {
// Dispatch token. Set by per-family load() to the family's Arch
// instance. The central dispatcher reads this for arch_string() and
// for per-call dispatch (init_context, run).
const transcribe::Arch * arch = nullptr;
// Identification, both surfaced via the public string accessors.
// variant is whatever the family decided (loader leaves it empty if
// stt.variant was absent; the family supplies a default).
std::string variant;
// Runtime backend currently bound to this model. Empty string means
// "no backend bound" — both pre-binding and the model == NULL case
// collapse to that one stable rule. See the public header for the
// full semantics. In 2B no real backend is wired, so loaders leave
// this empty.
std::string backend;
// Public capabilities. Per-family load() fills this in directly,
// calling set_languages() for the languages chain. Immutable after
// a successful load. Zero-initialized here; the capabilities
// copy-out accessor overwrites struct_size with the caller's view
// before returning, so the value of caps.struct_size on the model
// is irrelevant to external observers.
transcribe_capabilities caps{};
// Backing store for the transcribe_model_supports() probe. One bit
// per transcribe_feature value (0..63). Per-family load() opts in
// by calling transcribe::set_feature(m, FEATURE, true) inside
// apply_family_invariants. Zero means "not supported"; the probe
// returns false for any feature whose bit isn't set, including
// unknown enum values out of range.
uint64_t feature_bits = 0;
// Basis for the session-level limits query (transcribe_session_get_limits).
// A hard-context-cap family fills this at load() — the same place it
// computes caps.max_audio_ms — so the generic query in transcribe.cpp can
// recompute the effective limits for any session n_ctx without a
// per-family hook. Left zero by unbounded / soft-window families (which
// have no decoder context cap): zero model_max_ctx => the query reports
// effective_n_ctx 0 (unbounded), effective_max_audio_ms = caps.max_audio_ms
// (the soft window, independent of n_ctx), and max_kv_bytes 0.
// See docs/input-limits.md.
struct LimitsBasis {
// True for families whose decoder has a context window the session
// n_ctx cap applies to (qwen3_asr, granite, voxtral, cohere, canary,
// ...). When false the family has no decoder context cap and the
// fields below stay zero.
bool has_context_cap = false;
// When true, effective_max_audio_ms comes straight from
// caps.max_audio_ms rather than being derived from effective_n_ctx.
// Set by families whose AUDIO bound is the encoder positional table
// (cohere, canary), where the decoder context bounds the transcript
// length, not how much audio fits — so the audio limit must not shrink
// when the caller lowers n_ctx. effective_n_ctx / max_kv_bytes still
// come from the decoder (n_ctx-sensitive). Left false by families
// whose audio tokens consume the decoder context (qwen3_asr, granite,
// voxtral, ...), where the audio bound legitimately scales with n_ctx.
bool audio_from_caps = false;
// The model's trained decoder context window, in tokens.
int32_t model_max_ctx = 0;
// Representative non-audio prompt token overhead (chat affixes etc.).
int32_t prompt_overhead = 0;
// Generation budget reserved when sizing the input bound.
int32_t gen_reserve = 0;
// Milliseconds of 16 kHz audio per audio token (inverse encoder rate),
// used to turn an audio-token budget into effective_max_audio_ms.
double ms_per_audio_token = 0.0;
// KV elements per context token (n_kv_heads * head_dim * n_layers * 2,
// for K and V). The query multiplies by the session kv_type byte size
// and effective_n_ctx to estimate max_kv_bytes.
int64_t kv_elems_per_ctx_token = 0;
} limits{};
// Wall-clock load time in microseconds, captured by per-family
// load() (start at function entry, stop just before *out_model
// is set). Surfaced via transcribe_get_timings on any context
// derived from this model — load time is a model-scoped fact.
int64_t t_load_us = 0;
// Default-constructs every member via its in-class initializer
// (caps is zero-filled by `caps{}`). Explicitly defaulted because
// the deleted copy/move declarations below otherwise suppress the
// implicit default constructor.
transcribe_model() = default;
virtual ~transcribe_model();
transcribe_model(const transcribe_model &) = delete;
transcribe_model & operator=(const transcribe_model &) = delete;
transcribe_model(transcribe_model &&) = delete;
transcribe_model & operator=(transcribe_model &&) = delete;
// Optional accessor for an internal Tokenizer instance. The default
// returns nullptr; per-family models that load a tokenizer override
// it. Used by internal code (and tests) to inspect the vocabulary
// without dragging the per-family layout into the central dispatch.
virtual const transcribe::Tokenizer * tokenizer() const { return nullptr; }
// Replace the languages list. Stores the strings inside the model
// (so their c_str() lifetime is bound to the model lifetime), then
// rebuilds the capabilities pointer chain that the public ABI
// exposes via transcribe_capabilities::languages. Per-family load()
// calls this once after deciding the language list.
void set_languages(std::vector<std::string> langs);
private:
// Backing storage for the languages chain. Kept private so the only
// way to mutate it is through set_languages(), which guarantees the
// capability struct's pointer + count stay in sync.
std::vector<std::string> language_storage_;
std::vector<const char *> language_ptrs_;
};
namespace transcribe {
// Internal feature-bit helpers. Per-family load() / capability KV
// reader calls set_feature; the central probe (transcribe_model_supports)
// reads via has_feature. Both treat out-of-range values as no-ops / false
// so the bitset stays bounded and unused bits remain zero.
//
// The feature parameter is int, NOT transcribe_feature, on purpose: the
// public entry point receives whatever int a C caller passed, and loading
// an out-of-range value through the enum type is UB in C++. Enum constants
// convert implicitly, so internal callers are unaffected.
inline void set_feature(transcribe_model * m, int f, bool on) {
if (m == nullptr) return;
const unsigned bit = static_cast<unsigned>(f);
if (bit >= 64) return;
const uint64_t mask = (uint64_t) 1 << bit;
if (on) {
m->feature_bits |= mask;
} else {
m->feature_bits &= ~mask;
}
}
inline bool has_feature(const transcribe_model * m, int f) {
if (m == nullptr) return false;
const unsigned bit = static_cast<unsigned>(f);
if (bit >= 64) return false;
return (m->feature_bits & ((uint64_t) 1 << bit)) != 0;
}
} // namespace transcribe