frameyard 0.8.0 → 0.10.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 +177 -14
- package/GLOSSARY.md +112 -2
- package/README.md +302 -265
- 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 +1685 -93
- package/dist/cli.js.map +1 -1
- package/dist/composition/format.d.ts +1 -1
- package/dist/composition/format.d.ts.map +1 -1
- package/dist/composition/format.js +1 -1
- package/dist/composition/format.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 +25 -0
- package/dist/contracts.d.ts.map +1 -1
- 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/validate.d.ts.map +1 -1
- package/dist/html/validate.js +59 -0
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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/fonts/LICENSE-figtree.txt +93 -0
- package/dist/preview/fonts/LICENSE-space-mono.txt +93 -0
- package/dist/preview/fonts/README.md +23 -0
- package/dist/preview/fonts/figtree-latin-wght-normal.woff2 +0 -0
- package/dist/preview/fonts/space-mono-latin-400-normal.woff2 +0 -0
- package/dist/preview/fonts/space-mono-latin-700-normal.woff2 +0 -0
- package/dist/preview/main.js +5 -2
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +17 -69
- 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 +70 -45
- 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 +88 -27
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +10 -3
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -17
- package/dist/project.js.map +1 -1
- package/dist/renderer/audio.d.ts +6 -0
- package/dist/renderer/audio.d.ts.map +1 -1
- package/dist/renderer/audio.js +3 -1
- package/dist/renderer/audio.js.map +1 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +13 -0
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/concurrency.d.ts +6 -0
- package/dist/renderer/concurrency.d.ts.map +1 -0
- package/dist/renderer/concurrency.js +27 -0
- package/dist/renderer/concurrency.js.map +1 -0
- package/dist/renderer/ffmpeg.d.ts +104 -5
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +580 -67
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.js +1 -1
- 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/session-pool.d.ts +28 -0
- package/dist/renderer/session-pool.d.ts.map +1 -0
- package/dist/renderer/session-pool.js +91 -0
- package/dist/renderer/session-pool.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 +600 -147
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/renderer/timings.d.ts +13 -0
- package/dist/renderer/timings.d.ts.map +1 -0
- package/dist/renderer/timings.js +29 -0
- package/dist/renderer/timings.js.map +1 -0
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +4 -0
- 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 -320
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +2 -0
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +7 -0
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +74 -188
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/preview-style.d.ts +11 -0
- package/dist/server/preview-style.d.ts.map +1 -0
- package/dist/server/preview-style.js +298 -0
- package/dist/server/preview-style.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +31 -1
- 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 -34
- 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 +140 -148
- 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 +12 -0
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +105 -9
- 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 +31 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +240 -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.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
- package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
- 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 +2 -1
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +2 -2
- package/docs/adr/0016-author-compositions-as-html-fragments.md +1 -1
- 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/adr/0024-stream-composition-captures-and-skip-trivial-composites.md +86 -0
- package/docs/adr/0025-capture-timeline-layers-in-parallel-with-a-global-limit.md +67 -0
- package/docs/adr/0026-reuse-loaded-composition-sessions-across-segments.md +70 -0
- package/docs/adr/0027-encode-two-full-frame-layers-through-dual-pipes.md +76 -0
- package/docs/adr/0028-encode-full-frame-video-segments-directly.md +65 -0
- package/docs/migration-0.4.md +2 -2
- package/docs/preview-testing.md +27 -6
- package/docs/{v0.1-design.md → v0.x-design.md} +57 -32
- package/examples/blend/README.md +3 -3
- package/examples/demo/README.md +2 -2
- 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 +2 -2
- 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/webgl/README.md +1 -1
- package/package.json +13 -6
- 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 -61
- package/skills/frameyard/references/audio-assets.md +32 -3
- package/skills/frameyard/references/common-mistakes.md +53 -2
- package/skills/frameyard/references/deterministic-authoring.md +10 -3
- package/skills/frameyard/references/diagnostics.md +31 -1
- package/skills/frameyard/references/failure-recovery.md +32 -6
- package/skills/frameyard/references/fonts.md +132 -0
- package/skills/frameyard/references/json-results.md +119 -6
- package/skills/frameyard/references/quick-reference.md +222 -0
- package/skills/frameyard/references/timelines.md +97 -7
- /package/examples/blend/{video.config.js → fy.config.js} +0 -0
- /package/examples/demo/{video.config.js → fy.config.js} +0 -0
- /package/examples/static/{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,14 +600,28 @@ 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.
|
|
@@ -254,61 +634,106 @@ Options:
|
|
|
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 <file>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--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}. 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.\n`);
|
|
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
|
+
}
|
|
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
|
+
}
|
|
523
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`);
|
|
@@ -698,8 +1817,17 @@ function printStillResult(result, targetId, timeline, json) {
|
|
|
698
1817
|
process.stdout.write(`Wrote ${result.frames.length} frames of ${target} to ${directory} (${result.width}×${result.height})\n`);
|
|
699
1818
|
}
|
|
700
1819
|
}
|
|
701
|
-
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
|
+
}
|
|
702
1829
|
const project = await projectFromFlag(parsed.flags);
|
|
1830
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: true });
|
|
703
1831
|
const props = await readPropsFile(parsed.flags);
|
|
704
1832
|
const targetId = parsed.positionals[0];
|
|
705
1833
|
const frameValue = parsed.flags.get("frame");
|
|
@@ -707,6 +1835,7 @@ async function runStill(parsed, signal) {
|
|
|
707
1835
|
usage('"still" requires --frame <spec>.');
|
|
708
1836
|
}
|
|
709
1837
|
const frames = parseFrameSpec(frameValue);
|
|
1838
|
+
assertStillFrameLimit(policy, frames.length);
|
|
710
1839
|
const outputPlan = await resolveStillOutputs({
|
|
711
1840
|
targetId,
|
|
712
1841
|
frames,
|
|
@@ -751,8 +1880,17 @@ async function runStill(parsed, signal) {
|
|
|
751
1880
|
throw error;
|
|
752
1881
|
}
|
|
753
1882
|
}
|
|
754
|
-
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
|
+
}
|
|
755
1892
|
const project = await projectFromFlag(parsed.flags);
|
|
1893
|
+
await assertFontIntegrity(project.root, project.fonts, { hashes: true });
|
|
756
1894
|
const props = await readPropsFile(parsed.flags);
|
|
757
1895
|
const encoder = encoderChoice(parsed.flags);
|
|
758
1896
|
const containerValue = containerChoice(parsed.flags);
|
|
@@ -787,11 +1925,113 @@ async function runRender(parsed, signal) {
|
|
|
787
1925
|
}
|
|
788
1926
|
const scratchValue = flagString(parsed.flags, "scratch");
|
|
789
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
|
+
}
|
|
790
2030
|
const result = await renderTimeline({
|
|
791
2031
|
project,
|
|
792
2032
|
timelineId: targetId,
|
|
793
2033
|
outPath: timelineOutPath,
|
|
794
|
-
...(
|
|
2034
|
+
...(effectiveWorkers !== undefined ? { workers: effectiveWorkers } : {}),
|
|
795
2035
|
...(scratchDir !== undefined ? { scratchDir } : {}),
|
|
796
2036
|
...(encoder !== undefined ? { encoder } : {}),
|
|
797
2037
|
codec,
|
|
@@ -816,6 +2056,101 @@ async function runRender(parsed, signal) {
|
|
|
816
2056
|
const compositionId = targetId;
|
|
817
2057
|
const concurrencyValue = parsed.flags.get("concurrency");
|
|
818
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
|
+
}
|
|
819
2154
|
const result = await renderComposition({
|
|
820
2155
|
project,
|
|
821
2156
|
compositionId,
|
|
@@ -841,15 +2176,22 @@ async function runRender(parsed, signal) {
|
|
|
841
2176
|
}
|
|
842
2177
|
return 0;
|
|
843
2178
|
}
|
|
844
|
-
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
|
+
}
|
|
845
2188
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
846
2189
|
const json = parsed.flags.has("json") === true;
|
|
2190
|
+
const engine = (flagString(parsed.flags, "engine") ?? "qwen");
|
|
847
2191
|
const speaker = flagString(parsed.flags, "speaker");
|
|
848
|
-
const talkerModel = flagString(parsed.flags, "talker-model");
|
|
849
|
-
const codecModel = flagString(parsed.flags, "codec-model");
|
|
850
2192
|
const outValue = flagString(parsed.flags, "out");
|
|
851
|
-
if (!speaker || !
|
|
852
|
-
usage('"tts" requires --speaker
|
|
2193
|
+
if (!speaker || !outValue) {
|
|
2194
|
+
usage('"tts" requires --speaker and --out.');
|
|
853
2195
|
}
|
|
854
2196
|
const textFile = flagString(parsed.flags, "text-file");
|
|
855
2197
|
let text;
|
|
@@ -868,39 +2210,157 @@ async function runTts(parsed, signal) {
|
|
|
868
2210
|
}
|
|
869
2211
|
text = await readStdin();
|
|
870
2212
|
}
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
?
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
});
|
|
894
2346
|
if (json) {
|
|
895
2347
|
printJson({ ok: true, command: "tts", ...result });
|
|
896
2348
|
}
|
|
897
2349
|
else {
|
|
898
|
-
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`);
|
|
899
2351
|
process.stdout.write(`Updated provenance manifest ${result.manifest}\n`);
|
|
900
2352
|
}
|
|
901
2353
|
return 0;
|
|
902
2354
|
}
|
|
903
|
-
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
|
+
}
|
|
904
2364
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
905
2365
|
const json = parsed.flags.has("json") === true;
|
|
906
2366
|
const outValue = flagString(parsed.flags, "out");
|
|
@@ -908,13 +2368,82 @@ async function runTranscribe(parsed, signal) {
|
|
|
908
2368
|
if (!outValue || !modelValue) {
|
|
909
2369
|
usage('"transcribe" requires --out and --model.');
|
|
910
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
|
+
}
|
|
911
2440
|
const result = await transcribeAudio({
|
|
912
2441
|
projectRoot,
|
|
913
|
-
sourcePath
|
|
914
|
-
outPath
|
|
915
|
-
modelPath
|
|
2442
|
+
sourcePath,
|
|
2443
|
+
outPath,
|
|
2444
|
+
modelPath,
|
|
916
2445
|
words: parsed.flags.has("words") === true,
|
|
917
|
-
exports:
|
|
2446
|
+
exports: formats,
|
|
918
2447
|
...(flagString(parsed.flags, "language") !== undefined
|
|
919
2448
|
? { language: flagString(parsed.flags, "language") }
|
|
920
2449
|
: {}),
|
|
@@ -924,7 +2453,7 @@ async function runTranscribe(parsed, signal) {
|
|
|
924
2453
|
...(flagString(parsed.flags, "manifest") !== undefined
|
|
925
2454
|
? { manifestPath: flagString(parsed.flags, "manifest") }
|
|
926
2455
|
: {}),
|
|
927
|
-
force
|
|
2456
|
+
force,
|
|
928
2457
|
signal,
|
|
929
2458
|
});
|
|
930
2459
|
if (json) {
|
|
@@ -940,6 +2469,19 @@ async function runTranscribe(parsed, signal) {
|
|
|
940
2469
|
return 0;
|
|
941
2470
|
}
|
|
942
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
|
+
}
|
|
943
2485
|
const projectRoot = await assetRootFromFlag(parsed.flags);
|
|
944
2486
|
const manifestPath = resolveManifestPath(projectRoot, flagString(parsed.flags, "manifest"));
|
|
945
2487
|
const json = parsed.flags.has("json") === true;
|
|
@@ -949,6 +2491,9 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
949
2491
|
...(flagString(parsed.flags, "qwen-tts") !== undefined
|
|
950
2492
|
? { qwenTts: flagString(parsed.flags, "qwen-tts") }
|
|
951
2493
|
: {}),
|
|
2494
|
+
...(flagString(parsed.flags, "sherpa-onnx-tts") !== undefined
|
|
2495
|
+
? { sherpaOnnxTts: flagString(parsed.flags, "sherpa-onnx-tts") }
|
|
2496
|
+
: {}),
|
|
952
2497
|
...(flagString(parsed.flags, "whisper-cli") !== undefined
|
|
953
2498
|
? { whisperCli: flagString(parsed.flags, "whisper-cli") }
|
|
954
2499
|
: {}),
|
|
@@ -958,13 +2503,32 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
958
2503
|
...(flagString(parsed.flags, "codec-model") !== undefined
|
|
959
2504
|
? { codecModel: flagString(parsed.flags, "codec-model") }
|
|
960
2505
|
: {}),
|
|
2506
|
+
...(flagString(parsed.flags, "model-dir") !== undefined
|
|
2507
|
+
? { supertonicModelDir: flagString(parsed.flags, "model-dir") }
|
|
2508
|
+
: {}),
|
|
961
2509
|
...(flagString(parsed.flags, "model") !== undefined
|
|
962
2510
|
? { whisperModel: flagString(parsed.flags, "model") }
|
|
963
2511
|
: {}),
|
|
964
2512
|
signal,
|
|
965
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 };
|
|
966
2530
|
if (json) {
|
|
967
|
-
printJson({ ok: true, command: "doctor", ...
|
|
2531
|
+
printJson({ ok: true, command: "doctor", ...report });
|
|
968
2532
|
}
|
|
969
2533
|
else {
|
|
970
2534
|
for (const check of result.checks) {
|
|
@@ -972,7 +2536,11 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
972
2536
|
const suffix = check.required ? "" : " (optional)";
|
|
973
2537
|
process.stdout.write(`${mark} ${check.name}${suffix}: ${check.detail}\n`);
|
|
974
2538
|
}
|
|
975
|
-
for (const integration of [
|
|
2539
|
+
for (const integration of [
|
|
2540
|
+
result.integrations.tts,
|
|
2541
|
+
result.integrations.transcribe,
|
|
2542
|
+
result.integrations.sherpaOnnxTts,
|
|
2543
|
+
]) {
|
|
976
2544
|
const mark = integration.status === "ok" ? "✓" : integration.status === "missing" && !integration.requested ? "○" : "✗";
|
|
977
2545
|
const detail = integration.detail ? ` — ${integration.detail}` : "";
|
|
978
2546
|
const version = integration.version ? ` ${integration.version}` : "";
|
|
@@ -983,13 +2551,23 @@ async function runDoctorCommand(parsed, signal) {
|
|
|
983
2551
|
const detail = model.detail ? ` — ${model.detail}` : "";
|
|
984
2552
|
process.stdout.write(`${mark} model ${model.role}: ${model.path}${detail}\n`);
|
|
985
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`);
|
|
986
2563
|
process.stdout.write(`\ndoctor: ${result.healthy ? "healthy" : "problems found"}\n`);
|
|
987
2564
|
}
|
|
988
2565
|
return result.healthy ? 0 : 1;
|
|
989
2566
|
}
|
|
990
2567
|
function reportFailure(error, command, jsonRequested, signal) {
|
|
991
2568
|
const aborted = signal?.aborted === true;
|
|
992
|
-
const
|
|
2569
|
+
const cancelled = aborted || (isFrameyardError(error) && error.code === "CANCELLED");
|
|
2570
|
+
const code = cancelled ? "CANCELLED" : isFrameyardError(error) ? error.code : "INTERNAL";
|
|
993
2571
|
const message = aborted ? "Operation cancelled." : error instanceof Error ? error.message : String(error);
|
|
994
2572
|
const details = isFrameyardError(error) ? error.details : undefined;
|
|
995
2573
|
const diagnostics = isFrameyardError(error) ? error.diagnostics : undefined;
|
|
@@ -1005,14 +2583,14 @@ function reportFailure(error, command, jsonRequested, signal) {
|
|
|
1005
2583
|
...(diagnostics !== undefined ? { diagnostics } : {}),
|
|
1006
2584
|
});
|
|
1007
2585
|
}
|
|
1008
|
-
process.stderr.write(`${
|
|
2586
|
+
process.stderr.write(`${cancelled ? "Cancelled" : `error [${code}]`}${cancelled ? "" : `: ${message}`}\n`);
|
|
1009
2587
|
if (diagnostics !== undefined) {
|
|
1010
2588
|
printDiagnostics(diagnostics);
|
|
1011
2589
|
}
|
|
1012
2590
|
if (!aborted && !isFrameyardError(error) && process.env.FRAMEYARD_DEBUG === "1" && error instanceof Error) {
|
|
1013
2591
|
process.stderr.write(`${error.stack ?? ""}\n`);
|
|
1014
2592
|
}
|
|
1015
|
-
if (
|
|
2593
|
+
if (cancelled) {
|
|
1016
2594
|
return 130;
|
|
1017
2595
|
}
|
|
1018
2596
|
return code === "USAGE" ? 2 : 1;
|
|
@@ -1077,17 +2655,31 @@ async function main(argv) {
|
|
|
1077
2655
|
process.on("SIGINT", interrupt);
|
|
1078
2656
|
process.on("SIGTERM", interrupt);
|
|
1079
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;
|
|
1080
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);
|
|
1081
2665
|
case "check":
|
|
1082
|
-
return await runCheck(parsed, controller.signal);
|
|
2666
|
+
return await runCheck(parsed, controller.signal, policy);
|
|
1083
2667
|
case "still":
|
|
1084
|
-
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);
|
|
1085
2673
|
case "render":
|
|
1086
|
-
return await runRender(parsed, controller.signal);
|
|
2674
|
+
return await runRender(parsed, controller.signal, policy);
|
|
1087
2675
|
case "tts":
|
|
1088
|
-
return await runTts(parsed, controller.signal);
|
|
2676
|
+
return await runTts(parsed, controller.signal, policy);
|
|
1089
2677
|
case "transcribe":
|
|
1090
|
-
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);
|
|
1091
2683
|
case "doctor":
|
|
1092
2684
|
return await runDoctorCommand(parsed, controller.signal);
|
|
1093
2685
|
default:
|