frameyard 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/GLOSSARY.md +145 -0
  3. package/LICENSE +21 -0
  4. package/README.md +311 -2
  5. package/dist/assets/executable.d.ts +16 -0
  6. package/dist/assets/executable.d.ts.map +1 -0
  7. package/dist/assets/executable.js +59 -0
  8. package/dist/assets/executable.js.map +1 -0
  9. package/dist/assets/files.d.ts +4 -0
  10. package/dist/assets/files.d.ts.map +1 -0
  11. package/dist/assets/files.js +30 -0
  12. package/dist/assets/files.js.map +1 -0
  13. package/dist/assets/integrations.d.ts +15 -0
  14. package/dist/assets/integrations.d.ts.map +1 -0
  15. package/dist/assets/integrations.js +15 -0
  16. package/dist/assets/integrations.js.map +1 -0
  17. package/dist/assets/process.d.ts +29 -0
  18. package/dist/assets/process.d.ts.map +1 -0
  19. package/dist/assets/process.js +97 -0
  20. package/dist/assets/process.js.map +1 -0
  21. package/dist/assets/provenance.d.ts +77 -0
  22. package/dist/assets/provenance.d.ts.map +1 -0
  23. package/dist/assets/provenance.js +302 -0
  24. package/dist/assets/provenance.js.map +1 -0
  25. package/dist/assets/transcribe.d.ts +38 -0
  26. package/dist/assets/transcribe.d.ts.map +1 -0
  27. package/dist/assets/transcribe.js +357 -0
  28. package/dist/assets/transcribe.js.map +1 -0
  29. package/dist/assets/tts.d.ts +29 -0
  30. package/dist/assets/tts.d.ts.map +1 -0
  31. package/dist/assets/tts.js +180 -0
  32. package/dist/assets/tts.js.map +1 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +795 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/contracts.d.ts +106 -0
  38. package/dist/contracts.d.ts.map +1 -0
  39. package/dist/contracts.js +297 -0
  40. package/dist/contracts.js.map +1 -0
  41. package/dist/doctor.d.ts +56 -0
  42. package/dist/doctor.d.ts.map +1 -0
  43. package/dist/doctor.js +476 -0
  44. package/dist/doctor.js.map +1 -0
  45. package/dist/errors.d.ts +10 -0
  46. package/dist/errors.d.ts.map +1 -0
  47. package/dist/errors.js +18 -0
  48. package/dist/errors.js.map +1 -0
  49. package/dist/index.d.ts +8 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +5 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/preview/main.d.ts +19 -0
  54. package/dist/preview/main.d.ts.map +1 -0
  55. package/dist/preview/main.js +480 -0
  56. package/dist/preview/main.js.map +1 -0
  57. package/dist/project.d.ts +16 -0
  58. package/dist/project.d.ts.map +1 -0
  59. package/dist/project.js +88 -0
  60. package/dist/project.js.map +1 -0
  61. package/dist/renderer/audio.d.ts +31 -0
  62. package/dist/renderer/audio.d.ts.map +1 -0
  63. package/dist/renderer/audio.js +88 -0
  64. package/dist/renderer/audio.js.map +1 -0
  65. package/dist/renderer/browser.d.ts +22 -0
  66. package/dist/renderer/browser.d.ts.map +1 -0
  67. package/dist/renderer/browser.js +175 -0
  68. package/dist/renderer/browser.js.map +1 -0
  69. package/dist/renderer/ffmpeg.d.ts +58 -0
  70. package/dist/renderer/ffmpeg.d.ts.map +1 -0
  71. package/dist/renderer/ffmpeg.js +268 -0
  72. package/dist/renderer/ffmpeg.js.map +1 -0
  73. package/dist/renderer/index.d.ts +72 -0
  74. package/dist/renderer/index.d.ts.map +1 -0
  75. package/dist/renderer/index.js +281 -0
  76. package/dist/renderer/index.js.map +1 -0
  77. package/dist/renderer/transcripts.d.ts +30 -0
  78. package/dist/renderer/transcripts.d.ts.map +1 -0
  79. package/dist/renderer/transcripts.js +86 -0
  80. package/dist/renderer/transcripts.js.map +1 -0
  81. package/dist/runtime/clock.d.ts +5 -0
  82. package/dist/runtime/clock.d.ts.map +1 -0
  83. package/dist/runtime/clock.js +16 -0
  84. package/dist/runtime/clock.js.map +1 -0
  85. package/dist/runtime/index.d.ts +29 -0
  86. package/dist/runtime/index.d.ts.map +1 -0
  87. package/dist/runtime/index.js +289 -0
  88. package/dist/runtime/index.js.map +1 -0
  89. package/dist/runtime/random.d.ts +8 -0
  90. package/dist/runtime/random.d.ts.map +1 -0
  91. package/dist/runtime/random.js +21 -0
  92. package/dist/runtime/random.js.map +1 -0
  93. package/dist/scaffold.d.ts +3 -0
  94. package/dist/scaffold.d.ts.map +1 -0
  95. package/dist/scaffold.js +176 -0
  96. package/dist/scaffold.js.map +1 -0
  97. package/dist/server/preview-html.d.ts +2 -0
  98. package/dist/server/preview-html.d.ts.map +1 -0
  99. package/dist/server/preview-html.js +229 -0
  100. package/dist/server/preview-html.js.map +1 -0
  101. package/dist/server/server.d.ts +17 -0
  102. package/dist/server/server.d.ts.map +1 -0
  103. package/dist/server/server.js +237 -0
  104. package/dist/server/server.js.map +1 -0
  105. package/dist/transcript.d.ts +44 -0
  106. package/dist/transcript.d.ts.map +1 -0
  107. package/dist/transcript.js +130 -0
  108. package/dist/transcript.js.map +1 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.d.ts.map +1 -0
  111. package/dist/version.js +2 -0
  112. package/dist/version.js.map +1 -0
  113. package/docs/adr/0001-keep-composition-authoring-web-native.md +3 -0
  114. package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +3 -0
  115. package/docs/adr/0003-render-browser-frames-offline.md +3 -0
  116. package/docs/adr/0004-produce-local-ai-assets-outside-rendering.md +3 -0
  117. package/docs/v0.1-design.md +238 -0
  118. package/examples/demo/README.md +24 -0
  119. package/examples/demo/assets/chime.wav +0 -0
  120. package/examples/demo/compositions/demo.js +206 -0
  121. package/examples/demo/props/demo.json +5 -0
  122. package/examples/demo/video.config.js +5 -0
  123. package/package.json +73 -3
  124. package/skills/frameyard/README.md +35 -0
  125. package/skills/frameyard/SKILL.md +60 -0
  126. package/skills/frameyard/references/audio-assets.md +86 -0
  127. package/skills/frameyard/references/deterministic-authoring.md +107 -0
  128. package/skills/frameyard/references/failure-recovery.md +57 -0
  129. package/skills/frameyard/references/json-results.md +203 -0
  130. package/skills/frameyard/references/subtitles-karaoke.md +95 -0
  131. package/skills/frameyard/scripts/frameyard.mjs +60 -0
package/dist/cli.js ADDED
@@ -0,0 +1,795 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { FrameyardError, isFrameyardError } from "./errors.js";
5
+ import { loadProject, resolveProjectRoot } from "./project.js";
6
+ import { checkComposition, captureStill, renderComposition } from "./renderer/index.js";
7
+ import { startPreviewServer } from "./server/server.js";
8
+ import { scaffoldFiles, SCAFFOLD_NEXT } from "./scaffold.js";
9
+ import { VERSION } from "./version.js";
10
+ import { transcribeAudio } from "./assets/transcribe.js";
11
+ import { synthesizeSpeech } from "./assets/tts.js";
12
+ import { resolveManifestPath } from "./assets/provenance.js";
13
+ import { runDoctor } from "./doctor.js";
14
+ const COMMANDS = new Set(["init", "dev", "check", "still", "render", "tts", "transcribe", "doctor", "help"]);
15
+ const VALUE_FLAGS = new Set([
16
+ "project",
17
+ "props",
18
+ "out",
19
+ "frame",
20
+ "port",
21
+ "text-file",
22
+ "speaker",
23
+ "talker-model",
24
+ "codec-model",
25
+ "style",
26
+ "language",
27
+ "device",
28
+ "executable",
29
+ "model",
30
+ "export",
31
+ "manifest",
32
+ "qwen-tts",
33
+ "whisper-cli",
34
+ ]);
35
+ const BOOLEAN_FLAGS = new Set(["json", "overwrite", "force", "words", "help", "version"]);
36
+ function usage(message = "Invalid command line.") {
37
+ throw new FrameyardError("USAGE", message);
38
+ }
39
+ function parseArgs(argv) {
40
+ const positionals = [];
41
+ const flags = new Map();
42
+ let command = "";
43
+ let raw = false;
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const argument = argv[index];
46
+ if (raw) {
47
+ positionals.push(argument);
48
+ continue;
49
+ }
50
+ if (argument === "--") {
51
+ raw = true;
52
+ continue;
53
+ }
54
+ if (argument === "-h") {
55
+ flags.set("help", true);
56
+ continue;
57
+ }
58
+ if (argument === "-v") {
59
+ flags.set("version", true);
60
+ continue;
61
+ }
62
+ if (argument.startsWith("--")) {
63
+ const equals = argument.indexOf("=");
64
+ const name = equals === -1 ? argument.slice(2) : argument.slice(2, equals);
65
+ const inlineValue = equals === -1 ? undefined : argument.slice(equals + 1);
66
+ if (VALUE_FLAGS.has(name)) {
67
+ let value = inlineValue;
68
+ if (value === undefined) {
69
+ index += 1;
70
+ value = argv[index];
71
+ }
72
+ if (value === undefined || value === "") {
73
+ usage(`Option --${name} requires a value.`);
74
+ }
75
+ flags.set(name, value);
76
+ continue;
77
+ }
78
+ if (BOOLEAN_FLAGS.has(name)) {
79
+ if (inlineValue !== undefined) {
80
+ usage(`Option --${name} does not take a value.`);
81
+ }
82
+ flags.set(name, true);
83
+ continue;
84
+ }
85
+ usage(`Unknown option --${name}.`);
86
+ }
87
+ if (argument.startsWith("-") && argument !== "-") {
88
+ usage(`Unknown option ${argument}.`);
89
+ }
90
+ if (!command) {
91
+ command = argument;
92
+ }
93
+ else {
94
+ positionals.push(argument);
95
+ }
96
+ }
97
+ return { command, positionals, flags };
98
+ }
99
+ function allowedFlags(command) {
100
+ const common = new Set(["project", "json", "help"]);
101
+ switch (command) {
102
+ case "init":
103
+ return new Set(["json", "help"]);
104
+ case "dev":
105
+ return new Set(["project", "port", "help"]);
106
+ case "check":
107
+ return new Set([...common, "props"]);
108
+ case "still":
109
+ return new Set([...common, "props", "out", "frame", "overwrite"]);
110
+ case "render":
111
+ return new Set([...common, "props", "out", "overwrite"]);
112
+ case "tts":
113
+ return new Set([
114
+ ...common,
115
+ "text-file",
116
+ "speaker",
117
+ "talker-model",
118
+ "codec-model",
119
+ "out",
120
+ "style",
121
+ "language",
122
+ "device",
123
+ "executable",
124
+ "manifest",
125
+ "force",
126
+ ]);
127
+ case "transcribe":
128
+ return new Set([...common, "out", "model", "words", "export", "language", "executable", "manifest", "force"]);
129
+ case "doctor":
130
+ return new Set([...common, "manifest", "qwen-tts", "whisper-cli", "talker-model", "codec-model", "model"]);
131
+ case "help":
132
+ return new Set(["help"]);
133
+ default:
134
+ return new Set();
135
+ }
136
+ }
137
+ function validateArgs(parsed) {
138
+ if (parsed.flags.has("version")) {
139
+ return;
140
+ }
141
+ if (!parsed.command) {
142
+ return;
143
+ }
144
+ if (!COMMANDS.has(parsed.command)) {
145
+ usage(`Unknown command "${parsed.command}".`);
146
+ }
147
+ for (const flag of parsed.flags.keys()) {
148
+ if (!allowedFlags(parsed.command).has(flag)) {
149
+ usage(`Option --${flag} is not valid for "${parsed.command}".`);
150
+ }
151
+ }
152
+ if (["dev", "help", "doctor", "tts"].includes(parsed.command)) {
153
+ if (parsed.positionals.length > 0) {
154
+ usage(`"${parsed.command}" does not take a positional argument.`);
155
+ }
156
+ }
157
+ else if (parsed.command === "init") {
158
+ if (parsed.positionals.length > 1) {
159
+ usage(`"${parsed.command}" accepts at most 1 positional argument.`);
160
+ }
161
+ }
162
+ else if (parsed.positionals.length !== 1) {
163
+ usage(`"${parsed.command}" requires a ${parsed.command === "transcribe" ? "source path" : "composition id"}.`);
164
+ }
165
+ if (parsed.command === "still" && !parsed.flags.has("frame")) {
166
+ usage('"still" requires --frame <n>.');
167
+ }
168
+ if (parsed.command === "render" && !parsed.flags.has("out")) {
169
+ usage('"render" requires --out <video.mp4>.');
170
+ }
171
+ if (parsed.command === "tts") {
172
+ for (const required of ["speaker", "talker-model", "codec-model", "out"]) {
173
+ if (!parsed.flags.has(required)) {
174
+ usage(`"tts" requires --${required}.`);
175
+ }
176
+ }
177
+ }
178
+ if (parsed.command === "transcribe") {
179
+ for (const required of ["out", "model"]) {
180
+ if (!parsed.flags.has(required)) {
181
+ usage(`"transcribe" requires --${required}.`);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ function inferCommand(argv) {
187
+ for (let index = 0; index < argv.length; index += 1) {
188
+ const argument = argv[index];
189
+ if (argument === "--") {
190
+ return "";
191
+ }
192
+ if (argument.startsWith("--")) {
193
+ const name = argument.slice(2).split("=")[0];
194
+ if (VALUE_FLAGS.has(name) && !argument.includes("=")) {
195
+ index += 1;
196
+ }
197
+ continue;
198
+ }
199
+ if (argument.startsWith("-")) {
200
+ continue;
201
+ }
202
+ return argument;
203
+ }
204
+ return "";
205
+ }
206
+ function printHelp() {
207
+ process.stdout.write(`Frameyard ${VERSION}
208
+
209
+ Local-first, deterministic video compositions authored with HTML, CSS, and JavaScript.
210
+
211
+ Usage:
212
+ frameyard init [directory]
213
+ frameyard dev [--port <port>] [--project <dir>]
214
+ frameyard check <composition-id> [--props <file>] [--project <dir>] [--json]
215
+ frameyard still <composition-id> --frame <n> [--out <file.png>] [--props <file>] [--project <dir>] [--json]
216
+ frameyard render <composition-id> --out <video.mp4> [--props <file>] [--project <dir>] [--overwrite] [--json]
217
+ frameyard tts --speaker <name> --talker-model <gguf> --codec-model <gguf> --out <file.wav> [--text-file <file>] [--style <text>] [--language <lang>] [--device <name>] [--executable <path>] [--manifest <file>] [--force] [--json]
218
+ frameyard transcribe <source> --model <gguf> --out <file.json> [--words] [--export srt,vtt] [--language <lang>] [--executable <path>] [--manifest <file>] [--force] [--json]
219
+ frameyard doctor [--qwen-tts <path>] [--whisper-cli <path>] [--talker-model <gguf>] [--codec-model <gguf>] [--model <gguf>] [--manifest <file>] [--project <dir>] [--json]
220
+
221
+ Options:
222
+ --project <dir> Use a project other than the current directory.
223
+ --props <file> JSON props merged over composition defaults.
224
+ --out <file> Output path for still, render, tts, or transcribe.
225
+ --frame <n> Frame to capture for still.
226
+ --overwrite Replace an existing still or render output.
227
+ --force Replace existing generated assets and manifest entries.
228
+ --text-file <file> Text to synthesize. Reads stdin when omitted.
229
+ --speaker <name> Named speaker for CustomVoice speech synthesis.
230
+ --talker-model <gguf> Talker GGUF model path.
231
+ --codec-model <gguf> Codec GGUF model path.
232
+ --style <text> Optional style instruction for speech synthesis.
233
+ --language <lang> Spoken language, when known.
234
+ --device <name> Compute device for qwen-tts, such as CPU or CUDA0.
235
+ --model <gguf> Whisper GGML model path.
236
+ --words Request measured word timings during transcription.
237
+ --export srt,vtt Derived subtitle exports next to the transcript.
238
+ --executable <path> Explicit qwen-tts or whisper-cli executable.
239
+ --manifest <file> Provenance manifest path (default: frameyard.assets.json).
240
+ --qwen-tts <path> qwen-tts executable to inspect with doctor.
241
+ --whisper-cli <path> whisper-cli executable to inspect with doctor.
242
+ --json Print a structured result to stdout.
243
+ --help, -h Show this help.
244
+ --version, -v Print the version.
245
+ `);
246
+ }
247
+ function printCommandHelp(command) {
248
+ switch (command) {
249
+ case "init":
250
+ process.stdout.write("Usage: frameyard init [directory]\nScaffold a minimal Frameyard project.\n");
251
+ return;
252
+ case "dev":
253
+ process.stdout.write("Usage: frameyard dev [--port <port>] [--project <dir>]\nStart the browser preview server.\n");
254
+ return;
255
+ case "check":
256
+ process.stdout.write("Usage: frameyard check <composition-id> [--props <file>] [--project <dir>] [--json]\nValidate props, assets, audio, and every frame without encoding.\n");
257
+ return;
258
+ case "still":
259
+ process.stdout.write("Usage: frameyard still <composition-id> --frame <n> [--out <file.png>] [--props <file>] [--project <dir>] [--json]\nCapture one frame through the production rendering path.\n");
260
+ return;
261
+ case "render":
262
+ process.stdout.write("Usage: frameyard render <composition-id> --out <video.mp4> [--props <file>] [--project <dir>] [--overwrite] [--json]\nRender a complete MP4 with H.264 video and AAC audio.\n");
263
+ return;
264
+ case "tts":
265
+ process.stdout.write("Usage: frameyard tts --speaker <name> --talker-model <gguf> --codec-model <gguf> --out <file.wav> [--text-file <file>] [--style <text>] [--language <lang>] [--device <name>] [--executable <path>] [--manifest <file>] [--force] [--json]\nProduce one frozen 24 kHz mono WAV with local qwentts.cpp CustomVoice synthesis.\n");
266
+ return;
267
+ case "transcribe":
268
+ process.stdout.write("Usage: frameyard transcribe <source> --model <gguf> --out <file.json> [--words] [--export srt,vtt] [--language <lang>] [--executable <path>] [--manifest <file>] [--force] [--json]\nProduce a canonical Frameyard transcript, optionally with word timings and subtitle exports.\n");
269
+ return;
270
+ case "doctor":
271
+ process.stdout.write("Usage: frameyard doctor [--qwen-tts <path>] [--whisper-cli <path>] [--talker-model <gguf>] [--codec-model <gguf>] [--model <gguf>] [--manifest <file>] [--project <dir>] [--json]\nInspect core requirements, optional integrations, model readability, and provenance integrity without changing the environment.\n");
272
+ return;
273
+ default:
274
+ printHelp();
275
+ }
276
+ }
277
+ async function projectFromFlag(flags) {
278
+ const requested = flags.get("project");
279
+ const root = typeof requested === "string" ? path.resolve(process.cwd(), requested) : await resolveProjectRoot(process.cwd());
280
+ return loadProject(root);
281
+ }
282
+ /** Resolves the directory that anchors the project-local provenance manifest. */
283
+ async function assetRootFromFlag(flags) {
284
+ const requested = flags.get("project");
285
+ if (typeof requested === "string") {
286
+ const root = path.resolve(process.cwd(), requested);
287
+ let info;
288
+ try {
289
+ info = await stat(root);
290
+ }
291
+ catch {
292
+ throw new FrameyardError("CONFIG_NOT_FOUND", `Project directory does not exist: ${root}`);
293
+ }
294
+ if (!info.isDirectory()) {
295
+ throw new FrameyardError("CONFIG_NOT_FOUND", `Project directory is not a directory: ${root}`);
296
+ }
297
+ return root;
298
+ }
299
+ try {
300
+ return await resolveProjectRoot(process.cwd());
301
+ }
302
+ catch (error) {
303
+ if (isFrameyardError(error) && error.code === "CONFIG_NOT_FOUND") {
304
+ return process.cwd();
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ async function readStdin() {
310
+ const chunks = [];
311
+ for await (const chunk of process.stdin) {
312
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
313
+ }
314
+ return Buffer.concat(chunks).toString("utf8");
315
+ }
316
+ function flagString(flags, name) {
317
+ const value = flags.get(name);
318
+ return typeof value === "string" ? value : undefined;
319
+ }
320
+ function parseSubtitleFormats(value) {
321
+ if (value === undefined) {
322
+ return [];
323
+ }
324
+ if (typeof value !== "string") {
325
+ usage("--export requires a comma-separated list of srt and vtt.");
326
+ }
327
+ const formats = [];
328
+ for (const raw of value.split(",")) {
329
+ const format = raw.trim().toLowerCase();
330
+ if (format.length === 0) {
331
+ continue;
332
+ }
333
+ if (format !== "srt" && format !== "vtt") {
334
+ usage(`Unsupported subtitle export "${raw}". Use srt or vtt.`);
335
+ }
336
+ if (!formats.includes(format)) {
337
+ formats.push(format);
338
+ }
339
+ }
340
+ if (formats.length === 0) {
341
+ usage("--export requires at least one of srt or vtt.");
342
+ }
343
+ return formats;
344
+ }
345
+ async function readPropsFile(flags) {
346
+ const value = flags.get("props");
347
+ if (value === undefined) {
348
+ return undefined;
349
+ }
350
+ if (typeof value !== "string") {
351
+ usage("--props requires a file path.");
352
+ }
353
+ const propsPath = path.resolve(process.cwd(), value);
354
+ let text;
355
+ try {
356
+ text = await readFile(propsPath, "utf8");
357
+ }
358
+ catch (error) {
359
+ throw new FrameyardError("PROPS_INVALID", `Could not read props file ${propsPath}.`, { cause: error });
360
+ }
361
+ try {
362
+ return JSON.parse(text);
363
+ }
364
+ catch (error) {
365
+ throw new FrameyardError("PROPS_INVALID", `Props file ${propsPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
366
+ }
367
+ }
368
+ function progressPrinter(label) {
369
+ let nextPercent = 0;
370
+ return (event) => {
371
+ const percent = event.total === 0 ? 100 : Math.floor((event.frame / event.total) * 100);
372
+ if (event.frame === event.total || percent >= nextPercent) {
373
+ process.stderr.write(`${label} ${event.frame}/${event.total} (${percent}%) ${(event.elapsedMs / 1000).toFixed(1)}s\n`);
374
+ nextPercent = percent + 10;
375
+ }
376
+ };
377
+ }
378
+ function formatDuration(seconds) {
379
+ return `${seconds.toFixed(2)}s`;
380
+ }
381
+ function printWarnings(warnings) {
382
+ for (const warning of warnings) {
383
+ process.stderr.write(`warning: ${warning}\n`);
384
+ }
385
+ }
386
+ function printJson(value) {
387
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
388
+ }
389
+ async function runInit(parsed) {
390
+ const target = path.resolve(process.cwd(), parsed.positionals[0] ?? ".");
391
+ const json = parsed.flags.has("json") === true;
392
+ const files = scaffoldFiles(path.basename(target) || "frameyard-project");
393
+ for (const relativePath of Object.keys(files)) {
394
+ try {
395
+ await access(path.join(target, relativePath));
396
+ throw new FrameyardError("OUTPUT_EXISTS", `Refusing to overwrite ${path.join(target, relativePath)}. Choose an empty directory.`);
397
+ }
398
+ catch (error) {
399
+ if (isFrameyardError(error)) {
400
+ throw error;
401
+ }
402
+ if (error.code !== "ENOENT") {
403
+ throw error;
404
+ }
405
+ }
406
+ }
407
+ await mkdir(target, { recursive: true });
408
+ for (const [relativePath, contents] of Object.entries(files)) {
409
+ const destination = path.join(target, relativePath);
410
+ await mkdir(path.dirname(destination), { recursive: true });
411
+ await writeFile(destination, contents, { flag: "wx" });
412
+ }
413
+ const result = {
414
+ ok: true,
415
+ command: "init",
416
+ project: target,
417
+ files: Object.keys(files),
418
+ next: [...SCAFFOLD_NEXT],
419
+ };
420
+ if (json) {
421
+ printJson(result);
422
+ }
423
+ else {
424
+ process.stdout.write(`Created Frameyard project at ${target}\n`);
425
+ for (const file of Object.keys(files)) {
426
+ process.stdout.write(` ${file}\n`);
427
+ }
428
+ process.stdout.write("Next: " + SCAFFOLD_NEXT.slice(0, 2).join(" && ") + "\n");
429
+ }
430
+ return 0;
431
+ }
432
+ async function runDev(parsed) {
433
+ const portValue = parsed.flags.get("port");
434
+ const port = typeof portValue === "string" ? Number(portValue) : 5173;
435
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
436
+ usage("--port must be an integer between 0 and 65535.");
437
+ }
438
+ const project = await projectFromFlag(parsed.flags);
439
+ const server = await startPreviewServer({ projectRoot: project.root, port });
440
+ process.stdout.write(`Frameyard preview: ${server.url}\n`);
441
+ process.stdout.write(`Project: ${project.root}\n`);
442
+ process.stdout.write(`Compositions: ${[...project.compositions.keys()].join(", ") || "(none)"}\n`);
443
+ process.stdout.write("Press Ctrl+C to stop.\n");
444
+ await new Promise((resolve) => {
445
+ const stop = () => {
446
+ process.off("SIGINT", stop);
447
+ process.off("SIGTERM", stop);
448
+ resolve();
449
+ };
450
+ process.on("SIGINT", stop);
451
+ process.on("SIGTERM", stop);
452
+ });
453
+ await server.close();
454
+ return 0;
455
+ }
456
+ async function runCheck(parsed, signal) {
457
+ const project = await projectFromFlag(parsed.flags);
458
+ const props = await readPropsFile(parsed.flags);
459
+ const compositionId = parsed.positionals[0];
460
+ const json = parsed.flags.has("json") === true;
461
+ const result = await checkComposition({
462
+ project,
463
+ compositionId,
464
+ props,
465
+ signal,
466
+ onProgress: progressPrinter("Checking"),
467
+ });
468
+ if (json) {
469
+ printJson({ ok: true, command: "check", ...result });
470
+ }
471
+ else {
472
+ const meta = result.metadata;
473
+ process.stdout.write(`✓ ${compositionId}: ${result.framesChecked} frames, ${meta.width}×${meta.height} @ ${meta.fps} fps (${formatDuration(meta.durationInFrames / meta.fps)})\n`);
474
+ process.stdout.write(` audio: ${result.audio.clips} clip${result.audio.clips === 1 ? "" : "s"}, ${result.audio.warnings.length} warning${result.audio.warnings.length === 1 ? "" : "s"}\n`);
475
+ for (const clip of result.audio.details) {
476
+ process.stdout.write(` ${clip.src}: ${clip.startSeconds.toFixed(2)}s–${clip.endSeconds.toFixed(2)}s (source ${clip.sourceDurationSeconds.toFixed(2)}s, volume ${clip.volume})\n`);
477
+ }
478
+ if (result.transcripts.clips > 0) {
479
+ process.stdout.write(` transcripts: ${result.transcripts.clips} asset${result.transcripts.clips === 1 ? "" : "s"}, ${result.transcripts.warnings.length} warning${result.transcripts.warnings.length === 1 ? "" : "s"}\n`);
480
+ for (const transcript of result.transcripts.details) {
481
+ process.stdout.write(` ${transcript.src}: ${transcript.segments} segment${transcript.segments === 1 ? "" : "s"}${transcript.words === undefined ? "" : `, ${transcript.words} word${transcript.words === 1 ? "" : "s"}`}${transcript.language === undefined ? "" : `, ${transcript.language}`}, ${formatDuration(transcript.durationMs / 1000)}\n`);
482
+ }
483
+ }
484
+ }
485
+ printWarnings(result.audio.warnings);
486
+ printWarnings(result.transcripts.warnings);
487
+ return 0;
488
+ }
489
+ async function runStill(parsed, signal) {
490
+ const project = await projectFromFlag(parsed.flags);
491
+ const props = await readPropsFile(parsed.flags);
492
+ const compositionId = parsed.positionals[0];
493
+ const frame = Number(parsed.flags.get("frame"));
494
+ if (!Number.isInteger(frame) || frame < 0) {
495
+ usage("--frame must be a non-negative integer.");
496
+ }
497
+ const outValue = parsed.flags.get("out");
498
+ const outPath = path.resolve(process.cwd(), typeof outValue === "string" ? outValue : `still-${compositionId}-${frame}.png`);
499
+ const json = parsed.flags.has("json") === true;
500
+ const result = await captureStill({
501
+ project,
502
+ compositionId,
503
+ props,
504
+ frame,
505
+ outPath,
506
+ overwrite: parsed.flags.has("overwrite") === true,
507
+ signal,
508
+ });
509
+ if (json) {
510
+ printJson({ ok: true, command: "still", ...result });
511
+ }
512
+ else {
513
+ process.stdout.write(`Wrote frame ${result.frame} of ${compositionId} to ${result.path} (${result.width}×${result.height})\n`);
514
+ }
515
+ return 0;
516
+ }
517
+ async function runRender(parsed, signal) {
518
+ const project = await projectFromFlag(parsed.flags);
519
+ const props = await readPropsFile(parsed.flags);
520
+ const compositionId = parsed.positionals[0];
521
+ const outValue = parsed.flags.get("out");
522
+ if (typeof outValue !== "string") {
523
+ usage("--out requires a file path.");
524
+ }
525
+ const outPath = path.resolve(process.cwd(), outValue);
526
+ const json = parsed.flags.has("json") === true;
527
+ const result = await renderComposition({
528
+ project,
529
+ compositionId,
530
+ props,
531
+ outPath,
532
+ overwrite: parsed.flags.has("overwrite") === true,
533
+ signal,
534
+ onProgress: progressPrinter("Rendering"),
535
+ });
536
+ printWarnings(result.audio.warnings);
537
+ printWarnings(result.transcripts.warnings);
538
+ if (json) {
539
+ printJson({ ok: true, command: "render", ...result });
540
+ }
541
+ else {
542
+ process.stdout.write(`Rendered ${compositionId} to ${result.path} (${result.framesRendered} frames, ${formatDuration(result.durationSeconds)}, ${result.audio.clips} audio clip${result.audio.clips === 1 ? "" : "s"})\n`);
543
+ }
544
+ return 0;
545
+ }
546
+ async function runTts(parsed, signal) {
547
+ const projectRoot = await assetRootFromFlag(parsed.flags);
548
+ const json = parsed.flags.has("json") === true;
549
+ const speaker = flagString(parsed.flags, "speaker");
550
+ const talkerModel = flagString(parsed.flags, "talker-model");
551
+ const codecModel = flagString(parsed.flags, "codec-model");
552
+ const outValue = flagString(parsed.flags, "out");
553
+ if (!speaker || !talkerModel || !codecModel || !outValue) {
554
+ usage('"tts" requires --speaker, --talker-model, --codec-model, and --out.');
555
+ }
556
+ const textFile = flagString(parsed.flags, "text-file");
557
+ let text;
558
+ if (textFile !== undefined) {
559
+ const textPath = path.resolve(process.cwd(), textFile);
560
+ try {
561
+ text = await readFile(textPath, "utf8");
562
+ }
563
+ catch (error) {
564
+ throw new FrameyardError("ASSET_NOT_FOUND", `Could not read text file ${textPath}.`, { cause: error });
565
+ }
566
+ }
567
+ else {
568
+ if (process.stdin.isTTY) {
569
+ usage("Provide --text-file or pipe text to stdin.");
570
+ }
571
+ text = await readStdin();
572
+ }
573
+ const result = await synthesizeSpeech({
574
+ projectRoot,
575
+ text,
576
+ speaker,
577
+ talkerModelPath: path.resolve(process.cwd(), talkerModel),
578
+ codecModelPath: path.resolve(process.cwd(), codecModel),
579
+ outPath: path.resolve(process.cwd(), outValue),
580
+ ...(flagString(parsed.flags, "style") !== undefined ? { style: flagString(parsed.flags, "style") } : {}),
581
+ ...(flagString(parsed.flags, "language") !== undefined
582
+ ? { language: flagString(parsed.flags, "language") }
583
+ : {}),
584
+ ...(flagString(parsed.flags, "device") !== undefined
585
+ ? { device: flagString(parsed.flags, "device") }
586
+ : {}),
587
+ ...(flagString(parsed.flags, "executable") !== undefined
588
+ ? { executable: flagString(parsed.flags, "executable") }
589
+ : {}),
590
+ ...(flagString(parsed.flags, "manifest") !== undefined
591
+ ? { manifestPath: flagString(parsed.flags, "manifest") }
592
+ : {}),
593
+ force: parsed.flags.has("force") === true,
594
+ signal,
595
+ });
596
+ if (json) {
597
+ printJson({ ok: true, command: "tts", ...result });
598
+ }
599
+ else {
600
+ process.stdout.write(`Wrote 24 kHz mono WAV to ${result.path} (${(result.bytes / (1024 * 1024)).toFixed(2)} MB)\n`);
601
+ process.stdout.write(`Updated provenance manifest ${result.manifest}\n`);
602
+ }
603
+ return 0;
604
+ }
605
+ async function runTranscribe(parsed, signal) {
606
+ const projectRoot = await assetRootFromFlag(parsed.flags);
607
+ const json = parsed.flags.has("json") === true;
608
+ const outValue = flagString(parsed.flags, "out");
609
+ const modelValue = flagString(parsed.flags, "model");
610
+ if (!outValue || !modelValue) {
611
+ usage('"transcribe" requires --out and --model.');
612
+ }
613
+ const result = await transcribeAudio({
614
+ projectRoot,
615
+ sourcePath: path.resolve(process.cwd(), parsed.positionals[0]),
616
+ outPath: path.resolve(process.cwd(), outValue),
617
+ modelPath: path.resolve(process.cwd(), modelValue),
618
+ words: parsed.flags.has("words") === true,
619
+ exports: parseSubtitleFormats(parsed.flags.get("export")),
620
+ ...(flagString(parsed.flags, "language") !== undefined
621
+ ? { language: flagString(parsed.flags, "language") }
622
+ : {}),
623
+ ...(flagString(parsed.flags, "executable") !== undefined
624
+ ? { executable: flagString(parsed.flags, "executable") }
625
+ : {}),
626
+ ...(flagString(parsed.flags, "manifest") !== undefined
627
+ ? { manifestPath: flagString(parsed.flags, "manifest") }
628
+ : {}),
629
+ force: parsed.flags.has("force") === true,
630
+ signal,
631
+ });
632
+ if (json) {
633
+ printJson({ ok: true, command: "transcribe", ...result });
634
+ }
635
+ else {
636
+ process.stdout.write(`Transcribed ${result.source.path} -> ${result.path} (${result.segments} segment${result.segments === 1 ? "" : "s"}, ${result.words} word${result.words === 1 ? "" : "s"}${result.language === undefined ? "" : `, ${result.language}`})\n`);
637
+ for (const exported of result.exports) {
638
+ process.stdout.write(` ${exported.format}: ${exported.path}\n`);
639
+ }
640
+ process.stdout.write(`Updated provenance manifest ${result.manifest}\n`);
641
+ }
642
+ return 0;
643
+ }
644
+ async function runDoctorCommand(parsed, signal) {
645
+ const projectRoot = await assetRootFromFlag(parsed.flags);
646
+ const manifestPath = resolveManifestPath(projectRoot, flagString(parsed.flags, "manifest"));
647
+ const json = parsed.flags.has("json") === true;
648
+ const result = await runDoctor({
649
+ projectRoot,
650
+ manifestPath,
651
+ ...(flagString(parsed.flags, "qwen-tts") !== undefined
652
+ ? { qwenTts: flagString(parsed.flags, "qwen-tts") }
653
+ : {}),
654
+ ...(flagString(parsed.flags, "whisper-cli") !== undefined
655
+ ? { whisperCli: flagString(parsed.flags, "whisper-cli") }
656
+ : {}),
657
+ ...(flagString(parsed.flags, "talker-model") !== undefined
658
+ ? { talkerModel: flagString(parsed.flags, "talker-model") }
659
+ : {}),
660
+ ...(flagString(parsed.flags, "codec-model") !== undefined
661
+ ? { codecModel: flagString(parsed.flags, "codec-model") }
662
+ : {}),
663
+ ...(flagString(parsed.flags, "model") !== undefined
664
+ ? { whisperModel: flagString(parsed.flags, "model") }
665
+ : {}),
666
+ signal,
667
+ });
668
+ if (json) {
669
+ printJson({ ok: true, command: "doctor", ...result });
670
+ }
671
+ else {
672
+ for (const check of result.checks) {
673
+ const mark = check.status === "ok" ? "✓" : "✗";
674
+ const suffix = check.required ? "" : " (optional)";
675
+ process.stdout.write(`${mark} ${check.name}${suffix}: ${check.detail}\n`);
676
+ }
677
+ for (const integration of [result.integrations.tts, result.integrations.transcribe]) {
678
+ const mark = integration.status === "ok" ? "✓" : integration.status === "missing" && !integration.requested ? "○" : "✗";
679
+ const detail = integration.detail ? ` — ${integration.detail}` : "";
680
+ const version = integration.version ? ` ${integration.version}` : "";
681
+ process.stdout.write(`${mark} ${integration.name}: ${integration.status}${integration.executable ? ` (${integration.executable})` : ""}${version}${detail}\n`);
682
+ }
683
+ for (const model of result.models) {
684
+ const mark = model.status === "ok" ? "✓" : "✗";
685
+ const detail = model.detail ? ` — ${model.detail}` : "";
686
+ process.stdout.write(`${mark} model ${model.role}: ${model.path}${detail}\n`);
687
+ }
688
+ process.stdout.write(`\ndoctor: ${result.healthy ? "healthy" : "problems found"}\n`);
689
+ }
690
+ return result.healthy ? 0 : 1;
691
+ }
692
+ function reportFailure(error, command, jsonRequested, signal) {
693
+ const aborted = signal?.aborted === true;
694
+ const code = aborted ? "CANCELLED" : isFrameyardError(error) ? error.code : "INTERNAL";
695
+ const message = aborted ? "Operation cancelled." : error instanceof Error ? error.message : String(error);
696
+ if (jsonRequested) {
697
+ printJson({ ok: false, command: command || null, error: { code, message } });
698
+ }
699
+ process.stderr.write(`${aborted ? "Cancelled" : `error [${code}]`}${aborted ? "" : `: ${message}`}\n`);
700
+ if (!aborted && !isFrameyardError(error) && process.env.FRAMEYARD_DEBUG === "1" && error instanceof Error) {
701
+ process.stderr.write(`${error.stack ?? ""}\n`);
702
+ }
703
+ if (aborted) {
704
+ return 130;
705
+ }
706
+ return code === "USAGE" ? 2 : 1;
707
+ }
708
+ async function main(argv) {
709
+ const jsonRequested = argv.includes("--json");
710
+ let parsed;
711
+ try {
712
+ parsed = parseArgs(argv);
713
+ }
714
+ catch (error) {
715
+ return reportFailure(error, inferCommand(argv), jsonRequested);
716
+ }
717
+ try {
718
+ validateArgs(parsed);
719
+ }
720
+ catch (error) {
721
+ return reportFailure(error, parsed.command, jsonRequested);
722
+ }
723
+ if (parsed.flags.has("version")) {
724
+ process.stdout.write(`${VERSION}\n`);
725
+ return 0;
726
+ }
727
+ if (parsed.command === "help") {
728
+ printHelp();
729
+ return 0;
730
+ }
731
+ if (parsed.flags.has("help")) {
732
+ printCommandHelp(parsed.command);
733
+ return 0;
734
+ }
735
+ if (!parsed.command) {
736
+ printHelp();
737
+ return 0;
738
+ }
739
+ if (parsed.command === "init") {
740
+ try {
741
+ return await runInit(parsed);
742
+ }
743
+ catch (error) {
744
+ return reportFailure(error, "init", parsed.flags.has("json") === true);
745
+ }
746
+ }
747
+ if (parsed.command === "dev") {
748
+ try {
749
+ return await runDev(parsed);
750
+ }
751
+ catch (error) {
752
+ return reportFailure(error, "dev", false);
753
+ }
754
+ }
755
+ const controller = new AbortController();
756
+ let interrupted = false;
757
+ const interrupt = () => {
758
+ if (interrupted) {
759
+ process.exit(130);
760
+ }
761
+ interrupted = true;
762
+ process.stderr.write("\nCancelling...\n");
763
+ controller.abort();
764
+ };
765
+ process.on("SIGINT", interrupt);
766
+ process.on("SIGTERM", interrupt);
767
+ try {
768
+ switch (parsed.command) {
769
+ case "check":
770
+ return await runCheck(parsed, controller.signal);
771
+ case "still":
772
+ return await runStill(parsed, controller.signal);
773
+ case "render":
774
+ return await runRender(parsed, controller.signal);
775
+ case "tts":
776
+ return await runTts(parsed, controller.signal);
777
+ case "transcribe":
778
+ return await runTranscribe(parsed, controller.signal);
779
+ case "doctor":
780
+ return await runDoctorCommand(parsed, controller.signal);
781
+ default:
782
+ usage(`Unknown command "${parsed.command}".`);
783
+ }
784
+ }
785
+ catch (error) {
786
+ return reportFailure(error, parsed.command, parsed.flags.has("json") === true, controller.signal);
787
+ }
788
+ finally {
789
+ process.off("SIGINT", interrupt);
790
+ process.off("SIGTERM", interrupt);
791
+ }
792
+ }
793
+ const exitCode = await main(process.argv.slice(2));
794
+ process.exitCode = exitCode;
795
+ //# sourceMappingURL=cli.js.map