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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
//! The prediction vocabulary: [`LanguageScore`] (one ranked language) and the
//! min-heap top-k the identify path runs.
//!
//! Ranking follows the crate's existing `RankedScore` contract (ced's, which is
//! soundevents'): `f32::total_cmp` descending on the score, ties broken by
//! **ascending language index**. The score ranked on is the model's raw
//! natural-log probability; because `exp` is strictly monotonic, ranking in log
//! space and reporting in either space give the same order, and no
//! `NUM_LANGUAGES`-element sort is needed.
use ;
use BinaryHeap;
use crate;
/// One window's log-probability row paired with the [`Span`] it was scored
/// over — `windit`'s own value type, so the per-window output composes with the
/// windit post-processing stack (smoothing, segmentation) with no adapter.
///
/// [`Span`]: crate::audio::lid::Span
pub type WindowLogProbabilities = Windowed;
/// A full row of natural-log probabilities — one window's, or a whole clip's
/// after aggregation: always exactly [`NUM_LANGUAGES`] values, indexed by model
/// column, each `<= 0` and never NaN.
///
/// # What the invariant does and does not promise
///
/// Straight off the graph the row is a log-SOFTMAX: `exp` over it sums to 1 —
/// to the graph's own fp16 accuracy, which puts it as much as 7.7e-3 away from
/// 1 on [`ComputeUnits::CpuOnly`] — on EITHER side, the deviation being signed
/// and measured in both directions on every compute unit by
/// `the_graphs_largest_output_reaches_zero_and_never_passes_it`. Aggregation
/// makes that exact: `Vote` divides shares, the other three close with a
/// renormalization, and the result is
/// checked against 1 before it is returned. Where a fold cannot produce a
/// distribution it FAILS — [`Error::ZeroMassAggregate`] on rows whose honest
/// pool assigns every language probability zero — rather than hand back a row
/// that is not one.
///
/// The invariant this TYPE enforces is only the pointwise one (`<= 0`, not
/// NaN), because that is the part a hand-built row can be held to without
/// choosing a floating-point tolerance for "sums to 1". Both doors that ADMIT a
/// row apply it from `is_log_probability`, this module's single definition of
/// it: [`Self::try_from_slice`] to a caller's row, and
/// [`Identifier::log_probabilities`] to the graph's own output, so a model that
/// meets the feature-name/shape/dtype contract and then emits a positive score
/// is refused rather than ranked into a `probability()` above 1. The one thing
/// [`aggregate_windows`] additionally requires of a row it is given is that it
/// have a FINITE maximum ([`Error::UnnormalizableWindow`]) — a bound that is
/// finite and that the whole row sits under — which is exactly the condition
/// under which the row normalizes: a row that is `-∞` in every column rules
/// every language out, which is not evidence about any of them. That door
/// re-establishes the pointwise invariant rather than assuming it, because the
/// crate-internal constructor does not check it: a row reaching the fold from
/// inside this crate holding a `+∞` or a NaN is refused there too. It does NOT
/// require the row to sit at any particular scale — a row whose largest value
/// is `-800` folds exactly as one shifted up to `0` does.
///
/// [`ComputeUnits::CpuOnly`]: crate::ComputeUnits::CpuOnly
/// [`Identifier::log_probabilities`]: crate::audio::lid::Identifier::log_probabilities
/// [`aggregate_windows`]: crate::audio::lid::aggregate_windows
/// [`Error::ZeroMassAggregate`]: crate::audio::lid::Error::ZeroMassAggregate
/// [`Error::UnnormalizableWindow`]: crate::audio::lid::Error::UnnormalizableWindow
///
/// `-∞` is a legal value: it is the exact log of a zero probability, which
/// [`ScorePooling::Vote`] produces for any language no window chose.
/// [`LanguageScore::probability`] maps it to exactly `0.0`.
///
/// [`NUM_LANGUAGES`]: crate::audio::lid::NUM_LANGUAGES
/// [`ScorePooling`]: crate::audio::lid::ScorePooling
/// [`ScorePooling::Vote`]: crate::audio::lid::ScorePooling::Vote
/// The pointwise invariant every [`LogProbabilities`] row holds, as **one
/// definition** that both doors admitting a row read: a natural-log
/// probability is at most zero.
///
/// [`LogProbabilities::try_from_slice`] holds a CALLER's row to it;
/// [`Identifier::log_probabilities`] holds the GRAPH's output to it, through
/// [`is_finite_log_probability`], which adds finiteness and nothing else. The
/// two report different errors — a caller's bad row and a corrupt model output
/// are different diagnoses — but they must not disagree about what a
/// log-probability IS, and calling one function is what makes that structural
/// rather than remembered.
///
/// **It is written as the predicate that ACCEPTS**, `value <= 0.0`, rather
/// than as the `value.is_nan() || value > 0.0` that refuses. Those two are the
/// same test only over an ORDERED domain and `f32` is not one: every ordered
/// comparison against a NaN is false, so the accepting form rejects a NaN with
/// no second clause a later edit could leave behind. `aggregate`'s
/// postcondition is written this way for the same reason.
///
/// **`-∞` is accepted.** It is the exact log of a zero probability, which
/// [`ScorePooling::Vote`] genuinely produces for a language no window chose.
///
/// **Exactly zero is accepted, and that is a measurement rather than a
/// courtesy.** A log-softmax output is non-positive by construction, so
/// refusing `>= 0` at the model door would look free — and it would refuse
/// real audio. These rows are narrowed through fp16, and this graph really does
/// emit `0.0`: 22 of the 50 076 values in `lid_long_clip`'s published sweep, all
/// of them on [`ComputeUnits::CpuOnly`], whose narrowing is the loosest of the
/// four and rounds a top language already near probability 1 the rest of the
/// way up. Nothing in that sweep sits ABOVE zero on
/// any of the four compute units — the other three peak near `-1.4e-3`.
/// `the_graphs_largest_output_reaches_zero_and_never_passes_it` gates both
/// halves, because the predicate rests on both.
///
/// [`Identifier::log_probabilities`]: crate::audio::lid::Identifier::log_probabilities
/// [`ScorePooling::Vote`]: crate::audio::lid::ScorePooling::Vote
/// [`ComputeUnits::CpuOnly`]: crate::ComputeUnits::CpuOnly
pub
/// [`is_log_probability`] **plus finiteness** — the model door's form of the
/// same rule, and the only thing that door adds.
///
/// A caller may legitimately hand in `-∞`; a log-softmax GRAPH emitting one is
/// corruption, as `+∞` and a NaN are. That extra requirement is written here as
/// an addition to the shared predicate rather than as a second copy of it, so
/// the `<= 0` half still exists in exactly one place and the difference between
/// the two doors is a single readable conjunct rather than two rules to
/// compare.
pub
/// One ranked language: the roster row plus the model's natural-log
/// probability for it.
///
/// # Why a struct and not `(usize, f32)`
///
/// A bare tuple would be smaller to write and worse to use, on three counts
/// that all bite silently:
///
/// - **The number's meaning is not guessable.** This graph emits values that
/// are already log-softmaxed — natural log, summing to 1 under `exp`, all
/// `<= 0`. A tuple's `f32` gives a reader no way to tell that from a logit or
/// a probability, and the two plausible wrong readings (thresholding a
/// log-prob at 0.5, or `exp`-ing something that was already a probability)
/// both produce numbers rather than errors. [`Self::log_probability`] and
/// [`Self::probability`] each say what they are, and only one of them needs
/// an `exp`.
/// - **A raw index invites the wrong roster.** `.0` is meaningful only against
/// THIS door's [`languages`](crate::audio::lid::languages) table; carrying
/// the resolved [`Language`] means the code and name travel with the score
/// and cannot be looked up in something else.
/// - **A tuple's shape is frozen.** Adding a field to a struct is additive;
/// widening a tuple is a break for every destructuring caller.
///
/// It is also the shape this crate already uses for a ranked prediction
/// (`audio::ced`'s `EventPrediction`), so the two doors read alike.
/// Ranking key: score under `f32::total_cmp`, ties broken by ascending
/// language index (a smaller index compares GREATER at equal scores, so it
/// surfaces first in descending output) — the crate's existing `RankedScore`
/// contract.
/// Select the top `k` of `scores` (`(index, log_probability)` pairs) without a
/// full sort: a size-`k` min-heap of [`Reverse`]d [`RankedScore`]s, replacing
/// the smallest whenever a larger candidate arrives.
///
/// `k == 0` yields an empty vec; `k` above the roster size saturates. Capacity
/// is clamped to the roster size so a caller's "give me everything" sentinel
/// (`usize::MAX`) cannot overflow the pre-allocation.
///
/// # Errors
/// [`Error::UnknownLanguageIndex`] if a surviving index has no roster row
/// (defensive; unreachable for in-range indices).
pub