nixamp 0.23.7 → 0.24.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/README.md +79 -3
- package/dist/captions.d.ts +57 -10
- package/dist/captions.js +253 -29
- package/dist/main.js +63 -19
- package/dist/mcp.d.ts +7 -1
- package/dist/mcp.js +159 -24
- package/dist/server.d.ts +11 -0
- package/dist/server.js +353 -8
- package/dist/speech.d.ts +21 -3
- package/dist/speech.js +65 -8
- package/dist/transcribe.d.ts +70 -0
- package/dist/transcribe.js +337 -41
- package/dist/transcript-client.d.ts +81 -0
- package/dist/transcript-client.js +76 -0
- package/dist/transcript.d.ts +7 -2
- package/dist/transcript.js +73 -5
- package/dist/transcripts.d.ts +135 -0
- package/dist/transcripts.js +384 -0
- package/dist/translate-cli.d.ts +16 -0
- package/dist/translate-cli.js +97 -0
- package/dist/translate-jobs.d.ts +39 -0
- package/dist/translate-jobs.js +120 -0
- package/dist/translate.d.ts +78 -0
- package/dist/translate.js +241 -0
- package/dist/warm.d.ts +1 -0
- package/dist/warm.js +40 -0
- package/package.json +1 -1
- package/src/captions.ts +276 -29
- package/src/main.ts +63 -19
- package/src/mcp.ts +156 -21
- package/src/server.ts +352 -8
- package/src/speech.ts +103 -13
- package/src/transcribe.ts +374 -41
- package/src/transcript-client.ts +147 -0
- package/src/transcript.ts +76 -4
- package/src/transcripts.ts +462 -0
- package/src/translate-cli.ts +111 -0
- package/src/translate-jobs.ts +132 -0
- package/src/translate.ts +276 -0
- package/src/warm.ts +43 -0
- package/web/dist/assets/{hls-3VKVEQE3-CI1U7kbP.js → hls-3VKVEQE3-Dtl-3mpW.js} +1 -1
- package/web/dist/assets/index-B0h4Nexr.js +1 -0
- package/web/dist/assets/index-BTGV3Pi5.css +1 -0
- package/web/dist/assets/{mpegts-CPOYjgRP.js → mpegts-BJC48bFV.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CE8YPjx1.js → mpegts-LO6RVLD6-CUIAB9k3.js} +1 -1
- package/web/dist/index.html +12 -8
- package/web/dist/sw.js +6 -6
- package/web/dist/assets/index-46pwGn5-.css +0 -1
- package/web/dist/assets/index-5H3sUGHu.js +0 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Where the pair models come from: this prefix and `<from>-<to>`. */
|
|
2
|
+
export declare const MODEL_PREFIX = "Xenova/opus-mt-";
|
|
3
|
+
/** How many pairs stay loaded at once. Each holds three to five hundred megabytes beside the ear's four. */
|
|
4
|
+
export declare const KEEP_LOADED = 3;
|
|
5
|
+
/** How many lines a model is handed at a time. Past eight one of them may run away. */
|
|
6
|
+
export declare const BATCH = 8;
|
|
7
|
+
/** How much one account may have translated in a minute, in characters. Twelve captions a minute is under a thousand. */
|
|
8
|
+
export declare const CHARS_PER_MINUTE = 20000;
|
|
9
|
+
/** How many may wait on the CPU at once. */
|
|
10
|
+
export declare const QUEUE_LIMIT = 16;
|
|
11
|
+
/** A translation is never longer than this many tokens: the runaway guard. */
|
|
12
|
+
export declare const MAX_NEW_TOKENS = 160;
|
|
13
|
+
/**
|
|
14
|
+
* The directions a pair model exists for, as of the models that were
|
|
15
|
+
* measured. Anything else goes through English, and what English cannot
|
|
16
|
+
* reach is refused with a sentence rather than guessed at.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PAIRS: ReadonlySet<string>;
|
|
19
|
+
/** What the page and the CLI call a language. */
|
|
20
|
+
export declare const LANGUAGES: Record<string, {
|
|
21
|
+
name: string;
|
|
22
|
+
native: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** The models a translation from one language to another goes through, or null when there is no way. */
|
|
25
|
+
export declare function route(from: string, to: string, pairs?: ReadonlySet<string>): [string, string][] | null;
|
|
26
|
+
export declare function modelFor(from: string, to: string): string;
|
|
27
|
+
/** Lines in, lines out, one model. What a loaded pair is, to this file. */
|
|
28
|
+
export type Pair = {
|
|
29
|
+
translate: (texts: string[]) => Promise<string[]>;
|
|
30
|
+
dispose?: () => Promise<void> | void;
|
|
31
|
+
};
|
|
32
|
+
export interface TranslatorOptions {
|
|
33
|
+
cacheDir?: string;
|
|
34
|
+
/** How a pair is loaded. The tests hand in a fake; nothing else does. */
|
|
35
|
+
load?: (model: string, cacheDir: string) => Promise<Pair>;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
keep?: number;
|
|
38
|
+
pairs?: ReadonlySet<string>;
|
|
39
|
+
}
|
|
40
|
+
export interface Translated {
|
|
41
|
+
texts: string[];
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
/** The model, or the two, that did it. */
|
|
45
|
+
model: string;
|
|
46
|
+
}
|
|
47
|
+
export declare class Translator {
|
|
48
|
+
private readonly cacheDir;
|
|
49
|
+
private readonly load;
|
|
50
|
+
private readonly now;
|
|
51
|
+
private readonly keep;
|
|
52
|
+
private readonly pairs;
|
|
53
|
+
private readonly loaded;
|
|
54
|
+
private tail;
|
|
55
|
+
private waiting;
|
|
56
|
+
private readonly asked;
|
|
57
|
+
lastFailure: string;
|
|
58
|
+
constructor(options?: TranslatorOptions);
|
|
59
|
+
/** Whether one language can be said in another here, at all. */
|
|
60
|
+
can(from: string, to: string): boolean;
|
|
61
|
+
/** The languages something in `from` can be translated into. */
|
|
62
|
+
targets(from: string): string[];
|
|
63
|
+
private pair;
|
|
64
|
+
/** Load pairs now, so the first line is not the one that waits. Says whether every one of them could; never throws. */
|
|
65
|
+
warm(pairs: string[]): Promise<boolean>;
|
|
66
|
+
/** The pairs loaded right now, by model name. */
|
|
67
|
+
loadedModels(): string[];
|
|
68
|
+
/** Whether this account may have this much translated now, and the bookkeeping if so. */
|
|
69
|
+
allow(accountId: string, chars: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Texts in another language. Refuses a pair nobody can do, too much for
|
|
72
|
+
* one account, or too many waiting, each with a status. Empty texts come
|
|
73
|
+
* back empty and cost nothing.
|
|
74
|
+
*/
|
|
75
|
+
translate(texts: string[], from: string, to: string, options?: {
|
|
76
|
+
by?: string;
|
|
77
|
+
}): Promise<Translated>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation: a line in one language, said in another, on this CPU.
|
|
3
|
+
*
|
|
4
|
+
* The models are the Helsinki-NLP OPUS-MT pairs, one small Marian model
|
|
5
|
+
* per direction, run through Transformers.js like the ear (speech.ts):
|
|
6
|
+
* open weights, nothing sent anywhere, nothing billed. A pair is about a
|
|
7
|
+
* hundred megabytes and a sentence takes about a second; the many-language
|
|
8
|
+
* models that could do any pair in one model were measured and were not
|
|
9
|
+
* worth it -- NLLB-200 wants two gigabytes of memory and ten to twenty
|
|
10
|
+
* seconds a sentence here, and the multi-target Marian models echo their
|
|
11
|
+
* target prefix back and translate nothing. So a pair that has no model of
|
|
12
|
+
* its own goes through English: Swedish to German is Swedish to English,
|
|
13
|
+
* then English to German, twice the wait and the cost of the pivot in
|
|
14
|
+
* meaning, and still the honest choice.
|
|
15
|
+
*
|
|
16
|
+
* Pairs are loaded when first asked for and the least recently used is
|
|
17
|
+
* let go when there are more than a few, because each loaded pair holds a
|
|
18
|
+
* few hundred megabytes and a deployment has a ceiling. Batches are kept
|
|
19
|
+
* to a handful of lines with a cap on how much they may say: a Marian
|
|
20
|
+
* model handed twenty lines at once ran one of them into a wall of full
|
|
21
|
+
* stops until it hit the limit.
|
|
22
|
+
*/
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { stateDir } from "./daemon.js";
|
|
25
|
+
import { SpeechError, loadTransformers } from "./speech.js";
|
|
26
|
+
/** Where the pair models come from: this prefix and `<from>-<to>`. */
|
|
27
|
+
export const MODEL_PREFIX = "Xenova/opus-mt-";
|
|
28
|
+
/** How many pairs stay loaded at once. Each holds three to five hundred megabytes beside the ear's four. */
|
|
29
|
+
export const KEEP_LOADED = 3;
|
|
30
|
+
/** How many lines a model is handed at a time. Past eight one of them may run away. */
|
|
31
|
+
export const BATCH = 8;
|
|
32
|
+
/** How much one account may have translated in a minute, in characters. Twelve captions a minute is under a thousand. */
|
|
33
|
+
export const CHARS_PER_MINUTE = 20_000;
|
|
34
|
+
/** How many may wait on the CPU at once. */
|
|
35
|
+
export const QUEUE_LIMIT = 16;
|
|
36
|
+
/** A translation is never longer than this many tokens: the runaway guard. */
|
|
37
|
+
export const MAX_NEW_TOKENS = 160;
|
|
38
|
+
/**
|
|
39
|
+
* The directions a pair model exists for, as of the models that were
|
|
40
|
+
* measured. Anything else goes through English, and what English cannot
|
|
41
|
+
* reach is refused with a sentence rather than guessed at.
|
|
42
|
+
*/
|
|
43
|
+
export const PAIRS = new Set([
|
|
44
|
+
// Each of these answered 200 from the hub on 2026-09-13; de-sv, sv-de, de-it, de-nl and the Greek pairs did not.
|
|
45
|
+
"en-de", "en-sv", "en-es", "en-fr", "en-it", "en-nl", "en-ru", "en-zh", "en-ar", "en-fi", "en-da", "en-hu", "en-cs", "en-uk", "en-vi", "en-id", "en-hi",
|
|
46
|
+
"de-en", "sv-en", "es-en", "fr-en", "it-en", "nl-en", "pl-en", "ru-en", "ja-en", "zh-en", "ko-en", "ar-en", "tr-en", "fi-en", "da-en", "hu-en", "cs-en", "uk-en", "vi-en", "id-en", "hi-en",
|
|
47
|
+
"de-fr", "fr-de", "de-es", "es-de", "fr-es", "es-fr",
|
|
48
|
+
]);
|
|
49
|
+
/** What the page and the CLI call a language. */
|
|
50
|
+
export const LANGUAGES = {
|
|
51
|
+
en: { name: "English", native: "English" },
|
|
52
|
+
de: { name: "German", native: "Deutsch" },
|
|
53
|
+
sv: { name: "Swedish", native: "Svenska" },
|
|
54
|
+
es: { name: "Spanish", native: "Español" },
|
|
55
|
+
fr: { name: "French", native: "Français" },
|
|
56
|
+
it: { name: "Italian", native: "Italiano" },
|
|
57
|
+
nl: { name: "Dutch", native: "Nederlands" },
|
|
58
|
+
pl: { name: "Polish", native: "Polski" },
|
|
59
|
+
ru: { name: "Russian", native: "Русский" },
|
|
60
|
+
ja: { name: "Japanese", native: "日本語" },
|
|
61
|
+
zh: { name: "Chinese", native: "中文" },
|
|
62
|
+
ko: { name: "Korean", native: "한국어" },
|
|
63
|
+
ar: { name: "Arabic", native: "العربية" },
|
|
64
|
+
tr: { name: "Turkish", native: "Türkçe" },
|
|
65
|
+
fi: { name: "Finnish", native: "Suomi" },
|
|
66
|
+
da: { name: "Danish", native: "Dansk" },
|
|
67
|
+
hu: { name: "Hungarian", native: "Magyar" },
|
|
68
|
+
cs: { name: "Czech", native: "Čeština" },
|
|
69
|
+
uk: { name: "Ukrainian", native: "Українська" },
|
|
70
|
+
vi: { name: "Vietnamese", native: "Tiếng Việt" },
|
|
71
|
+
id: { name: "Indonesian", native: "Bahasa Indonesia" },
|
|
72
|
+
hi: { name: "Hindi", native: "हिन्दी" },
|
|
73
|
+
};
|
|
74
|
+
/** The models a translation from one language to another goes through, or null when there is no way. */
|
|
75
|
+
export function route(from, to, pairs = PAIRS) {
|
|
76
|
+
if (from === to)
|
|
77
|
+
return [];
|
|
78
|
+
if (pairs.has(`${from}-${to}`))
|
|
79
|
+
return [[from, to]];
|
|
80
|
+
if (from !== "en" && to !== "en" && pairs.has(`${from}-en`) && pairs.has(`en-${to}`))
|
|
81
|
+
return [[from, "en"], ["en", to]];
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
export function modelFor(from, to) {
|
|
85
|
+
return `${MODEL_PREFIX}${from}-${to}`;
|
|
86
|
+
}
|
|
87
|
+
async function loadMarian(model, cacheDir) {
|
|
88
|
+
const transformers = await loadTransformers();
|
|
89
|
+
transformers.env.cacheDir = cacheDir;
|
|
90
|
+
const pipeline = await transformers.pipeline("translation", model, { dtype: "q8" });
|
|
91
|
+
return {
|
|
92
|
+
translate: async (texts) => {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (let i = 0; i < texts.length; i += BATCH) {
|
|
95
|
+
const batch = texts.slice(i, i + BATCH);
|
|
96
|
+
// Twice the input and a little: a translation is about as long as
|
|
97
|
+
// what it translates, and a model that keeps going has gone wrong.
|
|
98
|
+
const longest = Math.max(...batch.map((text) => text.length));
|
|
99
|
+
const answers = await pipeline(batch, { max_new_tokens: Math.min(MAX_NEW_TOKENS, Math.ceil(longest / 2) + 16) });
|
|
100
|
+
out.push(...answers.map((answer) => answer.translation_text));
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
dispose: () => pipeline.dispose?.(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export class Translator {
|
|
108
|
+
cacheDir;
|
|
109
|
+
load;
|
|
110
|
+
now;
|
|
111
|
+
keep;
|
|
112
|
+
pairs;
|
|
113
|
+
loaded = new Map();
|
|
114
|
+
tail = Promise.resolve();
|
|
115
|
+
waiting = 0;
|
|
116
|
+
asked = new Map();
|
|
117
|
+
lastFailure = "";
|
|
118
|
+
constructor(options = {}) {
|
|
119
|
+
this.cacheDir = options.cacheDir ?? process.env["NIXAMP_STT_CACHE"] ?? join(stateDir(), "models");
|
|
120
|
+
this.load = options.load ?? loadMarian;
|
|
121
|
+
this.now = options.now ?? (() => Date.now());
|
|
122
|
+
this.keep = options.keep ?? KEEP_LOADED;
|
|
123
|
+
this.pairs = options.pairs ?? PAIRS;
|
|
124
|
+
}
|
|
125
|
+
/** Whether one language can be said in another here, at all. */
|
|
126
|
+
can(from, to) {
|
|
127
|
+
return route(from, to, this.pairs) !== null;
|
|
128
|
+
}
|
|
129
|
+
/** The languages something in `from` can be translated into. */
|
|
130
|
+
targets(from) {
|
|
131
|
+
return Object.keys(LANGUAGES).filter((to) => to !== from && this.can(from, to));
|
|
132
|
+
}
|
|
133
|
+
pair(from, to) {
|
|
134
|
+
const model = modelFor(from, to);
|
|
135
|
+
const held = this.loaded.get(model);
|
|
136
|
+
if (held) {
|
|
137
|
+
held.usedAt = this.now();
|
|
138
|
+
return held.pair;
|
|
139
|
+
}
|
|
140
|
+
const pair = this.load(model, this.cacheDir).catch((error) => {
|
|
141
|
+
// A failed load is tried again next time, not remembered forever.
|
|
142
|
+
this.loaded.delete(model);
|
|
143
|
+
throw error;
|
|
144
|
+
});
|
|
145
|
+
this.loaded.set(model, { pair, usedAt: this.now() });
|
|
146
|
+
if (this.loaded.size > this.keep) {
|
|
147
|
+
let oldest = null;
|
|
148
|
+
for (const entry of this.loaded)
|
|
149
|
+
if (entry[0] !== model && (oldest === null || entry[1].usedAt < oldest[1].usedAt))
|
|
150
|
+
oldest = entry;
|
|
151
|
+
if (oldest) {
|
|
152
|
+
this.loaded.delete(oldest[0]);
|
|
153
|
+
void oldest[1].pair.then((old) => old.dispose?.()).catch(() => undefined);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return pair;
|
|
157
|
+
}
|
|
158
|
+
/** Load pairs now, so the first line is not the one that waits. Says whether every one of them could; never throws. */
|
|
159
|
+
async warm(pairs) {
|
|
160
|
+
let all = true;
|
|
161
|
+
for (const name of pairs) {
|
|
162
|
+
const [from, to] = name.split("-");
|
|
163
|
+
if (!from || !to)
|
|
164
|
+
continue;
|
|
165
|
+
try {
|
|
166
|
+
await this.pair(from, to);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.lastFailure = error.message;
|
|
170
|
+
all = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return all;
|
|
174
|
+
}
|
|
175
|
+
/** The pairs loaded right now, by model name. */
|
|
176
|
+
loadedModels() {
|
|
177
|
+
return [...this.loaded.keys()];
|
|
178
|
+
}
|
|
179
|
+
/** Whether this account may have this much translated now, and the bookkeeping if so. */
|
|
180
|
+
allow(accountId, chars) {
|
|
181
|
+
const minute = Math.floor(this.now() / 60_000);
|
|
182
|
+
const record = this.asked.get(accountId) ?? { minute, chars: 0 };
|
|
183
|
+
if (record.minute !== minute) {
|
|
184
|
+
record.minute = minute;
|
|
185
|
+
record.chars = 0;
|
|
186
|
+
}
|
|
187
|
+
if (record.chars + chars > CHARS_PER_MINUTE) {
|
|
188
|
+
throw new SpeechError(`${CHARS_PER_MINUTE} characters a minute is plenty; try again in a moment`, 429);
|
|
189
|
+
}
|
|
190
|
+
record.chars += chars;
|
|
191
|
+
this.asked.set(accountId, record);
|
|
192
|
+
if (this.asked.size > 5000) {
|
|
193
|
+
for (const [id, one] of this.asked)
|
|
194
|
+
if (one.minute !== minute)
|
|
195
|
+
this.asked.delete(id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Texts in another language. Refuses a pair nobody can do, too much for
|
|
200
|
+
* one account, or too many waiting, each with a status. Empty texts come
|
|
201
|
+
* back empty and cost nothing.
|
|
202
|
+
*/
|
|
203
|
+
async translate(texts, from, to, options = {}) {
|
|
204
|
+
const hops = route(from, to, this.pairs);
|
|
205
|
+
if (hops === null) {
|
|
206
|
+
const known = Object.keys(LANGUAGES).includes(from) && Object.keys(LANGUAGES).includes(to);
|
|
207
|
+
throw new SpeechError(known ? `there is no model here from ${from} to ${to}` : `translation is between two-letter language codes, such as en and de`, 400);
|
|
208
|
+
}
|
|
209
|
+
const chars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
210
|
+
if (options.by)
|
|
211
|
+
this.allow(options.by, chars);
|
|
212
|
+
const model = hops.map(([a, b]) => modelFor(a, b)).join(" then ");
|
|
213
|
+
if (hops.length === 0 || chars === 0)
|
|
214
|
+
return { texts: [...texts], from, to, model };
|
|
215
|
+
if (this.waiting >= QUEUE_LIMIT)
|
|
216
|
+
throw new SpeechError("too much is being translated at once; try again in a moment", 503);
|
|
217
|
+
this.waiting += 1;
|
|
218
|
+
const turn = this.tail.then(async () => {
|
|
219
|
+
// Only the lines with something in them go to the model; the rest keep their place.
|
|
220
|
+
const spoken = texts.map((text) => text.trim());
|
|
221
|
+
const which = spoken.map((text, i) => (text === "" ? -1 : i)).filter((i) => i >= 0);
|
|
222
|
+
let current = which.map((i) => spoken[i]);
|
|
223
|
+
for (const [a, b] of hops) {
|
|
224
|
+
const pair = await this.pair(a, b);
|
|
225
|
+
current = await pair.translate(current);
|
|
226
|
+
}
|
|
227
|
+
const out = [...spoken];
|
|
228
|
+
which.forEach((i, at) => {
|
|
229
|
+
out[i] = (current[at] ?? "").trim();
|
|
230
|
+
});
|
|
231
|
+
return out;
|
|
232
|
+
});
|
|
233
|
+
this.tail = turn.catch(() => undefined);
|
|
234
|
+
try {
|
|
235
|
+
return { texts: await turn, from, to, model };
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
this.waiting -= 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
package/dist/warm.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/warm.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun dist/warm.js` -- fetch the models now, so the image carries them.
|
|
3
|
+
*
|
|
4
|
+
* Run at build time by the Dockerfile. nixamp.com's filesystem is thrown
|
|
5
|
+
* away with every deploy, so without this the ear (80 MB) and every
|
|
6
|
+
* translation pair (about 100 MB each) were downloaded again at the first
|
|
7
|
+
* ask after each one, and the first person to speak waited for it. The
|
|
8
|
+
* models land in NIXAMP_STT_CACHE and the image keeps them.
|
|
9
|
+
*
|
|
10
|
+
* NIXAMP_MT_WARM names the pairs (en-de,en-sv,de-en,sv-en by default):
|
|
11
|
+
* German and Swedish both ways, which are the two that were asked for.
|
|
12
|
+
* Exits non-zero when anything could not be fetched, so a build does not
|
|
13
|
+
* quietly ship without its ear.
|
|
14
|
+
*/
|
|
15
|
+
import { Speech } from "./speech.js";
|
|
16
|
+
import { Translator } from "./translate.js";
|
|
17
|
+
const DEFAULT_PAIRS = "en-de,en-sv,de-en,sv-en";
|
|
18
|
+
async function main() {
|
|
19
|
+
const speech = new Speech();
|
|
20
|
+
console.error(`warming ${speech.model}...`);
|
|
21
|
+
if (!(await speech.warm())) {
|
|
22
|
+
console.error(`could not load the ear: ${speech.lastFailure}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
const pairs = (process.env["NIXAMP_MT_WARM"] ?? DEFAULT_PAIRS).split(",").map((one) => one.trim()).filter(Boolean);
|
|
26
|
+
const translator = new Translator({ keep: pairs.length });
|
|
27
|
+
console.error(`warming ${pairs.join(", ")}...`);
|
|
28
|
+
if (!(await translator.warm(pairs))) {
|
|
29
|
+
console.error(`could not load a translation pair: ${translator.lastFailure}`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
console.error(`ready: ${speech.model} and ${translator.loadedModels().join(", ")}`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
main().then((code) => {
|
|
36
|
+
process.exit(code);
|
|
37
|
+
}, (error) => {
|
|
38
|
+
console.error(error.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|