nixamp 0.28.44 → 0.28.45

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.
Files changed (61) hide show
  1. package/dist/live-interpreter.d.ts +1 -72
  2. package/dist/live-interpreter.js +1 -269
  3. package/dist/live-recovery.d.ts +1 -6
  4. package/dist/live-recovery.js +1 -11
  5. package/dist/live-voice.d.ts +6 -76
  6. package/dist/live-voice.js +6 -352
  7. package/dist/owner.js +4 -0
  8. package/dist/pcm-wav.d.ts +1 -2
  9. package/dist/pcm-wav.js +1 -27
  10. package/dist/shared-translation.d.ts +6 -53
  11. package/dist/shared-translation.js +7 -336
  12. package/dist/speaker-turns.d.ts +1 -32
  13. package/dist/speaker-turns.js +1 -30
  14. package/dist/speech.d.ts +3 -5
  15. package/dist/speech.js +4 -7
  16. package/dist/translation-passes.d.ts +5 -125
  17. package/dist/translation-passes.js +5 -294
  18. package/dist/upgrade-allowance.d.ts +1 -23
  19. package/dist/upgrade-allowance.js +1 -73
  20. package/dist/voice-profile.d.ts +1 -7
  21. package/dist/voice-profile.js +1 -53
  22. package/dist/voice-provider.d.ts +1 -9
  23. package/dist/voice-provider.js +1 -27
  24. package/package.json +2 -1
  25. package/src/live-interpreter.ts +1 -213
  26. package/src/live-recovery.ts +1 -10
  27. package/src/live-voice.ts +6 -304
  28. package/src/owner.ts +3 -0
  29. package/src/pcm-wav.ts +1 -26
  30. package/src/shared-translation.ts +7 -222
  31. package/src/speaker-turns.ts +1 -34
  32. package/src/speech.ts +4 -5
  33. package/src/translation-passes.ts +6 -292
  34. package/src/upgrade-allowance.ts +1 -72
  35. package/src/voice-profile.ts +1 -46
  36. package/src/voice-provider.ts +1 -22
  37. package/web/dist/assets/{ar-CLNm1oA7.js → ar-8fDonUoB.js} +1 -1
  38. package/web/dist/assets/{cs-Cptn8RbB.js → cs-DCClolTy.js} +1 -1
  39. package/web/dist/assets/{da-CRbNCFOu.js → da-C3RPhXUd.js} +1 -1
  40. package/web/dist/assets/{de-D4DnC5NQ.js → de-CYBoEomk.js} +1 -1
  41. package/web/dist/assets/{es-DAHnD9bs.js → es-DS3ls4xT.js} +1 -1
  42. package/web/dist/assets/{fi-Csbl41Vz.js → fi-DbCZpO89.js} +1 -1
  43. package/web/dist/assets/{fr-jZYQJed8.js → fr-D0z3UpdL.js} +1 -1
  44. package/web/dist/assets/{hi-DjT9u0v_.js → hi-BTU3sRlQ.js} +1 -1
  45. package/web/dist/assets/{hls-3VKVEQE3-BjAVnvGY.js → hls-3VKVEQE3-CQXTVHn_.js} +1 -1
  46. package/web/dist/assets/{hu-CVrU8gC4.js → hu-B37Q8ldT.js} +1 -1
  47. package/web/dist/assets/{id-B_gN24X5.js → id-4pdXNa4G.js} +1 -1
  48. package/web/dist/assets/index-BDBbN5Qy.js +22 -0
  49. package/web/dist/assets/{index-BueBHHcv.css → index-Bo3utcDw.css} +1 -1
  50. package/web/dist/assets/{it-BUC1dMpV.js → it-CTe69-UY.js} +1 -1
  51. package/web/dist/assets/{mpegts-CnqmXTu2.js → mpegts-BBmkDppm.js} +1 -1
  52. package/web/dist/assets/{mpegts-LO6RVLD6-B4QgG3Xb.js → mpegts-LO6RVLD6-CUVdjYFi.js} +1 -1
  53. package/web/dist/assets/{nl-CGi-aIrz.js → nl-CxrmHrMn.js} +1 -1
  54. package/web/dist/assets/{ru-YIbS63cK.js → ru-zjVCfIKq.js} +1 -1
  55. package/web/dist/assets/{sv-CuJUpOCk.js → sv-CEoiT2Lr.js} +1 -1
  56. package/web/dist/assets/{uk-DLfG8YhV.js → uk-ZC6JzQvD.js} +1 -1
  57. package/web/dist/assets/{vi-nv2siY2T.js → vi-D9anF-cl.js} +1 -1
  58. package/web/dist/assets/{zh-P5x8w1OW.js → zh-BeLYdC0I.js} +1 -1
  59. package/web/dist/index.html +7 -12
  60. package/web/dist/sw.js +23 -23
  61. package/web/dist/assets/index-c3uxZS8_.js +0 -3
@@ -1,353 +1,7 @@
1
- /** ElevenLabs Flash for live captions. The key stays on the account server. */
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { SpeechError, decodeWav, encodeWav, quietSamples } from "./speech.js";
4
- import { speakerTurns } from "./speaker-turns.js";
5
- import { Guard } from "./guard.js";
6
- import { voiceProviderFailure } from "./voice-provider.js";
7
- export const LIVE_VOICE_MODEL = "eleven_flash_v2_5";
8
- export const LIVE_VOICE_RATE = 16_000;
9
- export const LIVE_VOICE_LANGUAGES = new Set("en ja zh de hi fr ko pt it es id nl tr fil pl sv bg ro ar cs el fi hr ms sk da ta uk ru hu no vi".split(" "));
10
- const HEADERS = { "content-type": "audio/pcm", "cache-control": "no-store", "x-audio-sample-rate": String(LIVE_VOICE_RATE) };
11
- const budget = (value, fallback) => Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
12
- export class LiveVoice {
13
- key;
14
- fetcher;
15
- now;
16
- catalog = null;
17
- catalogUntil = 0;
18
- cache = new Map();
19
- pending = new Map();
20
- usage = new Map();
21
- charsPerMinute;
22
- requests;
23
- grants = new Map();
24
- activeBy = new Map();
25
- db;
26
- billing;
27
- schema = null;
28
- dailyChars;
29
- cleanupAt = 0;
30
- hearing = new Set();
31
- dailyAudioSeconds;
32
- userDailyChars;
33
- userDailyAudioSeconds;
34
- providerPause = null;
35
- checkProvider() {
36
- if (this.providerPause && this.providerPause.until > this.now())
37
- throw this.providerPause.error;
38
- }
39
- async providerFailed(response, operation) {
40
- const failure = await voiceProviderFailure(response);
41
- const until = this.now() + failure.cooldownMs;
42
- // Another in-flight response must not shorten an existing billing pause.
43
- if (!this.providerPause || this.providerPause.until < until)
44
- this.providerPause = { until, error: failure.error };
45
- console.error(JSON.stringify({ event: "voice_provider_failure", provider: "elevenlabs", operation, status: response.status, code: failure.code, cooldownMs: failure.cooldownMs }));
46
- throw failure.error;
47
- }
48
- constructor(options = {}) {
49
- this.key = options.apiKey ?? process.env["ELEVENLABS_API_KEY"] ?? "";
50
- this.fetcher = options.fetcher ?? fetch;
51
- this.now = options.now ?? Date.now;
52
- this.charsPerMinute = budget(options.charsPerMinute, 3000);
53
- this.dailyChars = budget(options.dailyChars, 200_000);
54
- this.dailyAudioSeconds = budget(options.dailyAudioSeconds, 86_400);
55
- this.userDailyChars = budget(options.userDailyChars, 120_000);
56
- this.userDailyAudioSeconds = budget(options.userDailyAudioSeconds, 43_200);
57
- this.requests = new Guard(this.now);
58
- this.db = options.db;
59
- this.billing = options.billing;
60
- }
61
- available() { return this.key !== ""; }
62
- /** Optional diarization, billed only while a signed-in listener requests it.
63
- * Rolling audio is bounded to 15 seconds, including overlap. Every second
64
- * submitted (also repeated context) consumes the persistent provider budget. */
65
- async hear(bytes, by, signal, meter = this.billing, resource = "") {
66
- if (!this.available())
67
- throw new SpeechError("speaker voices are unavailable", 503);
68
- await meter?.require(by, resource);
69
- this.checkProvider();
70
- const wav = decodeWav(bytes);
71
- const seconds = wav.samples.length / wav.rate;
72
- if (wav.rate !== 16000 || wav.channels !== 1 || seconds < 0.2 || seconds > 15.1)
73
- throw new SpeechError("send up to 15 seconds of mono 16 kHz WAV", 400);
74
- if (wav.samples.some(sample => !Number.isFinite(sample)))
75
- throw new SpeechError("invalid audio samples", 400);
76
- if (!this.requests.check(`hear:${by}`, { allowed: 36, windowMs: 60_000 }).ok)
77
- throw new SpeechError("too many speaker transcription requests", 429);
78
- if (quietSamples(wav.samples))
79
- return { language: "", seconds, turns: [] };
80
- if (this.hearing.has(by) || this.hearing.size >= 4)
81
- throw new SpeechError("speaker transcription is busy", 429);
82
- this.hearing.add(by);
83
- let reservation;
84
- let accepted = false;
85
- try {
86
- const billed = Math.ceil(seconds);
87
- await this.reserve(`scribe:user:${by}`, billed, 300, 60_000);
88
- await this.reserve("scribe:server", billed, 600, 60_000);
89
- await this.reserve(`scribe:user:${by}`, billed, this.userDailyAudioSeconds, 86_400_000);
90
- await this.reserve("scribe:server", billed, this.dailyAudioSeconds, 86_400_000);
91
- signal?.throwIfAborted();
92
- this.checkProvider();
93
- const form = new FormData();
94
- // Canonical PCM prevents a crafted container from billing more audio
95
- // than the duration we validated, and removes uploaded metadata.
96
- form.set("file", new Blob([new Uint8Array(encodeWav(wav.samples))], { type: "audio/wav" }), "listening.wav");
97
- form.set("model_id", "scribe_v2");
98
- form.set("diarize", "true");
99
- form.set("tag_audio_events", "false");
100
- form.set("timestamps_granularity", "word");
101
- // No language_code: preserve the source language, including Spanish.
102
- reservation = await meter?.reserve(by, "transcription", wav.samples.length, resource);
103
- const answer = await this.fetcher("https://api.elevenlabs.io/v1/speech-to-text", {
104
- method: "POST", headers: { "xi-api-key": this.key }, body: form,
105
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
106
- });
107
- if (!answer.ok)
108
- await this.providerFailed(answer, "transcription");
109
- accepted = true;
110
- console.info(JSON.stringify({ event: "voice_provider_usage", provider: "elevenlabs", operation: "transcription", account: createHash("sha256").update(by).digest("hex").slice(0, 16), audioSeconds: seconds }));
111
- if (reservation)
112
- await meter.commit(reservation);
113
- return speakerTurns(await answer.json(), wav);
114
- }
115
- catch (error) {
116
- if (reservation && !accepted)
117
- await meter.refund(reservation);
118
- throw error;
119
- }
120
- finally {
121
- this.hearing.delete(by);
122
- }
123
- }
124
- async voices() {
125
- if (!this.available())
126
- throw new SpeechError("translated audio needs ELEVENLABS_API_KEY on the account server", 503);
127
- this.checkProvider();
128
- if (!this.catalog || this.now() >= this.catalogUntil) {
129
- this.catalogUntil = this.now() + 3600_000;
130
- this.catalog = this.fetcher("https://api.elevenlabs.io/v2/voices?page_size=100&voice_type=default", {
131
- headers: { "xi-api-key": this.key }, signal: AbortSignal.timeout(8000),
132
- }).then(async (response) => {
133
- if (!response.ok)
134
- await this.providerFailed(response, "voices");
135
- const body = await response.json();
136
- return body.voices.filter(voice => /^[a-zA-Z0-9_-]+$/.test(voice.voice_id)).map(voice => ({
137
- id: voice.voice_id, name: voice.name, gender: voice.labels?.["gender"] ?? "neutral", language: voice.labels?.["language"] ?? "",
138
- }));
139
- }).catch(error => { this.catalog = null; throw error; });
140
- }
141
- return this.catalog;
142
- }
143
- /** A 90-second capability for one channel, never the viewer's account credential. */
144
- async grant(by, channel) {
145
- if (!/^[\w-]{1,80}$/.test(channel))
146
- throw new SpeechError("choose a playback session or live channel", 400);
147
- if (!this.requests.check(`grant:${by}`, { allowed: 10, windowMs: 60_000 }).ok)
148
- throw new SpeechError("too many audio authorization requests", 429);
149
- await this.billing?.require(by, channel);
150
- for (const [token, grant] of this.grants)
151
- if (grant.expires <= this.now())
152
- this.grants.delete(token);
153
- if (this.grants.size >= 5000)
154
- throw new SpeechError("audio authorization is busy", 503);
155
- const token = `nxd_${randomBytes(32).toString("base64url")}`;
156
- const expires = this.now() + 90_000;
157
- if (this.db) {
158
- await this.ensure();
159
- await this.db.query("INSERT INTO live_voice_grants (token_hash, by_account, channel, expires_at, remaining) VALUES ($1, $2, $3, $4, 2000)", [createHash("sha256").update(token).digest("hex"), by, channel, new Date(expires)]);
160
- }
161
- else
162
- this.grants.set(token, { by, channel, expires, remaining: 2000 });
163
- return { token, expires };
164
- }
165
- async authorize(token, channel, chars) {
166
- if (!/^nxd_[A-Za-z0-9_-]{43}$/.test(token))
167
- throw new SpeechError("sign in to enable translated audio", 401);
168
- if (!Number.isFinite(chars) || chars < 1 || chars > 600)
169
- throw new SpeechError("invalid caption length", 400);
170
- if (this.db) {
171
- await this.ensure();
172
- const result = await this.db.query(`UPDATE live_voice_grants SET remaining = remaining - $3
173
- WHERE token_hash = $1 AND channel = $2 AND expires_at > $4 AND remaining >= $3 RETURNING by_account`, [createHash("sha256").update(token).digest("hex"), channel, chars, new Date(this.now())]);
174
- if (!result.rows.length)
175
- throw new SpeechError("audio authorization expired or reached its limit; enable translated audio again", 401);
176
- return String(result.rows[0]?.["by_account"]);
177
- }
178
- const grant = this.grants.get(token);
179
- if (!grant || grant.expires <= this.now() || grant.channel !== channel)
180
- throw new SpeechError("sign in to renew translated audio", 401);
181
- if (grant.remaining < chars)
182
- throw new SpeechError("this audio authorization reached its character limit", 429);
183
- grant.remaining -= chars;
184
- return grant.by;
185
- }
186
- async ensure() {
187
- if (!this.db)
188
- return;
189
- this.schema ??= (async () => {
190
- await this.db.query("CREATE TABLE IF NOT EXISTS live_voice_usage (bucket TEXT PRIMARY KEY, chars BIGINT NOT NULL, expires_at TIMESTAMPTZ NOT NULL)");
191
- await this.db.query("CREATE TABLE IF NOT EXISTS live_voice_grants (token_hash TEXT PRIMARY KEY, by_account TEXT NOT NULL, channel TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, remaining INTEGER NOT NULL)");
192
- })().catch(error => { this.schema = null; throw error; });
193
- await this.schema;
194
- if (this.now() >= this.cleanupAt) {
195
- this.cleanupAt = this.now() + 60_000;
196
- // Keep only live windows; accounting survives restarts and is shared by replicas.
197
- await this.db.query("DELETE FROM live_voice_usage WHERE expires_at <= $1", [new Date(this.now())]);
198
- await this.db.query("DELETE FROM live_voice_grants WHERE expires_at <= $1", [new Date(this.now())]);
199
- }
200
- }
201
- async reserve(bucket, chars, limit, windowMs) {
202
- if (chars > limit)
203
- throw new SpeechError("translated audio reached its character budget; try again later", 429);
204
- const window = Math.floor(this.now() / windowMs);
205
- const key = `${bucket}:${windowMs}:${window}`;
206
- if (this.db) {
207
- await this.ensure();
208
- const result = await this.db.query(`INSERT INTO live_voice_usage (bucket, chars, expires_at) VALUES ($1, $2, $4)
209
- ON CONFLICT (bucket) DO UPDATE SET chars = live_voice_usage.chars + EXCLUDED.chars
210
- WHERE live_voice_usage.chars + EXCLUDED.chars <= $3 RETURNING chars`, [key, chars, limit, new Date((window + 1) * windowMs)]);
211
- if (!result.rows.length)
212
- throw new SpeechError("translated audio reached its character budget; try again later", 429);
213
- return;
214
- }
215
- // Local/single-process installations without a database. Account servers pass their pool.
216
- const record = this.usage.get(key) ?? { minute: (window + 1) * windowMs, chars: 0 };
217
- if (record.chars + chars > limit)
218
- throw new SpeechError("translated audio reached its character budget; try again later", 429);
219
- record.chars += chars;
220
- this.usage.set(key, record);
221
- }
222
- async charge(by, channel, chars) {
223
- for (const [key, record] of this.usage)
224
- if (record.minute <= this.now())
225
- this.usage.delete(key);
226
- await this.reserve(`user:${by}`, chars, this.charsPerMinute, 60_000);
227
- await this.reserve(`channel:${channel}`, chars, 6000, 60_000);
228
- await this.reserve("server", chars, 12_000, 60_000);
229
- await this.reserve(`user:${by}`, chars, this.userDailyChars, 86400_000);
230
- await this.reserve("server", chars, this.dailyChars, 86400_000);
231
- }
232
- checkRequest(by) {
233
- if (!this.requests.check(`audio:${by}`, { allowed: 60, windowMs: 60_000 }).ok)
234
- throw new SpeechError("too many translated audio requests", 429);
235
- }
236
- /** Stream the first request immediately; concurrent listeners share its cached result. */
237
- async stream(ask, by, signal, meter = this.billing) {
238
- this.checkRequest(by);
239
- await meter?.require(by, ask.channel);
240
- const text = typeof ask.text === "string" ? ask.text.trim() : "";
241
- if (!text || text.length > 600)
242
- throw new SpeechError("translated audio needs a caption of 1–600 characters", 400);
243
- if (!LIVE_VOICE_LANGUAGES.has(ask.language))
244
- throw new SpeechError("this language is not supported by Flash voices", 400);
245
- const voices = await this.voices();
246
- signal?.throwIfAborted();
247
- // The browser chooses unique voices for speakers. Legacy callers get a
248
- // stable stock voice; pitch is not used to infer gender.
249
- const seed = createHash("sha256").update(`${ask.channel ?? ""}|${ask.speaker ?? ""}`).digest().readUInt32BE(0);
250
- const voice = ask.voice && ask.voice !== "auto"
251
- ? voices.find(voice => voice.id === ask.voice)
252
- : voices[seed % voices.length];
253
- if (!voice)
254
- throw new SpeechError("choose an available voice", 400);
255
- const id = createHash("sha256").update(JSON.stringify([LIVE_VOICE_MODEL, voice.id, ask.language, text])).digest("hex");
256
- for (const [key, item] of this.cache)
257
- if (item.until < this.now())
258
- this.cache.delete(key);
259
- const cached = this.cache.get(id);
260
- const pending = this.pending.get(id);
261
- if (cached || pending) {
262
- const bytes = cached?.bytes ?? await pending;
263
- signal?.throwIfAborted();
264
- const paid = await meter?.reserve(by, "voice", text.length, ask.channel);
265
- if (paid)
266
- await meter.commit(paid);
267
- return new Response(new Uint8Array(bytes), { headers: HEADERS });
268
- }
269
- if (this.pending.size >= 4)
270
- throw new SpeechError("translated audio is busy; waiting for the next caption", 429);
271
- if ((this.activeBy.get(by) ?? 0) >= 2)
272
- throw new SpeechError("two voice requests are already active for this account", 429);
273
- // Register before the first provider await, so identical requests cannot both bill.
274
- let done;
275
- let fail;
276
- const finished = new Promise((resolve, reject) => { done = resolve; fail = reject; });
277
- this.pending.set(id, finished);
278
- this.activeBy.set(by, (this.activeBy.get(by) ?? 0) + 1);
279
- const release = () => { this.pending.delete(id); this.activeBy.set(by, Math.max(0, (this.activeBy.get(by) ?? 1) - 1)); if (!this.activeBy.get(by))
280
- this.activeBy.delete(by); };
281
- void finished.catch(() => undefined);
282
- let reservation;
283
- let accepted = false;
284
- try {
285
- await this.charge(by, ask.channel ?? "direct", text.length);
286
- this.checkProvider();
287
- signal?.throwIfAborted();
288
- reservation = await meter?.reserve(by, "voice", text.length, ask.channel);
289
- const response = await this.fetcher(`https://api.elevenlabs.io/v1/text-to-speech/${voice.id}/stream?output_format=pcm_16000`, {
290
- method: "POST",
291
- headers: { "xi-api-key": this.key, "content-type": "application/json" },
292
- body: JSON.stringify({ text, model_id: LIVE_VOICE_MODEL, language_code: ask.language }),
293
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
294
- });
295
- if (!response.ok)
296
- await this.providerFailed(response, "synthesis");
297
- if (!response.body)
298
- throw new SpeechError("ElevenLabs returned no audio", 502);
299
- // Once the provider accepts, aborting playback cannot refund heard audio.
300
- accepted = true;
301
- console.info(JSON.stringify({ event: "voice_provider_usage", provider: "elevenlabs", operation: "synthesis", account: createHash("sha256").update(by).digest("hex").slice(0, 16), characters: text.length }));
302
- if (reservation)
303
- await meter.commit(reservation);
304
- const [play, keep] = response.body.tee();
305
- void (async () => {
306
- const reader = keep.getReader();
307
- const chunks = [];
308
- let size = 0;
309
- try {
310
- while (true) {
311
- const { done: ended, value } = await reader.read();
312
- if (ended)
313
- break;
314
- size += value.length;
315
- if (size > 2 * 1024 * 1024) {
316
- await reader.cancel();
317
- throw new SpeechError("voice response was too long", 502);
318
- }
319
- chunks.push(value);
320
- }
321
- if (!size || size % 2)
322
- throw new SpeechError("voice response contained incomplete audio", 502);
323
- const bytes = new Uint8Array(size);
324
- let at = 0;
325
- for (const chunk of chunks) {
326
- bytes.set(chunk, at);
327
- at += chunk.length;
328
- }
329
- this.cache.set(id, { until: this.now() + 60_000, bytes });
330
- // At most a few MB for recent lines, never a recording archive.
331
- while (this.cache.size > 32)
332
- this.cache.delete(this.cache.keys().next().value);
333
- done(bytes);
334
- }
335
- catch (error) {
336
- fail(error);
337
- }
338
- finally {
339
- reader.releaseLock();
340
- release();
341
- }
342
- })();
343
- return new Response(play, { headers: HEADERS });
344
- }
345
- catch (error) {
346
- fail(error);
347
- release();
348
- if (reservation && !accepted)
349
- await meter.refund(reservation);
350
- throw error;
351
- }
352
- }
1
+ /** ElevenLabs Flash/Scribe for live captions, from @profullstack/live-translate.
2
+ * nixamp keeps its `nxd_` grant tokens: captions.ts and older pages expect them. */
3
+ import { LiveVoice as Base } from "@profullstack/live-translate";
4
+ export { LIVE_VOICE_MODEL, LIVE_VOICE_RATE, LIVE_VOICE_LANGUAGES } from "@profullstack/live-translate";
5
+ export class LiveVoice extends Base {
6
+ constructor(options = {}) { super({ tokenPrefix: "nxd_", ...options }); }
353
7
  }
package/dist/owner.js CHANGED
@@ -116,6 +116,10 @@ export function needsAdmin(path, method = "GET") {
116
116
  // with a catalog entry, and the same question.
117
117
  if (/^\/api\/tracks\/\d+\/live$/.test(path))
118
118
  return true;
119
+ // And so is a folder: without this the gate never ran for it, and a
120
+ // listen link alone could put any folder on the air, uncapped.
121
+ if (path === "/api/folders/live")
122
+ return true;
119
123
  return false;
120
124
  }
121
125
  /**
package/dist/pcm-wav.d.ts CHANGED
@@ -1,2 +1 @@
1
- /** 16-bit mono PCM WAV bytes from samples in [-1, 1]. */
2
- export declare function encodeWav(samples: Float32Array, rate?: number): Uint8Array;
1
+ export { encodeWav } from "@profullstack/live-translate/core";
package/dist/pcm-wav.js CHANGED
@@ -1,27 +1 @@
1
- /** 16-bit mono PCM WAV bytes from samples in [-1, 1]. */
2
- export function encodeWav(samples, rate = 16000) {
3
- const bytes = new Uint8Array(44 + samples.length * 2);
4
- const view = new DataView(bytes.buffer);
5
- const ascii = (at, text) => {
6
- for (let i = 0; i < text.length; i++)
7
- bytes[at + i] = text.charCodeAt(i);
8
- };
9
- ascii(0, "RIFF");
10
- view.setUint32(4, 36 + samples.length * 2, true);
11
- ascii(8, "WAVE");
12
- ascii(12, "fmt ");
13
- view.setUint32(16, 16, true);
14
- view.setUint16(20, 1, true);
15
- view.setUint16(22, 1, true);
16
- view.setUint32(24, rate, true);
17
- view.setUint32(28, rate * 2, true);
18
- view.setUint16(32, 2, true);
19
- view.setUint16(34, 16, true);
20
- ascii(36, "data");
21
- view.setUint32(40, samples.length * 2, true);
22
- for (let i = 0; i < samples.length; i++) {
23
- const clipped = Math.max(-1, Math.min(1, samples[i]));
24
- view.setInt16(44 + i * 2, clipped < 0 ? clipped * 32768 : clipped * 32767, true);
25
- }
26
- return bytes;
27
- }
1
+ export { encodeWav } from "@profullstack/live-translate/core";
@@ -1,56 +1,9 @@
1
- import { type Caption } from "./live-interpreter.ts";
2
- import { LiveVoice } from "./live-voice.ts";
3
- import type { Translator } from "./translate.ts";
4
- import type { TranslationMeter } from "./translation-passes.ts";
5
- export declare function publicStreamAddress(address: string): boolean;
1
+ /** Shared live translation, from @profullstack/live-translate, limited to nixamp channels. */
2
+ import { SharedTranslations as Base, type OpenLiveInput } from "@profullstack/live-translate";
3
+ export { publicStreamAddress, type LiveInput, type OpenLiveInput, type SharedEvent } from "@profullstack/live-translate";
4
+ /** Only a live nixamp channel (`/api/channels/<id>`, optionally `?k=`). */
6
5
  export declare function sharedSource(raw: string): URL;
7
- export interface LiveInput {
8
- stop(): void;
9
- }
10
- export type OpenLiveInput = (source: URL, pcm: (bytes: Buffer) => void, ended: () => void, signal: AbortSignal) => Promise<LiveInput>;
11
- /** Resolve once and pin that public address; no redirects or ffmpeg URL fetches. */
12
6
  export declare function liveInput(ffmpeg?: string[]): OpenLiveInput;
13
- export type SharedEvent = {
14
- type: "status";
15
- listeners: number;
16
- } | {
17
- type: "line";
18
- id: string;
19
- line: Caption;
20
- } | {
21
- type: "audio";
22
- id: string;
23
- data: string;
24
- } | {
25
- type: "end";
26
- id: string;
27
- } | {
28
- type: "error";
29
- error: string;
30
- retryable?: boolean;
31
- };
32
- type Member = {
33
- by: string;
34
- send: (event: SharedEvent) => void;
35
- close: () => void;
36
- };
37
- export declare class SharedTranslations {
38
- private readonly options;
39
- private readonly feeds;
40
- constructor(options: {
41
- voice: LiveVoice;
42
- translator: Pick<Translator, "translate">;
43
- billing?: TranslationMeter;
44
- open?: OpenLiveInput;
45
- });
46
- get size(): number;
47
- join(raw: string, language: string, by: string, send: Member["send"], close: () => void): Promise<() => void>;
48
- private leave;
49
- private checkMembers;
50
- private broadcast;
51
- private stop;
52
- private make;
53
- private start;
54
- private speak;
7
+ export declare class SharedTranslations extends Base {
8
+ constructor(options: ConstructorParameters<typeof Base>[0]);
55
9
  }
56
- export {};