@hypit/hypit 0.2.5 → 0.2.6

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 CHANGED
@@ -15,6 +15,11 @@
15
15
  <a href="https://github.com/hypit-ai/hypit/blob/main/README.zh-CN.md"><strong>简体中文</strong></a>
16
16
  </p>
17
17
 
18
+ <p align="center">
19
+ <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily"></a>
20
+ <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 TypeScript Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily?language=TypeScript"></a>
21
+ </p>
22
+
18
23
  <p align="center">
19
24
  <img alt="Stars" src="https://img.shields.io/github/stars/hypit-ai/hypit?style=flat-square&color=FFD700&logo=github&logoColor=white&label=Stars">
20
25
  <a href="https://github.com/hypit-ai/hypit/blob/main/package.json"><img alt="Node 22.15+" src="https://img.shields.io/badge/Node.js-22.15%2B-5FA04E?style=flat-square&logo=nodedotjs&logoColor=white"></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -279,6 +279,7 @@
279
279
  "@hypit/typography-track-studio": "workspace:*",
280
280
  "@hypit/visual-ir": "workspace:*",
281
281
  "@hypit/volcengine-matting": "workspace:*",
282
+ "@hypit/wan": "workspace:*",
282
283
  "@hypit/whisperx": "workspace:*",
283
284
  "@types/node": "24.10.1",
284
285
  "rollup": "4.62.4",
@@ -17,6 +17,7 @@ export function temporalizeCaptionDocument(
17
17
  }
18
18
  const breaks = new Set(document.cueBreaks.map((cueBreak) => cueBreak.afterUnitId));
19
19
  const timed: Array<{ unit: CaptionDocument["units"][number]; timing: TimedCaptionUnit }> = [];
20
+ const previousByRole = new Map<string | undefined, { timing: TimedCaptionUnit }>();
20
21
  for (const unit of document.units) {
21
22
  const window = tokenFrameSpan(semantic, unit.sourceTokenIds);
22
23
  if (window === undefined) continue;
@@ -25,7 +26,16 @@ export function temporalizeCaptionDocument(
25
26
  }
26
27
  const startFrame = window.startFrame;
27
28
  const endFrameExclusive = Math.max(startFrame + 1, window.endFrameExclusive);
28
- timed.push({ unit, timing: { unitId: unit.id, startFrame, endFrameExclusive } });
29
+ // Acoustic Word windows may overlap. The Timeline keeps those measurements; an authored unit
30
+ // gets one owner: as soon as the next unit of the same Role starts, this one stops. A Cue
31
+ // envelope ends at its last unit, so Cue boundaries are exclusive by the same rule.
32
+ const previous = previousByRole.get(unit.role);
33
+ if (previous !== undefined && startFrame > previous.timing.startFrame && startFrame < previous.timing.endFrameExclusive) {
34
+ previous.timing = { ...previous.timing, endFrameExclusive: startFrame };
35
+ }
36
+ const entry = { unit, timing: { unitId: unit.id, startFrame, endFrameExclusive } };
37
+ previousByRole.set(unit.role, entry);
38
+ timed.push(entry);
29
39
  }
30
40
  const cues: TimedCaptionCue[] = [];
31
41
  let current: { units: TimedCaptionUnit[]; segmentId: string; turnId: string } | undefined;
@@ -38,6 +38,9 @@ switch ($Operation) {
38
38
  # Add replaces the same resource/account; do not destroy the old value before it succeeds.
39
39
  $bytes = [Convert]::FromBase64String([string]$request.secret)
40
40
  $secret = [System.Text.Encoding]::UTF8.GetString($bytes)
41
+ if ($secret.Length -gt 512) {
42
+ throw "PasswordVault cannot store a $($secret.Length)-character secret (limit 512). Select @hypit/credential-store-file for OAuth tokens."
43
+ }
41
44
  $credential = New-Object Windows.Security.Credentials.PasswordCredential(
42
45
  [string]$request.service,
43
46
  [string]$request.account,
@@ -9,6 +9,19 @@ type WindowsCredentialResult = {
9
9
 
10
10
  const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
11
11
 
12
+ /** PasswordVault password field; OAuth JSON routinely exceeds this. */
13
+ export const windowsLockerPasswordLimit = 512;
14
+
15
+ export function assertWindowsLockerSecret(secret: string, account: string): void {
16
+ if (secret.length > windowsLockerPasswordLimit) {
17
+ throw new Error(
18
+ `Windows Credential Locker cannot store the ${secret.length}-character secret for ${account}`
19
+ + ` (PasswordVault limit is ${windowsLockerPasswordLimit} characters).`
20
+ + " Select @hypit/credential-store-file for OAuth tokens.",
21
+ );
22
+ }
23
+ }
24
+
12
25
  /** The OS adapter owns this one child; Node owns its timeout, bounded output and termination. */
13
26
  export function windowsCredential(
14
27
  operation: "read" | "write" | "delete",
@@ -16,6 +29,7 @@ export function windowsCredential(
16
29
  account: string,
17
30
  secret?: string,
18
31
  ): Promise<WindowsCredentialResult> {
32
+ if (secret !== undefined) assertWindowsLockerSecret(secret, account);
19
33
  return new Promise((resolve, reject) => {
20
34
  const child = execFile("powershell.exe", [
21
35
  "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
@@ -1307,9 +1307,27 @@ export async function executeMuxProgramMedia(
1307
1307
  && compareTimestamp(finalVideo[0]!.startPts, finalAudio[0]!.startPts) === 0,
1308
1308
  "Final mux audio and video do not share one presentation origin");
1309
1309
  const finalAudioSpan = duration(finalAudio[0]!);
1310
- assert(roundPositive(finalAudioSpan.numerator * 48_000n, finalAudioSpan.denominator)
1311
- === need.audio.sampleFrames,
1312
- "Final mux audio presentation span differs from TimelineAudio");
1310
+ /*
1311
+ * The staged WAV is asserted sample-exact above; this checks what survived AAC.
1312
+ *
1313
+ * AAC cannot carry an arbitrary sample count: the encoder emits 1024-sample frames and
1314
+ * reports a priming delay that the MP4 muxer compensates with an edit list, so the
1315
+ * presented span lands a few samples away from the input. Measured here with ffmpeg 6.1
1316
+ * on synthetic tone (i.e. independent of any project's material): a 4 249 600-sample input
1317
+ * presents as 4 249 584 (-16), and 4 236 800 presents as 4 236 768 (-32). Padding the
1318
+ * timeline to a whole number of AAC frames does not remove it — the delay is not a
1319
+ * frame-alignment artifact.
1320
+ *
1321
+ * Demanding exact equality therefore rejects every correct mux whose length is not a
1322
+ * fixed point of that round trip. The real invariant AAC can hold is that no whole frame
1323
+ * of audio went missing, so the tolerance is one AAC frame rather than an arbitrary epsilon;
1324
+ * anything larger still means genuine desync and still fails.
1325
+ */
1326
+ const AAC_FRAME_SAMPLES = 1_024;
1327
+ const finalAudioSamples = roundPositive(finalAudioSpan.numerator * 48_000n, finalAudioSpan.denominator);
1328
+ assert(Math.abs(finalAudioSamples - need.audio.sampleFrames) < AAC_FRAME_SAMPLES,
1329
+ `Final mux audio presentation span differs from TimelineAudio by ${
1330
+ String(finalAudioSamples - need.audio.sampleFrames)} samples`);
1313
1331
  const artifact = await env.artifacts.putFile(output, "video/mp4");
1314
1332
  const value: MuxedMedia = sealMuxedMedia({
1315
1333
  frameRate: need.visual.frameRate,
@@ -54,6 +54,13 @@ Runtime Profile example:
54
54
 
55
55
  ```json
56
56
  {
57
+ "format": "hypit.runtime-local@1",
58
+ "dataRoot": ".hypit/runtimes/local",
59
+ "credentials": {
60
+ "platform": {
61
+ "use": "@hypit/credential-store-platform"
62
+ }
63
+ },
57
64
  "endpoints": {
58
65
  "hiapi.default": {
59
66
  "use": "@hypit/provider-hiapi",
@@ -64,7 +71,8 @@ Runtime Profile example:
64
71
  "pollIntervalMs": 10000
65
72
  }
66
73
  }
67
- }
74
+ },
75
+ "bindings": {}
68
76
  }
69
77
  ```
70
78
 
@@ -84,18 +84,26 @@ Runtime Profile example:
84
84
 
85
85
  ```json
86
86
  {
87
+ "format": "hypit.runtime-local@1",
88
+ "dataRoot": ".hypit/runtimes/local",
89
+ "credentials": {
90
+ "platform": {
91
+ "use": "@hypit/credential-store-platform"
92
+ }
93
+ },
87
94
  "endpoints": {
88
95
  "hypihub.default": {
89
96
  "use": "@hypit/provider-hypihub",
90
97
  "pool": "hypihub.default",
91
98
  "config": {
92
99
  "baseUrl": "https://hypit.ai",
93
- "apiKey": { "store": "os", "key": "hypihub.oauth" },
100
+ "apiKey": { "store": "platform", "key": "hypihub.oauth" },
94
101
  "defaultConcurrency": 3,
95
102
  "pollIntervalMs": 10000
96
103
  }
97
104
  }
98
- }
105
+ },
106
+ "bindings": {}
99
107
  }
100
108
  ```
101
109
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Hypit Runtime Provider for a [Monid](https://monid.ai) workspace. It runs Monid's generation
4
4
  endpoints through `POST /v1/run`, polls `GET /v1/runs/{runId}` until the run is terminal, downloads
5
- the returned video and stores it in the current Build.
5
+ every returned file and stores it in the current Build.
6
6
 
7
7
  | Capability | Monid endpoint |
8
8
  | --- | --- |
@@ -11,16 +11,17 @@ the returned video and stores it in the current Build.
11
11
  | `@hypit/seedance@1#seedance-2-mini` | `bytedance` `/v1/video/seedance-2.0-mini` |
12
12
  | `@hypit/seedance@1#seedance-2.5` | `bytedance` `/v1/video/seedance-2.5` |
13
13
  | `@hypit/minimax-h3@1#minimax-h3` | `minimax` `/v1/video/minimax-h3` |
14
+ | `@hypit/wan@1#wan-2.7-image` | `alibaba` `/v1/image/wan2.7-image` |
15
+ | `@hypit/wan@1#wan-2.7-image-pro` | `alibaba` `/v1/image/wan2.7-image-pro` |
14
16
 
15
17
  Monid's catalogue lists further generation endpoints, including the H3 Fast, Max and Max Turbo
16
- variants and Hailuo 2.3; their models are not the ones the Distribution describes. Input schemas
17
- are published through the authenticated `inspect` operation, which is where the request bodies
18
- above come from.
18
+ variants, Hailuo 2.3 and the Kling, Gemini and Qwen families; their models are not the ones the
19
+ Distribution describes. Input schemas are published through the authenticated `inspect` operation,
20
+ which is where the request bodies below come from.
19
21
 
20
- Every mapped endpoint relays a BytePlus ModelArk request: one `content` array holding the prompt
21
- and each media input as a typed item with its `role` (`first_frame`, `last_frame`,
22
- `reference_image`, `reference_video`, `reference_audio`), then `resolution`, `ratio` and
23
- `duration`.
22
+ The video endpoints relay a BytePlus ModelArk request: one `content` array holding the prompt and
23
+ each media input as a typed item with its `role` (`first_frame`, `last_frame`, `reference_image`,
24
+ `reference_video`, `reference_audio`), then `resolution`, `ratio` and `duration`.
24
25
 
25
26
  The Seedance endpoints add `generate_audio`. Monid documents no web search field for them, so
26
27
  `web-search="true"` is unsupported, and Seedance 2.5 frame mode (`first-frame` present) requires
@@ -37,6 +38,17 @@ text-to-video request carrying no `aspect-ratio` is reported unsupported before
37
38
  resolved, and the other two modes are sent as `adaptive`. H3 returns its video at `content.url`
38
39
  rather than the ModelArk `video_url`.
39
40
 
41
+ The Wan image endpoints take their fields directly: `prompt`, `images` as plain URLs, `size` for the
42
+ band, `n`, `enable_sequential`, `thinking_mode`, `watermark` and `seed`. Both variants accept the
43
+ same fields, and the Pro variant adds the `4K` band. Two service limits are reported before any
44
+ reference is resolved: `4K` renders from a prompt alone, without reference images or an image set,
45
+ and a request renders at most four pictures outside an image set. An image set returns several
46
+ files, so collection downloads each one.
47
+
48
+ The endpoints also document a custom colour palette and per-image bounding boxes. Neither has a
49
+ representation in the model port vocabulary, which carries scalars and media rather than object
50
+ arrays, so the Provider transmits neither.
51
+
40
52
  Reference media are uploaded through the workspace file system Monid provides for this purpose
41
53
  (`sfs`): `/put` signs an upload for `hypit/<resource>.<ext>`, the bytes are `PUT` to that URL, and
42
54
  `/cat` mints a one-day download URL that the generation endpoint fetches. Uploaded files stay in the
@@ -52,6 +64,13 @@ Runtime Profile example:
52
64
 
53
65
  ```json
54
66
  {
67
+ "format": "hypit.runtime-local@1",
68
+ "dataRoot": ".hypit/runtimes/local",
69
+ "credentials": {
70
+ "platform": {
71
+ "use": "@hypit/credential-store-platform"
72
+ }
73
+ },
55
74
  "endpoints": {
56
75
  "monid.default": {
57
76
  "use": "@hypit/provider-monid",
@@ -62,7 +81,8 @@ Runtime Profile example:
62
81
  "pollIntervalMs": 10000
63
82
  }
64
83
  }
65
- }
84
+ },
85
+ "bindings": {}
66
86
  }
67
87
  ```
68
88
 
@@ -12,7 +12,8 @@
12
12
  "@hypit/minimax-h3": "workspace:*",
13
13
  "@hypit/protocol": "workspace:*",
14
14
  "@hypit/runtime": "workspace:*",
15
- "@hypit/runtime-kit": "workspace:*"
15
+ "@hypit/runtime-kit": "workspace:*",
16
+ "@hypit/wan": "workspace:*"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@hypit/seedance": "workspace:*"
@@ -3,6 +3,7 @@ import type { GenerationWireMapping } from "@hypit/generation";
3
3
 
4
4
  const SEEDANCE: ModuleRef = { name: "@hypit/seedance", version: "1" };
5
5
  const MINIMAX_H3: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
6
+ const WAN: ModuleRef = { name: "@hypit/wan", version: "1" };
6
7
 
7
8
  /**
8
9
  * One mapping plus the Monid provider that relays the endpoint. Monid addresses an endpoint by
@@ -57,10 +58,33 @@ const minimaxH3: MonidMapping = {
57
58
  },
58
59
  };
59
60
 
61
+ /**
62
+ * Monid's `alibaba` Wan 2.7 image endpoints. Both variants take the same fields; the Pro variant
63
+ * adds a 4K band. Input images are plain public URLs rather than role-tagged items, and the model
64
+ * reads exclusions from the prompt, so it has no negative-prompt field.
65
+ */
66
+ const wan = (name: string, endpoint: string): MonidMapping => ({
67
+ service: "alibaba",
68
+ capability: { module: WAN, name }, result: "image",
69
+ routes: [{ model: endpoint }],
70
+ fields: {
71
+ prompt: { as: "value", field: "prompt" },
72
+ images: { as: "urlArray", field: "images" },
73
+ resolution: { as: "value", field: "size" },
74
+ count: { as: "value", field: "n" },
75
+ imageSet: { as: "value", field: "enable_sequential" },
76
+ extendedReasoning: { as: "value", field: "thinking_mode" },
77
+ watermark: { as: "value", field: "watermark" },
78
+ seed: { as: "value", field: "seed" },
79
+ },
80
+ });
81
+
60
82
  export const monidMappings: readonly MonidMapping[] = [
61
83
  seedance("seedance-2", "/v1/video/seedance-2.0"),
62
84
  seedance("seedance-2-fast", "/v1/video/seedance-2.0-fast"),
63
85
  seedance("seedance-2-mini", "/v1/video/seedance-2.0-mini"),
64
86
  seedance("seedance-2.5", "/v1/video/seedance-2.5"),
65
87
  minimaxH3,
88
+ wan("wan-2.7-image", "/v1/image/wan2.7-image"),
89
+ wan("wan-2.7-image-pro", "/v1/image/wan2.7-image-pro"),
66
90
  ];
@@ -31,7 +31,7 @@ type Handle = {
31
31
  readonly runId: string;
32
32
  readonly route: string;
33
33
  readonly startedAt: number;
34
- readonly url?: string;
34
+ readonly urls?: readonly string[];
35
35
  };
36
36
 
37
37
  function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
@@ -66,13 +66,21 @@ function httpsUrl(value: unknown, subject: string): string {
66
66
  return value;
67
67
  }
68
68
  /**
69
- * The relayed result. The ModelArk endpoints return the task's `video_url`; MiniMax-H3 returns the
70
- * video at `content.url`. The link expires, so collection downloads it rather than storing it.
69
+ * The relayed result. The ModelArk endpoints return the task's `video_url` and MiniMax-H3 its
70
+ * `content.url`; an image endpoint returns one entry per rendered picture. Every link expires, so
71
+ * collection downloads them rather than storing the addresses.
71
72
  */
72
- function outputVideoUrl(run: Record<string, unknown>): string {
73
+ function outputUrls(run: Record<string, unknown>): readonly string[] {
73
74
  const output = object(run.output, "Monid run output");
74
75
  const content = output.content === undefined ? undefined : object(output.content, "Monid run output content");
75
- return httpsUrl(output.video_url ?? content?.video_url ?? content?.url, "Monid run output");
76
+ const listed = [output.results, output.images, output.urls, content?.results, content?.images]
77
+ .find((value): value is readonly unknown[] => Array.isArray(value) && value.length > 0);
78
+ if (listed !== undefined) {
79
+ return listed.map((item, index) => httpsUrl(
80
+ typeof item === "string" ? item : object(item, `Monid run output ${index}`).url,
81
+ `Monid run output ${index}`));
82
+ }
83
+ return [httpsUrl(output.video_url ?? output.url ?? content?.video_url ?? content?.url, "Monid run output")];
76
84
  }
77
85
  const extensions: Readonly<Record<string, string>> = {
78
86
  "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "video/mp4": "mp4", "video/quicktime": "mov",
@@ -180,7 +188,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
180
188
  if (!ended) return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(run.status) }), receipt };
181
189
  const rejected = monidRunFailure(run, handle.runId);
182
190
  if (rejected !== undefined) return { ...failure(rejected), receipt };
183
- return { status: "ready", handle: canonicalize({ ...handle, url: outputVideoUrl(run) }), receipt };
191
+ return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
184
192
  } catch (error) {
185
193
  return failure(error);
186
194
  }
@@ -201,7 +209,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
201
209
  }
202
210
  const rejected = monidRunFailure(run, handle.runId);
203
211
  if (rejected !== undefined) return { ...failure(rejected), receipt };
204
- return { status: "ready", handle: canonicalize({ ...handle, url: outputVideoUrl(run) }), receipt };
212
+ return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
205
213
  } catch (error) {
206
214
  return failure(error);
207
215
  }
@@ -211,10 +219,15 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
211
219
  const handle = object(context.handle, "Monid handle") as unknown as Handle;
212
220
  const route = monidRouteForCapability(context.need.capability);
213
221
  assert(route !== undefined && handle.route === route.key, "Monid collection route differs");
214
- await context.reportProgress?.({ phase: "Receiving generated video" });
215
- const downloaded = await client.download(httpsUrl(handle.url, "Monid handle"));
216
- const blob = await context.resources.put(downloaded.bytes, downloaded.mediaType);
217
- return { status: "completed", result: { value: route.packageResult([blob]) }, receipt: { id: handle.runId } };
222
+ const urls = handle.urls ?? [];
223
+ assert(urls.length > 0, "Monid handle carries no result");
224
+ await context.reportProgress?.({ phase: `Receiving ${urls.length} generated ${route.media}${urls.length === 1 ? "" : "s"}` });
225
+ const blobs: BlobRef[] = [];
226
+ for (const url of urls) {
227
+ const downloaded = await client.download(httpsUrl(url, "Monid handle"));
228
+ blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
229
+ }
230
+ return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.runId } };
218
231
  } catch (error) {
219
232
  return failure(error);
220
233
  }
@@ -2,6 +2,7 @@ import {
2
2
  compileWireRequest,
3
3
  selectWireModelForRequest,
4
4
  generationTypes,
5
+ sealGeneratedImageSet,
5
6
  sealGeneratedVideoSet,
6
7
  } from "@hypit/generation";
7
8
  import type { GenerationArtifactUrlResolver, GenerationRequest } from "@hypit/generation";
@@ -22,6 +23,8 @@ export type MonidPreparedRequest = {
22
23
  export type MonidRoute = MonidMapping & {
23
24
  readonly key: string;
24
25
  readonly returns: TypeRef;
26
+ /** What the run produces, which decides how many results collection expects. */
27
+ readonly media: "image" | "video";
25
28
  readonly supports: (request: EndpointRequest) => EndpointSupport;
26
29
  readonly prepare: (constraints: CanonicalValue) => MonidPreparedRequest;
27
30
  readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
@@ -42,7 +45,20 @@ function referenceToVideo(request: GenerationRequest): boolean {
42
45
  return present(request, "referenceImage") || present(request, "referenceVideo") || present(request, "referenceAudio");
43
46
  }
44
47
 
48
+ function wanRejection(mapping: MonidMapping, request: GenerationRequest): string | undefined {
49
+ const set = scalar(request, "imageSet") === true;
50
+ if (scalar(request, "resolution") === "4K" && (present(request, "images") || set)) {
51
+ return "Wan renders 4K from a prompt alone, without reference images or an image set";
52
+ }
53
+ const count = scalar(request, "count");
54
+ if (typeof count === "number" && count > 4 && !set) {
55
+ return `Wan renders at most 4 pictures outside an image set, not ${count}`;
56
+ }
57
+ return undefined;
58
+ }
59
+
45
60
  function rejection(mapping: MonidMapping, request: GenerationRequest): string | undefined {
61
+ if (mapping.capability.module.name === "@hypit/wan") return wanRejection(mapping, request);
46
62
  if (mapping.capability.name === "minimax-h3") {
47
63
  // MiniMax-H3 resolves the framing from the uploaded image in frame mode and defaults
48
64
  // reference-to-video to adaptive. Text-to-video carries no such source, and the endpoint
@@ -103,7 +119,8 @@ function capabilityKey(capability: CapabilityRef): string {
103
119
  export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) => ({
104
120
  ...mapping,
105
121
  key: capabilityKey(mapping.capability),
106
- returns: generationTypes.videoSet,
122
+ returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
123
+ media: mapping.result === "image" ? "image" as const : "video" as const,
107
124
  supports: (request) => {
108
125
  const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
109
126
  return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
@@ -112,7 +129,10 @@ export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) =>
112
129
  const request = constraints as unknown as GenerationRequest;
113
130
  const reason = rejection(mapping, request);
114
131
  if (reason !== undefined) throw new Error(reason);
115
- const body = mapping.capability.name === "minimax-h3" ? minimaxH3Input : arkInput;
132
+ // The Wan endpoints take the compiled fields as their body; the ModelArk endpoints fold their
133
+ // media into one role-tagged `content` array first.
134
+ const body = mapping.capability.module.name === "@hypit/wan" ? (input: Record<string, unknown>) => input
135
+ : mapping.capability.name === "minimax-h3" ? minimaxH3Input : arkInput;
116
136
  return {
117
137
  service: mapping.service,
118
138
  endpoint: selectWireModelForRequest(mapping, request),
@@ -121,7 +141,9 @@ export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) =>
121
141
  },
122
142
  packageResult: (artifacts) => ({
123
143
  kind: "inline",
124
- value: canonicalize(sealGeneratedVideoSet({ videos: artifacts })),
144
+ value: canonicalize(mapping.result === "image"
145
+ ? sealGeneratedImageSet({ images: artifacts })
146
+ : sealGeneratedVideoSet({ videos: artifacts })),
125
147
  }),
126
148
  }));
127
149
 
@@ -37,6 +37,13 @@ Runtime Profile example:
37
37
 
38
38
  ```json
39
39
  {
40
+ "format": "hypit.runtime-local@1",
41
+ "dataRoot": ".hypit/runtimes/local",
42
+ "credentials": {
43
+ "platform": {
44
+ "use": "@hypit/credential-store-platform"
45
+ }
46
+ },
40
47
  "endpoints": {
41
48
  "pollo.default": {
42
49
  "use": "@hypit/provider-pollo",
@@ -47,7 +54,8 @@ Runtime Profile example:
47
54
  "pollIntervalMs": 10000
48
55
  }
49
56
  }
50
- }
57
+ },
58
+ "bindings": {}
51
59
  }
52
60
  ```
53
61
 
@@ -46,6 +46,13 @@ Runtime Profile example:
46
46
 
47
47
  ```json
48
48
  {
49
+ "format": "hypit.runtime-local@1",
50
+ "dataRoot": ".hypit/runtimes/local",
51
+ "credentials": {
52
+ "platform": {
53
+ "use": "@hypit/credential-store-platform"
54
+ }
55
+ },
49
56
  "endpoints": {
50
57
  "tokendance.default": {
51
58
  "use": "@hypit/provider-tokendance",
@@ -56,7 +63,8 @@ Runtime Profile example:
56
63
  "pollIntervalMs": 10000
57
64
  }
58
65
  }
59
- }
66
+ },
67
+ "bindings": {}
60
68
  }
61
69
  ```
62
70
 
@@ -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!);