@hypit/hypit 0.2.5 → 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.
Files changed (79) hide show
  1. package/README.md +6 -1
  2. package/dist/public/hyperframes.d.ts +19 -2
  3. package/package.json +3 -1
  4. package/packages/caption/src/temporalize.ts +11 -1
  5. package/packages/caption-fine/README.md +4 -4
  6. package/packages/credential-store-file/README.md +4 -0
  7. package/packages/credential-store-file/src/store.ts +2 -2
  8. package/packages/credential-store-os/runtime/windows-credential.ps1 +3 -0
  9. package/packages/credential-store-os/src/windows.ts +14 -0
  10. package/packages/credential-store-platform/README.md +2 -0
  11. package/packages/hyperframes/README.md +7 -0
  12. package/packages/hyperframes/src/html-project.ts +99 -0
  13. package/packages/hyperframes/src/index.ts +2 -0
  14. package/packages/hyperframes/src/project.ts +28 -0
  15. package/packages/media-execution/src/execute.ts +21 -3
  16. package/packages/pixverse/README.md +35 -0
  17. package/packages/pixverse/package.json +21 -0
  18. package/packages/pixverse/src/activation.ts +11 -0
  19. package/packages/pixverse/src/index.ts +122 -0
  20. package/packages/pixverse/src/surface.ts +146 -0
  21. package/packages/provider-hiapi/README.md +9 -1
  22. package/packages/provider-hyperframes-local/README.md +19 -7
  23. package/packages/provider-hyperframes-local/src/capture.ts +18 -8
  24. package/packages/provider-hyperframes-local/src/index.ts +1 -1
  25. package/packages/provider-hyperframes-local/src/output.ts +1 -1
  26. package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
  27. package/packages/provider-hyperframes-local/src/provider.ts +29 -2
  28. package/packages/provider-hyperframes-local/src/render.ts +45 -20
  29. package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
  30. package/packages/provider-hypihub/README.md +10 -2
  31. package/packages/provider-monid/README.md +29 -9
  32. package/packages/provider-monid/package.json +2 -1
  33. package/packages/provider-monid/src/mapping.ts +24 -0
  34. package/packages/provider-monid/src/provider.ts +24 -11
  35. package/packages/provider-monid/src/routes.ts +25 -3
  36. package/packages/provider-pollo/README.md +9 -1
  37. package/packages/provider-tokendance/README.md +9 -1
  38. package/packages/provider-whisperx-local/README.md +8 -5
  39. package/packages/render-hyperframes/README.md +9 -0
  40. package/packages/render-hyperframes/src/index.ts +3 -0
  41. package/packages/render-hyperframes/src/manifest.ts +7 -2
  42. package/packages/render-hyperframes/src/product.ts +41 -2
  43. package/packages/runtime-local/src/process-control.ts +4 -1
  44. package/packages/runtime-local/src/programs.ts +15 -3
  45. package/packages/runtime-local/src/supervisor.ts +8 -1
  46. package/packages/script/README.md +11 -4
  47. package/packages/script/src/format.ts +11 -24
  48. package/packages/script/src/manifest.ts +2 -2
  49. package/packages/script/src/parser.ts +16 -21
  50. package/packages/script/src/surface.ts +5 -3
  51. package/packages/script/src/types.ts +1 -1
  52. package/packages/source/README.md +1 -1
  53. package/packages/source/src/header.ts +2 -1
  54. package/packages/studio/README.md +13 -0
  55. package/packages/studio/src/feedback-server.ts +7 -0
  56. package/packages/studio/src/mutation-origin.ts +20 -0
  57. package/packages/studio/src/parameters.ts +3 -7
  58. package/packages/studio/src/preview/render.ts +7 -1
  59. package/packages/studio/src/server.ts +39 -0
  60. package/packages/studio/src/session.ts +7 -3
  61. package/packages/studio/src/ui/syntax.ts +11 -10
  62. package/packages/temporal-markup/EDITING.md +1 -1
  63. package/packages/video-cli/README.md +47 -4
  64. package/packages/video-cli/package.json +3 -0
  65. package/packages/video-cli/src/cli.ts +9 -9
  66. package/packages/video-cli/src/creation.ts +3 -3
  67. package/packages/video-cli/src/frame-grid.ts +34 -0
  68. package/packages/video-cli/src/index.ts +4 -0
  69. package/packages/video-cli/src/media-frames.ts +25 -0
  70. package/packages/video-cli/src/media.ts +198 -46
  71. package/packages/video-cli/src/process.ts +13 -3
  72. package/packages/video-cli/src/secret-input.ts +14 -0
  73. package/packages/video-cli/src/snapshot.ts +186 -0
  74. package/packages/wan/README.md +31 -0
  75. package/packages/wan/package.json +21 -0
  76. package/packages/wan/src/activation.ts +11 -0
  77. package/packages/wan/src/index.ts +160 -0
  78. package/packages/wan/src/surface.ts +147 -0
  79. 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
+ }
@@ -0,0 +1,31 @@
1
+ # `@hypit/wan`
2
+
3
+ Exact author/compute contracts and package-owned author Surfaces for Wan 2.7 Image and Wan 2.7 Image
4
+ Pro.
5
+
6
+ The model variants are separate endpoints with exact request validation. Their Surfaces project the
7
+ primary result to an ordinary image Artifact. The package contains no Provider selection, API key or
8
+ network execution. The selected Provider implements its exact capability.
9
+
10
+ Import the model variant you mean and connect prompt and references as ordinary graph edges:
11
+
12
+ ```xml
13
+ <wan:Image id="draft" prompt={prompt} resolution="2K">
14
+ <wan:Reference image={product.image}/>
15
+ </wan:Image>
16
+
17
+ <wan:ProImage id="hero" prompt={heroPrompt} resolution="4K"/>
18
+ ```
19
+
20
+ The Surface only lowers this syntax into the package's exact model request. It does not select a
21
+ Provider.
22
+
23
+ Both variants accept prompts up to 5,000 characters and up to nine references, and render at `1K` or
24
+ `2K`; the Pro variant adds `4K`. The model takes no aspect ratio: with references present the output
25
+ takes the shape of the last one, and a prompt-only element renders at the band's own framing. There
26
+ is no negative prompt either, so write exclusions into the prompt.
27
+
28
+ `count` renders up to four pictures. Setting `image-set` renders one storyline across several
29
+ pictures instead, where `count` names a ceiling of up to twelve and the model chooses how many it
30
+ returns. An image set shapes a storyline the way `extended-reasoning` shapes a single picture, so the
31
+ two are not stated together.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@hypit/wan",
3
+ "version": "0.0.0-dev",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "private": true,
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "hypit": {
11
+ "activation": "./src/activation.ts"
12
+ },
13
+ "dependencies": {
14
+ "@hypit/artifact": "workspace:*",
15
+ "@hypit/generation": "workspace:*",
16
+ "@hypit/markup": "workspace:*",
17
+ "@hypit/model-kit": "workspace:*",
18
+ "@hypit/protocol": "workspace:*",
19
+ "@hypit/text": "workspace:*"
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ import { createMarkupSurfaceHostFacet } from "@hypit/markup";
2
+ import { wanComponent, wanDefinition, wanManifest, wanModuleRef, wanMarkupSurfaces } from "./index.js";
3
+ import { decodeWanImageSurface, decodeWanProImageSurface } from "./surface.js";
4
+ export const hypitPackage = { format: "hypit.node-package@1" as const, modules: [{ manifest: wanManifest }], components: [wanComponent], hostFacets: [
5
+ wanDefinition.hostFacet,
6
+ createMarkupSurfaceHostFacet({ module: wanModuleRef,
7
+ declaration: wanMarkupSurfaces.find((item) => item.name === "image")!, handler: decodeWanImageSurface }),
8
+ createMarkupSurfaceHostFacet({ module: wanModuleRef,
9
+ declaration: wanMarkupSurfaces.find((item) => item.name === "pro-image")!, handler: decodeWanProImageSurface }),
10
+ ] };
11
+ export default hypitPackage;
@@ -0,0 +1,160 @@
1
+ import { artifactTypes } from "@hypit/artifact";
2
+ import { sealGenerationPortRequest, sealGenerationPortTable } from "@hypit/generation";
3
+ import type { GenerationPortTable, GenerationPortValue, GenerationRequest } from "@hypit/generation";
4
+ import type { SurfaceAttributeVocabulary, SurfaceChildVocabulary } from "@hypit/markup";
5
+ import { defineExactModelModule } from "@hypit/model-kit";
6
+ import { textTypes } from "@hypit/text";
7
+
8
+ export const wanModuleRef = { name: "@hypit/wan", version: "1" } as const;
9
+ export const wanModels = ["wan-2.7-image", "wan-2.7-image-pro"] as const;
10
+ export type WanModel = typeof wanModels[number];
11
+
12
+ /**
13
+ * The model renders at one of these bands. It takes no aspect ratio: with input images the output
14
+ * follows the last one, and a prompt-only request renders at the band's own framing.
15
+ */
16
+ const WAN_RESOLUTIONS = ["1K", "2K"] as const;
17
+ const WAN_PRO_RESOLUTIONS = ["1K", "2K", "4K"] as const;
18
+
19
+ function wanPortTable(model: WanModel): GenerationPortTable {
20
+ const pro = model === "wan-2.7-image-pro";
21
+ return sealGenerationPortTable({
22
+ model,
23
+ result: "image",
24
+ ports: [
25
+ { name: "prompt", value: { kind: "text", maxChars: 5_000 }, minItems: 1, maxItems: 1 },
26
+ { name: "images", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: 9 },
27
+ {
28
+ name: "resolution",
29
+ value: { kind: "enum", values: pro ? [...WAN_PRO_RESOLUTIONS] : [...WAN_RESOLUTIONS] },
30
+ minItems: 0,
31
+ maxItems: 1,
32
+ },
33
+ // One request renders up to four pictures, or up to twelve as a story-coherent set. The
34
+ // model chooses how many of an image set it returns, so this names a ceiling.
35
+ { name: "count", value: { kind: "number", integer: true, minimum: 1, maximum: 12 }, minItems: 0, maxItems: 1 },
36
+ { name: "imageSet", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
37
+ { name: "extendedReasoning", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
38
+ { name: "watermark", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
39
+ { name: "seed", value: { kind: "number", integer: true, minimum: 0, maximum: 2_147_483_647 }, minItems: 0, maxItems: 1 },
40
+ ],
41
+ // An image set renders one storyline in several pictures, which is what extended reasoning
42
+ // would otherwise shape for a single picture.
43
+ requires: [{ kind: "atMostOneOf", ports: ["imageSet", "extendedReasoning"] }],
44
+ });
45
+ }
46
+
47
+ export const wanPorts: Readonly<Record<WanModel, GenerationPortTable>> = {
48
+ "wan-2.7-image": wanPortTable("wan-2.7-image"),
49
+ "wan-2.7-image-pro": wanPortTable("wan-2.7-image-pro"),
50
+ };
51
+
52
+ export function sealWanRequest(
53
+ model: WanModel,
54
+ ports: Readonly<Record<string, readonly GenerationPortValue[]>>,
55
+ ): GenerationRequest {
56
+ return sealGenerationPortRequest(wanPorts[model], ports);
57
+ }
58
+
59
+ const wanBaseDefinition = defineExactModelModule({
60
+ module: wanModuleRef,
61
+ endpoints: wanModels.map((model) => ({
62
+ key: model === "wan-2.7-image" ? "image" : "pro",
63
+ requestTypeName: model === "wan-2.7-image" ? "WanImageRequest" : "WanProImageRequest",
64
+ producerName: `request-${model}`,
65
+ ports: wanPorts[model],
66
+ })),
67
+ });
68
+
69
+ export const wanEndpoints = wanBaseDefinition.endpoints;
70
+ export const wanComponent = wanBaseDefinition.component;
71
+
72
+ function wanAttributes(pro: boolean): readonly SurfaceAttributeVocabulary[] {
73
+ return [
74
+ { name: "id", kind: "identifier", required: true,
75
+ summary: "Names this generation and prefixes the bindings it publishes." },
76
+ { name: "prompt", kind: "reference", required: true, accepts: [textTypes.text],
77
+ summary: "The Text edge describing the picture, the edit to make, or the storyline of an image set." },
78
+ { name: "resolution", kind: "literal", required: false,
79
+ values: pro ? [...WAN_PRO_RESOLUTIONS] : [...WAN_RESOLUTIONS],
80
+ summary: "The size band the model renders at." },
81
+ { name: "count", kind: "literal", required: false,
82
+ summary: "How many pictures to render, up to four, or the ceiling of an image set." },
83
+ { name: "image-set", kind: "literal", required: false, values: ["true", "false"],
84
+ summary: "Renders one storyline as several pictures instead of one picture." },
85
+ { name: "extended-reasoning", kind: "literal", required: false, values: ["true", "false"],
86
+ summary: "Spends longer shaping a single picture before rendering it." },
87
+ { name: "watermark", kind: "literal", required: false, values: ["true", "false"],
88
+ summary: "Marks the returned pictures as AI generated." },
89
+ { name: "seed", kind: "literal", required: false,
90
+ summary: "Seeds the model's sampling so a rerun stays close to this one." },
91
+ ];
92
+ }
93
+
94
+ const wanChildren: readonly SurfaceChildVocabulary[] = [
95
+ { tag: "Reference", cardinality: "many",
96
+ summary: "Attaches one image Artifact the model edits or draws on.",
97
+ attributes: [
98
+ { name: "image", kind: "reference", required: true, accepts: [artifactTypes.blob],
99
+ summary: "Selects the image Artifact this reference contributes." },
100
+ ] },
101
+ ];
102
+
103
+ const surface = (
104
+ name: "image" | "pro-image",
105
+ tag: "Image" | "ProImage",
106
+ endpoint: (typeof wanEndpoints)["image" | "pro"],
107
+ pro: boolean,
108
+ summary: string,
109
+ example: string,
110
+ ) => ({
111
+ name,
112
+ tag,
113
+ mode: "structured" as const,
114
+ outputs: [endpoint!.draftType, endpoint!.mediaBindings.images!.type],
115
+ vocabulary: {
116
+ summary,
117
+ attributes: wanAttributes(pro),
118
+ children: wanChildren,
119
+ ports: [{
120
+ name: "image",
121
+ type: artifactTypes.blob,
122
+ summary: "The primary generated image, addressed as `<id>.image`.",
123
+ }],
124
+ example,
125
+ notes: [
126
+ "The element accepts at most 9 `Reference` children and no text content.",
127
+ "With references present the output takes the shape of the last one; a prompt-only element renders at the band's own framing.",
128
+ "The model reads exclusions from the prompt itself, so write them there.",
129
+ "Every `Reference` is an ordinary image Artifact edge; the Surface copies no runtime media into request metadata.",
130
+ "The Surface lowers the element into the package's exact model request and selects no Provider.",
131
+ ],
132
+ },
133
+ });
134
+
135
+ export const wanMarkupSurfaces = [
136
+ surface("image", "Image", wanEndpoints.image!, false,
137
+ "Generates pictures with the exact Wan 2.7 Image model from a Text prompt and optional reference images.",
138
+ `<wan:Image
139
+ id="draft"
140
+ prompt={prompt}
141
+ resolution="2K"
142
+ >
143
+ <wan:Reference image={product.image}/>
144
+ </wan:Image>`),
145
+ surface("pro-image", "ProImage", wanEndpoints.pro!, true,
146
+ "Generates pictures with the exact Wan 2.7 Image Pro model from a Text prompt and optional reference images.",
147
+ `<wan:ProImage
148
+ id="hero"
149
+ prompt={heroPrompt}
150
+ resolution="4K"
151
+ />`),
152
+ ] as const;
153
+
154
+ export const wanManifest = {
155
+ ...wanBaseDefinition.manifest,
156
+ };
157
+ export const wanDefinition = {
158
+ ...wanBaseDefinition,
159
+ manifest: wanManifest,
160
+ };
@@ -0,0 +1,147 @@
1
+ import { artifactTypes } from "@hypit/artifact";
2
+ import { generationPort, sealGenerationMediaBinding, sealGenerationRequestDraft } from "@hypit/generation";
3
+ import type { GenerationMediaPort, GenerationPortValue } from "@hypit/generation";
4
+ import {
5
+ createExactModelPrimaryGenerationFragment,
6
+ exactModelMediaInputNames,
7
+ exactModelTextInputName,
8
+ } from "@hypit/model-kit";
9
+ import type { ExactModelEndpoint } from "@hypit/model-kit";
10
+ import type {
11
+ MarkupAttributeValue,
12
+ StructuredElement,
13
+ StructuredSurfaceHandler,
14
+ SurfaceResolvedReference,
15
+ } from "@hypit/markup";
16
+ import type { CanonicalValue, TypeRef } from "@hypit/protocol";
17
+ import { textTypes, verifyText } from "@hypit/text";
18
+
19
+ import { wanEndpoints } from "./index.js";
20
+
21
+ function assert(condition: unknown, message: string): asserts condition {
22
+ if (!condition) throw new Error(message);
23
+ }
24
+
25
+ function localName(name: string): string {
26
+ return name.includes(":") ? name.slice(name.lastIndexOf(":") + 1) : name;
27
+ }
28
+
29
+ function sameType(left: TypeRef, right: TypeRef): boolean {
30
+ return left.name === right.name && left.module.name === right.module.name && left.module.version === right.module.version;
31
+ }
32
+
33
+ function exact(element: StructuredElement, allowed: readonly string[], required: readonly string[]): void {
34
+ const unknown = Object.keys(element.attributes).filter((name) => !allowed.includes(name));
35
+ assert(unknown.length === 0, `${element.name} does not accept ${unknown[0]}`);
36
+ const missing = required.filter((name) => element.attributes[name] === undefined);
37
+ assert(missing.length === 0, `${element.name} requires ${missing.join(", ")}`);
38
+ }
39
+
40
+ function text(element: StructuredElement, name: string): string {
41
+ const value = element.attributes[name];
42
+ assert(typeof value === "string" && value.trim().length > 0, `${element.name}.${name} must be text`);
43
+ return value.trim();
44
+ }
45
+
46
+ /** An absent optional attribute leaves its port unstated, which the model reads as its own default. */
47
+ function optionalText(element: StructuredElement, name: string): readonly GenerationPortValue[] | undefined {
48
+ return element.attributes[name] === undefined ? undefined : [text(element, name)];
49
+ }
50
+
51
+ function optionalFlag(element: StructuredElement, name: string): readonly GenerationPortValue[] | undefined {
52
+ if (element.attributes[name] === undefined) return undefined;
53
+ const value = text(element, name);
54
+ assert(value === "true" || value === "false", `${element.name}.${name} must be true or false`);
55
+ return [value === "true"];
56
+ }
57
+
58
+ function optionalInteger(element: StructuredElement, name: string): readonly GenerationPortValue[] | undefined {
59
+ if (element.attributes[name] === undefined) return undefined;
60
+ const value = text(element, name);
61
+ assert(/^\d+$/u.test(value), `${element.name}.${name} must be a whole number`);
62
+ return [Number(value)];
63
+ }
64
+
65
+ function ref(
66
+ element: StructuredElement,
67
+ name: string,
68
+ type: TypeRef,
69
+ resolve: (path: string) => SurfaceResolvedReference | undefined,
70
+ ): SurfaceResolvedReference {
71
+ const value: MarkupAttributeValue | undefined = element.attributes[name];
72
+ assert(typeof value === "object" && value.kind === "reference", `${element.name}.${name} must be a reference`);
73
+ const result = resolve(value.path);
74
+ assert(result !== undefined && sameType(result.type, type), `${element.name}.${name} has the wrong type`);
75
+ return result;
76
+ }
77
+
78
+ const attributes = ["id", "prompt", "resolution", "count", "image-set", "extended-reasoning", "watermark", "seed"] as const;
79
+
80
+ function decoder(endpoint: ExactModelEndpoint): StructuredSurfaceHandler {
81
+ return ({ element, resolveReference }) => {
82
+ exact(element, [...attributes], ["id", "prompt"]);
83
+ const id = text(element, "id");
84
+ const prompt = ref(element, "prompt", textTypes.text, resolveReference);
85
+ if (prompt.record !== undefined) {
86
+ assert(prompt.record.value.kind === "inline", `${element.name}.prompt must reference Text`);
87
+ verifyText(prompt.record.value.value);
88
+ }
89
+ const imagePort = generationPort(endpoint.ports, "images");
90
+ assert(imagePort.value.kind === "media", "Wan images port is not media");
91
+ const images: SurfaceResolvedReference[] = [];
92
+ for (const child of element.children) {
93
+ if (child.kind === "text") {
94
+ assert(child.value.trim().length === 0, `${element.name} accepts only Reference children`);
95
+ continue;
96
+ }
97
+ assert(localName(child.name) === "Reference", `${element.name} accepts only Reference children`);
98
+ exact(child, ["image"], ["image"]);
99
+ assert(!child.children.some((item) => item.kind === "element" || item.value.trim()), `${child.name} must be empty`);
100
+ const image = ref(child, "image", artifactTypes.blob, resolveReference);
101
+ if (image.record !== undefined) {
102
+ assert(image.record.value.kind === "blob" && image.record.value.mediaType.startsWith("image/"),
103
+ `${child.name}.image must reference image media`);
104
+ }
105
+ images.push(image);
106
+ }
107
+ assert(images.length <= imagePort.maxItems, `${element.name} accepts at most ${imagePort.maxItems} references`);
108
+ const stated: Record<string, readonly GenerationPortValue[] | undefined> = {
109
+ resolution: optionalText(element, "resolution"),
110
+ count: optionalInteger(element, "count"),
111
+ imageSet: optionalFlag(element, "image-set"),
112
+ extendedReasoning: optionalFlag(element, "extended-reasoning"),
113
+ watermark: optionalFlag(element, "watermark"),
114
+ seed: optionalInteger(element, "seed"),
115
+ };
116
+ const draft = sealGenerationRequestDraft(endpoint.ports, Object.fromEntries(
117
+ Object.entries(stated).filter((entry): entry is [string, readonly GenerationPortValue[]] => entry[1] !== undefined),
118
+ ));
119
+ const records: Array<{ id: string; type: TypeRef; value: { kind: "inline"; value: CanonicalValue }; range: StructuredElement["range"] }> = [{
120
+ id: `${id}.draft`, type: endpoint.draftType,
121
+ value: { kind: "inline", value: draft as unknown as CanonicalValue }, range: element.range,
122
+ }];
123
+ const inputs: Record<string, SurfaceResolvedReference["ref"] | { kind: "record"; id: string }> = {
124
+ draft: { kind: "record", id: `${id}.draft` }, [exactModelTextInputName("prompt")]: prompt.ref,
125
+ };
126
+ const media = images.map((image, index) => {
127
+ const name = `image-${String(index + 1).padStart(4, "0")}`;
128
+ const bindingId = `${id}.${name}.binding`;
129
+ records.push({
130
+ id: bindingId, type: endpoint.mediaBindings.images!.type,
131
+ value: { kind: "inline", value: sealGenerationMediaBinding(imagePort as GenerationMediaPort, { role: "image" }) as unknown as CanonicalValue },
132
+ range: element.range,
133
+ });
134
+ const names = exactModelMediaInputNames(name);
135
+ inputs[names.binding] = { kind: "record", id: bindingId };
136
+ inputs[names.artifact] = image.ref;
137
+ return { name, port: "images" } as const;
138
+ });
139
+ const fragment = createExactModelPrimaryGenerationFragment(endpoint, media, [{ name: "prompt", port: "prompt" }]);
140
+ return { records, fragments: [fragment], components: [{
141
+ id, fragment: fragment.id, inputs, outputs: { image: `${id}.image` }, range: element.range,
142
+ }] };
143
+ };
144
+ }
145
+
146
+ export const decodeWanImageSurface = decoder(wanEndpoints.image!);
147
+ export const decodeWanProImageSurface = decoder(wanEndpoints.pro!);
@@ -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 { spawnSync } from "node:child_process";
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
- const media = spawnSync("ffmpeg", ["-version"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
52
- if (media.status !== 0) throw new Error("FFmpeg is unavailable on PATH; install your selected media toolchain before fetching video");
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
- const result = spawnSync(executable, [
56
- "--ignore-config", "--no-update", "--no-remote-components", "--no-plugin-dirs",
57
- "--no-js-runtimes", "--js-runtimes", `node:${process.execPath}`,
58
- "--no-playlist", "--no-progress", "--quiet",
59
- "--format", "bv*+ba/b",
60
- "--merge-output-format", container,
61
- "--format-sort", "res:1080,vcodec:h264",
62
- "--output", join(work, "video.%(ext)s"),
63
- url,
64
- ], { encoding: "utf8", windowsHide: true, timeout: 900_000 });
65
-
66
- if (result.error !== undefined) throw result.error;
67
- if (result.status !== 0) {
68
- throw new Error(`yt-dlp could not fetch ${url}: ${(result.stderr ?? "").trim().slice(-2000)}`);
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();