@bitmagic/cli 0.1.64-dev.1 → 0.1.64-dev.10
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/README.md +80 -5
- package/assets/blender-level/README.md +25 -0
- package/assets/blender-level/audit.py +232 -0
- package/assets/blender-level/bmlevel/__init__.py +294 -0
- package/assets/blender-level/bmlevel/core.py +438 -0
- package/assets/blender-level/bmlevel/scene_bpy.py +186 -0
- package/assets/blender-level/bmlevel/scene_null.py +44 -0
- package/assets/blender-level/example_level.py +80 -0
- package/assets/blender-level/find_blender.py +62 -0
- package/assets/blender-level/review.py +117 -0
- package/assets/blender-level/sync_world.py +85 -0
- package/assets/blender-level/tests/test_core.py +189 -0
- package/dist/assets/keep-glb.d.ts +20 -0
- package/dist/assets/keep-glb.js +45 -0
- package/dist/assets/keep-glb.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.d.ts +3 -0
- package/dist/commands/assets.js +33 -3
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/cover.js +7 -1
- package/dist/commands/cover.js.map +1 -1
- package/dist/commands/forge.js +70 -11
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.d.ts +88 -1
- package/dist/commands/generate.js +400 -21
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +33 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/judge.d.ts +0 -11
- package/dist/commands/judge.js +15 -2
- package/dist/commands/judge.js.map +1 -1
- package/dist/commands/levels.d.ts +1 -1
- package/dist/commands/levels.js +14 -3
- package/dist/commands/levels.js.map +1 -1
- package/dist/commands/reference.js +5 -1
- package/dist/commands/reference.js.map +1 -1
- package/dist/commands/reset-account.js +2 -11
- package/dist/commands/reset-account.js.map +1 -1
- package/dist/commands/tools.d.ts +1 -0
- package/dist/commands/tools.js +73 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/trailer.js +18 -4
- package/dist/commands/trailer.js.map +1 -1
- package/dist/editor/asset-detail-panel.d.ts +8 -3
- package/dist/editor/asset-detail-panel.js +11 -1
- package/dist/editor/asset-detail-panel.js.map +1 -1
- package/dist/editor/hud-replay-page.js +317 -22
- package/dist/editor/hud-replay-page.js.map +1 -1
- package/dist/editor/shell-page.js +117 -7
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/forge/apply-modifications.js +70 -2
- package/dist/forge/apply-modifications.js.map +1 -1
- package/dist/forge/progress.d.ts +10 -0
- package/dist/forge/progress.js +7 -6
- package/dist/forge/progress.js.map +1 -1
- package/dist/forge/run-pipeline.d.ts +8 -0
- package/dist/forge/run-pipeline.js +37 -2
- package/dist/forge/run-pipeline.js.map +1 -1
- package/dist/forge/stream.d.ts +7 -1
- package/dist/forge/stream.js +5 -2
- package/dist/forge/stream.js.map +1 -1
- package/dist/forge/two-d-gate.d.ts +72 -0
- package/dist/forge/two-d-gate.js +198 -0
- package/dist/forge/two-d-gate.js.map +1 -0
- package/dist/generate/animation-from-video.d.ts +11 -3
- package/dist/generate/animation-from-video.js +12 -2
- package/dist/generate/animation-from-video.js.map +1 -1
- package/dist/generate/model.d.ts +11 -0
- package/dist/generate/model.js +41 -1
- package/dist/generate/model.js.map +1 -1
- package/dist/generate/prop.d.ts +9 -0
- package/dist/generate/prop.js +59 -9
- package/dist/generate/prop.js.map +1 -1
- package/dist/generate/video-review.d.ts +28 -0
- package/dist/generate/video-review.js +120 -0
- package/dist/generate/video-review.js.map +1 -0
- package/dist/project/art-style.d.ts +34 -0
- package/dist/project/art-style.js +63 -0
- package/dist/project/art-style.js.map +1 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +36 -0
- package/dist/prompt.js.map +1 -0
- package/dist/publish/bundle.d.ts +12 -0
- package/dist/publish/bundle.js +32 -0
- package/dist/publish/bundle.js.map +1 -1
- package/dist/render/qr-png.d.ts +6 -3
- package/dist/render/qr-png.js +8 -8
- package/dist/render/qr-png.js.map +1 -1
- package/dist/scaffold/blender-level-skill-files.d.ts +26 -0
- package/dist/scaffold/blender-level-skill-files.js +570 -0
- package/dist/scaffold/blender-level-skill-files.js.map +1 -0
- package/dist/scaffold/claude-settings.js +2 -0
- package/dist/scaffold/claude-settings.js.map +1 -1
- package/dist/scaffold/project-files.d.ts +2 -1
- package/dist/scaffold/project-files.js +125 -42
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.d.ts +9 -0
- package/dist/scaffold/project.js +29 -3
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/reference-guidance.d.ts +1 -1
- package/dist/scaffold/reference-guidance.js +2 -1
- package/dist/scaffold/reference-guidance.js.map +1 -1
- package/dist/scaffold/skill-files.js +3 -1
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/scaffold/unity-skill-files.js +1 -2
- package/dist/scaffold/unity-skill-files.js.map +1 -1
- package/dist/tools/bundles.d.ts +21 -0
- package/dist/tools/bundles.js +67 -0
- package/dist/tools/bundles.js.map +1 -0
- package/dist/trailer/ffmpeg.d.ts +10 -0
- package/dist/trailer/ffmpeg.js +51 -6
- package/dist/trailer/ffmpeg.js.map +1 -1
- package/dist/trailer/hud-raster.d.ts +12 -12
- package/dist/trailer/hud-raster.js +85 -32
- package/dist/trailer/hud-raster.js.map +1 -1
- package/dist/trailer/hud-state.d.ts +75 -4
- package/dist/trailer/hud-state.js +132 -17
- package/dist/trailer/hud-state.js.map +1 -1
- package/dist/trailer/io.d.ts +2 -0
- package/dist/trailer/io.js +6 -1
- package/dist/trailer/io.js.map +1 -1
- package/dist/trailer/link.d.ts +1 -0
- package/dist/trailer/link.js +15 -0
- package/dist/trailer/link.js.map +1 -0
- package/dist/trailer/make.d.ts +2 -0
- package/dist/trailer/make.js +1 -0
- package/dist/trailer/make.js.map +1 -1
- package/dist/trailer/merge.js +32 -11
- package/dist/trailer/merge.js.map +1 -1
- package/dist/trailer/music-beats.js +4 -39
- package/dist/trailer/music-beats.js.map +1 -1
- package/dist/trailer/render.d.ts +2 -0
- package/dist/trailer/render.js +94 -48
- package/dist/trailer/render.js.map +1 -1
- package/dist/trailer/timeline-types.d.ts +19 -0
- package/dist/trailer/timeline-types.js +42 -2
- package/dist/trailer/timeline-types.js.map +1 -1
- package/dist/trailer/title-raster.js +2 -1
- package/dist/trailer/title-raster.js.map +1 -1
- package/dist/trailer/titles.js +6 -1
- package/dist/trailer/titles.js.map +1 -1
- package/dist/trailer/video-chain.d.ts +96 -8
- package/dist/trailer/video-chain.js +183 -0
- package/dist/trailer/video-chain.js.map +1 -1
- package/dist/verify/browser.js +4 -1
- package/dist/verify/browser.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool bundles the CLI ships for skills that drive external programs (Blender, Python): the
|
|
3
|
+
* scripts live in the package's `assets/` rather than being rendered into every project, and a
|
|
4
|
+
* skill's first step copies the bundle it needs into the project's `tools/`.
|
|
5
|
+
*
|
|
6
|
+
* `bitmagic tools install <bundle>` is that copy. It exists because the shell one-liner the skills
|
|
7
|
+
* used before — `readlink -f "$(which bitmagic)"` to find the package — resolves to a shell shim
|
|
8
|
+
* under pnpm and to a symlink chain under npm and Homebrew, and the first creator to try the
|
|
9
|
+
* Blender skill spent two tool calls hunting through the pnpm store for `assets/`. The CLI knows
|
|
10
|
+
* where it is; nobody else has to.
|
|
11
|
+
*
|
|
12
|
+
* Pure planning here (which files land where), so it is testable against a temp directory; the
|
|
13
|
+
* command does the copying.
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
export const TOOL_BUNDLES = [
|
|
19
|
+
{
|
|
20
|
+
name: 'blender-level',
|
|
21
|
+
target: 'tools/blender-level',
|
|
22
|
+
extensions: null,
|
|
23
|
+
skill: 'building-levels-in-blender',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'unity-import',
|
|
27
|
+
target: 'tools',
|
|
28
|
+
extensions: ['.py', '.json'],
|
|
29
|
+
skill: 'converting-unity-levels',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
/** `assets/` of this package, from both `dist/tools/` and `src/tools/`. */
|
|
33
|
+
export function packageAssetsDir() {
|
|
34
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets');
|
|
35
|
+
}
|
|
36
|
+
export function findBundle(name) {
|
|
37
|
+
const bundle = TOOL_BUNDLES.find((b) => b.name === name);
|
|
38
|
+
if (!bundle) {
|
|
39
|
+
throw new Error(`no tool bundle named "${name}" — the CLI ships ${TOOL_BUNDLES.map((b) => `"${b.name}"`).join(', ')}.`);
|
|
40
|
+
}
|
|
41
|
+
return bundle;
|
|
42
|
+
}
|
|
43
|
+
/** Every file the install would write, relative paths resolved against `assetsDir` and `projectRoot`. */
|
|
44
|
+
export async function planBundleCopy(bundle, assetsDir, projectRoot) {
|
|
45
|
+
const source = path.join(assetsDir, bundle.name);
|
|
46
|
+
const entries = await fs.readdir(source, { withFileTypes: true, recursive: true });
|
|
47
|
+
const copies = [];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isFile())
|
|
50
|
+
continue;
|
|
51
|
+
const parent = entry.parentPath;
|
|
52
|
+
const rel = path.relative(source, path.join(parent, entry.name));
|
|
53
|
+
if (rel.split(path.sep).includes('__pycache__'))
|
|
54
|
+
continue;
|
|
55
|
+
if (bundle.extensions && !bundle.extensions.includes(path.extname(entry.name)))
|
|
56
|
+
continue;
|
|
57
|
+
copies.push({ from: path.join(source, rel), to: path.join(projectRoot, bundle.target, rel) });
|
|
58
|
+
}
|
|
59
|
+
return copies.sort((a, b) => (a.to < b.to ? -1 : a.to > b.to ? 1 : 0));
|
|
60
|
+
}
|
|
61
|
+
export async function copyBundle(copies) {
|
|
62
|
+
for (const { from, to } of copies) {
|
|
63
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
64
|
+
await fs.copyFile(from, to);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=bundles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundles.js","sourceRoot":"","sources":["../../src/tools/bundles.ts"],"names":[],"mappings":"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"}
|
package/dist/trailer/ffmpeg.d.ts
CHANGED
|
@@ -5,4 +5,14 @@ export declare function missingFfmpegMessage(tool?: FfmpegTool): string;
|
|
|
5
5
|
* with `-version` is the cheapest probe that also proves the binary actually runs.
|
|
6
6
|
*/
|
|
7
7
|
export declare function ensureFfmpeg(tools?: readonly FfmpegTool[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* The first of `tools` that is not on PATH, or null when they all are — the non-throwing half of
|
|
10
|
+
* {@link ensureFfmpeg}, for callers that skip a step rather than fail when ffmpeg is absent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function missingFfmpegTool(tools?: readonly FfmpegTool[]): FfmpegTool | null;
|
|
8
13
|
export declare function runFfmpeg(args: string[]): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Run a tool and keep stdout. {@link runFfmpeg} discards it — everything in the trailer pipeline
|
|
16
|
+
* writes files — so the PCM decode and every ffprobe query come through here instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function captureFfmpegTool(tool: FfmpegTool, args: string[]): Promise<Buffer>;
|
package/dist/trailer/ffmpeg.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thin ffmpeg process wrapper. Always argument arrays, never shell strings.
|
|
3
3
|
*
|
|
4
|
-
* ffmpeg is the one external tool
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* ffmpeg is the one external tool this CLI needs and the one a creator's machine is least likely to
|
|
5
|
+
* have, so its absence is reported as a sentence with the install command rather than as an ENOENT
|
|
6
|
+
* stack trace from deep inside a filtergraph.
|
|
7
|
+
*
|
|
8
|
+
* Two callers with opposite stakes, hence both `ensureFfmpeg` and `missingFfmpegTool`: the trailer
|
|
9
|
+
* lane cannot do its job at all without ffmpeg and refuses up front, while `generate video`'s clip
|
|
10
|
+
* review is a nicety on top of an already-paid generation and must degrade to a note.
|
|
7
11
|
*/
|
|
8
12
|
import { spawn, spawnSync } from 'child_process';
|
|
9
13
|
import { CliError, errnoCode } from '../errors.js';
|
|
@@ -19,12 +23,21 @@ export function missingFfmpegMessage(tool = 'ffmpeg') {
|
|
|
19
23
|
* with `-version` is the cheapest probe that also proves the binary actually runs.
|
|
20
24
|
*/
|
|
21
25
|
export function ensureFfmpeg(tools = ['ffmpeg']) {
|
|
26
|
+
const missing = missingFfmpegTool(tools);
|
|
27
|
+
if (missing !== null)
|
|
28
|
+
throw new CliError(missingFfmpegMessage(missing), 1, 'ffmpeg_missing');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The first of `tools` that is not on PATH, or null when they all are — the non-throwing half of
|
|
32
|
+
* {@link ensureFfmpeg}, for callers that skip a step rather than fail when ffmpeg is absent.
|
|
33
|
+
*/
|
|
34
|
+
export function missingFfmpegTool(tools = ['ffmpeg']) {
|
|
22
35
|
for (const tool of tools) {
|
|
23
36
|
const probe = spawnSync(tool, ['-version'], { stdio: 'ignore' });
|
|
24
|
-
if (probe.error !== undefined && errnoCode(probe.error) === 'ENOENT')
|
|
25
|
-
|
|
26
|
-
}
|
|
37
|
+
if (probe.error !== undefined && errnoCode(probe.error) === 'ENOENT')
|
|
38
|
+
return tool;
|
|
27
39
|
}
|
|
40
|
+
return null;
|
|
28
41
|
}
|
|
29
42
|
export async function runFfmpeg(args) {
|
|
30
43
|
return new Promise((resolve, reject) => {
|
|
@@ -52,4 +65,36 @@ export async function runFfmpeg(args) {
|
|
|
52
65
|
});
|
|
53
66
|
});
|
|
54
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Run a tool and keep stdout. {@link runFfmpeg} discards it — everything in the trailer pipeline
|
|
70
|
+
* writes files — so the PCM decode and every ffprobe query come through here instead.
|
|
71
|
+
*/
|
|
72
|
+
export async function captureFfmpegTool(tool, args) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const child = spawn(tool, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let stderr = '';
|
|
77
|
+
child.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
78
|
+
child.stderr.on('data', (chunk) => {
|
|
79
|
+
stderr += chunk.toString('utf-8');
|
|
80
|
+
if (stderr.length > STDERR_KEEP_BYTES)
|
|
81
|
+
stderr = stderr.slice(-STDERR_KEEP_BYTES / 2);
|
|
82
|
+
});
|
|
83
|
+
child.on('error', (err) => {
|
|
84
|
+
if (errnoCode(err) === 'ENOENT')
|
|
85
|
+
reject(new CliError(missingFfmpegMessage(tool), 1, 'ffmpeg_missing'));
|
|
86
|
+
else
|
|
87
|
+
reject(new Error(`failed to start ${tool}: ${err.message}`));
|
|
88
|
+
});
|
|
89
|
+
child.on('close', (code) => {
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
resolve(Buffer.concat(chunks));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const tail = stderr.trim().split('\n').slice(-STDERR_REPORT_LINES).join('\n');
|
|
95
|
+
reject(new Error(`${tool} exited with code ${code}:\n${tail}`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
55
100
|
//# sourceMappingURL=ffmpeg.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/trailer/ffmpeg.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/trailer/ffmpeg.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,22 +7,22 @@ export interface RasterizeOptions {
|
|
|
7
7
|
harnessUrl: string;
|
|
8
8
|
/** Overlay PNGs are written here as ui_NNNN.png, one per keyframe. */
|
|
9
9
|
outDir: string;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* CSS-pixel viewport the HUD lays out against — the recorded window's
|
|
12
|
+
* aspect-corrected size when the manifest carries one, else the recording
|
|
13
|
+
* resolution (see computeRasterGeometry in hud-state.ts).
|
|
14
|
+
*/
|
|
11
15
|
width: number;
|
|
12
16
|
height: number;
|
|
17
|
+
/** PNG pixels per CSS pixel; >1 keeps overlays at/above the recording size. */
|
|
18
|
+
deviceScaleFactor: number;
|
|
13
19
|
}
|
|
14
20
|
export declare function uiPngPath(outDir: string, index: number): string;
|
|
15
21
|
/**
|
|
16
22
|
* Render one transparent PNG per keyframe, reusing a single browser + page.
|
|
17
|
-
* Returns paths in keyframe order
|
|
18
|
-
* (
|
|
23
|
+
* Returns paths in keyframe order — a slot is null when that one keyframe
|
|
24
|
+
* failed (the caller holds the previous overlay across its window) — or null
|
|
25
|
+
* overall when UI must be skipped entirely (no Chrome, harness unreachable, the
|
|
26
|
+
* page died mid-run, or every keyframe failed).
|
|
19
27
|
*/
|
|
20
|
-
export declare function rasterizeUiKeyframes(keyframes: readonly UiKeyframe[], options: RasterizeOptions): Promise<string
|
|
21
|
-
/**
|
|
22
|
-
* Width/height straight out of a PNG's IHDR chunk, so the filtergraph only
|
|
23
|
-
* pays for a scale filter when the overlay really is the wrong size.
|
|
24
|
-
*/
|
|
25
|
-
export declare function readPngSize(filePath: string): {
|
|
26
|
-
width: number;
|
|
27
|
-
height: number;
|
|
28
|
-
} | null;
|
|
28
|
+
export declare function rasterizeUiKeyframes(keyframes: readonly UiKeyframe[], options: RasterizeOptions): Promise<Array<string | null> | null>;
|
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
* The harness page (`/hud-replay.html` on the running `bitmagic dev` sidecar) owns a real GameHUD,
|
|
5
5
|
* so the overlay is the game's own UI rather than a reimplementation. Contract:
|
|
6
6
|
* window.__hudReplayReady === true when the page can accept states
|
|
7
|
-
* window.__hudReplay.render(state)
|
|
7
|
+
* window.__hudReplay.render(state, ms) synchronous; applies a replay state,
|
|
8
|
+
* `ms` seeding animation phase on the
|
|
9
|
+
* source clock (older harness: ignored)
|
|
8
10
|
* window.__hudReplay.setViewport(w, h) sizes the HUD root
|
|
11
|
+
* window.__hudReplay.settle() optional; resolves when fonts/images
|
|
12
|
+
* the state started loading are in (or
|
|
13
|
+
* a short deadline passed)
|
|
9
14
|
* plus a transparent page background, so `omitBackground` yields real alpha.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* Failure here degrades instead of failing the trailer: a keyframe that will
|
|
17
|
+
* not rasterize becomes a null slot (the caller holds the previous overlay over
|
|
18
|
+
* its window), and only an unusable page — navigation failed, the page died, or
|
|
19
|
+
* every single keyframe threw — degrades to "no overlays" (null). A UI-less
|
|
20
|
+
* video still beats no video. Chrome is the creator's installed browser
|
|
13
21
|
* (`channel: 'chrome'`), the same one `bitmagic verify` drives — nothing is downloaded.
|
|
14
22
|
*/
|
|
15
23
|
import * as fs from 'fs';
|
|
@@ -18,20 +26,29 @@ import { chromium } from 'playwright-core';
|
|
|
18
26
|
import { out, warn } from './io.js';
|
|
19
27
|
const NAV_TIMEOUT_MS = 20000;
|
|
20
28
|
const READY_TIMEOUT_MS = 20000;
|
|
29
|
+
/** Consecutive keyframe failures after which the run gives up on UI entirely. */
|
|
30
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
21
31
|
export const CHROME_MISSING_HINT = 'Google Chrome could not be launched — install it from https://www.google.com/chrome/';
|
|
22
32
|
/**
|
|
23
33
|
* The state is embedded in the expression rather than passed as an argument:
|
|
24
34
|
* Playwright treats a string pageFunction as a plain expression and drops the
|
|
25
35
|
* arg, so an IIFE is the reliable form.
|
|
26
36
|
*/
|
|
27
|
-
function applyStateExpression(state, width, height) {
|
|
37
|
+
function applyStateExpression(state, width, height, frameTimeMs, clipIndex) {
|
|
28
38
|
return ('(() => {' +
|
|
29
39
|
' const replay = window.__hudReplay;' +
|
|
30
40
|
' if (!replay || typeof replay.render !== "function") throw new Error("window.__hudReplay.render is missing");' +
|
|
31
41
|
' if (typeof replay.setViewport === "function") replay.setViewport(' +
|
|
32
42
|
`${JSON.stringify(width)}, ${JSON.stringify(height)});` +
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
// The second and third arguments seed animation phase (source time, and
|
|
44
|
+
// the clip so the harness can tell a new clip's first keyframe from a
|
|
45
|
+
// mid-clip one); an older harness ignores them.
|
|
46
|
+
` replay.render(${JSON.stringify(state)}, ${JSON.stringify(frameTimeMs)}, ${JSON.stringify(clipIndex)});` +
|
|
47
|
+
// settle() is awaited here rather than in a second round trip: Playwright
|
|
48
|
+
// awaits a thenable returned from a string expression, so this is one
|
|
49
|
+
// protocol call per keyframe instead of two, with identical ordering. An
|
|
50
|
+
// older harness has no settle() and simply returns true.
|
|
51
|
+
' return typeof replay.settle === "function" ? replay.settle() : true;' +
|
|
35
52
|
'})()');
|
|
36
53
|
}
|
|
37
54
|
/** Headless system Chrome for offscreen rasterization; null (with a warning) when it cannot start. */
|
|
@@ -47,36 +64,90 @@ export async function launchRasterBrowser(purpose) {
|
|
|
47
64
|
export function uiPngPath(outDir, index) {
|
|
48
65
|
return path.join(outDir, `ui_${String(index).padStart(4, '0')}.png`);
|
|
49
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Cheap liveness probe. A crashed target or closed browser rejects every
|
|
69
|
+
* protocol call immediately, so this distinguishes "that one state was bad"
|
|
70
|
+
* from "there is no page any more".
|
|
71
|
+
*/
|
|
72
|
+
async function pageIsAlive(page) {
|
|
73
|
+
try {
|
|
74
|
+
await page.evaluate('1');
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
50
81
|
/**
|
|
51
82
|
* Render one transparent PNG per keyframe, reusing a single browser + page.
|
|
52
|
-
* Returns paths in keyframe order
|
|
53
|
-
* (
|
|
83
|
+
* Returns paths in keyframe order — a slot is null when that one keyframe
|
|
84
|
+
* failed (the caller holds the previous overlay across its window) — or null
|
|
85
|
+
* overall when UI must be skipped entirely (no Chrome, harness unreachable, the
|
|
86
|
+
* page died mid-run, or every keyframe failed).
|
|
54
87
|
*/
|
|
55
88
|
export async function rasterizeUiKeyframes(keyframes, options) {
|
|
56
89
|
if (keyframes.length === 0)
|
|
57
90
|
return [];
|
|
91
|
+
// Before the browser exists: a throwing mkdir must not leak a Chrome.
|
|
92
|
+
fs.mkdirSync(options.outDir, { recursive: true });
|
|
58
93
|
const browser = await launchRasterBrowser('ui');
|
|
59
94
|
if (browser === null) {
|
|
60
95
|
warn('ui: rendering without HUD overlays');
|
|
61
96
|
return null;
|
|
62
97
|
}
|
|
63
|
-
fs.mkdirSync(options.outDir, { recursive: true });
|
|
64
98
|
try {
|
|
65
99
|
const context = await browser.newContext({
|
|
66
100
|
viewport: { width: options.width, height: options.height },
|
|
67
|
-
deviceScaleFactor:
|
|
101
|
+
deviceScaleFactor: options.deviceScaleFactor
|
|
68
102
|
});
|
|
69
103
|
const page = await context.newPage();
|
|
70
104
|
await page.goto(options.harnessUrl, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
|
|
71
105
|
await page.waitForFunction('window.__hudReplayReady === true', undefined, { timeout: READY_TIMEOUT_MS });
|
|
72
106
|
const paths = [];
|
|
107
|
+
let failed = 0;
|
|
108
|
+
let consecutiveFailures = 0;
|
|
73
109
|
for (let i = 0; i < keyframes.length; i++) {
|
|
74
110
|
const pngPath = uiPngPath(options.outDir, i);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
try {
|
|
112
|
+
await page.evaluate(applyStateExpression(keyframes[i].state, options.width, options.height, Math.round(keyframes[i].frameTimeMs), keyframes[i].clipIndex));
|
|
113
|
+
await page.screenshot({ path: pngPath, omitBackground: true, type: 'png' });
|
|
114
|
+
paths.push(pngPath);
|
|
115
|
+
consecutiveFailures = 0;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
// One bad state must not throw away the overlays already on disk.
|
|
119
|
+
failed++;
|
|
120
|
+
consecutiveFailures++;
|
|
121
|
+
warn(`ui: keyframe ${i} failed to rasterize ` +
|
|
122
|
+
`(${err instanceof Error ? err.message : String(err)}) — holding the previous overlay`);
|
|
123
|
+
paths.push(null);
|
|
124
|
+
// A dead page fails every REMAINING keyframe too, and each null slot
|
|
125
|
+
// extends the last good overlay — which would freeze one stale HUD
|
|
126
|
+
// over the whole rest of the trailer. That is worse than no UI, so
|
|
127
|
+
// probe liveness and bail out whole rather than degrade per keyframe.
|
|
128
|
+
if (!(await pageIsAlive(page))) {
|
|
129
|
+
warn(`ui: the replay page died after ${paths.length - failed} overlay(s) — rendering without UI ` +
|
|
130
|
+
'rather than freezing the last one over the remaining clips');
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// A page that answers evaluate('1') but fails every screenshot (a
|
|
134
|
+
// stalled compositor, an unwritable .ui) would otherwise pay the full
|
|
135
|
+
// screenshot timeout once per remaining keyframe and then freeze the
|
|
136
|
+
// last good overlay across them — hours of waiting for a worse video.
|
|
137
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
138
|
+
warn(`ui: ${consecutiveFailures} keyframes in a row failed to rasterize — rendering without UI ` +
|
|
139
|
+
'rather than holding one overlay over everything that follows');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (failed === keyframes.length) {
|
|
145
|
+
warn(`ui: every keyframe failed to rasterize against ${options.harnessUrl} — rendering without UI`);
|
|
146
|
+
return null;
|
|
78
147
|
}
|
|
79
|
-
out(`ui: rasterized ${paths.length} overlay(s) at ${options.width}x${options.height}
|
|
148
|
+
out(`ui: rasterized ${paths.length - failed} overlay(s) at ${options.width}x${options.height}` +
|
|
149
|
+
`${options.deviceScaleFactor !== 1 ? `@${options.deviceScaleFactor.toFixed(2)}x` : ''} ` +
|
|
150
|
+
`from ${options.harnessUrl}${failed > 0 ? ` (${failed} failed, held over)` : ''}`);
|
|
80
151
|
return paths;
|
|
81
152
|
}
|
|
82
153
|
catch (err) {
|
|
@@ -93,22 +164,4 @@ export async function rasterizeUiKeyframes(keyframes, options) {
|
|
|
93
164
|
}
|
|
94
165
|
}
|
|
95
166
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Width/height straight out of a PNG's IHDR chunk, so the filtergraph only
|
|
98
|
-
* pays for a scale filter when the overlay really is the wrong size.
|
|
99
|
-
*/
|
|
100
|
-
export function readPngSize(filePath) {
|
|
101
|
-
const header = Buffer.alloc(24);
|
|
102
|
-
const fd = fs.openSync(filePath, 'r');
|
|
103
|
-
try {
|
|
104
|
-
if (fs.readSync(fd, header, 0, 24, 0) < 24)
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
fs.closeSync(fd);
|
|
109
|
-
}
|
|
110
|
-
if (header.readUInt32BE(0) !== 0x89504e47 || header.toString('ascii', 12, 16) !== 'IHDR')
|
|
111
|
-
return null;
|
|
112
|
-
return { width: header.readUInt32BE(16), height: header.readUInt32BE(20) };
|
|
113
|
-
}
|
|
114
167
|
//# sourceMappingURL=hud-raster.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hud-raster.js","sourceRoot":"","sources":["../../src/trailer/hud-raster.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hud-raster.js","sourceRoot":"","sources":["../../src/trailer/hud-raster.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,8 @@ export interface HudHealthState {
|
|
|
24
24
|
current: number;
|
|
25
25
|
max: number;
|
|
26
26
|
visible: boolean;
|
|
27
|
+
/** Pixel width from showHealth({width}); absent = the engine default. */
|
|
28
|
+
width?: number;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* One HUD element. Keys beyond id/type/anchor/visible are per-kind and are
|
|
@@ -52,6 +54,8 @@ export interface HudElementState {
|
|
|
52
54
|
export interface HudToastState {
|
|
53
55
|
message: string;
|
|
54
56
|
variant?: string;
|
|
57
|
+
/** IGameHUD anchor; absent on recordings that predate anchor capture. */
|
|
58
|
+
anchor?: string;
|
|
55
59
|
}
|
|
56
60
|
export interface HudReplayState {
|
|
57
61
|
version: 1;
|
|
@@ -63,7 +67,48 @@ export interface HudReplayState {
|
|
|
63
67
|
/** Creation order (= stacking order within an anchor); removed ones dropped. */
|
|
64
68
|
elements: HudElementState[];
|
|
65
69
|
toasts: HudToastState[];
|
|
70
|
+
/**
|
|
71
|
+
* Game-authored head stylesheets, first-logged order, last write per id.
|
|
72
|
+
* The harness injects these before drawing — they are what turn
|
|
73
|
+
* custom-element class names into pixels.
|
|
74
|
+
*
|
|
75
|
+
* Shared between snapshots while unchanged, so treat it as frozen: never
|
|
76
|
+
* mutate the array or its entries in place.
|
|
77
|
+
*/
|
|
78
|
+
stylesheets: ReadonlyArray<HudStylesheetState>;
|
|
79
|
+
}
|
|
80
|
+
export interface HudStylesheetState {
|
|
81
|
+
id: string;
|
|
82
|
+
css: string;
|
|
83
|
+
}
|
|
84
|
+
export interface RasterGeometry {
|
|
85
|
+
/** CSS-pixel viewport for the harness browser context. */
|
|
86
|
+
viewportWidth: number;
|
|
87
|
+
viewportHeight: number;
|
|
88
|
+
deviceScaleFactor: number;
|
|
66
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Where to lay the HUD out for rasterization.
|
|
92
|
+
*
|
|
93
|
+
* During capture only the CANVAS is resized to the recording resolution — the
|
|
94
|
+
* DOM HUD keeps laying out against the player's window. Rastering at the
|
|
95
|
+
* recording size therefore renders every fixed-pixel HUD metric too small
|
|
96
|
+
* relative to the footage (and flips vw-clamps and media queries). With the
|
|
97
|
+
* recorded window known, raster at an ASPECT-CORRECTED viewport: the window's
|
|
98
|
+
* height (so element size relative to screen height matches what the player
|
|
99
|
+
* saw) at the recording's aspect (raw window dimensions would stretch glyphs
|
|
100
|
+
* non-uniformly under the final scale whenever the aspects differ). The
|
|
101
|
+
* deviceScaleFactor keeps the PNG at or above the recording resolution so
|
|
102
|
+
* ffmpeg only ever downscales.
|
|
103
|
+
*
|
|
104
|
+
* No recorded window → the recording resolution at 1x, bit-for-bit the old
|
|
105
|
+
* behavior.
|
|
106
|
+
*/
|
|
107
|
+
export declare function computeRasterGeometry(recordingWidth: number, recordingHeight: number, window?: {
|
|
108
|
+
innerWidth: number;
|
|
109
|
+
innerHeight: number;
|
|
110
|
+
devicePixelRatio: number;
|
|
111
|
+
}): RasterGeometry;
|
|
67
112
|
/** The engine records `frame` on a 60fps source clock. */
|
|
68
113
|
export declare const DEFAULT_HUD_FPS = 60;
|
|
69
114
|
/** IGameHUD.showToast's default duration. */
|
|
@@ -78,6 +123,12 @@ export declare function isBlankHudState(state: HudReplayState): boolean;
|
|
|
78
123
|
export interface UiKeyframe {
|
|
79
124
|
/** Source frame whose state this is. */
|
|
80
125
|
sourceFrame: number;
|
|
126
|
+
/**
|
|
127
|
+
* `sourceFrame` as milliseconds on the SOURCE clock. The harness seeds
|
|
128
|
+
* animation phase from it, so motion stays locked to the footage through a
|
|
129
|
+
* speed ramp — output time would run it fast or slow against the frames.
|
|
130
|
+
*/
|
|
131
|
+
frameTimeMs: number;
|
|
81
132
|
clipIndex: number;
|
|
82
133
|
/** Output-timeline seconds this overlay becomes visible. */
|
|
83
134
|
outputStartSec: number;
|
|
@@ -88,17 +139,37 @@ export interface UiKeyframe {
|
|
|
88
139
|
export interface CollectUiKeyframesOptions {
|
|
89
140
|
/** Source-clock fps. Default 60. */
|
|
90
141
|
fps?: number;
|
|
91
|
-
/** Ceiling on keyframes per second of OUTPUT time. Default
|
|
142
|
+
/** Ceiling on keyframes per second of OUTPUT time. Default 30. 0 disables. */
|
|
92
143
|
maxPerSecond?: number;
|
|
93
|
-
/** Hard ceiling per clip.
|
|
144
|
+
/** Hard ceiling per clip. Defaults to what the clip's duration allows at `maxPerSecond`. */
|
|
94
145
|
maxPerClip?: number;
|
|
95
146
|
/** Per-clip opt-out; defaults to every clip. */
|
|
96
147
|
clipEnabled?: (clipIndex: number) => boolean;
|
|
97
148
|
/** Where truncation notices go. Defaults to stderr. */
|
|
98
149
|
log?: (line: string) => void;
|
|
99
150
|
}
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
/**
|
|
152
|
+
* 30, not 15: measured on a real cut, the selected footage held 227 distinct
|
|
153
|
+
* HUD states across 15.3 s of output (14.8/s average, four of seven clips over
|
|
154
|
+
* 15/s), so a 15/s ceiling was discarding roughly 45% of the HUD motion the
|
|
155
|
+
* game actually rendered.
|
|
156
|
+
*
|
|
157
|
+
* ffmpeg used to be the reason for a low ceiling — one PNG input plus one
|
|
158
|
+
* serial overlay node per keyframe cost ~23 MB of RSS each and corrupted the
|
|
159
|
+
* render above ~400. The overlays now ride one densified image2 sequence whose
|
|
160
|
+
* cost is flat in keyframe count (measured 2.83 s / 0.94 GB at both 126 and
|
|
161
|
+
* 902 distinct rasters), so the remaining cost is ~25 ms of Playwright per
|
|
162
|
+
* keyframe plus its PNG on disk. 0 disables the limit entirely.
|
|
163
|
+
*/
|
|
164
|
+
export declare const DEFAULT_MAX_KEYFRAMES_PER_SECOND = 30;
|
|
165
|
+
/**
|
|
166
|
+
* Sole per-clip ceiling, a backstop for `--ui-keyframe-rate 0`: at any real
|
|
167
|
+
* rate the limiter below admits at most rate·duration+1 keyframes per clip on
|
|
168
|
+
* its own, so nothing else can bind. A fixed 240 used to freeze the overlay
|
|
169
|
+
* after 16 s of any clip busy enough to sustain the default rate — exactly the
|
|
170
|
+
* clip a trailer is cut from. 60 s of output at 60/s.
|
|
171
|
+
*/
|
|
172
|
+
export declare const MAX_KEYFRAMES_PER_CLIP_CEILING = 3600;
|
|
102
173
|
/**
|
|
103
174
|
* Walk every enabled clip's source range and emit a keyframe wherever the
|
|
104
175
|
* reduced HUD state changes.
|