nixamp 0.28.43 → 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.
- package/README.md +8 -3
- package/dist/accounts.js +9 -1
- package/dist/live-interpreter.d.ts +1 -72
- package/dist/live-interpreter.js +1 -269
- package/dist/live-recovery.d.ts +1 -6
- package/dist/live-recovery.js +1 -11
- package/dist/live-voice.d.ts +6 -76
- package/dist/live-voice.js +6 -352
- package/dist/owner.js +4 -0
- package/dist/paywall.js +1 -1
- package/dist/pcm-wav.d.ts +1 -2
- package/dist/pcm-wav.js +1 -27
- package/dist/server.d.ts +16 -0
- package/dist/server.js +77 -6
- package/dist/shared-translation.d.ts +6 -53
- package/dist/shared-translation.js +7 -336
- package/dist/speaker-turns.d.ts +1 -32
- package/dist/speaker-turns.js +1 -30
- package/dist/speech.d.ts +3 -5
- package/dist/speech.js +4 -7
- package/dist/tickets.js +1 -1
- package/dist/translation-passes.d.ts +5 -96
- package/dist/translation-passes.js +5 -236
- package/dist/upgrade-allowance.d.ts +1 -23
- package/dist/upgrade-allowance.js +1 -73
- package/dist/voice-profile.d.ts +1 -7
- package/dist/voice-profile.js +1 -53
- package/dist/voice-provider.d.ts +1 -9
- package/dist/voice-provider.js +1 -27
- package/package.json +4 -2
- package/src/accounts.ts +9 -1
- package/src/live-interpreter.ts +1 -213
- package/src/live-recovery.ts +1 -10
- package/src/live-voice.ts +6 -304
- package/src/owner.ts +3 -0
- package/src/paywall.ts +1 -1
- package/src/pcm-wav.ts +1 -26
- package/src/server.ts +57 -6
- package/src/shared-translation.ts +7 -222
- package/src/speaker-turns.ts +1 -34
- package/src/speech.ts +4 -5
- package/src/tickets.ts +1 -1
- package/src/translation-passes.ts +6 -239
- package/src/types/auth-system.d.ts +12 -3
- package/src/upgrade-allowance.ts +1 -72
- package/src/voice-profile.ts +1 -46
- package/src/voice-provider.ts +1 -22
- package/web/dist/.well-known/openwebring.json +6 -0
- package/web/dist/assets/{ar-CR2NEW77.js → ar-8fDonUoB.js} +1 -1
- package/web/dist/assets/{cs-ivXHTYBK.js → cs-DCClolTy.js} +1 -1
- package/web/dist/assets/{da-DQEGAEdk.js → da-C3RPhXUd.js} +1 -1
- package/web/dist/assets/{de-B5VaSv3O.js → de-CYBoEomk.js} +1 -1
- package/web/dist/assets/{es-DUTtfGnk.js → es-DS3ls4xT.js} +1 -1
- package/web/dist/assets/{fi-CzQZtpO5.js → fi-DbCZpO89.js} +1 -1
- package/web/dist/assets/{fr-C2g4DUBr.js → fr-D0z3UpdL.js} +1 -1
- package/web/dist/assets/{hi-DKB7ZsZ8.js → hi-BTU3sRlQ.js} +1 -1
- package/web/dist/assets/{hls-3VKVEQE3-Bc75Z5iH.js → hls-3VKVEQE3-CQXTVHn_.js} +1 -1
- package/web/dist/assets/{hu--oH7WvAR.js → hu-B37Q8ldT.js} +1 -1
- package/web/dist/assets/{id-CjlpBIJc.js → id-4pdXNa4G.js} +1 -1
- package/web/dist/assets/index-BDBbN5Qy.js +22 -0
- package/web/dist/assets/{it-Dbctup0F.js → it-CTe69-UY.js} +1 -1
- package/web/dist/assets/{mpegts-D9VECdKk.js → mpegts-BBmkDppm.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CSb7Fz4U.js → mpegts-LO6RVLD6-CUVdjYFi.js} +1 -1
- package/web/dist/assets/{nl-BYw8ZbV_.js → nl-CxrmHrMn.js} +1 -1
- package/web/dist/assets/{ru-DFGYCrj-.js → ru-zjVCfIKq.js} +1 -1
- package/web/dist/assets/{sv-DbO1Sc1v.js → sv-CEoiT2Lr.js} +1 -1
- package/web/dist/assets/{uk-BTXHwsjV.js → uk-ZC6JzQvD.js} +1 -1
- package/web/dist/assets/{vi-DJ-vIPLS.js → vi-D9anF-cl.js} +1 -1
- package/web/dist/assets/{zh-D3Gju_Y2.js → zh-BeLYdC0I.js} +1 -1
- package/web/dist/index.html +6 -5
- package/web/dist/sw.js +23 -22
- package/web/dist/assets/index-CF04ablL.js +0 -3
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
41
|
-
export
|
|
42
|
-
|
|
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
|
}
|
package/dist/speaker-turns.d.ts
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
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";
|
package/dist/speaker-turns.js
CHANGED
|
@@ -1,30 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
package/dist/tickets.js
CHANGED
|
@@ -145,7 +145,7 @@ export class Tickets {
|
|
|
145
145
|
}
|
|
146
146
|
/** Read a Tickets configuration out of the environment. */
|
|
147
147
|
export function ticketsFromEnv(site, env = process.env) {
|
|
148
|
-
const coinpayKey = env["COINPAY_X402_KEY"]
|
|
148
|
+
const coinpayKey = env["COINPAY_API_KEY"] || env["COINPAY_X402_KEY"] || "";
|
|
149
149
|
if (!coinpayKey)
|
|
150
150
|
return null;
|
|
151
151
|
return {
|
|
@@ -1,97 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
export
|
|
4
|
-
export declare
|
|
5
|
-
|
|
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
|
-
export declare class TranslationPasses implements TranslationMeter {
|
|
58
|
-
private readonly options;
|
|
59
|
-
private schema;
|
|
60
|
-
private merchant;
|
|
61
|
-
private merchantUntil;
|
|
62
|
-
private readonly fetcher;
|
|
63
|
-
private readonly now;
|
|
64
|
-
readonly free: UpgradeAllowances;
|
|
65
|
-
constructor(options: {
|
|
66
|
-
db: Queryable;
|
|
67
|
-
key: string;
|
|
68
|
-
site: string;
|
|
69
|
-
fetcher?: typeof fetch;
|
|
70
|
-
now?: () => number;
|
|
71
|
-
});
|
|
72
|
-
private ensure;
|
|
73
|
-
private provider;
|
|
74
|
-
private settings;
|
|
75
|
-
access(by?: string): Promise<TranslationAccess>;
|
|
76
|
-
begin(by: string, resource: string): Promise<void>;
|
|
77
|
-
require(by: string, resource?: string): Promise<void>;
|
|
78
|
-
eligible(accounts: string[], resource?: string): Promise<string[]>;
|
|
79
|
-
reserve(by: string, kind: TranslationUsage, units: number, resource?: string): Promise<string>;
|
|
80
|
-
/** Batch a shared stream's access charges in one database round trip. */
|
|
81
|
-
reserveMany(accounts: string[], kind: TranslationUsage, units: number, resource?: string): Promise<{
|
|
82
|
-
by: string;
|
|
83
|
-
id: string;
|
|
84
|
-
}[]>;
|
|
85
|
-
commit(id: string): Promise<void>;
|
|
86
|
-
refund(id: string): Promise<void>;
|
|
87
|
-
commitMany(ids: string[]): Promise<void>;
|
|
88
|
-
refundMany(ids: string[]): Promise<void>;
|
|
89
|
-
checkout(by: string, planId: string, coin: string, requestKey: string): Promise<{
|
|
90
|
-
id: string;
|
|
91
|
-
url: string;
|
|
92
|
-
}>;
|
|
93
|
-
check(by: string, id: string): Promise<{
|
|
94
|
-
status: string;
|
|
95
|
-
access: TranslationAccess;
|
|
96
|
-
}>;
|
|
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]);
|
|
97
6
|
}
|