@hypit/hypit 0.2.6 → 0.2.7
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 +1 -1
- package/dist/public/hyperframes.d.ts +19 -2
- package/package.json +2 -1
- package/packages/caption-fine/README.md +4 -4
- package/packages/credential-store-file/README.md +4 -0
- package/packages/credential-store-file/src/store.ts +2 -2
- package/packages/credential-store-platform/README.md +2 -0
- package/packages/hyperframes/README.md +7 -0
- package/packages/hyperframes/src/html-project.ts +99 -0
- package/packages/hyperframes/src/index.ts +2 -0
- package/packages/hyperframes/src/project.ts +28 -0
- package/packages/pixverse/README.md +35 -0
- package/packages/pixverse/package.json +21 -0
- package/packages/pixverse/src/activation.ts +11 -0
- package/packages/pixverse/src/index.ts +122 -0
- package/packages/pixverse/src/surface.ts +146 -0
- package/packages/provider-hyperframes-local/README.md +19 -7
- package/packages/provider-hyperframes-local/src/capture.ts +18 -8
- package/packages/provider-hyperframes-local/src/index.ts +1 -1
- package/packages/provider-hyperframes-local/src/output.ts +1 -1
- package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
- package/packages/provider-hyperframes-local/src/provider.ts +29 -2
- package/packages/provider-hyperframes-local/src/render.ts +45 -20
- package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
- package/packages/provider-whisperx-local/README.md +8 -5
- package/packages/render-hyperframes/README.md +9 -0
- package/packages/render-hyperframes/src/index.ts +3 -0
- package/packages/render-hyperframes/src/manifest.ts +7 -2
- package/packages/render-hyperframes/src/product.ts +41 -2
- package/packages/runtime-local/src/process-control.ts +4 -1
- package/packages/runtime-local/src/programs.ts +15 -3
- package/packages/runtime-local/src/supervisor.ts +8 -1
- package/packages/script/README.md +11 -4
- package/packages/script/src/format.ts +11 -24
- package/packages/script/src/manifest.ts +2 -2
- package/packages/script/src/parser.ts +16 -21
- package/packages/script/src/surface.ts +5 -3
- package/packages/script/src/types.ts +1 -1
- package/packages/source/README.md +1 -1
- package/packages/source/src/header.ts +2 -1
- package/packages/studio/README.md +13 -0
- package/packages/studio/src/feedback-server.ts +7 -0
- package/packages/studio/src/mutation-origin.ts +20 -0
- package/packages/studio/src/parameters.ts +3 -7
- package/packages/studio/src/preview/render.ts +7 -1
- package/packages/studio/src/server.ts +39 -0
- package/packages/studio/src/session.ts +7 -3
- package/packages/studio/src/ui/syntax.ts +11 -10
- package/packages/temporal-markup/EDITING.md +1 -1
- package/packages/video-cli/README.md +47 -4
- package/packages/video-cli/package.json +3 -0
- package/packages/video-cli/src/cli.ts +9 -9
- package/packages/video-cli/src/creation.ts +3 -3
- package/packages/video-cli/src/frame-grid.ts +34 -0
- package/packages/video-cli/src/index.ts +4 -0
- package/packages/video-cli/src/media-frames.ts +25 -0
- package/packages/video-cli/src/media.ts +198 -46
- package/packages/video-cli/src/process.ts +13 -3
- package/packages/video-cli/src/secret-input.ts +14 -0
- package/packages/video-cli/src/snapshot.ts +186 -0
- package/packages/yt-dlp/src/download.ts +32 -18
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import type { CliIo } from "@hypit/cli";
|
|
10
|
+
import { assertHyperframesDocument, hyperframesHtmlAssetUrls, hyperframesHtmlDomain } from "@hypit/hyperframes";
|
|
11
|
+
import type { HyperframesDocument, HyperframesHtmlProject } from "@hypit/hyperframes";
|
|
12
|
+
import { FileResourceStore } from "@hypit/resource-store-fs";
|
|
13
|
+
import { hyperframesFramesRequest, renderHyperframesCapabilities, renderHyperframesTypes, verifyHyperframesFrames } from "@hypit/render-hyperframes";
|
|
14
|
+
import type { Need } from "@hypit/protocol";
|
|
15
|
+
import { creationEnvironment, selectedProvider, providerLine, providerView } from "./creation.js";
|
|
16
|
+
import type { CreationEnvironment } from "./creation.js";
|
|
17
|
+
import { snapshotFrameLabel, writeFrameGrid } from "./frame-grid.js";
|
|
18
|
+
|
|
19
|
+
const MEDIA_TYPES: Readonly<Record<string, string>> = {
|
|
20
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
|
|
21
|
+
".mp4": "video/mp4", ".webm": "video/webm", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function readProject(source: string, html: string, resources: FileResourceStore): Promise<HyperframesHtmlProject> {
|
|
25
|
+
const assets = [];
|
|
26
|
+
const base = /^https?:/u.test(source) ? new URL(source) : pathToFileURL(source);
|
|
27
|
+
for (const url of hyperframesHtmlAssetUrls(html)) {
|
|
28
|
+
const address = new URL(url, base);
|
|
29
|
+
let mediaType: string | undefined;
|
|
30
|
+
let stream: Readable;
|
|
31
|
+
if (address.protocol === "file:") {
|
|
32
|
+
if (base.protocol !== "file:") throw new Error(`Network HTML cannot read local asset ${url}`);
|
|
33
|
+
const path = fileURLToPath(address);
|
|
34
|
+
mediaType = MEDIA_TYPES[extname(path).toLowerCase()];
|
|
35
|
+
if (mediaType === undefined) throw new Error(`Unsupported snapshot asset ${url}; use compiled HTML with inline scripts and styles`);
|
|
36
|
+
stream = createReadStream(path);
|
|
37
|
+
} else if (address.protocol === "http:" || address.protocol === "https:") {
|
|
38
|
+
const response = await fetch(address);
|
|
39
|
+
if (!response.ok || response.body === null) throw new Error(`Snapshot asset ${url}: HTTP ${response.status}`);
|
|
40
|
+
mediaType = response.headers.get("content-type")?.split(";")[0]?.trim();
|
|
41
|
+
if (mediaType === undefined || !Object.values(MEDIA_TYPES).includes(mediaType)) {
|
|
42
|
+
await response.body.cancel();
|
|
43
|
+
throw new Error(`Unsupported snapshot asset ${url} (${mediaType}); use compiled HTML with inline scripts and styles`);
|
|
44
|
+
}
|
|
45
|
+
stream = Readable.fromWeb(response.body as NodeReadableStream<Uint8Array>);
|
|
46
|
+
} else throw new Error(`Unsupported snapshot asset protocol ${address.protocol}`);
|
|
47
|
+
try { assets.push({ url, artifact: await resources.putStream(stream, mediaType) }); }
|
|
48
|
+
finally { stream.destroy(); }
|
|
49
|
+
}
|
|
50
|
+
return { html, assets };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const OPTIONS = ["--studio", "--to", "--at-frame", "--start-frame", "--end-frame-exclusive", "--step-frames", "--grid", "--cell", "--runtime", "--workspace"];
|
|
54
|
+
|
|
55
|
+
export function writeSnapshotHelp(io: CliIo): void {
|
|
56
|
+
io.write(`hypit snapshot\nCapture exact frames from an existing compiled HyperFrames HTML programme through the selected Runtime Profile.\n\n`
|
|
57
|
+
+ ` hypit snapshot --studio <studio-url> --at-frame <n[,n,…]> --to <directory>\n`
|
|
58
|
+
+ ` hypit snapshot <index.html|HTML-URL> --at-frame <n[,n,…]> --to <directory>\n`
|
|
59
|
+
+ ` hypit snapshot <index.html|HTML-URL> --start-frame <n> --end-frame-exclusive <n> [--step-frames <n>] --to <directory>\n`
|
|
60
|
+
+ ` [--grid <columns>x<rows>] [--cell <pixels>] [--runtime <profile>] [--workspace <project>] [--json]\n\n`
|
|
61
|
+
+ `Frame indices are zero-based on the original programme clock. Ranges exclude the end.\n`
|
|
62
|
+
+ `Writes full-size PNGs and optional paginated grids. --cell controls grid image width (default 480).\n`
|
|
63
|
+
+ `Uses render-frames, one immediate request. Browser preparation belongs to the selected Provider.\n`
|
|
64
|
+
+ `HTML carries the compiled frame clock and media/font URLs; scripts and styles are inline.\nStudio exposes its current programme at http://<host>:<port>/__studio/visual.html.\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runSnapshotCli(argv: readonly string[], io: CliIo, environment: CreationEnvironment = creationEnvironment()): Promise<void> {
|
|
68
|
+
if (argv.includes("--help")) { writeSnapshotHelp(io); return; }
|
|
69
|
+
const options = new Map<string, string>();
|
|
70
|
+
const positionals = [];
|
|
71
|
+
const json = argv.includes("--json");
|
|
72
|
+
for (let index = 1; index < argv.length; index++) {
|
|
73
|
+
const arg = argv[index]!;
|
|
74
|
+
if (["--json", "--debug", "--verbose", "--no-color"].includes(arg)) continue;
|
|
75
|
+
if (arg === "--color") { index++; continue; }
|
|
76
|
+
if (!arg.startsWith("--")) { positionals.push(arg); continue; }
|
|
77
|
+
if (!OPTIONS.includes(arg)) throw new Error(`Unknown snapshot option ${arg}`);
|
|
78
|
+
if (options.has(arg)) throw new Error(`${arg} cannot be repeated`);
|
|
79
|
+
const value = argv[++index];
|
|
80
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
|
81
|
+
options.set(arg, value);
|
|
82
|
+
}
|
|
83
|
+
const studio = options.get("--studio");
|
|
84
|
+
if (positionals.length !== (studio === undefined ? 1 : 0) || !options.has("--to")) throw new Error("snapshot takes one HTML input or --studio <url>, and --to <directory>");
|
|
85
|
+
const integer = (raw: string | undefined, label: string, minimum = 0): number => {
|
|
86
|
+
if (raw === undefined || !/^\d+$/u.test(raw) || !Number.isSafeInteger(Number(raw)) || Number(raw) < minimum) throw new Error(`${label} needs an integer >= ${minimum}`);
|
|
87
|
+
return Number(raw);
|
|
88
|
+
};
|
|
89
|
+
const source = studio === undefined
|
|
90
|
+
? /^https?:\/\//u.test(positionals[0]!) ? positionals[0]! : resolve(environment.cwd, positionals[0]!)
|
|
91
|
+
: new URL("/__studio/document", studio).href;
|
|
92
|
+
let document: HyperframesDocument | undefined;
|
|
93
|
+
let html: string;
|
|
94
|
+
if (studio !== undefined) {
|
|
95
|
+
const response = await fetch(source);
|
|
96
|
+
if (!response.ok) throw new Error(`Studio programme: HTTP ${response.status} ${await response.text()}`);
|
|
97
|
+
document = await response.json() as HyperframesDocument;
|
|
98
|
+
assertHyperframesDocument(document);
|
|
99
|
+
html = document.html;
|
|
100
|
+
} else if (/^https?:/u.test(source)) {
|
|
101
|
+
const response = await fetch(source);
|
|
102
|
+
if (!response.ok) throw new Error(`Snapshot HTML: HTTP ${response.status} ${await response.text()}`);
|
|
103
|
+
html = await response.text();
|
|
104
|
+
} else html = await readFile(source, "utf8");
|
|
105
|
+
const domain = document ?? hyperframesHtmlDomain(html);
|
|
106
|
+
const explicit = options.get("--at-frame");
|
|
107
|
+
let frames: number[];
|
|
108
|
+
if (explicit !== undefined) {
|
|
109
|
+
if (["--start-frame", "--end-frame-exclusive", "--step-frames"].some(key => options.has(key))) throw new Error("Choose --at-frame or a frame range");
|
|
110
|
+
frames = explicit.split(",").map(value => integer(value, "--at-frame"));
|
|
111
|
+
} else {
|
|
112
|
+
const start = integer(options.get("--start-frame"), "--start-frame");
|
|
113
|
+
const end = integer(options.get("--end-frame-exclusive"), "--end-frame-exclusive");
|
|
114
|
+
const step = integer(options.get("--step-frames") ?? "1", "--step-frames", 1);
|
|
115
|
+
if (end <= start || end > domain.frameCount) throw new Error(`The frame range must satisfy 0 <= start < end <= ${domain.frameCount}`);
|
|
116
|
+
frames = Array.from({ length: Math.ceil((end - start) / step) }, (_, index) => start + index * step);
|
|
117
|
+
}
|
|
118
|
+
const grid = options.get("--grid")?.split("x");
|
|
119
|
+
if (grid !== undefined && grid.length !== 2) throw new Error("--grid needs columns x rows, for example 4x3");
|
|
120
|
+
const columns = grid === undefined ? undefined : integer(grid[0], "grid columns", 1);
|
|
121
|
+
const rows = grid === undefined ? undefined : integer(grid[1], "grid rows", 1);
|
|
122
|
+
if (options.has("--cell") && grid === undefined) throw new Error("--cell requires --grid");
|
|
123
|
+
const cell = integer(options.get("--cell") ?? "480", "--cell", 32);
|
|
124
|
+
const to = resolve(environment.cwd, options.get("--to")!);
|
|
125
|
+
if (await stat(to).then(() => true, () => false)) throw new Error(`Destination ${to} already exists`);
|
|
126
|
+
const temporary = await mkdtemp(join(tmpdir(), "hypit-snapshot-"));
|
|
127
|
+
let staging: string | undefined;
|
|
128
|
+
try {
|
|
129
|
+
const resources = new FileResourceStore(temporary);
|
|
130
|
+
let input: { document: HyperframesDocument } | { project: HyperframesHtmlProject };
|
|
131
|
+
if (document !== undefined) {
|
|
132
|
+
for (const artifact of document.artifacts) {
|
|
133
|
+
const response = await fetch(new URL(`/__studio/material/${artifact.resource}`, studio));
|
|
134
|
+
if (!response.ok || response.body === null) throw new Error(`Studio material ${artifact.resource}: HTTP ${response.status}`);
|
|
135
|
+
const stream = Readable.fromWeb(response.body as NodeReadableStream<Uint8Array>);
|
|
136
|
+
try { await resources.writeStream(artifact, stream); }
|
|
137
|
+
finally { stream.destroy(); }
|
|
138
|
+
}
|
|
139
|
+
input = { document };
|
|
140
|
+
} else input = { project: await readProject(source, html, resources) };
|
|
141
|
+
const need: Need = { id: "need:hypit-snapshot", capability: renderHyperframesCapabilities.renderFrames,
|
|
142
|
+
returns: renderHyperframesTypes.frames, result: "record:hypit-snapshot", constraints: hyperframesFramesRequest({ ...input, frames }) };
|
|
143
|
+
const { host, profile } = await environment.openHost(options.get("--runtime"), options.get("--workspace"));
|
|
144
|
+
const provider = await selectedProvider(host, need, profile);
|
|
145
|
+
if (!json) io.write(`Capturing ${frames.length} frames through ${providerLine(provider)}\n`);
|
|
146
|
+
const report = json ? io.writeProgress : io.writeProgress ?? io.write;
|
|
147
|
+
const fulfilled = await host.invoke(need, resources, {
|
|
148
|
+
reportProgress: async progress => { report?.(` · ${progress.phase}\n`); },
|
|
149
|
+
reportDiagnostic: async diagnostic => { report?.(` · ${diagnostic.message}\n`); },
|
|
150
|
+
});
|
|
151
|
+
if (fulfilled.value.kind !== "inline") throw new Error("The frame list came back by reference");
|
|
152
|
+
verifyHyperframesFrames(fulfilled.value.value);
|
|
153
|
+
const images = fulfilled.value.value;
|
|
154
|
+
if (images.length !== frames.length) throw new Error(`Requested ${frames.length} frames, received ${images.length}`);
|
|
155
|
+
await mkdir(dirname(to), { recursive: true });
|
|
156
|
+
staging = await mkdtemp(join(dirname(to), ".hypit-snapshot-"));
|
|
157
|
+
const files = [];
|
|
158
|
+
for (const [index, image] of images.entries()) {
|
|
159
|
+
const frame = frames[index]!;
|
|
160
|
+
const name = `frame-${String(frame).padStart(9, "0")}.png`;
|
|
161
|
+
const stream = await resources.open(image.resource);
|
|
162
|
+
if (stream === undefined) throw new Error(`Snapshot frame ${frame} is unavailable`);
|
|
163
|
+
await pipeline(stream, createWriteStream(join(staging, name), { flags: "wx" }));
|
|
164
|
+
files.push({ frame, seconds: frame * domain.frameRate.denominator / domain.frameRate.numerator, path: join(to, name) });
|
|
165
|
+
}
|
|
166
|
+
const grids: string[] = [];
|
|
167
|
+
if (columns !== undefined && rows !== undefined) {
|
|
168
|
+
const count = columns * rows;
|
|
169
|
+
for (let offset = 0; offset < files.length; offset += count) {
|
|
170
|
+
const page = [];
|
|
171
|
+
for (const file of files.slice(offset, offset + count)) page.push({ path: join(staging, `frame-${String(file.frame).padStart(9, "0")}.png`),
|
|
172
|
+
label: await snapshotFrameLabel(file.frame, file.seconds, cell) });
|
|
173
|
+
const name = `grid-${String(grids.length + 1).padStart(3, "0")}.jpg`;
|
|
174
|
+
await writeFrameGrid(page, join(staging, name), cell, columns);
|
|
175
|
+
grids.push(join(to, name));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
await rename(staging, to);
|
|
179
|
+
staging = undefined;
|
|
180
|
+
if (json) io.write(`${JSON.stringify({ format: "hypit.video-cli-snapshot@1", source, ...providerView(provider), frames: files, grids }, null, 2)}\n`);
|
|
181
|
+
else io.write(`Captured ${files.length} PNGs${grids.length === 0 ? "" : ` and ${grids.length} grid${grids.length === 1 ? "" : "s"}`}\n ${to}\n`);
|
|
182
|
+
} finally {
|
|
183
|
+
await rm(temporary, { recursive: true, force: true });
|
|
184
|
+
if (staging !== undefined) await rm(staging, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -10,13 +10,22 @@
|
|
|
10
10
|
* keep changing, so an unpinned copy makes the same link fetch differently on two machines. This is
|
|
11
11
|
* the same shape WhisperX and OpenCV already use for their Python programs.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
14
|
import { copyFile, mkdtemp, readdir, rename, rm } from "node:fs/promises";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { extname, join } from "node:path";
|
|
17
17
|
|
|
18
18
|
import { requireVideoDownload } from "./environment.js";
|
|
19
19
|
|
|
20
|
+
function runExec(file: string, args: readonly string[], options: { readonly timeout?: number; readonly maxBuffer?: number; readonly signal?: AbortSignal | undefined } = {}): Promise<{ readonly stdout: string; readonly stderr: string }> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
execFile(file, [...args], { encoding: "utf8", windowsHide: true, ...options }, (error, stdout, stderr) => {
|
|
23
|
+
if (error !== null) reject(Object.assign(error, { stdout, stderr }));
|
|
24
|
+
else resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* Whether this is a link to fetch rather than a path to open.
|
|
22
31
|
*
|
|
@@ -42,30 +51,35 @@ export function isVideoUrl(value: string): boolean {
|
|
|
42
51
|
* nothing under the bound. `--no-playlist` keeps a link inside a playlist from fetching the playlist.
|
|
43
52
|
* The container follows the target's extension.
|
|
44
53
|
*/
|
|
45
|
-
export async function downloadVideo(url: string, target: string): Promise<void> {
|
|
54
|
+
export async function downloadVideo(url: string, target: string, options: { readonly signal?: AbortSignal } = {}): Promise<void> {
|
|
46
55
|
const container = extname(target).slice(1).toLowerCase();
|
|
47
56
|
if (!["mp4", "mkv", "webm", "mov"].includes(container)) {
|
|
48
57
|
throw new Error(`${target} must end in .mp4, .mkv, .webm or .mov`);
|
|
49
58
|
}
|
|
50
59
|
const executable = requireVideoDownload();
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
try {
|
|
61
|
+
await runExec("ffmpeg", ["-version"], { timeout: 15_000, signal: options.signal });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (options.signal?.aborted) throw error;
|
|
64
|
+
throw new Error("FFmpeg is unavailable on PATH; install your selected media toolchain before fetching video");
|
|
65
|
+
}
|
|
53
66
|
const work = await mkdtemp(join(tmpdir(), "hypit-fetch-"));
|
|
54
67
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
try {
|
|
69
|
+
await runExec(executable, [
|
|
70
|
+
"--ignore-config", "--no-update", "--no-remote-components", "--no-plugin-dirs",
|
|
71
|
+
"--no-js-runtimes", "--js-runtimes", `node:${process.execPath}`,
|
|
72
|
+
"--no-playlist", "--no-progress", "--quiet",
|
|
73
|
+
"--format", "bv*+ba/b",
|
|
74
|
+
"--merge-output-format", container,
|
|
75
|
+
"--format-sort", "res:1080,vcodec:h264",
|
|
76
|
+
"--output", join(work, "video.%(ext)s"),
|
|
77
|
+
url,
|
|
78
|
+
], { timeout: 900_000, maxBuffer: 10 * 1024 * 1024, signal: options.signal });
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
if (options.signal?.aborted) throw cause;
|
|
81
|
+
const error = cause as { stderr?: string; message?: string };
|
|
82
|
+
throw new Error(`yt-dlp could not fetch ${url}: ${(error.stderr ?? error.message ?? "").trim().slice(-2000)}`);
|
|
69
83
|
}
|
|
70
84
|
const finished = (await readdir(work)).filter((name) => !name.endsWith(".part"));
|
|
71
85
|
const [file] = finished.sort();
|