@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,386 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// _ingest-footage.mjs — ingest a DIRECTORY of raw footage into the design root's
|
|
3
|
+
// content-addressed asset store, behind `maude design ingest-footage` (DDR-062
|
|
4
|
+
// dispatch). The folder-scale counterpart to `_fetch-asset.mjs` (single URL).
|
|
5
|
+
//
|
|
6
|
+
// feature-footage-analysis-director, Task 3. WHY THIS EXISTS: the `/design:reel`
|
|
7
|
+
// director pipeline wants to start from a folder of client clips
|
|
8
|
+
// (`/…/podklady/video`). This verb copies each recognised video (and any image/
|
|
9
|
+
// audio) into `<designRoot>/assets/<sha8>.<ext>` — the SAME content-addressed
|
|
10
|
+
// convention `/_api/asset` + `_fetch-asset.mjs` use, so a clip references
|
|
11
|
+
// uniformly as `assets/<sha8>.mp4` on both the main and the canvas origin.
|
|
12
|
+
//
|
|
13
|
+
// SECURITY / DISCIPLINE (mirrors _fetch-asset.mjs):
|
|
14
|
+
// • magic-byte sniff decides the category + extension — the source name /
|
|
15
|
+
// extension is NEVER trusted (a `.mp4` that is really an HTML polyglot →
|
|
16
|
+
// skipped). Mirrors api.ts sniffAssetType (mp4/mov/m4v/webm video · mp3/wav/
|
|
17
|
+
// m4a audio · png/jpg/gif/webp image); SVG/script → null → skipped.
|
|
18
|
+
// • content-addressed `<sha8>.<ext>` name, `[a-z0-9._-]` only, written FLAT
|
|
19
|
+
// under assets/ with a realpath containment assertion.
|
|
20
|
+
// • per-file size cap (default 100 MB — the DDR-148 video ceiling); oversized
|
|
21
|
+
// files are SKIPPED LOUDLY (listed in the manifest), never silently dropped.
|
|
22
|
+
// • the source directory is READ-ONLY — this never writes back into it.
|
|
23
|
+
// • no network, no shell interpolation, no container parsing (magic bytes only).
|
|
24
|
+
//
|
|
25
|
+
// Reached via `maude design ingest-footage <dir> --root <repo>`, never a raw path.
|
|
26
|
+
// stdout (default) = a JSON manifest { clips:[{asset,src,bytes,category}],
|
|
27
|
+
// skipped:[{src,why}], assetsDir }. Exit: 0 ok (even with skips) · 2 usage ·
|
|
28
|
+
// 6 write/containment error · 1 other.
|
|
29
|
+
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
import {
|
|
32
|
+
closeSync,
|
|
33
|
+
copyFileSync,
|
|
34
|
+
createReadStream,
|
|
35
|
+
existsSync,
|
|
36
|
+
mkdirSync,
|
|
37
|
+
openSync,
|
|
38
|
+
readdirSync,
|
|
39
|
+
readSync,
|
|
40
|
+
statSync,
|
|
41
|
+
} from 'node:fs';
|
|
42
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
43
|
+
import { pathToFileURL } from 'node:url';
|
|
44
|
+
|
|
45
|
+
// The DDR-148 video ceiling (100 MB). Images/audio also ride this generous cap
|
|
46
|
+
// here — the reel pipeline's inputs are dominated by video. Override per-file
|
|
47
|
+
// via --max-bytes.
|
|
48
|
+
const DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
49
|
+
|
|
50
|
+
class IngestError extends Error {
|
|
51
|
+
constructor(code, message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── magic-byte sniff (mirrors apps/studio/api.ts sniffAssetType) ──────────────
|
|
58
|
+
|
|
59
|
+
/** Sniff → { ext, category } or null. Bytes decide; name/extension untrusted. */
|
|
60
|
+
export function sniffAsset(b) {
|
|
61
|
+
// Images (mirrors sniffImageType).
|
|
62
|
+
if (
|
|
63
|
+
b.length >= 8 &&
|
|
64
|
+
b[0] === 0x89 &&
|
|
65
|
+
b[1] === 0x50 &&
|
|
66
|
+
b[2] === 0x4e &&
|
|
67
|
+
b[3] === 0x47 &&
|
|
68
|
+
b[4] === 0x0d &&
|
|
69
|
+
b[5] === 0x0a &&
|
|
70
|
+
b[6] === 0x1a &&
|
|
71
|
+
b[7] === 0x0a
|
|
72
|
+
)
|
|
73
|
+
return { ext: 'png', category: 'image' };
|
|
74
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
75
|
+
return { ext: 'jpg', category: 'image' };
|
|
76
|
+
if (
|
|
77
|
+
b.length >= 6 &&
|
|
78
|
+
b[0] === 0x47 &&
|
|
79
|
+
b[1] === 0x49 &&
|
|
80
|
+
b[2] === 0x46 &&
|
|
81
|
+
b[3] === 0x38 &&
|
|
82
|
+
(b[4] === 0x37 || b[4] === 0x39) &&
|
|
83
|
+
b[5] === 0x61
|
|
84
|
+
)
|
|
85
|
+
return { ext: 'gif', category: 'image' };
|
|
86
|
+
if (
|
|
87
|
+
b.length >= 12 &&
|
|
88
|
+
b[0] === 0x52 &&
|
|
89
|
+
b[1] === 0x49 &&
|
|
90
|
+
b[2] === 0x46 &&
|
|
91
|
+
b[3] === 0x46 &&
|
|
92
|
+
b[8] === 0x57 &&
|
|
93
|
+
b[9] === 0x45 &&
|
|
94
|
+
b[10] === 0x42 &&
|
|
95
|
+
b[11] === 0x50
|
|
96
|
+
)
|
|
97
|
+
return { ext: 'webp', category: 'image' };
|
|
98
|
+
// ISO-BMFF (mp4 / mov / m4a / m4v): "ftyp" at offset 4, brand at 8.
|
|
99
|
+
if (b.length >= 12 && b[4] === 0x66 && b[5] === 0x74 && b[6] === 0x79 && b[7] === 0x70) {
|
|
100
|
+
const brand = String.fromCharCode(b[8] ?? 0, b[9] ?? 0, b[10] ?? 0, b[11] ?? 0);
|
|
101
|
+
if (brand === 'qt ') return { ext: 'mov', category: 'video' };
|
|
102
|
+
if (brand.startsWith('M4A')) return { ext: 'm4a', category: 'audio' };
|
|
103
|
+
if (brand.startsWith('M4V')) return { ext: 'm4v', category: 'video' };
|
|
104
|
+
return { ext: 'mp4', category: 'video' };
|
|
105
|
+
}
|
|
106
|
+
// Matroska / WebM — EBML header.
|
|
107
|
+
if (b.length >= 4 && b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
|
|
108
|
+
return { ext: 'webm', category: 'video' };
|
|
109
|
+
// MP3 — "ID3" or frame-sync.
|
|
110
|
+
if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
|
|
111
|
+
return { ext: 'mp3', category: 'audio' };
|
|
112
|
+
if (b.length >= 2 && b[0] === 0xff && ((b[1] ?? 0) & 0xe0) === 0xe0)
|
|
113
|
+
return { ext: 'mp3', category: 'audio' };
|
|
114
|
+
// WAV.
|
|
115
|
+
if (
|
|
116
|
+
b.length >= 12 &&
|
|
117
|
+
b[0] === 0x52 &&
|
|
118
|
+
b[1] === 0x49 &&
|
|
119
|
+
b[2] === 0x46 &&
|
|
120
|
+
b[3] === 0x46 &&
|
|
121
|
+
b[8] === 0x57 &&
|
|
122
|
+
b[9] === 0x41 &&
|
|
123
|
+
b[10] === 0x56 &&
|
|
124
|
+
b[11] === 0x45
|
|
125
|
+
)
|
|
126
|
+
return { ext: 'wav', category: 'audio' };
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── path helpers (mirror _fetch-asset.mjs containment) ───────────────────────
|
|
131
|
+
|
|
132
|
+
/** Resolve `<root>/<designRootRel>/assets` with a containment assertion. */
|
|
133
|
+
export function assetsDirFor(root, designRootRel) {
|
|
134
|
+
const rootAbs = resolve(root);
|
|
135
|
+
const assetsDir = resolve(rootAbs, designRootRel, 'assets');
|
|
136
|
+
if (assetsDir !== rootAbs && !assetsDir.startsWith(rootAbs + sep))
|
|
137
|
+
throw new IngestError(6, `assets dir escapes root: ${assetsDir}`);
|
|
138
|
+
return assetsDir;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Assert a content-addressed name and resolve it inside assets/ (no traversal). */
|
|
142
|
+
export function containedName(assetsDir, name) {
|
|
143
|
+
if (!/^[a-z0-9]{8}\.(png|jpg|gif|webp|mp4|mov|m4v|webm|mp3|wav|m4a)$/.test(name))
|
|
144
|
+
throw new IngestError(6, `generated name failed the charset contract: ${name}`);
|
|
145
|
+
const fileAbs = resolve(assetsDir, name);
|
|
146
|
+
if (fileAbs !== join(assetsDir, name) || !fileAbs.startsWith(assetsDir + sep))
|
|
147
|
+
throw new IngestError(6, `resolved asset path escapes assets dir: ${fileAbs}`);
|
|
148
|
+
return fileAbs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── sniff head + full-file hash (streamed — clips can be 100 MB) ─────────────
|
|
152
|
+
|
|
153
|
+
/** Read the first `n` bytes of a file for the magic-byte sniff. */
|
|
154
|
+
function readHead(file, n = 16) {
|
|
155
|
+
const fd = openSync(file, 'r');
|
|
156
|
+
try {
|
|
157
|
+
const buf = Buffer.alloc(n);
|
|
158
|
+
const bytesRead = readSync(fd, buf, 0, n, 0);
|
|
159
|
+
return buf.subarray(0, bytesRead);
|
|
160
|
+
} finally {
|
|
161
|
+
closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Full-file sha256 (streamed) → first 8 hex chars. */
|
|
166
|
+
function hashFile(file) {
|
|
167
|
+
return new Promise((res, rej) => {
|
|
168
|
+
const h = createHash('sha256');
|
|
169
|
+
createReadStream(file)
|
|
170
|
+
.on('error', rej)
|
|
171
|
+
.on('data', (d) => h.update(d))
|
|
172
|
+
.on('end', () => res(h.digest('hex').slice(0, 8)));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── directory walk + ingest ──────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function listFiles(dir, recursive) {
|
|
179
|
+
const out = [];
|
|
180
|
+
const walk = (d) => {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new IngestError(2, `cannot read directory ${d}: ${e?.message ?? e}`);
|
|
186
|
+
}
|
|
187
|
+
for (const ent of entries) {
|
|
188
|
+
if (ent.name.startsWith('.')) continue; // skip dotfiles / .DS_Store
|
|
189
|
+
const abs = join(d, ent.name);
|
|
190
|
+
if (ent.isDirectory()) {
|
|
191
|
+
if (recursive) walk(abs);
|
|
192
|
+
} else if (ent.isFile()) {
|
|
193
|
+
out.push(abs);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
walk(dir);
|
|
198
|
+
return out.sort();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ingest every recognised media file under `dir` into assets/. Returns
|
|
203
|
+
* { clips, skipped, assetsDir }. Never throws on a per-file problem (that file
|
|
204
|
+
* is skipped with a reason); throws IngestError only for whole-run failures
|
|
205
|
+
* (unreadable dir, containment breach).
|
|
206
|
+
*/
|
|
207
|
+
export async function ingestFootage({
|
|
208
|
+
dir,
|
|
209
|
+
root,
|
|
210
|
+
designRootRel = '.design',
|
|
211
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
212
|
+
recursive = false,
|
|
213
|
+
}) {
|
|
214
|
+
const srcDir = resolve(dir);
|
|
215
|
+
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory())
|
|
216
|
+
throw new IngestError(2, `not a directory: ${srcDir}`);
|
|
217
|
+
|
|
218
|
+
const assetsDir = assetsDirFor(root, designRootRel);
|
|
219
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
const clips = [];
|
|
222
|
+
const skipped = [];
|
|
223
|
+
const seen = new Set(); // sha8 dedupe within this run
|
|
224
|
+
|
|
225
|
+
for (const file of listFiles(srcDir, recursive)) {
|
|
226
|
+
let size;
|
|
227
|
+
try {
|
|
228
|
+
size = statSync(file).size;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
skipped.push({ src: file, why: `stat failed: ${e?.message ?? e}` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (size === 0) {
|
|
234
|
+
skipped.push({ src: file, why: 'empty file' });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (size > maxBytes) {
|
|
238
|
+
skipped.push({
|
|
239
|
+
src: file,
|
|
240
|
+
why: `exceeds the ${Math.round(maxBytes / (1024 * 1024))} MB per-file cap (${Math.round(size / (1024 * 1024))} MB)`,
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let info;
|
|
245
|
+
try {
|
|
246
|
+
info = sniffAsset(readHead(file));
|
|
247
|
+
} catch (e) {
|
|
248
|
+
skipped.push({ src: file, why: `read failed: ${e?.message ?? e}` });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!info) {
|
|
252
|
+
skipped.push({
|
|
253
|
+
src: file,
|
|
254
|
+
why: `unrecognised media (not video/image/audio); ext ${extname(file) || '(none)'}`,
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
let sha8;
|
|
259
|
+
try {
|
|
260
|
+
sha8 = await hashFile(file);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
skipped.push({ src: file, why: `hash failed: ${e?.message ?? e}` });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const name = `${sha8}.${info.ext}`;
|
|
266
|
+
let fileAbs;
|
|
267
|
+
try {
|
|
268
|
+
fileAbs = containedName(assetsDir, name);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
skipped.push({ src: file, why: e?.message ?? 'containment error' });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Content-addressed dedupe: identical bytes → identical name → copy once.
|
|
274
|
+
if (!existsSync(fileAbs)) {
|
|
275
|
+
try {
|
|
276
|
+
copyFileSync(file, fileAbs);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
skipped.push({ src: file, why: `copy failed: ${e?.message ?? e}` });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!seen.has(sha8)) {
|
|
283
|
+
seen.add(sha8);
|
|
284
|
+
clips.push({ asset: `assets/${name}`, src: file, bytes: size, category: info.category });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { clips, skipped, assetsDir };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function parseArgv(argv) {
|
|
294
|
+
const out = {
|
|
295
|
+
dir: null,
|
|
296
|
+
root: null,
|
|
297
|
+
designRoot: '.design',
|
|
298
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
299
|
+
recursive: false,
|
|
300
|
+
json: true,
|
|
301
|
+
};
|
|
302
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
303
|
+
const a = argv[i];
|
|
304
|
+
switch (a) {
|
|
305
|
+
case '--root':
|
|
306
|
+
out.root = argv[++i];
|
|
307
|
+
break;
|
|
308
|
+
case '--design-root':
|
|
309
|
+
out.designRoot = argv[++i];
|
|
310
|
+
break;
|
|
311
|
+
case '--max-bytes':
|
|
312
|
+
out.maxBytes = Number(argv[++i]);
|
|
313
|
+
break;
|
|
314
|
+
case '--recursive':
|
|
315
|
+
out.recursive = true;
|
|
316
|
+
break;
|
|
317
|
+
case '--json':
|
|
318
|
+
out.json = true;
|
|
319
|
+
break;
|
|
320
|
+
case '--help':
|
|
321
|
+
case '-h':
|
|
322
|
+
out.help = true;
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
if (a.startsWith('-')) throw new IngestError(2, `unknown flag ${a}`);
|
|
326
|
+
if (out.dir === null) out.dir = a;
|
|
327
|
+
else throw new IngestError(2, `unexpected extra arg ${a}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const HELP = `ingest-footage — copy a folder of raw footage into the design asset store
|
|
334
|
+
(reached via \`maude design ingest-footage\`)
|
|
335
|
+
|
|
336
|
+
Usage:
|
|
337
|
+
maude design ingest-footage <dir> --root <repo> [--design-root .design]
|
|
338
|
+
[--recursive] [--max-bytes N]
|
|
339
|
+
|
|
340
|
+
Walks <dir>, magic-byte-sniffs each file (mp4/mov/m4v/webm video · mp3/wav/m4a
|
|
341
|
+
audio · png/jpg/gif/webp image; SVG/script/unrecognised skipped), content-
|
|
342
|
+
addresses each to <designRoot>/assets/<sha8>.<ext>, and prints a JSON manifest.
|
|
343
|
+
Oversized / unrecognised files are SKIPPED and listed under "skipped" — never
|
|
344
|
+
silently dropped. The source directory is read-only.
|
|
345
|
+
|
|
346
|
+
Exit: 0 ok (even with skips) · 2 usage · 6 write/containment error · 1 other.`;
|
|
347
|
+
|
|
348
|
+
async function main() {
|
|
349
|
+
let opts;
|
|
350
|
+
try {
|
|
351
|
+
opts = parseArgv(process.argv.slice(2));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
process.stderr.write(`ingest-footage: ${err.message}\n`);
|
|
354
|
+
process.exit(err instanceof IngestError ? err.code : 2);
|
|
355
|
+
}
|
|
356
|
+
if (opts.help) {
|
|
357
|
+
process.stdout.write(`${HELP}\n`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!opts.dir) {
|
|
361
|
+
process.stderr.write('ingest-footage: <dir> required\n');
|
|
362
|
+
process.exit(2);
|
|
363
|
+
}
|
|
364
|
+
if (!Number.isFinite(opts.maxBytes) || opts.maxBytes <= 0) {
|
|
365
|
+
process.stderr.write('ingest-footage: --max-bytes must be a positive number\n');
|
|
366
|
+
process.exit(2);
|
|
367
|
+
}
|
|
368
|
+
const root = opts.root || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
369
|
+
try {
|
|
370
|
+
const r = await ingestFootage({
|
|
371
|
+
dir: opts.dir,
|
|
372
|
+
root,
|
|
373
|
+
designRootRel: opts.designRoot,
|
|
374
|
+
maxBytes: opts.maxBytes,
|
|
375
|
+
recursive: opts.recursive,
|
|
376
|
+
});
|
|
377
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
process.stderr.write(`ingest-footage: ${err.message}\n`);
|
|
380
|
+
process.exit(err instanceof IngestError ? err.code : 1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
385
|
+
main();
|
|
386
|
+
}
|
|
@@ -88,11 +88,15 @@ try {
|
|
|
88
88
|
: screens[i];
|
|
89
89
|
// Pin the captured element's own artboard to (0,0) right before its
|
|
90
90
|
// capture so the world's multi-artboard layout doesn't push the bbox off
|
|
91
|
-
// the viewport.
|
|
92
|
-
|
|
91
|
+
// the viewport. Save the prior value so it can be restored right after
|
|
92
|
+
// this artboard's PDF is written — otherwise every artboard captured
|
|
93
|
+
// earlier in the loop stays stacked at (0,0) and bleeds into later pages.
|
|
94
|
+
const savedPos = await handle.evaluate((el) => {
|
|
93
95
|
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
96
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
94
97
|
ab.style.left = '0px';
|
|
95
98
|
ab.style.top = '0px';
|
|
99
|
+
return prev;
|
|
96
100
|
});
|
|
97
101
|
const rect = await handle.evaluate((el) => {
|
|
98
102
|
const r = el.getBoundingClientRect();
|
|
@@ -120,6 +124,13 @@ try {
|
|
|
120
124
|
});
|
|
121
125
|
writeFileSync(targetPath, pdf);
|
|
122
126
|
written.push(targetPath);
|
|
127
|
+
// Restore the artboard's original position now that its page is written.
|
|
128
|
+
await handle.evaluate((el, prev) => {
|
|
129
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
130
|
+
ab.style.left = prev.left;
|
|
131
|
+
ab.style.top = prev.top;
|
|
132
|
+
}, savedPos);
|
|
133
|
+
if (multi) console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
for (const w of written) console.log(w);
|
|
@@ -82,11 +82,16 @@ try {
|
|
|
82
82
|
// artboard #2 (or any non-first selection's host) left the wrong artboard
|
|
83
83
|
// at the origin (item 5 root cause). Each artboard carries
|
|
84
84
|
// `style="left:…; top:…;"` for the world layout — zero the right one so the
|
|
85
|
-
// screenshot clip starts at the viewport origin.
|
|
86
|
-
|
|
85
|
+
// screenshot clip starts at the viewport origin. Save the prior value so it
|
|
86
|
+
// can be restored after this artboard's capture — otherwise every artboard
|
|
87
|
+
// processed earlier in a `--multi` loop is left stacked at (0,0) and bleeds
|
|
88
|
+
// into every subsequent target's clip region (the scatter bug).
|
|
89
|
+
const savedPos = await widenedHandle.evaluate((el) => {
|
|
87
90
|
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
91
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
88
92
|
ab.style.left = '0px';
|
|
89
93
|
ab.style.top = '0px';
|
|
94
|
+
return prev;
|
|
90
95
|
});
|
|
91
96
|
const rect = await widenedHandle.evaluate((el) => {
|
|
92
97
|
const r = el.getBoundingClientRect();
|
|
@@ -118,6 +123,13 @@ try {
|
|
|
118
123
|
},
|
|
119
124
|
});
|
|
120
125
|
written.push(target);
|
|
126
|
+
// Restore the artboard's original position now that its capture is done,
|
|
127
|
+
// so it doesn't stay pinned at (0,0) and corrupt the next target's clip.
|
|
128
|
+
await widenedHandle.evaluate((el, prev) => {
|
|
129
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
130
|
+
ab.style.left = prev.left;
|
|
131
|
+
ab.style.top = prev.top;
|
|
132
|
+
}, savedPos);
|
|
121
133
|
};
|
|
122
134
|
|
|
123
135
|
if (multi) {
|
|
@@ -131,6 +143,7 @@ try {
|
|
|
131
143
|
const handle = screens[i];
|
|
132
144
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
133
145
|
await captureHandle(handle, join(outDir, `${id}.png`));
|
|
146
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
134
147
|
}
|
|
135
148
|
} else {
|
|
136
149
|
if (!out) {
|
|
@@ -48,10 +48,6 @@ try {
|
|
|
48
48
|
world.style.zoom = '1';
|
|
49
49
|
world.style.transform = 'none';
|
|
50
50
|
}
|
|
51
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
52
|
-
el.style.left = '0px';
|
|
53
|
-
el.style.top = '0px';
|
|
54
|
-
}
|
|
55
51
|
});
|
|
56
52
|
// dom-to-pptx ships a UMD bundle; addScriptTag exposes `window.domToPptx`.
|
|
57
53
|
await page.addScriptTag({ path: bundlePath });
|
|
@@ -74,6 +70,17 @@ try {
|
|
|
74
70
|
}
|
|
75
71
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
76
72
|
|
|
73
|
+
// Pin this artboard to (0,0) for its capture, restoring afterwards so a
|
|
74
|
+
// future multi-slide implementation doesn't inherit a stacked-at-origin bug
|
|
75
|
+
// (the scatter bug fixed in the png/pdf/svg/html shims).
|
|
76
|
+
const savedPos = await handle.evaluate((el) => {
|
|
77
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
78
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
79
|
+
ab.style.left = '0px';
|
|
80
|
+
ab.style.top = '0px';
|
|
81
|
+
return prev;
|
|
82
|
+
});
|
|
83
|
+
|
|
77
84
|
// Run dom-to-pptx inside the page. It returns a Blob; we serialise to a
|
|
78
85
|
// byte array for transport across the playwright boundary.
|
|
79
86
|
const bytesArray = await handle.evaluate(async (el) => {
|
|
@@ -88,6 +95,12 @@ try {
|
|
|
88
95
|
return Array.from(new Uint8Array(ab));
|
|
89
96
|
});
|
|
90
97
|
|
|
98
|
+
await handle.evaluate((el, prev) => {
|
|
99
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
100
|
+
ab.style.left = prev.left;
|
|
101
|
+
ab.style.top = prev.top;
|
|
102
|
+
}, savedPos);
|
|
103
|
+
|
|
91
104
|
writeFileSync(out, new Uint8Array(bytesArray));
|
|
92
105
|
console.log(out);
|
|
93
106
|
console.error(
|