@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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// _probe-footage-playwright.mjs — keyframe extraction for the footage director
|
|
2
|
+
// (feature-footage-analysis-director, Task 4). The READ counterpart to
|
|
3
|
+
// _ingest-footage.mjs: given ONE content-addressed clip, decode it in headless
|
|
4
|
+
// Chromium and dump N evenly-spaced keyframe PNGs the `footage-analyst` agent
|
|
5
|
+
// then WATCHES (vision) to characterize the clip.
|
|
6
|
+
//
|
|
7
|
+
// WHY A BROWSER, NOT ffmpeg: DDR-148's posture is "no native renderer binaries".
|
|
8
|
+
// Chromium already decodes video (it's the capture spine's engine). We don't even
|
|
9
|
+
// need Remotion here — a plain HTML5 <video>, seeked frame-by-frame and
|
|
10
|
+
// screenshotted, is the whole mechanism. No dev server is required either: the
|
|
11
|
+
// page is navigated as file:// and the clip is a file:// subresource (same
|
|
12
|
+
// scheme; `--allow-file-access-from-files` permits the load), so probing works
|
|
13
|
+
// offline / pre-server, unlike the export capture (which drives the live Player).
|
|
14
|
+
//
|
|
15
|
+
// Determinism note (DDR-094 — freeze-frames lie): the frames are evenly spaced
|
|
16
|
+
// midpoints across the clip, so a downstream reader gets genuine motion samples,
|
|
17
|
+
// not one repeated frame.
|
|
18
|
+
//
|
|
19
|
+
// OUTPUT: keyframe PNGs are THROWAWAY SCRATCH → written under --out-dir (a /tmp
|
|
20
|
+
// scratch path, DDR-115: capture scratch never gets a new `_*` dir, never lands
|
|
21
|
+
// in the VERSIONED assets/). stdout = a JSON manifest
|
|
22
|
+
// { asset, durationSec, width, height, frames:[{index,t,png}] }
|
|
23
|
+
//
|
|
24
|
+
// Reached via `maude design probe-footage`, never a raw path.
|
|
25
|
+
// Exit: 0 ok · 2 usage · 3 no browser (via _pw-launch) · 4 decode/probe error · 1 other.
|
|
26
|
+
|
|
27
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
29
|
+
import { pathToFileURL } from 'node:url';
|
|
30
|
+
|
|
31
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
32
|
+
|
|
33
|
+
const DEFAULT_FRAMES = 12;
|
|
34
|
+
// Cap the longest side of a keyframe PNG — vision only needs legible frames, and
|
|
35
|
+
// small PNGs keep the analyst's context lean. 960px is ample.
|
|
36
|
+
const MAX_FRAME_EDGE = 960;
|
|
37
|
+
|
|
38
|
+
function parseArgv(argv) {
|
|
39
|
+
const out = {
|
|
40
|
+
asset: null,
|
|
41
|
+
root: null,
|
|
42
|
+
designRoot: '.design',
|
|
43
|
+
frames: DEFAULT_FRAMES,
|
|
44
|
+
outDir: null,
|
|
45
|
+
};
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
switch (a) {
|
|
49
|
+
case '--asset':
|
|
50
|
+
out.asset = argv[++i];
|
|
51
|
+
break;
|
|
52
|
+
case '--root':
|
|
53
|
+
out.root = argv[++i];
|
|
54
|
+
break;
|
|
55
|
+
case '--design-root':
|
|
56
|
+
out.designRoot = argv[++i];
|
|
57
|
+
break;
|
|
58
|
+
case '--frames':
|
|
59
|
+
out.frames = Number(argv[++i]);
|
|
60
|
+
break;
|
|
61
|
+
case '--out-dir':
|
|
62
|
+
out.outDir = argv[++i];
|
|
63
|
+
break;
|
|
64
|
+
case '--help':
|
|
65
|
+
case '-h':
|
|
66
|
+
out.help = true;
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
if (a.startsWith('-')) throw usage(`unknown flag ${a}`);
|
|
70
|
+
if (out.asset === null) out.asset = a;
|
|
71
|
+
else throw usage(`unexpected extra arg ${a}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function usage(msg) {
|
|
78
|
+
const e = new Error(msg);
|
|
79
|
+
e.code = 2;
|
|
80
|
+
return e;
|
|
81
|
+
}
|
|
82
|
+
function probeError(msg) {
|
|
83
|
+
const e = new Error(msg);
|
|
84
|
+
e.code = 4;
|
|
85
|
+
return e;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const HELP = `probe-footage — extract keyframe PNGs from one clip for vision analysis
|
|
89
|
+
(reached via \`maude design probe-footage\`)
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
maude design probe-footage <assets/<sha8>.<ext>> --root <repo>
|
|
93
|
+
[--design-root .design] [--frames N] [--out-dir DIR]
|
|
94
|
+
|
|
95
|
+
Decodes the clip in headless Chromium, seeks to N evenly-spaced frames, writes a
|
|
96
|
+
PNG per frame to --out-dir (a /tmp scratch dir — NOT assets/), and prints a JSON
|
|
97
|
+
manifest { asset, durationSec, width, height, frames }.
|
|
98
|
+
|
|
99
|
+
Exit: 0 ok · 2 usage · 3 no browser · 4 decode/probe error · 1 other.`;
|
|
100
|
+
|
|
101
|
+
/** Resolve the clip to an absolute path inside <designRoot>/assets and assert containment. */
|
|
102
|
+
function resolveAsset(root, designRootRel, asset) {
|
|
103
|
+
const rel = String(asset).replace(/^\/+/, ''); // tolerate a leading slash
|
|
104
|
+
if (isAbsolute(rel) || rel.includes('..'))
|
|
105
|
+
throw usage(`asset must be a relative assets/ path: ${asset}`);
|
|
106
|
+
const assetsDir = resolve(root, designRootRel, 'assets');
|
|
107
|
+
// Accept both `assets/<name>` and a bare `<name>`.
|
|
108
|
+
const name = rel.startsWith('assets/') ? rel.slice('assets/'.length) : rel;
|
|
109
|
+
if (!/^[a-z0-9]{8}\.[a-z0-9]{2,4}$/.test(name))
|
|
110
|
+
throw usage(`asset name must be content-addressed <sha8>.<ext>: ${name}`);
|
|
111
|
+
const abs = resolve(assetsDir, name);
|
|
112
|
+
if (abs !== join(assetsDir, name) || !abs.startsWith(assetsDir + sep))
|
|
113
|
+
throw usage(`asset path escapes assets dir: ${abs}`);
|
|
114
|
+
return { abs, name, rel: `assets/${name}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function probe({ asset, root, designRootRel, frames, outDir }) {
|
|
118
|
+
const { abs, name, rel } = resolveAsset(root, designRootRel, asset);
|
|
119
|
+
const nFrames = Number.isInteger(frames) && frames > 0 ? Math.min(frames, 64) : DEFAULT_FRAMES;
|
|
120
|
+
const dir =
|
|
121
|
+
outDir || join(process.env.TMPDIR || '/tmp', `maude-footage-${name.replace(/\./g, '_')}`);
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
const browser = await launchChromium({
|
|
125
|
+
headless: true,
|
|
126
|
+
// file:// page loading a file:// video subresource needs this flag.
|
|
127
|
+
args: ['--allow-file-access-from-files', '--autoplay-policy=no-user-gesture-required'],
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
const page = await browser.newPage();
|
|
131
|
+
// A real file:// host page (NOT about:blank — its opaque origin can't load a
|
|
132
|
+
// file:// subresource even with --allow-file-access-from-files). With a
|
|
133
|
+
// file:// page + the flag, the clip loads as a same-scheme cross-dir resource.
|
|
134
|
+
const hostHtml = join(dir, '_probe.html');
|
|
135
|
+
writeFileSync(hostHtml, '<!doctype html><meta charset="utf-8"><body style="margin:0"></body>');
|
|
136
|
+
await page.goto(pathToFileURL(hostHtml).href);
|
|
137
|
+
|
|
138
|
+
// Load metadata: duration + intrinsic dimensions.
|
|
139
|
+
const fileUrl = pathToFileURL(abs).href;
|
|
140
|
+
const meta = await page.evaluate(async (url) => {
|
|
141
|
+
const v = document.createElement('video');
|
|
142
|
+
v.id = 'probe-v';
|
|
143
|
+
v.muted = true;
|
|
144
|
+
v.playsInline = true;
|
|
145
|
+
v.preload = 'auto';
|
|
146
|
+
v.style.position = 'fixed';
|
|
147
|
+
v.style.left = '0';
|
|
148
|
+
v.style.top = '0';
|
|
149
|
+
v.style.margin = '0';
|
|
150
|
+
document.body.style.margin = '0';
|
|
151
|
+
document.body.appendChild(v);
|
|
152
|
+
const done = new Promise((res, rej) => {
|
|
153
|
+
v.addEventListener('loadedmetadata', () => res(true), { once: true });
|
|
154
|
+
v.addEventListener('error', () => rej(new Error('video error while loading metadata')), {
|
|
155
|
+
once: true,
|
|
156
|
+
});
|
|
157
|
+
setTimeout(() => rej(new Error('timed out loading metadata (15s)')), 15000);
|
|
158
|
+
});
|
|
159
|
+
v.src = url;
|
|
160
|
+
await done;
|
|
161
|
+
return { duration: v.duration, width: v.videoWidth, height: v.videoHeight };
|
|
162
|
+
}, fileUrl);
|
|
163
|
+
|
|
164
|
+
if (!Number.isFinite(meta.duration) || meta.duration <= 0 || !meta.width || !meta.height) {
|
|
165
|
+
throw probeError(
|
|
166
|
+
`clip has no seekable video track (duration=${meta.duration}, ${meta.width}x${meta.height})`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Size the <video> element to its intrinsic size, capped, and set the viewport
|
|
171
|
+
// to match so an element screenshot captures the whole frame at 1:1-ish.
|
|
172
|
+
const scale = Math.min(1, MAX_FRAME_EDGE / Math.max(meta.width, meta.height));
|
|
173
|
+
const drawW = Math.max(1, Math.round(meta.width * scale));
|
|
174
|
+
const drawH = Math.max(1, Math.round(meta.height * scale));
|
|
175
|
+
await page.setViewportSize({ width: drawW, height: drawH });
|
|
176
|
+
await page.evaluate(
|
|
177
|
+
({ w, h }) => {
|
|
178
|
+
const v = document.getElementById('probe-v');
|
|
179
|
+
v.style.width = `${w}px`;
|
|
180
|
+
v.style.height = `${h}px`;
|
|
181
|
+
// Expose a promise-returning seek helper for the frame loop.
|
|
182
|
+
window.__seekTo = (t) =>
|
|
183
|
+
new Promise((res, rej) => {
|
|
184
|
+
const onSeeked = () => {
|
|
185
|
+
v.removeEventListener('seeked', onSeeked);
|
|
186
|
+
res(true);
|
|
187
|
+
};
|
|
188
|
+
v.addEventListener('seeked', onSeeked);
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
v.removeEventListener('seeked', onSeeked);
|
|
191
|
+
rej(new Error(`seek to ${t}s timed out`));
|
|
192
|
+
}, 8000);
|
|
193
|
+
v.currentTime = t;
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
{ w: drawW, h: drawH }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const video = page.locator('#probe-v');
|
|
200
|
+
const outFrames = [];
|
|
201
|
+
for (let i = 0; i < nFrames; i += 1) {
|
|
202
|
+
// Even midpoints across (0, duration) — avoids the exact-0 / exact-end
|
|
203
|
+
// frames some codecs won't decode cleanly.
|
|
204
|
+
const t = (meta.duration * (i + 0.5)) / nFrames;
|
|
205
|
+
try {
|
|
206
|
+
await page.evaluate((tt) => window.__seekTo(tt), t);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// A single flaky seek shouldn't abort the whole probe — skip this frame.
|
|
209
|
+
outFrames.push({
|
|
210
|
+
index: i,
|
|
211
|
+
t: Number(t.toFixed(3)),
|
|
212
|
+
png: null,
|
|
213
|
+
error: String(e?.message ?? e),
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const png = join(dir, `frame-${String(i).padStart(2, '0')}.png`);
|
|
218
|
+
await video.screenshot({ path: png });
|
|
219
|
+
outFrames.push({ index: i, t: Number(t.toFixed(3)), png });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
asset: rel,
|
|
224
|
+
durationSec: Number(meta.duration.toFixed(3)),
|
|
225
|
+
width: meta.width,
|
|
226
|
+
height: meta.height,
|
|
227
|
+
outDir: dir,
|
|
228
|
+
frames: outFrames,
|
|
229
|
+
};
|
|
230
|
+
} finally {
|
|
231
|
+
await browser.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main() {
|
|
236
|
+
let opts;
|
|
237
|
+
try {
|
|
238
|
+
opts = parseArgv(process.argv.slice(2));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
process.stderr.write(`probe-footage: ${err.message}\n`);
|
|
241
|
+
process.exit(err.code ?? 2);
|
|
242
|
+
}
|
|
243
|
+
if (opts.help) {
|
|
244
|
+
process.stdout.write(`${HELP}\n`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!opts.asset) {
|
|
248
|
+
process.stderr.write('probe-footage: <assets/<sha8>.<ext>> required\n');
|
|
249
|
+
process.exit(2);
|
|
250
|
+
}
|
|
251
|
+
const root = opts.root || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
252
|
+
try {
|
|
253
|
+
const r = await probe({
|
|
254
|
+
asset: opts.asset,
|
|
255
|
+
root,
|
|
256
|
+
designRootRel: opts.designRoot,
|
|
257
|
+
frames: opts.frames,
|
|
258
|
+
outDir: opts.outDir,
|
|
259
|
+
});
|
|
260
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
process.stderr.write(`probe-footage: ${err.message}\n`);
|
|
263
|
+
process.exit(err.code ?? 1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
268
|
+
main();
|
|
269
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Invocation (called by screenshot.sh — not directly by users):
|
|
7
7
|
// npm exec --package=playwright -- node _screenshot-playwright.mjs \
|
|
8
|
-
// --url <url> [--selector <css>] --out <path> [--timeout 8]
|
|
8
|
+
// --url <url> [--selector <css>] --out <path> [--timeout 8] [--theme <name>]
|
|
9
9
|
//
|
|
10
10
|
// First invocation may install chromium (~150 MB). Subsequent runs reuse cache.
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ const args = Object.fromEntries(
|
|
|
20
20
|
}, [])
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const { url, selector, out, timeout = '8' } = args;
|
|
23
|
+
const { url, selector, out, timeout = '8', theme } = args;
|
|
24
24
|
if (!url || !out) {
|
|
25
25
|
console.error(
|
|
26
26
|
'usage: _screenshot-playwright.mjs --url <url> [--selector <css>] --out <path> [--timeout 8]'
|
|
@@ -37,6 +37,21 @@ try {
|
|
|
37
37
|
const page = await ctx.newPage();
|
|
38
38
|
await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
|
|
39
39
|
|
|
40
|
+
if (theme) {
|
|
41
|
+
// Dual-theme reality check (/design:new step 9): force every DS artboard
|
|
42
|
+
// theme wrapper to `theme` before capture, deterministically, instead of
|
|
43
|
+
// relying on whatever the canvas is pinned to. No-op if the canvas has no
|
|
44
|
+
// `[data-theme]` elements (e.g. a single-theme DS).
|
|
45
|
+
const n = await page.evaluate((t) => {
|
|
46
|
+
const els = document.querySelectorAll('[data-theme]');
|
|
47
|
+
els.forEach((el) => {
|
|
48
|
+
el.setAttribute('data-theme', t);
|
|
49
|
+
});
|
|
50
|
+
return els.length;
|
|
51
|
+
}, theme);
|
|
52
|
+
console.error(`→ theme override: forced data-theme="${theme}" on ${n} element(s)`);
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
if (selector) {
|
|
41
56
|
const loc = page.locator(selector).first();
|
|
42
57
|
await loc.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
@@ -70,10 +70,6 @@ try {
|
|
|
70
70
|
world.style.zoom = '1';
|
|
71
71
|
world.style.transform = 'none';
|
|
72
72
|
}
|
|
73
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
74
|
-
el.style.left = '0px';
|
|
75
|
-
el.style.top = '0px';
|
|
76
|
-
}
|
|
77
73
|
});
|
|
78
74
|
// Inject dom-to-svg into the page. Bundle attaches its exports under
|
|
79
75
|
// `window.domToSvg`.
|
|
@@ -81,6 +77,27 @@ try {
|
|
|
81
77
|
|
|
82
78
|
const written = [];
|
|
83
79
|
|
|
80
|
+
// Pin one artboard to (0,0) for its capture, returning a restore function.
|
|
81
|
+
// Per-target rather than a one-shot reset of every `[data-dc-screen]` — a
|
|
82
|
+
// multi-target loop must not leave an already-captured artboard sitting at
|
|
83
|
+
// the origin, overlapping the next target's geometry (the scatter bug).
|
|
84
|
+
const pinArtboard = async (handle) => {
|
|
85
|
+
const saved = await handle.evaluate((el) => {
|
|
86
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
87
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
88
|
+
ab.style.left = '0px';
|
|
89
|
+
ab.style.top = '0px';
|
|
90
|
+
return prev;
|
|
91
|
+
});
|
|
92
|
+
return async () => {
|
|
93
|
+
await handle.evaluate((el, prev) => {
|
|
94
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
95
|
+
ab.style.left = prev.left;
|
|
96
|
+
ab.style.top = prev.top;
|
|
97
|
+
}, saved);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
84
101
|
// Single in-page serializer used by BOTH branches (single + multi) so they
|
|
85
102
|
// can't drift again (the `formatXML` 500 bug came from a divergent copy).
|
|
86
103
|
// Note: dom-to-svg exports only elementToSVG / inlineResources /
|
|
@@ -228,11 +245,14 @@ try {
|
|
|
228
245
|
for (let i = 0; i < screens.length; i += 1) {
|
|
229
246
|
const handle = screens[i];
|
|
230
247
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
248
|
+
const restore = await pinArtboard(handle);
|
|
231
249
|
// Each multi handle is already a `[data-dc-screen]` artboard — no widen.
|
|
232
250
|
const svg = await serializeOne(handle, false);
|
|
251
|
+
await restore();
|
|
233
252
|
const target = join(outDir, `${id}.svg`);
|
|
234
253
|
writeFileSync(target, svg, 'utf8');
|
|
235
254
|
written.push(target);
|
|
255
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
236
256
|
}
|
|
237
257
|
} else {
|
|
238
258
|
if (!out) {
|
|
@@ -242,7 +262,9 @@ try {
|
|
|
242
262
|
mkdirSync(dirname(out), { recursive: true });
|
|
243
263
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
244
264
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
265
|
+
const restore = await pinArtboard(handle);
|
|
245
266
|
const svg = await serializeOne(handle, widen);
|
|
267
|
+
await restore();
|
|
246
268
|
writeFileSync(out, svg, 'utf8');
|
|
247
269
|
written.push(out);
|
|
248
270
|
}
|
|
@@ -126,84 +126,132 @@ try {
|
|
|
126
126
|
// — both fall through to the frame-step path below.
|
|
127
127
|
const useRenderer = !!renderLib && format !== 'gif' && !!compId;
|
|
128
128
|
|
|
129
|
+
// When the whole-comp renderer path throws we DON'T fail the export. The most
|
|
130
|
+
// common cause is a `RangeError: Maximum call stack size exceeded` from
|
|
131
|
+
// @remotion/web-renderer's guardless recursive DOM precompositing on a
|
|
132
|
+
// deeply-nested comp (RCA issue-video-mp4-rendermediaonweb-stack-overflow):
|
|
133
|
+
// one native recursion level per nested opacity/transform/filter/mask layer,
|
|
134
|
+
// no depth cap. Instead we degrade to the frame-step screenshot + mediabunny
|
|
135
|
+
// path below, which goes through Chromium's native compositor and is immune to
|
|
136
|
+
// that recursion. The one cost is audio — the frame-step path is video-only —
|
|
137
|
+
// so a comp that overflows the audio renderer still exports, just muted. That
|
|
138
|
+
// beats a hard failure with no file. `video.ts` always passes --encode-lib
|
|
139
|
+
// (only --render-lib is conditional), so the fallback has all it needs here.
|
|
140
|
+
let renderFallbackReason = null;
|
|
141
|
+
|
|
129
142
|
if (useRenderer) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
try {
|
|
144
|
+
const t0 = Date.now();
|
|
145
|
+
const libSrc = readFileSync(renderLib, 'utf8');
|
|
146
|
+
await page.addScriptTag({ content: libSrc, type: 'module' });
|
|
147
|
+
await page.waitForFunction(() => typeof window.__maudeRenderVideo__ === 'function', {
|
|
148
|
+
timeout: timeoutMs,
|
|
149
|
+
});
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
const explicitFrames = Number(framesArg);
|
|
152
|
+
const frameRange =
|
|
153
|
+
Number.isFinite(explicitFrames) && explicitFrames > 0 ? [0, explicitFrames - 1] : null;
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
const rendered = await page.evaluate(
|
|
156
|
+
async ({ compId, container, scale, muted, frameRange, licenseKey }) => {
|
|
157
|
+
return window.__maude_render_video__(compId, {
|
|
158
|
+
container,
|
|
159
|
+
scale,
|
|
160
|
+
muted,
|
|
161
|
+
frameRange,
|
|
162
|
+
licenseKey,
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
compId,
|
|
167
|
+
container: format,
|
|
168
|
+
scale: deviceScaleFactor,
|
|
169
|
+
muted: !wantAudio,
|
|
147
170
|
frameRange,
|
|
148
171
|
licenseKey,
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
compId,
|
|
153
|
-
container: format,
|
|
154
|
-
scale: deviceScaleFactor,
|
|
155
|
-
muted: !wantAudio,
|
|
156
|
-
frameRange,
|
|
157
|
-
licenseKey,
|
|
158
|
-
}
|
|
159
|
-
);
|
|
172
|
+
}
|
|
173
|
+
);
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
writeFileSync(out, Buffer.from(rendered.b64, 'base64'));
|
|
162
176
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
177
|
+
// Comp meta for the summary (diagnostic only — video.ts reads `container`).
|
|
178
|
+
const compMeta = await page.evaluate(
|
|
179
|
+
(id) =>
|
|
180
|
+
(typeof window.__maude_comps__ === 'function' ? window.__maude_comps__() : []).find(
|
|
181
|
+
(c) => c.id === id
|
|
182
|
+
) ?? null,
|
|
183
|
+
compId
|
|
184
|
+
);
|
|
185
|
+
const frameCount = frameRange
|
|
186
|
+
? frameRange[1] - frameRange[0] + 1
|
|
187
|
+
: (compMeta?.durationInFrames ?? 0);
|
|
188
|
+
|
|
189
|
+
const result = {
|
|
190
|
+
fps: compMeta?.fps ?? null,
|
|
191
|
+
frameCount,
|
|
192
|
+
width: Math.round((compMeta?.width ?? 0) * deviceScaleFactor),
|
|
193
|
+
height: Math.round((compMeta?.height ?? 0) * deviceScaleFactor),
|
|
194
|
+
out,
|
|
195
|
+
bytes: rendered.bytes,
|
|
196
|
+
container: rendered.container,
|
|
197
|
+
codec: rendered.videoCodec,
|
|
198
|
+
audioCodec: rendered.audioCodec,
|
|
199
|
+
};
|
|
200
|
+
const ms = Date.now() - t0;
|
|
201
|
+
console.error(
|
|
202
|
+
`✓ rendered ${rendered.container}/${rendered.videoCodec}` +
|
|
203
|
+
`${rendered.audioCodec ? `+${rendered.audioCodec}` : ' (muted)'} → ${out} (${rendered.bytes} B) in ${ms}ms`
|
|
204
|
+
);
|
|
205
|
+
console.log(JSON.stringify(result));
|
|
206
|
+
} catch (err) {
|
|
207
|
+
// Collapse newlines — the message carries an in-page (untrusted canvas,
|
|
208
|
+
// DDR-054) error + stack, and it flows to console.error / the job's
|
|
209
|
+
// persisted error, so a forged "\n[FATAL] …" line can't be injected into
|
|
210
|
+
// the dev-server's stderr or the history ledger.
|
|
211
|
+
renderFallbackReason = oneLine(err instanceof Error ? err.message : String(err));
|
|
212
|
+
console.error(
|
|
213
|
+
`⚠ renderMediaOnWeb failed (${renderFallbackReason}); falling back to ` +
|
|
214
|
+
'frame-step screenshot capture — video will export WITHOUT audio'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Frame-step path: the normal route for gif / ordinary / comp-less artboards
|
|
220
|
+
// (useRenderer === false), AND the graceful-degradation fallback when the
|
|
221
|
+
// renderer above threw (renderFallbackReason !== null).
|
|
222
|
+
if (!useRenderer || renderFallbackReason !== null) {
|
|
223
|
+
try {
|
|
224
|
+
await frameStepCapture({
|
|
225
|
+
page,
|
|
226
|
+
handle,
|
|
227
|
+
compId,
|
|
228
|
+
fpsArg,
|
|
229
|
+
framesArg,
|
|
230
|
+
dumpDir,
|
|
231
|
+
encodeLib,
|
|
232
|
+
format,
|
|
233
|
+
out,
|
|
234
|
+
mode,
|
|
235
|
+
deviceScaleFactor,
|
|
236
|
+
timeoutMs,
|
|
237
|
+
fallbackReason: renderFallbackReason,
|
|
238
|
+
});
|
|
239
|
+
} catch (fallbackErr) {
|
|
240
|
+
// If we only reached the frame-step path because the renderer already
|
|
241
|
+
// failed, surface BOTH failures so a genuine bug isn't hidden behind the
|
|
242
|
+
// fallback's own error.
|
|
243
|
+
if (renderFallbackReason !== null) {
|
|
244
|
+
const fb = oneLine(
|
|
245
|
+
fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
|
|
246
|
+
);
|
|
247
|
+
throw new Error(
|
|
248
|
+
`video export failed on both paths — renderMediaOnWeb: ${renderFallbackReason}; ` +
|
|
249
|
+
`frame-step fallback: ${fb}`,
|
|
250
|
+
{ cause: fallbackErr }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
throw fallbackErr;
|
|
254
|
+
}
|
|
207
255
|
}
|
|
208
256
|
} finally {
|
|
209
257
|
await browser.close();
|
|
@@ -227,6 +275,10 @@ async function frameStepCapture({
|
|
|
227
275
|
mode,
|
|
228
276
|
deviceScaleFactor,
|
|
229
277
|
timeoutMs,
|
|
278
|
+
// Set (to the renderer's error message) only when this run is the
|
|
279
|
+
// graceful-degradation fallback for a failed renderMediaOnWeb path; stamps
|
|
280
|
+
// `degraded`/`audioDropped` onto the stdout summary so video.ts can warn.
|
|
281
|
+
fallbackReason = null,
|
|
230
282
|
}) {
|
|
231
283
|
const rect0 = await handle.evaluate((el) => {
|
|
232
284
|
const r = el.getBoundingClientRect();
|
|
@@ -352,10 +404,22 @@ async function frameStepCapture({
|
|
|
352
404
|
{ b64, isGif }
|
|
353
405
|
);
|
|
354
406
|
}
|
|
407
|
+
// Machine-readable progress (stdout) — spawnShim parses `MAUDE_PROGRESS`
|
|
408
|
+
// lines → onProgress → job.progress → the live bar in the Exports panel, so
|
|
409
|
+
// a long frame-step render (the renderMediaOnWeb-overflow fallback) shows
|
|
410
|
+
// "N of M" instead of a static "Exporting…". Filtered out of the summary
|
|
411
|
+
// stdout the exporter reads. Human log stays throttled on stderr.
|
|
412
|
+
console.log(`MAUDE_PROGRESS {"current":${f + 1},"total":${frameCount}}`);
|
|
355
413
|
if (f % 30 === 0) console.error(`frame ${f + 1}/${frameCount}`);
|
|
356
414
|
}
|
|
357
415
|
|
|
358
416
|
let result = { fps, frameCount, width: outW, height: outH, framePaths };
|
|
417
|
+
// Degradation marker — this run only encodes video (no audio), so when we're
|
|
418
|
+
// standing in for a failed audio renderer, tell the exporter the export is
|
|
419
|
+
// degraded and audio was dropped (RCA issue-video-mp4-rendermediaonweb-stack-overflow).
|
|
420
|
+
if (fallbackReason !== null) {
|
|
421
|
+
result = { ...result, degraded: true, audioDropped: true, fallbackReason };
|
|
422
|
+
}
|
|
359
423
|
if (encoding) {
|
|
360
424
|
const enc = await page.evaluate(
|
|
361
425
|
async ({ isGif }) =>
|
|
@@ -450,3 +514,9 @@ async function seekFrame(page, frame, fps, mode) {
|
|
|
450
514
|
function cssEscape(s) {
|
|
451
515
|
return String(s).replace(/["\\\]]/g, '\\$&');
|
|
452
516
|
}
|
|
517
|
+
|
|
518
|
+
/** Collapse CR/LF runs to a single space — used to neutralize log/ledger
|
|
519
|
+
* injection from untrusted in-page (canvas-origin, DDR-054) error text. */
|
|
520
|
+
function oneLine(s) {
|
|
521
|
+
return String(s).replace(/[\r\n]+/g, ' ');
|
|
522
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ingest-footage.sh — thin shim over _ingest-footage.mjs; reached via
|
|
3
|
+
# `maude design ingest-footage` (DDR-062), never a raw bin path. See
|
|
4
|
+
# _ingest-footage.mjs for the full rationale (feature-footage-analysis-director).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ingest-footage.sh <dir> --root <repo> [--design-root .design]
|
|
8
|
+
# [--recursive] [--max-bytes N]
|
|
9
|
+
#
|
|
10
|
+
# stdout on success = a JSON manifest { clips, skipped, assetsDir }.
|
|
11
|
+
# Exit: 0 ok (even with skips) · 2 usage · 6 write/containment error · 1 other.
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
|
|
14
|
+
case "$1" in
|
|
15
|
+
--help|-h) sed -n '2,14p' "$0" | sed 's/^# \?//'; exit 0 ;;
|
|
16
|
+
esac
|
|
17
|
+
|
|
18
|
+
# Prefer node (always present with a maude install); fall back to bun in a dev
|
|
19
|
+
# tree that has bun but a shimmed node. The module is pure Node ESM — no .ts.
|
|
20
|
+
if command -v node >/dev/null 2>&1; then
|
|
21
|
+
exec node "$SCRIPT_DIR/_ingest-footage.mjs" "$@"
|
|
22
|
+
elif command -v bun >/dev/null 2>&1; then
|
|
23
|
+
exec bun run "$SCRIPT_DIR/_ingest-footage.mjs" "$@"
|
|
24
|
+
else
|
|
25
|
+
echo "ingest-footage.sh: node or bun is required." >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|