frameyard 0.7.0 → 0.9.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.
- package/CHANGELOG.md +164 -7
- package/GLOSSARY.md +152 -18
- package/README.md +328 -285
- package/dist/assets/integrations.d.ts +8 -4
- package/dist/assets/integrations.d.ts.map +1 -1
- package/dist/assets/integrations.js +8 -4
- package/dist/assets/integrations.js.map +1 -1
- package/dist/assets/provenance.d.ts +2 -2
- package/dist/assets/provenance.d.ts.map +1 -1
- package/dist/assets/provenance.js +5 -3
- package/dist/assets/provenance.js.map +1 -1
- package/dist/assets/tts-supertonic.d.ts +28 -0
- package/dist/assets/tts-supertonic.d.ts.map +1 -0
- package/dist/assets/tts-supertonic.js +156 -0
- package/dist/assets/tts-supertonic.js.map +1 -0
- package/dist/assets/tts.d.ts +18 -6
- package/dist/assets/tts.d.ts.map +1 -1
- package/dist/assets/tts.js +219 -97
- package/dist/assets/tts.js.map +1 -1
- package/dist/cli.js +1705 -106
- package/dist/cli.js.map +1 -1
- package/dist/composition/descriptor.d.ts +17 -3
- package/dist/composition/descriptor.d.ts.map +1 -1
- package/dist/composition/descriptor.js +51 -28
- package/dist/composition/descriptor.js.map +1 -1
- package/dist/composition/format.d.ts +12 -0
- package/dist/composition/format.d.ts.map +1 -0
- package/dist/composition/format.js +28 -0
- package/dist/composition/format.js.map +1 -0
- package/dist/composition/index.d.ts +2 -1
- package/dist/composition/index.d.ts.map +1 -1
- package/dist/composition/index.js +1 -0
- package/dist/composition/index.js.map +1 -1
- package/dist/context.d.ts +82 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +277 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +80 -6
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +14 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +166 -7
- package/dist/doctor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fonts/add.d.ts +41 -0
- package/dist/fonts/add.d.ts.map +1 -0
- package/dist/fonts/add.js +259 -0
- package/dist/fonts/add.js.map +1 -0
- package/dist/fonts/binary.d.ts +4 -0
- package/dist/fonts/binary.d.ts.map +1 -0
- package/dist/fonts/binary.js +21 -0
- package/dist/fonts/binary.js.map +1 -0
- package/dist/fonts/css.d.ts +4 -0
- package/dist/fonts/css.d.ts.map +1 -0
- package/dist/fonts/css.js +50 -0
- package/dist/fonts/css.js.map +1 -0
- package/dist/fonts/fontsource.d.ts +107 -0
- package/dist/fonts/fontsource.d.ts.map +1 -0
- package/dist/fonts/fontsource.js +565 -0
- package/dist/fonts/fontsource.js.map +1 -0
- package/dist/fonts/index.d.ts +11 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +11 -0
- package/dist/fonts/index.js.map +1 -0
- package/dist/fonts/install.d.ts +24 -0
- package/dist/fonts/install.d.ts.map +1 -0
- package/dist/fonts/install.js +145 -0
- package/dist/fonts/install.js.map +1 -0
- package/dist/fonts/manifest.d.ts +21 -0
- package/dist/fonts/manifest.d.ts.map +1 -0
- package/dist/fonts/manifest.js +96 -0
- package/dist/fonts/manifest.js.map +1 -0
- package/dist/fonts/options.d.ts +9 -0
- package/dist/fonts/options.d.ts.map +1 -0
- package/dist/fonts/options.js +66 -0
- package/dist/fonts/options.js.map +1 -0
- package/dist/fonts/schema.d.ts +4 -0
- package/dist/fonts/schema.d.ts.map +1 -0
- package/dist/fonts/schema.js +316 -0
- package/dist/fonts/schema.js.map +1 -0
- package/dist/fonts/summary.d.ts +16 -0
- package/dist/fonts/summary.d.ts.map +1 -0
- package/dist/fonts/summary.js +22 -0
- package/dist/fonts/summary.js.map +1 -0
- package/dist/fonts/types.d.ts +56 -0
- package/dist/fonts/types.d.ts.map +1 -0
- package/dist/fonts/types.js +8 -0
- package/dist/fonts/types.js.map +1 -0
- package/dist/fonts/verify.d.ts +33 -0
- package/dist/fonts/verify.d.ts.map +1 -0
- package/dist/fonts/verify.js +200 -0
- package/dist/fonts/verify.js.map +1 -0
- package/dist/html/document-adapter.d.ts +3 -3
- package/dist/html/document-adapter.d.ts.map +1 -1
- package/dist/html/document-adapter.js +1 -1
- package/dist/html/document-adapter.js.map +1 -1
- package/dist/html/index.d.ts +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/parse.d.ts +10 -2
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js +14 -5
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts +12 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +292 -61
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +325 -0
- package/dist/inspect.js.map +1 -0
- package/dist/lint.d.ts +63 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +265 -0
- package/dist/lint.js.map +1 -0
- package/dist/policy.d.ts +69 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +361 -0
- package/dist/policy.js.map +1 -0
- package/dist/preview/main.d.ts +2 -2
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +35 -14
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +17 -65
- package/dist/preview/timeline-data.d.ts.map +1 -1
- package/dist/preview/timeline-host.d.ts.map +1 -1
- package/dist/preview/timeline-host.js +73 -38
- package/dist/preview/timeline-host.js.map +1 -1
- package/dist/preview/timeline-main.d.ts.map +1 -1
- package/dist/preview/timeline-main.js +47 -3
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +13 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -17
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +2 -2
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +10 -4
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/ffmpeg.d.ts +44 -1
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +371 -73
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.d.ts +40 -4
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +100 -25
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyframe-expressions.d.ts +39 -0
- package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
- package/dist/renderer/keyframe-expressions.js +180 -0
- package/dist/renderer/keyframe-expressions.js.map +1 -0
- package/dist/renderer/timeline.d.ts +2 -0
- package/dist/renderer/timeline.d.ts.map +1 -1
- package/dist/renderer/timeline.js +168 -106
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +159 -31
- package/dist/runtime/host.js.map +1 -1
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +48 -317
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +15 -7
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +72 -16
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +56 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts.map +1 -1
- package/dist/server/timeline-preview-data.js +12 -23
- package/dist/server/timeline-preview-data.js.map +1 -1
- package/dist/server/timeline-preview-html.d.ts.map +1 -1
- package/dist/server/timeline-preview-html.js +20 -3
- package/dist/server/timeline-preview-html.js.map +1 -1
- package/dist/templates.d.ts +16 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +186 -0
- package/dist/templates.js.map +1 -0
- package/dist/timeline/easing.d.ts +36 -0
- package/dist/timeline/easing.d.ts.map +1 -0
- package/dist/timeline/easing.js +225 -0
- package/dist/timeline/easing.js.map +1 -0
- package/dist/timeline/keyframes.d.ts +23 -0
- package/dist/timeline/keyframes.d.ts.map +1 -0
- package/dist/timeline/keyframes.js +95 -0
- package/dist/timeline/keyframes.js.map +1 -0
- package/dist/timeline/plan.d.ts +6 -12
- package/dist/timeline/plan.d.ts.map +1 -1
- package/dist/timeline/plan.js +17 -30
- package/dist/timeline/plan.js.map +1 -1
- package/dist/timeline/resolve.d.ts +16 -3
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +119 -13
- package/dist/timeline/resolve.js.map +1 -1
- package/dist/timeline/scene.d.ts +126 -0
- package/dist/timeline/scene.d.ts.map +1 -0
- package/dist/timeline/scene.js +109 -0
- package/dist/timeline/scene.js.map +1 -0
- package/dist/timeline/schema.d.ts +38 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +290 -9
- package/dist/timeline/schema.js.map +1 -1
- package/dist/timeline/transitions.d.ts +21 -0
- package/dist/timeline/transitions.d.ts.map +1 -0
- package/dist/timeline/transitions.js +23 -0
- package/dist/timeline/transitions.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
- package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
- package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
- package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
- package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
- package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
- package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
- package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
- package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
- package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
- package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
- package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
- package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
- package/docs/migration-0.4.md +5 -3
- package/docs/migration-0.8.md +163 -0
- package/docs/preview-testing.md +23 -3
- package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
- package/examples/blend/README.md +106 -0
- package/examples/blend/assets/ink.mp4 +0 -0
- package/examples/blend/compositions/aurora.html +55 -0
- package/examples/blend/compositions/backdrop.html +74 -0
- package/examples/blend/compositions/labels.html +149 -0
- package/examples/blend/fy.config.js +11 -0
- package/examples/blend/scripts/make-ink.sh +26 -0
- package/examples/blend/timelines/blend.timeline.json +155 -0
- package/examples/demo/README.md +2 -2
- package/examples/demo/compositions/demo.html +243 -251
- package/examples/intro/README.md +71 -0
- package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
- package/examples/intro/assets/grain.png +0 -0
- package/examples/intro/assets/score.ogg +0 -0
- package/examples/intro/compositions/brand.html +412 -0
- package/examples/intro/compositions/chrome.controller.js +63 -0
- package/examples/intro/compositions/chrome.html +256 -0
- package/examples/intro/compositions/lattice.controller.js +148 -0
- package/examples/intro/compositions/lattice.html +308 -0
- package/examples/intro/compositions/opening.html +504 -0
- package/examples/intro/compositions/studio.html +930 -0
- package/examples/intro/compositions/theme.css +218 -0
- package/examples/intro/fy.config.js +13 -0
- package/examples/intro/fy.fonts.json +100 -0
- package/examples/intro/scripts/make-grain.mjs +79 -0
- package/examples/intro/scripts/make-score.mjs +702 -0
- package/examples/intro/timelines/intro.timeline.json +60 -0
- package/examples/keyframes/README.md +76 -0
- package/examples/keyframes/compositions/backdrop.html +60 -0
- package/examples/keyframes/compositions/card.html +99 -0
- package/examples/keyframes/fy.config.js +10 -0
- package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
- package/examples/nested/README.md +112 -0
- package/examples/nested/assets/pulse.wav +0 -0
- package/examples/nested/compositions/backdrop.html +84 -0
- package/examples/nested/compositions/card.html +275 -0
- package/examples/nested/compositions/overlay.html +307 -0
- package/examples/nested/fy.config.js +13 -0
- package/examples/nested/scripts/make-pulse.sh +20 -0
- package/examples/nested/timelines/chapter-a.timeline.json +39 -0
- package/examples/nested/timelines/chapter-b.timeline.json +39 -0
- package/examples/nested/timelines/nested-reel.timeline.json +52 -0
- package/examples/static/README.md +17 -0
- package/examples/static/compositions/still.html +37 -0
- package/examples/static/fy.config.js +9 -0
- package/examples/static/timelines/still.timeline.json +13 -0
- package/examples/transitions/README.md +128 -0
- package/examples/transitions/assets/alpha.wav +0 -0
- package/examples/transitions/assets/beta.wav +0 -0
- package/examples/transitions/assets/gamma.wav +0 -0
- package/examples/transitions/compositions/alpha.html +56 -0
- package/examples/transitions/compositions/beta.html +56 -0
- package/examples/transitions/compositions/gamma.html +56 -0
- package/examples/transitions/fy.config.js +12 -0
- package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
- package/examples/transitions/timelines/transitions.timeline.json +31 -0
- package/examples/video/README.md +4 -4
- package/examples/video/compositions/video-mp4.html +349 -357
- package/examples/video/compositions/video-overlay.html +337 -345
- package/examples/video/compositions/video.html +349 -357
- package/examples/webgl/README.md +1 -1
- package/examples/webgl/compositions/webgl.html +205 -213
- package/package.json +13 -5
- package/scaffolds/init/AGENTS.md +131 -0
- package/scaffolds/init/README.md +56 -0
- package/scaffolds/init/compositions/hello.html +79 -0
- package/scaffolds/init/fy.config.js +9 -0
- package/scaffolds/init/gitignore.template +1 -0
- package/scaffolds/init/package.json +11 -0
- package/scaffolds/init/props/hello.json +4 -0
- package/scaffolds/init/props/main.json +6 -0
- package/scaffolds/init/timelines/main.timeline.json +20 -0
- package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
- package/scaffolds/local-models/fy.policy.json +5 -0
- package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
- package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
- package/scaffolds/local-models/pi/sandbox.json +15 -0
- package/scaffolds/local-models/pi/settings.json +15 -0
- package/skills/frameyard/README.md +1 -0
- package/skills/frameyard/SKILL.md +59 -59
- package/skills/frameyard/references/audio-assets.md +32 -3
- package/skills/frameyard/references/common-mistakes.md +81 -7
- package/skills/frameyard/references/controllers.md +5 -4
- package/skills/frameyard/references/deterministic-authoring.md +49 -37
- package/skills/frameyard/references/diagnostics.md +42 -4
- package/skills/frameyard/references/failure-recovery.md +56 -10
- package/skills/frameyard/references/fonts.md +132 -0
- package/skills/frameyard/references/json-results.md +165 -10
- package/skills/frameyard/references/quick-reference.md +222 -0
- package/skills/frameyard/references/timelines.md +109 -15
- /package/examples/demo/{video.config.js → fy.config.js} +0 -0
- /package/examples/video/{video.config.js → fy.config.js} +0 -0
- /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { access, mkdir, readFile, rmdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { cpus, freemem, loadavg, totalmem } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
4
6
|
import { isPlainObject } from "./contracts.js";
|
|
5
7
|
import { FrameyardError, errorMessage, isFrameyardError } from "./errors.js";
|
|
6
8
|
import { loadProject, resolveProjectRoot } from "./project.js";
|
|
9
|
+
import { assertOperationAllowed, assertOverwriteAllowed, assertPolicyTier, assertProfileLimit, assertStillFrameLimit, describePolicy, loadResourcePolicy, lowerProcessPriority, MAX_STILL_FRAMES, policyDetails, resolveProfile, SHARED_CPU_MAX_CONCURRENCY, SHARED_CPU_MAX_WORKERS, shouldLowerPriority, } from "./policy.js";
|
|
10
|
+
import { lintComposition, lintResolvedTimeline, lintTimeline } from "./lint.js";
|
|
11
|
+
import { buildProjectContext } from "./context.js";
|
|
12
|
+
import { analyzeRgba, compareRgba, decodeRgba, probeImage } from "./inspect.js";
|
|
13
|
+
import { findTemplate, listTemplates, templateRegistration } from "./templates.js";
|
|
14
|
+
import { planTimelineRender } from "./timeline/plan.js";
|
|
15
|
+
import { resolveProjectTimeline } from "./timeline/resolve.js";
|
|
7
16
|
import { checkComposition, captureStills, MAX_RENDER_CONCURRENCY, renderComposition, validateRenderConcurrency, } from "./renderer/index.js";
|
|
8
17
|
import { MAX_TIMELINE_WORKERS, captureTimelineStills, checkTimeline, renderTimeline, } from "./renderer/timeline.js";
|
|
9
18
|
import { assertContainerCodec, CONTAINER_EXTENSION, defaultCodecForContainer, encoderNameFor, } from "./renderer/ffmpeg.js";
|
|
10
19
|
import { startPreviewServer } from "./server/server.js";
|
|
11
|
-
import { scaffoldFiles, SCAFFOLD_NEXT } from "./scaffold.js";
|
|
20
|
+
import { localModelScaffoldFiles, scaffoldFiles, SCAFFOLD_NEXT } from "./scaffold.js";
|
|
12
21
|
import { VERSION } from "./version.js";
|
|
13
22
|
import { transcribeAudio } from "./assets/transcribe.js";
|
|
14
23
|
import { synthesizeSpeech } from "./assets/tts.js";
|
|
24
|
+
import { isSupertonicLanguage, parseSupertonicSpeaker, resolveSupertonicModelFiles, SUPERTONIC_LANGUAGES, } from "./assets/tts-supertonic.js";
|
|
15
25
|
import { resolveManifestPath } from "./assets/provenance.js";
|
|
16
26
|
import { runDoctor } from "./doctor.js";
|
|
17
|
-
|
|
27
|
+
import { addLocalFont, removeFont } from "./fonts/add.js";
|
|
28
|
+
import { addFontsource, addUrlFont, searchFontsource } from "./fonts/fontsource.js";
|
|
29
|
+
import { fontManifestPath, readFontManifest } from "./fonts/manifest.js";
|
|
30
|
+
import { parseFontStyle, parseFontWeight } from "./fonts/options.js";
|
|
31
|
+
import { summarizeFont } from "./fonts/summary.js";
|
|
32
|
+
import { assertFontIntegrity, fontStatuses, verifyFontManifest } from "./fonts/verify.js";
|
|
33
|
+
import { emptyFontManifest } from "./fonts/types.js";
|
|
34
|
+
const COMMANDS = new Set([
|
|
35
|
+
"init",
|
|
36
|
+
"dev",
|
|
37
|
+
"context",
|
|
38
|
+
"lint",
|
|
39
|
+
"check",
|
|
40
|
+
"still",
|
|
41
|
+
"inspect",
|
|
42
|
+
"compare",
|
|
43
|
+
"render",
|
|
44
|
+
"tts",
|
|
45
|
+
"transcribe",
|
|
46
|
+
"add",
|
|
47
|
+
"font",
|
|
48
|
+
"doctor",
|
|
49
|
+
"help",
|
|
50
|
+
]);
|
|
18
51
|
const VALUE_FLAGS = new Set([
|
|
19
52
|
"project",
|
|
20
53
|
"props",
|
|
@@ -29,6 +62,8 @@ const VALUE_FLAGS = new Set([
|
|
|
29
62
|
"container",
|
|
30
63
|
"text-file",
|
|
31
64
|
"speaker",
|
|
65
|
+
"engine",
|
|
66
|
+
"model-dir",
|
|
32
67
|
"talker-model",
|
|
33
68
|
"codec-model",
|
|
34
69
|
"style",
|
|
@@ -39,10 +74,38 @@ const VALUE_FLAGS = new Set([
|
|
|
39
74
|
"export",
|
|
40
75
|
"manifest",
|
|
41
76
|
"qwen-tts",
|
|
77
|
+
"sherpa-onnx-tts",
|
|
42
78
|
"whisper-cli",
|
|
79
|
+
"profile",
|
|
80
|
+
"template",
|
|
81
|
+
"file",
|
|
82
|
+
"family",
|
|
83
|
+
"id",
|
|
84
|
+
"weight",
|
|
85
|
+
"subset",
|
|
86
|
+
"unicode-range",
|
|
87
|
+
"license",
|
|
88
|
+
"license-file",
|
|
89
|
+
"category",
|
|
90
|
+
"limit",
|
|
91
|
+
"url",
|
|
92
|
+
"sha256",
|
|
93
|
+
]);
|
|
94
|
+
const BOOLEAN_FLAGS = new Set([
|
|
95
|
+
"json",
|
|
96
|
+
"overwrite",
|
|
97
|
+
"force",
|
|
98
|
+
"words",
|
|
99
|
+
"dry-run",
|
|
100
|
+
"low-priority",
|
|
101
|
+
"list",
|
|
102
|
+
"local-models",
|
|
103
|
+
"keep-files",
|
|
104
|
+
"static",
|
|
105
|
+
"variable",
|
|
106
|
+
"help",
|
|
107
|
+
"version",
|
|
43
108
|
]);
|
|
44
|
-
const BOOLEAN_FLAGS = new Set(["json", "overwrite", "force", "words", "help", "version"]);
|
|
45
|
-
const MAX_STILL_FRAMES = 32;
|
|
46
109
|
function usage(message = "Invalid command line.") {
|
|
47
110
|
throw new FrameyardError("USAGE", message);
|
|
48
111
|
}
|
|
@@ -108,15 +171,47 @@ function parseArgs(argv) {
|
|
|
108
171
|
}
|
|
109
172
|
function allowedFlags(command) {
|
|
110
173
|
const common = new Set(["project", "json", "help"]);
|
|
174
|
+
const resource = ["profile", "low-priority"];
|
|
111
175
|
switch (command) {
|
|
112
176
|
case "init":
|
|
113
|
-
return new Set(["json", "help"]);
|
|
177
|
+
return new Set(["json", "local-models", "help"]);
|
|
114
178
|
case "dev":
|
|
115
179
|
return new Set(["project", "port", "help"]);
|
|
116
|
-
case "
|
|
180
|
+
case "context":
|
|
181
|
+
return new Set(common);
|
|
182
|
+
case "lint":
|
|
117
183
|
return new Set([...common, "props"]);
|
|
184
|
+
case "inspect":
|
|
185
|
+
return new Set(["json", "help"]);
|
|
186
|
+
case "compare":
|
|
187
|
+
return new Set(["json", "help"]);
|
|
188
|
+
case "add":
|
|
189
|
+
return new Set([...common, "template", "list"]);
|
|
190
|
+
case "font":
|
|
191
|
+
return new Set([
|
|
192
|
+
...common,
|
|
193
|
+
"file",
|
|
194
|
+
"family",
|
|
195
|
+
"id",
|
|
196
|
+
"weight",
|
|
197
|
+
"style",
|
|
198
|
+
"subset",
|
|
199
|
+
"unicode-range",
|
|
200
|
+
"license",
|
|
201
|
+
"license-file",
|
|
202
|
+
"category",
|
|
203
|
+
"limit",
|
|
204
|
+
"url",
|
|
205
|
+
"sha256",
|
|
206
|
+
"static",
|
|
207
|
+
"variable",
|
|
208
|
+
"force",
|
|
209
|
+
"keep-files",
|
|
210
|
+
]);
|
|
211
|
+
case "check":
|
|
212
|
+
return new Set([...common, "props", ...resource]);
|
|
118
213
|
case "still":
|
|
119
|
-
return new Set([...common, "props", "out", "frame", "overwrite"]);
|
|
214
|
+
return new Set([...common, "props", "out", "frame", "overwrite", ...resource]);
|
|
120
215
|
case "render":
|
|
121
216
|
return new Set([
|
|
122
217
|
...common,
|
|
@@ -129,12 +224,16 @@ function allowedFlags(command) {
|
|
|
129
224
|
"encoder",
|
|
130
225
|
"codec",
|
|
131
226
|
"container",
|
|
227
|
+
"dry-run",
|
|
228
|
+
...resource,
|
|
132
229
|
]);
|
|
133
230
|
case "tts":
|
|
134
231
|
return new Set([
|
|
135
232
|
...common,
|
|
136
233
|
"text-file",
|
|
137
234
|
"speaker",
|
|
235
|
+
"engine",
|
|
236
|
+
"model-dir",
|
|
138
237
|
"talker-model",
|
|
139
238
|
"codec-model",
|
|
140
239
|
"out",
|
|
@@ -144,11 +243,35 @@ function allowedFlags(command) {
|
|
|
144
243
|
"executable",
|
|
145
244
|
"manifest",
|
|
146
245
|
"force",
|
|
246
|
+
"dry-run",
|
|
247
|
+
...resource,
|
|
147
248
|
]);
|
|
148
249
|
case "transcribe":
|
|
149
|
-
return new Set([
|
|
250
|
+
return new Set([
|
|
251
|
+
...common,
|
|
252
|
+
"out",
|
|
253
|
+
"model",
|
|
254
|
+
"words",
|
|
255
|
+
"export",
|
|
256
|
+
"language",
|
|
257
|
+
"executable",
|
|
258
|
+
"manifest",
|
|
259
|
+
"force",
|
|
260
|
+
"dry-run",
|
|
261
|
+
...resource,
|
|
262
|
+
]);
|
|
150
263
|
case "doctor":
|
|
151
|
-
return new Set([
|
|
264
|
+
return new Set([
|
|
265
|
+
...common,
|
|
266
|
+
"manifest",
|
|
267
|
+
"qwen-tts",
|
|
268
|
+
"sherpa-onnx-tts",
|
|
269
|
+
"whisper-cli",
|
|
270
|
+
"talker-model",
|
|
271
|
+
"codec-model",
|
|
272
|
+
"model-dir",
|
|
273
|
+
"model",
|
|
274
|
+
]);
|
|
152
275
|
case "help":
|
|
153
276
|
return new Set(["help"]);
|
|
154
277
|
default:
|
|
@@ -170,7 +293,7 @@ function validateArgs(parsed) {
|
|
|
170
293
|
usage(`Option --${flag} is not valid for "${parsed.command}".`);
|
|
171
294
|
}
|
|
172
295
|
}
|
|
173
|
-
if (["dev", "help", "doctor", "tts"].includes(parsed.command)) {
|
|
296
|
+
if (["dev", "help", "doctor", "tts", "context"].includes(parsed.command)) {
|
|
174
297
|
if (parsed.positionals.length > 0) {
|
|
175
298
|
usage(`"${parsed.command}" does not take a positional argument.`);
|
|
176
299
|
}
|
|
@@ -180,13 +303,218 @@ function validateArgs(parsed) {
|
|
|
180
303
|
usage(`"${parsed.command}" accepts at most 1 positional argument.`);
|
|
181
304
|
}
|
|
182
305
|
}
|
|
306
|
+
else if (parsed.command === "inspect") {
|
|
307
|
+
if (parsed.positionals.length !== 1) {
|
|
308
|
+
usage('"inspect" requires an image path.');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (parsed.command === "compare") {
|
|
312
|
+
if (parsed.positionals.length !== 2) {
|
|
313
|
+
usage('"compare" requires two image paths.');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (parsed.command === "add") {
|
|
317
|
+
if (parsed.flags.has("list")) {
|
|
318
|
+
if (parsed.positionals.length > 0) {
|
|
319
|
+
usage('"add --list" does not take positional arguments.');
|
|
320
|
+
}
|
|
321
|
+
if (parsed.flags.has("template")) {
|
|
322
|
+
usage("--template is only valid when adding an entry.");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if (parsed.positionals[0] === "local-models") {
|
|
326
|
+
if (parsed.positionals.length !== 1) {
|
|
327
|
+
usage('"add local-models" does not take an id.');
|
|
328
|
+
}
|
|
329
|
+
if (parsed.flags.has("template")) {
|
|
330
|
+
usage('--template is not valid for "add local-models".');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
if (parsed.positionals.length !== 2) {
|
|
335
|
+
usage('"add" requires <composition|timeline> <id> and --template <name>, or "add local-models".');
|
|
336
|
+
}
|
|
337
|
+
const kind = parsed.positionals[0];
|
|
338
|
+
if (kind !== "composition" && kind !== "timeline") {
|
|
339
|
+
usage('"add" kind must be "composition", "timeline", or "local-models".');
|
|
340
|
+
}
|
|
341
|
+
const templateName = parsed.flags.get("template");
|
|
342
|
+
if (typeof templateName !== "string") {
|
|
343
|
+
usage('"add" requires --template <name>.');
|
|
344
|
+
}
|
|
345
|
+
const template = findTemplate(templateName);
|
|
346
|
+
if (!template) {
|
|
347
|
+
usage(`Unknown template "${templateName}". Available: ${listTemplates()
|
|
348
|
+
.map((entry) => entry.name)
|
|
349
|
+
.join(", ")}.`);
|
|
350
|
+
}
|
|
351
|
+
if (template.kind !== kind) {
|
|
352
|
+
usage(`Template "${templateName}" is a ${template.kind} template, not ${kind}.`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (parsed.command === "font") {
|
|
357
|
+
const action = parsed.positionals[0];
|
|
358
|
+
if (action === "search") {
|
|
359
|
+
if (parsed.positionals.length > 2) {
|
|
360
|
+
usage('"font search" accepts at most one query.');
|
|
361
|
+
}
|
|
362
|
+
const query = parsed.positionals[1] ?? "";
|
|
363
|
+
const hasFilter = parsed.flags.has("category") ||
|
|
364
|
+
parsed.flags.has("subset") ||
|
|
365
|
+
parsed.flags.has("style") ||
|
|
366
|
+
parsed.flags.has("variable");
|
|
367
|
+
if (query.length === 0 && !hasFilter) {
|
|
368
|
+
usage('"font search" requires a query or a --category, --subset, --style, or --variable filter.');
|
|
369
|
+
}
|
|
370
|
+
for (const flag of [
|
|
371
|
+
"file",
|
|
372
|
+
"family",
|
|
373
|
+
"id",
|
|
374
|
+
"weight",
|
|
375
|
+
"url",
|
|
376
|
+
"license",
|
|
377
|
+
"license-file",
|
|
378
|
+
"sha256",
|
|
379
|
+
"force",
|
|
380
|
+
"keep-files",
|
|
381
|
+
"static",
|
|
382
|
+
]) {
|
|
383
|
+
if (parsed.flags.has(flag)) {
|
|
384
|
+
usage(`--${flag} is not valid for "font search".`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const limit = parsed.flags.get("limit");
|
|
388
|
+
if (limit !== undefined &&
|
|
389
|
+
(typeof limit !== "string" || !/^\d+$/.test(limit) || Number(limit) < 1 || Number(limit) > 100)) {
|
|
390
|
+
usage("--limit must be an integer between 1 and 100.");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (action === "add") {
|
|
394
|
+
const file = typeof parsed.flags.get("file") === "string";
|
|
395
|
+
const url = typeof parsed.flags.get("url") === "string";
|
|
396
|
+
if (file && url) {
|
|
397
|
+
usage('"font add" accepts only one source: --file, --url, or a Fontsource id.');
|
|
398
|
+
}
|
|
399
|
+
if (parsed.flags.has("category") || parsed.flags.has("limit") || parsed.flags.has("variable")) {
|
|
400
|
+
usage('--category, --limit, and --variable are only valid for "font search".');
|
|
401
|
+
}
|
|
402
|
+
if (parsed.flags.has("keep-files")) {
|
|
403
|
+
usage('--keep-files is only valid for "font remove".');
|
|
404
|
+
}
|
|
405
|
+
if (parsed.flags.has("static") && file) {
|
|
406
|
+
usage("--static is only valid for a Fontsource id.");
|
|
407
|
+
}
|
|
408
|
+
if (parsed.flags.has("static") && url) {
|
|
409
|
+
usage("--static is only valid for a Fontsource id.");
|
|
410
|
+
}
|
|
411
|
+
if (file) {
|
|
412
|
+
if (parsed.positionals.length !== 1) {
|
|
413
|
+
usage('"font add --file" does not take an id; use --id to override the derived font id.');
|
|
414
|
+
}
|
|
415
|
+
if (typeof parsed.flags.get("family") !== "string") {
|
|
416
|
+
usage('"font add --file" requires --family <name>.');
|
|
417
|
+
}
|
|
418
|
+
if (parsed.flags.has("sha256")) {
|
|
419
|
+
usage("--sha256 is only valid with --url.");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (url) {
|
|
423
|
+
if (parsed.positionals.length !== 1) {
|
|
424
|
+
usage('"font add --url" does not take an id; use --id to override the derived font id.');
|
|
425
|
+
}
|
|
426
|
+
if (typeof parsed.flags.get("family") !== "string") {
|
|
427
|
+
usage('"font add --url" requires --family <name>.');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
if (parsed.positionals.length !== 2) {
|
|
432
|
+
usage('"font add" requires a Fontsource id, or --file/--url with --family.');
|
|
433
|
+
}
|
|
434
|
+
if (parsed.flags.has("id")) {
|
|
435
|
+
usage("The Fontsource id is positional; --id is only valid with --file or --url.");
|
|
436
|
+
}
|
|
437
|
+
if (parsed.flags.has("license") || parsed.flags.has("license-file")) {
|
|
438
|
+
usage("Fontsource supplies the license; --license/--license-file are only valid with --file or --url.");
|
|
439
|
+
}
|
|
440
|
+
if (parsed.flags.has("sha256")) {
|
|
441
|
+
usage("--sha256 is only valid with --url.");
|
|
442
|
+
}
|
|
443
|
+
if (parsed.flags.has("unicode-range")) {
|
|
444
|
+
usage("--unicode-range is only valid with --file or --url.");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (action === "list" || action === "verify") {
|
|
449
|
+
if (parsed.positionals.length !== 1) {
|
|
450
|
+
usage(`"font ${action}" does not take positional arguments.`);
|
|
451
|
+
}
|
|
452
|
+
for (const flag of [
|
|
453
|
+
"file",
|
|
454
|
+
"family",
|
|
455
|
+
"id",
|
|
456
|
+
"weight",
|
|
457
|
+
"style",
|
|
458
|
+
"subset",
|
|
459
|
+
"unicode-range",
|
|
460
|
+
"license",
|
|
461
|
+
"license-file",
|
|
462
|
+
"category",
|
|
463
|
+
"limit",
|
|
464
|
+
"url",
|
|
465
|
+
"sha256",
|
|
466
|
+
"static",
|
|
467
|
+
"variable",
|
|
468
|
+
"force",
|
|
469
|
+
"keep-files",
|
|
470
|
+
]) {
|
|
471
|
+
if (parsed.flags.has(flag)) {
|
|
472
|
+
usage(`--${flag} is not valid for "font ${action}".`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (action === "remove") {
|
|
477
|
+
if (parsed.positionals.length !== 2) {
|
|
478
|
+
usage('"font remove" requires a font id.');
|
|
479
|
+
}
|
|
480
|
+
for (const flag of [
|
|
481
|
+
"file",
|
|
482
|
+
"family",
|
|
483
|
+
"id",
|
|
484
|
+
"weight",
|
|
485
|
+
"style",
|
|
486
|
+
"subset",
|
|
487
|
+
"unicode-range",
|
|
488
|
+
"license",
|
|
489
|
+
"license-file",
|
|
490
|
+
"category",
|
|
491
|
+
"limit",
|
|
492
|
+
"url",
|
|
493
|
+
"sha256",
|
|
494
|
+
"static",
|
|
495
|
+
"variable",
|
|
496
|
+
"force",
|
|
497
|
+
]) {
|
|
498
|
+
if (parsed.flags.has(flag)) {
|
|
499
|
+
usage(`--${flag} is not valid for "font remove".`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
usage('"font" requires one of: search, add, list, verify, remove.');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
183
507
|
else if (parsed.positionals.length !== 1) {
|
|
184
508
|
usage(`"${parsed.command}" requires a ${parsed.command === "transcribe"
|
|
185
509
|
? "source path"
|
|
186
|
-
: parsed.command === "render"
|
|
510
|
+
: parsed.command === "render" || parsed.command === "lint"
|
|
187
511
|
? "composition or timeline id"
|
|
188
512
|
: "composition id"}.`);
|
|
189
513
|
}
|
|
514
|
+
const profile = parsed.flags.get("profile");
|
|
515
|
+
if (profile !== undefined && profile !== "shared-cpu") {
|
|
516
|
+
usage('--profile must be "shared-cpu".');
|
|
517
|
+
}
|
|
190
518
|
if (parsed.command === "still" && !parsed.flags.has("frame")) {
|
|
191
519
|
usage('"still" requires --frame <spec>.');
|
|
192
520
|
}
|
|
@@ -194,9 +522,47 @@ function validateArgs(parsed) {
|
|
|
194
522
|
validateRenderConcurrency(Number(parsed.flags.get("concurrency")));
|
|
195
523
|
}
|
|
196
524
|
if (parsed.command === "tts") {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
525
|
+
const requestedEngine = parsed.flags.get("engine");
|
|
526
|
+
let engine = "qwen";
|
|
527
|
+
if (requestedEngine !== undefined) {
|
|
528
|
+
if (typeof requestedEngine !== "string" || (requestedEngine !== "qwen" && requestedEngine !== "supertonic")) {
|
|
529
|
+
usage('--engine must be "qwen" or "supertonic".');
|
|
530
|
+
}
|
|
531
|
+
engine = requestedEngine;
|
|
532
|
+
}
|
|
533
|
+
if (engine === "supertonic") {
|
|
534
|
+
for (const required of ["speaker", "model-dir", "language", "out"]) {
|
|
535
|
+
if (!parsed.flags.has(required)) {
|
|
536
|
+
usage(`"tts" requires --${required} with --engine supertonic.`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
for (const forbidden of ["talker-model", "codec-model", "style", "device"]) {
|
|
540
|
+
if (parsed.flags.has(forbidden)) {
|
|
541
|
+
usage(`--${forbidden} is not valid with --engine supertonic.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const speaker = parsed.flags.get("speaker");
|
|
545
|
+
if (typeof speaker === "string") {
|
|
546
|
+
try {
|
|
547
|
+
parseSupertonicSpeaker(speaker);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
usage(error instanceof Error ? error.message : String(error));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const language = parsed.flags.get("language");
|
|
554
|
+
if (typeof language === "string" && !isSupertonicLanguage(language)) {
|
|
555
|
+
usage(`--language must be one of: ${SUPERTONIC_LANGUAGES.join(", ")}.`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
for (const required of ["speaker", "talker-model", "codec-model", "out"]) {
|
|
560
|
+
if (!parsed.flags.has(required)) {
|
|
561
|
+
usage(`"tts" requires --${required}.`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (parsed.flags.has("model-dir")) {
|
|
565
|
+
usage("--model-dir is only valid with --engine supertonic.");
|
|
200
566
|
}
|
|
201
567
|
}
|
|
202
568
|
}
|
|
@@ -234,81 +600,140 @@ function printHelp() {
|
|
|
234
600
|
Local-first, deterministic video compositions authored with HTML, CSS, and JavaScript.
|
|
235
601
|
|
|
236
602
|
Usage:
|
|
237
|
-
frameyard init [directory]
|
|
603
|
+
frameyard init [directory] [--local-models]
|
|
238
604
|
frameyard dev [--port <port>] [--project <dir>]
|
|
239
|
-
frameyard
|
|
240
|
-
frameyard
|
|
241
|
-
frameyard
|
|
242
|
-
frameyard
|
|
243
|
-
frameyard
|
|
244
|
-
frameyard
|
|
605
|
+
frameyard context [--project <dir>] [--json]
|
|
606
|
+
frameyard lint <composition-or-timeline-id> [--props <file>] [--project <dir>] [--json]
|
|
607
|
+
frameyard check <composition-id> [--props <file>] [--project <dir>] [--profile <name>] [--low-priority] [--json]
|
|
608
|
+
frameyard still <composition-or-timeline-id> --frame <spec> [--out <file-or-dir>] [--props <file>] [--project <dir>] [--profile <name>] [--low-priority] [--json]
|
|
609
|
+
frameyard inspect <image> [--json]
|
|
610
|
+
frameyard compare <first-image> <second-image> [--json]
|
|
611
|
+
frameyard render <composition-or-timeline-id> [--out <file>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--dry-run] [--profile <name>] [--low-priority] [--json]
|
|
612
|
+
frameyard tts [--engine qwen] --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] [--dry-run] [--profile <name>] [--low-priority] [--json]
|
|
613
|
+
frameyard tts --engine supertonic --speaker <sid> --model-dir <dir> --language <code> --out <file.wav> [--text-file <file>] [--executable <path>] [--manifest <file>] [--force] [--dry-run] [--profile <name>] [--low-priority] [--json]
|
|
614
|
+
frameyard transcribe <source> --model <gguf> --out <file.json> [--words] [--export srt,vtt] [--language <lang>] [--executable <path>] [--manifest <file>] [--force] [--dry-run] [--profile <name>] [--low-priority] [--json]
|
|
615
|
+
frameyard add --list [--json]
|
|
616
|
+
frameyard add local-models [--project <dir>] [--json]
|
|
617
|
+
frameyard add <composition|timeline> <id> --template <name> [--project <dir>] [--json]
|
|
618
|
+
frameyard font search [query] [--category <category>] [--subset <subset>] [--style <normal|italic>] [--variable] [--limit <n>] [--json]
|
|
619
|
+
frameyard font add <fontsource-id> [--family <alias>] [--subset <subset>] [--style <normal|italic>] [--weight <n>] [--static] [--force] [--project <dir>] [--json]
|
|
620
|
+
frameyard font add --file <path> --family <name> [--id <id>] [--weight <n|n-n>] [--style normal|italic] [--subset <name>] [--license <id>] [--license-file <path>] [--force] [--project <dir>] [--json]
|
|
621
|
+
frameyard font add --url <https-url> --family <name> [--id <id>] [--weight <n|n-n>] [--style normal|italic] [--license <id>] [--license-file <path>] [--sha256 <hash>] [--force] [--project <dir>] [--json]
|
|
622
|
+
frameyard font list|verify [--project <dir>] [--json]
|
|
623
|
+
frameyard font remove <id> [--keep-files] [--project <dir>] [--json]
|
|
624
|
+
frameyard doctor [--qwen-tts <path>] [--sherpa-onnx-tts <path>] [--whisper-cli <path>] [--talker-model <gguf>] [--codec-model <gguf>] [--model-dir <dir>] [--model <gguf>] [--manifest <file>] [--project <dir>] [--json]
|
|
245
625
|
|
|
246
626
|
Options:
|
|
247
627
|
--project <dir> Use a project other than the current directory.
|
|
248
628
|
--props <file> Composition props, or a { "<clip-id>": {...} } override map for a timeline.
|
|
249
|
-
--out <path> Output path for still, render, tts, or transcribe. For still, one frame uses a .png file and multiple frames use a directory. Render defaults to out/<id>.mp4
|
|
629
|
+
--out <path> Output path for still, render, tts, or transcribe. For still, one frame uses a .png file and multiple frames use a directory. Render defaults to out/<id>.mp4 for video and out/<id>.png for a static composition.
|
|
250
630
|
--frame <spec> Still frames: n, n-m, or a comma-separated list (maximum 32 unique frames).
|
|
251
|
-
--concurrency <n> Parallel browser sessions for composition render, 1-${MAX_RENDER_CONCURRENCY} (default: 1).
|
|
631
|
+
--concurrency <n> Parallel browser sessions for animated composition render, 1-${MAX_RENDER_CONCURRENCY} (default: 1).
|
|
252
632
|
--workers <n> Parallel segment workers for timeline render, 1-${MAX_TIMELINE_WORKERS} (default: 8).
|
|
253
633
|
--encoder <name> Encoder: auto, software, nvenc, or qsv (default: auto).
|
|
254
634
|
--codec <name> Delivery codec for render: h264, hevc, or vp9 (default: h264; vp9 for webm).
|
|
255
635
|
--container <name> Delivery container: mp4, mov, mkv, or webm (default: mp4).
|
|
256
636
|
--scratch <dir> Scratch directory for timeline intermediates (default: system temp).
|
|
257
|
-
--overwrite Replace an existing still or render output.
|
|
258
|
-
--force Replace existing generated assets and manifest entries.
|
|
637
|
+
--overwrite Replace an existing still or render output when the resource policy allows it.
|
|
638
|
+
--force Replace existing generated assets and manifest entries when the resource policy allows it.
|
|
639
|
+
--dry-run Validate and print the plan and handoff command without rendering, synthesizing, transcribing, or writing outputs.
|
|
640
|
+
--profile <name> Resource profile for shared machines. shared-cpu caps workers at 2, concurrency at 1, and lowers process priority.
|
|
641
|
+
--low-priority Lower the process priority without changing worker caps.
|
|
642
|
+
--local-models With init, also install the optional local-model host scaffold.
|
|
643
|
+
--template <name> Template for "add": static-poster, title-card, lower-third, or slideshow.
|
|
644
|
+
--list List add templates and scaffolds without writing files.
|
|
645
|
+
--file <path> Local font file for "font add": .woff2, .woff, .ttf, or .otf.
|
|
646
|
+
--url <https-url> Download a font from HTTPS (loopback HTTP is allowed for tests).
|
|
647
|
+
--family <name> CSS font-family name or alias for "font add".
|
|
648
|
+
--id <id> Override the font manifest id derived from the family name.
|
|
649
|
+
--weight <value> Font weight or variable range; comma-separated weights for Fontsource --static.
|
|
650
|
+
--style <value> qwen style instruction, or normal|italic (comma list for Fontsource).
|
|
651
|
+
--subset <name> Font subset label; comma-separated for Fontsource.
|
|
652
|
+
--unicode-range <r> CSS unicode-range for a local or URL font face.
|
|
653
|
+
--license <id> License identifier for --file/--url; warns when omitted.
|
|
654
|
+
--license-file <path> Copy a license file into the font directory.
|
|
655
|
+
--sha256 <hash> Expected SHA-256 for --url; the computed hash is always recorded.
|
|
656
|
+
--category <name> Fontsource category filter for "font search".
|
|
657
|
+
--limit <n> Fontsource result limit for "font search", 1-100 (default: 20).
|
|
658
|
+
--variable Fontsource search filter: only variable fonts.
|
|
659
|
+
--static Install static Fontsource weights instead of the variable face.
|
|
660
|
+
--keep-files Keep font files when removing a font from the manifest.
|
|
259
661
|
--text-file <file> Text to synthesize. Reads stdin when omitted.
|
|
260
|
-
--speaker <
|
|
261
|
-
--
|
|
262
|
-
--
|
|
263
|
-
--
|
|
264
|
-
--
|
|
662
|
+
--speaker <value> Named speaker for qwen, or the numeric speaker id for Supertonic.
|
|
663
|
+
--engine <name> Speech engine: qwen or supertonic (default: qwen).
|
|
664
|
+
--model-dir <dir> Supertonic model directory from the sherpa-onnx archive.
|
|
665
|
+
--talker-model <gguf> Talker GGUF model path for qwen.
|
|
666
|
+
--codec-model <gguf> Codec GGUF model path for qwen.
|
|
667
|
+
--style <value> Optional qwen style instruction, or normal|italic for "font add".
|
|
668
|
+
--language <lang> Spoken language. Supertonic requires one of its 31 upstream language codes.
|
|
265
669
|
--device <name> Compute device for qwen-tts, such as CPU or CUDA0.
|
|
266
670
|
--model <gguf> Whisper GGML model path.
|
|
267
671
|
--words Request measured word timings during transcription.
|
|
268
672
|
--export srt,vtt Derived subtitle exports next to the transcript.
|
|
269
|
-
--executable <path> Explicit qwen-tts or whisper-cli executable.
|
|
270
|
-
--manifest <file> Provenance manifest path (default:
|
|
673
|
+
--executable <path> Explicit qwen-tts, sherpa-onnx-offline-tts, or whisper-cli executable.
|
|
674
|
+
--manifest <file> Provenance manifest path (default: fy.assets.json).
|
|
271
675
|
--qwen-tts <path> qwen-tts executable to inspect with doctor.
|
|
676
|
+
--sherpa-onnx-tts <path> sherpa-onnx-offline-tts executable to inspect with doctor.
|
|
272
677
|
--whisper-cli <path> whisper-cli executable to inspect with doctor.
|
|
273
678
|
--json Print a structured result to stdout.
|
|
274
679
|
--help, -h Show this help.
|
|
275
680
|
--version, -v Print the version.
|
|
681
|
+
|
|
682
|
+
Resource policy: the nearest fy.policy.json from the working directory upward sets light, authoring, or full; FRAMEYARD_POLICY overrides it. tts, transcribe, and render are blocked outside full. font add requires authoring or full; font remove and font add --force require full. Use --dry-run to validate and produce a handoff command without doing heavy work.
|
|
276
683
|
`);
|
|
277
684
|
}
|
|
278
685
|
function printCommandHelp(command) {
|
|
279
686
|
switch (command) {
|
|
280
687
|
case "init":
|
|
281
|
-
process.stdout.write("Usage: frameyard init [directory]\nScaffold a minimal Frameyard project.\n");
|
|
688
|
+
process.stdout.write("Usage: frameyard init [directory] [--local-models]\nScaffold a minimal Frameyard project. Without a directory, init asks for a project name (default: my-video) and creates that directory; non-interactive and --json runs use the default. --local-models also installs the optional policy, OpenCode, and Pi scaffold.\n");
|
|
282
689
|
return;
|
|
283
690
|
case "dev":
|
|
284
691
|
process.stdout.write("Usage: frameyard dev [--port <port>] [--project <dir>]\nStart the browser preview server.\n");
|
|
285
692
|
return;
|
|
693
|
+
case "context":
|
|
694
|
+
process.stdout.write("Usage: frameyard context [--project <dir>] [--json]\nPrint a compact digest of the project, resolved timelines, assets, diagnostics, active policy, and next commands. It never launches Chromium.\n");
|
|
695
|
+
return;
|
|
696
|
+
case "lint":
|
|
697
|
+
process.stdout.write("Usage: frameyard lint <composition-or-timeline-id> [--props <file>] [--project <dir>] [--json]\nRun static validation, timeline resolution, scene and keyframe projection for every timeline frame, and a controller determinism scan without launching Chromium. It does not run validateProps or capture frames; use check for the full gate.\n");
|
|
698
|
+
return;
|
|
286
699
|
case "check":
|
|
287
|
-
process.stdout.write("Usage: frameyard check <composition-id> [--props <file>] [--project <dir>] [--json]\nValidate props, assets, audio, and every frame without encoding.\n");
|
|
700
|
+
process.stdout.write("Usage: frameyard check <composition-id> [--props <file>] [--project <dir>] [--profile <name>] [--low-priority] [--json]\nValidate props, assets, audio, and every frame without encoding. The authoring policy lowers process priority for check.\n");
|
|
288
701
|
return;
|
|
289
702
|
case "still":
|
|
290
|
-
process.stdout.write("Usage: frameyard still <composition-or-timeline-id> --frame <spec> [--out <path>] [--props <file>] [--project <dir>] [--overwrite] [--json]\nCapture one or more frames through the production rendering path. <spec> is a frame number, an inclusive range like 2-12, or a comma-separated list
|
|
703
|
+
process.stdout.write("Usage: frameyard still <composition-or-timeline-id> --frame <spec> [--out <path>] [--props <file>] [--project <dir>] [--overwrite] [--profile <name>] [--low-priority] [--json]\nCapture one or more frames through the production rendering path. <spec> is a frame number, an inclusive range like 2-12, or a comma-separated list; the policy caps unique frames per invocation. With one frame, --out is a .png file; with multiple frames, --out is a directory. A static composition exposes only frame 0.\n");
|
|
704
|
+
return;
|
|
705
|
+
case "inspect":
|
|
706
|
+
process.stdout.write("Usage: frameyard inspect <image> [--json]\nDecode the first frame and report dimensions, alpha coverage, blank detection, background and dominant colors, content bbox, a coarse luminance grid, and edge density. It never launches Chromium.\n");
|
|
707
|
+
return;
|
|
708
|
+
case "compare":
|
|
709
|
+
process.stdout.write("Usage: frameyard compare <first-image> <second-image> [--json]\nCompare two decoded images pixel by pixel and by 3×3 region. Exits 1 when the images differ; the JSON result still has ok: true and identical: false.\n");
|
|
291
710
|
return;
|
|
292
711
|
case "render":
|
|
293
|
-
process.stdout.write(`Usage: frameyard render <composition-or-timeline-id> [--out <
|
|
712
|
+
process.stdout.write(`Usage: frameyard render <composition-or-timeline-id> [--out <file>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--dry-run] [--profile <name>] [--low-priority] [--json]\nRender an MP4, MOV, MKV, or WebM with H.264, HEVC, or VP9 video and AAC or Opus audio, or a PNG for a static composition. The default output is out/<id>.<extension> for video and out/<id>.png for a static composition. Animated composition concurrency is 1-${MAX_RENDER_CONCURRENCY}; timeline workers are 1-${MAX_TIMELINE_WORKERS}. The shared-cpu profile caps workers at 2 and concurrency at 1. Encoder choices are auto, software, nvenc, and qsv; the default auto probes NVENC, then Quick Sync, then the codec's software encoder. WebM and VP9 use Opus audio, and HEVC in MP4/MOV keeps the hvc1 sample entry. A static composition rejects --concurrency, --container, --codec, and --encoder. With --dry-run, validates and prints the plan and handoff command without writing outputs or launching Chromium.\n`);
|
|
294
713
|
return;
|
|
295
714
|
case "tts":
|
|
296
|
-
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");
|
|
715
|
+
process.stdout.write("Usage: frameyard tts [--engine qwen] --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] [--dry-run] [--profile <name>] [--low-priority] [--json]\n frameyard tts --engine supertonic --speaker <sid> --model-dir <dir> --language <code> --out <file.wav> [--text-file <file>] [--executable <path>] [--manifest <file>] [--force] [--dry-run] [--profile <name>] [--low-priority] [--json]\nProduce one frozen 24 kHz mono WAV with local qwentts.cpp CustomVoice or sherpa-onnx Supertonic 3 synthesis. Supertonic language codes: ar, bg, cs, da, de, el, en, es, et, fi, fr, hi, hr, hu, id, it, ja, ko, lt, lv, nl, pl, pt, ro, ru, sk, sl, sv, tr, uk, vi. With --dry-run, validates inputs and prints the plan and command without loading a model.\n");
|
|
297
716
|
return;
|
|
298
717
|
case "transcribe":
|
|
299
|
-
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");
|
|
718
|
+
process.stdout.write("Usage: frameyard transcribe <source> --model <gguf> --out <file.json> [--words] [--export srt,vtt] [--language <lang>] [--executable <path>] [--manifest <file>] [--force] [--dry-run] [--profile <name>] [--low-priority] [--json]\nProduce a canonical Frameyard transcript, optionally with word timings and subtitle exports. With --dry-run, validates inputs and prints the plan and command without loading a model.\n");
|
|
719
|
+
return;
|
|
720
|
+
case "add":
|
|
721
|
+
process.stdout.write("Usage: frameyard add --list [--json]\n frameyard add local-models [--project <dir>] [--json]\n frameyard add <composition|timeline> <id> --template <name> [--project <dir>] [--json]\nWrite a valid template entry and print the exact fy.config.js registration snippet, or install the optional local-model host scaffold. Templates are static-poster, title-card, lower-third, and slideshow. The command refuses to overwrite an existing file or an already registered id.\n");
|
|
722
|
+
return;
|
|
723
|
+
case "font":
|
|
724
|
+
process.stdout.write("Usage: frameyard font search [query] [--category <name>] [--subset <name>] [--style normal|italic] [--variable] [--limit <n>] [--json]\n frameyard font add <fontsource-id> [--family <alias>] [--subset <name>] [--style normal|italic] [--weight <n>] [--static] [--force] [--project <dir>] [--json]\n frameyard font add --file <path> --family <name> [--id <id>] [--weight <n|n-n>] [--style normal|italic] [--subset <name>] [--unicode-range <range>] [--license <id>] [--license-file <path>] [--force] [--project <dir>] [--json]\n frameyard font add --url <https-url> --family <name> [--id <id>] [--weight <n|n-n>] [--style normal|italic] [--subset <name>] [--unicode-range <range>] [--license <id>] [--license-file <path>] [--sha256 <hash>] [--force] [--project <dir>] [--json]\n frameyard font list|verify [--project <dir>] [--json]\n frameyard font remove <id> [--keep-files] [--project <dir>] [--json]\nSearch Fontsource, vendor a Fontsource/local/URL font into assets/fonts/<id>/, and manage fy.fonts.json. The server serves the generated @font-face sheet at /__frameyard/fonts.css. Project policy allows search/list/verify everywhere, add in authoring and full, and remove or --force only in full. A --url import without --license also requires full.\n");
|
|
300
725
|
return;
|
|
301
726
|
case "doctor":
|
|
302
|
-
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");
|
|
727
|
+
process.stdout.write("Usage: frameyard doctor [--qwen-tts <path>] [--sherpa-onnx-tts <path>] [--whisper-cli <path>] [--talker-model <gguf>] [--codec-model <gguf>] [--model-dir <dir>] [--model <gguf>] [--manifest <file>] [--project <dir>] [--json]\nInspect core requirements, optional integrations, model readability, and provenance integrity without changing the environment.\n");
|
|
303
728
|
return;
|
|
304
729
|
default:
|
|
305
730
|
printHelp();
|
|
306
731
|
}
|
|
307
732
|
}
|
|
308
|
-
async function projectFromFlag(flags) {
|
|
733
|
+
async function projectFromFlag(flags, options = {}) {
|
|
309
734
|
const requested = flags.get("project");
|
|
310
735
|
const root = typeof requested === "string" ? path.resolve(process.cwd(), requested) : await resolveProjectRoot(process.cwd());
|
|
311
|
-
return loadProject(root);
|
|
736
|
+
return loadProject(root, options);
|
|
312
737
|
}
|
|
313
738
|
/** Resolves the directory that anchors the project-local provenance manifest. */
|
|
314
739
|
async function assetRootFromFlag(flags) {
|
|
@@ -452,10 +877,138 @@ function printDiagnostics(diagnostics) {
|
|
|
452
877
|
function printJson(value) {
|
|
453
878
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
454
879
|
}
|
|
880
|
+
async function pathExists(filePath) {
|
|
881
|
+
try {
|
|
882
|
+
await stat(filePath);
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function shellQuote(value) {
|
|
890
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
891
|
+
}
|
|
892
|
+
function handoffCommand(args) {
|
|
893
|
+
return ["npx", "frameyard", ...args].map(shellQuote).join(" ");
|
|
894
|
+
}
|
|
895
|
+
function collectPrerequisites(timeline) {
|
|
896
|
+
const values = new Set();
|
|
897
|
+
const visit = (current) => {
|
|
898
|
+
for (const clip of current.clips) {
|
|
899
|
+
const source = clip.source;
|
|
900
|
+
if (source.kind === "video" || source.kind === "image" || source.kind === "audio") {
|
|
901
|
+
values.add(source.src);
|
|
902
|
+
}
|
|
903
|
+
else if (source.kind === "composition") {
|
|
904
|
+
values.add(source.entry);
|
|
905
|
+
for (const audio of source.audioClips) {
|
|
906
|
+
values.add(audio.src);
|
|
907
|
+
}
|
|
908
|
+
for (const transcript of source.transcriptClips) {
|
|
909
|
+
values.add(transcript.src);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
else if (source.kind === "timeline") {
|
|
913
|
+
visit(source.timeline);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
visit(timeline);
|
|
918
|
+
return [...values].sort();
|
|
919
|
+
}
|
|
920
|
+
function printDryRun(label, plan, commandLine, prerequisites, blockers) {
|
|
921
|
+
process.stdout.write(`${label} dry run\n`);
|
|
922
|
+
process.stdout.write(` plan: ${JSON.stringify(plan)}\n`);
|
|
923
|
+
process.stdout.write(` command: ${commandLine}\n`);
|
|
924
|
+
if (prerequisites.length > 0) {
|
|
925
|
+
process.stdout.write(` prerequisites: ${prerequisites.join(", ")}\n`);
|
|
926
|
+
}
|
|
927
|
+
for (const blocker of blockers) {
|
|
928
|
+
process.stdout.write(` blocker [${blocker.code}]: ${blocker.message}\n`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function prerequisiteBlockers(items) {
|
|
932
|
+
const blockers = [];
|
|
933
|
+
for (const item of items) {
|
|
934
|
+
let info;
|
|
935
|
+
try {
|
|
936
|
+
info = await stat(item.path);
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
blockers.push({
|
|
940
|
+
code: "ASSET_NOT_FOUND",
|
|
941
|
+
message: `${item.label} does not exist: ${item.path}`,
|
|
942
|
+
suggestion: "Check the path before running the heavy operation.",
|
|
943
|
+
});
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (item.directory === true ? !info.isDirectory() : !info.isFile()) {
|
|
947
|
+
blockers.push({
|
|
948
|
+
code: "ASSET_NOT_FOUND",
|
|
949
|
+
message: `${item.label} is not a ${item.directory === true ? "directory" : "file"}: ${item.path}`,
|
|
950
|
+
suggestion: "Check the path before running the heavy operation.",
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return blockers;
|
|
955
|
+
}
|
|
956
|
+
const DEFAULT_INIT_PROJECT_NAME = "my-video";
|
|
957
|
+
function assertProjectDirectoryName(name) {
|
|
958
|
+
if (name === "." || name === ".." || name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
|
959
|
+
usage(`Project name must be a directory name without path separators; received "${name}".`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Asks for the project name on an interactive terminal. EOF accepts the
|
|
964
|
+
* default; Ctrl+C cancels the command.
|
|
965
|
+
*/
|
|
966
|
+
async function promptProjectName(defaultName) {
|
|
967
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
968
|
+
let interrupted = false;
|
|
969
|
+
rl.on("SIGINT", () => {
|
|
970
|
+
interrupted = true;
|
|
971
|
+
rl.close();
|
|
972
|
+
});
|
|
973
|
+
try {
|
|
974
|
+
const closed = new Promise((resolve) => {
|
|
975
|
+
rl.once("close", () => resolve(null));
|
|
976
|
+
});
|
|
977
|
+
const answer = await Promise.race([rl.question(`Project name (${defaultName}): `), closed]);
|
|
978
|
+
if (interrupted) {
|
|
979
|
+
throw new FrameyardError("CANCELLED", "Project creation cancelled.");
|
|
980
|
+
}
|
|
981
|
+
const name = (answer ?? "").trim();
|
|
982
|
+
return name === "" ? defaultName : name;
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
rl.close();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* `init <directory>` scaffolds into that directory. A bare `init` asks for a
|
|
990
|
+
* project name on an interactive terminal and creates a directory with it;
|
|
991
|
+
* non-interactive and `--json` runs use the default name without prompting.
|
|
992
|
+
*/
|
|
993
|
+
async function resolveInitTarget(parsed) {
|
|
994
|
+
const positional = parsed.positionals[0];
|
|
995
|
+
if (positional !== undefined) {
|
|
996
|
+
return path.resolve(process.cwd(), positional);
|
|
997
|
+
}
|
|
998
|
+
const interactive = parsed.flags.has("json") !== true && process.stdin.isTTY === true;
|
|
999
|
+
const name = interactive ? await promptProjectName(DEFAULT_INIT_PROJECT_NAME) : DEFAULT_INIT_PROJECT_NAME;
|
|
1000
|
+
assertProjectDirectoryName(name);
|
|
1001
|
+
return path.resolve(process.cwd(), name);
|
|
1002
|
+
}
|
|
455
1003
|
async function runInit(parsed) {
|
|
456
|
-
const target =
|
|
1004
|
+
const target = await resolveInitTarget(parsed);
|
|
457
1005
|
const json = parsed.flags.has("json") === true;
|
|
458
|
-
const
|
|
1006
|
+
const localModels = parsed.flags.has("local-models") === true;
|
|
1007
|
+
const files = {
|
|
1008
|
+
...scaffoldFiles(path.basename(target) || "frameyard-project"),
|
|
1009
|
+
...(localModels ? localModelScaffoldFiles() : {}),
|
|
1010
|
+
};
|
|
1011
|
+
const next = localModels ? [...SCAFFOLD_NEXT, "Review .pi/settings.json and run pi --approve"] : [...SCAFFOLD_NEXT];
|
|
459
1012
|
for (const relativePath of Object.keys(files)) {
|
|
460
1013
|
try {
|
|
461
1014
|
await access(path.join(target, relativePath));
|
|
@@ -481,7 +1034,8 @@ async function runInit(parsed) {
|
|
|
481
1034
|
command: "init",
|
|
482
1035
|
project: target,
|
|
483
1036
|
files: Object.keys(files),
|
|
484
|
-
next
|
|
1037
|
+
next,
|
|
1038
|
+
...(localModels ? { localModels: true } : {}),
|
|
485
1039
|
};
|
|
486
1040
|
if (json) {
|
|
487
1041
|
printJson(result);
|
|
@@ -491,7 +1045,7 @@ async function runInit(parsed) {
|
|
|
491
1045
|
for (const file of Object.keys(files)) {
|
|
492
1046
|
process.stdout.write(` ${file}\n`);
|
|
493
1047
|
}
|
|
494
|
-
process.stdout.write("Next: " +
|
|
1048
|
+
process.stdout.write("Next: " + next.slice(0, 2).join(" && ") + "\n");
|
|
495
1049
|
}
|
|
496
1050
|
return 0;
|
|
497
1051
|
}
|
|
@@ -519,8 +1073,573 @@ async function runDev(parsed) {
|
|
|
519
1073
|
await server.close();
|
|
520
1074
|
return 0;
|
|
521
1075
|
}
|
|
522
|
-
async function
|
|
1076
|
+
async function runContext(parsed, signal, policy) {
|
|
1077
|
+
assertOperationAllowed(policy, "context");
|
|
1078
|
+
const project = await projectFromFlag(parsed.flags);
|
|
1079
|
+
const result = await buildProjectContext({ project, policy, signal });
|
|
1080
|
+
const json = parsed.flags.has("json") === true;
|
|
1081
|
+
if (json) {
|
|
1082
|
+
printJson({ ok: true, command: "context", ...result });
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
process.stdout.write(`Frameyard project: ${result.root}\n`);
|
|
1086
|
+
if (result.format !== undefined) {
|
|
1087
|
+
process.stdout.write(`format: ${result.format.width}×${result.format.height} @ ${result.format.fps} fps\n`);
|
|
1088
|
+
}
|
|
1089
|
+
process.stdout.write(`policy: ${describePolicy(policy)}\n`);
|
|
1090
|
+
process.stdout.write(`compositions: ${result.compositions.length}\n`);
|
|
1091
|
+
for (const composition of result.compositions) {
|
|
1092
|
+
const metadata = composition.metadata;
|
|
1093
|
+
const detail = metadata === undefined
|
|
1094
|
+
? composition.kind
|
|
1095
|
+
: metadata.static === true
|
|
1096
|
+
? `static ${metadata.width}×${metadata.height}`
|
|
1097
|
+
: `${metadata.width}×${metadata.height} @ ${metadata.fps} fps, ${metadata.durationInFrames} frames`;
|
|
1098
|
+
process.stdout.write(` ${composition.id}: ${detail}, ${composition.props.length} props, ${composition.diagnostics.errors} errors/${composition.diagnostics.warnings} warnings\n`);
|
|
1099
|
+
}
|
|
1100
|
+
process.stdout.write(`timelines: ${result.timelines.length}\n`);
|
|
1101
|
+
for (const timeline of result.timelines) {
|
|
1102
|
+
const detail = timeline.durationInFrames === undefined
|
|
1103
|
+
? "unresolved"
|
|
1104
|
+
: `${timeline.durationInFrames} frames (${timeline.durationSeconds?.toFixed(1)}s), ${timeline.tracks} tracks, ${timeline.clips?.length ?? 0} clips, ${timeline.transitions} transitions, ${timeline.audio} audio, ${timeline.transcripts} transcripts`;
|
|
1105
|
+
process.stdout.write(` ${timeline.id}: ${detail}, ${timeline.diagnostics.errors} errors/${timeline.diagnostics.warnings} warnings\n`);
|
|
1106
|
+
}
|
|
1107
|
+
process.stdout.write(`assets: ${result.assets.length}\n`);
|
|
1108
|
+
process.stdout.write(`fonts: ${result.fonts.length}\n`);
|
|
1109
|
+
for (const font of result.fonts) {
|
|
1110
|
+
const subsets = font.subsets.length === 0 ? "no subset" : font.subsets.join(",");
|
|
1111
|
+
process.stdout.write(` ${font.id}: ${font.family} (${font.provider}${font.version ? ` ${font.version}` : ""}), ${subsets}, ${font.styles.join("/")}, ${font.license}, ${font.files} files, ${font.status}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
process.stdout.write(`next:\n`);
|
|
1114
|
+
for (const command of result.next) {
|
|
1115
|
+
process.stdout.write(` ${command}\n`);
|
|
1116
|
+
}
|
|
1117
|
+
return 0;
|
|
1118
|
+
}
|
|
1119
|
+
async function runInspect(parsed) {
|
|
1120
|
+
const startedAt = performance.now();
|
|
1121
|
+
const filePath = path.resolve(process.cwd(), parsed.positionals[0]);
|
|
1122
|
+
const probed = await probeImage(filePath);
|
|
1123
|
+
const pixels = await decodeRgba(filePath, probed.width, probed.height);
|
|
1124
|
+
const stats = analyzeRgba(pixels, probed.width, probed.height);
|
|
1125
|
+
const json = parsed.flags.has("json") === true;
|
|
1126
|
+
const result = {
|
|
1127
|
+
ok: true,
|
|
1128
|
+
command: "inspect",
|
|
1129
|
+
path: filePath,
|
|
1130
|
+
bytes: probed.bytes,
|
|
1131
|
+
...stats,
|
|
1132
|
+
elapsedMs: performance.now() - startedAt,
|
|
1133
|
+
};
|
|
1134
|
+
if (json) {
|
|
1135
|
+
printJson(result);
|
|
1136
|
+
return 0;
|
|
1137
|
+
}
|
|
1138
|
+
process.stdout.write(`Inspected ${filePath} (${stats.width}×${stats.height}, ${probed.bytes} bytes)\n`);
|
|
1139
|
+
process.stdout.write(` blank: ${stats.blank}; alpha ${stats.alpha.min}..${stats.alpha.max}, coverage ${(stats.alpha.coverage * 100).toFixed(1)}%\n`);
|
|
1140
|
+
process.stdout.write(` background: ${stats.background.hex} (${(stats.background.share * 100).toFixed(1)}%); content: ${stats.content.pixels} px (${(stats.content.share * 100).toFixed(1)}%)${stats.content.bbox === null ? "" : ` at ${stats.content.bbox.x},${stats.content.bbox.y} ${stats.content.bbox.width}×${stats.content.bbox.height}`}\n`);
|
|
1141
|
+
process.stdout.write(` colors: ${stats.colors.map((color) => `${color.hex} ${(color.share * 100).toFixed(1)}%`).join(", ") || "(none)"}\n`);
|
|
1142
|
+
process.stdout.write(` edge density: ${stats.edgeDensity}\n`);
|
|
1143
|
+
return 0;
|
|
1144
|
+
}
|
|
1145
|
+
async function runCompare(parsed) {
|
|
1146
|
+
const startedAt = performance.now();
|
|
1147
|
+
const firstPath = path.resolve(process.cwd(), parsed.positionals[0]);
|
|
1148
|
+
const secondPath = path.resolve(process.cwd(), parsed.positionals[1]);
|
|
1149
|
+
const [first, second] = await Promise.all([probeImage(firstPath), probeImage(secondPath)]);
|
|
1150
|
+
if (first.width !== second.width || first.height !== second.height) {
|
|
1151
|
+
throw new FrameyardError("IMAGE_INVALID", `Images have different dimensions: ${firstPath} is ${first.width}×${first.height}, ${secondPath} is ${second.width}×${second.height}.`, { details: { first: firstPath, second: secondPath } });
|
|
1152
|
+
}
|
|
1153
|
+
const [firstPixels, secondPixels] = await Promise.all([
|
|
1154
|
+
decodeRgba(firstPath, first.width, first.height),
|
|
1155
|
+
decodeRgba(secondPath, second.width, second.height),
|
|
1156
|
+
]);
|
|
1157
|
+
const stats = compareRgba(firstPixels, secondPixels, first.width, first.height);
|
|
1158
|
+
const json = parsed.flags.has("json") === true;
|
|
1159
|
+
const result = {
|
|
1160
|
+
ok: true,
|
|
1161
|
+
command: "compare",
|
|
1162
|
+
first: firstPath,
|
|
1163
|
+
second: secondPath,
|
|
1164
|
+
...stats,
|
|
1165
|
+
elapsedMs: performance.now() - startedAt,
|
|
1166
|
+
};
|
|
1167
|
+
if (json) {
|
|
1168
|
+
printJson(result);
|
|
1169
|
+
}
|
|
1170
|
+
else if (stats.identical) {
|
|
1171
|
+
process.stdout.write(`${firstPath} and ${secondPath} are pixel-identical (${stats.width}×${stats.height}).\n`);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
process.stdout.write(`${firstPath} and ${secondPath} differ:\n`);
|
|
1175
|
+
process.stdout.write(` dimensions: ${stats.width}×${stats.height}\n`);
|
|
1176
|
+
process.stdout.write(` differing pixels: ${stats.differingPixels} / ${stats.totalPixels} (${stats.percent}%)\n`);
|
|
1177
|
+
process.stdout.write(` max channel delta: ${stats.maxChannelDelta}; mean ${stats.meanChannelDelta}\n`);
|
|
1178
|
+
process.stdout.write(` bounding box: ${stats.bbox === null ? "(none)" : `x=${stats.bbox.x}..${stats.bbox.x + stats.bbox.width - 1}, y=${stats.bbox.y}..${stats.bbox.y + stats.bbox.height - 1}`}\n`);
|
|
1179
|
+
}
|
|
1180
|
+
return stats.identical ? 0 : 1;
|
|
1181
|
+
}
|
|
1182
|
+
async function runAdd(parsed) {
|
|
1183
|
+
const json = parsed.flags.has("json") === true;
|
|
1184
|
+
if (parsed.flags.has("list")) {
|
|
1185
|
+
const templates = listTemplates().map((template) => ({
|
|
1186
|
+
name: template.name,
|
|
1187
|
+
kind: template.kind,
|
|
1188
|
+
description: template.description,
|
|
1189
|
+
...(template.prerequisites !== undefined ? { prerequisites: template.prerequisites } : {}),
|
|
1190
|
+
}));
|
|
1191
|
+
const scaffolds = [
|
|
1192
|
+
{
|
|
1193
|
+
name: "local-models",
|
|
1194
|
+
description: "Optional policy, OpenCode permissions, and Pi settings for a shared local model.",
|
|
1195
|
+
},
|
|
1196
|
+
];
|
|
1197
|
+
if (json) {
|
|
1198
|
+
printJson({ ok: true, command: "add", templates, scaffolds });
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
for (const template of templates) {
|
|
1202
|
+
process.stdout.write(`${template.kind}\t${template.name}\t${template.description}\n`);
|
|
1203
|
+
}
|
|
1204
|
+
for (const scaffold of scaffolds) {
|
|
1205
|
+
process.stdout.write(`scaffold\t${scaffold.name}\t${scaffold.description}\n`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return 0;
|
|
1209
|
+
}
|
|
1210
|
+
if (parsed.positionals[0] === "local-models") {
|
|
1211
|
+
const root = await assetRootFromFlag(parsed.flags);
|
|
1212
|
+
const files = localModelScaffoldFiles();
|
|
1213
|
+
const conflicts = [];
|
|
1214
|
+
for (const relative of Object.keys(files)) {
|
|
1215
|
+
if (await pathExists(path.join(root, relative))) {
|
|
1216
|
+
conflicts.push(relative);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (conflicts.length > 0) {
|
|
1220
|
+
throw new FrameyardError("OUTPUT_EXISTS", `Refusing to overwrite local-model scaffold files: ${conflicts.join(", ")}.`, { details: { conflicts } });
|
|
1221
|
+
}
|
|
1222
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
1223
|
+
const target = path.join(root, relative);
|
|
1224
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
1225
|
+
await writeFile(target, content, { flag: "wx" });
|
|
1226
|
+
}
|
|
1227
|
+
const result = {
|
|
1228
|
+
ok: true,
|
|
1229
|
+
command: "add",
|
|
1230
|
+
kind: "scaffold",
|
|
1231
|
+
scaffold: "local-models",
|
|
1232
|
+
path: root,
|
|
1233
|
+
files: Object.keys(files),
|
|
1234
|
+
next: [
|
|
1235
|
+
"Review .opencode/opencode.jsonc and .pi/settings.json",
|
|
1236
|
+
"Run pi --approve in the project",
|
|
1237
|
+
"Run npx frameyard context --json from the project",
|
|
1238
|
+
],
|
|
1239
|
+
};
|
|
1240
|
+
if (json) {
|
|
1241
|
+
printJson(result);
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
process.stdout.write(`Installed local-model scaffold in ${root}\n`);
|
|
1245
|
+
for (const file of result.files) {
|
|
1246
|
+
process.stdout.write(` ${file}\n`);
|
|
1247
|
+
}
|
|
1248
|
+
process.stdout.write(`Next: ${result.next[0]}\n`);
|
|
1249
|
+
}
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
const kind = parsed.positionals[0];
|
|
1253
|
+
const id = parsed.positionals[1];
|
|
1254
|
+
const templateName = flagString(parsed.flags, "template");
|
|
1255
|
+
const template = findTemplate(templateName);
|
|
1256
|
+
if (template === undefined) {
|
|
1257
|
+
usage(`Unknown template "${templateName}".`);
|
|
1258
|
+
}
|
|
1259
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
|
|
1260
|
+
throw new FrameyardError("CONFIG_INVALID", `Invalid id "${id}". Use letters, digits, dots, underscores, or hyphens, starting with a letter or digit.`);
|
|
1261
|
+
}
|
|
523
1262
|
const project = await projectFromFlag(parsed.flags);
|
|
1263
|
+
if (project.compositions.has(id) || project.timelines.has(id)) {
|
|
1264
|
+
throw new FrameyardError("OUTPUT_EXISTS", `Project id "${id}" is already registered in fy.config.js.`);
|
|
1265
|
+
}
|
|
1266
|
+
if (project.format === undefined) {
|
|
1267
|
+
throw new FrameyardError("CONFIG_INVALID", "Project templates inherit the project format. Add format { width, height, fps } to fy.config.js first.");
|
|
1268
|
+
}
|
|
1269
|
+
const entry = template.entry(id);
|
|
1270
|
+
const targetPath = path.resolve(project.root, entry);
|
|
1271
|
+
if (await pathExists(targetPath)) {
|
|
1272
|
+
throw new FrameyardError("OUTPUT_EXISTS", `Refusing to overwrite ${targetPath}.`);
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
1276
|
+
await writeFile(targetPath, template.content, { flag: "wx" });
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
throw new FrameyardError("OUTPUT_FAILED", `Could not write ${targetPath}: ${errorMessage(error)}`, {
|
|
1280
|
+
cause: error,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const registration = templateRegistration(template, id);
|
|
1284
|
+
const register = Object.fromEntries(Object.entries(registration).filter(([, value]) => value !== undefined));
|
|
1285
|
+
const result = {
|
|
1286
|
+
ok: true,
|
|
1287
|
+
command: "add",
|
|
1288
|
+
kind,
|
|
1289
|
+
id,
|
|
1290
|
+
template: template.name,
|
|
1291
|
+
entry,
|
|
1292
|
+
path: targetPath,
|
|
1293
|
+
register,
|
|
1294
|
+
prerequisites: template.prerequisites ?? [],
|
|
1295
|
+
next: ["Add the register entry to fy.config.js", `npx frameyard lint ${id} --json`],
|
|
1296
|
+
};
|
|
1297
|
+
if (json) {
|
|
1298
|
+
printJson(result);
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
process.stdout.write(`Created ${targetPath}\n`);
|
|
1302
|
+
const key = kind === "composition" ? "compositions" : "timelines";
|
|
1303
|
+
process.stdout.write(`Add to fy.config.js:\n ${key}: { "${id}": "./${entry}" }\n`);
|
|
1304
|
+
if ((template.prerequisites ?? []).length > 0) {
|
|
1305
|
+
process.stdout.write(`Provide: ${(template.prerequisites ?? []).join(", ")}\n`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return 0;
|
|
1309
|
+
}
|
|
1310
|
+
function splitFontList(value, flag) {
|
|
1311
|
+
if (value === undefined) {
|
|
1312
|
+
return undefined;
|
|
1313
|
+
}
|
|
1314
|
+
const parts = value
|
|
1315
|
+
.split(",")
|
|
1316
|
+
.map((part) => part.trim())
|
|
1317
|
+
.filter((part) => part.length > 0);
|
|
1318
|
+
if (parts.length === 0) {
|
|
1319
|
+
usage(`--${flag} requires at least one value.`);
|
|
1320
|
+
}
|
|
1321
|
+
return parts;
|
|
1322
|
+
}
|
|
1323
|
+
function parseFontsourceWeights(value) {
|
|
1324
|
+
const parts = splitFontList(value, "weight");
|
|
1325
|
+
if (parts === undefined) {
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
return parts.map((part) => {
|
|
1329
|
+
const weight = Number(part);
|
|
1330
|
+
if (!Number.isInteger(weight) || weight < 1 || weight > 1000) {
|
|
1331
|
+
usage(`--weight entries must be integers between 1 and 1000; received "${part}".`);
|
|
1332
|
+
}
|
|
1333
|
+
return weight;
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
function parseFontsourceStyles(value) {
|
|
1337
|
+
const parts = splitFontList(value, "style");
|
|
1338
|
+
return parts?.map((part) => parseFontStyle(part));
|
|
1339
|
+
}
|
|
1340
|
+
function printFontAddResult(result, json) {
|
|
1341
|
+
if (json) {
|
|
1342
|
+
printJson({ ok: true, command: "font", action: "add", ...result });
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
for (const warning of result.warnings) {
|
|
1346
|
+
process.stderr.write(`⚠ ${warning.code}: ${warning.message}\n`);
|
|
1347
|
+
}
|
|
1348
|
+
process.stdout.write(`✓ font ${result.font.id}: ${result.font.family} (${result.font.provider}${result.font.version ? ` ${result.font.version}` : ""}), ${result.font.files} file${result.font.files === 1 ? "" : "s"}, ${result.font.bytes} bytes\n`);
|
|
1349
|
+
process.stdout.write(` manifest: ${result.manifest}\n`);
|
|
1350
|
+
process.stdout.write(` css: /__frameyard/fonts.css\n`);
|
|
1351
|
+
process.stdout.write(` use: font-family: "${result.font.family}", system-ui, sans-serif\n`);
|
|
1352
|
+
}
|
|
1353
|
+
async function runFont(parsed, signal, policy) {
|
|
1354
|
+
const action = parsed.positionals[0];
|
|
1355
|
+
const json = parsed.flags.has("json") === true;
|
|
1356
|
+
assertOperationAllowed(policy, "font");
|
|
1357
|
+
if (action === "search") {
|
|
1358
|
+
const query = parsed.positionals[1] ?? "";
|
|
1359
|
+
const category = flagString(parsed.flags, "category");
|
|
1360
|
+
const subset = flagString(parsed.flags, "subset");
|
|
1361
|
+
const styleValue = flagString(parsed.flags, "style");
|
|
1362
|
+
const style = styleValue === undefined ? undefined : parseFontStyle(styleValue);
|
|
1363
|
+
const variable = parsed.flags.has("variable") ? true : undefined;
|
|
1364
|
+
const limitValue = flagString(parsed.flags, "limit");
|
|
1365
|
+
const limit = limitValue === undefined ? undefined : Number(limitValue);
|
|
1366
|
+
const result = await searchFontsource({
|
|
1367
|
+
query,
|
|
1368
|
+
...(category !== undefined ? { category } : {}),
|
|
1369
|
+
...(subset !== undefined ? { subset } : {}),
|
|
1370
|
+
...(style !== undefined ? { style } : {}),
|
|
1371
|
+
...(variable !== undefined ? { variable } : {}),
|
|
1372
|
+
...(limit !== undefined ? { limit } : {}),
|
|
1373
|
+
signal,
|
|
1374
|
+
});
|
|
1375
|
+
if (json) {
|
|
1376
|
+
printJson({ ok: true, command: "font", action: "search", ...result });
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
process.stdout.write(`fontsource: ${result.results.length} result${result.results.length === 1 ? "" : "s"}${result.truncated ? " (truncated)" : ""}\n`);
|
|
1380
|
+
for (const item of result.results) {
|
|
1381
|
+
process.stdout.write(` ${item.id}: ${item.family} [${item.category}]${item.variable ? " variable" : ""}, weights ${item.weights.join(",")}, ${item.styles.join("/")}, subsets ${item.subsets.join(",")}, ${item.license}\n`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return 0;
|
|
1385
|
+
}
|
|
1386
|
+
if (action === "add") {
|
|
1387
|
+
assertPolicyTier(policy, "authoring", "font add");
|
|
1388
|
+
if (typeof parsed.flags.get("url") === "string" &&
|
|
1389
|
+
!parsed.flags.has("license") &&
|
|
1390
|
+
!parsed.flags.has("license-file")) {
|
|
1391
|
+
assertPolicyTier(policy, "full", "font add --url without --license");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (action === "remove") {
|
|
1395
|
+
assertPolicyTier(policy, "full", "font remove");
|
|
1396
|
+
}
|
|
1397
|
+
if (parsed.flags.has("force")) {
|
|
1398
|
+
assertOverwriteAllowed(policy, "force");
|
|
1399
|
+
}
|
|
1400
|
+
const project = await projectFromFlag(parsed.flags, { fonts: false });
|
|
1401
|
+
let manifest;
|
|
1402
|
+
try {
|
|
1403
|
+
manifest = await readFontManifest(project.root);
|
|
1404
|
+
}
|
|
1405
|
+
catch (error) {
|
|
1406
|
+
if (action === "verify" && isFrameyardError(error)) {
|
|
1407
|
+
const verification = {
|
|
1408
|
+
manifest: fontManifestPath(project.root),
|
|
1409
|
+
fonts: 0,
|
|
1410
|
+
files: 0,
|
|
1411
|
+
bytes: 0,
|
|
1412
|
+
diagnostics: [
|
|
1413
|
+
{
|
|
1414
|
+
severity: "error",
|
|
1415
|
+
code: error.code,
|
|
1416
|
+
message: error.message,
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
valid: false,
|
|
1420
|
+
};
|
|
1421
|
+
if (json) {
|
|
1422
|
+
printJson({ ok: true, command: "font", action: "verify", ...verification });
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
process.stderr.write(`✗ ${error.code}: ${error.message}\n`);
|
|
1426
|
+
}
|
|
1427
|
+
return 1;
|
|
1428
|
+
}
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
if (action === "add") {
|
|
1432
|
+
const file = flagString(parsed.flags, "file");
|
|
1433
|
+
const url = flagString(parsed.flags, "url");
|
|
1434
|
+
const family = flagString(parsed.flags, "family");
|
|
1435
|
+
const id = flagString(parsed.flags, "id");
|
|
1436
|
+
const weightValue = flagString(parsed.flags, "weight");
|
|
1437
|
+
const styleValue = flagString(parsed.flags, "style");
|
|
1438
|
+
const subsetValue = flagString(parsed.flags, "subset");
|
|
1439
|
+
const force = parsed.flags.has("force") === true;
|
|
1440
|
+
if (file !== undefined) {
|
|
1441
|
+
printFontAddResult(await addLocalFont({
|
|
1442
|
+
projectRoot: project.root,
|
|
1443
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
1444
|
+
file,
|
|
1445
|
+
family: family,
|
|
1446
|
+
...(id !== undefined ? { id } : {}),
|
|
1447
|
+
weight: weightValue === undefined ? 400 : parseFontWeight(weightValue),
|
|
1448
|
+
style: styleValue === undefined ? "normal" : parseFontStyle(styleValue),
|
|
1449
|
+
...(subsetValue !== undefined ? { subset: subsetValue } : {}),
|
|
1450
|
+
...(flagString(parsed.flags, "unicode-range") !== undefined
|
|
1451
|
+
? { unicodeRange: flagString(parsed.flags, "unicode-range") }
|
|
1452
|
+
: {}),
|
|
1453
|
+
...(flagString(parsed.flags, "license") !== undefined
|
|
1454
|
+
? { license: flagString(parsed.flags, "license") }
|
|
1455
|
+
: {}),
|
|
1456
|
+
...(flagString(parsed.flags, "license-file") !== undefined
|
|
1457
|
+
? { licenseFile: flagString(parsed.flags, "license-file") }
|
|
1458
|
+
: {}),
|
|
1459
|
+
force,
|
|
1460
|
+
}), json);
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
if (url !== undefined) {
|
|
1464
|
+
printFontAddResult(await addUrlFont({
|
|
1465
|
+
projectRoot: project.root,
|
|
1466
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
1467
|
+
url,
|
|
1468
|
+
family: family,
|
|
1469
|
+
...(id !== undefined ? { id } : {}),
|
|
1470
|
+
weight: weightValue === undefined ? 400 : parseFontWeight(weightValue),
|
|
1471
|
+
style: styleValue === undefined ? "normal" : parseFontStyle(styleValue),
|
|
1472
|
+
...(subsetValue !== undefined ? { subset: subsetValue } : {}),
|
|
1473
|
+
...(flagString(parsed.flags, "unicode-range") !== undefined
|
|
1474
|
+
? { unicodeRange: flagString(parsed.flags, "unicode-range") }
|
|
1475
|
+
: {}),
|
|
1476
|
+
...(flagString(parsed.flags, "license") !== undefined
|
|
1477
|
+
? { license: flagString(parsed.flags, "license") }
|
|
1478
|
+
: {}),
|
|
1479
|
+
...(flagString(parsed.flags, "license-file") !== undefined
|
|
1480
|
+
? { licenseFile: flagString(parsed.flags, "license-file") }
|
|
1481
|
+
: {}),
|
|
1482
|
+
...(flagString(parsed.flags, "sha256") !== undefined
|
|
1483
|
+
? { sha256: flagString(parsed.flags, "sha256") }
|
|
1484
|
+
: {}),
|
|
1485
|
+
force,
|
|
1486
|
+
signal,
|
|
1487
|
+
}), json);
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
printFontAddResult(await addFontsource({
|
|
1491
|
+
projectRoot: project.root,
|
|
1492
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
1493
|
+
id: parsed.positionals[1],
|
|
1494
|
+
...(family !== undefined ? { family } : {}),
|
|
1495
|
+
...(subsetValue !== undefined ? { subsets: splitFontList(subsetValue, "subset") } : {}),
|
|
1496
|
+
...(styleValue !== undefined ? { styles: parseFontsourceStyles(styleValue) } : {}),
|
|
1497
|
+
...(weightValue !== undefined ? { weights: parseFontsourceWeights(weightValue) } : {}),
|
|
1498
|
+
static: parsed.flags.has("static") === true,
|
|
1499
|
+
force,
|
|
1500
|
+
signal,
|
|
1501
|
+
}), json);
|
|
1502
|
+
return 0;
|
|
1503
|
+
}
|
|
1504
|
+
if (action === "list") {
|
|
1505
|
+
const verification = manifest === undefined
|
|
1506
|
+
? undefined
|
|
1507
|
+
: await verifyFontManifest({
|
|
1508
|
+
projectRoot: project.root,
|
|
1509
|
+
manifest,
|
|
1510
|
+
hashes: false,
|
|
1511
|
+
licenses: false,
|
|
1512
|
+
orphans: false,
|
|
1513
|
+
});
|
|
1514
|
+
const statuses = verification === undefined ? new Map() : fontStatuses(verification);
|
|
1515
|
+
const fonts = (manifest?.fonts ?? [])
|
|
1516
|
+
.map((font) => summarizeFont(font, statuses.get(font.id) ?? "ok"))
|
|
1517
|
+
.sort((left, right) => left.family.localeCompare(right.family));
|
|
1518
|
+
const result = {
|
|
1519
|
+
ok: true,
|
|
1520
|
+
command: "font",
|
|
1521
|
+
action: "list",
|
|
1522
|
+
manifest: fontManifestPath(project.root),
|
|
1523
|
+
fonts,
|
|
1524
|
+
};
|
|
1525
|
+
if (json) {
|
|
1526
|
+
printJson(result);
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
if (fonts.length === 0) {
|
|
1530
|
+
process.stdout.write("No project fonts are installed.\n");
|
|
1531
|
+
}
|
|
1532
|
+
else {
|
|
1533
|
+
process.stdout.write(`fonts: ${fonts.length}\n`);
|
|
1534
|
+
for (const font of fonts) {
|
|
1535
|
+
const subsets = font.subsets.length === 0 ? "no subset" : font.subsets.join(", ");
|
|
1536
|
+
process.stdout.write(` ${font.id}: ${font.family} (${font.provider}${font.version ? ` ${font.version}` : ""}), ${subsets}, ${font.styles.join("/")}, ${font.license}, ${font.files} files, ${font.bytes} bytes, ${font.status}\n`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return 0;
|
|
1541
|
+
}
|
|
1542
|
+
if (action === "verify") {
|
|
1543
|
+
const verification = await verifyFontManifest({
|
|
1544
|
+
projectRoot: project.root,
|
|
1545
|
+
manifest: manifest ?? emptyFontManifest(),
|
|
1546
|
+
hashes: true,
|
|
1547
|
+
licenses: true,
|
|
1548
|
+
orphans: true,
|
|
1549
|
+
});
|
|
1550
|
+
if (json) {
|
|
1551
|
+
printJson({ ok: true, command: "font", action: "verify", ...verification });
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
if (verification.fonts === 0) {
|
|
1555
|
+
process.stdout.write("No project fonts are installed.\n");
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
process.stdout.write(`${verification.valid ? "✓" : "✗"} font manifest: ${verification.fonts} fonts, ${verification.files} files, ${verification.bytes} bytes\n`);
|
|
1559
|
+
process.stdout.write(` ${verification.manifest}\n`);
|
|
1560
|
+
}
|
|
1561
|
+
for (const diagnostic of verification.diagnostics) {
|
|
1562
|
+
const marker = diagnostic.severity === "error" ? "✗" : "⚠";
|
|
1563
|
+
process.stderr.write(`${marker} ${diagnostic.code}: ${diagnostic.message}${diagnostic.file !== undefined ? ` (${diagnostic.file})` : ""}\n`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return verification.valid ? 0 : 1;
|
|
1567
|
+
}
|
|
1568
|
+
if (action === "remove") {
|
|
1569
|
+
const result = await removeFont({
|
|
1570
|
+
projectRoot: project.root,
|
|
1571
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
1572
|
+
id: parsed.positionals[1],
|
|
1573
|
+
keepFiles: parsed.flags.has("keep-files") === true,
|
|
1574
|
+
});
|
|
1575
|
+
if (json) {
|
|
1576
|
+
printJson({ ok: true, command: "font", action: "remove", ...result });
|
|
1577
|
+
}
|
|
1578
|
+
else {
|
|
1579
|
+
process.stdout.write(`Removed font ${result.id} (${result.family}).\n`);
|
|
1580
|
+
if (result.keptFiles) {
|
|
1581
|
+
process.stdout.write(` kept files: ${result.files.join(", ")}\n`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return 0;
|
|
1585
|
+
}
|
|
1586
|
+
usage(`Unknown font action "${action}". Use add, list, verify, or remove.`);
|
|
1587
|
+
}
|
|
1588
|
+
async function runLint(parsed, signal, policy) {
|
|
1589
|
+
const project = await projectFromFlag(parsed.flags);
|
|
1590
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: false });
|
|
1591
|
+
const props = await readPropsFile(parsed.flags);
|
|
1592
|
+
const targetId = parsed.positionals[0];
|
|
1593
|
+
const json = parsed.flags.has("json") === true;
|
|
1594
|
+
if (project.timelines.has(targetId)) {
|
|
1595
|
+
if (props !== undefined && !isPlainObject(props)) {
|
|
1596
|
+
usage("Timeline --props must be a JSON object keyed by composition clip id.");
|
|
1597
|
+
}
|
|
1598
|
+
const result = await lintTimeline({
|
|
1599
|
+
project,
|
|
1600
|
+
timelineId: targetId,
|
|
1601
|
+
...(props !== undefined ? { propsOverrides: props } : {}),
|
|
1602
|
+
signal,
|
|
1603
|
+
});
|
|
1604
|
+
printDiagnostics(result.diagnostics);
|
|
1605
|
+
if (json) {
|
|
1606
|
+
printJson({ ok: true, command: "lint", policy: policyDetails(policy), ...result });
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
process.stdout.write(`✓ lint timeline ${targetId}: ${result.framesProjected} frames projected, ${result.segments} segment${result.segments === 1 ? "" : "s"}, ${result.tracks} track${result.tracks === 1 ? "" : "s"}, ${result.clips} clip${result.clips === 1 ? "" : "s"}\n`);
|
|
1610
|
+
process.stdout.write(` policy: ${describePolicy(policy)}; lint does not replace check (props hooks and frame runtime are not evaluated)\n`);
|
|
1611
|
+
}
|
|
1612
|
+
return 0;
|
|
1613
|
+
}
|
|
1614
|
+
const compositionId = targetId;
|
|
1615
|
+
const result = await lintComposition({
|
|
1616
|
+
project,
|
|
1617
|
+
compositionId,
|
|
1618
|
+
props,
|
|
1619
|
+
signal,
|
|
1620
|
+
});
|
|
1621
|
+
printDiagnostics(result.diagnostics);
|
|
1622
|
+
if (json) {
|
|
1623
|
+
printJson({ ok: true, command: "lint", policy: policyDetails(policy), ...result });
|
|
1624
|
+
}
|
|
1625
|
+
else if (result.metadata.static === true) {
|
|
1626
|
+
process.stdout.write(`✓ lint ${compositionId}: static ${result.metadata.width}×${result.metadata.height}; controller and document validated\n`);
|
|
1627
|
+
process.stdout.write(` policy: ${describePolicy(policy)}; lint does not replace check\n`);
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
process.stdout.write(`✓ lint ${compositionId}: document and controller validated for ${result.metadata.durationInFrames} frames; frame runtime is not evaluated\n`);
|
|
1631
|
+
process.stdout.write(` policy: ${describePolicy(policy)}; use check for props hooks and every frame\n`);
|
|
1632
|
+
}
|
|
1633
|
+
return 0;
|
|
1634
|
+
}
|
|
1635
|
+
async function runCheck(parsed, signal, policy) {
|
|
1636
|
+
assertOperationAllowed(policy, "check");
|
|
1637
|
+
const profile = resolveProfile(policy, flagString(parsed.flags, "profile"));
|
|
1638
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
1639
|
+
lowerProcessPriority();
|
|
1640
|
+
}
|
|
1641
|
+
const project = await projectFromFlag(parsed.flags);
|
|
1642
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: true });
|
|
524
1643
|
const props = await readPropsFile(parsed.flags);
|
|
525
1644
|
const targetId = parsed.positionals[0];
|
|
526
1645
|
const json = parsed.flags.has("json") === true;
|
|
@@ -540,7 +1659,7 @@ async function runCheck(parsed, signal) {
|
|
|
540
1659
|
printJson({ ok: true, command: "check", ...result });
|
|
541
1660
|
}
|
|
542
1661
|
else {
|
|
543
|
-
process.stdout.write(`✓ timeline ${targetId}: ${result.framesChecked} frames, ${result.format.width}×${result.format.height} @ ${result.format.fps} fps (${formatDuration(result.durationInFrames / result.format.fps)}), ${result.tracks} track${result.tracks === 1 ? "" : "s"}, ${result.clips} clip${result.clips === 1 ? "" : "s"}\n`);
|
|
1662
|
+
process.stdout.write(`✓ timeline ${targetId}: ${result.framesChecked} frames, ${result.format.width}×${result.format.height} @ ${result.format.fps} fps (${formatDuration(result.durationInFrames / result.format.fps)}), ${result.tracks} track${result.tracks === 1 ? "" : "s"}, ${result.clips} clip${result.clips === 1 ? "" : "s"}, ${result.transitions} transition${result.transitions === 1 ? "" : "s"}\n`);
|
|
544
1663
|
process.stdout.write(` audio: ${result.audio.clips} clip${result.audio.clips === 1 ? "" : "s"}\n`);
|
|
545
1664
|
if (result.transcripts.clips > 0) {
|
|
546
1665
|
process.stdout.write(` transcripts: ${result.transcripts.clips} asset${result.transcripts.clips === 1 ? "" : "s"}\n`);
|
|
@@ -559,6 +1678,9 @@ async function runCheck(parsed, signal) {
|
|
|
559
1678
|
if (json) {
|
|
560
1679
|
printJson({ ok: true, command: "check", ...result });
|
|
561
1680
|
}
|
|
1681
|
+
else if (result.kind === "static") {
|
|
1682
|
+
process.stdout.write(`✓ ${compositionId}: static ${result.metadata.width}×${result.metadata.height}\n`);
|
|
1683
|
+
}
|
|
562
1684
|
else {
|
|
563
1685
|
const meta = result.metadata;
|
|
564
1686
|
process.stdout.write(`✓ ${compositionId}: ${result.framesChecked} frames, ${meta.width}×${meta.height} @ ${meta.fps} fps (${formatDuration(meta.durationInFrames / meta.fps)})\n`);
|
|
@@ -695,8 +1817,17 @@ function printStillResult(result, targetId, timeline, json) {
|
|
|
695
1817
|
process.stdout.write(`Wrote ${result.frames.length} frames of ${target} to ${directory} (${result.width}×${result.height})\n`);
|
|
696
1818
|
}
|
|
697
1819
|
}
|
|
698
|
-
async function runStill(parsed, signal) {
|
|
1820
|
+
async function runStill(parsed, signal, policy) {
|
|
1821
|
+
assertOperationAllowed(policy, "still");
|
|
1822
|
+
if (parsed.flags.has("overwrite")) {
|
|
1823
|
+
assertOverwriteAllowed(policy, "overwrite");
|
|
1824
|
+
}
|
|
1825
|
+
const profile = resolveProfile(policy, flagString(parsed.flags, "profile"));
|
|
1826
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
1827
|
+
lowerProcessPriority();
|
|
1828
|
+
}
|
|
699
1829
|
const project = await projectFromFlag(parsed.flags);
|
|
1830
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: true });
|
|
700
1831
|
const props = await readPropsFile(parsed.flags);
|
|
701
1832
|
const targetId = parsed.positionals[0];
|
|
702
1833
|
const frameValue = parsed.flags.get("frame");
|
|
@@ -704,6 +1835,7 @@ async function runStill(parsed, signal) {
|
|
|
704
1835
|
usage('"still" requires --frame <spec>.');
|
|
705
1836
|
}
|
|
706
1837
|
const frames = parseFrameSpec(frameValue);
|
|
1838
|
+
assertStillFrameLimit(policy, frames.length);
|
|
707
1839
|
const outputPlan = await resolveStillOutputs({
|
|
708
1840
|
targetId,
|
|
709
1841
|
frames,
|
|
@@ -748,13 +1880,24 @@ async function runStill(parsed, signal) {
|
|
|
748
1880
|
throw error;
|
|
749
1881
|
}
|
|
750
1882
|
}
|
|
751
|
-
async function runRender(parsed, signal) {
|
|
1883
|
+
async function runRender(parsed, signal, policy) {
|
|
1884
|
+
const dryRun = parsed.flags.has("dry-run") === true;
|
|
1885
|
+
const profile = resolveProfile(policy, flagString(parsed.flags, "profile"));
|
|
1886
|
+
if (!dryRun) {
|
|
1887
|
+
assertOperationAllowed(policy, "render");
|
|
1888
|
+
}
|
|
1889
|
+
if (parsed.flags.has("overwrite")) {
|
|
1890
|
+
assertOverwriteAllowed(policy, "overwrite");
|
|
1891
|
+
}
|
|
752
1892
|
const project = await projectFromFlag(parsed.flags);
|
|
1893
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: true });
|
|
753
1894
|
const props = await readPropsFile(parsed.flags);
|
|
754
1895
|
const encoder = encoderChoice(parsed.flags);
|
|
755
|
-
const
|
|
1896
|
+
const containerValue = containerChoice(parsed.flags);
|
|
1897
|
+
const container = containerValue ?? "mp4";
|
|
756
1898
|
// WebM is a VP9/Opus container; every other container defaults to H.264.
|
|
757
|
-
const
|
|
1899
|
+
const codecValue = codecChoice(parsed.flags);
|
|
1900
|
+
const codec = codecValue ?? defaultCodecForContainer(container);
|
|
758
1901
|
assertContainerCodec(container, codec);
|
|
759
1902
|
if (encoder !== undefined && encoder !== "auto") {
|
|
760
1903
|
// Rejects codec/vendor mismatches such as --codec hevc --encoder libx264.
|
|
@@ -762,9 +1905,8 @@ async function runRender(parsed, signal) {
|
|
|
762
1905
|
}
|
|
763
1906
|
const targetId = parsed.positionals[0];
|
|
764
1907
|
const outValue = flagString(parsed.flags, "out");
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
: path.resolve(process.cwd(), outValue);
|
|
1908
|
+
const explicitOutPath = outValue === undefined ? undefined : path.resolve(process.cwd(), outValue);
|
|
1909
|
+
const timelineOutPath = explicitOutPath ?? path.join(project.root, "out", `${targetId}.${CONTAINER_EXTENSION[container]}`);
|
|
768
1910
|
const json = parsed.flags.has("json") === true;
|
|
769
1911
|
if (project.timelines.has(targetId)) {
|
|
770
1912
|
if (parsed.flags.has("concurrency")) {
|
|
@@ -783,11 +1925,113 @@ async function runRender(parsed, signal) {
|
|
|
783
1925
|
}
|
|
784
1926
|
const scratchValue = flagString(parsed.flags, "scratch");
|
|
785
1927
|
const scratchDir = scratchValue === undefined ? undefined : path.resolve(process.cwd(), scratchValue);
|
|
1928
|
+
let effectiveWorkers = workers;
|
|
1929
|
+
if (profile === "shared-cpu") {
|
|
1930
|
+
if (workers !== undefined) {
|
|
1931
|
+
assertProfileLimit(profile, "workers", workers, SHARED_CPU_MAX_WORKERS);
|
|
1932
|
+
}
|
|
1933
|
+
effectiveWorkers = workers ?? SHARED_CPU_MAX_WORKERS;
|
|
1934
|
+
}
|
|
1935
|
+
if (dryRun) {
|
|
1936
|
+
const projectTimeline = project.timelines.get(targetId);
|
|
1937
|
+
if (!projectTimeline) {
|
|
1938
|
+
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${targetId}" is not listed in fy.config.js.`);
|
|
1939
|
+
}
|
|
1940
|
+
const resolved = await resolveProjectTimeline({
|
|
1941
|
+
project,
|
|
1942
|
+
timeline: projectTimeline,
|
|
1943
|
+
...(props !== undefined ? { propsOverrides: props } : {}),
|
|
1944
|
+
signal,
|
|
1945
|
+
});
|
|
1946
|
+
const firstError = resolved.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
1947
|
+
if (!resolved.timeline || firstError) {
|
|
1948
|
+
throw new FrameyardError(firstError?.code ?? "TIMELINE_INVALID", firstError?.message ?? "Timeline resolution failed.", { diagnostics: resolved.diagnostics });
|
|
1949
|
+
}
|
|
1950
|
+
const lintResult = await lintResolvedTimeline({
|
|
1951
|
+
timeline: resolved.timeline,
|
|
1952
|
+
projectRoot: project.root,
|
|
1953
|
+
diagnostics: resolved.diagnostics,
|
|
1954
|
+
signal,
|
|
1955
|
+
});
|
|
1956
|
+
const lintError = lintResult.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
1957
|
+
if (lintError) {
|
|
1958
|
+
throw new FrameyardError(lintError.code, lintError.message, { diagnostics: lintResult.diagnostics });
|
|
1959
|
+
}
|
|
1960
|
+
const plan = planTimelineRender(resolved.timeline);
|
|
1961
|
+
const planWorkers = effectiveWorkers ?? 8;
|
|
1962
|
+
const planDetails = {
|
|
1963
|
+
kind: "video",
|
|
1964
|
+
frames: resolved.timeline.durationInFrames,
|
|
1965
|
+
durationSeconds: resolved.timeline.durationInFrames / resolved.timeline.format.fps,
|
|
1966
|
+
segments: plan.segments.length,
|
|
1967
|
+
workers: planWorkers,
|
|
1968
|
+
encoder: encoder ?? "auto",
|
|
1969
|
+
codec,
|
|
1970
|
+
container,
|
|
1971
|
+
probed: false,
|
|
1972
|
+
...(scratchDir !== undefined ? { scratch: scratchDir } : {}),
|
|
1973
|
+
};
|
|
1974
|
+
const outputExists = await pathExists(timelineOutPath);
|
|
1975
|
+
const blockers = outputExists
|
|
1976
|
+
? [
|
|
1977
|
+
{
|
|
1978
|
+
code: "OUTPUT_EXISTS",
|
|
1979
|
+
message: `Output already exists: ${timelineOutPath}.`,
|
|
1980
|
+
suggestion: policy.allowOverwrite
|
|
1981
|
+
? "Pass --overwrite to replace it."
|
|
1982
|
+
: "Choose a new --out path or ask the user to run with FRAMEYARD_POLICY=full.",
|
|
1983
|
+
},
|
|
1984
|
+
]
|
|
1985
|
+
: [];
|
|
1986
|
+
const prerequisites = collectPrerequisites(resolved.timeline);
|
|
1987
|
+
const args = ["render", targetId];
|
|
1988
|
+
if (outValue !== undefined)
|
|
1989
|
+
args.push("--out", outValue);
|
|
1990
|
+
const propsValue = flagString(parsed.flags, "props");
|
|
1991
|
+
if (propsValue !== undefined)
|
|
1992
|
+
args.push("--props", propsValue);
|
|
1993
|
+
args.push("--workers", String(planWorkers));
|
|
1994
|
+
if (encoder !== undefined)
|
|
1995
|
+
args.push("--encoder", encoder);
|
|
1996
|
+
args.push("--codec", codec, "--container", container);
|
|
1997
|
+
if (scratchValue !== undefined)
|
|
1998
|
+
args.push("--scratch", scratchValue);
|
|
1999
|
+
if (profile !== undefined)
|
|
2000
|
+
args.push("--profile", profile);
|
|
2001
|
+
if (parsed.flags.has("low-priority"))
|
|
2002
|
+
args.push("--low-priority");
|
|
2003
|
+
const prefix = policy.tier === "full" ? "" : "FRAMEYARD_POLICY=full ";
|
|
2004
|
+
const commandLine = prefix + handoffCommand(args);
|
|
2005
|
+
printDiagnostics(lintResult.diagnostics);
|
|
2006
|
+
if (json) {
|
|
2007
|
+
printJson({
|
|
2008
|
+
ok: true,
|
|
2009
|
+
command: "render",
|
|
2010
|
+
dryRun: true,
|
|
2011
|
+
deferred: true,
|
|
2012
|
+
policy: policyDetails(policy),
|
|
2013
|
+
...(profile !== undefined ? { profile } : {}),
|
|
2014
|
+
plan: planDetails,
|
|
2015
|
+
commandLine,
|
|
2016
|
+
prerequisites,
|
|
2017
|
+
blockers,
|
|
2018
|
+
diagnostics: lintResult.diagnostics,
|
|
2019
|
+
output: { path: timelineOutPath, exists: outputExists },
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
printDryRun("render", planDetails, commandLine, prerequisites, blockers);
|
|
2024
|
+
}
|
|
2025
|
+
return 0;
|
|
2026
|
+
}
|
|
2027
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
2028
|
+
lowerProcessPriority();
|
|
2029
|
+
}
|
|
786
2030
|
const result = await renderTimeline({
|
|
787
2031
|
project,
|
|
788
2032
|
timelineId: targetId,
|
|
789
|
-
outPath,
|
|
790
|
-
...(
|
|
2033
|
+
outPath: timelineOutPath,
|
|
2034
|
+
...(effectiveWorkers !== undefined ? { workers: effectiveWorkers } : {}),
|
|
791
2035
|
...(scratchDir !== undefined ? { scratchDir } : {}),
|
|
792
2036
|
...(encoder !== undefined ? { encoder } : {}),
|
|
793
2037
|
codec,
|
|
@@ -811,16 +2055,111 @@ async function runRender(parsed, signal) {
|
|
|
811
2055
|
}
|
|
812
2056
|
const compositionId = targetId;
|
|
813
2057
|
const concurrencyValue = parsed.flags.get("concurrency");
|
|
814
|
-
const concurrency =
|
|
2058
|
+
const concurrency = typeof concurrencyValue === "string" ? validateRenderConcurrency(Number(concurrencyValue)) : undefined;
|
|
2059
|
+
let effectiveConcurrency = concurrency;
|
|
2060
|
+
if (profile === "shared-cpu") {
|
|
2061
|
+
if (concurrency !== undefined) {
|
|
2062
|
+
assertProfileLimit(profile, "concurrency", concurrency, SHARED_CPU_MAX_CONCURRENCY);
|
|
2063
|
+
}
|
|
2064
|
+
effectiveConcurrency = concurrency ?? SHARED_CPU_MAX_CONCURRENCY;
|
|
2065
|
+
}
|
|
2066
|
+
if (dryRun) {
|
|
2067
|
+
const lintResult = await lintComposition({ project, compositionId, props, signal });
|
|
2068
|
+
const metadata = lintResult.metadata;
|
|
2069
|
+
const staticTarget = metadata.static === true;
|
|
2070
|
+
if (staticTarget &&
|
|
2071
|
+
(parsed.flags.has("concurrency") ||
|
|
2072
|
+
parsed.flags.has("container") ||
|
|
2073
|
+
parsed.flags.has("codec") ||
|
|
2074
|
+
parsed.flags.has("encoder"))) {
|
|
2075
|
+
usage("A static composition render rejects --concurrency, --container, --codec, and --encoder.");
|
|
2076
|
+
}
|
|
2077
|
+
const compositionOutPath = explicitOutPath ??
|
|
2078
|
+
path.join(project.root, "out", staticTarget ? `${compositionId}.png` : `${compositionId}.${CONTAINER_EXTENSION[container]}`);
|
|
2079
|
+
const outputExists = await pathExists(compositionOutPath);
|
|
2080
|
+
const blockers = outputExists
|
|
2081
|
+
? [
|
|
2082
|
+
{
|
|
2083
|
+
code: "OUTPUT_EXISTS",
|
|
2084
|
+
message: `Output already exists: ${compositionOutPath}.`,
|
|
2085
|
+
suggestion: policy.allowOverwrite
|
|
2086
|
+
? "Pass --overwrite to replace it."
|
|
2087
|
+
: "Choose a new --out path or ask the user to run with FRAMEYARD_POLICY=full.",
|
|
2088
|
+
},
|
|
2089
|
+
]
|
|
2090
|
+
: [];
|
|
2091
|
+
const planDetails = metadata.static === true
|
|
2092
|
+
? {
|
|
2093
|
+
kind: "image",
|
|
2094
|
+
width: metadata.width,
|
|
2095
|
+
height: metadata.height,
|
|
2096
|
+
frames: 1,
|
|
2097
|
+
}
|
|
2098
|
+
: {
|
|
2099
|
+
kind: "video",
|
|
2100
|
+
frames: metadata.durationInFrames,
|
|
2101
|
+
durationSeconds: metadata.durationInFrames / metadata.fps,
|
|
2102
|
+
concurrency: effectiveConcurrency ?? 1,
|
|
2103
|
+
encoder: encoder ?? "auto",
|
|
2104
|
+
codec,
|
|
2105
|
+
container,
|
|
2106
|
+
probed: false,
|
|
2107
|
+
};
|
|
2108
|
+
const args = ["render", compositionId];
|
|
2109
|
+
if (outValue !== undefined)
|
|
2110
|
+
args.push("--out", outValue);
|
|
2111
|
+
const propsValue = flagString(parsed.flags, "props");
|
|
2112
|
+
if (propsValue !== undefined)
|
|
2113
|
+
args.push("--props", propsValue);
|
|
2114
|
+
if (!staticTarget) {
|
|
2115
|
+
args.push("--concurrency", String(effectiveConcurrency ?? 1));
|
|
2116
|
+
if (encoder !== undefined)
|
|
2117
|
+
args.push("--encoder", encoder);
|
|
2118
|
+
if (codecValue !== undefined)
|
|
2119
|
+
args.push("--codec", codecValue);
|
|
2120
|
+
if (containerValue !== undefined)
|
|
2121
|
+
args.push("--container", containerValue);
|
|
2122
|
+
}
|
|
2123
|
+
if (profile !== undefined)
|
|
2124
|
+
args.push("--profile", profile);
|
|
2125
|
+
if (parsed.flags.has("low-priority"))
|
|
2126
|
+
args.push("--low-priority");
|
|
2127
|
+
const prefix = policy.tier === "full" ? "" : "FRAMEYARD_POLICY=full ";
|
|
2128
|
+
const commandLine = prefix + handoffCommand(args);
|
|
2129
|
+
printDiagnostics(lintResult.diagnostics);
|
|
2130
|
+
if (json) {
|
|
2131
|
+
printJson({
|
|
2132
|
+
ok: true,
|
|
2133
|
+
command: "render",
|
|
2134
|
+
dryRun: true,
|
|
2135
|
+
deferred: true,
|
|
2136
|
+
policy: policyDetails(policy),
|
|
2137
|
+
...(profile !== undefined ? { profile } : {}),
|
|
2138
|
+
plan: planDetails,
|
|
2139
|
+
commandLine,
|
|
2140
|
+
prerequisites: [],
|
|
2141
|
+
blockers,
|
|
2142
|
+
diagnostics: lintResult.diagnostics,
|
|
2143
|
+
output: { path: compositionOutPath, exists: outputExists },
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
else {
|
|
2147
|
+
printDryRun("render", planDetails, commandLine, [], blockers);
|
|
2148
|
+
}
|
|
2149
|
+
return 0;
|
|
2150
|
+
}
|
|
2151
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
2152
|
+
lowerProcessPriority();
|
|
2153
|
+
}
|
|
815
2154
|
const result = await renderComposition({
|
|
816
2155
|
project,
|
|
817
2156
|
compositionId,
|
|
818
2157
|
props,
|
|
819
|
-
outPath,
|
|
820
|
-
concurrency,
|
|
2158
|
+
...(explicitOutPath !== undefined ? { outPath: explicitOutPath } : {}),
|
|
2159
|
+
...(concurrency !== undefined ? { concurrency } : {}),
|
|
821
2160
|
...(encoder !== undefined ? { encoder } : {}),
|
|
822
|
-
codec,
|
|
823
|
-
container,
|
|
2161
|
+
...(codecValue !== undefined ? { codec: codecValue } : {}),
|
|
2162
|
+
...(containerValue !== undefined ? { container: containerValue } : {}),
|
|
824
2163
|
overwrite: parsed.flags.has("overwrite") === true,
|
|
825
2164
|
signal,
|
|
826
2165
|
onProgress: progressPrinter("Rendering"),
|
|
@@ -829,20 +2168,30 @@ async function runRender(parsed, signal) {
|
|
|
829
2168
|
if (json) {
|
|
830
2169
|
printJson({ ok: true, command: "render", ...result });
|
|
831
2170
|
}
|
|
2171
|
+
else if (result.kind === "image") {
|
|
2172
|
+
process.stdout.write(`Rendered ${compositionId} to ${result.path} (${result.width}×${result.height}, ${result.bytes} bytes)\n`);
|
|
2173
|
+
}
|
|
832
2174
|
else {
|
|
833
2175
|
process.stdout.write(`Rendered ${compositionId} to ${result.path} (${result.framesRendered} frames, ${formatDuration(result.durationSeconds)}, ${result.audio.clips} audio clip${result.audio.clips === 1 ? "" : "s"}, ${result.encoder} encoder)\n`);
|
|
834
2176
|
}
|
|
835
2177
|
return 0;
|
|
836
2178
|
}
|
|
837
|
-
async function runTts(parsed, signal) {
|
|
2179
|
+
async function runTts(parsed, signal, policy) {
|
|
2180
|
+
const dryRun = parsed.flags.has("dry-run") === true;
|
|
2181
|
+
const profile = resolveProfile(policy, flagString(parsed.flags, "profile"));
|
|
2182
|
+
if (!dryRun) {
|
|
2183
|
+
assertOperationAllowed(policy, "tts");
|
|
2184
|
+
}
|
|
2185
|
+
if (parsed.flags.has("force")) {
|
|
2186
|
+
assertOverwriteAllowed(policy, "force");
|
|
2187
|
+
}
|
|
838
2188
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
839
2189
|
const json = parsed.flags.has("json") === true;
|
|
2190
|
+
const engine = (flagString(parsed.flags, "engine") ?? "qwen");
|
|
840
2191
|
const speaker = flagString(parsed.flags, "speaker");
|
|
841
|
-
const talkerModel = flagString(parsed.flags, "talker-model");
|
|
842
|
-
const codecModel = flagString(parsed.flags, "codec-model");
|
|
843
2192
|
const outValue = flagString(parsed.flags, "out");
|
|
844
|
-
if (!speaker || !
|
|
845
|
-
usage('"tts" requires --speaker
|
|
2193
|
+
if (!speaker || !outValue) {
|
|
2194
|
+
usage('"tts" requires --speaker and --out.');
|
|
846
2195
|
}
|
|
847
2196
|
const textFile = flagString(parsed.flags, "text-file");
|
|
848
2197
|
let text;
|
|
@@ -861,39 +2210,157 @@ async function runTts(parsed, signal) {
|
|
|
861
2210
|
}
|
|
862
2211
|
text = await readStdin();
|
|
863
2212
|
}
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
?
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
2213
|
+
const outPath = path.resolve(process.cwd(), outValue);
|
|
2214
|
+
const force = parsed.flags.has("force") === true;
|
|
2215
|
+
const manifestPath = resolveManifestPath(projectRoot, flagString(parsed.flags, "manifest"));
|
|
2216
|
+
if (dryRun) {
|
|
2217
|
+
if (text.trim().length === 0) {
|
|
2218
|
+
throw new FrameyardError("TTS_FAILED", "Speech synthesis requires non-empty text.");
|
|
2219
|
+
}
|
|
2220
|
+
if (!outPath.toLowerCase().endsWith(".wav")) {
|
|
2221
|
+
throw new FrameyardError("TTS_FAILED", `Speech output must use a .wav extension: ${outPath}`);
|
|
2222
|
+
}
|
|
2223
|
+
const executableValue = flagString(parsed.flags, "executable");
|
|
2224
|
+
const prerequisites = engine === "supertonic"
|
|
2225
|
+
? [
|
|
2226
|
+
{
|
|
2227
|
+
label: "Supertonic model directory",
|
|
2228
|
+
path: path.resolve(process.cwd(), flagString(parsed.flags, "model-dir")),
|
|
2229
|
+
directory: true,
|
|
2230
|
+
},
|
|
2231
|
+
...(executableValue !== undefined ? [{ label: "TTS executable", path: executableValue }] : []),
|
|
2232
|
+
]
|
|
2233
|
+
: [
|
|
2234
|
+
{
|
|
2235
|
+
label: "talker model",
|
|
2236
|
+
path: path.resolve(process.cwd(), flagString(parsed.flags, "talker-model")),
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
label: "codec model",
|
|
2240
|
+
path: path.resolve(process.cwd(), flagString(parsed.flags, "codec-model")),
|
|
2241
|
+
},
|
|
2242
|
+
...(executableValue !== undefined ? [{ label: "TTS executable", path: executableValue }] : []),
|
|
2243
|
+
];
|
|
2244
|
+
const blockers = await prerequisiteBlockers(prerequisites);
|
|
2245
|
+
if (engine === "supertonic") {
|
|
2246
|
+
try {
|
|
2247
|
+
await resolveSupertonicModelFiles(path.resolve(process.cwd(), flagString(parsed.flags, "model-dir")));
|
|
2248
|
+
}
|
|
2249
|
+
catch (error) {
|
|
2250
|
+
blockers.push({
|
|
2251
|
+
code: isFrameyardError(error) ? error.code : "TTS_FAILED",
|
|
2252
|
+
message: errorMessage(error),
|
|
2253
|
+
suggestion: "Check --model-dir and the seven Supertonic model files.",
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
const outputExists = await pathExists(outPath);
|
|
2258
|
+
if (outputExists) {
|
|
2259
|
+
blockers.push({
|
|
2260
|
+
code: "OUTPUT_EXISTS",
|
|
2261
|
+
message: `Output already exists: ${outPath}.`,
|
|
2262
|
+
suggestion: policy.allowOverwrite
|
|
2263
|
+
? "Pass --force to replace it."
|
|
2264
|
+
: "Choose a new --out path or ask the user to run with FRAMEYARD_POLICY=full.",
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
const plan = {
|
|
2268
|
+
kind: "tts",
|
|
2269
|
+
engine,
|
|
2270
|
+
speaker,
|
|
2271
|
+
textCharacters: text.length,
|
|
2272
|
+
out: outPath,
|
|
2273
|
+
manifest: manifestPath,
|
|
2274
|
+
probed: false,
|
|
2275
|
+
};
|
|
2276
|
+
const args = process.argv.slice(2).filter((argument) => argument !== "--dry-run" && argument !== "--json");
|
|
2277
|
+
const prefix = policy.tier === "full" ? "" : "FRAMEYARD_POLICY=full ";
|
|
2278
|
+
const commandLine = prefix + handoffCommand(args);
|
|
2279
|
+
if (json) {
|
|
2280
|
+
printJson({
|
|
2281
|
+
ok: true,
|
|
2282
|
+
command: "tts",
|
|
2283
|
+
dryRun: true,
|
|
2284
|
+
deferred: true,
|
|
2285
|
+
policy: policyDetails(policy),
|
|
2286
|
+
...(profile !== undefined ? { profile } : {}),
|
|
2287
|
+
plan,
|
|
2288
|
+
commandLine,
|
|
2289
|
+
prerequisites: prerequisites.map((item) => item.path),
|
|
2290
|
+
blockers,
|
|
2291
|
+
output: { path: outPath, exists: outputExists },
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
else {
|
|
2295
|
+
printDryRun("tts", plan, commandLine, prerequisites.map((item) => item.path), blockers);
|
|
2296
|
+
}
|
|
2297
|
+
return 0;
|
|
2298
|
+
}
|
|
2299
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
2300
|
+
lowerProcessPriority();
|
|
2301
|
+
}
|
|
2302
|
+
const result = engine === "supertonic"
|
|
2303
|
+
? await synthesizeSpeech({
|
|
2304
|
+
engine: "supertonic",
|
|
2305
|
+
projectRoot,
|
|
2306
|
+
text,
|
|
2307
|
+
speaker,
|
|
2308
|
+
modelDir: path.resolve(process.cwd(), flagString(parsed.flags, "model-dir")),
|
|
2309
|
+
language: flagString(parsed.flags, "language"),
|
|
2310
|
+
outPath,
|
|
2311
|
+
...(flagString(parsed.flags, "executable") !== undefined
|
|
2312
|
+
? { executable: flagString(parsed.flags, "executable") }
|
|
2313
|
+
: {}),
|
|
2314
|
+
...(flagString(parsed.flags, "manifest") !== undefined
|
|
2315
|
+
? { manifestPath: flagString(parsed.flags, "manifest") }
|
|
2316
|
+
: {}),
|
|
2317
|
+
force,
|
|
2318
|
+
signal,
|
|
2319
|
+
})
|
|
2320
|
+
: await synthesizeSpeech({
|
|
2321
|
+
engine: "qwen",
|
|
2322
|
+
projectRoot,
|
|
2323
|
+
text,
|
|
2324
|
+
speaker,
|
|
2325
|
+
talkerModelPath: path.resolve(process.cwd(), flagString(parsed.flags, "talker-model")),
|
|
2326
|
+
codecModelPath: path.resolve(process.cwd(), flagString(parsed.flags, "codec-model")),
|
|
2327
|
+
outPath,
|
|
2328
|
+
...(flagString(parsed.flags, "style") !== undefined
|
|
2329
|
+
? { style: flagString(parsed.flags, "style") }
|
|
2330
|
+
: {}),
|
|
2331
|
+
...(flagString(parsed.flags, "language") !== undefined
|
|
2332
|
+
? { language: flagString(parsed.flags, "language") }
|
|
2333
|
+
: {}),
|
|
2334
|
+
...(flagString(parsed.flags, "device") !== undefined
|
|
2335
|
+
? { device: flagString(parsed.flags, "device") }
|
|
2336
|
+
: {}),
|
|
2337
|
+
...(flagString(parsed.flags, "executable") !== undefined
|
|
2338
|
+
? { executable: flagString(parsed.flags, "executable") }
|
|
2339
|
+
: {}),
|
|
2340
|
+
...(flagString(parsed.flags, "manifest") !== undefined
|
|
2341
|
+
? { manifestPath: flagString(parsed.flags, "manifest") }
|
|
2342
|
+
: {}),
|
|
2343
|
+
force,
|
|
2344
|
+
signal,
|
|
2345
|
+
});
|
|
887
2346
|
if (json) {
|
|
888
2347
|
printJson({ ok: true, command: "tts", ...result });
|
|
889
2348
|
}
|
|
890
2349
|
else {
|
|
891
|
-
process.stdout.write(`Wrote 24 kHz mono WAV to ${result.path} (${(result.bytes / (1024 * 1024)).toFixed(2)} MB)\n`);
|
|
2350
|
+
process.stdout.write(`Wrote 24 kHz mono WAV to ${result.path} (${(result.bytes / (1024 * 1024)).toFixed(2)} MB, ${result.engine})\n`);
|
|
892
2351
|
process.stdout.write(`Updated provenance manifest ${result.manifest}\n`);
|
|
893
2352
|
}
|
|
894
2353
|
return 0;
|
|
895
2354
|
}
|
|
896
|
-
async function runTranscribe(parsed, signal) {
|
|
2355
|
+
async function runTranscribe(parsed, signal, policy) {
|
|
2356
|
+
const dryRun = parsed.flags.has("dry-run") === true;
|
|
2357
|
+
const profile = resolveProfile(policy, flagString(parsed.flags, "profile"));
|
|
2358
|
+
if (!dryRun) {
|
|
2359
|
+
assertOperationAllowed(policy, "transcribe");
|
|
2360
|
+
}
|
|
2361
|
+
if (parsed.flags.has("force")) {
|
|
2362
|
+
assertOverwriteAllowed(policy, "force");
|
|
2363
|
+
}
|
|
897
2364
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
898
2365
|
const json = parsed.flags.has("json") === true;
|
|
899
2366
|
const outValue = flagString(parsed.flags, "out");
|
|
@@ -901,13 +2368,82 @@ async function runTranscribe(parsed, signal) {
|
|
|
901
2368
|
if (!outValue || !modelValue) {
|
|
902
2369
|
usage('"transcribe" requires --out and --model.');
|
|
903
2370
|
}
|
|
2371
|
+
const outPath = path.resolve(process.cwd(), outValue);
|
|
2372
|
+
const sourcePath = path.resolve(process.cwd(), parsed.positionals[0]);
|
|
2373
|
+
const modelPath = path.resolve(process.cwd(), modelValue);
|
|
2374
|
+
const formats = parseSubtitleFormats(parsed.flags.get("export"));
|
|
2375
|
+
const exports = formats.map((format) => ({
|
|
2376
|
+
format,
|
|
2377
|
+
path: `${outPath.toLowerCase().endsWith(".json") ? outPath.slice(0, -5) : outPath}.${format}`,
|
|
2378
|
+
}));
|
|
2379
|
+
const force = parsed.flags.has("force") === true;
|
|
2380
|
+
const manifestPath = resolveManifestPath(projectRoot, flagString(parsed.flags, "manifest"));
|
|
2381
|
+
if (dryRun) {
|
|
2382
|
+
if (!outPath.toLowerCase().endsWith(".json")) {
|
|
2383
|
+
throw new FrameyardError("TRANSCRIPT_INVALID", `Transcript output must use a .json extension: ${outPath}`);
|
|
2384
|
+
}
|
|
2385
|
+
const executableValue = flagString(parsed.flags, "executable");
|
|
2386
|
+
const prerequisites = [
|
|
2387
|
+
{ label: "transcription source", path: sourcePath },
|
|
2388
|
+
{ label: "Whisper model", path: modelPath },
|
|
2389
|
+
...(executableValue !== undefined ? [{ label: "transcribe executable", path: executableValue }] : []),
|
|
2390
|
+
];
|
|
2391
|
+
const blockers = await prerequisiteBlockers(prerequisites);
|
|
2392
|
+
for (const destination of [outPath, ...exports.map((entry) => entry.path)]) {
|
|
2393
|
+
if (await pathExists(destination)) {
|
|
2394
|
+
blockers.push({
|
|
2395
|
+
code: "OUTPUT_EXISTS",
|
|
2396
|
+
message: `Output already exists: ${destination}.`,
|
|
2397
|
+
suggestion: policy.allowOverwrite
|
|
2398
|
+
? "Pass --force to replace it."
|
|
2399
|
+
: "Choose a new --out path or ask the user to run with FRAMEYARD_POLICY=full.",
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
const plan = {
|
|
2404
|
+
kind: "transcribe",
|
|
2405
|
+
source: sourcePath,
|
|
2406
|
+
model: modelPath,
|
|
2407
|
+
words: parsed.flags.has("words") === true,
|
|
2408
|
+
exports: formats,
|
|
2409
|
+
out: outPath,
|
|
2410
|
+
manifest: manifestPath,
|
|
2411
|
+
probed: false,
|
|
2412
|
+
};
|
|
2413
|
+
const args = process.argv.slice(2).filter((argument) => argument !== "--dry-run" && argument !== "--json");
|
|
2414
|
+
const prefix = policy.tier === "full" ? "" : "FRAMEYARD_POLICY=full ";
|
|
2415
|
+
const commandLine = prefix + handoffCommand(args);
|
|
2416
|
+
if (json) {
|
|
2417
|
+
printJson({
|
|
2418
|
+
ok: true,
|
|
2419
|
+
command: "transcribe",
|
|
2420
|
+
dryRun: true,
|
|
2421
|
+
deferred: true,
|
|
2422
|
+
policy: policyDetails(policy),
|
|
2423
|
+
...(profile !== undefined ? { profile } : {}),
|
|
2424
|
+
plan,
|
|
2425
|
+
commandLine,
|
|
2426
|
+
prerequisites: prerequisites.map((item) => item.path),
|
|
2427
|
+
blockers,
|
|
2428
|
+
output: { path: outPath, exists: await pathExists(outPath) },
|
|
2429
|
+
exports,
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
else {
|
|
2433
|
+
printDryRun("transcribe", plan, commandLine, prerequisites.map((item) => item.path), blockers);
|
|
2434
|
+
}
|
|
2435
|
+
return 0;
|
|
2436
|
+
}
|
|
2437
|
+
if (shouldLowerPriority(policy, profile, parsed.flags.has("low-priority") === true)) {
|
|
2438
|
+
lowerProcessPriority();
|
|
2439
|
+
}
|
|
904
2440
|
const result = await transcribeAudio({
|
|
905
2441
|
projectRoot,
|
|
906
|
-
sourcePath
|
|
907
|
-
outPath
|
|
908
|
-
modelPath
|
|
2442
|
+
sourcePath,
|
|
2443
|
+
outPath,
|
|
2444
|
+
modelPath,
|
|
909
2445
|
words: parsed.flags.has("words") === true,
|
|
910
|
-
exports:
|
|
2446
|
+
exports: formats,
|
|
911
2447
|
...(flagString(parsed.flags, "language") !== undefined
|
|
912
2448
|
? { language: flagString(parsed.flags, "language") }
|
|
913
2449
|
: {}),
|
|
@@ -917,7 +2453,7 @@ async function runTranscribe(parsed, signal) {
|
|
|
917
2453
|
...(flagString(parsed.flags, "manifest") !== undefined
|
|
918
2454
|
? { manifestPath: flagString(parsed.flags, "manifest") }
|
|
919
2455
|
: {}),
|
|
920
|
-
force
|
|
2456
|
+
force,
|
|
921
2457
|
signal,
|
|
922
2458
|
});
|
|
923
2459
|
if (json) {
|
|
@@ -933,6 +2469,19 @@ async function runTranscribe(parsed, signal) {
|
|
|
933
2469
|
return 0;
|
|
934
2470
|
}
|
|
935
2471
|
async function runDoctorCommand(parsed, signal) {
|
|
2472
|
+
let policy;
|
|
2473
|
+
let policyError;
|
|
2474
|
+
try {
|
|
2475
|
+
policy = await loadResourcePolicy({ cwd: process.cwd(), env: process.env });
|
|
2476
|
+
}
|
|
2477
|
+
catch (error) {
|
|
2478
|
+
if (isFrameyardError(error) && error.code === "POLICY_INVALID") {
|
|
2479
|
+
policyError = error;
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
throw error;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
936
2485
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
937
2486
|
const manifestPath = resolveManifestPath(projectRoot, flagString(parsed.flags, "manifest"));
|
|
938
2487
|
const json = parsed.flags.has("json") === true;
|
|
@@ -942,6 +2491,9 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
942
2491
|
...(flagString(parsed.flags, "qwen-tts") !== undefined
|
|
943
2492
|
? { qwenTts: flagString(parsed.flags, "qwen-tts") }
|
|
944
2493
|
: {}),
|
|
2494
|
+
...(flagString(parsed.flags, "sherpa-onnx-tts") !== undefined
|
|
2495
|
+
? { sherpaOnnxTts: flagString(parsed.flags, "sherpa-onnx-tts") }
|
|
2496
|
+
: {}),
|
|
945
2497
|
...(flagString(parsed.flags, "whisper-cli") !== undefined
|
|
946
2498
|
? { whisperCli: flagString(parsed.flags, "whisper-cli") }
|
|
947
2499
|
: {}),
|
|
@@ -951,13 +2503,32 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
951
2503
|
...(flagString(parsed.flags, "codec-model") !== undefined
|
|
952
2504
|
? { codecModel: flagString(parsed.flags, "codec-model") }
|
|
953
2505
|
: {}),
|
|
2506
|
+
...(flagString(parsed.flags, "model-dir") !== undefined
|
|
2507
|
+
? { supertonicModelDir: flagString(parsed.flags, "model-dir") }
|
|
2508
|
+
: {}),
|
|
954
2509
|
...(flagString(parsed.flags, "model") !== undefined
|
|
955
2510
|
? { whisperModel: flagString(parsed.flags, "model") }
|
|
956
2511
|
: {}),
|
|
957
2512
|
signal,
|
|
958
2513
|
});
|
|
2514
|
+
const policyReport = policy !== undefined
|
|
2515
|
+
? { status: "ok", ...policyDetails(policy) }
|
|
2516
|
+
: {
|
|
2517
|
+
status: "error",
|
|
2518
|
+
code: policyError?.code ?? "POLICY_INVALID",
|
|
2519
|
+
detail: policyError?.message ?? "Resource policy could not be loaded.",
|
|
2520
|
+
};
|
|
2521
|
+
const resources = {
|
|
2522
|
+
platform: process.platform,
|
|
2523
|
+
arch: process.arch,
|
|
2524
|
+
cpus: cpus().length,
|
|
2525
|
+
loadAverage: loadavg(),
|
|
2526
|
+
totalMemoryBytes: totalmem(),
|
|
2527
|
+
freeMemoryBytes: freemem(),
|
|
2528
|
+
};
|
|
2529
|
+
const report = { ...result, policy: policyReport, resources };
|
|
959
2530
|
if (json) {
|
|
960
|
-
printJson({ ok: true, command: "doctor", ...
|
|
2531
|
+
printJson({ ok: true, command: "doctor", ...report });
|
|
961
2532
|
}
|
|
962
2533
|
else {
|
|
963
2534
|
for (const check of result.checks) {
|
|
@@ -965,7 +2536,11 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
965
2536
|
const suffix = check.required ? "" : " (optional)";
|
|
966
2537
|
process.stdout.write(`${mark} ${check.name}${suffix}: ${check.detail}\n`);
|
|
967
2538
|
}
|
|
968
|
-
for (const integration of [
|
|
2539
|
+
for (const integration of [
|
|
2540
|
+
result.integrations.tts,
|
|
2541
|
+
result.integrations.transcribe,
|
|
2542
|
+
result.integrations.sherpaOnnxTts,
|
|
2543
|
+
]) {
|
|
969
2544
|
const mark = integration.status === "ok" ? "✓" : integration.status === "missing" && !integration.requested ? "○" : "✗";
|
|
970
2545
|
const detail = integration.detail ? ` — ${integration.detail}` : "";
|
|
971
2546
|
const version = integration.version ? ` ${integration.version}` : "";
|
|
@@ -976,13 +2551,23 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
976
2551
|
const detail = model.detail ? ` — ${model.detail}` : "";
|
|
977
2552
|
process.stdout.write(`${mark} model ${model.role}: ${model.path}${detail}\n`);
|
|
978
2553
|
}
|
|
2554
|
+
const fontDetail = result.fonts.detail ? ` — ${result.fonts.detail}` : "";
|
|
2555
|
+
process.stdout.write(`${result.fonts.valid ? "✓" : "✗"} fonts: ${result.fonts.exists ? `${result.fonts.fonts} installed, ${result.fonts.files} files, ${result.fonts.bytes} bytes` : "none"}${fontDetail}\n`);
|
|
2556
|
+
if (policyReport.status === "ok") {
|
|
2557
|
+
process.stdout.write(`✓ policy: ${policyReport.policy}${policyReport.profile ? `, ${policyReport.profile}` : ""}, still ≤${policyReport.maxStillFrames}, ${policyReport.allowOverwrite ? "overwrite allowed" : "overwrite blocked"} (${policyReport.source})\n`);
|
|
2558
|
+
}
|
|
2559
|
+
else {
|
|
2560
|
+
process.stdout.write(`✗ policy: ${policyReport.detail}\n`);
|
|
2561
|
+
}
|
|
2562
|
+
process.stdout.write(`resources: ${resources.cpus} cpus, load ${resources.loadAverage.map((value) => value.toFixed(2)).join(" ")}, ${(resources.totalMemoryBytes / 1024 ** 3).toFixed(1)} GB RAM\n`);
|
|
979
2563
|
process.stdout.write(`\ndoctor: ${result.healthy ? "healthy" : "problems found"}\n`);
|
|
980
2564
|
}
|
|
981
2565
|
return result.healthy ? 0 : 1;
|
|
982
2566
|
}
|
|
983
2567
|
function reportFailure(error, command, jsonRequested, signal) {
|
|
984
2568
|
const aborted = signal?.aborted === true;
|
|
985
|
-
const
|
|
2569
|
+
const cancelled = aborted || (isFrameyardError(error) && error.code === "CANCELLED");
|
|
2570
|
+
const code = cancelled ? "CANCELLED" : isFrameyardError(error) ? error.code : "INTERNAL";
|
|
986
2571
|
const message = aborted ? "Operation cancelled." : error instanceof Error ? error.message : String(error);
|
|
987
2572
|
const details = isFrameyardError(error) ? error.details : undefined;
|
|
988
2573
|
const diagnostics = isFrameyardError(error) ? error.diagnostics : undefined;
|
|
@@ -998,14 +2583,14 @@ function reportFailure(error, command, jsonRequested, signal) {
|
|
|
998
2583
|
...(diagnostics !== undefined ? { diagnostics } : {}),
|
|
999
2584
|
});
|
|
1000
2585
|
}
|
|
1001
|
-
process.stderr.write(`${
|
|
2586
|
+
process.stderr.write(`${cancelled ? "Cancelled" : `error [${code}]`}${cancelled ? "" : `: ${message}`}\n`);
|
|
1002
2587
|
if (diagnostics !== undefined) {
|
|
1003
2588
|
printDiagnostics(diagnostics);
|
|
1004
2589
|
}
|
|
1005
2590
|
if (!aborted && !isFrameyardError(error) && process.env.FRAMEYARD_DEBUG === "1" && error instanceof Error) {
|
|
1006
2591
|
process.stderr.write(`${error.stack ?? ""}\n`);
|
|
1007
2592
|
}
|
|
1008
|
-
if (
|
|
2593
|
+
if (cancelled) {
|
|
1009
2594
|
return 130;
|
|
1010
2595
|
}
|
|
1011
2596
|
return code === "USAGE" ? 2 : 1;
|
|
@@ -1070,17 +2655,31 @@ async function main(argv) {
|
|
|
1070
2655
|
process.on("SIGINT", interrupt);
|
|
1071
2656
|
process.on("SIGTERM", interrupt);
|
|
1072
2657
|
try {
|
|
2658
|
+
const needsPolicy = !["doctor", "inspect", "compare", "add"].includes(parsed.command);
|
|
2659
|
+
const policy = needsPolicy ? await loadResourcePolicy({ cwd: process.cwd(), env: process.env }) : undefined;
|
|
1073
2660
|
switch (parsed.command) {
|
|
2661
|
+
case "context":
|
|
2662
|
+
return await runContext(parsed, controller.signal, policy);
|
|
2663
|
+
case "lint":
|
|
2664
|
+
return await runLint(parsed, controller.signal, policy);
|
|
1074
2665
|
case "check":
|
|
1075
|
-
return await runCheck(parsed, controller.signal);
|
|
2666
|
+
return await runCheck(parsed, controller.signal, policy);
|
|
1076
2667
|
case "still":
|
|
1077
|
-
return await runStill(parsed, controller.signal);
|
|
2668
|
+
return await runStill(parsed, controller.signal, policy);
|
|
2669
|
+
case "inspect":
|
|
2670
|
+
return await runInspect(parsed);
|
|
2671
|
+
case "compare":
|
|
2672
|
+
return await runCompare(parsed);
|
|
1078
2673
|
case "render":
|
|
1079
|
-
return await runRender(parsed, controller.signal);
|
|
2674
|
+
return await runRender(parsed, controller.signal, policy);
|
|
1080
2675
|
case "tts":
|
|
1081
|
-
return await runTts(parsed, controller.signal);
|
|
2676
|
+
return await runTts(parsed, controller.signal, policy);
|
|
1082
2677
|
case "transcribe":
|
|
1083
|
-
return await runTranscribe(parsed, controller.signal);
|
|
2678
|
+
return await runTranscribe(parsed, controller.signal, policy);
|
|
2679
|
+
case "font":
|
|
2680
|
+
return await runFont(parsed, controller.signal, policy);
|
|
2681
|
+
case "add":
|
|
2682
|
+
return await runAdd(parsed);
|
|
1084
2683
|
case "doctor":
|
|
1085
2684
|
return await runDoctorCommand(parsed, controller.signal);
|
|
1086
2685
|
default:
|