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,27 +1,7 @@
1
- /** One live source/language pipeline, paid access for every listening account.
2
- * Browsers receive the same PCM, never upload a replacement for a public feed. */
3
- import { transientAudioError } from "./live-recovery.js";
4
- import { createHash, randomUUID } from "node:crypto";
5
- import { lookup } from "node:dns/promises";
6
- import { BlockList, isIP } from "node:net";
7
- import http from "node:http";
8
- import https from "node:https";
9
- import { spawn } from "node:child_process";
10
- import { Interpreter } from "./live-interpreter.js";
11
- import { LIVE_VOICE_LANGUAGES } from "./live-voice.js";
12
- import { SpeechError } from "./speech.js";
13
- const blocked = new BlockList();
14
- for (const [ip, bits] of [["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8], ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.0.2.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15], ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4]])
15
- blocked.addSubnet(ip, bits, "ipv4");
16
- const v6global = new BlockList();
17
- v6global.addSubnet("2000::", 3, "ipv6");
18
- blocked.addSubnet("2001:db8::", 32, "ipv6");
19
- blocked.addSubnet("2002::", 16, "ipv6");
20
- blocked.addSubnet("2001::", 32, "ipv6");
21
- export function publicStreamAddress(address) {
22
- const family = isIP(address);
23
- return family === 4 ? !blocked.check(address, "ipv4") : family === 6 && v6global.check(address, "ipv6") && !blocked.check(address, "ipv6");
24
- }
1
+ /** Shared live translation, from @profullstack/live-translate, limited to nixamp channels. */
2
+ import { SharedTranslations as Base, liveInput as baseLiveInput, SpeechError } from "@profullstack/live-translate";
3
+ export { publicStreamAddress } from "@profullstack/live-translate";
4
+ /** Only a live nixamp channel (`/api/channels/<id>`, optionally `?k=`). */
25
5
  export function sharedSource(raw) {
26
6
  let url;
27
7
  try {
@@ -37,316 +17,7 @@ export function sharedSource(raw) {
37
17
  url.searchParams.sort();
38
18
  return url;
39
19
  }
40
- /** Resolve once and pin that public address; no redirects or ffmpeg URL fetches. */
41
- export function liveInput(ffmpeg = ["ffmpeg"]) {
42
- return async (source, pcm, ended, signal) => {
43
- const hostname = source.hostname.replace(/^\[|\]$/g, "");
44
- const addresses = await lookup(hostname, { all: true });
45
- if (!addresses.length || addresses.some(one => !publicStreamAddress(one.address)))
46
- throw new SpeechError("This live source is not publicly reachable.", 400);
47
- signal.throwIfAborted();
48
- const selected = addresses[0];
49
- const child = spawn(ffmpeg[0], [...ffmpeg.slice(1), "-v", "error", "-nostats", "-protocol_whitelist", "pipe", "-readrate", "1", "-analyzeduration", "500000", "-probesize", "262144", "-i", "pipe:0", "-vn", "-ac", "1", "-ar", "16000", "-f", "s16le", "pipe:1"], { stdio: ["pipe", "pipe", "ignore"] });
50
- let stopped = false;
51
- const request = (source.protocol === "https:" ? https : http).get(source, {
52
- signal, headers: { "user-agent": "Nixamp shared translated audio", accept: "audio/*,video/*,application/octet-stream" },
53
- lookup: (_name, options, callback) => { if (typeof options === "object" && options.all)
54
- callback(null, [selected]);
55
- else
56
- callback(null, selected.address, selected.family); },
57
- });
58
- const stop = () => { if (stopped)
59
- return; stopped = true; request.destroy(); child.stdin.destroy(); child.kill("SIGKILL"); signal.removeEventListener("abort", stop); };
60
- const end = () => { if (!stopped) {
61
- stop();
62
- ended();
63
- } };
64
- signal.addEventListener("abort", stop, { once: true });
65
- child.stdout.on("data", pcm);
66
- child.on("error", end);
67
- child.on("close", end);
68
- child.stdin.on("error", end);
69
- request.on("response", response => {
70
- if (response.statusCode !== 200) {
71
- response.destroy();
72
- end();
73
- return;
74
- }
75
- response.on("error", end);
76
- response.on("end", end);
77
- response.pipe(child.stdin);
78
- });
79
- request.on("error", end);
80
- request.setTimeout(15_000, end);
81
- return { stop };
82
- };
83
- }
84
- export class SharedTranslations {
85
- options;
86
- feeds = new Map();
87
- constructor(options) {
88
- this.options = options;
89
- }
90
- get size() { return this.feeds.size; }
91
- async join(raw, language, by, send, close) {
92
- if (!this.options.voice.available())
93
- throw new SpeechError("Translated audio is unavailable.", 503);
94
- const source = sharedSource(raw);
95
- if (!LIVE_VOICE_LANGUAGES.has(language))
96
- throw new SpeechError("Choose a supported audio language.", 400);
97
- const id = `dub-${createHash("sha256").update(`${source}|${language}`).digest("hex").slice(0, 40)}`;
98
- const capacity = () => {
99
- const connections = [...this.feeds.values()].reduce((total, feed) => total + [...feed.members].filter(member => member.by === by).length, 0);
100
- if (connections >= 2)
101
- throw new SpeechError("Translated audio is already open in two players for this account.", 429);
102
- const feed = this.feeds.get(id);
103
- if (!feed && this.feeds.size >= 4)
104
- throw new SpeechError("Live translation is busy. Try again shortly.", 429);
105
- if (feed && feed.members.size >= 1000)
106
- throw new SpeechError("This translated stream is full. Try again shortly.", 429);
107
- };
108
- capacity();
109
- if (this.options.billing?.begin)
110
- await this.options.billing.begin(by, id);
111
- else
112
- await this.options.billing?.require(by, id);
113
- capacity(); // Other joins can finish during the account/database check.
114
- let feed = this.feeds.get(id);
115
- if (!feed) {
116
- feed = this.make(id, source, language);
117
- this.feeds.set(id, feed);
118
- const current = feed;
119
- current.heartbeat = setInterval(() => void this.checkMembers(current), 10_000);
120
- current.heartbeat.unref?.();
121
- }
122
- const member = { by, send, close };
123
- feed.members.add(member);
124
- this.broadcast(feed, { type: "status", listeners: feed.members.size });
125
- if (feed.members.size === 1 && !feed.input)
126
- void this.start(feed);
127
- const current = feed;
128
- return () => this.leave(current, member);
129
- }
130
- leave(feed, member) {
131
- if (!feed.members.delete(member))
132
- return;
133
- if (!feed.members.size) {
134
- if (feed.heartbeat)
135
- clearInterval(feed.heartbeat);
136
- feed.controller.abort();
137
- feed.input?.stop();
138
- feed.interpreter.reset();
139
- feed.queue = [];
140
- this.feeds.delete(feed.id);
141
- }
142
- else
143
- this.broadcast(feed, { type: "status", listeners: feed.members.size });
144
- }
145
- async checkMembers(feed) {
146
- const accounts = [...new Set([...feed.members].map(member => member.by))];
147
- let funded = new Set(accounts), message = "This free session ended and audio credit is used up. Start a remaining free session or buy a pass.";
148
- try {
149
- if (this.options.billing?.eligible)
150
- funded = new Set(await this.options.billing.eligible(accounts, feed.id));
151
- else if (this.options.billing) {
152
- const results = await Promise.allSettled(accounts.map(by => this.options.billing.require(by, feed.id)));
153
- funded = new Set(accounts.filter((_by, index) => results[index]?.status === "fulfilled"));
154
- }
155
- }
156
- catch {
157
- funded.clear();
158
- message = "Your translation access could not be checked. Try again shortly.";
159
- }
160
- for (const member of [...feed.members])
161
- if (accounts.includes(member.by) && !funded.has(member.by)) {
162
- try {
163
- member.send({ type: "error", error: message });
164
- }
165
- catch { /* disconnected */ }
166
- finally {
167
- this.leave(feed, member);
168
- member.close();
169
- }
170
- }
171
- }
172
- broadcast(feed, event) {
173
- for (const member of [...feed.members]) {
174
- try {
175
- member.send(event);
176
- }
177
- catch {
178
- this.leave(feed, member);
179
- member.close();
180
- }
181
- }
182
- }
183
- stop(feed, error, retryable = false) {
184
- this.broadcast(feed, { type: "error", error, retryable });
185
- for (const member of [...feed.members]) {
186
- this.leave(feed, member);
187
- member.close();
188
- }
189
- }
190
- make(id, source, language) {
191
- const feed = { id, source, language, members: new Set(), controller: new AbortController(), reservations: new Map(), queue: [], speaking: false, voiceFailures: 0, previous: Buffer.alloc(0), pending: Buffer.alloc(0), until: Date.now() };
192
- // Each account uses free access first, then the same published credit rate.
193
- // Additional listeners do not trigger recognition or synthesis.
194
- feed.meter = {
195
- require: async () => { if (!feed.members.size)
196
- throw new SpeechError("No listeners.", 410); },
197
- reserve: async (_by, kind, units) => {
198
- const ids = [];
199
- const accounts = [...new Set([...feed.members].map(member => member.by))];
200
- const accepted = new Set();
201
- const billing = this.options.billing;
202
- if (billing?.reserveMany) {
203
- for (const charged of await billing.reserveMany(accounts, kind, units, feed.id)) {
204
- ids.push(charged.id);
205
- accepted.add(charged.by);
206
- }
207
- }
208
- else {
209
- const results = await Promise.allSettled(accounts.map(async (account) => ({ by: account, id: await billing?.reserve(account, kind, units, feed.id) })));
210
- let failure;
211
- for (const result of results) {
212
- if (result.status === "fulfilled") {
213
- accepted.add(result.value.by);
214
- if (result.value.id)
215
- ids.push(result.value.id);
216
- }
217
- else if (!(result.reason instanceof SpeechError) || result.reason.status !== 402)
218
- failure = result.reason;
219
- }
220
- if (failure) {
221
- await Promise.all(ids.map(id => billing.refund(id)));
222
- throw failure;
223
- }
224
- }
225
- for (const member of [...feed.members])
226
- if (accounts.includes(member.by) && !accepted.has(member.by)) {
227
- try {
228
- member.send({ type: "error", error: "This free session ended and audio credit is used up. Start a remaining free session or buy a pass." });
229
- }
230
- catch { /* disconnected */ }
231
- finally {
232
- this.leave(feed, member);
233
- member.close();
234
- }
235
- }
236
- if (!feed.members.size) {
237
- await Promise.all(ids.map(id => this.options.billing.refund(id)));
238
- throw new SpeechError("No funded listeners.", 402);
239
- }
240
- const reservation = randomUUID();
241
- feed.reservations.set(reservation, ids);
242
- return reservation;
243
- },
244
- commit: async (id) => { const ids = feed.reservations.get(id) ?? []; if (this.options.billing?.commitMany)
245
- await this.options.billing.commitMany(ids);
246
- else
247
- await Promise.all(ids.map(id => this.options.billing.commit(id))); feed.reservations.delete(id); },
248
- refund: async (id) => { const ids = feed.reservations.get(id) ?? []; if (this.options.billing?.refundMany)
249
- await this.options.billing.refundMany(ids);
250
- else
251
- await Promise.all(ids.map(id => this.options.billing.refund(id))); feed.reservations.delete(id); },
252
- };
253
- let voices = [];
254
- feed.interpreter = new Interpreter({
255
- language: () => language, speakers: () => true, voices: () => voices, channel: () => id,
256
- status: text => { if (!feed.controller.signal.aborted && !feed.members.size)
257
- this.stop(feed, text); },
258
- failed: message => this.stop(feed, message),
259
- lines: lines => {
260
- feed.queue.push(...lines);
261
- feed.queue = feed.queue.filter(line => Date.now() - line.until <= 12_000).slice(-24);
262
- if (!feed.speaking)
263
- void this.speak(feed);
264
- },
265
- fetcher: (async (url, init) => {
266
- feed.controller.signal.throwIfAborted();
267
- const signal = AbortSignal.any([feed.controller.signal, ...(init?.signal ? [init.signal] : [])]);
268
- if (String(url).includes("/speakers")) {
269
- voices = await this.options.voice.voices();
270
- return Response.json(await this.options.voice.hear(new Uint8Array(init?.body), id, signal, feed.meter));
271
- }
272
- const body = JSON.parse(String(init?.body));
273
- return Response.json(await this.options.translator.translate(body.texts, body.from, body.to, { by: id, deadline: Date.now() + 10_000 }));
274
- }),
275
- });
276
- return feed;
277
- }
278
- async start(feed) {
279
- try {
280
- const input = await (this.options.open ?? liveInput())(feed.source, bytes => {
281
- if (feed.controller.signal.aborted)
282
- return;
283
- feed.pending = Buffer.concat([feed.pending, bytes]);
284
- while (feed.pending.length >= 64_000) {
285
- const fresh = feed.pending.subarray(0, 64_000);
286
- feed.pending = feed.pending.subarray(64_000);
287
- const pcm = Buffer.concat([feed.previous, fresh]);
288
- feed.previous = pcm.subarray(-128_000);
289
- feed.until += 2000;
290
- if (Math.abs(Date.now() - feed.until) > 2000)
291
- feed.until = Date.now();
292
- const samples = new Float32Array(pcm.length / 2);
293
- for (let index = 0; index < samples.length; index++)
294
- samples[index] = pcm.readInt16LE(index * 2) / 32768;
295
- feed.interpreter.push({ samples, at: feed.until - samples.length / 16, until: feed.until, freshAt: feed.until - 2000 });
296
- }
297
- }, () => this.stop(feed, "The live source disconnected.", true), feed.controller.signal);
298
- if (feed.controller.signal.aborted)
299
- input.stop();
300
- else
301
- feed.input = input;
302
- }
303
- catch (error) {
304
- this.stop(feed, error instanceof SpeechError ? error.message : "The live source could not be opened.");
305
- }
306
- }
307
- async speak(feed) {
308
- feed.speaking = true;
309
- try {
310
- while (feed.queue.length && !feed.controller.signal.aborted) {
311
- const line = feed.queue.shift();
312
- if (Date.now() - line.until > 12_000)
313
- continue;
314
- try {
315
- 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);
316
- const id = randomUUID();
317
- this.broadcast(feed, { type: "line", id, line });
318
- const reader = response.body.getReader();
319
- try {
320
- while (true) {
321
- const { done, value } = await reader.read();
322
- if (done)
323
- break;
324
- for (let at = 0; at < value.length; at += 16_384)
325
- this.broadcast(feed, { type: "audio", id, data: Buffer.from(value.subarray(at, at + 16_384)).toString("base64") });
326
- }
327
- }
328
- finally {
329
- reader.releaseLock();
330
- this.broadcast(feed, { type: "end", id });
331
- }
332
- feed.voiceFailures = 0;
333
- }
334
- catch (error) {
335
- if (feed.controller.signal.aborted)
336
- return;
337
- if (!transientAudioError(error) || ++feed.voiceFailures >= 3)
338
- throw error;
339
- // A provider failure skips this phrase; the source and other listeners
340
- // remain connected. Never replay partial speech or retry a charge.
341
- }
342
- }
343
- }
344
- catch (error) {
345
- if (!feed.controller.signal.aborted)
346
- this.stop(feed, error instanceof SpeechError ? error.message : "Translated audio was interrupted.");
347
- }
348
- finally {
349
- feed.speaking = false;
350
- }
351
- }
20
+ export function liveInput(ffmpeg = ["ffmpeg"]) { return baseLiveInput(ffmpeg, "Nixamp shared translated audio"); }
21
+ export class SharedTranslations extends Base {
22
+ constructor(options) { super({ parse: sharedSource, open: liveInput(), ...options }); }
352
23
  }
@@ -1,32 +1 @@
1
- /** Provider speaker IDs belong to a clip. Clients reconcile the overlapping
2
- * word timestamps before assigning a voice; these are not people's identities. */
3
- import { type Wav } from "./speech.ts";
4
- import { type VoiceProfile } from "./voice-profile.ts";
5
- export interface SpeakerTurn {
6
- start: number;
7
- end: number;
8
- text: string;
9
- speaker: string;
10
- profile: VoiceProfile;
11
- words: {
12
- text: string;
13
- start: number;
14
- end: number;
15
- }[];
16
- }
17
- export interface SpeakerTranscript {
18
- language: string;
19
- seconds: number;
20
- turns: SpeakerTurn[];
21
- }
22
- export interface ScribeResult {
23
- language_code?: string;
24
- words?: {
25
- text: string;
26
- start: number;
27
- end: number;
28
- type: string;
29
- speaker_id?: string | null;
30
- }[];
31
- }
32
- export declare function speakerTurns(body: ScribeResult, wav: Wav): SpeakerTranscript;
1
+ export { speakerTurns, type SpeakerTurn, type SpeakerTranscript, type ScribeResult } from "@profullstack/live-translate";
@@ -1,30 +1 @@
1
- /** Provider speaker IDs belong to a clip. Clients reconcile the overlapping
2
- * word timestamps before assigning a voice; these are not people's identities. */
3
- import { reliableText } from "./speech.js";
4
- import { voiceProfile } from "./voice-profile.js";
5
- const ISO = Object.fromEntries("eng:en spa:es deu:de ger:de fra:fr fre:fr por:pt ita:it nld:nl dut:nl swe:sv dan:da fin:fi rus:ru ukr:uk ces:cs cze:cs hun:hu cmn:zh zho:zh ara:ar hin:hi vie:vi ind:id jpn:ja kor:ko pol:pl tur:tr ron:ro bul:bg ell:el nor:no nob:no nno:no".split(" ").map(pair => pair.split(":")));
6
- export function speakerTurns(body, wav) {
7
- const seconds = wav.samples.length / wav.rate;
8
- const language = ISO[body.language_code ?? ""] ?? body.language_code ?? "";
9
- const turns = [];
10
- for (const word of (body.words ?? []).slice(0, 1500)) {
11
- if (word.type !== "word" || typeof word.text !== "string" || !Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end < word.start || word.end > seconds + 0.5)
12
- continue;
13
- const speaker = typeof word.speaker_id === "string" && /^[\w-]{1,80}$/.test(word.speaker_id) ? word.speaker_id : "unknown";
14
- const last = turns.at(-1);
15
- if (last && last.speaker === speaker && word.start - last.end < 0.8 && last.text.length + word.text.length < 400) {
16
- last.text += ` ${word.text}`;
17
- last.end = Math.max(last.end, word.end);
18
- last.words.push({ text: word.text, start: word.start, end: word.end });
19
- }
20
- else
21
- turns.push({ start: word.start, end: word.end, text: word.text, speaker, profile: "unknown", words: [{ text: word.text, start: word.start, end: word.end }] });
22
- }
23
- const pcm = Buffer.alloc(wav.samples.length * 2);
24
- wav.samples.forEach((sample, i) => pcm.writeInt16LE(Math.round(Math.max(-1, Math.min(1, sample)) * 32767), i * 2));
25
- for (const turn of turns) {
26
- turn.text = reliableText(turn.text, Math.max(1, turn.end - turn.start));
27
- turn.profile = voiceProfile(pcm.subarray(Math.floor(turn.start * 16000) * 2, Math.ceil(turn.end * 16000) * 2));
28
- }
29
- return { language, seconds, turns: turns.filter(turn => turn.text !== "") };
30
- }
1
+ export { speakerTurns } from "@profullstack/live-translate";
package/dist/speech.d.ts CHANGED
@@ -18,10 +18,9 @@ export declare const QUEUE_LIMIT = 8;
18
18
  export declare const DEFAULT_MODEL = "onnx-community/whisper-base";
19
19
  /** Stored live lines from older recognition settings must be heard again. */
20
20
  export declare const NATIVE_REVISION = "native-v2";
21
- export declare class SpeechError extends Error {
22
- readonly status: number;
23
- constructor(message: string, status: number);
24
- }
21
+ /** The package's class, so an error thrown inside @profullstack/live-translate
22
+ * is still `instanceof SpeechError` to the server's handlers. */
23
+ export { SpeechError } from "@profullstack/live-translate/core";
25
24
  /** A stretch of the clip and what was said in it, seconds from the clip's start. */
26
25
  export interface Segment {
27
26
  start: number;
@@ -160,4 +159,3 @@ export declare class Speech {
160
159
  deadline?: number;
161
160
  }): Promise<Heard>;
162
161
  }
163
- export {};
package/dist/speech.js CHANGED
@@ -49,13 +49,10 @@ export const QUEUE_LIMIT = 8;
49
49
  export const DEFAULT_MODEL = "onnx-community/whisper-base";
50
50
  /** Stored live lines from older recognition settings must be heard again. */
51
51
  export const NATIVE_REVISION = "native-v2";
52
- export class SpeechError extends Error {
53
- status;
54
- constructor(message, status) {
55
- super(message);
56
- this.status = status;
57
- }
58
- }
52
+ /** The package's class, so an error thrown inside @profullstack/live-translate
53
+ * is still `instanceof SpeechError` to the server's handlers. */
54
+ export { SpeechError } from "@profullstack/live-translate/core";
55
+ import { SpeechError } from "@profullstack/live-translate/core";
59
56
  /** Whether these bytes are a RIFF/WAVE file, whatever the request called them. */
60
57
  export function isWav(bytes) {
61
58
  if (bytes.length < 12)
@@ -1,126 +1,6 @@
1
- import type { Queryable } from "./follows.ts";
2
- import { UpgradeAllowances, type UpgradeAllowance } from "./upgrade-allowance.ts";
3
- export declare const TRANSLATION_MULTIPLIER = 5;
4
- export declare const TRANSLATION_PLANS: readonly [{
5
- readonly id: "day";
6
- readonly name: "Day";
7
- readonly days: 1;
8
- readonly priceCents: 100;
9
- }, {
10
- readonly id: "month";
11
- readonly name: "Month";
12
- readonly days: 30;
13
- readonly priceCents: 500;
14
- }];
15
- export type TranslationPlan = typeof TRANSLATION_PLANS[number];
16
- export type TranslationUsage = "transcription" | "voice";
17
- export interface TranslationMeter {
18
- begin?(by: string, resource: string): Promise<void>;
19
- require(by: string, resource?: string): Promise<void>;
20
- eligible?(accounts: string[], resource?: string): Promise<string[]>;
21
- reserve(by: string, kind: TranslationUsage, units: number, resource?: string): Promise<string>;
22
- reserveMany?(accounts: string[], kind: TranslationUsage, units: number, resource?: string): Promise<{
23
- by: string;
24
- id: string;
25
- }[]>;
26
- commitMany?(ids: string[]): Promise<void>;
27
- refundMany?(ids: string[]): Promise<void>;
28
- commit(id: string): Promise<void>;
29
- refund(id: string): Promise<void>;
30
- }
31
- export interface TranslationAccess {
32
- required: boolean;
33
- available: boolean;
34
- balanceMicros: number;
35
- expires: string | null;
36
- plans: readonly TranslationPlan[];
37
- coins: string[];
38
- orders: {
39
- id: string;
40
- url: string;
41
- plan: string;
42
- }[];
43
- rates: {
44
- transcriptionHour: number;
45
- voiceThousand: number;
46
- multiplier: number;
47
- };
48
- free: UpgradeAllowance;
49
- }
50
- /** At the published rates: Scribe $0.22/submitted hour, Flash $0.05/1k chars.
51
- * Transcription units are 16 kHz samples; overlap is charged exactly as sent. */
52
- export declare function translationCost(kind: TranslationUsage, units: number): {
53
- cost: number;
54
- charge: number;
55
- };
56
- export declare function verifyTranslationPayment(payment: Record<string, unknown>, id: string, priceCents: number): boolean;
57
- /** Which CoinPay key this deployment sells with. `COINPAY_API_KEY` is nixamp's
58
- * own business; `COINPAY_X402_KEY` is the older name, still honoured so a box
59
- * that has only it keeps selling. The own key wins, because the fleet-wide
60
- * x402 key belongs to another site's business and would book nixamp's sales
61
- * (and send its webhooks) there. */
62
- export declare function coinpayKeyFrom(env?: NodeJS.ProcessEnv): string;
63
- /** CoinPay signs `t=<unix>,v1=<hex HMAC-SHA256 of "t.rawBody">` and allows
64
- * 300 s of skew. Computed over the raw bytes: reserialising changes them. */
65
- export declare function verifyCoinpaySignature(raw: string, header: string | undefined, secret: string, nowMs?: number): boolean;
66
- /** CoinPay's webhook nests the payment under `data`, and its top-level `id` is
67
- * the EVENT (`evt_…`), not the payment. Read `data` first; the flat shape is
68
- * only the older test sender's. */
69
- export declare function readCoinpayWebhook(payload: unknown): {
70
- type: string;
71
- paymentId: string;
72
- status: string;
73
- };
74
- export declare class TranslationPasses implements TranslationMeter {
75
- private readonly options;
76
- private schema;
77
- private merchant;
78
- private merchantUntil;
79
- private readonly fetcher;
80
- private readonly now;
81
- readonly free: UpgradeAllowances;
82
- constructor(options: {
83
- db: Queryable;
84
- key: string;
85
- site: string;
86
- fetcher?: typeof fetch;
87
- now?: () => number;
88
- });
89
- private ensure;
90
- private provider;
91
- private settings;
92
- access(by?: string): Promise<TranslationAccess>;
93
- begin(by: string, resource: string): Promise<void>;
94
- require(by: string, resource?: string): Promise<void>;
95
- eligible(accounts: string[], resource?: string): Promise<string[]>;
96
- reserve(by: string, kind: TranslationUsage, units: number, resource?: string): Promise<string>;
97
- /** Batch a shared stream's access charges in one database round trip. */
98
- reserveMany(accounts: string[], kind: TranslationUsage, units: number, resource?: string): Promise<{
99
- by: string;
100
- id: string;
101
- }[]>;
102
- commit(id: string): Promise<void>;
103
- refund(id: string): Promise<void>;
104
- commitMany(ids: string[]): Promise<void>;
105
- refundMany(ids: string[]): Promise<void>;
106
- checkout(by: string, planId: string, coin: string, requestKey: string): Promise<{
107
- id: string;
108
- url: string;
109
- }>;
110
- /**
111
- * A CoinPay webhook only says "look again". The order is settled by asking
112
- * CoinPay, exactly as a returning buyer's check does, so a replayed or
113
- * stale body cannot credit anything and a second delivery (confirmed, then
114
- * forwarded) credits nothing twice. A payment that is not ours is answered,
115
- * not failed: CoinPay would otherwise retry it forever.
116
- */
117
- settle(paymentId: string): Promise<{
118
- settled: boolean;
119
- status?: string;
120
- reason?: string;
121
- }>;
122
- check(by: string, id: string): Promise<{
123
- status: string;
124
- access: TranslationAccess;
125
- }>;
1
+ /** Prepaid translation credit, from @profullstack/live-translate, sold as nixamp. */
2
+ import { TranslationPasses as Base } from "@profullstack/live-translate";
3
+ export { TRANSLATION_MULTIPLIER, TRANSLATION_PLANS, translationCost, verifyTranslationPayment, coinpayKeyFrom, verifyCoinpaySignature, readCoinpayWebhook, type TranslationPlan, type TranslationUsage, type TranslationMeter, type TranslationAccess } from "@profullstack/live-translate";
4
+ export declare class TranslationPasses extends Base {
5
+ constructor(options: ConstructorParameters<typeof Base>[0]);
126
6
  }