nixamp 0.27.2 → 0.27.3
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 +39 -16
- package/backtoschool/dist/assets/{index-OE2xWZUH.css → index-Bot4FEXQ.css} +1 -1
- package/backtoschool/dist/assets/index-Cjai7LgT.js +93 -0
- package/backtoschool/dist/index.html +13 -2
- package/dist/event-writer.d.ts +39 -0
- package/dist/event-writer.js +139 -0
- package/dist/live-api.d.ts +2 -0
- package/dist/live-api.js +37 -2
- package/dist/live-interpreter.d.ts +3 -1
- package/dist/live-interpreter.js +109 -83
- package/dist/live-recovery.d.ts +6 -0
- package/dist/live-recovery.js +11 -0
- package/dist/mail-sender.d.ts +9 -0
- package/dist/mail-sender.js +11 -0
- package/dist/notify.d.ts +3 -3
- package/dist/notify.js +8 -5
- package/dist/password-reset.d.ts +2 -3
- package/dist/password-reset.js +4 -2
- package/dist/server.d.ts +2 -0
- package/dist/server.js +13 -1
- package/dist/shared-translation.d.ts +1 -0
- package/dist/shared-translation.js +32 -23
- package/package.json +1 -1
- package/src/event-writer.ts +126 -0
- package/src/live-api.ts +26 -2
- package/src/live-interpreter.ts +87 -68
- package/src/live-recovery.ts +10 -0
- package/src/mail-sender.ts +12 -0
- package/src/notify.ts +10 -7
- package/src/password-reset.ts +5 -5
- package/src/server.ts +14 -1
- package/src/shared-translation.ts +27 -19
- package/web/dist/assets/background.worker-Hz7oaQ8X.js +547 -0
- package/web/dist/assets/{hls-3VKVEQE3-CAL4qBEq.js → hls-3VKVEQE3-_BiJh76x.js} +1 -1
- package/web/dist/assets/index-BgLxkXKe.js +3 -0
- package/web/dist/assets/{mpegts-H9oC2fAK.js → mpegts-CV0glck7.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-Bkd9Sqr6.js → mpegts-LO6RVLD6-iuTMg-O6.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +6 -6
- package/backtoschool/dist/assets/index-pQ_uH4bZ.js +0 -93
- package/web/dist/assets/background.worker-DH8ag5Q4.js +0 -547
- package/web/dist/assets/index-CLeEWlms.js +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** One live source/language pipeline, paid access for every listening account.
|
|
2
2
|
* Browsers receive the same PCM, never upload a replacement for a public feed. */
|
|
3
|
+
import { transientAudioError } from "./live-recovery.ts";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { lookup } from "node:dns/promises";
|
|
5
6
|
import { BlockList, isIP } from "node:net";
|
|
@@ -58,9 +59,9 @@ export function liveInput(ffmpeg: string[] = ["ffmpeg"]): OpenLiveInput {
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export type SharedEvent = { type: "status"; listeners: number } | { type: "line"; id: string; line: Caption } | { type: "audio"; id: string; data: string } | { type: "end"; id: string } | { type: "error"; error: string };
|
|
62
|
+
export type SharedEvent = { type: "status"; listeners: number } | { type: "line"; id: string; line: Caption } | { type: "audio"; id: string; data: string } | { type: "end"; id: string } | { type: "error"; error: string; retryable?: boolean };
|
|
62
63
|
type Member = { by: string; send: (event: SharedEvent) => void; close: () => void };
|
|
63
|
-
interface Feed { id: string; source: URL; language: string; members: Set<Member>; controller: AbortController; input?: LiveInput; interpreter: Interpreter; meter: TranslationMeter; reservations: Map<string, string[]>; queue: Caption[]; speaking: boolean; previous: Buffer; pending: Buffer; until: number; heartbeat?: ReturnType<typeof setInterval>; }
|
|
64
|
+
interface Feed { id: string; source: URL; language: string; members: Set<Member>; controller: AbortController; input?: LiveInput; interpreter: Interpreter; meter: TranslationMeter; reservations: Map<string, string[]>; queue: Caption[]; speaking: boolean; voiceFailures: number; previous: Buffer; pending: Buffer; until: number; heartbeat?: ReturnType<typeof setInterval>; }
|
|
64
65
|
|
|
65
66
|
export class SharedTranslations {
|
|
66
67
|
private readonly feeds = new Map<string, Feed>();
|
|
@@ -123,12 +124,12 @@ export class SharedTranslations {
|
|
|
123
124
|
try { member.send(event); } catch { this.leave(feed, member); member.close(); }
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
private stop(feed: Feed, error: string): void {
|
|
127
|
-
this.broadcast(feed, { type: "error", error });
|
|
127
|
+
private stop(feed: Feed, error: string, retryable = false): void {
|
|
128
|
+
this.broadcast(feed, { type: "error", error, retryable });
|
|
128
129
|
for (const member of [...feed.members]) { this.leave(feed, member); member.close(); }
|
|
129
130
|
}
|
|
130
131
|
private make(id: string, source: URL, language: string): Feed {
|
|
131
|
-
const feed = { id, source, language, members: new Set<Member>(), controller: new AbortController(), reservations: new Map<string, string[]>(), queue: [], speaking: false, previous: Buffer.alloc(0), pending: Buffer.alloc(0), until: Date.now() } as unknown as Feed;
|
|
132
|
+
const feed = { id, source, language, members: new Set<Member>(), controller: new AbortController(), reservations: new Map<string, string[]>(), queue: [], speaking: false, voiceFailures: 0, previous: Buffer.alloc(0), pending: Buffer.alloc(0), until: Date.now() } as unknown as Feed;
|
|
132
133
|
// Each account uses free access first, then the same published credit rate.
|
|
133
134
|
// Additional listeners do not trigger recognition or synthesis.
|
|
134
135
|
feed.meter = {
|
|
@@ -163,10 +164,10 @@ export class SharedTranslations {
|
|
|
163
164
|
feed.interpreter = new Interpreter({
|
|
164
165
|
language: () => language, speakers: () => true, voices: () => voices, channel: () => id,
|
|
165
166
|
status: text => { if (!feed.controller.signal.aborted && !feed.members.size) this.stop(feed, text); },
|
|
166
|
-
failed:
|
|
167
|
+
failed: message => this.stop(feed, message),
|
|
167
168
|
lines: lines => {
|
|
168
169
|
feed.queue.push(...lines);
|
|
169
|
-
|
|
170
|
+
feed.queue = feed.queue.filter(line => Date.now() - line.until <= 12_000).slice(-24);
|
|
170
171
|
if (!feed.speaking) void this.speak(feed);
|
|
171
172
|
},
|
|
172
173
|
fetcher: (async (url, init) => {
|
|
@@ -196,7 +197,7 @@ export class SharedTranslations {
|
|
|
196
197
|
for (let index = 0; index < samples.length; index++) samples[index] = pcm.readInt16LE(index * 2) / 32768;
|
|
197
198
|
feed.interpreter.push({ samples, at: feed.until - samples.length / 16, until: feed.until, freshAt: feed.until - 2000 });
|
|
198
199
|
}
|
|
199
|
-
}, () => this.stop(feed, "The live source
|
|
200
|
+
}, () => this.stop(feed, "The live source disconnected.", true), feed.controller.signal);
|
|
200
201
|
if (feed.controller.signal.aborted) input.stop(); else feed.input = input;
|
|
201
202
|
} catch (error) { this.stop(feed, error instanceof SpeechError ? error.message : "The live source could not be opened."); }
|
|
202
203
|
}
|
|
@@ -205,18 +206,25 @@ export class SharedTranslations {
|
|
|
205
206
|
try {
|
|
206
207
|
while (feed.queue.length && !feed.controller.signal.aborted) {
|
|
207
208
|
const line = feed.queue.shift()!;
|
|
208
|
-
if (Date.now() - line.until > 12_000)
|
|
209
|
-
const response = await this.options.voice.stream({ text: line.text, language: feed.language, channel: feed.id, speaker: line.speaker, voice: feed.interpreter.tracker.speakers.get(line.speaker ?? "")?.voice }, feed.id, feed.controller.signal, feed.meter);
|
|
210
|
-
const id = randomUUID();
|
|
211
|
-
this.broadcast(feed, { type: "line", id, line });
|
|
212
|
-
const reader = response.body!.getReader();
|
|
209
|
+
if (Date.now() - line.until > 12_000) continue;
|
|
213
210
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
const response = await this.options.voice.stream({ text: line.text, language: feed.language, channel: feed.id, speaker: line.speaker, voice: feed.interpreter.tracker.speakers.get(line.speaker ?? "")?.voice }, feed.id, feed.controller.signal, feed.meter);
|
|
212
|
+
const id = randomUUID();
|
|
213
|
+
this.broadcast(feed, { type: "line", id, line });
|
|
214
|
+
const reader = response.body!.getReader();
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
const { done, value } = await reader.read(); if (done) break;
|
|
218
|
+
for (let at = 0; at < value.length; at += 16_384) this.broadcast(feed, { type: "audio", id, data: Buffer.from(value.subarray(at, at + 16_384)).toString("base64") });
|
|
219
|
+
}
|
|
220
|
+
} finally { reader.releaseLock(); this.broadcast(feed, { type: "end", id }); }
|
|
221
|
+
feed.voiceFailures = 0;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (feed.controller.signal.aborted) return;
|
|
224
|
+
if (!transientAudioError(error) || ++feed.voiceFailures >= 3) throw error;
|
|
225
|
+
// A provider failure skips this phrase; the source and other listeners
|
|
226
|
+
// remain connected. Never replay partial speech or retry a charge.
|
|
227
|
+
}
|
|
220
228
|
}
|
|
221
229
|
} catch (error) { if (!feed.controller.signal.aborted) this.stop(feed, error instanceof SpeechError ? error.message : "Translated audio was interrupted."); }
|
|
222
230
|
finally { feed.speaking = false; }
|