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.
Files changed (42) hide show
  1. package/README.md +39 -16
  2. package/backtoschool/dist/assets/{index-OE2xWZUH.css → index-Bot4FEXQ.css} +1 -1
  3. package/backtoschool/dist/assets/index-Cjai7LgT.js +93 -0
  4. package/backtoschool/dist/index.html +13 -2
  5. package/dist/event-writer.d.ts +39 -0
  6. package/dist/event-writer.js +139 -0
  7. package/dist/live-api.d.ts +2 -0
  8. package/dist/live-api.js +37 -2
  9. package/dist/live-interpreter.d.ts +3 -1
  10. package/dist/live-interpreter.js +109 -83
  11. package/dist/live-recovery.d.ts +6 -0
  12. package/dist/live-recovery.js +11 -0
  13. package/dist/mail-sender.d.ts +9 -0
  14. package/dist/mail-sender.js +11 -0
  15. package/dist/notify.d.ts +3 -3
  16. package/dist/notify.js +8 -5
  17. package/dist/password-reset.d.ts +2 -3
  18. package/dist/password-reset.js +4 -2
  19. package/dist/server.d.ts +2 -0
  20. package/dist/server.js +13 -1
  21. package/dist/shared-translation.d.ts +1 -0
  22. package/dist/shared-translation.js +32 -23
  23. package/package.json +1 -1
  24. package/src/event-writer.ts +126 -0
  25. package/src/live-api.ts +26 -2
  26. package/src/live-interpreter.ts +87 -68
  27. package/src/live-recovery.ts +10 -0
  28. package/src/mail-sender.ts +12 -0
  29. package/src/notify.ts +10 -7
  30. package/src/password-reset.ts +5 -5
  31. package/src/server.ts +14 -1
  32. package/src/shared-translation.ts +27 -19
  33. package/web/dist/assets/background.worker-Hz7oaQ8X.js +547 -0
  34. package/web/dist/assets/{hls-3VKVEQE3-CAL4qBEq.js → hls-3VKVEQE3-_BiJh76x.js} +1 -1
  35. package/web/dist/assets/index-BgLxkXKe.js +3 -0
  36. package/web/dist/assets/{mpegts-H9oC2fAK.js → mpegts-CV0glck7.js} +1 -1
  37. package/web/dist/assets/{mpegts-LO6RVLD6-Bkd9Sqr6.js → mpegts-LO6RVLD6-iuTMg-O6.js} +1 -1
  38. package/web/dist/index.html +1 -1
  39. package/web/dist/sw.js +6 -6
  40. package/backtoschool/dist/assets/index-pQ_uH4bZ.js +0 -93
  41. package/web/dist/assets/background.worker-DH8ag5Q4.js +0 -547
  42. 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: () => this.stop(feed, "Live translation stopped. Enable it again to retry."),
167
+ failed: message => this.stop(feed, message),
167
168
  lines: lines => {
168
169
  feed.queue.push(...lines);
169
- if (feed.queue.length > 24) { this.stop(feed, "Live translation fell behind. Enable it again to retry."); return; }
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 stopped. Enable translated audio again when playback resumes."), feed.controller.signal);
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) throw new SpeechError("Live translation fell behind. Enable it again to retry.", 503);
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
- while (true) {
215
- const { done, value } = await reader.read(); if (done) break;
216
- 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") });
217
- }
218
- } finally { reader.releaseLock(); }
219
- this.broadcast(feed, { type: "end", id });
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; }