@1agh/maude 0.41.0 → 0.43.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -63
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +476 -88
- package/apps/studio/client/export-center.jsx +460 -0
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +59 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/export-dialog.tsx +18 -25
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +376 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +37 -19
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +198 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +318 -64
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +8 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +70 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -13,10 +13,11 @@ import path from 'node:path';
|
|
|
13
13
|
import JSZip from 'jszip';
|
|
14
14
|
|
|
15
15
|
import { getBrowserBundle } from './_browser-bundles.ts';
|
|
16
|
-
import { exportShimPath,
|
|
16
|
+
import { exportShimPath, runShim } from './_runtime.ts';
|
|
17
17
|
import {
|
|
18
18
|
canvasShellUrl,
|
|
19
19
|
type ExportContext,
|
|
20
|
+
type ExportHooks,
|
|
20
21
|
type ExportOptions,
|
|
21
22
|
type ExportResult,
|
|
22
23
|
} from './index.ts';
|
|
@@ -30,7 +31,8 @@ async function captureSvg(
|
|
|
30
31
|
ctx: ExportContext,
|
|
31
32
|
outDir: string,
|
|
32
33
|
timeoutSec: number,
|
|
33
|
-
bundlePath: string
|
|
34
|
+
bundlePath: string,
|
|
35
|
+
hooks?: ExportHooks
|
|
34
36
|
): Promise<string[]> {
|
|
35
37
|
const args = [
|
|
36
38
|
SVG_PLAYWRIGHT,
|
|
@@ -55,31 +57,20 @@ async function captureSvg(
|
|
|
55
57
|
// Run via a resolved node/bun runtime so the shim's `import 'playwright'`
|
|
56
58
|
// resolves against dev-server/node_modules (playwright is a devDep). `npm exec`
|
|
57
59
|
// doesn't bridge the module path for ESM imports — confirmed against npm 10.x.
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
60
|
+
// runShim resolves the runtime internally so a compiled binary without
|
|
61
|
+
// `node` on PATH surfaces an actionable error, not `posix_spawn 'node'`.
|
|
62
|
+
return runShim(args, {
|
|
61
63
|
cwd: path.dirname(SVG_PLAYWRIGHT),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
signal: hooks?.signal,
|
|
65
|
+
onProgress: hooks?.onProgress,
|
|
64
66
|
});
|
|
65
|
-
const [stdout, stderr] = await Promise.all([
|
|
66
|
-
new Response(proc.stdout).text(),
|
|
67
|
-
new Response(proc.stderr).text(),
|
|
68
|
-
]);
|
|
69
|
-
const code = await proc.exited;
|
|
70
|
-
if (code !== 0) {
|
|
71
|
-
throw new Error(`_svg-playwright exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
72
|
-
}
|
|
73
|
-
return stdout
|
|
74
|
-
.split('\n')
|
|
75
|
-
.map((s) => s.trim())
|
|
76
|
-
.filter(Boolean);
|
|
77
67
|
}
|
|
78
68
|
|
|
79
69
|
export async function run(
|
|
80
70
|
targets: Target[],
|
|
81
71
|
options: ExportOptions,
|
|
82
|
-
ctx: ExportContext
|
|
72
|
+
ctx: ExportContext,
|
|
73
|
+
hooks?: ExportHooks
|
|
83
74
|
): Promise<ExportResult> {
|
|
84
75
|
if (!targets.length) {
|
|
85
76
|
return { filename: 'export.svg', contentType: 'image/svg+xml', body: new Uint8Array(0) };
|
|
@@ -96,9 +87,10 @@ export async function run(
|
|
|
96
87
|
|
|
97
88
|
try {
|
|
98
89
|
const written: string[] = [];
|
|
99
|
-
for (
|
|
100
|
-
const paths = await captureSvg(
|
|
90
|
+
for (let i = 0; i < elementTargets.length; i += 1) {
|
|
91
|
+
const paths = await captureSvg(elementTargets[i], ctx, tmp, timeoutSec, bundlePath, hooks);
|
|
101
92
|
written.push(...paths);
|
|
93
|
+
hooks?.onProgress?.({ current: i + 1, total: elementTargets.length });
|
|
102
94
|
}
|
|
103
95
|
if (!written.length) {
|
|
104
96
|
return { filename: 'export.svg', contentType: 'image/svg+xml', body: new Uint8Array(0) };
|
|
@@ -20,10 +20,11 @@ import { tmpdir } from 'node:os';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
|
|
22
22
|
import { getEncodeLibBundle, getWebRendererBundle } from './_browser-bundles.ts';
|
|
23
|
-
import { exportShimPath,
|
|
23
|
+
import { exportShimPath, runShim } from './_runtime.ts';
|
|
24
24
|
import {
|
|
25
25
|
canvasShellUrl,
|
|
26
26
|
type ExportContext,
|
|
27
|
+
type ExportHooks,
|
|
27
28
|
type ExportOptions,
|
|
28
29
|
type ExportResult,
|
|
29
30
|
} from './index.ts';
|
|
@@ -47,7 +48,8 @@ async function runVideo(
|
|
|
47
48
|
format: VideoFormat,
|
|
48
49
|
targets: Target[],
|
|
49
50
|
options: ExportOptions,
|
|
50
|
-
ctx: ExportContext
|
|
51
|
+
ctx: ExportContext,
|
|
52
|
+
hooks?: ExportHooks
|
|
51
53
|
): Promise<ExportResult> {
|
|
52
54
|
const el = targets.find((t): t is Extract<Target, { kind: 'element' }> => t.kind === 'element');
|
|
53
55
|
if (!el) {
|
|
@@ -119,27 +121,40 @@ async function runVideo(
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
try {
|
|
122
|
-
const
|
|
124
|
+
const stdoutLines = await runShim(args, {
|
|
123
125
|
cwd: path.dirname(VIDEO_PLAYWRIGHT),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
signal: hooks?.signal,
|
|
127
|
+
// Frame-step capture emits `MAUDE_PROGRESS {current,total}` per frame → a
|
|
128
|
+
// live "N of M" progress bar in the Exports panel. This is exactly the
|
|
129
|
+
// slow path (the renderMediaOnWeb-overflow fallback can run many minutes);
|
|
130
|
+
// the fast one-pass renderer finishes before progress matters and reports
|
|
131
|
+
// none until done (it exposes no per-frame hook) — acceptable.
|
|
132
|
+
onProgress: hooks?.onProgress,
|
|
126
133
|
});
|
|
127
|
-
const [stdout, stderr] = await Promise.all([
|
|
128
|
-
new Response(proc.stdout).text(),
|
|
129
|
-
new Response(proc.stderr).text(),
|
|
130
|
-
]);
|
|
131
|
-
const code = await proc.exited;
|
|
132
|
-
if (code !== 0) {
|
|
133
|
-
throw new Error(`_video-playwright exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
134
|
-
}
|
|
135
134
|
const body = new Uint8Array(readFileSync(outPath));
|
|
136
135
|
// The container can differ from the request (mp4 → webm fallback when the
|
|
137
|
-
// browser has no H.264 encoder). Read the summary line for the real ext
|
|
136
|
+
// browser has no H.264 encoder). Read the summary line for the real ext, and
|
|
137
|
+
// for the degradation marker the shim sets when it had to fall back from the
|
|
138
|
+
// renderMediaOnWeb audio path to frame-step screenshots (RCA
|
|
139
|
+
// issue-video-mp4-rendermediaonweb-stack-overflow) — surfaced here so the
|
|
140
|
+
// dropped audio isn't silent (job stderr / logs; a UI toast is a follow-up).
|
|
138
141
|
let ext: string = format;
|
|
139
142
|
try {
|
|
140
|
-
const lastLine =
|
|
141
|
-
const summary = JSON.parse(lastLine) as {
|
|
143
|
+
const lastLine = stdoutLines.at(-1) ?? '{}';
|
|
144
|
+
const summary = JSON.parse(lastLine) as {
|
|
145
|
+
container?: string;
|
|
146
|
+
degraded?: boolean;
|
|
147
|
+
audioDropped?: boolean;
|
|
148
|
+
fallbackReason?: string;
|
|
149
|
+
};
|
|
142
150
|
if (summary.container) ext = summary.container;
|
|
151
|
+
if (summary.degraded) {
|
|
152
|
+
console.error(
|
|
153
|
+
`⚠ ${format} export degraded: the audio renderer failed ` +
|
|
154
|
+
`(${summary.fallbackReason ?? 'unknown'}), so this file was captured ` +
|
|
155
|
+
'frame-by-frame and has no audio.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
143
158
|
} catch {
|
|
144
159
|
/* keep the requested ext */
|
|
145
160
|
}
|
|
@@ -174,11 +189,14 @@ function resolveFrames(options: ExportOptions, fps: number | undefined): number
|
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
export const mp4 = {
|
|
177
|
-
run: (t: Target[], o: ExportOptions, c: ExportContext
|
|
192
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext, h?: ExportHooks) =>
|
|
193
|
+
runVideo('mp4', t, o, c, h),
|
|
178
194
|
};
|
|
179
195
|
export const webm = {
|
|
180
|
-
run: (t: Target[], o: ExportOptions, c: ExportContext
|
|
196
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext, h?: ExportHooks) =>
|
|
197
|
+
runVideo('webm', t, o, c, h),
|
|
181
198
|
};
|
|
182
199
|
export const gif = {
|
|
183
|
-
run: (t: Target[], o: ExportOptions, c: ExportContext
|
|
200
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext, h?: ExportHooks) =>
|
|
201
|
+
runVideo('gif', t, o, c, h),
|
|
184
202
|
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// footage/schema — validator + helper coverage (feature-footage-analysis-director).
|
|
2
|
+
// Mirrors the photo/schema house style: a valid fixture round-trips, and every
|
|
3
|
+
// rejection lane (unknown key / bad type / out-of-range / non-relative asset /
|
|
4
|
+
// end≤start / end>duration / bad transition presentation) is asserted so a
|
|
5
|
+
// crafted field can never reach disk via the `/_api/footage` route.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
EDL_VERSION,
|
|
11
|
+
type Edl,
|
|
12
|
+
emptyFootageAnalysis,
|
|
13
|
+
FOOTAGE_ANALYSIS_VERSION,
|
|
14
|
+
type FootageAnalysis,
|
|
15
|
+
isEmptyAnalysis,
|
|
16
|
+
validateEdl,
|
|
17
|
+
validateFootageAnalysis,
|
|
18
|
+
} from './schema.ts';
|
|
19
|
+
|
|
20
|
+
const GOOD_ANALYSIS: FootageAnalysis = {
|
|
21
|
+
version: FOOTAGE_ANALYSIS_VERSION,
|
|
22
|
+
asset: 'assets/a1b2c3d4.mp4',
|
|
23
|
+
durationSec: 12.5,
|
|
24
|
+
width: 1920,
|
|
25
|
+
height: 1080,
|
|
26
|
+
keyframes: 12,
|
|
27
|
+
shots: [
|
|
28
|
+
{
|
|
29
|
+
start: 0,
|
|
30
|
+
end: 3.2,
|
|
31
|
+
kind: 'establishing',
|
|
32
|
+
motion: 'push-in',
|
|
33
|
+
subject: 'drone push over the river',
|
|
34
|
+
lighting: 'golden hour',
|
|
35
|
+
mood: 'calm',
|
|
36
|
+
quality: 0.9,
|
|
37
|
+
usable: true,
|
|
38
|
+
note: 'hero shot',
|
|
39
|
+
},
|
|
40
|
+
{ start: 4, end: 6, kind: 'detail', usable: false },
|
|
41
|
+
],
|
|
42
|
+
summary: 'Calm aspirational exterior b-roll.',
|
|
43
|
+
tags: ['rebrand', 'exterior'],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GOOD_EDL: Edl = {
|
|
47
|
+
version: EDL_VERSION,
|
|
48
|
+
title: 'Alligators rebrand reel',
|
|
49
|
+
fps: 30,
|
|
50
|
+
width: 1920,
|
|
51
|
+
height: 1080,
|
|
52
|
+
beats: [
|
|
53
|
+
{
|
|
54
|
+
clip: 'assets/a1b2c3d4.mp4',
|
|
55
|
+
startSec: 0,
|
|
56
|
+
durationFrames: 60,
|
|
57
|
+
name: 'intro',
|
|
58
|
+
transition: null,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
clip: 'assets/a1b2c3d4.mp4',
|
|
62
|
+
startSec: 8,
|
|
63
|
+
durationFrames: 45,
|
|
64
|
+
why: 'second shot from the same clip',
|
|
65
|
+
transition: { presentation: 'fade', frames: 15 },
|
|
66
|
+
overlay: { kind: 'title', text: 'Alligators' },
|
|
67
|
+
name: 'hero',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
music: { asset: 'assets/deadbeef.mp3', fadeOutFrames: 20 },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe('validateFootageAnalysis', () => {
|
|
74
|
+
test('accepts a well-formed analysis', () => {
|
|
75
|
+
expect(validateFootageAnalysis(GOOD_ANALYSIS)).toEqual({ ok: true, errors: [] });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('accepts an empty analysis from the helper', () => {
|
|
79
|
+
expect(validateFootageAnalysis(emptyFootageAnalysis('assets/a1b2c3d4.mp4')).ok).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('rejects a non-object root', () => {
|
|
83
|
+
expect(validateFootageAnalysis(null).ok).toBe(false);
|
|
84
|
+
expect(validateFootageAnalysis(42).ok).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('rejects an unknown top-level key', () => {
|
|
88
|
+
const r = validateFootageAnalysis({ ...GOOD_ANALYSIS, evil: 1 });
|
|
89
|
+
expect(r.ok).toBe(false);
|
|
90
|
+
expect(r.errors.join()).toContain('unknown key "evil"');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('rejects a non-relative / traversing asset path', () => {
|
|
94
|
+
for (const asset of ['/etc/passwd', '../secret.mp4', 'assets/../x.mp4', 'http://x/y.mp4']) {
|
|
95
|
+
expect(validateFootageAnalysis({ ...GOOD_ANALYSIS, asset }).ok).toBe(false);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('rejects a shot whose end ≤ start', () => {
|
|
100
|
+
const r = validateFootageAnalysis({
|
|
101
|
+
...GOOD_ANALYSIS,
|
|
102
|
+
shots: [{ start: 5, end: 5 }],
|
|
103
|
+
});
|
|
104
|
+
expect(r.ok).toBe(false);
|
|
105
|
+
expect(r.errors.join()).toContain('end must be > start');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('rejects a shot that runs past the clip duration', () => {
|
|
109
|
+
const r = validateFootageAnalysis({
|
|
110
|
+
...GOOD_ANALYSIS,
|
|
111
|
+
durationSec: 5,
|
|
112
|
+
shots: [{ start: 1, end: 9 }],
|
|
113
|
+
});
|
|
114
|
+
expect(r.ok).toBe(false);
|
|
115
|
+
expect(r.errors.join()).toContain('exceeds clip duration');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('rejects an out-of-range quality + bad shot kind', () => {
|
|
119
|
+
expect(
|
|
120
|
+
validateFootageAnalysis({ ...GOOD_ANALYSIS, shots: [{ start: 0, end: 1, quality: 2 }] }).ok
|
|
121
|
+
).toBe(false);
|
|
122
|
+
expect(
|
|
123
|
+
validateFootageAnalysis({ ...GOOD_ANALYSIS, shots: [{ start: 0, end: 1, kind: 'nope' }] }).ok
|
|
124
|
+
).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('isEmptyAnalysis', () => {
|
|
129
|
+
test('true for null / no shots / no usable shots', () => {
|
|
130
|
+
expect(isEmptyAnalysis(null)).toBe(true);
|
|
131
|
+
expect(isEmptyAnalysis({ shots: [] })).toBe(true);
|
|
132
|
+
expect(isEmptyAnalysis({ shots: [{ start: 0, end: 1, usable: false }] })).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
test('false when at least one shot is usable', () => {
|
|
135
|
+
expect(isEmptyAnalysis(GOOD_ANALYSIS)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('validateEdl', () => {
|
|
140
|
+
test('accepts a well-formed EDL', () => {
|
|
141
|
+
expect(validateEdl(GOOD_EDL)).toEqual({ ok: true, errors: [] });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('rejects a beat with no clip', () => {
|
|
145
|
+
const r = validateEdl({ ...GOOD_EDL, beats: [{ startSec: 0, durationFrames: 30 }] });
|
|
146
|
+
expect(r.ok).toBe(false);
|
|
147
|
+
expect(r.errors.join()).toContain('clip');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('rejects a non-relative beat clip', () => {
|
|
151
|
+
expect(
|
|
152
|
+
validateEdl({ ...GOOD_EDL, beats: [{ clip: '/x.mp4', startSec: 0, durationFrames: 30 }] }).ok
|
|
153
|
+
).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('rejects durationFrames < 1 / non-integer', () => {
|
|
157
|
+
expect(
|
|
158
|
+
validateEdl({
|
|
159
|
+
...GOOD_EDL,
|
|
160
|
+
beats: [{ clip: 'assets/a1b2c3d4.mp4', startSec: 0, durationFrames: 0 }],
|
|
161
|
+
}).ok
|
|
162
|
+
).toBe(false);
|
|
163
|
+
expect(
|
|
164
|
+
validateEdl({
|
|
165
|
+
...GOOD_EDL,
|
|
166
|
+
beats: [{ clip: 'assets/a1b2c3d4.mp4', startSec: 0, durationFrames: 12.5 }],
|
|
167
|
+
}).ok
|
|
168
|
+
).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('rejects an unbundled transition presentation', () => {
|
|
172
|
+
const r = validateEdl({
|
|
173
|
+
...GOOD_EDL,
|
|
174
|
+
beats: [
|
|
175
|
+
{
|
|
176
|
+
clip: 'assets/a1b2c3d4.mp4',
|
|
177
|
+
startSec: 0,
|
|
178
|
+
durationFrames: 30,
|
|
179
|
+
transition: { presentation: 'dreamy-zoom', frames: 10 },
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
expect(r.ok).toBe(false);
|
|
184
|
+
expect(r.errors.join()).toContain('presentation');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('rejects an fps above 120 / below 1', () => {
|
|
188
|
+
expect(validateEdl({ ...GOOD_EDL, fps: 999 }).ok).toBe(false);
|
|
189
|
+
expect(validateEdl({ ...GOOD_EDL, fps: 0 }).ok).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('rejects a music bed with a non-relative asset', () => {
|
|
193
|
+
expect(validateEdl({ ...GOOD_EDL, music: { asset: 'http://x/y.mp3' } }).ok).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|