@openvidstudio/mcp-server 0.1.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/dist/beats.d.ts +47 -0
- package/dist/beats.js +9 -0
- package/dist/capture.d.ts +116 -0
- package/dist/capture.js +148 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.js +130 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +57 -0
- package/dist/scenes/pngSize.d.ts +11 -0
- package/dist/scenes/pngSize.js +75 -0
- package/dist/scenes/templates.d.ts +42 -0
- package/dist/scenes/templates.js +468 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +37 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +33 -0
- package/dist/tools/captureScreenRecording.d.ts +28 -0
- package/dist/tools/captureScreenRecording.js +181 -0
- package/dist/tools/captureScreenshot.d.ts +34 -0
- package/dist/tools/captureScreenshot.js +148 -0
- package/dist/tools/contactSheet.d.ts +36 -0
- package/dist/tools/contactSheet.js +116 -0
- package/dist/tools/generateNarration.d.ts +54 -0
- package/dist/tools/generateNarration.js +185 -0
- package/dist/tools/importHiggsfieldClip.d.ts +41 -0
- package/dist/tools/importHiggsfieldClip.js +108 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +47 -0
- package/dist/tools/initProject.d.ts +37 -0
- package/dist/tools/initProject.js +166 -0
- package/dist/tools/mcp.d.ts +33 -0
- package/dist/tools/mcp.js +36 -0
- package/dist/tools/planBeats.d.ts +40 -0
- package/dist/tools/planBeats.js +143 -0
- package/dist/tools/preflight.d.ts +30 -0
- package/dist/tools/preflight.js +170 -0
- package/dist/tools/qcExtractFrames.d.ts +25 -0
- package/dist/tools/qcExtractFrames.js +129 -0
- package/dist/tools/renderVideo.d.ts +43 -0
- package/dist/tools/renderVideo.js +120 -0
- package/dist/tools/scaffoldScene.d.ts +23 -0
- package/dist/tools/scaffoldScene.js +147 -0
- package/dist/tools/stitchComposition.d.ts +24 -0
- package/dist/tools/stitchComposition.js +230 -0
- package/dist/tools/validateBeats.d.ts +14 -0
- package/dist/tools/validateBeats.js +164 -0
- package/dist/tools/validateScenes.d.ts +31 -0
- package/dist/tools/validateScenes.js +191 -0
- package/dist/tools/writeBeatsFile.d.ts +20 -0
- package/dist/tools/writeBeatsFile.js +81 -0
- package/dist/util.d.ts +43 -0
- package/dist/util.js +163 -0
- package/docs/API.md +344 -0
- package/docs/CAPTURE.md +106 -0
- package/docs/HIGGSFIELD.md +118 -0
- package/docs/NARRATION.md +111 -0
- package/docs/OVERVIEW.md +112 -0
- package/docs/PIPELINE.md +173 -0
- package/docs/PLANNING.md +206 -0
- package/docs/SCRIPT.md +50 -0
- package/docs/STYLE.md +88 -0
- package/package.json +54 -0
- package/templates/default/CLAUDE.md +63 -0
- package/templates/default/openvidstudio-core/package.json +51 -0
- package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
- package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
- package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
- package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
- package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
- package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
- package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
- package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
- package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
- package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
- package/templates/default/openvidstudio-core/src/index.ts +60 -0
- package/templates/default/openvidstudio-core/src/look.tsx +90 -0
- package/templates/default/openvidstudio-core/src/motion.ts +50 -0
- package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
- package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
- package/templates/default/package.json +31 -0
- package/templates/default/public/sfx/bell.wav +0 -0
- package/templates/default/public/sfx/blip.wav +0 -0
- package/templates/default/public/sfx/click.wav +0 -0
- package/templates/default/public/sfx/key_a.wav +0 -0
- package/templates/default/public/sfx/key_b.wav +0 -0
- package/templates/default/public/sfx/key_c.wav +0 -0
- package/templates/default/public/sfx/key_enter.wav +0 -0
- package/templates/default/public/sfx/music-bed.mp3 +0 -0
- package/templates/default/public/sfx/success.wav +0 -0
- package/templates/default/public/sfx/whoosh.wav +0 -0
- package/templates/default/remotion.config.ts +13 -0
- package/templates/default/src/Root.tsx +9 -0
- package/templates/default/src/index.ts +4 -0
- package/templates/default/tsconfig.json +16 -0
package/dist/beats.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared beats.json TypeScript types, matching packages/docs/PLANNING.md §4's
|
|
3
|
+
* captureMethod field spec. These describe a beats.json that has already passed
|
|
4
|
+
* validate_beats -- tools that read an *unvalidated* beatsJson (validate_beats
|
|
5
|
+
* itself, write_beats_file's internal check) work off `unknown` instead, since
|
|
6
|
+
* the whole point of validation is to not assume this shape holds yet.
|
|
7
|
+
*/
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import type { interactionSchema } from "./capture";
|
|
10
|
+
export type CaptureMethod = "screenshot" | "recording" | "dom-demo" | "higgsfield";
|
|
11
|
+
/**
|
|
12
|
+
* Derived from capture.ts's `interactionSchema` (the zod discriminated union
|
|
13
|
+
* capture_screenshot/capture_screen_recording actually enforce at replay time), via
|
|
14
|
+
* `z.infer`, rather than restated by hand -- this is the fix for the beats.ts/capture.ts
|
|
15
|
+
* Interaction fork (docs said "navigate"/"click"/"type"/"scroll"/"wait", capture.ts
|
|
16
|
+
* enforced "click"/"fill"/"select"/"hover"/"scroll"/"wait"; capture.ts is the real,
|
|
17
|
+
* enforced schema, so it's now the single source of truth both sides derive from and
|
|
18
|
+
* cannot drift apart from again). `import type` only: no runtime dependency on
|
|
19
|
+
* capture.ts (or its playwright import) is introduced by this type alias.
|
|
20
|
+
*/
|
|
21
|
+
export type Interaction = z.infer<typeof interactionSchema>;
|
|
22
|
+
export interface CaptureVisual {
|
|
23
|
+
captureMethod: "screenshot" | "recording";
|
|
24
|
+
url: string;
|
|
25
|
+
interactions: Interaction[];
|
|
26
|
+
}
|
|
27
|
+
export interface DomDemoVisual {
|
|
28
|
+
captureMethod: "dom-demo";
|
|
29
|
+
}
|
|
30
|
+
export interface HiggsfieldVisual {
|
|
31
|
+
captureMethod: "higgsfield";
|
|
32
|
+
higgsfieldPrompt: string;
|
|
33
|
+
}
|
|
34
|
+
export type Visual = CaptureVisual | DomDemoVisual | HiggsfieldVisual;
|
|
35
|
+
export interface Beat {
|
|
36
|
+
id: string;
|
|
37
|
+
start: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
vo: string;
|
|
40
|
+
visual: Visual;
|
|
41
|
+
}
|
|
42
|
+
export interface BeatsFile {
|
|
43
|
+
/** 30fps assumed if omitted, per PLANNING.md / PIPELINE.md. */
|
|
44
|
+
fps?: number;
|
|
45
|
+
title: string;
|
|
46
|
+
beats: Beat[];
|
|
47
|
+
}
|
package/dist/beats.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared beats.json TypeScript types, matching packages/docs/PLANNING.md §4's
|
|
4
|
+
* captureMethod field spec. These describe a beats.json that has already passed
|
|
5
|
+
* validate_beats -- tools that read an *unvalidated* beatsJson (validate_beats
|
|
6
|
+
* itself, write_beats_file's internal check) work off `unknown` instead, since
|
|
7
|
+
* the whole point of validation is to not assume this shape holds yet.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Browser, Page } from "playwright";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Shared building blocks for capture_screenshot and capture_screen_recording:
|
|
5
|
+
* the Interaction type both replay identically, and CAPTURE.md's zoom-desync
|
|
6
|
+
* detection/compensation math, extracted once so the two tools' protocols
|
|
7
|
+
* cannot silently drift apart from each other.
|
|
8
|
+
*/
|
|
9
|
+
export interface Viewport {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
/** CAPTURE.md's worked example. */
|
|
14
|
+
export declare const DEFAULT_VIEWPORT: Viewport;
|
|
15
|
+
export declare const viewportSchema: z.ZodObject<{
|
|
16
|
+
width: z.ZodNumber;
|
|
17
|
+
height: z.ZodNumber;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export type Interaction = {
|
|
20
|
+
type: "click";
|
|
21
|
+
selector: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: "fill";
|
|
24
|
+
selector: string;
|
|
25
|
+
value: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "select";
|
|
28
|
+
selector: string;
|
|
29
|
+
value: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "hover";
|
|
32
|
+
selector: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: "scroll";
|
|
35
|
+
selector?: string;
|
|
36
|
+
x?: number;
|
|
37
|
+
y?: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: "wait";
|
|
40
|
+
ms?: number;
|
|
41
|
+
selector?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const interactionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
44
|
+
type: z.ZodLiteral<"click">;
|
|
45
|
+
selector: z.ZodString;
|
|
46
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
47
|
+
type: z.ZodLiteral<"fill">;
|
|
48
|
+
selector: z.ZodString;
|
|
49
|
+
value: z.ZodString;
|
|
50
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
51
|
+
type: z.ZodLiteral<"select">;
|
|
52
|
+
selector: z.ZodString;
|
|
53
|
+
value: z.ZodString;
|
|
54
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
55
|
+
type: z.ZodLiteral<"hover">;
|
|
56
|
+
selector: z.ZodString;
|
|
57
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
58
|
+
type: z.ZodLiteral<"scroll">;
|
|
59
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
60
|
+
x: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
y: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
63
|
+
type: z.ZodLiteral<"wait">;
|
|
64
|
+
ms: z.ZodOptional<z.ZodNumber>;
|
|
65
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>], "type">;
|
|
67
|
+
/**
|
|
68
|
+
* Replays a beat's interactions in array order. Shared verbatim by both capture
|
|
69
|
+
* tools so their replay semantics never drift apart -- called after navigation
|
|
70
|
+
* and (for both tools) after the zoom-desync compensation below, in the same
|
|
71
|
+
* effective CSS-pixel viewport CAPTURE.md measured.
|
|
72
|
+
*/
|
|
73
|
+
export declare function replayInteractions(page: Page, interactions: Interaction[] | undefined): Promise<void>;
|
|
74
|
+
export declare const ZOOM_EPSILON = 0.01;
|
|
75
|
+
export interface ZoomCompensation {
|
|
76
|
+
/** The viewport actually in effect for capture: `target` unchanged, or the compensated size. */
|
|
77
|
+
viewport: Viewport;
|
|
78
|
+
/** Measured zoom = target.width / (effective width before compensation). 1 means no desync. */
|
|
79
|
+
zoom: number;
|
|
80
|
+
/** The last-measured `[window.innerWidth, window.innerHeight]`. */
|
|
81
|
+
effective: Viewport;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Thrown when a single compensation attempt doesn't converge -- CAPTURE.md
|
|
85
|
+
* doesn't explicitly cover this case (it assumes the desync ratio is
|
|
86
|
+
* consistent), so this is surfaced as a real error rather than silently
|
|
87
|
+
* proceeding with a crop rect measured against the wrong effective viewport.
|
|
88
|
+
*/
|
|
89
|
+
export declare class ZoomCompensationError extends Error {
|
|
90
|
+
readonly target: Viewport;
|
|
91
|
+
readonly zoom: number;
|
|
92
|
+
readonly effective: Viewport;
|
|
93
|
+
constructor(target: Viewport, zoom: number, effective: Viewport);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* CAPTURE.md's zoom-desync detection + compensation (protocol steps 1-2).
|
|
97
|
+
* Assumes the caller already called `page.setViewportSize(target)` and
|
|
98
|
+
* `page.goto(...)` -- this only measures and, if needed, re-requests a
|
|
99
|
+
* compensated viewport and re-verifies once.
|
|
100
|
+
*
|
|
101
|
+
* Never hardcodes a zoom constant (CAPTURE.md is explicit the ratio "can
|
|
102
|
+
* differ per machine/profile") -- it is measured live on every call. If the
|
|
103
|
+
* one compensation attempt doesn't bring the effective viewport back to
|
|
104
|
+
* `target` within `ZOOM_EPSILON`, throws `ZoomCompensationError` rather than
|
|
105
|
+
* silently proceeding.
|
|
106
|
+
*/
|
|
107
|
+
export declare function detectAndCompensateZoom(page: Page, target: Viewport): Promise<ZoomCompensation>;
|
|
108
|
+
/**
|
|
109
|
+
* Playwright's bundled browser binary is not installed by a plain `npm
|
|
110
|
+
* install`/`pnpm install` (it needs a separate `npx playwright install
|
|
111
|
+
* chromium`, deliberately not wired as a postinstall script -- see this
|
|
112
|
+
* package's README/tool descriptions). Rather than let that surface as a
|
|
113
|
+
* cryptic native-binary launch error, catch it here and return a clear,
|
|
114
|
+
* actionable message.
|
|
115
|
+
*/
|
|
116
|
+
export declare function launchChromium(): Promise<Browser>;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZoomCompensationError = exports.ZOOM_EPSILON = exports.interactionSchema = exports.viewportSchema = exports.DEFAULT_VIEWPORT = void 0;
|
|
4
|
+
exports.replayInteractions = replayInteractions;
|
|
5
|
+
exports.detectAndCompensateZoom = detectAndCompensateZoom;
|
|
6
|
+
exports.launchChromium = launchChromium;
|
|
7
|
+
const playwright_1 = require("playwright");
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
/** CAPTURE.md's worked example. */
|
|
10
|
+
exports.DEFAULT_VIEWPORT = { width: 1600, height: 1000 };
|
|
11
|
+
exports.viewportSchema = zod_1.z.object({
|
|
12
|
+
width: zod_1.z.number().int().positive(),
|
|
13
|
+
height: zod_1.z.number().int().positive(),
|
|
14
|
+
});
|
|
15
|
+
exports.interactionSchema = zod_1.z.discriminatedUnion("type", [
|
|
16
|
+
zod_1.z.object({ type: zod_1.z.literal("click"), selector: zod_1.z.string().min(1) }),
|
|
17
|
+
zod_1.z.object({ type: zod_1.z.literal("fill"), selector: zod_1.z.string().min(1), value: zod_1.z.string() }),
|
|
18
|
+
zod_1.z.object({ type: zod_1.z.literal("select"), selector: zod_1.z.string().min(1), value: zod_1.z.string() }),
|
|
19
|
+
zod_1.z.object({ type: zod_1.z.literal("hover"), selector: zod_1.z.string().min(1) }),
|
|
20
|
+
zod_1.z.object({
|
|
21
|
+
type: zod_1.z.literal("scroll"),
|
|
22
|
+
selector: zod_1.z.string().optional(),
|
|
23
|
+
x: zod_1.z.number().optional(),
|
|
24
|
+
y: zod_1.z.number().optional(),
|
|
25
|
+
}),
|
|
26
|
+
zod_1.z.object({ type: zod_1.z.literal("wait"), ms: zod_1.z.number().optional(), selector: zod_1.z.string().optional() }),
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Replays a beat's interactions in array order. Shared verbatim by both capture
|
|
30
|
+
* tools so their replay semantics never drift apart -- called after navigation
|
|
31
|
+
* and (for both tools) after the zoom-desync compensation below, in the same
|
|
32
|
+
* effective CSS-pixel viewport CAPTURE.md measured.
|
|
33
|
+
*/
|
|
34
|
+
async function replayInteractions(page, interactions) {
|
|
35
|
+
for (const interaction of interactions ?? []) {
|
|
36
|
+
switch (interaction.type) {
|
|
37
|
+
case "click":
|
|
38
|
+
await page.click(interaction.selector);
|
|
39
|
+
break;
|
|
40
|
+
case "fill":
|
|
41
|
+
await page.fill(interaction.selector, interaction.value);
|
|
42
|
+
break;
|
|
43
|
+
case "select":
|
|
44
|
+
await page.selectOption(interaction.selector, interaction.value);
|
|
45
|
+
break;
|
|
46
|
+
case "hover":
|
|
47
|
+
await page.hover(interaction.selector);
|
|
48
|
+
break;
|
|
49
|
+
case "scroll":
|
|
50
|
+
if (interaction.selector) {
|
|
51
|
+
await page.$eval(interaction.selector, (el, coords) => el.scrollTo(coords.x ?? 0, coords.y ?? 0), { x: interaction.x, y: interaction.y });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await page.evaluate((coords) => window.scrollTo(coords.x ?? 0, coords.y ?? 0), { x: interaction.x, y: interaction.y });
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case "wait":
|
|
58
|
+
if (interaction.selector) {
|
|
59
|
+
await page.waitForSelector(interaction.selector, { timeout: interaction.ms });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await page.waitForTimeout(interaction.ms ?? 500);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.ZOOM_EPSILON = 0.01;
|
|
69
|
+
/**
|
|
70
|
+
* Thrown when a single compensation attempt doesn't converge -- CAPTURE.md
|
|
71
|
+
* doesn't explicitly cover this case (it assumes the desync ratio is
|
|
72
|
+
* consistent), so this is surfaced as a real error rather than silently
|
|
73
|
+
* proceeding with a crop rect measured against the wrong effective viewport.
|
|
74
|
+
*/
|
|
75
|
+
class ZoomCompensationError extends Error {
|
|
76
|
+
constructor(target, zoom, effective) {
|
|
77
|
+
super(`Zoom-desync compensation did not converge: requested ${target.width}x${target.height}, measured zoom ` +
|
|
78
|
+
`${zoom.toFixed(4)} (per CAPTURE.md, this ratio is not hardcoded, it can differ per machine/profile), ` +
|
|
79
|
+
`but after one compensation attempt the effective viewport was ${effective.width}x${effective.height}, ` +
|
|
80
|
+
`not ${target.width}x${target.height} within ${(exports.ZOOM_EPSILON * 100).toFixed(0)}%. Refusing to proceed ` +
|
|
81
|
+
`with a screenshot/crop measured against the wrong effective viewport -- see packages/docs/CAPTURE.md.`);
|
|
82
|
+
this.target = target;
|
|
83
|
+
this.zoom = zoom;
|
|
84
|
+
this.effective = effective;
|
|
85
|
+
this.name = "ZoomCompensationError";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.ZoomCompensationError = ZoomCompensationError;
|
|
89
|
+
async function measureEffectiveViewport(page) {
|
|
90
|
+
const [width, height] = await page.evaluate(() => [window.innerWidth, window.innerHeight]);
|
|
91
|
+
return { width, height };
|
|
92
|
+
}
|
|
93
|
+
function relativeDiff(actual, target) {
|
|
94
|
+
return Math.abs(actual - target) / target;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* CAPTURE.md's zoom-desync detection + compensation (protocol steps 1-2).
|
|
98
|
+
* Assumes the caller already called `page.setViewportSize(target)` and
|
|
99
|
+
* `page.goto(...)` -- this only measures and, if needed, re-requests a
|
|
100
|
+
* compensated viewport and re-verifies once.
|
|
101
|
+
*
|
|
102
|
+
* Never hardcodes a zoom constant (CAPTURE.md is explicit the ratio "can
|
|
103
|
+
* differ per machine/profile") -- it is measured live on every call. If the
|
|
104
|
+
* one compensation attempt doesn't bring the effective viewport back to
|
|
105
|
+
* `target` within `ZOOM_EPSILON`, throws `ZoomCompensationError` rather than
|
|
106
|
+
* silently proceeding.
|
|
107
|
+
*/
|
|
108
|
+
async function detectAndCompensateZoom(page, target) {
|
|
109
|
+
const firstMeasurement = await measureEffectiveViewport(page);
|
|
110
|
+
const zoom = target.width / firstMeasurement.width;
|
|
111
|
+
if (Math.abs(zoom - 1) <= exports.ZOOM_EPSILON) {
|
|
112
|
+
return { viewport: target, zoom, effective: firstMeasurement };
|
|
113
|
+
}
|
|
114
|
+
const compensated = {
|
|
115
|
+
width: Math.round(target.width * zoom),
|
|
116
|
+
height: Math.round(target.height * zoom),
|
|
117
|
+
};
|
|
118
|
+
await page.setViewportSize(compensated);
|
|
119
|
+
const secondMeasurement = await measureEffectiveViewport(page);
|
|
120
|
+
const converged = relativeDiff(secondMeasurement.width, target.width) <= exports.ZOOM_EPSILON &&
|
|
121
|
+
relativeDiff(secondMeasurement.height, target.height) <= exports.ZOOM_EPSILON;
|
|
122
|
+
if (!converged) {
|
|
123
|
+
throw new ZoomCompensationError(target, zoom, secondMeasurement);
|
|
124
|
+
}
|
|
125
|
+
return { viewport: compensated, zoom, effective: secondMeasurement };
|
|
126
|
+
}
|
|
127
|
+
const BROWSER_NOT_INSTALLED_RE = /Executable doesn't exist at/;
|
|
128
|
+
/**
|
|
129
|
+
* Playwright's bundled browser binary is not installed by a plain `npm
|
|
130
|
+
* install`/`pnpm install` (it needs a separate `npx playwright install
|
|
131
|
+
* chromium`, deliberately not wired as a postinstall script -- see this
|
|
132
|
+
* package's README/tool descriptions). Rather than let that surface as a
|
|
133
|
+
* cryptic native-binary launch error, catch it here and return a clear,
|
|
134
|
+
* actionable message.
|
|
135
|
+
*/
|
|
136
|
+
async function launchChromium() {
|
|
137
|
+
try {
|
|
138
|
+
return await playwright_1.chromium.launch({ headless: true });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
if (BROWSER_NOT_INSTALLED_RE.test(message)) {
|
|
143
|
+
throw new Error(`Playwright's Chromium browser is not installed. Run "npx playwright install chromium" (in the ` +
|
|
144
|
+
`project, or wherever @openvidstudio/mcp-server is installed) and retry. Original error: ${message}`);
|
|
145
|
+
}
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openvidstudio.config.json's shape, written by init_project into a *user's* project
|
|
3
|
+
* root (not this monorepo). hasHiggsfield is what import_higgsfield_clip's tool
|
|
4
|
+
* registration gates on (read once at server-startup by stdio.ts, see registerTools);
|
|
5
|
+
* targetDurationSeconds/videoConfig are read by stitch_composition and render_video for
|
|
6
|
+
* defaults not stated by beats.json itself.
|
|
7
|
+
*/
|
|
8
|
+
export interface VideoConfig {
|
|
9
|
+
fps: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface CaptureConfig {
|
|
14
|
+
/** The running app every capture points at unless a beat names its own url. */
|
|
15
|
+
appUrl: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** Milliseconds to wait after load, so entrance animations finish before the shot. */
|
|
19
|
+
settleMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface NarrationConfig {
|
|
22
|
+
engine: "edge-tts" | "say" | "espeak";
|
|
23
|
+
voice: string;
|
|
24
|
+
}
|
|
25
|
+
export interface OpenvidstudioConfig {
|
|
26
|
+
hasHiggsfield: boolean;
|
|
27
|
+
targetDurationSeconds: number;
|
|
28
|
+
videoConfig: VideoConfig;
|
|
29
|
+
capture: CaptureConfig;
|
|
30
|
+
narration: NarrationConfig;
|
|
31
|
+
}
|
|
32
|
+
export interface ConfigPatch {
|
|
33
|
+
hasHiggsfield?: boolean;
|
|
34
|
+
targetDurationSeconds?: number;
|
|
35
|
+
videoConfig?: Partial<VideoConfig>;
|
|
36
|
+
capture?: Partial<CaptureConfig>;
|
|
37
|
+
narration?: Partial<NarrationConfig>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Defaults chosen so a bare request works with no configuration.
|
|
41
|
+
*
|
|
42
|
+
* `settleMs` is deliberately not zero. A capture taken the instant a page loads
|
|
43
|
+
* catches entrance animations mid-flight, and the resulting frame looks broken
|
|
44
|
+
* rather than obviously wrong, so it survives review. 1200ms clears a typical
|
|
45
|
+
* entrance without making capture feel slow.
|
|
46
|
+
*/
|
|
47
|
+
export declare const DEFAULT_CONFIG: OpenvidstudioConfig;
|
|
48
|
+
export declare const CONFIG_FILE_NAME = "openvidstudio.config.json";
|
|
49
|
+
export declare function configPath(projectRoot: string): string;
|
|
50
|
+
/** Reads openvidstudio.config.json, filling in defaults for any field it omits. */
|
|
51
|
+
export declare function loadConfig(projectRoot: string): OpenvidstudioConfig;
|
|
52
|
+
/**
|
|
53
|
+
* Merges `patch` over whatever config already exists (or the defaults, if none does),
|
|
54
|
+
* writes the result, and returns it. Only fields actually present in `patch` override
|
|
55
|
+
* the existing value -- an `undefined` field in `patch` (the common case, since every
|
|
56
|
+
* init_project input field is optional) never clobbers an existing real value.
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeConfig(projectRoot: string, patch: ConfigPatch): OpenvidstudioConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CONFIG_FILE_NAME = exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.configPath = configPath;
|
|
38
|
+
exports.loadConfig = loadConfig;
|
|
39
|
+
exports.writeConfig = writeConfig;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
/**
|
|
43
|
+
* Defaults chosen so a bare request works with no configuration.
|
|
44
|
+
*
|
|
45
|
+
* `settleMs` is deliberately not zero. A capture taken the instant a page loads
|
|
46
|
+
* catches entrance animations mid-flight, and the resulting frame looks broken
|
|
47
|
+
* rather than obviously wrong, so it survives review. 1200ms clears a typical
|
|
48
|
+
* entrance without making capture feel slow.
|
|
49
|
+
*/
|
|
50
|
+
exports.DEFAULT_CONFIG = {
|
|
51
|
+
hasHiggsfield: false,
|
|
52
|
+
targetDurationSeconds: 60,
|
|
53
|
+
videoConfig: { fps: 30, width: 1920, height: 1080 },
|
|
54
|
+
capture: { appUrl: "http://localhost:3000", width: 1440, height: 900, settleMs: 1200 },
|
|
55
|
+
narration: { engine: "edge-tts", voice: "en-US-AndrewNeural" },
|
|
56
|
+
};
|
|
57
|
+
exports.CONFIG_FILE_NAME = "openvidstudio.config.json";
|
|
58
|
+
function configPath(projectRoot) {
|
|
59
|
+
return path.join(projectRoot, exports.CONFIG_FILE_NAME);
|
|
60
|
+
}
|
|
61
|
+
/** Reads openvidstudio.config.json, filling in defaults for any field it omits. */
|
|
62
|
+
function loadConfig(projectRoot) {
|
|
63
|
+
const file = configPath(projectRoot);
|
|
64
|
+
if (!fs.existsSync(file)) {
|
|
65
|
+
return {
|
|
66
|
+
hasHiggsfield: exports.DEFAULT_CONFIG.hasHiggsfield,
|
|
67
|
+
targetDurationSeconds: exports.DEFAULT_CONFIG.targetDurationSeconds,
|
|
68
|
+
videoConfig: { ...exports.DEFAULT_CONFIG.videoConfig },
|
|
69
|
+
capture: { ...exports.DEFAULT_CONFIG.capture },
|
|
70
|
+
narration: { ...exports.DEFAULT_CONFIG.narration },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new Error(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
hasHiggsfield: raw.hasHiggsfield ?? exports.DEFAULT_CONFIG.hasHiggsfield,
|
|
82
|
+
targetDurationSeconds: raw.targetDurationSeconds ?? exports.DEFAULT_CONFIG.targetDurationSeconds,
|
|
83
|
+
videoConfig: {
|
|
84
|
+
fps: raw.videoConfig?.fps ?? exports.DEFAULT_CONFIG.videoConfig.fps,
|
|
85
|
+
width: raw.videoConfig?.width ?? exports.DEFAULT_CONFIG.videoConfig.width,
|
|
86
|
+
height: raw.videoConfig?.height ?? exports.DEFAULT_CONFIG.videoConfig.height,
|
|
87
|
+
},
|
|
88
|
+
capture: {
|
|
89
|
+
appUrl: raw.capture?.appUrl ?? exports.DEFAULT_CONFIG.capture.appUrl,
|
|
90
|
+
width: raw.capture?.width ?? exports.DEFAULT_CONFIG.capture.width,
|
|
91
|
+
height: raw.capture?.height ?? exports.DEFAULT_CONFIG.capture.height,
|
|
92
|
+
settleMs: raw.capture?.settleMs ?? exports.DEFAULT_CONFIG.capture.settleMs,
|
|
93
|
+
},
|
|
94
|
+
narration: {
|
|
95
|
+
engine: raw.narration?.engine ?? exports.DEFAULT_CONFIG.narration.engine,
|
|
96
|
+
voice: raw.narration?.voice ?? exports.DEFAULT_CONFIG.narration.voice,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Merges `patch` over whatever config already exists (or the defaults, if none does),
|
|
102
|
+
* writes the result, and returns it. Only fields actually present in `patch` override
|
|
103
|
+
* the existing value -- an `undefined` field in `patch` (the common case, since every
|
|
104
|
+
* init_project input field is optional) never clobbers an existing real value.
|
|
105
|
+
*/
|
|
106
|
+
function writeConfig(projectRoot, patch) {
|
|
107
|
+
const existing = loadConfig(projectRoot);
|
|
108
|
+
const merged = {
|
|
109
|
+
hasHiggsfield: patch.hasHiggsfield ?? existing.hasHiggsfield,
|
|
110
|
+
targetDurationSeconds: patch.targetDurationSeconds ?? existing.targetDurationSeconds,
|
|
111
|
+
videoConfig: {
|
|
112
|
+
fps: patch.videoConfig?.fps ?? existing.videoConfig.fps,
|
|
113
|
+
width: patch.videoConfig?.width ?? existing.videoConfig.width,
|
|
114
|
+
height: patch.videoConfig?.height ?? existing.videoConfig.height,
|
|
115
|
+
},
|
|
116
|
+
capture: {
|
|
117
|
+
appUrl: patch.capture?.appUrl ?? existing.capture.appUrl,
|
|
118
|
+
width: patch.capture?.width ?? existing.capture.width,
|
|
119
|
+
height: patch.capture?.height ?? existing.capture.height,
|
|
120
|
+
settleMs: patch.capture?.settleMs ?? existing.capture.settleMs,
|
|
121
|
+
},
|
|
122
|
+
narration: {
|
|
123
|
+
engine: patch.narration?.engine ?? existing.narration.engine,
|
|
124
|
+
voice: patch.narration?.voice ?? existing.narration.voice,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
128
|
+
fs.writeFileSync(configPath(projectRoot), JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
129
|
+
return merged;
|
|
130
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled copy of templates/default, produced by scripts/copy-template.mjs at build
|
|
3
|
+
* time and shipped inside the published npm package (see package.json's "files").
|
|
4
|
+
*
|
|
5
|
+
* This module compiles to dist/paths.js, so `__dirname` here is the package's own
|
|
6
|
+
* dist/ directory at runtime -- "../templates/default" resolves to
|
|
7
|
+
* <package root>/templates/default, never to the monorepo's own templates/default/
|
|
8
|
+
* (that path does not exist once this package is installed standalone in someone
|
|
9
|
+
* else's project). Deliberately kept in a top-level src/ module (not nested under
|
|
10
|
+
* src/tools/) so this one-line relative-path computation stays easy to audit.
|
|
11
|
+
*/
|
|
12
|
+
export declare const TEMPLATE_DIR: string;
|
|
13
|
+
/**
|
|
14
|
+
* Bundled copy of packages/docs/*.md, produced by scripts/copy-docs.mjs at build time
|
|
15
|
+
* and shipped inside the published npm package (see package.json's "files"). Same
|
|
16
|
+
* __dirname-relative reasoning as TEMPLATE_DIR above: resolves to
|
|
17
|
+
* <package root>/docs, never the monorepo's own packages/docs/. init_project copies
|
|
18
|
+
* these into a scaffolded project's own docs/ folder (see tools/initProject.ts).
|
|
19
|
+
*/
|
|
20
|
+
export declare const DOCS_DIR: string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DOCS_DIR = exports.TEMPLATE_DIR = void 0;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
/**
|
|
39
|
+
* Bundled copy of templates/default, produced by scripts/copy-template.mjs at build
|
|
40
|
+
* time and shipped inside the published npm package (see package.json's "files").
|
|
41
|
+
*
|
|
42
|
+
* This module compiles to dist/paths.js, so `__dirname` here is the package's own
|
|
43
|
+
* dist/ directory at runtime -- "../templates/default" resolves to
|
|
44
|
+
* <package root>/templates/default, never to the monorepo's own templates/default/
|
|
45
|
+
* (that path does not exist once this package is installed standalone in someone
|
|
46
|
+
* else's project). Deliberately kept in a top-level src/ module (not nested under
|
|
47
|
+
* src/tools/) so this one-line relative-path computation stays easy to audit.
|
|
48
|
+
*/
|
|
49
|
+
exports.TEMPLATE_DIR = path.join(__dirname, "..", "templates", "default");
|
|
50
|
+
/**
|
|
51
|
+
* Bundled copy of packages/docs/*.md, produced by scripts/copy-docs.mjs at build time
|
|
52
|
+
* and shipped inside the published npm package (see package.json's "files"). Same
|
|
53
|
+
* __dirname-relative reasoning as TEMPLATE_DIR above: resolves to
|
|
54
|
+
* <package root>/docs, never the monorepo's own packages/docs/. init_project copies
|
|
55
|
+
* these into a scaffolded project's own docs/ folder (see tools/initProject.ts).
|
|
56
|
+
*/
|
|
57
|
+
exports.DOCS_DIR = path.join(__dirname, "..", "docs");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Width and height from a PNG's IHDR chunk.
|
|
3
|
+
*
|
|
4
|
+
* A capture template needs the real dimensions of the file it is about to composite,
|
|
5
|
+
* otherwise the frame is a guess and the page comes out letterboxed or stretched.
|
|
6
|
+
* The header is fixed layout, so this is a 24 byte read rather than a dependency.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pngSize(file: string): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
} | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.pngSize = pngSize;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
/**
|
|
39
|
+
* Width and height from a PNG's IHDR chunk.
|
|
40
|
+
*
|
|
41
|
+
* A capture template needs the real dimensions of the file it is about to composite,
|
|
42
|
+
* otherwise the frame is a guess and the page comes out letterboxed or stretched.
|
|
43
|
+
* The header is fixed layout, so this is a 24 byte read rather than a dependency.
|
|
44
|
+
*/
|
|
45
|
+
function pngSize(file) {
|
|
46
|
+
let fd;
|
|
47
|
+
try {
|
|
48
|
+
fd = fs.openSync(file, "r");
|
|
49
|
+
const head = Buffer.alloc(24);
|
|
50
|
+
if (fs.readSync(fd, head, 0, 24, 0) < 24)
|
|
51
|
+
return null;
|
|
52
|
+
// 8 byte signature, then a length + "IHDR" tag, then width and height.
|
|
53
|
+
const isPng = head.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
|
54
|
+
if (!isPng || head.subarray(12, 16).toString("ascii") !== "IHDR")
|
|
55
|
+
return null;
|
|
56
|
+
const width = head.readUInt32BE(16);
|
|
57
|
+
const height = head.readUInt32BE(20);
|
|
58
|
+
if (!width || !height)
|
|
59
|
+
return null;
|
|
60
|
+
return { width, height };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (fd !== undefined) {
|
|
67
|
+
try {
|
|
68
|
+
fs.closeSync(fd);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* nothing useful to do here */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|