@odori/cli 0.0.3 → 0.0.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/{chunk-RXLB2CXH.js → chunk-RHG23EWW.js} +457 -241
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +45 -5
- package/dist/index.js +3 -3
- package/dist/registry-snapshot-JEVXYGS2.js +4868 -0
- package/package.json +3 -3
- package/src/assets.ts +90 -0
- package/src/brand-file.ts +16 -4
- package/src/cli.ts +38 -13
- package/src/commands/add.ts +37 -1
- package/src/commands/dev.ts +32 -2
- package/src/commands/doctor.ts +47 -2
- package/src/commands/exportVideo.ts +6 -0
- package/src/commands/{still.ts → frame.ts} +23 -9
- package/src/commands/update.ts +58 -7
- package/src/discovery.ts +63 -2
- package/src/index.ts +1 -1
- package/src/jobs.ts +1 -1
- package/src/registry-snapshot.json +1529 -327
- package/src/registry-source.ts +37 -2
- package/src/render.ts +8 -1
- package/src/server.ts +7 -1
- package/studio/src/Studio.tsx +6 -22
- package/studio/src/components/ExportPanel.tsx +90 -6
- package/studio/src/components/Inspector.tsx +101 -1
- package/studio/src/components/Navigator.tsx +149 -0
- package/studio/src/components/Settings.tsx +109 -0
- package/studio/src/components/Transport.tsx +98 -55
- package/studio/src/components/ui.tsx +16 -1
- package/studio/src/lib/highlight.ts +85 -0
- package/studio/src/settings.ts +87 -0
- package/studio/src/studio.css +350 -8
- package/studio/src/views/BrandsView.tsx +18 -1
- package/studio/src/views/ComponentsView.tsx +191 -26
- package/studio/src/views/HomeView.tsx +7 -4
- package/studio/src/views/VideosView.tsx +33 -6
- package/studio/src/virtual.d.ts +4 -1
- package/dist/registry-snapshot-BDP6PVYB.js +0 -3559
|
@@ -20,9 +20,9 @@ var log = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// src/commands/add.ts
|
|
23
|
-
import { mkdir as
|
|
24
|
-
import { existsSync as
|
|
25
|
-
import { relative as
|
|
23
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
24
|
+
import { existsSync as existsSync7 } from "fs";
|
|
25
|
+
import { relative as relative5, resolve as resolve7 } from "path";
|
|
26
26
|
import { hashString as hashString2 } from "odori";
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
@@ -86,6 +86,7 @@ var brandFiles = async (config) => {
|
|
|
86
86
|
return found.sort((left, right) => left.depth - right.depth).map((entry) => entry.file);
|
|
87
87
|
};
|
|
88
88
|
var registerCueInBrand = async (config, cue, componentName) => {
|
|
89
|
+
const url = "url" in cue ? cue.url : null;
|
|
89
90
|
for (const file of await brandFiles(config)) {
|
|
90
91
|
const source = await readFile(file, "utf8");
|
|
91
92
|
if (source.includes(`"${cue.name}"`) || source.includes(`'${cue.name}'`)) {
|
|
@@ -93,19 +94,24 @@ var registerCueInBrand = async (config, cue, componentName) => {
|
|
|
93
94
|
}
|
|
94
95
|
const block = source.match(/(audio:\s*\{[\s\S]*?cues:\s*\{)([\s\S]*?)(\n(\s*)\},)/);
|
|
95
96
|
const inline = source.match(/(audio:\s*\{[^\n}]*cues:\s*\{)([^\n{}]*)(\})/);
|
|
97
|
+
const value = url ? JSON.stringify(url) : `${cue.export}()`;
|
|
96
98
|
let withCue;
|
|
97
99
|
if (block) {
|
|
98
100
|
const indent = `${block[4]} `;
|
|
99
101
|
const entry = `
|
|
100
|
-
${indent}"${cue.name}": ${
|
|
102
|
+
${indent}"${cue.name}": ${value},`;
|
|
101
103
|
withCue = source.replace(block[0], `${block[1]}${block[2]}${entry}${block[3]}`);
|
|
102
104
|
} else if (inline) {
|
|
103
105
|
const existing = inline[2].trim();
|
|
104
|
-
const entry = `"${cue.name}": ${
|
|
106
|
+
const entry = `"${cue.name}": ${value}`;
|
|
105
107
|
withCue = source.replace(inline[0], `${inline[1]}${existing ? `${existing.replace(/,$/, "")}, ` : ""}${entry}${inline[3]}`);
|
|
106
108
|
} else {
|
|
107
109
|
continue;
|
|
108
110
|
}
|
|
111
|
+
if (url) {
|
|
112
|
+
await writeFile(file, withCue, "utf8");
|
|
113
|
+
return { file: relative(config.root, file), already: false };
|
|
114
|
+
}
|
|
109
115
|
const from = resolve2(config.root, config.componentsDir, componentName, componentName);
|
|
110
116
|
const specifier = relative(resolve2(file, ".."), from).split("\\").join("/");
|
|
111
117
|
const importLine = `import {${cue.export}} from "${specifier.startsWith(".") ? specifier : `./${specifier}`}";`;
|
|
@@ -288,12 +294,21 @@ var resolveWithinRoot = (root, target) => {
|
|
|
288
294
|
};
|
|
289
295
|
var DEFAULT_URL = "https://odori.dev/r/v1";
|
|
290
296
|
var registryUrl = (config) => (config.registryUrl ?? process.env.ODORI_REGISTRY ?? DEFAULT_URL).replace(/\/$/, "");
|
|
297
|
+
var registryOrigin = (config) => {
|
|
298
|
+
const url = registryUrl(config);
|
|
299
|
+
try {
|
|
300
|
+
return new URL(url).origin;
|
|
301
|
+
} catch {
|
|
302
|
+
return url.replace(/\/r(\/v\d+)?$/, "");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
291
305
|
var cacheDir = (url) => resolve4(cacheRoot(), "registry", createHash("sha256").update(url).digest("hex").slice(0, 16));
|
|
292
306
|
var toComponent = (item) => ({
|
|
293
307
|
name: item.name,
|
|
294
308
|
namespaced: item.meta?.namespaced ?? `@odori/${item.name}`,
|
|
295
309
|
kind: item.meta?.kind ?? "component",
|
|
296
310
|
...item.meta?.cue ? { cue: item.meta.cue } : {},
|
|
311
|
+
...item.meta?.asset ? { asset: item.meta.asset } : {},
|
|
297
312
|
family: item.meta?.family ?? "Uncategorized",
|
|
298
313
|
description: item.description ?? "",
|
|
299
314
|
files: item.files.map((file) => file.path.split("/").pop() ?? file.path),
|
|
@@ -302,7 +317,7 @@ var toComponent = (item) => ({
|
|
|
302
317
|
});
|
|
303
318
|
var snapshotItems = async () => {
|
|
304
319
|
try {
|
|
305
|
-
const loaded = await import("./registry-snapshot-
|
|
320
|
+
const loaded = await import("./registry-snapshot-JEVXYGS2.js");
|
|
306
321
|
return loaded.default.items;
|
|
307
322
|
} catch {
|
|
308
323
|
throw new Error(
|
|
@@ -393,10 +408,67 @@ Nothing was written. This is a truncated download, a stale proxy, or a tampered
|
|
|
393
408
|
);
|
|
394
409
|
};
|
|
395
410
|
|
|
396
|
-
// src/
|
|
397
|
-
import {
|
|
411
|
+
// src/assets.ts
|
|
412
|
+
import { createHash as createHash2 } from "crypto";
|
|
398
413
|
import { existsSync as existsSync5 } from "fs";
|
|
399
|
-
import {
|
|
414
|
+
import { mkdir as mkdir3, readFile as readFile4, rm, writeFile as writeFile4 } from "fs/promises";
|
|
415
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve5 } from "path";
|
|
416
|
+
var assetCache = (integrity) => resolve5(cacheRoot(), "assets", createHash2("sha256").update(integrity).digest("hex").slice(0, 16));
|
|
417
|
+
var verify = (bytes, expected, source) => {
|
|
418
|
+
const actual = `sha256-${createHash2("sha256").update(bytes).digest("base64")}`;
|
|
419
|
+
if (actual === expected) return;
|
|
420
|
+
throw new Error(
|
|
421
|
+
`The bytes at ${source} do not match the hash the registry published.
|
|
422
|
+
expected ${expected}
|
|
423
|
+
received ${actual}
|
|
424
|
+
Nothing was written. This is a truncated download, a stale proxy, or a tampered file.`
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
var installAsset = async (config, component, options = {}) => {
|
|
428
|
+
const asset = component.asset;
|
|
429
|
+
if (!asset) return false;
|
|
430
|
+
const destination = resolveWithinRoot(config.root, asset.target);
|
|
431
|
+
const exists = existsSync5(destination);
|
|
432
|
+
log.detail(` ${exists ? "replace" : "create "} ${relative3(config.root, destination)}`);
|
|
433
|
+
if (options.dryRun) return false;
|
|
434
|
+
if (exists && !options.force) {
|
|
435
|
+
log.warn(`${relative3(config.root, destination)} already exists. Keeping it. Use --force to replace it.`);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
const cached = assetCache(asset.integrity);
|
|
439
|
+
let bytes = null;
|
|
440
|
+
if (existsSync5(cached)) {
|
|
441
|
+
const stored = await readFile4(cached);
|
|
442
|
+
try {
|
|
443
|
+
verify(stored, asset.integrity, cached);
|
|
444
|
+
bytes = stored;
|
|
445
|
+
} catch {
|
|
446
|
+
log.detail(" the cached copy did not verify; downloading it again");
|
|
447
|
+
await rm(cached, { force: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!bytes) {
|
|
451
|
+
const url = new URL(asset.url, `${registryOrigin(config)}/`).toString();
|
|
452
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
453
|
+
if (!response.ok) throw new Error(`Could not download ${url}: ${response.status} ${response.statusText}`);
|
|
454
|
+
const downloaded = new Uint8Array(await response.arrayBuffer());
|
|
455
|
+
verify(downloaded, asset.integrity, url);
|
|
456
|
+
await mkdir3(dirname3(cached), { recursive: true });
|
|
457
|
+
await writeFile4(cached, downloaded);
|
|
458
|
+
bytes = downloaded;
|
|
459
|
+
}
|
|
460
|
+
await mkdir3(dirname3(destination), { recursive: true });
|
|
461
|
+
await writeFile4(destination, bytes);
|
|
462
|
+
log.success(`${component.namespaced} to ${relative3(config.root, destination)}`);
|
|
463
|
+
log.detail(` ${component.family} \xB7 ${Math.round(asset.bytes / 1024)} KB \xB7 answers to "${asset.cue}"`);
|
|
464
|
+
return true;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/commands/update.ts
|
|
468
|
+
import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
469
|
+
import { existsSync as existsSync6 } from "fs";
|
|
470
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
471
|
+
import { relative as relative4, resolve as resolve6 } from "path";
|
|
400
472
|
import { hashString } from "odori";
|
|
401
473
|
|
|
402
474
|
// src/diff.ts
|
|
@@ -459,16 +531,24 @@ var formatDiff = (lines, context = 2) => {
|
|
|
459
531
|
};
|
|
460
532
|
|
|
461
533
|
// src/commands/update.ts
|
|
462
|
-
var
|
|
534
|
+
var LOCKFILE = "odori.lock.json";
|
|
535
|
+
var provenanceFile = (config) => resolve6(config.root, LOCKFILE);
|
|
536
|
+
var legacyProvenanceFile = (config) => resolve6(config.root, config.outDir, "components.json");
|
|
463
537
|
var readProvenance = async (config) => {
|
|
464
|
-
const file = provenanceFile(config);
|
|
465
|
-
if (!
|
|
466
|
-
|
|
538
|
+
const file = existsSync6(provenanceFile(config)) ? provenanceFile(config) : legacyProvenanceFile(config);
|
|
539
|
+
if (!existsSync6(file)) return {};
|
|
540
|
+
try {
|
|
541
|
+
return JSON.parse(await readFile5(file, "utf8"));
|
|
542
|
+
} catch {
|
|
543
|
+
log.warn(`${relative4(config.root, file)} is not readable JSON. Ignoring it.`);
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
467
546
|
};
|
|
468
547
|
var writeProvenance = async (config, provenance) => {
|
|
469
|
-
await
|
|
470
|
-
await
|
|
548
|
+
await mkdir4(config.root, { recursive: true });
|
|
549
|
+
await writeFile5(provenanceFile(config), `${JSON.stringify(provenance, null, 2)}
|
|
471
550
|
`, "utf8");
|
|
551
|
+
await rm2(legacyProvenanceFile(config), { force: true });
|
|
472
552
|
};
|
|
473
553
|
var componentStatus = async (config, only) => {
|
|
474
554
|
const { items: registry } = await resolveRegistry(config);
|
|
@@ -483,10 +563,10 @@ var componentStatus = async (config, only) => {
|
|
|
483
563
|
const upstreamFiles = new Map(item.files.map((file) => [file.path.split("/").pop() ?? file.path, file.content]));
|
|
484
564
|
const files = await Promise.all(
|
|
485
565
|
component.files.map(async (file) => {
|
|
486
|
-
const localPath =
|
|
566
|
+
const localPath = resolve6(config.root, config.componentsDir, name, file);
|
|
487
567
|
const content = upstreamFiles.get(file);
|
|
488
568
|
if (content === void 0) throw new Error(`The registry document for "${name}" has no file named ${file}.`);
|
|
489
|
-
const local =
|
|
569
|
+
const local = existsSync6(localPath) ? hashString(await readFile5(localPath, "utf8")) : null;
|
|
490
570
|
return {
|
|
491
571
|
file,
|
|
492
572
|
localPath,
|
|
@@ -512,25 +592,39 @@ var LABELS = {
|
|
|
512
592
|
diverged: "modified locally and updated upstream",
|
|
513
593
|
missing: "files missing"
|
|
514
594
|
};
|
|
595
|
+
var explainEmpty = async (config, named) => {
|
|
596
|
+
if (named.length > 0) {
|
|
597
|
+
log.detail(`${named.join(", ")} ${named.length === 1 ? "is" : "are"} not recorded in ${LOCKFILE}.`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const components = resolve6(config.root, config.componentsDir);
|
|
601
|
+
const installed = existsSync6(components) ? (await readdir2(components)).filter((e) => !e.startsWith(".")) : [];
|
|
602
|
+
if (installed.length === 0) {
|
|
603
|
+
log.detail("No registry components are installed yet. Run odori add first.");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
log.warn(`${installed.length} components are in ${config.componentsDir} but none are recorded in ${LOCKFILE}.`);
|
|
607
|
+
log.detail("Run odori add <name> to re-record them, or commit the lockfile if a teammate has one.");
|
|
608
|
+
};
|
|
515
609
|
var diffCommand = async (names, options = {}) => {
|
|
516
610
|
const config = await loadConfig(process.cwd());
|
|
517
611
|
const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
|
|
518
612
|
if (statuses.length === 0) {
|
|
519
|
-
|
|
613
|
+
await explainEmpty(config, names);
|
|
520
614
|
return;
|
|
521
615
|
}
|
|
522
616
|
for (const status of statuses) {
|
|
523
617
|
log.title(`@odori/${status.name} ${LABELS[status.state]}`);
|
|
524
618
|
for (const file of status.files) {
|
|
525
619
|
if (file.local === null) {
|
|
526
|
-
log.error(` ${file.file} is missing from ${
|
|
620
|
+
log.error(` ${file.file} is missing from ${relative4(config.root, resolve6(file.localPath, ".."))}`);
|
|
527
621
|
continue;
|
|
528
622
|
}
|
|
529
623
|
if (file.local === file.upstream) {
|
|
530
624
|
log.detail(` ${file.file} identical to upstream`);
|
|
531
625
|
continue;
|
|
532
626
|
}
|
|
533
|
-
const lines = diffLines(await
|
|
627
|
+
const lines = diffLines(await readFile5(file.localPath, "utf8"), file.content);
|
|
534
628
|
const { added, removed } = countChanges(lines);
|
|
535
629
|
log.info(` ${file.file} +${added} -${removed} against upstream`);
|
|
536
630
|
if (options.full) for (const line of formatDiff(lines)) log.detail(` ${line}`);
|
|
@@ -542,7 +636,7 @@ var updateCommand = async (names, options = {}) => {
|
|
|
542
636
|
const config = await loadConfig(process.cwd());
|
|
543
637
|
const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
|
|
544
638
|
if (statuses.length === 0) {
|
|
545
|
-
|
|
639
|
+
await explainEmpty(config, names);
|
|
546
640
|
return;
|
|
547
641
|
}
|
|
548
642
|
const provenance = await readProvenance(config);
|
|
@@ -563,8 +657,8 @@ var updateCommand = async (names, options = {}) => {
|
|
|
563
657
|
continue;
|
|
564
658
|
}
|
|
565
659
|
for (const file of status.files) {
|
|
566
|
-
await
|
|
567
|
-
await
|
|
660
|
+
await mkdir4(resolve6(file.localPath, ".."), { recursive: true });
|
|
661
|
+
await writeFile5(file.localPath, file.content, "utf8");
|
|
568
662
|
}
|
|
569
663
|
provenance[status.name] = {
|
|
570
664
|
source: `@odori/${status.name}`,
|
|
@@ -591,6 +685,7 @@ var addCommand = async (names, options = {}) => {
|
|
|
591
685
|
else log.warn(`registry: the copy built into this CLI. It may be older than ${registryUrl(config)}.`);
|
|
592
686
|
const queue = [...names.map(normalizeComponentName)];
|
|
593
687
|
const installed = [];
|
|
688
|
+
const kept = [];
|
|
594
689
|
while (queue.length > 0) {
|
|
595
690
|
const name = queue.shift();
|
|
596
691
|
if (installed.includes(name)) continue;
|
|
@@ -604,34 +699,55 @@ var addCommand = async (names, options = {}) => {
|
|
|
604
699
|
if (provider) queue.push(provider.name);
|
|
605
700
|
else log.warn(`${component.namespaced} needs a "${cue}" cue and no registry entry provides one.`);
|
|
606
701
|
}
|
|
702
|
+
if (component.kind === "asset" && component.asset) {
|
|
703
|
+
const written = await installAsset(config, component, { dryRun: options.dryRun, force: options.force });
|
|
704
|
+
if (!options.dryRun && written) {
|
|
705
|
+
installed.push(component.name);
|
|
706
|
+
const registered = await registerCueInBrand(
|
|
707
|
+
config,
|
|
708
|
+
{ name: component.asset.cue, url: component.asset.url },
|
|
709
|
+
component.name
|
|
710
|
+
);
|
|
711
|
+
if (registered?.already) {
|
|
712
|
+
log.detail(` "${component.asset.cue}" is already registered in ${registered.file}`);
|
|
713
|
+
} else if (registered) {
|
|
714
|
+
log.detail(` registered "${component.asset.cue}" in ${registered.file}`);
|
|
715
|
+
} else {
|
|
716
|
+
log.warn(` No brand with an audio.cues block found. Add it yourself:`);
|
|
717
|
+
log.detail(` audio: {cues: {"${component.asset.cue}": "${component.asset.url}"}}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
607
722
|
const { item, origin } = await resolveItem(config, component.name);
|
|
608
723
|
verifyIntegrity(item, origin);
|
|
609
|
-
const target =
|
|
724
|
+
const target = resolve7(config.root, config.componentsDir, assertSafeName(component.name));
|
|
610
725
|
const hashes = {};
|
|
611
726
|
for (const file of item.files) {
|
|
612
727
|
const destination = resolveWithinRoot(config.root, file.target);
|
|
613
|
-
const exists =
|
|
614
|
-
log.detail(` ${exists ? "replace" : "create "} ${
|
|
728
|
+
const exists = existsSync7(destination);
|
|
729
|
+
log.detail(` ${exists ? "replace" : "create "} ${relative5(config.root, destination)}`);
|
|
615
730
|
}
|
|
616
731
|
if (options.dryRun) {
|
|
617
732
|
installed.push(component.name);
|
|
618
733
|
continue;
|
|
619
734
|
}
|
|
620
|
-
await
|
|
735
|
+
await mkdir5(target, { recursive: true });
|
|
621
736
|
for (const file of item.files) {
|
|
622
737
|
const name2 = file.path.split("/").pop() ?? file.path;
|
|
623
738
|
const destination = resolveWithinRoot(config.root, file.target);
|
|
624
739
|
hashes[name2] = hashString2(file.content);
|
|
625
|
-
if (
|
|
626
|
-
const current = hashString2(await
|
|
740
|
+
if (existsSync7(destination) && !options.force) {
|
|
741
|
+
const current = hashString2(await readFile6(destination, "utf8"));
|
|
627
742
|
const recorded = provenance[component.name]?.hashes[name2];
|
|
628
743
|
if (current !== recorded) {
|
|
629
|
-
log.warn(`${
|
|
744
|
+
log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version.`);
|
|
745
|
+
kept.push(relative5(config.root, destination));
|
|
630
746
|
continue;
|
|
631
747
|
}
|
|
632
748
|
}
|
|
633
|
-
await
|
|
634
|
-
await
|
|
749
|
+
await mkdir5(resolve7(destination, ".."), { recursive: true });
|
|
750
|
+
await writeFile6(destination, file.content, "utf8");
|
|
635
751
|
}
|
|
636
752
|
provenance[component.name] = {
|
|
637
753
|
source: component.namespaced,
|
|
@@ -640,7 +756,7 @@ var addCommand = async (names, options = {}) => {
|
|
|
640
756
|
hashes
|
|
641
757
|
};
|
|
642
758
|
installed.push(component.name);
|
|
643
|
-
log.success(`${component.namespaced} to ${
|
|
759
|
+
log.success(`${component.namespaced} to ${relative5(config.root, target)}/`);
|
|
644
760
|
if (component.kind === "cue" && component.cue) {
|
|
645
761
|
log.detail(
|
|
646
762
|
` ${component.family} \xB7 ${component.contract.recommendedDurationInFrames} frames \xB7 registers "${component.cue.name}"`
|
|
@@ -666,6 +782,11 @@ var addCommand = async (names, options = {}) => {
|
|
|
666
782
|
return;
|
|
667
783
|
}
|
|
668
784
|
await writeProvenance(config, provenance);
|
|
785
|
+
if (kept.length > 0) {
|
|
786
|
+
log.warn(`Kept ${kept.length} locally modified ${kept.length === 1 ? "file" : "files"}:`);
|
|
787
|
+
for (const file of kept) log.detail(` ${file}`);
|
|
788
|
+
log.detail(`Run odori diff to see what upstream changed, or odori add <name> --force to replace them.`);
|
|
789
|
+
}
|
|
669
790
|
log.detail("Run odori dev to preview the installed component fixtures.");
|
|
670
791
|
};
|
|
671
792
|
var registryCommand = async () => {
|
|
@@ -688,19 +809,19 @@ var registryCommand = async () => {
|
|
|
688
809
|
};
|
|
689
810
|
|
|
690
811
|
// src/commands/dev.ts
|
|
691
|
-
import { resolve as
|
|
812
|
+
import { relative as relative7, resolve as resolve19 } from "path";
|
|
692
813
|
import { homedir as homedir2 } from "os";
|
|
693
|
-
import { existsSync as
|
|
694
|
-
import { readFile as
|
|
814
|
+
import { existsSync as existsSync16 } from "fs";
|
|
815
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
695
816
|
|
|
696
817
|
// src/jobs.ts
|
|
697
|
-
import { mkdir as
|
|
698
|
-
import { existsSync as
|
|
699
|
-
import { join as join2, resolve as
|
|
700
|
-
var buildsDir = (config) =>
|
|
818
|
+
import { mkdir as mkdir6, readFile as readFile7, readdir as readdir3, rename, writeFile as writeFile7 } from "fs/promises";
|
|
819
|
+
import { existsSync as existsSync8 } from "fs";
|
|
820
|
+
import { join as join2, resolve as resolve8 } from "path";
|
|
821
|
+
var buildsDir = (config) => resolve8(config.root, config.outDir, "builds");
|
|
701
822
|
var jobFile = (config, id) => join2(buildsDir(config), `${id}.json`);
|
|
702
823
|
var createJob = async (config, manifest, output, render) => {
|
|
703
|
-
await
|
|
824
|
+
await mkdir6(buildsDir(config), { recursive: true });
|
|
704
825
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
705
826
|
const job = {
|
|
706
827
|
id: `job-${manifest.manifestHash.slice(0, 10)}-${Date.now().toString(36)}`,
|
|
@@ -714,14 +835,14 @@ var createJob = async (config, manifest, output, render) => {
|
|
|
714
835
|
updatedAt: now
|
|
715
836
|
};
|
|
716
837
|
const record = { job, manifest, output, ...render ? { render } : {} };
|
|
717
|
-
await
|
|
838
|
+
await writeFile7(jobFile(config, job.id), `${JSON.stringify(record, null, 2)}
|
|
718
839
|
`, "utf8");
|
|
719
840
|
return record;
|
|
720
841
|
};
|
|
721
842
|
var readJob = async (config, id) => {
|
|
722
843
|
const file = jobFile(config, id);
|
|
723
|
-
if (!
|
|
724
|
-
return JSON.parse(await
|
|
844
|
+
if (!existsSync8(file)) throw new Error(`Unknown job "${id}". Run odori jobs to list them.`);
|
|
845
|
+
return JSON.parse(await readFile7(file, "utf8"));
|
|
725
846
|
};
|
|
726
847
|
var writeLocks = /* @__PURE__ */ new Map();
|
|
727
848
|
var withJobLock = (id, task) => {
|
|
@@ -736,7 +857,7 @@ var withJobLock = (id, task) => {
|
|
|
736
857
|
var writeRecord = async (config, record) => {
|
|
737
858
|
const file = jobFile(config, record.job.id);
|
|
738
859
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
739
|
-
await
|
|
860
|
+
await writeFile7(temporary, `${JSON.stringify(record, null, 2)}
|
|
740
861
|
`, "utf8");
|
|
741
862
|
await rename(temporary, file);
|
|
742
863
|
};
|
|
@@ -779,13 +900,13 @@ var reconcileJobs = async (config) => {
|
|
|
779
900
|
return stale.length;
|
|
780
901
|
};
|
|
781
902
|
var listJobs = async (config, options = {}) => {
|
|
782
|
-
if (!
|
|
903
|
+
if (!existsSync8(buildsDir(config))) return [];
|
|
783
904
|
if (options.reconcile !== false) await reconcileJobs(config);
|
|
784
|
-
const files = (await
|
|
905
|
+
const files = (await readdir3(buildsDir(config))).filter((file) => file.endsWith(".json"));
|
|
785
906
|
const jobs = [];
|
|
786
907
|
for (const file of files) {
|
|
787
908
|
try {
|
|
788
|
-
const raw = await
|
|
909
|
+
const raw = await readFile7(join2(buildsDir(config), file), "utf8");
|
|
789
910
|
jobs.push(JSON.parse(raw).job);
|
|
790
911
|
} catch {
|
|
791
912
|
continue;
|
|
@@ -803,13 +924,13 @@ var JobQueue = class {
|
|
|
803
924
|
};
|
|
804
925
|
|
|
805
926
|
// src/discovery.ts
|
|
806
|
-
import { mkdir as
|
|
807
|
-
import { existsSync as
|
|
808
|
-
import { join as join3, relative as
|
|
927
|
+
import { mkdir as mkdir7, readdir as readdir4, readFile as readFile8, stat, writeFile as writeFile8 } from "fs/promises";
|
|
928
|
+
import { existsSync as existsSync9 } from "fs";
|
|
929
|
+
import { join as join3, relative as relative6, resolve as resolve9, sep as sep2 } from "path";
|
|
809
930
|
import { hashString as hashString3 } from "odori";
|
|
810
931
|
var IGNORED = /* @__PURE__ */ new Set(["node_modules", ".git", ".odori", "out", "dist", ".next"]);
|
|
811
932
|
var walk = async (directory2, files = []) => {
|
|
812
|
-
const entries = await
|
|
933
|
+
const entries = await readdir4(directory2, { withFileTypes: true });
|
|
813
934
|
for (const entry of entries) {
|
|
814
935
|
if (entry.name.startsWith(".") || IGNORED.has(entry.name)) continue;
|
|
815
936
|
const full = join3(directory2, entry.name);
|
|
@@ -827,22 +948,22 @@ var toIdentifier = (value, prefix) => {
|
|
|
827
948
|
};
|
|
828
949
|
var AUDIO_EXTENSIONS = /\.(m4a|mp3|wav|aac|ogg|opus|flac)$/i;
|
|
829
950
|
var discoverAudio = async (config) => {
|
|
830
|
-
const root =
|
|
831
|
-
if (!
|
|
832
|
-
const publicRoot =
|
|
951
|
+
const root = resolve9(config.root, config.audioDir);
|
|
952
|
+
if (!existsSync9(root)) return [];
|
|
953
|
+
const publicRoot = resolve9(config.root, "public");
|
|
833
954
|
const files = (await walk(root)).filter((file) => AUDIO_EXTENSIONS.test(file)).sort();
|
|
834
955
|
return Promise.all(
|
|
835
956
|
files.map(async (file) => ({
|
|
836
|
-
name:
|
|
837
|
-
url: file.startsWith(`${publicRoot}${sep2}`) ? `/${
|
|
838
|
-
relativeFile:
|
|
957
|
+
name: relative6(root, file).replace(AUDIO_EXTENSIONS, "").split(sep2).join("/"),
|
|
958
|
+
url: file.startsWith(`${publicRoot}${sep2}`) ? `/${relative6(publicRoot, file).split(sep2).join("/")}` : `/${relative6(config.root, file).split(sep2).join("/")}`,
|
|
959
|
+
relativeFile: relative6(config.root, file),
|
|
839
960
|
bytes: (await stat(file)).size
|
|
840
961
|
}))
|
|
841
962
|
);
|
|
842
963
|
};
|
|
843
964
|
var discoverProject = async (config) => {
|
|
844
|
-
const videosRoot =
|
|
845
|
-
if (!
|
|
965
|
+
const videosRoot = resolve9(config.root, config.videosDir);
|
|
966
|
+
if (!existsSync9(videosRoot)) {
|
|
846
967
|
throw new Error(`No ${config.videosDir}/ directory found in ${config.root}. Run "odori init" first.`);
|
|
847
968
|
}
|
|
848
969
|
const files = (await walk(videosRoot)).sort();
|
|
@@ -850,18 +971,42 @@ var discoverProject = async (config) => {
|
|
|
850
971
|
const videos = [];
|
|
851
972
|
const previews = [];
|
|
852
973
|
const brands = [];
|
|
974
|
+
const categories = [];
|
|
853
975
|
const hashParts = [];
|
|
854
|
-
const
|
|
976
|
+
const importedBy = {};
|
|
977
|
+
const componentsRoot = resolve9(config.root, config.componentsDir);
|
|
855
978
|
for (const file of files) {
|
|
856
|
-
const relativeFile =
|
|
979
|
+
const relativeFile = relative6(config.root, file);
|
|
857
980
|
let contents = "";
|
|
858
981
|
if (/\.(tsx|ts|css|json)$/.test(file)) {
|
|
859
|
-
contents = await
|
|
982
|
+
contents = await readFile8(file, "utf8");
|
|
860
983
|
hashParts.push(`${relativeFile}:${hashString3(contents)}`);
|
|
861
984
|
}
|
|
862
985
|
const base = file.split(sep2).pop() ?? "";
|
|
986
|
+
if (base === "category.json") {
|
|
987
|
+
const path = relative6(componentsRoot, resolve9(file, "..")).split(sep2).join("/");
|
|
988
|
+
if (!path.startsWith("..")) {
|
|
989
|
+
try {
|
|
990
|
+
const declared = JSON.parse(contents);
|
|
991
|
+
categories.push({
|
|
992
|
+
path,
|
|
993
|
+
...typeof declared.name === "string" ? { name: declared.name } : {},
|
|
994
|
+
...typeof declared.order === "number" ? { order: declared.order } : {}
|
|
995
|
+
});
|
|
996
|
+
} catch (error) {
|
|
997
|
+
log.warn(
|
|
998
|
+
`${relativeFile} is not valid JSON, so that directory names itself: ${error instanceof Error ? error.message : String(error)}`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (base === "video.tsx") {
|
|
1004
|
+
for (const match of contents.matchAll(/from\s+["'][^"']*\/components\/([^/"']+)\//g)) {
|
|
1005
|
+
(importedBy[match[1]] ??= /* @__PURE__ */ new Set()).add(relative6(videosRoot, file).replace(/\/?video\.tsx$/, "") || "video");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
863
1008
|
if (base === "video.tsx") {
|
|
864
|
-
const slug =
|
|
1009
|
+
const slug = relative6(videosRoot, file).replace(/\/?video\.tsx$/, "").split(sep2).join("/") || "video";
|
|
865
1010
|
videos.push({
|
|
866
1011
|
slug,
|
|
867
1012
|
file,
|
|
@@ -887,7 +1032,7 @@ var discoverProject = async (config) => {
|
|
|
887
1032
|
// From the whole relative path, like previews: basenames repeat
|
|
888
1033
|
// (`layout.tsx` beside `brands/layout.ts`), identifiers cannot.
|
|
889
1034
|
identifier: toIdentifier(
|
|
890
|
-
`${
|
|
1035
|
+
`${relative6(videosRoot, file).replace(/\.tsx?$/, "").split(sep2).join("-")}-module`,
|
|
891
1036
|
"brands"
|
|
892
1037
|
)
|
|
893
1038
|
});
|
|
@@ -898,15 +1043,19 @@ var discoverProject = async (config) => {
|
|
|
898
1043
|
file,
|
|
899
1044
|
relativeFile,
|
|
900
1045
|
importPath: file,
|
|
901
|
-
identifier: toIdentifier(`${
|
|
1046
|
+
identifier: toIdentifier(`${relative6(videosRoot, file).split(sep2).join("-")}`, "preview")
|
|
902
1047
|
});
|
|
903
1048
|
}
|
|
904
1049
|
}
|
|
905
|
-
|
|
1050
|
+
for (const preview of previews) {
|
|
1051
|
+
const users = importedBy[preview.name];
|
|
1052
|
+
if (users) preview.usedBy = [...users].sort();
|
|
1053
|
+
}
|
|
1054
|
+
return { videos, previews, brands, audio, categories, sourceHash: hashString3(hashParts.join("|")) };
|
|
906
1055
|
};
|
|
907
1056
|
var generateImports = (graph, outDir) => {
|
|
908
1057
|
const importPath = (file) => {
|
|
909
|
-
const relativePath =
|
|
1058
|
+
const relativePath = relative6(outDir, file).split(sep2).join("/");
|
|
910
1059
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
911
1060
|
};
|
|
912
1061
|
const lines = [
|
|
@@ -939,17 +1088,21 @@ var generateImports = (graph, outDir) => {
|
|
|
939
1088
|
return lines.join("\n");
|
|
940
1089
|
};
|
|
941
1090
|
var writeGenerated = async (config, graph) => {
|
|
942
|
-
const outDir =
|
|
943
|
-
await
|
|
1091
|
+
const outDir = resolve9(config.root, config.outDir);
|
|
1092
|
+
await mkdir7(outDir, { recursive: true });
|
|
944
1093
|
const target = join3(outDir, "imports.generated.ts");
|
|
945
|
-
await
|
|
946
|
-
await
|
|
1094
|
+
await writeFile8(target, generateImports(graph, outDir), "utf8");
|
|
1095
|
+
await writeFile8(
|
|
947
1096
|
join3(outDir, "catalog.json"),
|
|
948
1097
|
`${JSON.stringify(
|
|
949
1098
|
{
|
|
950
1099
|
sourceHash: graph.sourceHash,
|
|
951
1100
|
videos: graph.videos.map((video) => ({ slug: video.slug, file: video.relativeFile })),
|
|
952
|
-
previews: graph.previews.map((preview) => ({
|
|
1101
|
+
previews: graph.previews.map((preview) => ({
|
|
1102
|
+
name: preview.name,
|
|
1103
|
+
file: preview.relativeFile,
|
|
1104
|
+
usedBy: preview.usedBy ?? []
|
|
1105
|
+
})),
|
|
953
1106
|
brands: graph.brands.map((brand) => ({ name: brand.name, file: brand.relativeFile })),
|
|
954
1107
|
audio: graph.audio.map((entry) => ({ name: entry.name, url: entry.url, file: entry.relativeFile }))
|
|
955
1108
|
},
|
|
@@ -963,7 +1116,7 @@ var writeGenerated = async (config, graph) => {
|
|
|
963
1116
|
};
|
|
964
1117
|
|
|
965
1118
|
// src/project.ts
|
|
966
|
-
import { resolve as
|
|
1119
|
+
import { resolve as resolve12 } from "path";
|
|
967
1120
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
968
1121
|
import {
|
|
969
1122
|
createRenderManifest,
|
|
@@ -973,45 +1126,45 @@ import {
|
|
|
973
1126
|
} from "odori";
|
|
974
1127
|
|
|
975
1128
|
// src/integrity.ts
|
|
976
|
-
import { createHash as
|
|
977
|
-
import { existsSync as
|
|
978
|
-
import { mkdir as
|
|
979
|
-
import { dirname as
|
|
980
|
-
var cacheFile = (config) =>
|
|
1129
|
+
import { createHash as createHash3 } from "crypto";
|
|
1130
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1131
|
+
import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
|
|
1132
|
+
import { dirname as dirname4, resolve as resolve10 } from "path";
|
|
1133
|
+
var cacheFile = (config) => resolve10(config.root, config.outDir, "cache", "integrity.json");
|
|
981
1134
|
var readCache = async (config) => {
|
|
982
1135
|
const file = cacheFile(config);
|
|
983
|
-
if (!
|
|
1136
|
+
if (!existsSync10(file)) return {};
|
|
984
1137
|
try {
|
|
985
|
-
return JSON.parse(await
|
|
1138
|
+
return JSON.parse(await readFile9(file, "utf8"));
|
|
986
1139
|
} catch {
|
|
987
1140
|
return {};
|
|
988
1141
|
}
|
|
989
1142
|
};
|
|
990
1143
|
var writeCache = async (config, cache) => {
|
|
991
1144
|
const file = cacheFile(config);
|
|
992
|
-
await
|
|
993
|
-
await
|
|
1145
|
+
await mkdir8(dirname4(file), { recursive: true });
|
|
1146
|
+
await writeFile9(file, `${JSON.stringify(cache, null, 2)}
|
|
994
1147
|
`, "utf8");
|
|
995
1148
|
};
|
|
996
|
-
var sha256 = (bytes) => `sha256-${
|
|
1149
|
+
var sha256 = (bytes) => `sha256-${createHash3("sha256").update(bytes).digest("base64")}`;
|
|
997
1150
|
var localCandidates = (config, url) => [
|
|
998
|
-
|
|
999
|
-
|
|
1151
|
+
resolve10(config.root, "public", url.replace(/^\//, "")),
|
|
1152
|
+
resolve10(config.root, url.replace(/^\//, ""))
|
|
1000
1153
|
];
|
|
1001
|
-
var isServed = (config, file) => file.startsWith(
|
|
1154
|
+
var isServed = (config, file) => file.startsWith(resolve10(config.root, "public") + "/");
|
|
1002
1155
|
var createIntegrityResolver = async (config) => {
|
|
1003
1156
|
const cache = await readCache(config);
|
|
1004
1157
|
const warned = /* @__PURE__ */ new Set();
|
|
1005
1158
|
let dirty = false;
|
|
1006
1159
|
const resolveIntegrity = async (url) => {
|
|
1007
1160
|
if (url.startsWith("/__odori/cue/")) return `cue-${url.slice(url.lastIndexOf("-") + 1).replace(/\.wav$/, "")}`;
|
|
1008
|
-
const local = localCandidates(config, url).find((candidate) =>
|
|
1161
|
+
const local = localCandidates(config, url).find((candidate) => existsSync10(candidate));
|
|
1009
1162
|
if (local) {
|
|
1010
1163
|
if (!isServed(config, local) && !warned.has(url)) {
|
|
1011
1164
|
warned.add(url);
|
|
1012
1165
|
log.warn(`${url} resolves to ${local}, which is outside public/ and will not be served. Move it into public/.`);
|
|
1013
1166
|
}
|
|
1014
|
-
const bytes = await
|
|
1167
|
+
const bytes = await readFile9(local);
|
|
1015
1168
|
const { mtimeMs } = await import("fs/promises").then((fs) => fs.stat(local));
|
|
1016
1169
|
const hit = cache[url];
|
|
1017
1170
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === bytes.byteLength) return hit.integrity;
|
|
@@ -1043,9 +1196,9 @@ var createIntegrityResolver = async (config) => {
|
|
|
1043
1196
|
};
|
|
1044
1197
|
|
|
1045
1198
|
// src/prepare-cache.ts
|
|
1046
|
-
import { existsSync as
|
|
1047
|
-
import { mkdir as
|
|
1048
|
-
import { join as join4, resolve as
|
|
1199
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1200
|
+
import { mkdir as mkdir9, readFile as readFile10, readdir as readdir5, rm as rm3, writeFile as writeFile10 } from "fs/promises";
|
|
1201
|
+
import { join as join4, resolve as resolve11 } from "path";
|
|
1049
1202
|
import { hashValue } from "odori";
|
|
1050
1203
|
|
|
1051
1204
|
// src/paths.ts
|
|
@@ -1053,13 +1206,13 @@ var outputName = (id) => id.split("/").join("-");
|
|
|
1053
1206
|
var fileKey = (id) => id.split("/").join("+");
|
|
1054
1207
|
|
|
1055
1208
|
// src/prepare-cache.ts
|
|
1056
|
-
var directory = (config) =>
|
|
1209
|
+
var directory = (config) => resolve11(config.root, config.outDir, "cache", "prepare");
|
|
1057
1210
|
var prepareCacheKey = (key) => `${fileKey(key.videoId)}__${hashValue(key)}`;
|
|
1058
1211
|
var readPrepareCache = async (config, key) => {
|
|
1059
1212
|
const file = join4(directory(config), `${prepareCacheKey(key)}.json`);
|
|
1060
|
-
if (!
|
|
1213
|
+
if (!existsSync11(file)) return { hit: false, value: void 0 };
|
|
1061
1214
|
try {
|
|
1062
|
-
const entry = JSON.parse(await
|
|
1215
|
+
const entry = JSON.parse(await readFile10(file, "utf8"));
|
|
1063
1216
|
return { hit: true, value: entry.value };
|
|
1064
1217
|
} catch {
|
|
1065
1218
|
return { hit: false, value: void 0 };
|
|
@@ -1068,17 +1221,17 @@ var readPrepareCache = async (config, key) => {
|
|
|
1068
1221
|
var writePrepareCache = async (config, key, value) => {
|
|
1069
1222
|
if (value === void 0) return;
|
|
1070
1223
|
const target = directory(config);
|
|
1071
|
-
await
|
|
1224
|
+
await mkdir9(target, { recursive: true });
|
|
1072
1225
|
const entry = { key, value, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1073
|
-
await
|
|
1226
|
+
await writeFile10(join4(target, `${prepareCacheKey(key)}.json`), `${JSON.stringify(entry, null, 2)}
|
|
1074
1227
|
`, "utf8");
|
|
1075
1228
|
};
|
|
1076
1229
|
var clearPrepareCache = async (config, videoId) => {
|
|
1077
1230
|
const target = directory(config);
|
|
1078
|
-
if (!
|
|
1079
|
-
const files = await
|
|
1231
|
+
if (!existsSync11(target)) return 0;
|
|
1232
|
+
const files = await readdir5(target);
|
|
1080
1233
|
const matches = files.filter((file) => videoId ? file.startsWith(`${fileKey(videoId)}__`) : file.endsWith(".json"));
|
|
1081
|
-
await Promise.all(matches.map((file) =>
|
|
1234
|
+
await Promise.all(matches.map((file) => rm3(join4(target, file), { force: true })));
|
|
1082
1235
|
return matches.length;
|
|
1083
1236
|
};
|
|
1084
1237
|
|
|
@@ -1122,7 +1275,7 @@ var findVideo = (videos, id) => {
|
|
|
1122
1275
|
return found;
|
|
1123
1276
|
};
|
|
1124
1277
|
var runPrepare = async (video, config, graph, input, options = {}) => {
|
|
1125
|
-
const prepareFile =
|
|
1278
|
+
const prepareFile = resolve12(video.file, "..", "prepare.ts");
|
|
1126
1279
|
let prepare;
|
|
1127
1280
|
try {
|
|
1128
1281
|
const module = await import(pathToFileURL2(prepareFile).href);
|
|
@@ -1319,20 +1472,20 @@ var alphaWarning = (format, transparent) => transparent && !format.alpha ? `${fo
|
|
|
1319
1472
|
|
|
1320
1473
|
// src/render.ts
|
|
1321
1474
|
import { spawn as spawn2 } from "child_process";
|
|
1322
|
-
import { copyFile as copyFile2, mkdir as
|
|
1475
|
+
import { copyFile as copyFile2, mkdir as mkdir12, rm as rm4, writeFile as writeFile13 } from "fs/promises";
|
|
1323
1476
|
import { cpus } from "os";
|
|
1324
|
-
import { dirname as
|
|
1477
|
+
import { dirname as dirname5, join as join6, resolve as resolve16 } from "path";
|
|
1325
1478
|
import { chromium } from "playwright-core";
|
|
1326
1479
|
|
|
1327
1480
|
// src/audio-mix.ts
|
|
1328
|
-
import { existsSync as
|
|
1329
|
-
import { resolve as
|
|
1481
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1482
|
+
import { resolve as resolve14 } from "path";
|
|
1330
1483
|
import { duckEnvelope, envelopeAtFrame } from "odori";
|
|
1331
1484
|
|
|
1332
1485
|
// src/cues.ts
|
|
1333
|
-
import { existsSync as
|
|
1334
|
-
import { mkdir as
|
|
1335
|
-
import { basename, resolve as
|
|
1486
|
+
import { existsSync as existsSync12, statSync } from "fs";
|
|
1487
|
+
import { mkdir as mkdir10, writeFile as writeFile11 } from "fs/promises";
|
|
1488
|
+
import { basename, resolve as resolve13 } from "path";
|
|
1336
1489
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
1337
1490
|
import {
|
|
1338
1491
|
SAMPLE_RATE,
|
|
@@ -1343,8 +1496,8 @@ import {
|
|
|
1343
1496
|
isCueDefinition,
|
|
1344
1497
|
resolveEntryLayout as resolveEntryLayout2
|
|
1345
1498
|
} from "odori";
|
|
1346
|
-
var cueCacheDir = (config) =>
|
|
1347
|
-
var cueFile = (config, url) =>
|
|
1499
|
+
var cueCacheDir = (config) => resolve13(config.root, config.outDir, "cues");
|
|
1500
|
+
var cueFile = (config, url) => resolve13(cueCacheDir(config), basename(url));
|
|
1348
1501
|
var materializeCues = async (config, brands, fps) => {
|
|
1349
1502
|
const seen = /* @__PURE__ */ new Map();
|
|
1350
1503
|
for (const brand of brands) {
|
|
@@ -1353,17 +1506,17 @@ var materializeCues = async (config, brands, fps) => {
|
|
|
1353
1506
|
}
|
|
1354
1507
|
}
|
|
1355
1508
|
if (seen.size === 0) return [];
|
|
1356
|
-
await
|
|
1509
|
+
await mkdir10(cueCacheDir(config), { recursive: true });
|
|
1357
1510
|
const written = [];
|
|
1358
1511
|
for (const [url, cue] of seen) {
|
|
1359
1512
|
const file = cueFile(config, url);
|
|
1360
|
-
if (
|
|
1513
|
+
if (existsSync12(file)) {
|
|
1361
1514
|
written.push({ cue, file, rendered: false });
|
|
1362
1515
|
continue;
|
|
1363
1516
|
}
|
|
1364
1517
|
const samples = cueSamples(cue, fps);
|
|
1365
1518
|
const signal = cue.render({ samples, sampleRate: SAMPLE_RATE });
|
|
1366
|
-
await
|
|
1519
|
+
await writeFile11(file, encodeWav(signal));
|
|
1367
1520
|
written.push({ cue, file, rendered: true });
|
|
1368
1521
|
}
|
|
1369
1522
|
return written;
|
|
@@ -1421,13 +1574,13 @@ var resolveCueFile = (config, src) => {
|
|
|
1421
1574
|
if (/^https?:\/\//.test(src)) return null;
|
|
1422
1575
|
if (src.startsWith("/__odori/cue/")) {
|
|
1423
1576
|
const generated = cueFile(config, src);
|
|
1424
|
-
return
|
|
1577
|
+
return existsSync13(generated) ? generated : null;
|
|
1425
1578
|
}
|
|
1426
1579
|
const candidates = [
|
|
1427
|
-
|
|
1428
|
-
|
|
1580
|
+
resolve14(config.root, "public", src.replace(/^\//, "")),
|
|
1581
|
+
resolve14(config.root, src.replace(/^\//, ""))
|
|
1429
1582
|
];
|
|
1430
|
-
return candidates.find((candidate) =>
|
|
1583
|
+
return candidates.find((candidate) => existsSync13(candidate)) ?? null;
|
|
1431
1584
|
};
|
|
1432
1585
|
var volumeFilter = (cue, cues, fps) => {
|
|
1433
1586
|
const authored = cue.gainPoints ?? [];
|
|
@@ -1547,11 +1700,11 @@ var planChunks = ({
|
|
|
1547
1700
|
var chunkFrames = (chunk) => Array.from({ length: length(chunk) }, (_, offset) => chunk.start + offset);
|
|
1548
1701
|
|
|
1549
1702
|
// src/chunk-cache.ts
|
|
1550
|
-
import { existsSync as
|
|
1551
|
-
import { copyFile, mkdir as
|
|
1552
|
-
import { join as join5, resolve as
|
|
1703
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1704
|
+
import { copyFile, mkdir as mkdir11, readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
|
|
1705
|
+
import { join as join5, resolve as resolve15 } from "path";
|
|
1553
1706
|
import { hashValue as hashValue2 } from "odori";
|
|
1554
|
-
var cacheDir2 = (config) =>
|
|
1707
|
+
var cacheDir2 = (config) => resolve15(config.root, config.outDir, "cache", "chunks");
|
|
1555
1708
|
var chunkKey = (identity) => hashValue2({
|
|
1556
1709
|
videoId: identity.videoId,
|
|
1557
1710
|
// The browser that drew the frames is part of what the frames are. Without
|
|
@@ -1576,9 +1729,9 @@ var chunkKey = (identity) => hashValue2({
|
|
|
1576
1729
|
var readChunkRecord = async (config, key) => {
|
|
1577
1730
|
const meta = join5(cacheDir2(config), `${key}.json`);
|
|
1578
1731
|
const media = join5(cacheDir2(config), `${key}.mp4`);
|
|
1579
|
-
if (!
|
|
1732
|
+
if (!existsSync14(meta) || !existsSync14(media)) return null;
|
|
1580
1733
|
try {
|
|
1581
|
-
return JSON.parse(await
|
|
1734
|
+
return JSON.parse(await readFile11(meta, "utf8"));
|
|
1582
1735
|
} catch {
|
|
1583
1736
|
return null;
|
|
1584
1737
|
}
|
|
@@ -1588,10 +1741,10 @@ var useChunkRecord = async (config, key, destination) => {
|
|
|
1588
1741
|
};
|
|
1589
1742
|
var writeChunkRecord = async (config, key, signatures, file) => {
|
|
1590
1743
|
const directory2 = cacheDir2(config);
|
|
1591
|
-
await
|
|
1744
|
+
await mkdir11(directory2, { recursive: true });
|
|
1592
1745
|
await copyFile(file, join5(directory2, `${key}.mp4`));
|
|
1593
1746
|
const record = { key, signatures, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1594
|
-
await
|
|
1747
|
+
await writeFile12(join5(directory2, `${key}.json`), `${JSON.stringify(record)}
|
|
1595
1748
|
`, "utf8");
|
|
1596
1749
|
};
|
|
1597
1750
|
var signaturesMatch = (recorded, observed) => recorded.length === observed.length && recorded.every((signature, index) => signature === observed[index]);
|
|
@@ -1739,7 +1892,7 @@ var ensureFfmpeg = async (config) => {
|
|
|
1739
1892
|
var renderStill = async (origin, target, frame, output, config) => {
|
|
1740
1893
|
const { browser, page, errors } = await openRenderPage(origin, target, config);
|
|
1741
1894
|
try {
|
|
1742
|
-
await
|
|
1895
|
+
await mkdir12(dirname5(output), { recursive: true });
|
|
1743
1896
|
await seekTo(page, frame);
|
|
1744
1897
|
await page.screenshot({ path: output });
|
|
1745
1898
|
if (errors.length > 0) log.warn(`The page reported an error while rendering: ${errors[0]}`);
|
|
@@ -1895,8 +2048,8 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1895
2048
|
const chunkFormat = chunkable ? format : LOSSLESS;
|
|
1896
2049
|
const skipUnchanged = options.skipUnchangedFrames ?? config.skipUnchangedFrames ?? true;
|
|
1897
2050
|
const cache = options.cache ?? config.cacheChunks ?? true;
|
|
1898
|
-
const work = options.workDir ??
|
|
1899
|
-
await
|
|
2051
|
+
const work = options.workDir ?? resolve16(config.root, config.outDir, "frames", `${target.videoId.split("/").join("-")}-${Date.now().toString(36)}`);
|
|
2052
|
+
await mkdir12(work, { recursive: true });
|
|
1900
2053
|
const concurrency = chunkable ? requested : 1;
|
|
1901
2054
|
const { chunks, lanes } = planChunks({
|
|
1902
2055
|
durationInFrames: target.durationInFrames,
|
|
@@ -1907,7 +2060,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1907
2060
|
const stats = { captured: 0, reused: 0, cachedChunks: 0 };
|
|
1908
2061
|
let succeeded = false;
|
|
1909
2062
|
try {
|
|
1910
|
-
await
|
|
2063
|
+
await mkdir12(dirname5(output), { recursive: true });
|
|
1911
2064
|
const captureStart = performance.now();
|
|
1912
2065
|
await Promise.all(
|
|
1913
2066
|
lanes.map(
|
|
@@ -1942,7 +2095,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1942
2095
|
);
|
|
1943
2096
|
const captureMs = performance.now() - captureStart;
|
|
1944
2097
|
onProgress?.(1, "encoding");
|
|
1945
|
-
const mixInputs = (target.audio ?? []).map((cue) => {
|
|
2098
|
+
const mixInputs = (options.audio === false ? [] : target.audio ?? []).map((cue) => {
|
|
1946
2099
|
const file = resolveCueFile(config, cue.src);
|
|
1947
2100
|
if (!file) log.warn(`Skipping audio cue ${cue.src}: only project-local files can be encoded.`);
|
|
1948
2101
|
return file ? { file, cue } : null;
|
|
@@ -1954,12 +2107,12 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1954
2107
|
await copyFile2(ordered[0], silent);
|
|
1955
2108
|
} else {
|
|
1956
2109
|
const list = join6(work, "chunks.txt");
|
|
1957
|
-
await
|
|
2110
|
+
await writeFile13(list, ordered.map((file) => `file '${file.split("'").join("'\\''")}'`).join("\n"), "utf8");
|
|
1958
2111
|
await run(ffmpeg, ["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", silent], options.signal);
|
|
1959
2112
|
}
|
|
1960
2113
|
if (!chunkable) {
|
|
1961
2114
|
const destination = format.name === "png" && !output.includes("%") ? sequencePattern(output) : output;
|
|
1962
|
-
if (destination !== output) await
|
|
2115
|
+
if (destination !== output) await mkdir12(dirname5(destination), { recursive: true });
|
|
1963
2116
|
await run(ffmpeg, ["-y", "-i", silent, ...format.args(encode), destination], options.signal);
|
|
1964
2117
|
if (mixInputs.length > 0) {
|
|
1965
2118
|
log.detail(`${format.name} carries no audio track; ${mixInputs.length} cue(s) were not mixed in.`);
|
|
@@ -2013,7 +2166,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
2013
2166
|
succeeded = true;
|
|
2014
2167
|
return output;
|
|
2015
2168
|
} finally {
|
|
2016
|
-
if (succeeded && !options.workDir) await
|
|
2169
|
+
if (succeeded && !options.workDir) await rm4(work, { recursive: true, force: true });
|
|
2017
2170
|
else if (!succeeded) log.detail(`Chunks left for inspection in ${work}`);
|
|
2018
2171
|
}
|
|
2019
2172
|
};
|
|
@@ -2045,25 +2198,25 @@ var openInBrowser = (url) => {
|
|
|
2045
2198
|
};
|
|
2046
2199
|
|
|
2047
2200
|
// src/server.ts
|
|
2048
|
-
import { existsSync as
|
|
2201
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2049
2202
|
import { createRequire as createRequire2 } from "module";
|
|
2050
2203
|
import { fileURLToPath } from "url";
|
|
2051
2204
|
import { createServer } from "vite";
|
|
2052
2205
|
import react from "@vitejs/plugin-react";
|
|
2053
|
-
import { readFile as
|
|
2054
|
-
import { dirname as
|
|
2055
|
-
var cliRoot =
|
|
2056
|
-
var studioRoot =
|
|
2057
|
-
var studioEntry =
|
|
2058
|
-
var installRoot =
|
|
2206
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2207
|
+
import { dirname as dirname6, resolve as resolve17, sep as sep3 } from "path";
|
|
2208
|
+
var cliRoot = resolve17(dirname6(fileURLToPath(import.meta.url)), "..");
|
|
2209
|
+
var studioRoot = resolve17(cliRoot, "studio");
|
|
2210
|
+
var studioEntry = resolve17(studioRoot, "index.html");
|
|
2211
|
+
var installRoot = resolve17(cliRoot, "..", "..");
|
|
2059
2212
|
var VIRTUAL_ID = "virtual:odori-project";
|
|
2060
2213
|
var RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
|
2061
2214
|
var runtimeSource = (root) => {
|
|
2062
|
-
for (const from of [
|
|
2215
|
+
for (const from of [resolve17(root, "package.json"), import.meta.url]) {
|
|
2063
2216
|
try {
|
|
2064
2217
|
const manifest = createRequire2(from).resolve("odori/package.json");
|
|
2065
|
-
const src =
|
|
2066
|
-
if (
|
|
2218
|
+
const src = resolve17(manifest, "..", "src");
|
|
2219
|
+
if (existsSync15(resolve17(src, "index.tsx"))) return src;
|
|
2067
2220
|
} catch {
|
|
2068
2221
|
}
|
|
2069
2222
|
}
|
|
@@ -2102,6 +2255,8 @@ var odoriProjectPlugin = (config, getGraph) => ({
|
|
|
2102
2255
|
`export const project = ${JSON.stringify({
|
|
2103
2256
|
root: config.root,
|
|
2104
2257
|
videosDir: config.videosDir,
|
|
2258
|
+
componentsDir: config.componentsDir,
|
|
2259
|
+
categories: graph.categories,
|
|
2105
2260
|
exportDir: config.exportDir,
|
|
2106
2261
|
audioDir: config.audioDir,
|
|
2107
2262
|
docsUrl: config.docsUrl,
|
|
@@ -2110,7 +2265,11 @@ var odoriProjectPlugin = (config, getGraph) => ({
|
|
|
2110
2265
|
assets: config.assets ?? [],
|
|
2111
2266
|
files: {
|
|
2112
2267
|
videos: graph.videos.map((video) => ({ id: video.slug, file: video.relativeFile })),
|
|
2113
|
-
previews: graph.previews.map((preview) => ({
|
|
2268
|
+
previews: graph.previews.map((preview) => ({
|
|
2269
|
+
id: preview.name,
|
|
2270
|
+
file: preview.relativeFile,
|
|
2271
|
+
usedBy: preview.usedBy ?? []
|
|
2272
|
+
})),
|
|
2114
2273
|
brands: graph.brands.map((brand) => ({ id: brand.name, file: brand.relativeFile }))
|
|
2115
2274
|
}
|
|
2116
2275
|
})};`
|
|
@@ -2180,15 +2339,15 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2180
2339
|
],
|
|
2181
2340
|
// The project's public/ directory is served at the root, so brand fonts,
|
|
2182
2341
|
// logos, and footage resolve identically in preview and render.
|
|
2183
|
-
publicDir:
|
|
2342
|
+
publicDir: existsSync15(resolve17(config.root, "public")) ? resolve17(config.root, "public") : false,
|
|
2184
2343
|
resolve: {
|
|
2185
2344
|
dedupe: ["react", "react-dom", "odori"],
|
|
2186
2345
|
// Only when the runtime is present as source. A consumer resolves the
|
|
2187
2346
|
// published package through its exports map instead.
|
|
2188
2347
|
alias: odoriSrc ? [
|
|
2189
|
-
{ find: /^odori\/preview$/, replacement:
|
|
2190
|
-
{ find: /^odori\/manifest$/, replacement:
|
|
2191
|
-
{ find: /^odori$/, replacement:
|
|
2348
|
+
{ find: /^odori\/preview$/, replacement: resolve17(odoriSrc, "preview.ts") },
|
|
2349
|
+
{ find: /^odori\/manifest$/, replacement: resolve17(odoriSrc, "manifest.ts") },
|
|
2350
|
+
{ find: /^odori$/, replacement: resolve17(odoriSrc, "index.tsx") }
|
|
2192
2351
|
] : []
|
|
2193
2352
|
},
|
|
2194
2353
|
server: {
|
|
@@ -2218,7 +2377,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2218
2377
|
}, 150);
|
|
2219
2378
|
};
|
|
2220
2379
|
const rediscover = async (file) => {
|
|
2221
|
-
if (!file.startsWith(
|
|
2380
|
+
if (!file.startsWith(resolve17(config.root, config.videosDir))) return;
|
|
2222
2381
|
const isEntry = file.endsWith("video.tsx") || file.endsWith(".preview.tsx") || file.includes(`${sep3}brands${sep3}`);
|
|
2223
2382
|
if (!isEntry) return;
|
|
2224
2383
|
try {
|
|
@@ -2235,7 +2394,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2235
2394
|
};
|
|
2236
2395
|
vite.watcher.on("add", (file) => void rediscover(file));
|
|
2237
2396
|
vite.watcher.on("unlink", (file) => void rediscover(file));
|
|
2238
|
-
vite.watcher.add(
|
|
2397
|
+
vite.watcher.add(resolve17(config.root, config.videosDir));
|
|
2239
2398
|
const reloadConfig = async (file) => {
|
|
2240
2399
|
if (!/odori\.config\.(?:ts|mjs|js)$/.test(file)) return;
|
|
2241
2400
|
try {
|
|
@@ -2252,14 +2411,14 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2252
2411
|
vite.ws.send({ type: "full-reload" });
|
|
2253
2412
|
};
|
|
2254
2413
|
const refreshCues = (file) => {
|
|
2255
|
-
if (!file.startsWith(
|
|
2414
|
+
if (!file.startsWith(resolve17(config.root, config.videosDir) + sep3)) return;
|
|
2256
2415
|
if (!/\.tsx?$/.test(file)) return;
|
|
2257
2416
|
scheduleCueRefresh();
|
|
2258
2417
|
};
|
|
2259
2418
|
vite.watcher.on("change", (file) => refreshCues(file));
|
|
2260
2419
|
vite.watcher.on("change", (file) => void reloadConfig(file));
|
|
2261
2420
|
for (const name of ["odori.config.ts", "odori.config.mjs", "odori.config.js"]) {
|
|
2262
|
-
vite.watcher.add(
|
|
2421
|
+
vite.watcher.add(resolve17(config.root, name));
|
|
2263
2422
|
}
|
|
2264
2423
|
vite.middlewares.use(async (request, response, next) => {
|
|
2265
2424
|
const url = (request.url ?? "/").split("?")[0];
|
|
@@ -2269,7 +2428,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2269
2428
|
return;
|
|
2270
2429
|
}
|
|
2271
2430
|
try {
|
|
2272
|
-
const html = await
|
|
2431
|
+
const html = await readFile12(studioEntry, "utf8");
|
|
2273
2432
|
response.statusCode = 200;
|
|
2274
2433
|
response.setHeader("content-type", "text/html");
|
|
2275
2434
|
response.end(await vite.transformIndexHtml(url, html));
|
|
@@ -2290,7 +2449,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2290
2449
|
};
|
|
2291
2450
|
|
|
2292
2451
|
// src/commands/exportVideo.ts
|
|
2293
|
-
import { resolve as
|
|
2452
|
+
import { resolve as resolve18 } from "path";
|
|
2294
2453
|
import { resolveEntryLayout as resolveEntryLayout4 } from "odori";
|
|
2295
2454
|
|
|
2296
2455
|
// src/commands/shared.ts
|
|
@@ -2405,6 +2564,7 @@ var runJob = async (config, origin, record, video, options = {}) => exportQueue.
|
|
|
2405
2564
|
quality: options.quality ?? record.render?.quality,
|
|
2406
2565
|
scale: options.scale ?? record.render?.scale,
|
|
2407
2566
|
format: options.format ?? (record.render?.format ? resolveFormat(record.render.format, record.output) : void 0),
|
|
2567
|
+
audio: options.audio ?? record.render?.audio,
|
|
2408
2568
|
skipUnchangedFrames: options.skipUnchangedFrames,
|
|
2409
2569
|
signal: controller.signal,
|
|
2410
2570
|
onTimings: (timings) => {
|
|
@@ -2459,7 +2619,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2459
2619
|
options.input ?? {},
|
|
2460
2620
|
{ scenes: compiled.scenes, audio: compiled.audio }
|
|
2461
2621
|
);
|
|
2462
|
-
const output =
|
|
2622
|
+
const output = resolve18(
|
|
2463
2623
|
config.root,
|
|
2464
2624
|
options.output ?? `${config.exportDir}/${outputName(id)}${format.extension}`
|
|
2465
2625
|
);
|
|
@@ -2467,6 +2627,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2467
2627
|
format: format.name,
|
|
2468
2628
|
quality,
|
|
2469
2629
|
scale,
|
|
2630
|
+
audio: options.audio !== false,
|
|
2470
2631
|
...options.preset ? { preset: options.preset } : {}
|
|
2471
2632
|
});
|
|
2472
2633
|
})();
|
|
@@ -2483,6 +2644,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2483
2644
|
quality: options.retry && options.quality === void 0 ? void 0 : quality,
|
|
2484
2645
|
scale: options.retry && options.scale === void 0 ? void 0 : scale,
|
|
2485
2646
|
format: options.retry ? void 0 : format,
|
|
2647
|
+
audio: options.audio,
|
|
2486
2648
|
skipUnchangedFrames: options.skipUnchangedFrames,
|
|
2487
2649
|
onProgress: (next) => {
|
|
2488
2650
|
if (next.status === "rendering" || next.status === "encoding") {
|
|
@@ -2525,8 +2687,8 @@ var json = (response, status, payload) => {
|
|
|
2525
2687
|
response.end(JSON.stringify(payload));
|
|
2526
2688
|
};
|
|
2527
2689
|
var exportDestination = (config) => {
|
|
2528
|
-
const downloads =
|
|
2529
|
-
return
|
|
2690
|
+
const downloads = resolve19(homedir2(), "Downloads");
|
|
2691
|
+
return existsSync16(downloads) ? downloads : resolve19(config.root, config.exportDir);
|
|
2530
2692
|
};
|
|
2531
2693
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
2532
2694
|
var hostOf = (value) => {
|
|
@@ -2585,7 +2747,7 @@ var devCommand = async (options = {}) => {
|
|
|
2585
2747
|
);
|
|
2586
2748
|
const frame = Number(body.frame ?? 0);
|
|
2587
2749
|
const inline = body.inline === true;
|
|
2588
|
-
const file = inline ?
|
|
2750
|
+
const file = inline ? resolve19(config.root, config.outDir, `${outputName(video.entry.metadata.id)}-${frame}.png`) : resolve19(exportDestination(config), `${outputName(video.entry.metadata.id)}-${frame}.png`);
|
|
2589
2751
|
await renderStill(
|
|
2590
2752
|
origin,
|
|
2591
2753
|
targetFor(
|
|
@@ -2601,7 +2763,7 @@ var devCommand = async (options = {}) => {
|
|
|
2601
2763
|
if (inline) {
|
|
2602
2764
|
response.statusCode = 200;
|
|
2603
2765
|
response.setHeader("content-type", "image/png");
|
|
2604
|
-
response.end(await
|
|
2766
|
+
response.end(await readFile13(file));
|
|
2605
2767
|
return;
|
|
2606
2768
|
}
|
|
2607
2769
|
json(response, 200, { id: "still", status: "ready", progress: 1, output: file });
|
|
@@ -2623,11 +2785,12 @@ var devCommand = async (options = {}) => {
|
|
|
2623
2785
|
input,
|
|
2624
2786
|
{ scenes: compiled.scenes, audio: compiled.audio }
|
|
2625
2787
|
);
|
|
2626
|
-
const output =
|
|
2788
|
+
const output = resolve19(
|
|
2627
2789
|
exportDestination(config),
|
|
2628
2790
|
`${outputName(video.entry.metadata.id)}${format.extension}`
|
|
2629
2791
|
);
|
|
2630
|
-
const
|
|
2792
|
+
const audio = body.audio !== false;
|
|
2793
|
+
const record = await createJob(config, manifest, output, { format: format.name, quality, scale, audio });
|
|
2631
2794
|
json(response, 202, record.job);
|
|
2632
2795
|
void runJob(config, origin, record, video).catch((error) => {
|
|
2633
2796
|
log.error(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2656,6 +2819,24 @@ var devCommand = async (options = {}) => {
|
|
|
2656
2819
|
json(response, 200, (await readJob(config, id)).job);
|
|
2657
2820
|
return;
|
|
2658
2821
|
}
|
|
2822
|
+
if (request.method === "GET" && url.startsWith("/source")) {
|
|
2823
|
+
const asked = new URL(url, "http://localhost").searchParams.get("file") ?? "";
|
|
2824
|
+
const file = resolve19(config.root, asked);
|
|
2825
|
+
const inside = relative7(config.root, file);
|
|
2826
|
+
const readable = /\.(tsx?|jsx?|css|json|md)$/.test(file);
|
|
2827
|
+
if (!inside || inside.startsWith("..") || !readable) {
|
|
2828
|
+
json(response, 400, { error: `Refusing to read ${asked}.` });
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
try {
|
|
2832
|
+
response.statusCode = 200;
|
|
2833
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2834
|
+
response.end(await readFile13(file, "utf8"));
|
|
2835
|
+
} catch {
|
|
2836
|
+
json(response, 404, { error: `${asked} is not there.` });
|
|
2837
|
+
}
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2659
2840
|
if (request.method === "GET" && url.startsWith("/jobs")) {
|
|
2660
2841
|
json(response, 200, await listJobs(config));
|
|
2661
2842
|
return;
|
|
@@ -2680,16 +2861,16 @@ var devCommand = async (options = {}) => {
|
|
|
2680
2861
|
|
|
2681
2862
|
// src/commands/doctor.ts
|
|
2682
2863
|
import { constants } from "fs";
|
|
2683
|
-
import { access, mkdir as
|
|
2684
|
-
import { existsSync as
|
|
2864
|
+
import { access, mkdir as mkdir13, readFile as readFile14, rm as rm5, writeFile as writeFile14 } from "fs/promises";
|
|
2865
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2685
2866
|
import { createRequire as createRequire3 } from "module";
|
|
2686
|
-
import { relative as
|
|
2867
|
+
import { relative as relative8, resolve as resolve20 } from "path";
|
|
2687
2868
|
var MINIMUM_NODE = 20;
|
|
2688
2869
|
var version = (value) => value.replace(/^v/, "").split(".").map(Number);
|
|
2689
2870
|
var runChecks = async (root) => {
|
|
2690
2871
|
const checks = [];
|
|
2691
2872
|
const config = await loadConfig(root);
|
|
2692
|
-
const require2 = createRequire3(
|
|
2873
|
+
const require2 = createRequire3(resolve20(root, "package.json"));
|
|
2693
2874
|
const [major] = version(process.version);
|
|
2694
2875
|
checks.push({
|
|
2695
2876
|
name: "Node",
|
|
@@ -2700,7 +2881,7 @@ var runChecks = async (root) => {
|
|
|
2700
2881
|
let react2 = "not found";
|
|
2701
2882
|
let reactOk = false;
|
|
2702
2883
|
try {
|
|
2703
|
-
const manifest = JSON.parse(await
|
|
2884
|
+
const manifest = JSON.parse(await readFile14(require2.resolve("react/package.json"), "utf8"));
|
|
2704
2885
|
react2 = manifest.version;
|
|
2705
2886
|
reactOk = version(react2)[0] >= 19;
|
|
2706
2887
|
} catch {
|
|
@@ -2712,16 +2893,16 @@ var runChecks = async (root) => {
|
|
|
2712
2893
|
ok: reactOk,
|
|
2713
2894
|
fix: "The runtime needs React 19. Install it: npm install react@19 react-dom@19"
|
|
2714
2895
|
});
|
|
2715
|
-
const videosDir =
|
|
2896
|
+
const videosDir = resolve20(config.root, config.videosDir);
|
|
2716
2897
|
checks.push({
|
|
2717
2898
|
name: "Source root",
|
|
2718
|
-
detail:
|
|
2719
|
-
ok:
|
|
2899
|
+
detail: existsSync17(videosDir) ? relative8(config.root, videosDir) + "/" : `no ${config.videosDir}/`,
|
|
2900
|
+
ok: existsSync17(videosDir),
|
|
2720
2901
|
fix: 'Run "odori init" to add the videos source root.'
|
|
2721
2902
|
});
|
|
2722
2903
|
checks.push({
|
|
2723
2904
|
name: "Config",
|
|
2724
|
-
detail: config.configPath ?
|
|
2905
|
+
detail: config.configPath ? relative8(config.root, config.configPath) : "defaults (no odori.config.ts)",
|
|
2725
2906
|
// Loading got this far, so a config that exists also parsed.
|
|
2726
2907
|
ok: true
|
|
2727
2908
|
});
|
|
@@ -2747,23 +2928,42 @@ var runChecks = async (root) => {
|
|
|
2747
2928
|
detail: unpinned.length === 0 ? `pinned binaries from ${cacheRoot()}` : `${unpinned.length} of 2 from the host; frames may differ from another machine`,
|
|
2748
2929
|
ok: true
|
|
2749
2930
|
});
|
|
2750
|
-
const generated =
|
|
2931
|
+
const generated = resolve20(config.root, ".odori");
|
|
2751
2932
|
let writable = false;
|
|
2752
2933
|
try {
|
|
2753
|
-
await
|
|
2754
|
-
const probe =
|
|
2755
|
-
await
|
|
2934
|
+
await mkdir13(generated, { recursive: true });
|
|
2935
|
+
const probe = resolve20(generated, ".doctor");
|
|
2936
|
+
await writeFile14(probe, "", "utf8");
|
|
2756
2937
|
await access(probe, constants.W_OK);
|
|
2757
|
-
await
|
|
2938
|
+
await rm5(probe, { force: true });
|
|
2758
2939
|
writable = true;
|
|
2759
2940
|
} catch {
|
|
2760
2941
|
writable = false;
|
|
2761
2942
|
}
|
|
2943
|
+
const componentsRoot = resolve20(root, config.componentsDir);
|
|
2944
|
+
const orphans = [];
|
|
2945
|
+
if (existsSync17(componentsRoot)) {
|
|
2946
|
+
const { readdir: readdir9 } = await import("fs/promises");
|
|
2947
|
+
for (const entry of await readdir9(componentsRoot, { withFileTypes: true })) {
|
|
2948
|
+
if (!entry.isDirectory()) continue;
|
|
2949
|
+
const files = await readdir9(resolve20(componentsRoot, entry.name));
|
|
2950
|
+
const source = files.some((file) => /\.tsx$/.test(file) && !file.endsWith(".preview.tsx"));
|
|
2951
|
+
const fixture = files.some((file) => file.endsWith(".preview.tsx"));
|
|
2952
|
+
if (source && !fixture) orphans.push(entry.name);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
checks.push({
|
|
2956
|
+
name: "Component previews",
|
|
2957
|
+
detail: orphans.length === 0 ? "every component has a fixture" : `${orphans.length} without a fixture: ${orphans.slice(0, 4).join(", ")}${orphans.length > 4 ? ", \u2026" : ""}`,
|
|
2958
|
+
ok: true,
|
|
2959
|
+
warn: orphans.length > 0,
|
|
2960
|
+
fix: `Add a sibling <name>.preview.tsx with defineComponentPreview so Studio can play it on its own. A component with no fixture only ever renders inside a video.`
|
|
2961
|
+
});
|
|
2762
2962
|
checks.push({
|
|
2763
2963
|
name: "Generated cache",
|
|
2764
2964
|
detail: writable ? ".odori/ is writable" : ".odori/ cannot be written",
|
|
2765
2965
|
ok: writable,
|
|
2766
|
-
fix: `Odori writes its import graph and render cache to ${
|
|
2966
|
+
fix: `Odori writes its import graph and render cache to ${relative8(process.cwd(), generated) || ".odori"}. Check the directory's permissions.`
|
|
2767
2967
|
});
|
|
2768
2968
|
return checks;
|
|
2769
2969
|
};
|
|
@@ -2773,12 +2973,15 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2773
2973
|
log.title("odori doctor");
|
|
2774
2974
|
for (const check of checks) {
|
|
2775
2975
|
const label = check.name.padEnd(width);
|
|
2776
|
-
if (check.ok) log.
|
|
2976
|
+
if (check.ok && check.warn) log.warn(`${label} ${check.detail}`);
|
|
2977
|
+
else if (check.ok) log.success(`${label} ${check.detail}`);
|
|
2777
2978
|
else log.error(`${label} ${check.detail}`);
|
|
2778
2979
|
}
|
|
2980
|
+
const warned = checks.filter((check) => check.ok && check.warn);
|
|
2779
2981
|
const failed = checks.filter((check) => !check.ok);
|
|
2780
2982
|
if (failed.length === 0) {
|
|
2781
2983
|
log.detail("Everything a render needs is present.");
|
|
2984
|
+
for (const check of warned) log.detail(` ${check.fix}`);
|
|
2782
2985
|
return 0;
|
|
2783
2986
|
}
|
|
2784
2987
|
log.info("");
|
|
@@ -2787,14 +2990,14 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2787
2990
|
};
|
|
2788
2991
|
|
|
2789
2992
|
// src/commands/init.ts
|
|
2790
|
-
import { mkdir as
|
|
2791
|
-
import { existsSync as
|
|
2792
|
-
import { relative as
|
|
2993
|
+
import { mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
|
|
2994
|
+
import { existsSync as existsSync19 } from "fs";
|
|
2995
|
+
import { relative as relative10, resolve as resolve22 } from "path";
|
|
2793
2996
|
|
|
2794
2997
|
// src/commands/new.ts
|
|
2795
|
-
import { mkdir as
|
|
2796
|
-
import { existsSync as
|
|
2797
|
-
import { relative as
|
|
2998
|
+
import { mkdir as mkdir14, readdir as readdir6, writeFile as writeFile15 } from "fs/promises";
|
|
2999
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3000
|
+
import { relative as relative9, resolve as resolve21 } from "path";
|
|
2798
3001
|
var titleCase = (value) => value.split(/[-_\s]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2799
3002
|
var pascalCase = (value) => titleCase(value).replace(/\s+/g, "");
|
|
2800
3003
|
var videoTemplate = (name, hasLayout) => `import {Scene, Video, defineVideoMetadata} from "odori";
|
|
@@ -2852,27 +3055,27 @@ ${closing}
|
|
|
2852
3055
|
`;
|
|
2853
3056
|
};
|
|
2854
3057
|
var installedParts = async (config) => {
|
|
2855
|
-
const componentsDir =
|
|
2856
|
-
if (!
|
|
2857
|
-
const entries = (await
|
|
3058
|
+
const componentsDir = resolve21(config.root, config.componentsDir);
|
|
3059
|
+
if (!existsSync18(componentsDir)) return { title: false, end: false };
|
|
3060
|
+
const entries = (await readdir6(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2858
3061
|
return { title: entries.includes("title-reveal"), end: entries.includes("end-card") };
|
|
2859
3062
|
};
|
|
2860
3063
|
var newCommand = async (name, options = {}) => {
|
|
2861
3064
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error("Use a lowercase, dash separated name.");
|
|
2862
3065
|
const config = await loadConfig(process.cwd());
|
|
2863
|
-
const directory2 =
|
|
2864
|
-
const file =
|
|
2865
|
-
if (
|
|
2866
|
-
const hasLayout =
|
|
3066
|
+
const directory2 = resolve21(config.root, config.videosDir, name);
|
|
3067
|
+
const file = resolve21(directory2, "video.tsx");
|
|
3068
|
+
if (existsSync18(file)) throw new Error(`${relative9(config.root, file)} already exists.`);
|
|
3069
|
+
const hasLayout = existsSync18(resolve21(config.root, config.videosDir, "layout.tsx"));
|
|
2867
3070
|
const parts = options.blank === true ? { title: false, end: false } : await installedParts(config);
|
|
2868
3071
|
const composed = parts.title || parts.end;
|
|
2869
|
-
await
|
|
2870
|
-
await
|
|
3072
|
+
await mkdir14(directory2, { recursive: true });
|
|
3073
|
+
await writeFile15(
|
|
2871
3074
|
file,
|
|
2872
3075
|
composed ? composedTemplate(name, hasLayout, parts) : videoTemplate(name, hasLayout),
|
|
2873
3076
|
"utf8"
|
|
2874
3077
|
);
|
|
2875
|
-
log.success(`Created ${
|
|
3078
|
+
log.success(`Created ${relative9(config.root, file)}`);
|
|
2876
3079
|
if (composed) log.detail("Composed from the components this project has installed.");
|
|
2877
3080
|
else if (options.blank !== true) {
|
|
2878
3081
|
log.detail("No registry components installed yet: odori add title-reveal end-card");
|
|
@@ -2906,21 +3109,21 @@ export const productLayout = defineVideoLayout({
|
|
|
2906
3109
|
});
|
|
2907
3110
|
`;
|
|
2908
3111
|
var initCommand = async (root = process.cwd()) => {
|
|
2909
|
-
const videosDir =
|
|
2910
|
-
await
|
|
3112
|
+
const videosDir = resolve22(root, defaultConfig.videosDir);
|
|
3113
|
+
await mkdir15(resolve22(videosDir, "components"), { recursive: true });
|
|
2911
3114
|
const files = [
|
|
2912
|
-
[
|
|
2913
|
-
[
|
|
2914
|
-
[
|
|
3115
|
+
[resolve22(root, "odori.config.ts"), CONFIG_TEMPLATE],
|
|
3116
|
+
[resolve22(videosDir, "layout.tsx"), LAYOUT_TEMPLATE],
|
|
3117
|
+
[resolve22(videosDir, "launch", "video.tsx"), videoTemplate("launch", true)]
|
|
2915
3118
|
];
|
|
2916
3119
|
for (const [file, contents] of files) {
|
|
2917
|
-
if (
|
|
2918
|
-
log.detail(`Kept existing ${
|
|
3120
|
+
if (existsSync19(file)) {
|
|
3121
|
+
log.detail(`Kept existing ${relative10(root, file)}`);
|
|
2919
3122
|
continue;
|
|
2920
3123
|
}
|
|
2921
|
-
await
|
|
2922
|
-
await
|
|
2923
|
-
log.success(`Created ${
|
|
3124
|
+
await mkdir15(resolve22(file, ".."), { recursive: true });
|
|
3125
|
+
await writeFile16(file, contents, "utf8");
|
|
3126
|
+
log.success(`Created ${relative10(root, file)}`);
|
|
2924
3127
|
}
|
|
2925
3128
|
log.detail("Next: odori doctor, then odori add title-reveal end-card, then odori dev.");
|
|
2926
3129
|
};
|
|
@@ -3011,15 +3214,16 @@ var listCommand = async () => {
|
|
|
3011
3214
|
}
|
|
3012
3215
|
};
|
|
3013
3216
|
|
|
3014
|
-
// src/commands/
|
|
3015
|
-
import { resolve as
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}
|
|
3217
|
+
// src/commands/frame.ts
|
|
3218
|
+
import { resolve as resolve23 } from "path";
|
|
3219
|
+
import { framesFromOffset, resolveEntryLayout as resolveEntryLayout7 } from "odori";
|
|
3220
|
+
var frameCommand = async (id, options = {}) => {
|
|
3221
|
+
const at = options.at ?? 0;
|
|
3222
|
+
framesFromOffset(at, 30);
|
|
3021
3223
|
const { config, graph, videos } = await createContext();
|
|
3022
3224
|
const video = findVideo(videos, id);
|
|
3225
|
+
const fps = resolveEntryLayout7(video.entry).format.fps;
|
|
3226
|
+
const frame = framesFromOffset(at, fps);
|
|
3023
3227
|
const output = await withServer(config, async (server) => {
|
|
3024
3228
|
const { durationInFrames, scenes, audio } = await compileInBrowser(server.url, targetFor(video, options.input), config);
|
|
3025
3229
|
const { manifest, input, prepared } = await freezeManifest(
|
|
@@ -3033,21 +3237,21 @@ var stillCommand = async (id, options = {}) => {
|
|
|
3033
3237
|
throw new Error(`Frame ${frame} is past the last frame (${manifest.format.durationInFrames - 1}).`);
|
|
3034
3238
|
}
|
|
3035
3239
|
const target = targetFor({ ...video, durationInFrames: manifest.format.durationInFrames }, input, prepared);
|
|
3036
|
-
const file =
|
|
3240
|
+
const file = resolve23(config.root, options.output ?? `${config.exportDir}/${outputName(id)}-${frame}.png`);
|
|
3037
3241
|
return renderStill(server.url, target, frame, file, config);
|
|
3038
3242
|
});
|
|
3039
|
-
log.success(`
|
|
3243
|
+
log.success(`Frame ${frame} written to ${output}`);
|
|
3040
3244
|
return output;
|
|
3041
3245
|
};
|
|
3042
3246
|
|
|
3043
3247
|
// src/commands/test.ts
|
|
3044
|
-
import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as
|
|
3248
|
+
import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as resolveEntryLayout9 } from "odori";
|
|
3045
3249
|
|
|
3046
3250
|
// src/contracts.ts
|
|
3047
|
-
import { existsSync as
|
|
3048
|
-
import { readdir as
|
|
3049
|
-
import { resolve as
|
|
3050
|
-
import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as
|
|
3251
|
+
import { existsSync as existsSync20 } from "fs";
|
|
3252
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
3253
|
+
import { resolve as resolve24 } from "path";
|
|
3254
|
+
import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as resolveEntryLayout8 } from "odori";
|
|
3051
3255
|
var primaryFamily = (stack) => (stack.split(",")[0] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
3052
3256
|
var SYSTEM_FAMILIES = /* @__PURE__ */ new Set([
|
|
3053
3257
|
"system-ui",
|
|
@@ -3113,8 +3317,8 @@ var checkAudioWindows = (cues, brand, videoId) => {
|
|
|
3113
3317
|
return failures;
|
|
3114
3318
|
};
|
|
3115
3319
|
var checkInstalledContracts = async (config, videos) => {
|
|
3116
|
-
const componentsDir =
|
|
3117
|
-
const onDisk =
|
|
3320
|
+
const componentsDir = resolve24(config.root, config.componentsDir);
|
|
3321
|
+
const onDisk = existsSync20(componentsDir) ? (await readdir7(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
3118
3322
|
const names = /* @__PURE__ */ new Set([...Object.keys(await readProvenance(config)), ...onDisk]);
|
|
3119
3323
|
if (names.size === 0) return [];
|
|
3120
3324
|
const { items } = await resolveRegistry(config, { allowNetwork: false });
|
|
@@ -3123,7 +3327,7 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3123
3327
|
const seen = /* @__PURE__ */ new Set();
|
|
3124
3328
|
const failures = [];
|
|
3125
3329
|
for (const video of videos) {
|
|
3126
|
-
const { brand } =
|
|
3330
|
+
const { brand } = resolveEntryLayout8(video.entry);
|
|
3127
3331
|
for (const failure of checkComponentRequirements(installed, brand, video.entry.metadata.id)) {
|
|
3128
3332
|
const key = `${brand.name}:${failure.message}`;
|
|
3129
3333
|
if (seen.has(key)) continue;
|
|
@@ -3135,9 +3339,9 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3135
3339
|
};
|
|
3136
3340
|
|
|
3137
3341
|
// src/determinism.ts
|
|
3138
|
-
import { readdir as
|
|
3139
|
-
import { existsSync as
|
|
3140
|
-
import { join as join7, relative as
|
|
3342
|
+
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
3343
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3344
|
+
import { join as join7, relative as relative11, resolve as resolve25 } from "path";
|
|
3141
3345
|
var FORBIDDEN = [
|
|
3142
3346
|
{
|
|
3143
3347
|
pattern: /\bMath\.random\s*\(/,
|
|
@@ -3169,7 +3373,7 @@ var scanSource = (source, file) => {
|
|
|
3169
3373
|
return findings;
|
|
3170
3374
|
};
|
|
3171
3375
|
var walk2 = async (directory2, files = []) => {
|
|
3172
|
-
for (const entry of await
|
|
3376
|
+
for (const entry of await readdir8(directory2, { withFileTypes: true })) {
|
|
3173
3377
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3174
3378
|
const full = join7(directory2, entry.name);
|
|
3175
3379
|
if (entry.isDirectory()) await walk2(full, files);
|
|
@@ -3178,11 +3382,11 @@ var walk2 = async (directory2, files = []) => {
|
|
|
3178
3382
|
return files;
|
|
3179
3383
|
};
|
|
3180
3384
|
var checkDeterminism = async (config) => {
|
|
3181
|
-
const root =
|
|
3182
|
-
if (!
|
|
3385
|
+
const root = resolve25(config.root, config.videosDir);
|
|
3386
|
+
if (!existsSync21(root)) return [];
|
|
3183
3387
|
const files = await walk2(root);
|
|
3184
3388
|
const findings = await Promise.all(
|
|
3185
|
-
files.map(async (file) => scanSource(await
|
|
3389
|
+
files.map(async (file) => scanSource(await readFile15(file, "utf8"), relative11(config.root, file)))
|
|
3186
3390
|
);
|
|
3187
3391
|
return findings.flat();
|
|
3188
3392
|
};
|
|
@@ -3281,7 +3485,7 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3281
3485
|
})()`;
|
|
3282
3486
|
var testVideo = async (origin, video, config, failures, quiet = false) => {
|
|
3283
3487
|
const id = video.entry.metadata.id;
|
|
3284
|
-
const layout =
|
|
3488
|
+
const layout = resolveEntryLayout9(video.entry);
|
|
3285
3489
|
if (isOdoriSchema2(video.entry.metadata.schema)) {
|
|
3286
3490
|
const result = video.entry.metadata.schema.safeParse(video.entry.metadata.defaultProps ?? {});
|
|
3287
3491
|
if (!result.success) failures.push({ video: id, message: `defaultProps fail the schema: ${result.issues.join("; ")}` });
|
|
@@ -3390,6 +3594,8 @@ var testCommand = async (id, options = {}) => {
|
|
|
3390
3594
|
};
|
|
3391
3595
|
|
|
3392
3596
|
// src/cli.ts
|
|
3597
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["force", "dry-run", "json", "no-audio", "no-frame-skip", "no-open", "open", "help", "version"]);
|
|
3598
|
+
var RENAMED = { still: "frame" };
|
|
3393
3599
|
var parseArgs = (argv) => {
|
|
3394
3600
|
const [command2 = "help", ...rest] = argv;
|
|
3395
3601
|
const positionals = [];
|
|
@@ -3404,7 +3610,7 @@ var parseArgs = (argv) => {
|
|
|
3404
3610
|
}
|
|
3405
3611
|
const name = token.slice(2);
|
|
3406
3612
|
const next = rest[index + 1];
|
|
3407
|
-
if (next === void 0 || next.startsWith("--")) flags[name] = true;
|
|
3613
|
+
if (BOOLEAN_FLAGS.has(name) || next === void 0 || next.startsWith("--")) flags[name] = true;
|
|
3408
3614
|
else {
|
|
3409
3615
|
flags[name] = next;
|
|
3410
3616
|
index += 1;
|
|
@@ -3446,9 +3652,9 @@ var COMMAND_FLAGS = {
|
|
|
3446
3652
|
update: ["force"],
|
|
3447
3653
|
list: [],
|
|
3448
3654
|
inspect: ["json", "input"],
|
|
3449
|
-
|
|
3655
|
+
frame: ["at", "output", "input"],
|
|
3450
3656
|
test: ["json"],
|
|
3451
|
-
export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-frame-skip", "retry"],
|
|
3657
|
+
export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-audio", "no-frame-skip", "retry"],
|
|
3452
3658
|
jobs: [],
|
|
3453
3659
|
help: []
|
|
3454
3660
|
};
|
|
@@ -3509,18 +3715,20 @@ var USAGE = {
|
|
|
3509
3715
|
Print discovered video ids and formats.`,
|
|
3510
3716
|
inspect: `odori inspect <id> [--json] [--input <json>]
|
|
3511
3717
|
Show resolved layout, inputs, scenes, and assets.`,
|
|
3512
|
-
|
|
3513
|
-
Render one deterministic frame
|
|
3718
|
+
frame: `odori frame <id> --at <time> [--output <path>] [--input <json>]
|
|
3719
|
+
Render one deterministic frame to a PNG. --at is a duration: 4s, 500ms, or
|
|
3720
|
+
120f for frame 120. A bare number is seconds.`,
|
|
3514
3721
|
test: `odori test [id] [--json]
|
|
3515
3722
|
Validate contracts and representative frames. --json emits one object per
|
|
3516
3723
|
check, for CI.`,
|
|
3517
3724
|
export: `odori export <id> [--output <path>] [--input <json>] [--concurrency <n>]
|
|
3518
3725
|
[--preset <name>] [--format <name>] [--quality <tier>] [--scale <n>]
|
|
3519
|
-
[--no-frame-skip] [--retry <job>]
|
|
3726
|
+
[--no-audio] [--no-frame-skip] [--retry <job>]
|
|
3520
3727
|
Render and encode a distributable file. --format is mp4, webm, prores, gif,
|
|
3521
3728
|
or png; without it the output's extension decides, and mp4 is the default.
|
|
3522
3729
|
--quality is studio, social, or web. --scale multiplies the output size,
|
|
3523
|
-
0.25 to 2.
|
|
3730
|
+
0.25 to 2. --no-audio writes the picture with no sound. A retry keeps the
|
|
3731
|
+
settings its job was created with.`,
|
|
3524
3732
|
jobs: `odori jobs
|
|
3525
3733
|
List export jobs and their status.`
|
|
3526
3734
|
};
|
|
@@ -3538,14 +3746,14 @@ Usage
|
|
|
3538
3746
|
odori update [components] Apply upstream component changes
|
|
3539
3747
|
odori list Print discovered video ids and formats
|
|
3540
3748
|
odori inspect <id> [--json] Show resolved layout, inputs, scenes, assets
|
|
3541
|
-
odori
|
|
3749
|
+
odori frame <id> --at 4s Render one deterministic frame to a PNG
|
|
3542
3750
|
odori test [id] [--json] Validate contracts and representative frames
|
|
3543
3751
|
odori export <id> [--output f] Render and encode a distributable file
|
|
3544
3752
|
odori jobs List export jobs and their status
|
|
3545
3753
|
|
|
3546
3754
|
Options
|
|
3547
3755
|
--input '{"headline":"..."}' Serializable input for the video schema
|
|
3548
|
-
--output <path> Output path for
|
|
3756
|
+
--output <path> Output path for frame and export
|
|
3549
3757
|
--force Replace locally modified component source
|
|
3550
3758
|
--concurrency <n> Parallel render workers for export
|
|
3551
3759
|
--preset <name> x264 preset for export, default medium
|
|
@@ -3622,9 +3830,11 @@ var run2 = async (argv) => {
|
|
|
3622
3830
|
case "inspect":
|
|
3623
3831
|
await inspectCommand(positionals[0] ?? "", { json: flags.json === true, input: parseInput(flags) });
|
|
3624
3832
|
return 0;
|
|
3625
|
-
case "
|
|
3626
|
-
await
|
|
3627
|
-
|
|
3833
|
+
case "frame":
|
|
3834
|
+
await frameCommand(positionals[0] ?? "", {
|
|
3835
|
+
// A duration, so "4s" and "120f" both work; a bare number is
|
|
3836
|
+
// seconds, the way every other time value in Odori reads.
|
|
3837
|
+
at: typeof flags.at === "string" ? flags.at : numberFlag(flags, "at") ?? 0,
|
|
3628
3838
|
output: typeof flags.output === "string" ? flags.output : void 0,
|
|
3629
3839
|
input: parseInput(flags)
|
|
3630
3840
|
});
|
|
@@ -3641,6 +3851,7 @@ var run2 = async (argv) => {
|
|
|
3641
3851
|
quality: typeof flags.quality === "string" ? flags.quality : void 0,
|
|
3642
3852
|
scale: numberFlag(flags, "scale"),
|
|
3643
3853
|
format: typeof flags.format === "string" ? flags.format : void 0,
|
|
3854
|
+
audio: flags["no-audio"] === true ? false : void 0,
|
|
3644
3855
|
skipUnchangedFrames: flags["no-frame-skip"] === true ? false : void 0,
|
|
3645
3856
|
retry: typeof flags.retry === "string" ? flags.retry : void 0
|
|
3646
3857
|
});
|
|
@@ -3654,6 +3865,11 @@ var run2 = async (argv) => {
|
|
|
3654
3865
|
log.info(HELP);
|
|
3655
3866
|
return 0;
|
|
3656
3867
|
default: {
|
|
3868
|
+
const renamed = RENAMED[command2];
|
|
3869
|
+
if (renamed) {
|
|
3870
|
+
log.error(`"odori ${command2}" is now "odori ${renamed}".`);
|
|
3871
|
+
return 1;
|
|
3872
|
+
}
|
|
3657
3873
|
const commands = Object.keys(COMMAND_FLAGS).filter((name) => name !== "help");
|
|
3658
3874
|
const suggestion = nearest(command2, commands);
|
|
3659
3875
|
log.error(`Unknown command "${command2}".${suggestion ? ` Did you mean "odori ${suggestion}"?` : ""}`);
|
|
@@ -3729,7 +3945,7 @@ export {
|
|
|
3729
3945
|
exportCommand,
|
|
3730
3946
|
jobsCommand,
|
|
3731
3947
|
devCommand,
|
|
3732
|
-
|
|
3948
|
+
frameCommand,
|
|
3733
3949
|
diffLines,
|
|
3734
3950
|
countChanges,
|
|
3735
3951
|
formatDiff,
|