nixamp 0.28.44 → 0.29.1

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 (89) hide show
  1. package/dist/audio.js +3 -0
  2. package/dist/channels.d.ts +14 -0
  3. package/dist/channels.js +16 -3
  4. package/dist/decoder-policy.d.ts +20 -0
  5. package/dist/decoder-policy.js +34 -0
  6. package/dist/destinations.d.ts +5 -0
  7. package/dist/destinations.js +82 -0
  8. package/dist/library.d.ts +2 -0
  9. package/dist/library.js +4 -1
  10. package/dist/links.d.ts +4 -0
  11. package/dist/links.js +20 -2
  12. package/dist/live-interpreter.d.ts +1 -72
  13. package/dist/live-interpreter.js +1 -269
  14. package/dist/live-recovery.d.ts +1 -6
  15. package/dist/live-recovery.js +1 -11
  16. package/dist/live-voice.d.ts +6 -76
  17. package/dist/live-voice.js +6 -352
  18. package/dist/owner.js +4 -0
  19. package/dist/pcm-wav.d.ts +1 -2
  20. package/dist/pcm-wav.js +1 -27
  21. package/dist/playlist-input.d.ts +46 -0
  22. package/dist/playlist-input.js +158 -54
  23. package/dist/publish-choice.d.ts +21 -0
  24. package/dist/publish-choice.js +17 -0
  25. package/dist/server.js +66 -11
  26. package/dist/share.d.ts +1 -1
  27. package/dist/share.js +5 -4
  28. package/dist/shared-translation.d.ts +6 -53
  29. package/dist/shared-translation.js +7 -336
  30. package/dist/speaker-turns.d.ts +1 -32
  31. package/dist/speaker-turns.js +1 -30
  32. package/dist/speech.d.ts +3 -5
  33. package/dist/speech.js +4 -7
  34. package/dist/translation-passes.d.ts +5 -125
  35. package/dist/translation-passes.js +5 -294
  36. package/dist/upgrade-allowance.d.ts +1 -23
  37. package/dist/upgrade-allowance.js +1 -73
  38. package/dist/voice-profile.d.ts +1 -7
  39. package/dist/voice-profile.js +1 -53
  40. package/dist/voice-provider.d.ts +1 -9
  41. package/dist/voice-provider.js +1 -27
  42. package/package.json +2 -1
  43. package/src/audio.ts +3 -0
  44. package/src/channels.ts +26 -4
  45. package/src/decoder-policy.ts +33 -0
  46. package/src/destinations.ts +78 -0
  47. package/src/library.ts +6 -1
  48. package/src/links.ts +23 -4
  49. package/src/live-interpreter.ts +1 -213
  50. package/src/live-recovery.ts +1 -10
  51. package/src/live-voice.ts +6 -304
  52. package/src/owner.ts +3 -0
  53. package/src/pcm-wav.ts +1 -26
  54. package/src/playlist-input.ts +172 -50
  55. package/src/publish-choice.ts +35 -0
  56. package/src/server.ts +58 -11
  57. package/src/share.ts +5 -4
  58. package/src/shared-translation.ts +7 -222
  59. package/src/speaker-turns.ts +1 -34
  60. package/src/speech.ts +4 -5
  61. package/src/translation-passes.ts +6 -292
  62. package/src/upgrade-allowance.ts +1 -72
  63. package/src/voice-profile.ts +1 -46
  64. package/src/voice-provider.ts +1 -22
  65. package/web/dist/assets/{ar-CLNm1oA7.js → ar-D6_ZFDan.js} +1 -1
  66. package/web/dist/assets/{cs-Cptn8RbB.js → cs-DEPGOGEV.js} +1 -1
  67. package/web/dist/assets/{da-CRbNCFOu.js → da-CQw7SPC9.js} +1 -1
  68. package/web/dist/assets/{de-D4DnC5NQ.js → de-OWnobbah.js} +1 -1
  69. package/web/dist/assets/{es-DAHnD9bs.js → es-qMpyH9bM.js} +1 -1
  70. package/web/dist/assets/{fi-Csbl41Vz.js → fi-DpqqwJTD.js} +1 -1
  71. package/web/dist/assets/{fr-jZYQJed8.js → fr-BiO_ceen.js} +1 -1
  72. package/web/dist/assets/{hi-DjT9u0v_.js → hi-D-q5wZuY.js} +1 -1
  73. package/web/dist/assets/{hls-3VKVEQE3-BjAVnvGY.js → hls-3VKVEQE3-D2W4K8VR.js} +1 -1
  74. package/web/dist/assets/{hu-CVrU8gC4.js → hu-FIjHyhVC.js} +1 -1
  75. package/web/dist/assets/{id-B_gN24X5.js → id-gmB6lu56.js} +1 -1
  76. package/web/dist/assets/{index-BueBHHcv.css → index-Bo3utcDw.css} +1 -1
  77. package/web/dist/assets/index-UmGvzuun.js +22 -0
  78. package/web/dist/assets/{it-BUC1dMpV.js → it-Dxfo713A.js} +1 -1
  79. package/web/dist/assets/{mpegts-CnqmXTu2.js → mpegts-BT4XGr_E.js} +1 -1
  80. package/web/dist/assets/{mpegts-LO6RVLD6-B4QgG3Xb.js → mpegts-LO6RVLD6-CpZnlq0W.js} +1 -1
  81. package/web/dist/assets/{nl-CGi-aIrz.js → nl-mk5eN3Io.js} +1 -1
  82. package/web/dist/assets/{ru-YIbS63cK.js → ru-B2psU0kN.js} +1 -1
  83. package/web/dist/assets/{sv-CuJUpOCk.js → sv-Cg6waoNZ.js} +1 -1
  84. package/web/dist/assets/{uk-DLfG8YhV.js → uk-BcFkukLd.js} +1 -1
  85. package/web/dist/assets/{vi-nv2siY2T.js → vi-CnqIC1Sz.js} +1 -1
  86. package/web/dist/assets/{zh-P5x8w1OW.js → zh-CvnXIFvN.js} +1 -1
  87. package/web/dist/index.html +7 -12
  88. package/web/dist/sw.js +23 -23
  89. package/web/dist/assets/index-c3uxZS8_.js +0 -3
@@ -1,269 +1 @@
1
- import { audioError, transientAudioError } from "./live-recovery.js";
2
- import { encodeWav } from "./pcm-wav.js";
3
- /** Match provider labels across overlapping timestamps. A speaker absent from
4
- * the rolling context gets a new label; pitch alone never identifies someone. */
5
- export class SpeakerTracker {
6
- random;
7
- previous = [];
8
- sequence = 0;
9
- speakers = new Map();
10
- constructor(random = Math.random) {
11
- this.random = random;
12
- }
13
- reset() { this.previous = []; this.sequence = 0; this.speakers.clear(); }
14
- reconcile(turns, at, voices) {
15
- const links = new Map();
16
- const used = new Set();
17
- const candidates = [];
18
- const totals = new Map();
19
- for (const turn of turns)
20
- for (const old of this.previous) {
21
- const overlap = Math.min(at + turn.end * 1000, old.end) - Math.max(at + turn.start * 1000, old.start);
22
- if (overlap > 120) {
23
- const key = `${turn.speaker}|${old.speaker}`;
24
- totals.set(key, (totals.get(key) ?? 0) + overlap);
25
- }
26
- }
27
- for (const [key, overlap] of totals) {
28
- const [local, global] = key.split("|");
29
- candidates.push({ local: local, global: global, overlap });
30
- }
31
- for (const one of candidates.sort((a, b) => b.overlap - a.overlap)) {
32
- const speaker = this.speakers.get(one.global);
33
- if (speaker && !links.has(one.local) && !used.has(one.global)) {
34
- links.set(one.local, speaker);
35
- used.add(one.global);
36
- }
37
- }
38
- for (const turn of turns) {
39
- let speaker = links.get(turn.speaker);
40
- if (!speaker) {
41
- // Assign contrasting stock voices, without guessing a person's
42
- // gender from pitch or a noisy, short opening phrase.
43
- const taken = new Set([...this.speakers.values()].map(one => one.voice));
44
- const available = voices.filter(voice => !taken.has(voice.id));
45
- const pool = available.length ? available : voices;
46
- const voice = pool[Math.floor(this.random() * pool.length)];
47
- speaker = { id: `speaker-${++this.sequence}`, profile: turn.profile, voice: voice?.id ?? "auto" };
48
- this.speakers.set(speaker.id, speaker);
49
- links.set(turn.speaker, speaker);
50
- }
51
- }
52
- this.previous = turns.map(turn => ({ start: at + turn.start * 1000, end: at + turn.end * 1000, speaker: links.get(turn.speaker).id }));
53
- const present = new Set(this.previous.map(turn => turn.speaker));
54
- for (const key of this.speakers.keys())
55
- if (this.speakers.size > 64 && !present.has(key))
56
- this.speakers.delete(key);
57
- return links;
58
- }
59
- }
60
- /** Listener-local interpretation for the playing media. One request in flight and only the latest pending audio window. */
61
- export class Interpreter {
62
- options;
63
- tracker = new SpeakerTracker();
64
- generation = 0;
65
- pending = null;
66
- committedUntil = null;
67
- running = null;
68
- controller = null;
69
- translating = null;
70
- translationController = null;
71
- recognitionFailures = 0;
72
- translationFailures = 0;
73
- translations = [];
74
- constructor(options) {
75
- this.options = options;
76
- }
77
- reset() {
78
- this.generation++;
79
- this.controller?.abort();
80
- this.translationController?.abort();
81
- this.pending = null;
82
- this.running = null;
83
- this.translating = null;
84
- this.recognitionFailures = this.translationFailures = 0;
85
- this.translations = [];
86
- this.committedUntil = null;
87
- this.tracker.reset();
88
- }
89
- push(window) {
90
- this.committedUntil ??= window.freshAt;
91
- this.pending = window;
92
- if (this.running === null)
93
- void this.run(this.generation);
94
- }
95
- async json(path, init, signal) {
96
- const response = await (this.options.fetcher ?? fetch)(path, { ...init, signal });
97
- const body = await response.json();
98
- if (!response.ok)
99
- throw audioError(body.error || "Audio translation is unavailable.", response.status);
100
- return body;
101
- }
102
- async run(generation) {
103
- this.running = generation;
104
- try {
105
- while (this.pending && generation === this.generation) {
106
- const window = this.pending;
107
- this.pending = null;
108
- try {
109
- if (Date.now() - window.until > 12_000)
110
- continue;
111
- const controller = new AbortController();
112
- this.controller = controller;
113
- const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(12_000)]);
114
- const speakers = this.options.speakers(), target = this.options.language();
115
- const heard = await this.json(speakers ? "/api/v1/speech/speakers" : "/api/v1/speech/transcribe?live=1", {
116
- method: "POST", headers: { "content-type": "audio/wav" }, body: new Uint8Array(encodeWav(speakers ? window.samples : window.samples.slice(-80_000))),
117
- }, signal);
118
- if (generation !== this.generation)
119
- return;
120
- this.recognitionFailures = 0;
121
- const language = heard.language;
122
- let lines = [];
123
- if (speakers) {
124
- const links = this.tracker.reconcile(heard.turns ?? [], window.at, this.options.voices());
125
- // The watermark follows emitted words, not the newest capture
126
- // interval: an overlapping window can recover audio that arrived
127
- // while recognition/translation was busy. Keep unfinished phrases
128
- // in that overlap so short capture intervals do not split every
129
- // sentence (especially damaging when German changes word order).
130
- const cutoff = this.committedUntil ?? window.freshAt;
131
- const turns = heard.turns ?? [];
132
- for (const [index, turn] of turns.entries()) {
133
- const speaker = links.get(turn.speaker);
134
- const words = turn.words.filter(word => window.at + (word.start + word.end) * 500 >= cutoff &&
135
- (window.at + word.end * 1000 <= window.until - 100 || /[.!?。!?]$/.test(word.text)));
136
- let phrase = [];
137
- const emit = () => {
138
- if (!phrase.length)
139
- return;
140
- lines.push({ channel: this.options.channel(), at: window.at + phrase[0].start * 1000,
141
- until: window.at + phrase.at(-1).end * 1000, text: phrase.map(word => word.text).join(" ").trim(),
142
- language, speaker: speaker.id, voiceProfile: turn.profile });
143
- phrase = [];
144
- };
145
- for (const word of words) {
146
- if (phrase.length && word.start - phrase.at(-1).end >= 0.45)
147
- emit();
148
- phrase.push(word);
149
- if (/[.!?。!?]$/.test(word.text) || phrase.map(word => word.text).join(" ").length >= 400)
150
- emit();
151
- }
152
- if (phrase.length && (index < turns.length - 1 ||
153
- window.until - (window.at + phrase.at(-1).end * 1000) >= 350 ||
154
- window.until - (window.at + phrase[0].start * 1000) >= 3000))
155
- emit();
156
- }
157
- }
158
- else if (heard.text) {
159
- lines = [{ channel: this.options.channel(), at: window.freshAt, until: window.until, text: heard.text, language }];
160
- }
161
- if (!lines.length)
162
- continue;
163
- this.committedUntil = Math.max(this.committedUntil ?? window.freshAt, ...lines.map(line => line.until));
164
- // Drop expired work instead of turning off an otherwise healthy stream.
165
- this.translations = this.translations.filter(item => Date.now() - item.until <= 12_000).slice(-11);
166
- this.translations.push({ lines, language, target, until: window.until });
167
- // Recognition of the next audio window can proceed while the text
168
- // model translates this phrase. Voice synthesis is a third stage.
169
- if (this.translating === null)
170
- void this.translate(this.generation);
171
- }
172
- catch (error) {
173
- if (generation !== this.generation)
174
- return;
175
- if (!transientAudioError(error) || ++this.recognitionFailures >= 3)
176
- throw error;
177
- this.options.status("Speech recognition interrupted · listening for the next phrase…");
178
- }
179
- }
180
- }
181
- catch (error) {
182
- if (generation === this.generation) {
183
- const message = error instanceof Error ? error.message : "Live translation stopped.";
184
- this.reset();
185
- this.options.failed(message);
186
- this.options.status(message);
187
- }
188
- }
189
- finally {
190
- if (this.running === generation)
191
- this.running = null;
192
- }
193
- }
194
- async translate(generation) {
195
- this.translating = generation;
196
- try {
197
- while (this.translations.length && generation === this.generation) {
198
- const item = this.translations.shift();
199
- try {
200
- let { lines } = item;
201
- const { language, target, until } = item;
202
- if (Date.now() - until > 12_000)
203
- continue;
204
- const controller = new AbortController();
205
- this.translationController = controller;
206
- if (target && language !== target) {
207
- if (!language)
208
- throw audioError("The audio language could not be detected. Waiting for clearer speech.", 502);
209
- const translated = await this.json("/api/v1/translate?live=1", {
210
- method: "POST", headers: { "content-type": "application/json" },
211
- body: JSON.stringify({ texts: lines.map(line => line.text), from: language, to: target }),
212
- }, AbortSignal.any([controller.signal, AbortSignal.timeout(12_000)]));
213
- if (translated.texts.length !== lines.length || translated.texts.some(text => !text.trim()))
214
- throw audioError("Translation omitted a phrase. Waiting for the next phrase.", 502);
215
- lines = lines.map((line, i) => ({ ...line, original: line.text, sourceLanguage: language, language: target, text: translated.texts[i] }));
216
- }
217
- if (generation !== this.generation)
218
- return;
219
- if (Date.now() - until > 12_000)
220
- continue;
221
- this.translationFailures = 0;
222
- this.options.lines(lines.flatMap(line => splitCaption(line)));
223
- this.options.status(target ? `${language} → ${target} · live translation` : `Original audio language: ${language || "detecting…"}`);
224
- }
225
- catch (error) {
226
- if (generation !== this.generation)
227
- return;
228
- if (!transientAudioError(error) || ++this.translationFailures >= 3)
229
- throw error;
230
- this.options.status("Translation interrupted · catching up with live speech…");
231
- }
232
- }
233
- }
234
- catch (error) {
235
- if (generation === this.generation) {
236
- const message = error instanceof Error ? error.message : "Live translation stopped.";
237
- this.reset();
238
- this.options.failed(message);
239
- this.options.status(message);
240
- }
241
- }
242
- finally {
243
- if (this.translating === generation)
244
- this.translating = null;
245
- }
246
- }
247
- }
248
- /** Preserve all words while respecting the speech API's character limit. */
249
- export function splitCaption(line) {
250
- const text = line.text.trim();
251
- if (!text)
252
- return [];
253
- const parts = [];
254
- let rest = text;
255
- while (rest.length > 600) {
256
- const space = rest.lastIndexOf(" ", 600);
257
- const end = space > 300 ? space : 600;
258
- parts.push(rest.slice(0, end));
259
- rest = rest.slice(end).trimStart();
260
- }
261
- if (rest)
262
- parts.push(rest);
263
- let offset = 0;
264
- return parts.map(part => {
265
- const at = line.at + (line.until - line.at) * offset / text.length;
266
- offset += part.length + 1;
267
- return { ...line, text: part, at, until: Math.min(line.until, line.at + (line.until - line.at) * offset / text.length) };
268
- });
269
- }
1
+ export { Interpreter, SpeakerTracker, splitCaption } from "@profullstack/live-translate/core";
@@ -1,6 +1 @@
1
- /** Access, billing and throttling failures are terminal. Only transient service
2
- * and transport failures may recover, with a bounded consecutive failure count. */
3
- export declare function transientAudioError(error: unknown): boolean;
4
- export declare function audioError(message: string, status: number): Error & {
5
- status: number;
6
- };
1
+ export { audioError, transientAudioError } from "@profullstack/live-translate/core";
@@ -1,11 +1 @@
1
- /** Access, billing and throttling failures are terminal. Only transient service
2
- * and transport failures may recover, with a bounded consecutive failure count. */
3
- export function transientAudioError(error) {
4
- const status = error?.status;
5
- if (typeof status === "number")
6
- return [408, 502, 503, 504].includes(status);
7
- return error instanceof TypeError || (error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name));
8
- }
9
- export function audioError(message, status) {
10
- return Object.assign(new Error(message), { status });
11
- }
1
+ export { audioError, transientAudioError } from "@profullstack/live-translate/core";
@@ -1,77 +1,7 @@
1
- import { type SpeakerTranscript } from "./speaker-turns.ts";
2
- import type { VoiceProfile } from "./voice-profile.ts";
3
- import type { TranslationMeter } from "./translation-passes.ts";
4
- import type { Queryable } from "./follows.ts";
5
- export declare const LIVE_VOICE_MODEL = "eleven_flash_v2_5";
6
- export declare const LIVE_VOICE_RATE = 16000;
7
- export declare const LIVE_VOICE_LANGUAGES: Set<string>;
8
- export interface LiveVoiceChoice {
9
- id: string;
10
- name: string;
11
- gender: string;
12
- language: string;
13
- }
14
- export interface VoiceRequest {
15
- text: string;
16
- language: string;
17
- voice?: string;
18
- profile?: VoiceProfile;
19
- channel?: string;
20
- speaker?: string;
21
- }
22
- export declare class LiveVoice {
23
- private readonly key;
24
- private readonly fetcher;
25
- private readonly now;
26
- private catalog;
27
- private catalogUntil;
28
- private readonly cache;
29
- private readonly pending;
30
- private readonly usage;
31
- private readonly charsPerMinute;
32
- private readonly requests;
33
- private readonly grants;
34
- private readonly activeBy;
35
- private readonly db?;
36
- private readonly billing?;
37
- private schema;
38
- private readonly dailyChars;
39
- private cleanupAt;
40
- private readonly hearing;
41
- private readonly dailyAudioSeconds;
42
- private readonly userDailyChars;
43
- private readonly userDailyAudioSeconds;
44
- private providerPause;
45
- private checkProvider;
46
- private providerFailed;
47
- constructor(options?: {
48
- apiKey?: string;
49
- fetcher?: typeof fetch;
50
- now?: () => number;
51
- charsPerMinute?: number;
52
- dailyChars?: number;
53
- dailyAudioSeconds?: number;
54
- userDailyChars?: number;
55
- userDailyAudioSeconds?: number;
56
- db?: Queryable;
57
- billing?: TranslationMeter;
58
- });
59
- available(): boolean;
60
- /** Optional diarization, billed only while a signed-in listener requests it.
61
- * Rolling audio is bounded to 15 seconds, including overlap. Every second
62
- * submitted (also repeated context) consumes the persistent provider budget. */
63
- hear(bytes: Uint8Array, by: string, signal?: AbortSignal, meter?: TranslationMeter | undefined, resource?: string): Promise<SpeakerTranscript>;
64
- voices(): Promise<LiveVoiceChoice[]>;
65
- /** A 90-second capability for one channel, never the viewer's account credential. */
66
- grant(by: string, channel: string): Promise<{
67
- token: string;
68
- expires: number;
69
- }>;
70
- authorize(token: string, channel: string, chars: number): Promise<string>;
71
- private ensure;
72
- private reserve;
73
- private charge;
74
- private checkRequest;
75
- /** Stream the first request immediately; concurrent listeners share its cached result. */
76
- stream(ask: VoiceRequest, by: string, signal?: AbortSignal, meter?: TranslationMeter | undefined): Promise<Response>;
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, type LiveVoiceChoice, type VoiceRequest } from "@profullstack/live-translate";
5
+ export declare class LiveVoice extends Base {
6
+ constructor(options?: ConstructorParameters<typeof Base>[0]);
77
7
  }