@crievo/omni 0.1.7 → 0.1.9
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/omnivideo.config.example.json +2 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/scenes-contract.md +3 -2
- package/reference/studio.md +3 -2
- package/studio/api/app.ts +4 -0
- package/studio/api/routes/cost.ts +13 -0
- package/studio/api/routes/me.ts +8 -0
- package/studio/api/services/agent-persona.ts +1 -0
- package/studio/api/services/agent-tools.ts +2 -2
- package/studio/api/services/cost.ts +22 -0
- package/studio/api/services/me.ts +67 -0
- package/studio/api/services/sessions.ts +8 -2
- package/studio/api/services/sound.ts +17 -6
- package/studio/runner/runner.ts +57 -10
- package/studio/shared/omni-screens.ts +1 -1
- package/studio/shared/schemas/cost.ts +10 -0
- package/studio/shared/types/api.ts +77 -1
- package/studio/studio.ts +27 -2
- package/studio/web/components/AppShell.tsx +11 -3
- package/studio/web/components/FirstConnection.tsx +222 -0
- package/studio/web/components/OfflineNotice.tsx +59 -0
- package/studio/web/components/SetupSteps.tsx +63 -0
- package/studio/web/components/Sidebar.tsx +10 -25
- package/studio/web/components/StudioOffline.tsx +48 -50
- package/studio/web/components/UserCard.tsx +79 -0
- package/studio/web/components/icons.tsx +2 -0
- package/studio/web/hooks/useComputers.ts +55 -0
- package/studio/web/hooks/useCost.ts +11 -0
- package/studio/web/hooks/useMe.ts +32 -0
- package/studio/web/hooks/useSetupProgress.ts +54 -0
- package/studio/web/lib/me.ts +17 -0
- package/studio/web/lib/query-client.ts +6 -0
- package/studio/web/lib/settings-categories.ts +2 -2
- package/studio/web/lib/setup-retry.ts +29 -0
- package/studio/web/lib/sound-timeline.ts +33 -0
- package/studio/web/lib/studio-offline.ts +105 -59
- package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
- package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
- package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
- package/studio/web/pages/SettingsPage/index.tsx +15 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +10 -8
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +31 -18
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/setup-steps.css +163 -0
- package/studio/web/styles/components/user-card.css +121 -0
- package/studio/web/styles/index.css +2 -0
- package/studio/web/styles/pages/settings/shell.css +79 -0
- package/studio/web/styles/pages/settings/usage.css +47 -0
- package/studio/web/styles/pages/studio-offline.css +224 -38
- package/tools/cost.mjs +114 -0
- package/tools/lib/agents.mjs +3 -2
- package/tools/lib/channel.mjs +1 -1
- package/tools/lib/cloud-user.mjs +58 -0
- package/tools/lib/cost.mjs +357 -0
- package/tools/lib/deps.mjs +27 -12
- package/tools/lib/oauth-client.mjs +28 -14
- package/tools/lib/pages-style.mjs +67 -0
- package/tools/lib/pairing.mjs +1 -1
- package/tools/lib/schedule/schedule.mjs +1 -1
- package/tools/lib/self-update.mjs +122 -0
- package/tools/lib/setup-progress.mjs +315 -0
- package/tools/lib/up.mjs +176 -61
- package/tools/metrics.mjs +6 -1
- package/tools/omni.mjs +41 -16
- package/tools/omnichat.mjs +45 -9
- package/tools/platforms.mjs +75 -14
- package/tools/runner.mjs +6 -6
- package/tools/setup.mjs +18 -5
- package/tools/sound.mjs +198 -94
package/tools/runner.mjs
CHANGED
|
@@ -67,7 +67,7 @@ async function login() {
|
|
|
67
67
|
fail(`${e.message}; rode de novo node tools/runner.mjs login`),
|
|
68
68
|
);
|
|
69
69
|
writeEnv({ [KEY]: paired.key });
|
|
70
|
-
console.log(`
|
|
70
|
+
console.log(`Máquina pareada como "${paired.name}". A chave ficou no .env (${KEY}).`);
|
|
71
71
|
console.log(
|
|
72
72
|
service().loaded
|
|
73
73
|
? "O studio (serviço) conecta sozinho em até 30 s."
|
|
@@ -119,25 +119,25 @@ async function status() {
|
|
|
119
119
|
if (!out.url) return console.log("Studio hospedado: não configurado (cloud.studio_url no config).");
|
|
120
120
|
console.log(`Studio hospedado: ${out.url}`);
|
|
121
121
|
console.log(serviceLine());
|
|
122
|
-
if (!out.hasKey) return console.log("
|
|
122
|
+
if (!out.hasKey) return console.log("Esta máquina: não pareada (node tools/runner.mjs login).");
|
|
123
123
|
if (!out.paired)
|
|
124
124
|
return console.log(
|
|
125
|
-
`
|
|
125
|
+
`Esta máquina: a chave não vale mais (${out.error}). O studio aberto pareia de novo sozinho (aprove na página que ele abre), ou: node tools/runner.mjs login`,
|
|
126
126
|
);
|
|
127
127
|
console.log(
|
|
128
|
-
`
|
|
128
|
+
`Esta máquina: pareada como "${out.name}", ${out.online ? "conectada agora" : "desconectada (o studio está aberto? veja o serviço acima)"}.`,
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async function logout() {
|
|
133
133
|
const url = studioUrl();
|
|
134
134
|
const token = env(KEY);
|
|
135
|
-
if (!token) return console.log("
|
|
135
|
+
if (!token) return console.log("Esta máquina já não está pareada.");
|
|
136
136
|
const r = await call(url, "/runner/logout", { token });
|
|
137
137
|
writeEnv({ [KEY]: "" });
|
|
138
138
|
if (!r.ok && r.status !== 401)
|
|
139
139
|
fail(`a chave saiu do .env, mas o studio não confirmou (${r.status}: ${r.body?.error ?? "?"})`);
|
|
140
|
-
console.log("
|
|
140
|
+
console.log("Máquina desconectada do studio hospedado. Para voltar: node tools/runner.mjs login");
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const [cmd, sub] = args;
|
package/tools/setup.mjs
CHANGED
|
@@ -49,6 +49,13 @@ const readJson = file => {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
/* the sound kits: the folders under kit/sfx/ (a kit is a folder with its kit.json) */
|
|
53
|
+
const listKits = dir =>
|
|
54
|
+
existsSync(dir)
|
|
55
|
+
? readdirSync(dir)
|
|
56
|
+
.filter(k => statSync(join(dir, k)).isDirectory())
|
|
57
|
+
.sort()
|
|
58
|
+
: [];
|
|
52
59
|
const toPath = p => resolve(ROOT, String(p).replace(/^~(?=\/|$)/, homedir()));
|
|
53
60
|
const cfg = () => readJson(CONFIG);
|
|
54
61
|
|
|
@@ -279,8 +286,12 @@ function check() {
|
|
|
279
286
|
|
|
280
287
|
/* 6. sound: the kit may start empty, but then Claude searches Epidemic through Chrome */
|
|
281
288
|
{
|
|
282
|
-
|
|
283
|
-
const
|
|
289
|
+
/* every kit folder under kit/sfx/ (motion-ui, epidemic-sounds…) counts its effects */
|
|
290
|
+
const kits = listKits(join(ROOT, "kit", "sfx"));
|
|
291
|
+
const n = kits.reduce(
|
|
292
|
+
(sum, k) => sum + Object.keys(readJson(join(ROOT, "kit", "sfx", k, "kit.json")) ?? {}).length,
|
|
293
|
+
0,
|
|
294
|
+
);
|
|
284
295
|
/* any Claude account that already connected the extension */
|
|
285
296
|
const ext = accountsOf(readJson(CONFIG) ?? {})
|
|
286
297
|
.filter(a => a.provider === "claude")
|
|
@@ -393,11 +404,13 @@ function prepare() {
|
|
|
393
404
|
mkdirSync(library, { recursive: true });
|
|
394
405
|
done.push(`criei a biblioteca de estilos visuais em ${library}`);
|
|
395
406
|
}
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
/* the kits: motion-ui is the default; epidemic-sounds holds the effects captured from Epidemic */
|
|
408
|
+
for (const name of ["motion-ui", "epidemic-sounds"]) {
|
|
409
|
+
const kit = join(ROOT, "kit", "sfx", name);
|
|
410
|
+
if (existsSync(join(kit, "kit.json"))) continue;
|
|
398
411
|
mkdirSync(kit, { recursive: true });
|
|
399
412
|
writeFileSync(join(kit, "kit.json"), "{}\n");
|
|
400
|
-
done.push(
|
|
413
|
+
done.push(`criei o kit de efeitos vazio (kit/sfx/${name}/kit.json)`);
|
|
401
414
|
}
|
|
402
415
|
mkdirSync(join(ROOT, "videos"), { recursive: true });
|
|
403
416
|
|
package/tools/sound.mjs
CHANGED
|
@@ -4,29 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
node tools/sound.mjs capture [n] prints the capture code (Chrome)
|
|
6
6
|
node tools/sound.mjs download videos/<slug> <url> [name] [--search <term>]
|
|
7
|
-
node tools/sound.mjs download kit <url> <name> --role <role> [--search <term>]
|
|
8
|
-
node tools/sound.mjs kit [role|text]
|
|
9
|
-
node tools/sound.mjs kit adopt videos/<slug> <name> --role <role> [--as <name-in-kit>]
|
|
10
|
-
moves an effect
|
|
7
|
+
node tools/sound.mjs download kit <url> <name> --role <role> [--kit <kit>] [--search <term>]
|
|
8
|
+
node tools/sound.mjs kit [role|text] [--kit <kit>] lists the kits (or one) by role
|
|
9
|
+
node tools/sound.mjs kit adopt videos/<slug> <name> --role <role> [--kit <kit>] [--as <name-in-kit>]
|
|
10
|
+
moves an effect downloaded for the video into a kit
|
|
11
|
+
node tools/sound.mjs kit add <kit> <file> --role <role> [--as <name>] [--title <text>]
|
|
12
|
+
a file the user chose becomes an MP3 of that kit
|
|
11
13
|
node tools/sound.mjs import videos/<slug> <file> [name] [--title <text>]
|
|
12
14
|
a track the user sent (the editor's upload), as an MP3 of the video's sfx/
|
|
13
15
|
node tools/sound.mjs effects videos/<slug> [text] lists the effects captured for the video
|
|
14
16
|
node tools/sound.mjs videos/<slug> prepares the video's sound/
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sfx/ (sfx/sources.json
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
config
|
|
25
|
-
(audiocdn.epidemicsound.com/lqmp3/<id>.mp3); `download` writes the MP3 to
|
|
26
|
-
the kit or the video's sfx/ and records the origin. An effect outside both
|
|
27
|
-
registries is refused. The name is looked up in the video first, then in the
|
|
28
|
-
kit. From the kit, the effect brings its role's volume (sound.roles in the
|
|
29
|
-
config, or its own `db` in kit.json), added to the sound's `db`.
|
|
18
|
+
Kits: one folder each under kit/sfx/ (its kit.json lists the effects, with
|
|
19
|
+
role and db), curated per sound palette. The default kit (sound.default_kit in
|
|
20
|
+
the config, motion-ui) is what a sound without `kit` uses; a scene's sound names
|
|
21
|
+
another with `"kit": "<name>"` (epidemic-sounds, for the effects captured from
|
|
22
|
+
Epidemic). The video's own sfx/ (sfx/sources.json, captured or sent by the user)
|
|
23
|
+
always comes first. A sound never falls back to another kit silently: if the
|
|
24
|
+
name lives only in another kit, the error says which one to name. Any provider's
|
|
25
|
+
audio can be in a kit; from a kit, the effect brings its role's volume (sound.roles
|
|
26
|
+
in the config, or its own `db` in kit.json), added to the sound's `db`.
|
|
30
27
|
|
|
31
28
|
Where sound goes in scenes.json:
|
|
32
29
|
beat.sound at the beat's instant
|
|
@@ -83,7 +80,7 @@
|
|
|
83
80
|
queue turn (tools/job-queue.mjs).
|
|
84
81
|
============================================================================ */
|
|
85
82
|
|
|
86
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
83
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, statSync, readdirSync } from "node:fs";
|
|
87
84
|
import { spawnSync } from "node:child_process";
|
|
88
85
|
import { join, resolve, basename, relative } from "node:path";
|
|
89
86
|
import { fileURLToPath } from "node:url";
|
|
@@ -124,7 +121,33 @@ const ROLES = {
|
|
|
124
121
|
texture: { db: 0, stretchable: true },
|
|
125
122
|
...CFG.sound?.roles,
|
|
126
123
|
};
|
|
127
|
-
|
|
124
|
+
/* the kits: one folder each under KITS (its kit.json lists the effects). The default kit is what a sound
|
|
125
|
+
without `kit` uses; the video's own sfx/ always comes first (effectFile) */
|
|
126
|
+
const KITS = resolve(ROOT, CFG.sound?.kit ?? "kit/sfx");
|
|
127
|
+
const DEFAULT_KIT = CFG.sound?.default_kit ?? "motion-ui";
|
|
128
|
+
const KIT_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
129
|
+
const kitDirOf = kit => join(KITS, kit);
|
|
130
|
+
const kitRegistry = kit => join(kitDirOf(kit), "kit.json");
|
|
131
|
+
/* a sound's file: its name and kit, or the ref the studio shows, "kit/name.mp3" (the kit comes first) */
|
|
132
|
+
const parseRef = (name, kit) => {
|
|
133
|
+
const i = typeof name === "string" ? name.indexOf("/") : -1;
|
|
134
|
+
return i < 0 ? { name, kit: kit ?? null } : { name: name.slice(i + 1), kit: name.slice(0, i) };
|
|
135
|
+
};
|
|
136
|
+
/* the kits that exist: each folder under KITS with a valid name */
|
|
137
|
+
const kitNames = () =>
|
|
138
|
+
existsSync(KITS)
|
|
139
|
+
? readdirSync(KITS)
|
|
140
|
+
.filter(k => KIT_NAME.test(k) && statSync(join(KITS, k)).isDirectory())
|
|
141
|
+
.sort()
|
|
142
|
+
: [];
|
|
143
|
+
/* a kit named by a command: valid, and existing unless `create` (kit add makes one) */
|
|
144
|
+
function checkKit(kit, { create = false } = {}) {
|
|
145
|
+
const k = kit ?? DEFAULT_KIT;
|
|
146
|
+
if (!KIT_NAME.test(k)) throw new Error(`kit “${k}”: use letras minúsculas, números e hífen (ex.: motion-ui)`);
|
|
147
|
+
if (!create && !existsSync(kitDirOf(k)))
|
|
148
|
+
throw new Error(`o kit “${k}” não existe (kits: ${kitNames().join(", ") || "nenhum"})`);
|
|
149
|
+
return k;
|
|
150
|
+
}
|
|
128
151
|
const CDN = /^https:\/\/audiocdn\.epidemicsound\.com\/lqmp3\/([0-9A-Za-z]+)\.mp3$/;
|
|
129
152
|
const NAME = /^[a-z0-9][a-z0-9._-]*\.mp3$/i;
|
|
130
153
|
const MUSIC_S = 30; /* above this it is a soundtrack, not an effect (the bed may) */
|
|
@@ -327,47 +350,60 @@ export function measureEffect(file) {
|
|
|
327
350
|
}
|
|
328
351
|
|
|
329
352
|
/* --- where an effect comes from --------------------------------------------------
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
looked up in the video first, then in the kit. */
|
|
353
|
+
Two places: the video's sfx/ (sfx/sources.json: captured for the video, or sent by the user), which
|
|
354
|
+
always wins, and one kit (kit/sfx/<kit>/kit.json): the sound's `kit`, else the default kit. A sound
|
|
355
|
+
never falls back to another kit silently: if the name is only in another kit, the error says so. */
|
|
334
356
|
const effectsDir = dir => join(resolve(dir), "sfx");
|
|
335
357
|
const readJson = f => (existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : {});
|
|
336
358
|
const sources = dir => readJson(join(effectsDir(dir), "sources.json"));
|
|
337
|
-
const kitSources =
|
|
359
|
+
const kitSources = kit => readJson(kitRegistry(kit));
|
|
338
360
|
|
|
339
|
-
|
|
361
|
+
/* the file of an effect for a video, and the kit entry (with its kit's name) when it comes from a kit */
|
|
362
|
+
export function effectFile(dir, name, kit) {
|
|
363
|
+
({ name, kit } = parseRef(name, kit));
|
|
340
364
|
if (typeof name !== "string" || !NAME.test(name) || basename(name) !== name)
|
|
341
|
-
return { error: `efeito “${name}”: o nome é um MP3 do sfx/ do vídeo ou
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (
|
|
365
|
+
return { error: `efeito “${name}”: o nome é um MP3 do sfx/ do vídeo ou de um kit (ex.: clique-seco.mp3)` };
|
|
366
|
+
const k = kit ?? DEFAULT_KIT;
|
|
367
|
+
if (!KIT_NAME.test(k)) return { error: `kit “${k}”: use letras minúsculas, números e hífen` };
|
|
368
|
+
let file,
|
|
369
|
+
entry = null,
|
|
370
|
+
where;
|
|
371
|
+
if (sources(dir)[name]) {
|
|
372
|
+
file = join(effectsDir(dir), name);
|
|
373
|
+
where = "sfx/sources.json";
|
|
374
|
+
} else if (kitSources(k)[name]) {
|
|
375
|
+
file = join(kitDirOf(k), name);
|
|
376
|
+
entry = { ...kitSources(k)[name], kitName: k };
|
|
377
|
+
where = `o kit ${k}`;
|
|
378
|
+
} else {
|
|
379
|
+
const elsewhere = kitNames().find(o => o !== k && kitSources(o)[name]);
|
|
348
380
|
return {
|
|
349
|
-
error:
|
|
381
|
+
error: elsewhere
|
|
382
|
+
? `efeito “${name}” não está no kit ${k}, mas está no kit ${elsewhere}: ponha "kit": "${elsewhere}" no som`
|
|
383
|
+
: `efeito “${name}” não está no kit ${k} nem no sfx/ deste vídeo (falta em sfx/sources.json): use um do kit (sound.mjs kit --kit ${k}) ou baixe com sound.mjs download`,
|
|
350
384
|
};
|
|
351
|
-
|
|
385
|
+
}
|
|
352
386
|
if (!existsSync(file))
|
|
353
387
|
return { error: `efeito “${name}” está em ${where}, mas o arquivo sumiu: rode sound.mjs download de novo` };
|
|
354
|
-
return { file, kit:
|
|
388
|
+
return { file, kit: entry };
|
|
355
389
|
}
|
|
356
390
|
|
|
357
391
|
/* `download` destination: a video dir or the kit ("kit", with --role) */
|
|
358
|
-
|
|
392
|
+
/* `download` destination: a video dir, or a kit ("kit", with --role and --kit) */
|
|
393
|
+
function destination(videoDir, role, kit) {
|
|
359
394
|
if (videoDir === "kit") {
|
|
360
395
|
if (!ROLES[role]) throw new Error(`no kit, --role é obrigatório: ${Object.keys(ROLES).join(", ")}`);
|
|
361
|
-
|
|
396
|
+
const k = checkKit(kit);
|
|
397
|
+
return { dir: kitDirOf(k), registry: kitRegistry(k), extra: { role }, kit: k };
|
|
362
398
|
}
|
|
363
399
|
const dir = resolve(videoDir ?? "");
|
|
364
400
|
if (!existsSync(join(dir, "scenes.json")))
|
|
365
401
|
throw new Error(`${dir} não é a pasta de um vídeo (falta scenes.json) nem "kit"`);
|
|
366
|
-
return { dir: effectsDir(dir), registry: join(effectsDir(dir), "sources.json"), extra: {} };
|
|
402
|
+
return { dir: effectsDir(dir), registry: join(effectsDir(dir), "sources.json"), extra: {}, kit: null };
|
|
367
403
|
}
|
|
368
404
|
|
|
369
|
-
async function download(videoDir, url, name, search, role) {
|
|
370
|
-
const { dir, registry, extra } = destination(videoDir, role);
|
|
405
|
+
async function download(videoDir, url, name, search, role, kit) {
|
|
406
|
+
const { dir, registry, extra, kit: k } = destination(videoDir, role, kit);
|
|
371
407
|
const m = CDN.exec(url ?? "");
|
|
372
408
|
if (!m)
|
|
373
409
|
throw new Error(
|
|
@@ -376,14 +412,15 @@ async function download(videoDir, url, name, search, role) {
|
|
|
376
412
|
name = `${(name ?? m[1]).replace(/\.mp3$/i, "")}.mp3`;
|
|
377
413
|
if (!NAME.test(name)) throw new Error(`nome “${name}”: letras, números, ponto, hífen e sublinhado`);
|
|
378
414
|
const list = readJson(registry);
|
|
379
|
-
const whereAlready =
|
|
415
|
+
const whereAlready = k ? `no kit ${k}` : "no vídeo";
|
|
380
416
|
const other = Object.entries(list).find(([n, f]) => f.id === m[1] && n !== name);
|
|
381
417
|
if (other) throw new Error(`esse efeito já está ${whereAlready} como “${other[0]}”`);
|
|
382
418
|
if (list[name] && list[name].id !== m[1])
|
|
383
419
|
throw new Error(
|
|
384
420
|
`já existe “${name}” ${whereAlready} com outro efeito (${list[name].title || list[name].id}): escolha outro nome`,
|
|
385
421
|
);
|
|
386
|
-
|
|
422
|
+
const clash = !k && kitNames().find(o => kitSources(o)[name]);
|
|
423
|
+
if (clash) console.log(` ⚠ o kit ${clash} já tem um “${name}”; o do vídeo passa na frente dele`);
|
|
387
424
|
|
|
388
425
|
const r = await fetch(url);
|
|
389
426
|
if (!r.ok) throw new Error(`a Epidemic respondeu ${r.status} para ${url}`);
|
|
@@ -402,7 +439,7 @@ async function download(videoDir, url, name, search, role) {
|
|
|
402
439
|
...extra,
|
|
403
440
|
};
|
|
404
441
|
writeFileSync(registry, JSON.stringify(list, null, 1) + "\n");
|
|
405
|
-
console.log(`${name} ← ${med.title || m[1]}${
|
|
442
|
+
console.log(`${name} ← ${med.title || m[1]}${k ? ` · kit ${k}, papel ${extra.role}` : ""}`);
|
|
406
443
|
console.log(
|
|
407
444
|
med.music
|
|
408
445
|
? ` ⚠ ${med.duration.toFixed(0)}s: é trilha, não efeito; só serve de música de fundo`
|
|
@@ -426,7 +463,9 @@ function importUpload(videoDir, file, wanted, title) {
|
|
|
426
463
|
.replace(/[^a-z0-9._-]+/g, "-")
|
|
427
464
|
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "") || "musica";
|
|
428
465
|
const taken = n =>
|
|
429
|
-
!!sources(dir)[`${n}.mp3`] ||
|
|
466
|
+
!!sources(dir)[`${n}.mp3`] ||
|
|
467
|
+
kitNames().some(o => !!kitSources(o)[`${n}.mp3`]) ||
|
|
468
|
+
existsSync(join(effectsDir(dir), `${n}.mp3`));
|
|
430
469
|
let n = slug;
|
|
431
470
|
for (let i = 2; taken(n); i++) n = `${slug}-${i}`;
|
|
432
471
|
const name = `${n}.mp3`;
|
|
@@ -491,55 +530,109 @@ function effects(videoDir, filter) {
|
|
|
491
530
|
}
|
|
492
531
|
}
|
|
493
532
|
|
|
494
|
-
/* moves an effect already
|
|
495
|
-
|
|
533
|
+
/* moves an effect already downloaded for a video into the kit, with its origin (never a user's upload) */
|
|
534
|
+
/* moves an effect already downloaded for a video into a kit, with its origin (never a user's upload) */
|
|
535
|
+
function adopt(videoDir, name, role, as, kit) {
|
|
496
536
|
const dir = resolve(videoDir ?? "");
|
|
497
537
|
if (!ROLES[role]) throw new Error(`--role é obrigatório: ${Object.keys(ROLES).join(", ")}`);
|
|
538
|
+
const k = checkKit(kit);
|
|
498
539
|
const source = sources(dir)[name];
|
|
499
540
|
if (!source) throw new Error(`“${name}” não está em ${join(effectsDir(dir), "sources.json")}`);
|
|
500
541
|
if (source.source === "upload")
|
|
501
|
-
throw new Error(`“${name}” foi enviado pelo usuário para este vídeo:
|
|
542
|
+
throw new Error(`“${name}” foi enviado pelo usuário para este vídeo: esse áudio não vai para o kit`);
|
|
502
543
|
const newName = `${(as ?? name).replace(/\.mp3$/i, "")}.mp3`;
|
|
503
544
|
if (!NAME.test(newName)) throw new Error(`nome “${newName}”: letras, números, ponto, hífen e sublinhado`);
|
|
504
|
-
const list = kitSources();
|
|
545
|
+
const list = kitSources(k);
|
|
505
546
|
const other = Object.entries(list).find(([, e]) => e.id === source.id);
|
|
506
|
-
if (other) throw new Error(`esse efeito já está no kit como “${other[0]}”`);
|
|
547
|
+
if (other) throw new Error(`esse efeito já está no kit ${k} como “${other[0]}”`);
|
|
507
548
|
if (list[newName])
|
|
508
|
-
throw new Error(
|
|
509
|
-
|
|
510
|
-
|
|
549
|
+
throw new Error(
|
|
550
|
+
`o kit ${k} já tem “${newName}” (${list[newName].title || list[newName].id}): use --as <outro-nome>`,
|
|
551
|
+
);
|
|
552
|
+
writeFileSync(join(kitDirOf(k), newName), readFileSync(join(effectsDir(dir), name)));
|
|
511
553
|
list[newName] = { ...source, role, adopted_from: basename(dir) };
|
|
512
|
-
writeFileSync(
|
|
513
|
-
console.log(`${newName} ← ${source.title || source.id} · kit, papel ${role}`);
|
|
554
|
+
writeFileSync(kitRegistry(k), JSON.stringify(list, null, 1) + "\n");
|
|
555
|
+
console.log(`${newName} ← ${source.title || source.id} · kit ${k}, papel ${role}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* a file the user chose (their own pack, a library they point at) becomes an MP3 of a kit, with its role.
|
|
559
|
+
Nothing is picked on its own: the path comes from the command. A new kit is made here. */
|
|
560
|
+
function addToKit(kit, file, role, as, title) {
|
|
561
|
+
const k = checkKit(kit, { create: true });
|
|
562
|
+
if (!ROLES[role]) throw new Error(`--role é obrigatório: ${Object.keys(ROLES).join(", ")}`);
|
|
563
|
+
if (!file || !existsSync(file)) throw new Error(`o arquivo “${file}” não existe`);
|
|
564
|
+
const base = (as || basename(file)).replace(/\.[A-Za-z0-9]{1,5}$/, "");
|
|
565
|
+
const slug =
|
|
566
|
+
base
|
|
567
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
568
|
+
.normalize("NFD")
|
|
569
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
570
|
+
.toLowerCase()
|
|
571
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
572
|
+
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "") || "efeito";
|
|
573
|
+
const list = kitSources(k);
|
|
574
|
+
let n = slug;
|
|
575
|
+
for (let i = 2; list[`${n}.mp3`] || existsSync(join(kitDirOf(k), `${n}.mp3`)); i++) n = `${slug}-${i}`;
|
|
576
|
+
const name = `${n}.mp3`;
|
|
577
|
+
mkdirSync(kitDirOf(k), { recursive: true });
|
|
578
|
+
const out = join(kitDirOf(k), name);
|
|
579
|
+
const isMp3 = /\.mp3$/i.test(file);
|
|
580
|
+
run("ffmpeg", [
|
|
581
|
+
"-y",
|
|
582
|
+
"-v",
|
|
583
|
+
"error",
|
|
584
|
+
"-i",
|
|
585
|
+
file,
|
|
586
|
+
"-vn",
|
|
587
|
+
"-map_metadata",
|
|
588
|
+
"-1",
|
|
589
|
+
...(isMp3 ? ["-c:a", "copy"] : ["-c:a", "libmp3lame", "-q:a", "2"]),
|
|
590
|
+
out,
|
|
591
|
+
]);
|
|
592
|
+
cache.delete(out);
|
|
593
|
+
list[name] = {
|
|
594
|
+
title: title || basename(file).replace(/\.[A-Za-z0-9]{1,5}$/, ""),
|
|
595
|
+
role,
|
|
596
|
+
added_at: new Date().toISOString().slice(0, 10),
|
|
597
|
+
};
|
|
598
|
+
writeFileSync(kitRegistry(k), JSON.stringify(list, null, 1) + "\n");
|
|
599
|
+
console.log(`${name} ← ${list[name].title} · kit ${k}, papel ${role}`);
|
|
600
|
+
console.log(` ${describe(measureEffect(out))}`);
|
|
514
601
|
}
|
|
515
602
|
|
|
516
603
|
/* the kit grouped by role; `filter` is a role or a piece of the name/title */
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
console.log(`o kit está vazio (${KIT}): baixe com sound.mjs download kit <url> <nome> --role <papel>`);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
604
|
+
/* the kits grouped by role; `filter` is a role or a piece of the name/title; `only` names one kit */
|
|
605
|
+
function kit(filter, only) {
|
|
606
|
+
const names = only ? [checkKit(only)] : kitNames();
|
|
523
607
|
const f = filter?.toLowerCase();
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (!existsSync(file)) {
|
|
534
|
-
console.log(` ${name} ⚠ arquivo sumiu`);
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
const m = measureEffect(file);
|
|
538
|
-
console.log(
|
|
539
|
-
` ${name.padEnd(28)} ${m.music ? `${m.duration.toFixed(0)}s · trilha` : describe(m)}${e.db != null ? ` · db próprio ${e.db}` : ""}`,
|
|
608
|
+
let any = false;
|
|
609
|
+
for (const k of names) {
|
|
610
|
+
const list = Object.entries(kitSources(k));
|
|
611
|
+
if (!list.length) continue;
|
|
612
|
+
any = true;
|
|
613
|
+
console.log(`== kit ${k}${k === DEFAULT_KIT ? " (padrão)" : ""} ==`);
|
|
614
|
+
for (const role of Object.keys(ROLES)) {
|
|
615
|
+
const ofRole = list.filter(
|
|
616
|
+
([n, e]) => e.role === role && (!f || f === role || `${n} ${e.title}`.toLowerCase().includes(f)),
|
|
540
617
|
);
|
|
618
|
+
if (!ofRole.length) continue;
|
|
619
|
+
const p = ROLES[role];
|
|
620
|
+
console.log(`${role} (db ${p.db}${p.stretchable ? ", estica" : ""})`);
|
|
621
|
+
for (const [name, e] of ofRole) {
|
|
622
|
+
const file = join(kitDirOf(k), name);
|
|
623
|
+
if (!existsSync(file)) {
|
|
624
|
+
console.log(` ${name} ⚠ arquivo sumiu`);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const m = measureEffect(file);
|
|
628
|
+
console.log(
|
|
629
|
+
` ${name.padEnd(28)} ${m.music ? `${m.duration.toFixed(0)}s · trilha` : describe(m)}${e.db != null ? ` · db próprio ${e.db}` : ""}`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
541
632
|
}
|
|
542
633
|
}
|
|
634
|
+
if (!any)
|
|
635
|
+
console.log(`os kits estão vazios (${KITS}): adicione com sound.mjs kit add <kit> <arquivo> --role <papel>`);
|
|
543
636
|
}
|
|
544
637
|
|
|
545
638
|
function capture(n) {
|
|
@@ -740,13 +833,15 @@ function pair(v, name, max, nullAtEnd = false) {
|
|
|
740
833
|
}
|
|
741
834
|
|
|
742
835
|
export function sanitizeSound(dir, s) {
|
|
743
|
-
const
|
|
744
|
-
if (!
|
|
745
|
-
const {
|
|
836
|
+
const given = typeof s === "string" ? { name: s } : s;
|
|
837
|
+
if (!given || typeof given !== "object") throw new Error("som inválido");
|
|
838
|
+
const o = { ...given, ...parseRef(given.name, given.kit) };
|
|
839
|
+
const { file, error, kit } = effectFile(dir, o.name, o.kit);
|
|
746
840
|
if (error) throw new Error(error);
|
|
747
841
|
const m = measureEffect(file);
|
|
748
842
|
if (m.music) throw new Error(`“${o.name}” tem ${m.duration.toFixed(0)}s: é trilha, só serve de música de fundo`);
|
|
749
843
|
const v = { name: o.name };
|
|
844
|
+
if (kit) v.kit = kit.kitName;
|
|
750
845
|
if (o.anchor === "start") v.anchor = "start";
|
|
751
846
|
else if (o.anchor != null && o.anchor !== "peak") throw new Error(`âncora: ${o.anchor} (use peak ou start)`);
|
|
752
847
|
for (const k of ["offset", "db", "dur", "speed", "pitch"]) {
|
|
@@ -768,10 +863,12 @@ export function sanitizeSound(dir, s) {
|
|
|
768
863
|
|
|
769
864
|
export function sanitizeBed(dir, b, total) {
|
|
770
865
|
if (b == null) return null;
|
|
771
|
-
const
|
|
772
|
-
const
|
|
866
|
+
const given = typeof b === "string" ? { name: b } : b;
|
|
867
|
+
const o = { ...given, ...parseRef(given.name, given.kit) };
|
|
868
|
+
const { error, kit } = effectFile(dir, o.name, o.kit);
|
|
773
869
|
if (error) throw new Error(`música de fundo: ${error}`);
|
|
774
870
|
const v = { name: o.name };
|
|
871
|
+
if (kit) v.kit = kit.kitName;
|
|
775
872
|
if (o.db != null && +o.db !== 0) v.db = number(o.db, [-40, 20], "música de fundo db");
|
|
776
873
|
if (o.from != null && +o.from > 0) v.from = number(o.from, [0, total], "música de fundo from");
|
|
777
874
|
if (o.to != null) v.to = number(o.to, [0, total], "música de fundo to");
|
|
@@ -839,13 +936,15 @@ export function sanitizeSpeed(s) {
|
|
|
839
936
|
|
|
840
937
|
/* what the studio offers: the kit by role and what was captured for the video, measured */
|
|
841
938
|
export function soundCatalog(dir) {
|
|
842
|
-
const item = (from, name, e, origin) => {
|
|
939
|
+
const item = (from, name, e, origin, kit = null) => {
|
|
843
940
|
const file = join(from, name);
|
|
844
941
|
if (!existsSync(file)) return null;
|
|
845
942
|
const m = measureEffect(file);
|
|
846
943
|
return {
|
|
847
|
-
name
|
|
944
|
+
/* a kit's effect is named by its ref, "kit/name.mp3": the studio shows and sends that one */
|
|
945
|
+
name: kit ? `${kit}/${name}` : name,
|
|
848
946
|
origin,
|
|
947
|
+
kit,
|
|
849
948
|
role: e.role ?? null,
|
|
850
949
|
title: e.title ?? "",
|
|
851
950
|
music: m.music,
|
|
@@ -856,19 +955,21 @@ export function soundCatalog(dir) {
|
|
|
856
955
|
};
|
|
857
956
|
};
|
|
858
957
|
return {
|
|
859
|
-
|
|
860
|
-
|
|
958
|
+
kits: kitNames().map(k => ({ name: k, default: k === DEFAULT_KIT, count: Object.keys(kitSources(k)).length })),
|
|
959
|
+
kit: kitNames()
|
|
960
|
+
.flatMap(k => Object.entries(kitSources(k)).map(([n, e]) => item(kitDirOf(k), n, e, "kit", k)))
|
|
861
961
|
.filter(Boolean),
|
|
862
962
|
video: Object.entries(sources(dir))
|
|
863
963
|
.map(([n, e]) => item(effectsDir(dir), n, e, "video"))
|
|
864
964
|
.filter(Boolean),
|
|
865
965
|
roles: ROLES,
|
|
966
|
+
defaultKit: DEFAULT_KIT,
|
|
866
967
|
level_db: S.level_db,
|
|
867
968
|
ducking: S.ducking,
|
|
868
969
|
bed: S.bed,
|
|
869
970
|
};
|
|
870
971
|
}
|
|
871
|
-
export
|
|
972
|
+
export { KITS, DEFAULT_KIT, kitDirOf, kitNames };
|
|
872
973
|
|
|
873
974
|
/* --- the bed's clock -----------------------------------------------------------------
|
|
874
975
|
The bed is isolated: the editor's area cuts and the speed (`cuts` and `speed` at the root of
|
|
@@ -914,7 +1015,7 @@ export function planSound(dir, video, { outDir, scenes = null, withBed = true, c
|
|
|
914
1015
|
for (const p of requests(video)) {
|
|
915
1016
|
if (scenes && !scenes.has(p.scene)) continue;
|
|
916
1017
|
const { s, id } = p;
|
|
917
|
-
const { file, error, kit } = effectFile(dir, s.name);
|
|
1018
|
+
const { file, error, kit } = effectFile(dir, s.name, s.kit);
|
|
918
1019
|
if (error) throw new Error(`${id}: ${error}`);
|
|
919
1020
|
const m = measureEffect(file);
|
|
920
1021
|
if (m.music)
|
|
@@ -1023,7 +1124,7 @@ export function planSound(dir, video, { outDir, scenes = null, withBed = true, c
|
|
|
1023
1124
|
layer: e.layer,
|
|
1024
1125
|
scene: e.scene,
|
|
1025
1126
|
element: e.element,
|
|
1026
|
-
name: e.name,
|
|
1127
|
+
name: e.kit ? `${e.kit.kitName}/${e.name}` : e.name,
|
|
1027
1128
|
file: wav,
|
|
1028
1129
|
wav: relative(dir, wav),
|
|
1029
1130
|
start: +e.start.toFixed(3),
|
|
@@ -1041,6 +1142,7 @@ export function planSound(dir, video, { outDir, scenes = null, withBed = true, c
|
|
|
1041
1142
|
cut_start: +e.cut.toFixed(3),
|
|
1042
1143
|
...(e.gave_way ? { gave_way: e.gave_way } : {}),
|
|
1043
1144
|
origin: e.kit ? "kit" : "video",
|
|
1145
|
+
kit: e.kit?.kitName ?? null,
|
|
1044
1146
|
role: e.kit?.role ?? null,
|
|
1045
1147
|
kit_db: e.kitDb,
|
|
1046
1148
|
});
|
|
@@ -1120,7 +1222,7 @@ function giveWay(a, b) {
|
|
|
1120
1222
|
function makeBed(dir, video, { outDir, voice, ducking, warnings, clock }) {
|
|
1121
1223
|
const b = typeof video.bed === "string" ? { name: video.bed } : video.bed;
|
|
1122
1224
|
const total = video.total_duration;
|
|
1123
|
-
const { file, error } = effectFile(dir, b.name);
|
|
1225
|
+
const { file, error } = effectFile(dir, b.name, b.kit);
|
|
1124
1226
|
if (error) throw new Error(`música de fundo: ${error}`);
|
|
1125
1227
|
const start = clock.at(Math.max(0, num(b.from, 0))),
|
|
1126
1228
|
end = clock.at(Math.min(total, num(b.to, total)));
|
|
@@ -1358,13 +1460,15 @@ if (isMainModule(import.meta.url)) {
|
|
|
1358
1460
|
const option = n => (i => (i < 0 ? undefined : rest.splice(i, 2)[1]))(rest.indexOf(`--${n}`));
|
|
1359
1461
|
const search = option("search"),
|
|
1360
1462
|
role = option("role"),
|
|
1361
|
-
title = option("title")
|
|
1463
|
+
title = option("title"),
|
|
1464
|
+
kitOpt = option("kit");
|
|
1362
1465
|
if (cmd === "capture") capture(+(rest[0] ?? 8));
|
|
1363
|
-
else if (cmd === "download") await download(rest[0], rest[1], rest[2], search, role);
|
|
1466
|
+
else if (cmd === "download") await download(rest[0], rest[1], rest[2], search, role, kitOpt);
|
|
1364
1467
|
else if (cmd === "import") importUpload(rest[0], rest[1], rest[2], title);
|
|
1365
1468
|
else if (cmd === "effects") effects(rest[0], rest[1]);
|
|
1366
|
-
else if (cmd === "kit" && rest[0] === "adopt") adopt(rest[1], rest[2], role, option("as") ?? rest[3]);
|
|
1367
|
-
else if (cmd === "kit")
|
|
1469
|
+
else if (cmd === "kit" && rest[0] === "adopt") adopt(rest[1], rest[2], role, option("as") ?? rest[3], kitOpt);
|
|
1470
|
+
else if (cmd === "kit" && rest[0] === "add") addToKit(rest[1], rest[2], role, option("as") ?? rest[3], title);
|
|
1471
|
+
else if (cmd === "kit") kit(rest[0], kitOpt);
|
|
1368
1472
|
else if (cmd && existsSync(join(resolve(cmd), "scenes.json"))) {
|
|
1369
1473
|
await turn("sound", cmd);
|
|
1370
1474
|
prepareSound(cmd);
|