nixamp 0.67.3 → 0.67.5
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/dist/ai-label.js +8 -7
- package/dist/paywall.d.ts +13 -0
- package/dist/paywall.js +28 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +14 -1
- package/dist/veo-studio.d.ts +6 -0
- package/dist/veo-studio.js +68 -36
- package/package.json +1 -1
- package/src/ai-label.ts +8 -7
- package/src/paywall.ts +31 -0
- package/src/server.ts +17 -0
- package/src/veo-studio.ts +62 -30
- package/web/dist/assets/{ar-DxGw3Rs5.js → ar-BREzwKYv.js} +1 -1
- package/web/dist/assets/{cs-CvhFUJ-M.js → cs-DUVh2e_f.js} +1 -1
- package/web/dist/assets/{da-DNNaWmG1.js → da-BV7puYd_.js} +1 -1
- package/web/dist/assets/{de-Bd73nJDj.js → de-DuJvzIkT.js} +1 -1
- package/web/dist/assets/{es-kql7koI9.js → es-B82DV18M.js} +1 -1
- package/web/dist/assets/{fi-DOSVk45m.js → fi-C9Sbcfm5.js} +1 -1
- package/web/dist/assets/{fr-C-PWAVGY.js → fr-BLzdVInb.js} +1 -1
- package/web/dist/assets/{hi-DSccyns6.js → hi-Bry2gSsk.js} +1 -1
- package/web/dist/assets/{hu-nhOx8KN8.js → hu-BhFqISxK.js} +1 -1
- package/web/dist/assets/{id-B_7XcFMO.js → id-Wa5i1bzz.js} +1 -1
- package/web/dist/assets/{it-DS_wAQLZ.js → it-CNl6O9Gb.js} +1 -1
- package/web/dist/assets/{main-DBqNZR5i.js → main-DZilIsh4.js} +2 -2
- package/web/dist/assets/{nl-E4kRa4hT.js → nl-Bhe9R9t9.js} +1 -1
- package/web/dist/assets/{ru-BBTDvHvj.js → ru-3HuDrBeM.js} +1 -1
- package/web/dist/assets/{sv-eb2QkbPB.js → sv-ChxUm_Ff.js} +1 -1
- package/web/dist/assets/{uk-D8ewz76c.js → uk-CtgnVIti.js} +1 -1
- package/web/dist/assets/{vi-DjxETiKX.js → vi-DSuEsy5l.js} +1 -1
- package/web/dist/assets/{zh-pjVIcKxb.js → zh-DHGXXu3t.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +19 -19
package/dist/ai-label.js
CHANGED
|
@@ -535,13 +535,14 @@ const SHOTS = [
|
|
|
535
535
|
"Slow aerial push over the crowd toward the stage, fans with hands up, the stage tiny in the distance",
|
|
536
536
|
];
|
|
537
537
|
// The singer's close-ups, which are lip-synced to the song afterwards (fal.ai). The mouth
|
|
538
|
-
// must be clear: whatever covers it
|
|
539
|
-
//
|
|
538
|
+
// must be clear: whatever covers it is smeared into the face when the mouth is redrawn,
|
|
539
|
+
// and Veo puts a microphone in front of it whenever there is one in the scene, however
|
|
540
|
+
// the prompt asks for it held low. So there is none: the singer sings in the story's world.
|
|
540
541
|
const CLOSE_UPS = [
|
|
541
|
-
"
|
|
542
|
-
"
|
|
543
|
-
"
|
|
544
|
-
"Close-up of the lead vocalist singing in
|
|
542
|
+
"Close-up of the lead vocalist singing straight to camera at night in the rain on a neon-lit street, face and mouth fully lit, no microphone anywhere",
|
|
543
|
+
"Medium close-up of the lead vocalist singing to camera from the driver's seat of a car at dusk, window down, wind in the hair, no microphone",
|
|
544
|
+
"Close-up of the lead vocalist singing on an open road at golden hour, the camera slowly circling, face to camera, no microphone",
|
|
545
|
+
"Close-up of the lead vocalist singing in a dark room lit by one warm lamp, head and shoulders, face to camera, no microphone",
|
|
545
546
|
];
|
|
546
547
|
/**
|
|
547
548
|
* The prompts for a single's filmed shots. The people are the reference
|
|
@@ -551,7 +552,7 @@ const CLOSE_UPS = [
|
|
|
551
552
|
export function veoPrompts(artist, song, members, count, kind = "band") {
|
|
552
553
|
const cast = members.slice(0, 3).map((one, i) => `the ${one.role} from reference image ${i + 1} (${one.look.replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "")})`).join("; ");
|
|
553
554
|
const rule = kind === "singer"
|
|
554
|
-
? "The vocalist sings clearly, the whole mouth always visible
|
|
555
|
+
? "The vocalist sings clearly to camera, the whole mouth always visible. There is no microphone, mic stand or instrument anywhere in the shot, and nothing in front of the face. Cinematic, 35mm film look. No text, no logos."
|
|
555
556
|
: "The lead vocalist is not in this shot, and nobody sings, speaks or lip-syncs on camera. Cinematic, 35mm film look, dynamic camera. No text, no logos.";
|
|
556
557
|
return (kind === "singer" ? CLOSE_UPS : SHOTS).slice(0, count).map((shot) => [
|
|
557
558
|
`Music video for a fictional ${artist.genre} band. ${shot}.`,
|
package/dist/paywall.d.ts
CHANGED
|
@@ -58,3 +58,16 @@ export declare function paywallFromEnv(env?: NodeJS.ProcessEnv): PaywallConfig;
|
|
|
58
58
|
* a payTo the operator did not set is never invented here.
|
|
59
59
|
*/
|
|
60
60
|
export declare function applyRemoteConfig(current: PaywallConfig, remote: unknown): PaywallConfig;
|
|
61
|
+
/**
|
|
62
|
+
* The crawl door, nixamp.com only: the fleet's day pass ($1 a day over x402, or
|
|
63
|
+
* any coin / PayPal through CoinPay's hosted checkout) sold on /crawl and charged
|
|
64
|
+
* to AI training crawlers, headless browsers and SEO scrapers everywhere else.
|
|
65
|
+
* People, verified search crawlers (Googlebot, Bingbot from their published
|
|
66
|
+
* ranges) and the AI chat/search fetchers are never charged, nor is anyone
|
|
67
|
+
* presenting a session. Separate from the listener paywall above, which is about
|
|
68
|
+
* how busy a stream is rather than who is asking; it signs its passes with its
|
|
69
|
+
* own secret so a crawl pass is not a listening pass.
|
|
70
|
+
*
|
|
71
|
+
* Built only with a scoped key and a payee, so a self-hosted server never sells.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createCrawlGate(env?: NodeJS.ProcessEnv, exempt?: (request: Request) => boolean): import("@profullstack/x402-gateway").Gateway | null;
|
package/dist/paywall.js
CHANGED
|
@@ -160,3 +160,31 @@ export function applyRemoteConfig(current, remote) {
|
|
|
160
160
|
passMinutes: Number.isFinite(minutes) && minutes > 0 ? Math.floor(minutes) : current.passMinutes,
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The crawl door, nixamp.com only: the fleet's day pass ($1 a day over x402, or
|
|
165
|
+
* any coin / PayPal through CoinPay's hosted checkout) sold on /crawl and charged
|
|
166
|
+
* to AI training crawlers, headless browsers and SEO scrapers everywhere else.
|
|
167
|
+
* People, verified search crawlers (Googlebot, Bingbot from their published
|
|
168
|
+
* ranges) and the AI chat/search fetchers are never charged, nor is anyone
|
|
169
|
+
* presenting a session. Separate from the listener paywall above, which is about
|
|
170
|
+
* how busy a stream is rather than who is asking; it signs its passes with its
|
|
171
|
+
* own secret so a crawl pass is not a listening pass.
|
|
172
|
+
*
|
|
173
|
+
* Built only with a scoped key and a payee, so a self-hosted server never sells.
|
|
174
|
+
*/
|
|
175
|
+
export function createCrawlGate(env = process.env, exempt = () => false) {
|
|
176
|
+
const apiKey = env["COINPAY_X402_KEY"] ?? "";
|
|
177
|
+
const payTo = env["CRAWL_PAY_TO"] ?? "";
|
|
178
|
+
if (!apiKey || !payTo)
|
|
179
|
+
return null;
|
|
180
|
+
return createGateway({
|
|
181
|
+
siteUrl: (env["SITE_URL"] || "https://nixamp.com").replace(/\/+$/, ""),
|
|
182
|
+
siteName: "nixamp",
|
|
183
|
+
coinpay: { apiKey },
|
|
184
|
+
payTo,
|
|
185
|
+
secret: `${apiKey}:crawl`,
|
|
186
|
+
contact: "mailto:anthony@profullstack.com",
|
|
187
|
+
openPaths: ["/api/health", "/llms.txt", "/.well-known/"],
|
|
188
|
+
exempt,
|
|
189
|
+
});
|
|
190
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -898,6 +898,10 @@ export interface HandlerOptions {
|
|
|
898
898
|
* Response it is refused with.
|
|
899
899
|
*/
|
|
900
900
|
throttle?: Throttle;
|
|
901
|
+
/** The crawl door from paywall.ts (nixamp.com only): a 402 for training crawlers, the /crawl shop. */
|
|
902
|
+
crawlGate?: {
|
|
903
|
+
handle: (request: Request) => Promise<Response | null>;
|
|
904
|
+
} | null;
|
|
901
905
|
/**
|
|
902
906
|
* The VAPID pair, read from the environment when the server starts. Only the
|
|
903
907
|
* public half leaves: it is what a browser needs before it can subscribe.
|
package/dist/server.js
CHANGED
|
@@ -132,7 +132,7 @@ import { WATCH_EVERY_MS, remember, watchOnce } from "./media-index.js";
|
|
|
132
132
|
import { Outro } from "./outro.js";
|
|
133
133
|
import { Profiles, Voices, spokenLine, spokenVoiceFor } from "./voices.js";
|
|
134
134
|
import { confirm, DEFAULT_DIRECTORY, Publisher } from "./publish.js";
|
|
135
|
-
import { applyRemoteConfig, createPaywall, FREE_LISTENERS, paywallFromEnv, } from "./paywall.js";
|
|
135
|
+
import { applyRemoteConfig, createPaywall, FREE_LISTENERS, paywallFromEnv, createCrawlGate, } from "./paywall.js";
|
|
136
136
|
import { isRemote, isTransportStream, playsInBrowser, sourceLabel } from "./sources.js";
|
|
137
137
|
import { codecsOf, probeAsync, transportInputArgs, videoArgs } from "./audio.js";
|
|
138
138
|
import { allowedForListening, elevate, firewallInUse, certifiable, keyCookie, keyInPath, rememberedKeys, keyFrom, lookupPublicIp, portCommands, reachableAddresses, scopeOf, shareLink, audioLink, KEY_QUERY, } from "./share.js";
|
|
@@ -1657,6 +1657,14 @@ export function createHandler(engine, options) {
|
|
|
1657
1657
|
// Metered before anything is done for the request, so a caller over its
|
|
1658
1658
|
// allowance costs nothing but this check. The throttle decides; this only
|
|
1659
1659
|
// carries its refusal back through Node's response.
|
|
1660
|
+
// Training crawlers buy a day pass before anything else is done for them.
|
|
1661
|
+
if (options.crawlGate) {
|
|
1662
|
+
const sold = await options.crawlGate.handle(requestFor(request));
|
|
1663
|
+
if (sold) {
|
|
1664
|
+
await answerWith(response, sold);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1660
1668
|
if (options.throttle) {
|
|
1661
1669
|
const refused = await options.throttle.handle(requestFor(request));
|
|
1662
1670
|
if (refused) {
|
|
@@ -7882,6 +7890,10 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
7882
7890
|
});
|
|
7883
7891
|
certs = new Certs(pool, zone, issuer, { log: (line) => console.log(` ${line}`) });
|
|
7884
7892
|
}
|
|
7893
|
+
// nixamp.com only (it has the database): a self-hosted server never sells crawl passes.
|
|
7894
|
+
const crawlGate = pool
|
|
7895
|
+
? createCrawlGate(process.env, (request) => /(?:^|;\s*)nixamp_session=/.test(request.headers.get("cookie") ?? ""))
|
|
7896
|
+
: null;
|
|
7885
7897
|
const throttle = pool
|
|
7886
7898
|
? createThrottle({
|
|
7887
7899
|
rules: [
|
|
@@ -8399,6 +8411,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
8399
8411
|
...(certs ? { certs } : {}),
|
|
8400
8412
|
dnsZone: zoneName,
|
|
8401
8413
|
...(throttle ? { throttle } : {}),
|
|
8414
|
+
...(crawlGate ? { crawlGate } : {}),
|
|
8402
8415
|
...(partyLine ? { partyLine } : {}),
|
|
8403
8416
|
// Accounts live where the directory lives, and only there: a nixamp on a
|
|
8404
8417
|
// laptop has nobody to be an account of.
|
package/dist/veo-studio.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare class VeoStudio {
|
|
|
52
52
|
seconds: number;
|
|
53
53
|
references: Buffer[];
|
|
54
54
|
}): Promise<Buffer>;
|
|
55
|
+
/** Veo 3.1 Fast on fal.ai: with the portraits as references when there are any, else from the prompt alone. */
|
|
56
|
+
private viaFal;
|
|
57
|
+
/** fal.ai takes inputs by URL only (a data: URI is "invalid"): its own storage first. */
|
|
58
|
+
private falUpload;
|
|
59
|
+
/** One fal.ai queue job to its video: submitted, waited on (up to 10 minutes), downloaded. */
|
|
60
|
+
private falJob;
|
|
55
61
|
get canLipsync(): boolean;
|
|
56
62
|
/**
|
|
57
63
|
* A singer's close-up with the mouth redrawn to `audio` (the same seconds of
|
package/dist/veo-studio.js
CHANGED
|
@@ -20,6 +20,8 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
export class VeoBudgetError extends Error {
|
|
22
22
|
}
|
|
23
|
+
/** Google will make no more today (quota) or has no credit left: worth trying fal.ai instead. */
|
|
24
|
+
const GOOGLE_OUT = /quota|prepayment|depleted|RESOURCE_EXHAUSTED|\b429\b/i;
|
|
23
25
|
export class VeoRefused extends Error {
|
|
24
26
|
}
|
|
25
27
|
/** The spend in Postgres, one row a month, reserved atomically. */
|
|
@@ -101,13 +103,76 @@ export class VeoStudio {
|
|
|
101
103
|
if (!(await this.options.ledger.reserve(month, cost, this.cap)))
|
|
102
104
|
throw new VeoBudgetError(`this month's $${this.cap} of Veo is spent`);
|
|
103
105
|
try {
|
|
104
|
-
|
|
106
|
+
try {
|
|
107
|
+
return await this.make(input.prompt, seconds, input.references.slice(0, 3));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Google out of quota for the day, or its prepaid credit spent: the same Veo on fal.ai.
|
|
111
|
+
if (!this.options.falKey || !(error instanceof VeoRefused) || !GOOGLE_OUT.test(error.message))
|
|
112
|
+
throw error;
|
|
113
|
+
return await this.viaFal(this.options.falKey, input.prompt, seconds, input.references.slice(0, 3));
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
catch (error) {
|
|
107
117
|
await this.options.ledger.refund(month, cost).catch(() => undefined);
|
|
108
118
|
throw error;
|
|
109
119
|
}
|
|
110
120
|
}
|
|
121
|
+
/** Veo 3.1 Fast on fal.ai: with the portraits as references when there are any, else from the prompt alone. */
|
|
122
|
+
async viaFal(key, prompt, seconds, references) {
|
|
123
|
+
const settings = { prompt: prompt.slice(0, 1800), duration: `${seconds}s`, aspect_ratio: "16:9", resolution: "1080p", generate_audio: false };
|
|
124
|
+
if (references.length) {
|
|
125
|
+
const image_urls = await Promise.all(references.map((bytes, i) => this.falUpload(key, bytes, "image/jpeg", `ref-${i}.jpg`)));
|
|
126
|
+
try {
|
|
127
|
+
return await this.falJob(key, "fal-ai/veo3.1/fast/reference-to-video", { ...settings, image_urls }, "the fal.ai Veo shot");
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// A reference shot it will not make is tried once from the words alone.
|
|
131
|
+
if (!(error instanceof VeoRefused))
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return await this.falJob(key, "fal-ai/veo3.1/fast", settings, "the fal.ai Veo shot");
|
|
136
|
+
}
|
|
137
|
+
/** fal.ai takes inputs by URL only (a data: URI is "invalid"): its own storage first. */
|
|
138
|
+
async falUpload(key, bytes, type, name) {
|
|
139
|
+
const send = this.options.fetcher ?? fetch;
|
|
140
|
+
const init = await send("https://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3", { method: "POST", headers: { authorization: `Key ${key}`, "content-type": "application/json" }, body: JSON.stringify({ content_type: type, file_name: name }), signal: AbortSignal.timeout(30_000) });
|
|
141
|
+
const slot = await init.json().catch(() => ({}));
|
|
142
|
+
if (!slot.upload_url || !slot.file_url)
|
|
143
|
+
throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${init.status})`);
|
|
144
|
+
const put = await send(slot.upload_url, { method: "PUT", headers: { "content-type": type }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(120_000) });
|
|
145
|
+
if (!put.ok)
|
|
146
|
+
throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${put.status})`);
|
|
147
|
+
return slot.file_url;
|
|
148
|
+
}
|
|
149
|
+
/** One fal.ai queue job to its video: submitted, waited on (up to 10 minutes), downloaded. */
|
|
150
|
+
async falJob(key, model, body, what) {
|
|
151
|
+
const send = this.options.fetcher ?? fetch;
|
|
152
|
+
const auth = { authorization: `Key ${key}`, "content-type": "application/json" };
|
|
153
|
+
const started = await send(`https://queue.fal.run/${model}`, { method: "POST", headers: auth, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000) });
|
|
154
|
+
const job = await started.json().catch(() => ({}));
|
|
155
|
+
if (!started.ok || !job.status_url || !job.response_url)
|
|
156
|
+
throw new VeoRefused(`fal.ai refused ${what}: HTTP ${started.status}`);
|
|
157
|
+
const sleep = this.options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
158
|
+
for (let tries = 0; tries < 120; tries++) {
|
|
159
|
+
await sleep(5_000);
|
|
160
|
+
const state = await (await send(job.status_url, { headers: auth, signal: AbortSignal.timeout(30_000) })).json().catch(() => ({}));
|
|
161
|
+
if (state.status !== "COMPLETED")
|
|
162
|
+
continue;
|
|
163
|
+
const answer = await send(job.response_url, { headers: auth, signal: AbortSignal.timeout(30_000) });
|
|
164
|
+
const result = await answer.json().catch(() => ({}));
|
|
165
|
+
const why = typeof result.detail === "string" ? result.detail : result.detail?.[0]?.msg;
|
|
166
|
+
if (!answer.ok || !result.video?.url)
|
|
167
|
+
throw new VeoRefused(`${what} failed: ${why ?? `HTTP ${answer.status}`}`);
|
|
168
|
+
const out = await send(result.video.url, { signal: AbortSignal.timeout(120_000) });
|
|
169
|
+
const bytes = Buffer.from(await out.arrayBuffer());
|
|
170
|
+
if (!out.ok || bytes.length < 10_000)
|
|
171
|
+
throw new VeoRefused(`${what} did not download (HTTP ${out.status})`);
|
|
172
|
+
return bytes;
|
|
173
|
+
}
|
|
174
|
+
throw new VeoRefused(`${what} took too long (10 minutes)`);
|
|
175
|
+
}
|
|
111
176
|
get canLipsync() { return Boolean(this.options.falKey); }
|
|
112
177
|
/**
|
|
113
178
|
* A singer's close-up with the mouth redrawn to `audio` (the same seconds of
|
|
@@ -131,41 +196,8 @@ export class VeoStudio {
|
|
|
131
196
|
}
|
|
132
197
|
}
|
|
133
198
|
async sync(key, video, audio) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
// fal.ai takes inputs by URL only (a data: URI is "invalid"): its own storage first.
|
|
137
|
-
const upload = async (bytes, type, name) => {
|
|
138
|
-
const init = await send("https://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3", { method: "POST", headers: auth, body: JSON.stringify({ content_type: type, file_name: name }), signal: AbortSignal.timeout(30_000) });
|
|
139
|
-
const slot = await init.json().catch(() => ({}));
|
|
140
|
-
if (!slot.upload_url || !slot.file_url)
|
|
141
|
-
throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${init.status})`);
|
|
142
|
-
const put = await send(slot.upload_url, { method: "PUT", headers: { "content-type": type }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(120_000) });
|
|
143
|
-
if (!put.ok)
|
|
144
|
-
throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${put.status})`);
|
|
145
|
-
return slot.file_url;
|
|
146
|
-
};
|
|
147
|
-
const [videoUrl, audioUrl] = await Promise.all([upload(video, "video/mp4", "shot.mp4"), upload(audio, "audio/mpeg", "vocal.mp3")]);
|
|
148
|
-
const started = await send("https://queue.fal.run/fal-ai/sync-lipsync/v2", { method: "POST", headers: auth, body: JSON.stringify({ video_url: videoUrl, audio_url: audioUrl }), signal: AbortSignal.timeout(60_000) });
|
|
149
|
-
const job = await started.json().catch(() => ({}));
|
|
150
|
-
if (!started.ok || !job.status_url || !job.response_url)
|
|
151
|
-
throw new VeoRefused(`fal.ai refused the lip sync: HTTP ${started.status}`);
|
|
152
|
-
const sleep = this.options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
153
|
-
for (let tries = 0; tries < 120; tries++) {
|
|
154
|
-
await sleep(5_000);
|
|
155
|
-
const state = await (await send(job.status_url, { headers: auth, signal: AbortSignal.timeout(30_000) })).json().catch(() => ({}));
|
|
156
|
-
if (state.status !== "COMPLETED")
|
|
157
|
-
continue;
|
|
158
|
-
const answer = await send(job.response_url, { headers: auth, signal: AbortSignal.timeout(30_000) });
|
|
159
|
-
const result = await answer.json().catch(() => ({}));
|
|
160
|
-
if (!answer.ok || !result.video?.url)
|
|
161
|
-
throw new VeoRefused(`the lip sync failed: ${result.detail?.[0]?.msg ?? `HTTP ${answer.status}`}`);
|
|
162
|
-
const out = await send(result.video.url, { signal: AbortSignal.timeout(120_000) });
|
|
163
|
-
const bytes = Buffer.from(await out.arrayBuffer());
|
|
164
|
-
if (!out.ok || bytes.length < 10_000)
|
|
165
|
-
throw new VeoRefused(`the lip-synced clip did not download (HTTP ${out.status})`);
|
|
166
|
-
return bytes;
|
|
167
|
-
}
|
|
168
|
-
throw new VeoRefused("the lip sync took too long (10 minutes)");
|
|
199
|
+
const [videoUrl, audioUrl] = await Promise.all([this.falUpload(key, video, "video/mp4", "shot.mp4"), this.falUpload(key, audio, "audio/mpeg", "vocal.mp3")]);
|
|
200
|
+
return await this.falJob(key, "fal-ai/sync-lipsync/v2", { video_url: videoUrl, audio_url: audioUrl }, "the lip sync");
|
|
169
201
|
}
|
|
170
202
|
async make(prompt, seconds, references) {
|
|
171
203
|
const send = this.options.fetcher ?? fetch;
|
package/package.json
CHANGED
package/src/ai-label.ts
CHANGED
|
@@ -690,13 +690,14 @@ const SHOTS = [
|
|
|
690
690
|
];
|
|
691
691
|
|
|
692
692
|
// The singer's close-ups, which are lip-synced to the song afterwards (fal.ai). The mouth
|
|
693
|
-
// must be clear: whatever covers it
|
|
694
|
-
//
|
|
693
|
+
// must be clear: whatever covers it is smeared into the face when the mouth is redrawn,
|
|
694
|
+
// and Veo puts a microphone in front of it whenever there is one in the scene, however
|
|
695
|
+
// the prompt asks for it held low. So there is none: the singer sings in the story's world.
|
|
695
696
|
const CLOSE_UPS = [
|
|
696
|
-
"
|
|
697
|
-
"
|
|
698
|
-
"
|
|
699
|
-
"Close-up of the lead vocalist singing in
|
|
697
|
+
"Close-up of the lead vocalist singing straight to camera at night in the rain on a neon-lit street, face and mouth fully lit, no microphone anywhere",
|
|
698
|
+
"Medium close-up of the lead vocalist singing to camera from the driver's seat of a car at dusk, window down, wind in the hair, no microphone",
|
|
699
|
+
"Close-up of the lead vocalist singing on an open road at golden hour, the camera slowly circling, face to camera, no microphone",
|
|
700
|
+
"Close-up of the lead vocalist singing in a dark room lit by one warm lamp, head and shoulders, face to camera, no microphone",
|
|
700
701
|
];
|
|
701
702
|
|
|
702
703
|
/**
|
|
@@ -707,7 +708,7 @@ const CLOSE_UPS = [
|
|
|
707
708
|
export function veoPrompts(artist: Pick<LabelArtist, "genre" | "sound">, song: Pick<LabelSong, "style">, members: Array<Pick<Member, "role" | "look">>, count: number, kind: "band" | "singer" = "band"): string[] {
|
|
708
709
|
const cast = members.slice(0, 3).map((one, i) => `the ${one.role} from reference image ${i + 1} (${one.look.replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "")})`).join("; ");
|
|
709
710
|
const rule = kind === "singer"
|
|
710
|
-
? "The vocalist sings clearly, the whole mouth always visible
|
|
711
|
+
? "The vocalist sings clearly to camera, the whole mouth always visible. There is no microphone, mic stand or instrument anywhere in the shot, and nothing in front of the face. Cinematic, 35mm film look. No text, no logos."
|
|
711
712
|
: "The lead vocalist is not in this shot, and nobody sings, speaks or lip-syncs on camera. Cinematic, 35mm film look, dynamic camera. No text, no logos.";
|
|
712
713
|
return (kind === "singer" ? CLOSE_UPS : SHOTS).slice(0, count).map((shot) => [
|
|
713
714
|
`Music video for a fictional ${artist.genre} band. ${shot}.`,
|
package/src/paywall.ts
CHANGED
|
@@ -196,3 +196,34 @@ export function applyRemoteConfig(current: PaywallConfig, remote: unknown): Payw
|
|
|
196
196
|
passMinutes: Number.isFinite(minutes) && minutes > 0 ? Math.floor(minutes) : current.passMinutes,
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The crawl door, nixamp.com only: the fleet's day pass ($1 a day over x402, or
|
|
202
|
+
* any coin / PayPal through CoinPay's hosted checkout) sold on /crawl and charged
|
|
203
|
+
* to AI training crawlers, headless browsers and SEO scrapers everywhere else.
|
|
204
|
+
* People, verified search crawlers (Googlebot, Bingbot from their published
|
|
205
|
+
* ranges) and the AI chat/search fetchers are never charged, nor is anyone
|
|
206
|
+
* presenting a session. Separate from the listener paywall above, which is about
|
|
207
|
+
* how busy a stream is rather than who is asking; it signs its passes with its
|
|
208
|
+
* own secret so a crawl pass is not a listening pass.
|
|
209
|
+
*
|
|
210
|
+
* Built only with a scoped key and a payee, so a self-hosted server never sells.
|
|
211
|
+
*/
|
|
212
|
+
export function createCrawlGate(
|
|
213
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
214
|
+
exempt: (request: Request) => boolean = () => false,
|
|
215
|
+
) {
|
|
216
|
+
const apiKey = env["COINPAY_X402_KEY"] ?? "";
|
|
217
|
+
const payTo = env["CRAWL_PAY_TO"] ?? "";
|
|
218
|
+
if (!apiKey || !payTo) return null;
|
|
219
|
+
return createGateway({
|
|
220
|
+
siteUrl: (env["SITE_URL"] || "https://nixamp.com").replace(/\/+$/, ""),
|
|
221
|
+
siteName: "nixamp",
|
|
222
|
+
coinpay: { apiKey },
|
|
223
|
+
payTo,
|
|
224
|
+
secret: `${apiKey}:crawl`,
|
|
225
|
+
contact: "mailto:anthony@profullstack.com",
|
|
226
|
+
openPaths: ["/api/health", "/llms.txt", "/.well-known/"],
|
|
227
|
+
exempt,
|
|
228
|
+
});
|
|
229
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -166,6 +166,7 @@ import {
|
|
|
166
166
|
FREE_LISTENERS,
|
|
167
167
|
type PaywallConfig,
|
|
168
168
|
paywallFromEnv,
|
|
169
|
+
createCrawlGate,
|
|
169
170
|
} from "./paywall.ts";
|
|
170
171
|
import { isRemote, isTransportStream, playsInBrowser, sourceLabel } from "./sources.ts";
|
|
171
172
|
import { codecsOf, probeAsync, transportInputArgs, videoArgs, type Codecs } from "./audio.ts";
|
|
@@ -2018,6 +2019,8 @@ export interface HandlerOptions {
|
|
|
2018
2019
|
* Response it is refused with.
|
|
2019
2020
|
*/
|
|
2020
2021
|
throttle?: Throttle;
|
|
2022
|
+
/** The crawl door from paywall.ts (nixamp.com only): a 402 for training crawlers, the /crawl shop. */
|
|
2023
|
+
crawlGate?: { handle: (request: Request) => Promise<Response | null> } | null;
|
|
2021
2024
|
/**
|
|
2022
2025
|
* The VAPID pair, read from the environment when the server starts. Only the
|
|
2023
2026
|
* public half leaves: it is what a browser needs before it can subscribe.
|
|
@@ -2212,6 +2215,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2212
2215
|
// Metered before anything is done for the request, so a caller over its
|
|
2213
2216
|
// allowance costs nothing but this check. The throttle decides; this only
|
|
2214
2217
|
// carries its refusal back through Node's response.
|
|
2218
|
+
// Training crawlers buy a day pass before anything else is done for them.
|
|
2219
|
+
if (options.crawlGate) {
|
|
2220
|
+
const sold = await options.crawlGate.handle(requestFor(request));
|
|
2221
|
+
if (sold) {
|
|
2222
|
+
await answerWith(response, sold);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2215
2227
|
if (options.throttle) {
|
|
2216
2228
|
const refused = await options.throttle.handle(requestFor(request));
|
|
2217
2229
|
if (refused) {
|
|
@@ -8314,6 +8326,10 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
8314
8326
|
});
|
|
8315
8327
|
certs = new Certs(pool, zone, issuer, { log: (line) => console.log(` ${line}`) });
|
|
8316
8328
|
}
|
|
8329
|
+
// nixamp.com only (it has the database): a self-hosted server never sells crawl passes.
|
|
8330
|
+
const crawlGate = pool
|
|
8331
|
+
? createCrawlGate(process.env, (request) => /(?:^|;\s*)nixamp_session=/.test(request.headers.get("cookie") ?? ""))
|
|
8332
|
+
: null;
|
|
8317
8333
|
const throttle = pool
|
|
8318
8334
|
? createThrottle({
|
|
8319
8335
|
rules: [
|
|
@@ -8835,6 +8851,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
8835
8851
|
...(certs ? { certs } : {}),
|
|
8836
8852
|
dnsZone: zoneName,
|
|
8837
8853
|
...(throttle ? { throttle } : {}),
|
|
8854
|
+
...(crawlGate ? { crawlGate } : {}),
|
|
8838
8855
|
...(partyLine ? { partyLine } : {}),
|
|
8839
8856
|
// Accounts live where the directory lives, and only there: a nixamp on a
|
|
8840
8857
|
// laptop has nobody to be an account of.
|
package/src/veo-studio.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
|
|
22
22
|
export class VeoBudgetError extends Error {}
|
|
23
|
+
|
|
24
|
+
/** Google will make no more today (quota) or has no credit left: worth trying fal.ai instead. */
|
|
25
|
+
const GOOGLE_OUT = /quota|prepayment|depleted|RESOURCE_EXHAUSTED|\b429\b/i;
|
|
23
26
|
export class VeoRefused extends Error {}
|
|
24
27
|
|
|
25
28
|
/** Where the month's spend lives. `reserve` adds `usd` only if that keeps the month at or under `cap`. */
|
|
@@ -123,13 +126,69 @@ export class VeoStudio {
|
|
|
123
126
|
// Reserved first, in one step that will not go over: two at once cannot both squeeze under the cap.
|
|
124
127
|
if (!(await this.options.ledger.reserve(month, cost, this.cap))) throw new VeoBudgetError(`this month's $${this.cap} of Veo is spent`);
|
|
125
128
|
try {
|
|
126
|
-
|
|
129
|
+
try {
|
|
130
|
+
return await this.make(input.prompt, seconds, input.references.slice(0, 3));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Google out of quota for the day, or its prepaid credit spent: the same Veo on fal.ai.
|
|
133
|
+
if (!this.options.falKey || !(error instanceof VeoRefused) || !GOOGLE_OUT.test(error.message)) throw error;
|
|
134
|
+
return await this.viaFal(this.options.falKey, input.prompt, seconds, input.references.slice(0, 3));
|
|
135
|
+
}
|
|
127
136
|
} catch (error) {
|
|
128
137
|
await this.options.ledger.refund(month, cost).catch(() => undefined);
|
|
129
138
|
throw error;
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
/** Veo 3.1 Fast on fal.ai: with the portraits as references when there are any, else from the prompt alone. */
|
|
143
|
+
private async viaFal(key: string, prompt: string, seconds: number, references: Buffer[]): Promise<Buffer> {
|
|
144
|
+
const settings = { prompt: prompt.slice(0, 1800), duration: `${seconds}s`, aspect_ratio: "16:9", resolution: "1080p", generate_audio: false };
|
|
145
|
+
if (references.length) {
|
|
146
|
+
const image_urls = await Promise.all(references.map((bytes, i) => this.falUpload(key, bytes, "image/jpeg", `ref-${i}.jpg`)));
|
|
147
|
+
try {
|
|
148
|
+
return await this.falJob(key, "fal-ai/veo3.1/fast/reference-to-video", { ...settings, image_urls }, "the fal.ai Veo shot");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// A reference shot it will not make is tried once from the words alone.
|
|
151
|
+
if (!(error instanceof VeoRefused)) throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return await this.falJob(key, "fal-ai/veo3.1/fast", settings, "the fal.ai Veo shot");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** fal.ai takes inputs by URL only (a data: URI is "invalid"): its own storage first. */
|
|
158
|
+
private async falUpload(key: string, bytes: Buffer, type: string, name: string): Promise<string> {
|
|
159
|
+
const send = this.options.fetcher ?? fetch;
|
|
160
|
+
const init = await send("https://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3", { method: "POST", headers: { authorization: `Key ${key}`, "content-type": "application/json" }, body: JSON.stringify({ content_type: type, file_name: name }), signal: AbortSignal.timeout(30_000) });
|
|
161
|
+
const slot = await init.json().catch(() => ({})) as { upload_url?: string; file_url?: string };
|
|
162
|
+
if (!slot.upload_url || !slot.file_url) throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${init.status})`);
|
|
163
|
+
const put = await send(slot.upload_url, { method: "PUT", headers: { "content-type": type }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(120_000) });
|
|
164
|
+
if (!put.ok) throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${put.status})`);
|
|
165
|
+
return slot.file_url;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** One fal.ai queue job to its video: submitted, waited on (up to 10 minutes), downloaded. */
|
|
169
|
+
private async falJob(key: string, model: string, body: Record<string, unknown>, what: string): Promise<Buffer> {
|
|
170
|
+
const send = this.options.fetcher ?? fetch;
|
|
171
|
+
const auth = { authorization: `Key ${key}`, "content-type": "application/json" };
|
|
172
|
+
const started = await send(`https://queue.fal.run/${model}`, { method: "POST", headers: auth, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000) });
|
|
173
|
+
const job = await started.json().catch(() => ({})) as { status_url?: string; response_url?: string };
|
|
174
|
+
if (!started.ok || !job.status_url || !job.response_url) throw new VeoRefused(`fal.ai refused ${what}: HTTP ${started.status}`);
|
|
175
|
+
const sleep = this.options.sleep ?? ((ms: number) => new Promise((done) => setTimeout(done, ms)));
|
|
176
|
+
for (let tries = 0; tries < 120; tries++) {
|
|
177
|
+
await sleep(5_000);
|
|
178
|
+
const state = await (await send(job.status_url, { headers: auth, signal: AbortSignal.timeout(30_000) })).json().catch(() => ({})) as { status?: string };
|
|
179
|
+
if (state.status !== "COMPLETED") continue;
|
|
180
|
+
const answer = await send(job.response_url, { headers: auth, signal: AbortSignal.timeout(30_000) });
|
|
181
|
+
const result = await answer.json().catch(() => ({})) as { video?: { url?: string }; detail?: Array<{ msg?: string }> | string };
|
|
182
|
+
const why = typeof result.detail === "string" ? result.detail : result.detail?.[0]?.msg;
|
|
183
|
+
if (!answer.ok || !result.video?.url) throw new VeoRefused(`${what} failed: ${why ?? `HTTP ${answer.status}`}`);
|
|
184
|
+
const out = await send(result.video.url, { signal: AbortSignal.timeout(120_000) });
|
|
185
|
+
const bytes = Buffer.from(await out.arrayBuffer());
|
|
186
|
+
if (!out.ok || bytes.length < 10_000) throw new VeoRefused(`${what} did not download (HTTP ${out.status})`);
|
|
187
|
+
return bytes;
|
|
188
|
+
}
|
|
189
|
+
throw new VeoRefused(`${what} took too long (10 minutes)`);
|
|
190
|
+
}
|
|
191
|
+
|
|
133
192
|
get canLipsync(): boolean { return Boolean(this.options.falKey); }
|
|
134
193
|
|
|
135
194
|
/**
|
|
@@ -152,35 +211,8 @@ export class VeoStudio {
|
|
|
152
211
|
}
|
|
153
212
|
|
|
154
213
|
private async sync(key: string, video: Buffer, audio: Buffer): Promise<Buffer> {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
// fal.ai takes inputs by URL only (a data: URI is "invalid"): its own storage first.
|
|
158
|
-
const upload = async (bytes: Buffer, type: string, name: string): Promise<string> => {
|
|
159
|
-
const init = await send("https://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3", { method: "POST", headers: auth, body: JSON.stringify({ content_type: type, file_name: name }), signal: AbortSignal.timeout(30_000) });
|
|
160
|
-
const slot = await init.json().catch(() => ({})) as { upload_url?: string; file_url?: string };
|
|
161
|
-
if (!slot.upload_url || !slot.file_url) throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${init.status})`);
|
|
162
|
-
const put = await send(slot.upload_url, { method: "PUT", headers: { "content-type": type }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(120_000) });
|
|
163
|
-
if (!put.ok) throw new VeoRefused(`fal.ai storage refused the upload (HTTP ${put.status})`);
|
|
164
|
-
return slot.file_url;
|
|
165
|
-
};
|
|
166
|
-
const [videoUrl, audioUrl] = await Promise.all([upload(video, "video/mp4", "shot.mp4"), upload(audio, "audio/mpeg", "vocal.mp3")]);
|
|
167
|
-
const started = await send("https://queue.fal.run/fal-ai/sync-lipsync/v2", { method: "POST", headers: auth, body: JSON.stringify({ video_url: videoUrl, audio_url: audioUrl }), signal: AbortSignal.timeout(60_000) });
|
|
168
|
-
const job = await started.json().catch(() => ({})) as { status_url?: string; response_url?: string; detail?: unknown };
|
|
169
|
-
if (!started.ok || !job.status_url || !job.response_url) throw new VeoRefused(`fal.ai refused the lip sync: HTTP ${started.status}`);
|
|
170
|
-
const sleep = this.options.sleep ?? ((ms: number) => new Promise((done) => setTimeout(done, ms)));
|
|
171
|
-
for (let tries = 0; tries < 120; tries++) {
|
|
172
|
-
await sleep(5_000);
|
|
173
|
-
const state = await (await send(job.status_url, { headers: auth, signal: AbortSignal.timeout(30_000) })).json().catch(() => ({})) as { status?: string };
|
|
174
|
-
if (state.status !== "COMPLETED") continue;
|
|
175
|
-
const answer = await send(job.response_url, { headers: auth, signal: AbortSignal.timeout(30_000) });
|
|
176
|
-
const result = await answer.json().catch(() => ({})) as { video?: { url?: string }; detail?: Array<{ msg?: string }> };
|
|
177
|
-
if (!answer.ok || !result.video?.url) throw new VeoRefused(`the lip sync failed: ${result.detail?.[0]?.msg ?? `HTTP ${answer.status}`}`);
|
|
178
|
-
const out = await send(result.video.url, { signal: AbortSignal.timeout(120_000) });
|
|
179
|
-
const bytes = Buffer.from(await out.arrayBuffer());
|
|
180
|
-
if (!out.ok || bytes.length < 10_000) throw new VeoRefused(`the lip-synced clip did not download (HTTP ${out.status})`);
|
|
181
|
-
return bytes;
|
|
182
|
-
}
|
|
183
|
-
throw new VeoRefused("the lip sync took too long (10 minutes)");
|
|
214
|
+
const [videoUrl, audioUrl] = await Promise.all([this.falUpload(key, video, "video/mp4", "shot.mp4"), this.falUpload(key, audio, "audio/mpeg", "vocal.mp3")]);
|
|
215
|
+
return await this.falJob(key, "fal-ai/sync-lipsync/v2", { video_url: videoUrl, audio_url: audioUrl }, "the lip sync");
|
|
184
216
|
}
|
|
185
217
|
|
|
186
218
|
private async make(prompt: string, seconds: number, references: Buffer[]): Promise<Buffer> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./main-
|
|
1
|
+
import{t as e}from"./main-DZilIsh4.js";var t={...e,"No parties live right now.":`لا توجد جلسات مباشرة الآن.`,"Could not refresh parties.":`تعذر تحديث الجلسات.`,"Pick a server in the directory, or paste its link here.":`اختر خادمًا من الدليل أو الصق رابطه هنا.`,"Interface language":`لغة الواجهة`,Language:`اللغة`,"Translation language":`لغة الترجمة`,"Original (auto-detect)":`الأصل (اكتشاف تلقائي)`,Play:`تشغيل`,Pause:`إيقاف مؤقت`,Stop:`إيقاف`,Next:`التالي`,Previous:`السابق`,Select:`اختيار`,"Next/Prev":`التالي/السابق`,Quit:`خروج`,"Now Playing":`قيد التشغيل`,"Spectrum Analyser":`محلل الطيف`,Playlist:`قائمة التشغيل`,Files:`الملفات`,"Open files":`فتح ملفات`,"Open folder":`فتح مجلد`,"Add to library":`إضافة إلى المكتبة`,"Replace playlist":`استبدال قائمة التشغيل`,Filter:`تصفية`,Search:`بحث`,"Nothing loaded.":`لم يُحمّل أي محتوى.`,"Empty.":`فارغ.`,"Loading…":`جارٍ التحميل…`,"Connecting…":`جارٍ الاتصال…`,Connected:`متصل`,"Not connected":`غير متصل`,"No source":`لا يوجد مصدر`,Playing:`يعمل`,Stopped:`متوقف`,Volume:`مستوى الصوت`,Seek:`موضع التشغيل`,"Full screen":`ملء الشاشة`,Download:`تنزيل`,Share:`مشاركة`,Copy:`نسخ`,"Copy link":`نسخ الرابط`,Copied:`تم النسخ`,Save:`حفظ`,Cancel:`إلغاء`,Close:`إغلاق`,Remove:`إزالة`,Delete:`حذف`,Edit:`تعديل`,Add:`إضافة`,Send:`إرسال`,Retry:`إعادة المحاولة`,Back:`رجوع`,"Next page":`الصفحة التالية`,"Previous page":`الصفحة السابقة`,"Show more":`عرض المزيد`,Panels:`اللوحات`,"Reset layout":`إعادة ضبط التخطيط`,Show:`إظهار`,Hide:`إخفاء`,Collapse:`طي`,Expand:`توسيع`,"Move up":`نقل لأعلى`,"Move down":`نقل لأسفل`,Install:`تثبيت`,"TV mode":`وضع التلفاز`,On:`مفعّل`,Off:`معطّل`,Account:`الحساب`,"Sign in":`تسجيل الدخول`,"Sign out":`تسجيل الخروج`,"Create account":`إنشاء حساب`,Email:`البريد الإلكتروني`,Password:`كلمة المرور`,"Forgot password?":`نسيت كلمة المرور؟`,"Welcome back":`مرحبًا بعودتك`,Profile:`الملف الشخصي`,Handle:`اسم المستخدم`,"Your servers":`خوادمك`,Server:`الخادم`,Connect:`اتصال`,Disconnect:`قطع الاتصال`,Directory:`الدليل`,Favourites:`المفضلة`,Follow:`متابعة`,Unfollow:`إلغاء المتابعة`,Notifications:`الإشعارات`,"Phone number":`رقم الهاتف`,"Save number":`حفظ الرقم`,Transcript:`النص المفرغ`,Captions:`الترجمة النصية`,"Translate audio":`ترجمة الصوت`,"Audio language":`لغة الصوت`,"Audio options":`خيارات الصوت`,"Keep background sound":`الاحتفاظ بأصوات الخلفية`,"Background level":`مستوى صوت الخلفية`,"Speaker voices":`أصوات المتحدثين`,"Any voice":`أي صوت`,"Male voice":`صوت رجالي`,"Female voice":`صوت نسائي`,"Original audio.":`الصوت الأصلي.`,"Buy translated audio":`شراء الصوت المترجم`,"Audio credit":`رصيد الصوت`,"Buy pass":`شراء تصريح`,Pass:`تصريح`,"Pay with":`الدفع باستخدام`,"Check payment":`التحقق من الدفع`,"Open checkout":`الانتقال للدفع`,"Free sessions":`جلسات مجانية`,"Usage rates and expiry":`الأسعار ومدة الصلاحية`,Catalogs:`الكتالوجات`,"Add catalog":`إضافة كتالوج`,"Catalog name":`اسم الكتالوج`,Link:`الرابط`,"Play link":`تشغيل الرابط`,"Go live":`بدء بث مباشر`,Publish:`نشر`,Listen:`استماع`,"Live now":`مباشر الآن`,Upcoming:`قريبًا`,Parties:`حفلات المشاهدة`,"Join party":`الانضمام للحفلة`,"Invite code":`رمز الدعوة`,Invite:`دعوة`,Chat:`الدردشة`,"Chat message":`رسالة دردشة`,People:`الأشخاص`,Speaker:`متحدث`,Speakers:`المتحدثون`,Listener:`مستمع`,Moderator:`مشرف`,"Raise hand":`رفع اليد`,"Hand raises":`الأيدي المرفوعة`,Questions:`الأسئلة`,Resources:`الموارد`,Replay:`إعادة العرض`,Schedule:`الجدول`,Event:`الفعالية`,Host:`المضيف`,About:`حول`,Stage:`المنصة`,Title:`العنوان`,Description:`الوصف`,Topic:`الموضوع`,Public:`عام`,"Anyone with the link":`أي شخص لديه الرابط`,"Invited people":`الأشخاص المدعوون`,"Who can join":`من يمكنه الانضمام`,"Date and time":`التاريخ والوقت`,"Expected length":`المدة المتوقعة`,Repeat:`تكرار`,"Does not repeat":`لا يتكرر`,"Every day":`كل يوم`,"Every week":`كل أسبوع`,Homepage:`الموقع الإلكتروني`,"Host name":`اسم المضيف`,"Profile photo URL":`رابط صورة الملف الشخصي`,"Broadcast link":`رابط البث`,"Edit event":`تعديل الفعالية`,"Save changes":`حفظ التغييرات`,"Create live":`إنشاء بث مباشر`,"Schedule class":`جدولة درس`,"Start class":`بدء الدرس`,"End event":`إنهاء الفعالية`,"End session":`إنهاء الجلسة`,"Session ended":`انتهت الجلسة`,"Event ended":`انتهت الفعالية`,Cancelled:`ملغاة`,"Replay available":`إعادة العرض متاحة`,Join:`انضمام`,"Create invite":`إنشاء دعوة`,"Invite as":`الدعوة بصفة`,"Watch now":`شاهد الآن`,"Start a room":`إنشاء غرفة`,"Recent replays":`إعادات العرض الأخيرة`,"Your account":`حسابك`,"Write with AI":`الكتابة بالذكاء الاصطناعي`,"Suggested title":`العنوان المقترح`,"Suggested description":`الوصف المقترح`,"Suggested topic":`الموضوع المقترح`,"Use these details":`استخدام هذه التفاصيل`,"Learn something live.":`تعلّم شيئًا مباشرة.`,"Screen share":`مشاركة الشاشة`,"Upload and broadcast":`رفع وبث`,Help:`مساعدة`,"Reload Player":`إعادة تحميل المشغل`,"Copy Bundled CLI Path":`نسخ مسار أداة سطر الأوامر المضمنة`,"Developer tools":`أدوات المطور`,Undo:`تراجع`,Redo:`إعادة`,Cut:`قص`,Paste:`لصق`,"Select all":`تحديد الكل`,Status:`الحالة`,"Activity log":`سجل النشاط`,Admin:`الإدارة`,"Playback controls":`عناصر التحكم بالتشغيل`,"Tracks and folders":`المقاطع والمجلدات`,"Original-language captions.":`ترجمة نصية باللغة الأصلية.`,"Skip to playback controls":`الانتقال إلى عناصر التحكم بالتشغيل`,"Skip to playlist":`الانتقال إلى قائمة التشغيل`,"Skip to transcript and translated audio":`الانتقال إلى النص المفرغ والصوت المترجم`};export{t as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./main-
|
|
1
|
+
import{t as e}from"./main-DZilIsh4.js";var t={...e,"No parties live right now.":`Právě neprobíhají žádné párty.`,"Could not refresh parties.":`Párty se nepodařilo aktualizovat.`,"Pick a server in the directory, or paste its link here.":`Vyberte server v seznamu nebo sem vložte jeho odkaz.`,"Interface language":`Jazyk rozhraní`,Language:`Jazyk`,"Translation language":`Jazyk překladu`,"Original (auto-detect)":`Původní (automatické rozpoznání)`,Play:`Přehrát`,Pause:`Pozastavit`,Stop:`Zastavit`,Next:`Další`,Previous:`Předchozí`,Select:`Vybrat`,"Next/Prev":`Další/Předchozí`,Quit:`Ukončit`,"Now Playing":`Právě hraje`,"Spectrum Analyser":`Spektrální analyzátor`,Playlist:`Seznam skladeb`,Files:`Soubory`,"Open files":`Otevřít soubory`,"Open folder":`Otevřít složku`,"Add to library":`Přidat do knihovny`,"Replace playlist":`Nahradit seznam`,Filter:`Filtrovat`,Search:`Hledat`,"Nothing loaded.":`Nic není načteno.`,"Empty.":`Prázdné.`,"Loading…":`Načítání…`,"Connecting…":`Připojování…`,Connected:`Připojeno`,"Not connected":`Nepřipojeno`,"No source":`Žádný zdroj`,Playing:`Přehrávání`,Stopped:`Zastaveno`,Volume:`Hlasitost`,Seek:`Pozice přehrávání`,"Full screen":`Celá obrazovka`,Download:`Stáhnout`,Share:`Sdílet`,Copy:`Kopírovat`,"Copy link":`Kopírovat odkaz`,Copied:`Zkopírováno`,Save:`Uložit`,Cancel:`Zrušit`,Close:`Zavřít`,Remove:`Odebrat`,Delete:`Smazat`,Edit:`Upravit`,Add:`Přidat`,Send:`Odeslat`,Retry:`Zkusit znovu`,Back:`Zpět`,"Next page":`Další stránka`,"Previous page":`Předchozí stránka`,"Show more":`Zobrazit více`,Panels:`Panely`,"Reset layout":`Obnovit rozložení`,Show:`Zobrazit`,Hide:`Skrýt`,Collapse:`Sbalit`,Expand:`Rozbalit`,"Move up":`Nahoru`,"Move down":`Dolů`,Install:`Nainstalovat`,"TV mode":`Režim TV`,On:`Zapnuto`,Off:`Vypnuto`,Account:`Účet`,"Sign in":`Přihlásit se`,"Sign out":`Odhlásit se`,"Create account":`Vytvořit účet`,Email:`E-mail`,Password:`Heslo`,"Forgot password?":`Zapomenuté heslo?`,"Welcome back":`Vítejte zpět`,Profile:`Profil`,Handle:`Uživatelské jméno`,"Your servers":`Vaše servery`,Server:`Server`,Connect:`Připojit`,Disconnect:`Odpojit`,Directory:`Adresář`,Favourites:`Oblíbené`,Follow:`Sledovat`,Unfollow:`Přestat sledovat`,Notifications:`Oznámení`,"Phone number":`Telefonní číslo`,"Save number":`Uložit číslo`,Transcript:`Přepis`,Captions:`Titulky`,"Translate audio":`Překládat zvuk`,"Audio language":`Jazyk zvuku`,"Audio options":`Možnosti zvuku`,"Keep background sound":`Zachovat zvuk na pozadí`,"Background level":`Hlasitost pozadí`,"Speaker voices":`Hlasy mluvčích`,"Any voice":`Libovolný hlas`,"Male voice":`Mužský hlas`,"Female voice":`Ženský hlas`,"Original audio.":`Původní zvuk.`,"Buy translated audio":`Koupit překlad zvuku`,"Audio credit":`Zvukový kredit`,"Buy pass":`Koupit přístup`,Pass:`Přístup`,"Pay with":`Zaplatit pomocí`,"Check payment":`Ověřit platbu`,"Open checkout":`Přejít k platbě`,"Free sessions":`Bezplatné relace`,"Usage rates and expiry":`Ceny a platnost`,Catalogs:`Katalogy`,"Add catalog":`Přidat katalog`,"Catalog name":`Název katalogu`,Link:`Odkaz`,"Play link":`Přehrát odkaz`,"Go live":`Vysílat živě`,Publish:`Zveřejnit`,Listen:`Poslouchat`,"Live now":`Právě živě`,Upcoming:`Připravované`,Parties:`Společná sledování`,"Join party":`Připojit se ke sledování`,"Invite code":`Kód pozvánky`,Invite:`Pozvat`,Chat:`Chat`,"Chat message":`Zpráva chatu`,People:`Lidé`,Speaker:`Mluvčí`,Speakers:`Mluvčí`,Listener:`Posluchač`,Moderator:`Moderátor`,"Raise hand":`Zvednout ruku`,"Hand raises":`Zvednuté ruce`,Questions:`Otázky`,Resources:`Materiály`,Replay:`Záznam`,Schedule:`Rozvrh`,Event:`Událost`,Host:`Pořadatel`,About:`O události`,Stage:`Pódium`,Title:`Název`,Description:`Popis`,Topic:`Téma`,Public:`Veřejné`,"Anyone with the link":`Kdokoli s odkazem`,"Invited people":`Pozvané osoby`,"Who can join":`Kdo se může připojit`,"Date and time":`Datum a čas`,"Expected length":`Očekávaná délka`,Repeat:`Opakovat`,"Does not repeat":`Neopakuje se`,"Every day":`Každý den`,"Every week":`Každý týden`,Homepage:`Webové stránky`,"Host name":`Jméno pořadatele`,"Profile photo URL":`URL profilové fotky`,"Broadcast link":`Odkaz na vysílání`,"Edit event":`Upravit událost`,"Save changes":`Uložit změny`,"Create live":`Vytvořit vysílání`,"Schedule class":`Naplánovat lekci`,"Start class":`Zahájit lekci`,"End event":`Ukončit událost`,"End session":`Ukončit relaci`,"Session ended":`Relace ukončena`,"Event ended":`Událost ukončena`,Cancelled:`Zrušeno`,"Replay available":`Záznam je k dispozici`,Join:`Připojit se`,"Create invite":`Vytvořit pozvánku`,"Invite as":`Pozvat jako`,"Watch now":`Sledovat nyní`,"Start a room":`Vytvořit místnost`,"Recent replays":`Nedávné záznamy`,"Your account":`Váš účet`,"Write with AI":`Psát pomocí AI`,"Suggested title":`Navrhovaný název`,"Suggested description":`Navrhovaný popis`,"Suggested topic":`Navrhované téma`,"Use these details":`Použít tyto údaje`,"Learn something live.":`Učte se naživo.`,"Screen share":`Sdílet obrazovku`,"Upload and broadcast":`Nahrát a vysílat`,Help:`Nápověda`,"Reload Player":`Znovu načíst přehrávač`,"Copy Bundled CLI Path":`Kopírovat cestu k přibalenému CLI`,"Developer tools":`Vývojářské nástroje`,Undo:`Zpět`,Redo:`Znovu`,Cut:`Vyjmout`,Paste:`Vložit`,"Select all":`Vybrat vše`,Status:`Stav`,"Activity log":`Protokol aktivit`,Admin:`Správa`,"Playback controls":`Ovládání přehrávání`,"Tracks and folders":`Skladby a složky`,"Original-language captions.":`Titulky v původním jazyce.`,"Skip to playback controls":`Přejít k ovládání přehrávání`,"Skip to playlist":`Přejít k seznamu skladeb`,"Skip to transcript and translated audio":`Přejít k přepisu a přeloženému zvuku`};export{t as default};
|