@earendil-works/pi-voice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/catalog/recommendations.json +710 -0
- package/index.ts +1 -0
- package/package.json +69 -0
- package/src/async-limiter.ts +77 -0
- package/src/audio-constants.ts +1 -0
- package/src/audio.ts +193 -0
- package/src/catalog.generated.ts +1305 -0
- package/src/catalog.ts +89 -0
- package/src/chinese.ts +52 -0
- package/src/deferred.ts +30 -0
- package/src/dictation-controller.ts +204 -0
- package/src/file-audio.ts +164 -0
- package/src/file-transcription.ts +212 -0
- package/src/index.ts +114 -0
- package/src/install-migration.ts +47 -0
- package/src/keybindings.ts +118 -0
- package/src/languages.ts +22 -0
- package/src/microphone-picker.ts +99 -0
- package/src/model-activation.ts +74 -0
- package/src/model-cells.ts +218 -0
- package/src/model-picker.ts +1026 -0
- package/src/model-ratings-help.md +41 -0
- package/src/model-ratings-help.ts +146 -0
- package/src/model-selection-controller.ts +185 -0
- package/src/models.ts +263 -0
- package/src/onboarding.ts +314 -0
- package/src/pcm-chunker.ts +42 -0
- package/src/pcm.ts +19 -0
- package/src/recommendation-picker.ts +512 -0
- package/src/recommendations.ts +423 -0
- package/src/runtime.ts +501 -0
- package/src/settings-menu.ts +410 -0
- package/src/settings-path.ts +13 -0
- package/src/settings.ts +235 -0
- package/src/shortcut-core.ts +85 -0
- package/src/shortcuts.ts +167 -0
- package/src/startup-shortcut.ts +24 -0
- package/src/transcript-preview.ts +52 -0
- package/src/transcription-service.ts +548 -0
- package/src/transcription.ts +186 -0
- package/src/try-it.ts +327 -0
- package/src/ui-components.ts +432 -0
- package/src/visualizer.ts +269 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import recommendationData from "../catalog/recommendations.json" with { type: "json" };
|
|
2
|
+
import {
|
|
3
|
+
languageIdentity as recommendationLanguage,
|
|
4
|
+
modelMatchesLanguage as modelMatchesRecommendationLanguage,
|
|
5
|
+
displayLanguage,
|
|
6
|
+
rankCatalogModels,
|
|
7
|
+
type CatalogModel,
|
|
8
|
+
} from "./catalog.js";
|
|
9
|
+
|
|
10
|
+
export type MachineTier = "accelerated" | "cpu";
|
|
11
|
+
|
|
12
|
+
export type RecommendationRole = "best" | "fast" | "accurate";
|
|
13
|
+
|
|
14
|
+
export type RecommendationStatus =
|
|
15
|
+
| "eligible"
|
|
16
|
+
| "experimental"
|
|
17
|
+
| "unsupported"
|
|
18
|
+
| "unbenchmarked";
|
|
19
|
+
|
|
20
|
+
export type ModelRecommendation = {
|
|
21
|
+
model: CatalogModel;
|
|
22
|
+
roles: RecommendationRole[];
|
|
23
|
+
status: RecommendationStatus;
|
|
24
|
+
/** Geometric mean of the per-language error, in percent. */
|
|
25
|
+
error?: number;
|
|
26
|
+
/** Seconds after the dictation on the benchmark GPU. */
|
|
27
|
+
waitSeconds?: number;
|
|
28
|
+
/** Whether this model meets the Fast role's wait target on the benchmark CPU. */
|
|
29
|
+
withinFastWaitTarget?: boolean;
|
|
30
|
+
/** The chosen language this model handles worst. */
|
|
31
|
+
worstLanguage?: string;
|
|
32
|
+
/** Its measured word or character error, in percent. */
|
|
33
|
+
worstError?: number;
|
|
34
|
+
/** True when even the worst language is over the usability floor. */
|
|
35
|
+
overFloor?: boolean;
|
|
36
|
+
/** Usable, but close enough to the floor that the pane says so. */
|
|
37
|
+
nearFloor?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type AccuracyCell = {
|
|
41
|
+
error: number;
|
|
42
|
+
ciLower?: number;
|
|
43
|
+
};
|
|
44
|
+
type RecommendationModelData = {
|
|
45
|
+
accuracy: Record<string, AccuracyCell>;
|
|
46
|
+
performance: Partial<Record<MachineTier, number>>;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The picks in plain words. Wait is the seconds after a dictation of
|
|
50
|
+
* `dictationSeconds`, on the benchmark GPU unless said otherwise. When
|
|
51
|
+
* several languages were chosen, automatic switching is preferred, but a
|
|
52
|
+
* usable manual model beats an automatic model over the error floor. Quality
|
|
53
|
+
* is a hard constraint whenever any model stays under
|
|
54
|
+
* `maxLanguageErrorPercent` on every chosen language. The overall pick has
|
|
55
|
+
* the lowest error plus wait penalty, where the first `freeWaitSeconds` cost
|
|
56
|
+
* nothing and each second beyond costs `waitWeight` points; its
|
|
57
|
+
* `overallMaxWaitSeconds` limit relaxes before quality does. The fast pick is
|
|
58
|
+
* judged on the benchmark CPU: the most accurate usable model within
|
|
59
|
+
* `fastCpuMaxWaitSeconds`, else the quickest usable model. The most accurate
|
|
60
|
+
* pick has the lowest error among usable models within
|
|
61
|
+
* `accurateMaxWaitSeconds`, else the most accurate usable model. When no
|
|
62
|
+
* usable model exists, the closest model carries every role. It remains
|
|
63
|
+
* experimental below `experimentalMaxErrorPercent`, or when its lower
|
|
64
|
+
* confidence bound reaches that cutoff; otherwise it is unsupported. A pick
|
|
65
|
+
* that is usable but reaches `noteMinErrorPercent` on some chosen language is
|
|
66
|
+
* still recommended, with the shortfall named rather than left to be
|
|
67
|
+
* discovered: the usable band spans most of an order of magnitude, so silence
|
|
68
|
+
* across all of it would say the same thing about very different models.
|
|
69
|
+
*/
|
|
70
|
+
type Methodology = {
|
|
71
|
+
dictationSeconds: number;
|
|
72
|
+
freeWaitSeconds: number;
|
|
73
|
+
waitWeight: number;
|
|
74
|
+
overallMaxWaitSeconds: number;
|
|
75
|
+
maxLanguageErrorPercent: number;
|
|
76
|
+
noteMinErrorPercent: number;
|
|
77
|
+
experimentalMaxErrorPercent: number;
|
|
78
|
+
fastCpuMaxWaitSeconds: number;
|
|
79
|
+
accurateMaxWaitSeconds: number;
|
|
80
|
+
};
|
|
81
|
+
type RecommendationData = {
|
|
82
|
+
methodology: Methodology;
|
|
83
|
+
models: Record<string, RecommendationModelData>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const DATA: RecommendationData = recommendationData;
|
|
87
|
+
|
|
88
|
+
export const EXPERIMENTAL_MAX_ERROR_PERCENT = DATA.methodology.experimentalMaxErrorPercent;
|
|
89
|
+
|
|
90
|
+
export type AccuracyLetter = "A" | "B" | "C" | "D" | "F";
|
|
91
|
+
|
|
92
|
+
export type AccuracyGrade = {
|
|
93
|
+
letter: AccuracyLetter;
|
|
94
|
+
/** The letter with its modifier, e.g. "A-" or "B+"; D and F carry none. */
|
|
95
|
+
label: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Display bands are independent of recommendation eligibility policy. */
|
|
99
|
+
const ACCURACY_CEILINGS = { A: 5, B: 10, C: 20, D: 30 } as const;
|
|
100
|
+
/** Each sub-grade step multiplies the measured error by this. */
|
|
101
|
+
const SUB_GRADE_STEP = Math.cbrt(2);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A compact benchmark comparison, not a prediction of editing effort or
|
|
105
|
+
* whether the coding assistant will understand a request. A–C modifiers
|
|
106
|
+
* use proportional changes in error. Keep model-ratings-help.md in sync
|
|
107
|
+
* when changing display bands.
|
|
108
|
+
*/
|
|
109
|
+
export function accuracyGrade(errorPercent: number): AccuracyGrade {
|
|
110
|
+
if (errorPercent >= ACCURACY_CEILINGS.D) return { letter: "F", label: "F" };
|
|
111
|
+
if (errorPercent >= ACCURACY_CEILINGS.C) return { letter: "D", label: "D" };
|
|
112
|
+
const letters: [AccuracyLetter, number][] = [
|
|
113
|
+
["A", ACCURACY_CEILINGS.A],
|
|
114
|
+
["B", ACCURACY_CEILINGS.B],
|
|
115
|
+
["C", ACCURACY_CEILINGS.C],
|
|
116
|
+
];
|
|
117
|
+
const [letter, ceiling] = letters.find(([, limit]) => errorPercent < limit)!;
|
|
118
|
+
const modifier = errorPercent < ceiling / SUB_GRADE_STEP ** 2
|
|
119
|
+
? "+"
|
|
120
|
+
: errorPercent < ceiling / SUB_GRADE_STEP
|
|
121
|
+
? ""
|
|
122
|
+
: "-";
|
|
123
|
+
return { letter, label: `${letter}${modifier}` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Processing time for the display's reference recording on the benchmark tier. */
|
|
127
|
+
export function modelWaitSeconds(
|
|
128
|
+
model: CatalogModel,
|
|
129
|
+
tier: MachineTier = "accelerated",
|
|
130
|
+
): number | undefined {
|
|
131
|
+
const xrt = DATA.models[model.id]?.performance[tier];
|
|
132
|
+
return xrt ? BENCHMARK_DICTATION_SECONDS / xrt : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Display-only reference and bands; changing recommendation wait budgets must
|
|
136
|
+
// not silently redefine the meter. See model-ratings-help.md for the explanation.
|
|
137
|
+
export const BENCHMARK_DICTATION_SECONDS = 30;
|
|
138
|
+
const SPEED_CUTOFF_SECONDS = [1.5, 3, 5, 10, 20] as const;
|
|
139
|
+
export const SPEED_METER_STEPS = SPEED_CUTOFF_SECONDS.length;
|
|
140
|
+
export function speedMeterLevel(processingSeconds: number): number {
|
|
141
|
+
return SPEED_CUTOFF_SECONDS.filter((cutoff) => processingSeconds < cutoff).length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The model's grade for one language, or undefined when the benchmark has
|
|
146
|
+
* no measurement for it (whether or not the model claims the language).
|
|
147
|
+
*/
|
|
148
|
+
export function languageAccuracyGrade(
|
|
149
|
+
model: CatalogModel,
|
|
150
|
+
language: string,
|
|
151
|
+
): AccuracyGrade | undefined {
|
|
152
|
+
const cell = DATA.models[model.id]?.accuracy[recommendationLanguage(language)];
|
|
153
|
+
return cell ? accuracyGrade(cell.error) : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The speed the overall pick was chosen for: a dictation of
|
|
158
|
+
* `dictationSeconds` back within `overallMaxWaitSeconds`. A model measured
|
|
159
|
+
* below this on someone's machine misses the budget it was recommended by.
|
|
160
|
+
*/
|
|
161
|
+
export const COMFORTABLE_REAL_TIME_FACTOR =
|
|
162
|
+
DATA.methodology.dictationSeconds / DATA.methodology.overallMaxWaitSeconds;
|
|
163
|
+
|
|
164
|
+
type ScoredModel = {
|
|
165
|
+
model: CatalogModel;
|
|
166
|
+
/** Geometric mean of the per-language error, in percent. */
|
|
167
|
+
error: number;
|
|
168
|
+
/** The chosen language this model handles worst, and its error in percent. */
|
|
169
|
+
worstLanguage: string;
|
|
170
|
+
worstError: number;
|
|
171
|
+
/** Lower confidence bound for that error, when the benchmark provides one. */
|
|
172
|
+
worstCiLower: number | undefined;
|
|
173
|
+
/** Seconds after the dictation on the benchmark GPU. */
|
|
174
|
+
waitSeconds: number;
|
|
175
|
+
/** The same on the benchmark CPU, when measured. */
|
|
176
|
+
cpuWaitSeconds: number | undefined;
|
|
177
|
+
/** Needs the language set by hand for this set of languages. */
|
|
178
|
+
manual: boolean;
|
|
179
|
+
score: number;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function scoreModels(
|
|
183
|
+
models: readonly CatalogModel[],
|
|
184
|
+
languages: readonly string[],
|
|
185
|
+
): ScoredModel[] {
|
|
186
|
+
const wanted = [...new Set(languages.map(recommendationLanguage))];
|
|
187
|
+
const method = DATA.methodology;
|
|
188
|
+
const scored: ScoredModel[] = [];
|
|
189
|
+
|
|
190
|
+
for (const model of models) {
|
|
191
|
+
if (!wanted.every((language) => modelMatchesRecommendationLanguage(model, language))) continue;
|
|
192
|
+
const data = DATA.models[model.id];
|
|
193
|
+
const cells = wanted.map((language) => data?.accuracy[language]);
|
|
194
|
+
const xrt = data?.performance.accelerated;
|
|
195
|
+
if (!data || !xrt || cells.some((cell) => cell === undefined)) continue;
|
|
196
|
+
const accuracyCells = cells as AccuracyCell[];
|
|
197
|
+
const numericErrors = accuracyCells.map((cell) => cell.error);
|
|
198
|
+
const error = Math.exp(
|
|
199
|
+
numericErrors.reduce((sum, value) => sum + Math.log(value), 0) /
|
|
200
|
+
numericErrors.length,
|
|
201
|
+
);
|
|
202
|
+
const waitSeconds = method.dictationSeconds / xrt;
|
|
203
|
+
const cpuXrt = data.performance.cpu;
|
|
204
|
+
const worstError = Math.max(...numericErrors);
|
|
205
|
+
const worstIndex = numericErrors.indexOf(worstError);
|
|
206
|
+
scored.push({
|
|
207
|
+
model,
|
|
208
|
+
error,
|
|
209
|
+
worstLanguage: wanted[worstIndex]!,
|
|
210
|
+
worstError,
|
|
211
|
+
worstCiLower: accuracyCells[worstIndex]!.ciLower,
|
|
212
|
+
waitSeconds,
|
|
213
|
+
cpuWaitSeconds: cpuXrt ? method.dictationSeconds / cpuXrt : undefined,
|
|
214
|
+
manual: wanted.length > 1 && !model.capabilities.languageDetection,
|
|
215
|
+
score: error + method.waitWeight * Math.max(0, waitSeconds - method.freeWaitSeconds),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return scored;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A model's benchmark against a set of chosen languages, for the picker. */
|
|
222
|
+
export type ModelBenchmark = {
|
|
223
|
+
/** Geometric mean of the per-language error, in percent. */
|
|
224
|
+
error: number;
|
|
225
|
+
/** Seconds after the benchmark dictation on the benchmark GPU. */
|
|
226
|
+
waitSeconds: number;
|
|
227
|
+
/** Every chosen language is under the usability floor. */
|
|
228
|
+
usable: boolean;
|
|
229
|
+
/** Needs the language set by hand for this set of languages. */
|
|
230
|
+
manual: boolean;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Benchmarks for every model measured on all the chosen languages, keyed by
|
|
235
|
+
* model id. Models that lack a language or a measurement are absent.
|
|
236
|
+
*/
|
|
237
|
+
export function benchmarkModels(
|
|
238
|
+
models: readonly CatalogModel[],
|
|
239
|
+
languages: readonly string[],
|
|
240
|
+
): Map<string, ModelBenchmark> {
|
|
241
|
+
const floor = DATA.methodology.maxLanguageErrorPercent;
|
|
242
|
+
return new Map(
|
|
243
|
+
scoreModels(models, languages).map((candidate) => [
|
|
244
|
+
candidate.model.id,
|
|
245
|
+
{
|
|
246
|
+
error: candidate.error,
|
|
247
|
+
waitSeconds: candidate.waitSeconds,
|
|
248
|
+
usable: candidate.worstError < floor,
|
|
249
|
+
manual: candidate.manual,
|
|
250
|
+
},
|
|
251
|
+
]),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The usable models on the speed/accuracy frontier: nothing usable is both
|
|
257
|
+
* quicker and more accurate. Equal models both stay, so a tie never hides one.
|
|
258
|
+
*/
|
|
259
|
+
export function frontierModelIds(
|
|
260
|
+
benchmarks: ReadonlyMap<string, ModelBenchmark>,
|
|
261
|
+
): Set<string> {
|
|
262
|
+
const usable = [...benchmarks]
|
|
263
|
+
.filter(([, benchmark]) => benchmark.usable)
|
|
264
|
+
.map(([id, benchmark]) => ({ id, error: benchmark.error, wait: benchmark.waitSeconds }));
|
|
265
|
+
return new Set(
|
|
266
|
+
usable
|
|
267
|
+
.filter((candidate) =>
|
|
268
|
+
!usable.some((other) =>
|
|
269
|
+
other.error <= candidate.error &&
|
|
270
|
+
other.wait <= candidate.wait &&
|
|
271
|
+
(other.error < candidate.error || other.wait < candidate.wait),
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
.map((candidate) => candidate.id),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function fallbackRecommendation(
|
|
279
|
+
models: readonly CatalogModel[],
|
|
280
|
+
languages: readonly string[],
|
|
281
|
+
): ModelRecommendation[] {
|
|
282
|
+
const wanted = [...new Set(languages.map(recommendationLanguage))];
|
|
283
|
+
const compatible = models.filter(
|
|
284
|
+
(model) =>
|
|
285
|
+
wanted.every((language) =>
|
|
286
|
+
modelMatchesRecommendationLanguage(model, language),
|
|
287
|
+
) && (wanted.length === 1 || model.capabilities.languageDetection),
|
|
288
|
+
);
|
|
289
|
+
const model = rankCatalogModels(compatible.length ? compatible : models, wanted)[0];
|
|
290
|
+
return model
|
|
291
|
+
? [{ model, roles: ["best", "fast", "accurate"], status: "unbenchmarked" }]
|
|
292
|
+
: [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function lowest<T>(items: readonly T[], key: (item: T) => number): T | undefined {
|
|
296
|
+
let best: T | undefined;
|
|
297
|
+
for (const item of items) {
|
|
298
|
+
if (best === undefined || key(item) < key(best)) best = item;
|
|
299
|
+
}
|
|
300
|
+
return best;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Overall, fast, and accurate picks. Every role is always assigned: when no
|
|
305
|
+
* other model earns the fast or accurate role, the overall pick carries it,
|
|
306
|
+
* so callers can rely on all three without special cases. The picks do not
|
|
307
|
+
* depend on the machine: they are judged on the benchmark rig, and the Try
|
|
308
|
+
* it step measures the real wait.
|
|
309
|
+
*/
|
|
310
|
+
export function recommendModels(
|
|
311
|
+
models: readonly CatalogModel[],
|
|
312
|
+
languages: readonly string[],
|
|
313
|
+
): ModelRecommendation[] {
|
|
314
|
+
const scored = scoreModels(models, languages);
|
|
315
|
+
if (scored.length === 0) return fallbackRecommendation(models, languages);
|
|
316
|
+
const method = DATA.methodology;
|
|
317
|
+
const byError = (candidate: ScoredModel) => candidate.error;
|
|
318
|
+
|
|
319
|
+
// Quality outranks both latency and automatic switching. Prefer an
|
|
320
|
+
// automatic model when one clears the floor, then a usable manual model.
|
|
321
|
+
// Only compare over-floor models when no usable candidate exists at all.
|
|
322
|
+
const withinFloor = scored.filter(
|
|
323
|
+
(candidate) => candidate.worstError < method.maxLanguageErrorPercent,
|
|
324
|
+
);
|
|
325
|
+
const detectingWithinFloor = withinFloor.filter((candidate) => !candidate.manual);
|
|
326
|
+
const hasUsableModel = withinFloor.length > 0;
|
|
327
|
+
const pool = detectingWithinFloor.length
|
|
328
|
+
? detectingWithinFloor
|
|
329
|
+
: hasUsableModel
|
|
330
|
+
? withinFloor
|
|
331
|
+
: scored;
|
|
332
|
+
|
|
333
|
+
const withinOverallBudget = pool.filter(
|
|
334
|
+
(candidate) => candidate.waitSeconds <= method.overallMaxWaitSeconds,
|
|
335
|
+
);
|
|
336
|
+
const overall = hasUsableModel
|
|
337
|
+
? lowest(
|
|
338
|
+
withinOverallBudget.length ? withinOverallBudget : pool,
|
|
339
|
+
(candidate) => candidate.score,
|
|
340
|
+
)!
|
|
341
|
+
: lowest(pool, (candidate) => candidate.worstError + candidate.error / 1000)!;
|
|
342
|
+
|
|
343
|
+
// Fast means the best quality inside the CPU target. If every usable model
|
|
344
|
+
// misses that target, prefer the quickest usable model rather than a fast
|
|
345
|
+
// model whose transcript is not useful.
|
|
346
|
+
const onCpu = pool.filter(
|
|
347
|
+
(candidate): candidate is ScoredModel & { cpuWaitSeconds: number } =>
|
|
348
|
+
candidate.cpuWaitSeconds !== undefined,
|
|
349
|
+
);
|
|
350
|
+
const withinCpuBudget = onCpu.filter(
|
|
351
|
+
(candidate) => candidate.cpuWaitSeconds <= method.fastCpuMaxWaitSeconds,
|
|
352
|
+
);
|
|
353
|
+
const fast = hasUsableModel
|
|
354
|
+
? lowest(withinCpuBudget, byError) ??
|
|
355
|
+
lowest(onCpu, (candidate) => candidate.cpuWaitSeconds + candidate.error / 1000) ??
|
|
356
|
+
overall
|
|
357
|
+
: overall;
|
|
358
|
+
|
|
359
|
+
const accurate = hasUsableModel
|
|
360
|
+
? lowest(
|
|
361
|
+
pool.filter((candidate) => candidate.waitSeconds <= method.accurateMaxWaitSeconds),
|
|
362
|
+
byError,
|
|
363
|
+
) ?? lowest(pool, byError) ?? overall
|
|
364
|
+
: overall;
|
|
365
|
+
|
|
366
|
+
const byId = new Map<string, ModelRecommendation>();
|
|
367
|
+
for (const [candidate, role] of [
|
|
368
|
+
[overall, "best"],
|
|
369
|
+
[fast, "fast"],
|
|
370
|
+
[accurate, "accurate"],
|
|
371
|
+
] as const) {
|
|
372
|
+
const current = byId.get(candidate.model.id);
|
|
373
|
+
if (current) current.roles.push(role);
|
|
374
|
+
else {
|
|
375
|
+
byId.set(candidate.model.id, {
|
|
376
|
+
model: candidate.model,
|
|
377
|
+
roles: [role],
|
|
378
|
+
status: hasUsableModel
|
|
379
|
+
? "eligible"
|
|
380
|
+
: candidate.worstError < method.experimentalMaxErrorPercent ||
|
|
381
|
+
(candidate.worstCiLower !== undefined && candidate.worstCiLower <= method.experimentalMaxErrorPercent)
|
|
382
|
+
? "experimental"
|
|
383
|
+
: "unsupported",
|
|
384
|
+
error: candidate.error,
|
|
385
|
+
waitSeconds: candidate.waitSeconds,
|
|
386
|
+
withinFastWaitTarget:
|
|
387
|
+
candidate.cpuWaitSeconds !== undefined &&
|
|
388
|
+
candidate.cpuWaitSeconds <= method.fastCpuMaxWaitSeconds,
|
|
389
|
+
worstLanguage: candidate.worstLanguage,
|
|
390
|
+
worstError: candidate.worstError,
|
|
391
|
+
overFloor: candidate.worstError >= method.maxLanguageErrorPercent,
|
|
392
|
+
nearFloor:
|
|
393
|
+
candidate.worstError >= method.noteMinErrorPercent &&
|
|
394
|
+
candidate.worstError < method.maxLanguageErrorPercent,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return [...byId.values()];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Languages suitable for the recommendation-oriented preferred-language
|
|
403
|
+
* picker. Experimental languages remain discoverable; unsupported and
|
|
404
|
+
* unbenchmarked model-card claims stay available only in model-specific
|
|
405
|
+
* language controls.
|
|
406
|
+
*/
|
|
407
|
+
export function getPreferredRecommendationLanguages(
|
|
408
|
+
models: readonly CatalogModel[],
|
|
409
|
+
): string[] {
|
|
410
|
+
const languages = new Set(
|
|
411
|
+
models.flatMap((model) => model.languages.map(recommendationLanguage)),
|
|
412
|
+
);
|
|
413
|
+
return [...languages]
|
|
414
|
+
.filter((language) => {
|
|
415
|
+
const status = recommendModels(models, [language])[0]?.status;
|
|
416
|
+
return status === "eligible" || status === "experimental";
|
|
417
|
+
})
|
|
418
|
+
.sort((left, right) => {
|
|
419
|
+
if (left === "en") return -1;
|
|
420
|
+
if (right === "en") return 1;
|
|
421
|
+
return displayLanguage(left).localeCompare(displayLanguage(right));
|
|
422
|
+
});
|
|
423
|
+
}
|