@hypit/hypit 0.2.4 → 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 +5 -0
- package/package.json +2 -1
- package/packages/caption/src/temporalize.ts +11 -1
- package/packages/credential-store-os/runtime/windows-credential.ps1 +3 -0
- package/packages/credential-store-os/src/windows.ts +14 -0
- package/packages/media-execution/src/execute.ts +21 -3
- package/packages/provider-hiapi/README.md +9 -1
- package/packages/provider-hypihub/README.md +10 -2
- package/packages/provider-monid/README.md +48 -16
- package/packages/provider-monid/package.json +3 -1
- package/packages/provider-monid/src/mapping.ts +59 -2
- package/packages/provider-monid/src/provider.ts +31 -15
- package/packages/provider-monid/src/routes.ts +58 -7
- package/packages/provider-pollo/README.md +9 -1
- package/packages/provider-tokendance/README.md +9 -1
- package/packages/wan/README.md +31 -0
- package/packages/wan/package.json +21 -0
- package/packages/wan/src/activation.ts +11 -0
- package/packages/wan/src/index.ts +160 -0
- package/packages/wan/src/surface.ts +147 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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": "
|
|
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
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# `@hypit/provider-monid`
|
|
2
2
|
|
|
3
|
-
Hypit Runtime Provider for a [Monid](https://monid.ai) workspace. It runs Monid's
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Hypit Runtime Provider for a [Monid](https://monid.ai) workspace. It runs Monid's generation
|
|
4
|
+
endpoints through `POST /v1/run`, polls `GET /v1/runs/{runId}` until the run is terminal, downloads
|
|
5
|
+
every returned file and stores it in the current Build.
|
|
6
6
|
|
|
7
7
|
| Capability | Monid endpoint |
|
|
8
8
|
| --- | --- |
|
|
@@ -10,20 +10,44 @@ downloads the returned `video_url` and stores the video in the current Build.
|
|
|
10
10
|
| `@hypit/seedance@1#seedance-2-fast` | `bytedance` `/v1/video/seedance-2.0-fast` |
|
|
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
|
+
| `@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` |
|
|
13
16
|
|
|
14
|
-
Monid's catalogue
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
Monid's catalogue lists further generation endpoints, including the H3 Fast, Max and Max Turbo
|
|
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.
|
|
17
21
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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`.
|
|
25
|
+
|
|
26
|
+
The Seedance endpoints add `generate_audio`. Monid documents no web search field for them, so
|
|
27
|
+
`web-search="true"` is unsupported, and Seedance 2.5 frame mode (`first-frame` present) requires
|
|
28
|
+
`aspect-ratio="adaptive"`. Seedance visual references may carry `person-reference`; the Provider
|
|
29
|
+
accepts the declaration and transmits nothing for it, since the endpoint has no field for it.
|
|
30
|
+
Seedance rejects reference images and videos that contain a real human face; Monid offers no way to
|
|
31
|
+
register authorized portrait material, so such a request fails with the upstream moderation error.
|
|
32
|
+
|
|
33
|
+
MiniMax H3 names its model in the body and takes neither of those two fields. The endpoint requires
|
|
34
|
+
a resolution, so a request that states none is sent at `2K`, the resolution the HypiHub Provider
|
|
35
|
+
also selects. It also requires a ratio that is not `adaptive` for text-to-video, while frame mode
|
|
36
|
+
resolves the framing from the uploaded image and reference-to-video defaults to adaptive: a
|
|
37
|
+
text-to-video request carrying no `aspect-ratio` is reported unsupported before any reference is
|
|
38
|
+
resolved, and the other two modes are sent as `adaptive`. H3 returns its video at `content.url`
|
|
39
|
+
rather than the ModelArk `video_url`.
|
|
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.
|
|
27
51
|
|
|
28
52
|
Reference media are uploaded through the workspace file system Monid provides for this purpose
|
|
29
53
|
(`sfs`): `/put` signs an upload for `hypit/<resource>.<ext>`, the bytes are `PUT` to that URL, and
|
|
@@ -40,6 +64,13 @@ Runtime Profile example:
|
|
|
40
64
|
|
|
41
65
|
```json
|
|
42
66
|
{
|
|
67
|
+
"format": "hypit.runtime-local@1",
|
|
68
|
+
"dataRoot": ".hypit/runtimes/local",
|
|
69
|
+
"credentials": {
|
|
70
|
+
"platform": {
|
|
71
|
+
"use": "@hypit/credential-store-platform"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
43
74
|
"endpoints": {
|
|
44
75
|
"monid.default": {
|
|
45
76
|
"use": "@hypit/provider-monid",
|
|
@@ -50,7 +81,8 @@ Runtime Profile example:
|
|
|
50
81
|
"pollIntervalMs": 10000
|
|
51
82
|
}
|
|
52
83
|
}
|
|
53
|
-
}
|
|
84
|
+
},
|
|
85
|
+
"bindings": {}
|
|
54
86
|
}
|
|
55
87
|
```
|
|
56
88
|
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@hypit/endpoint-kit": "workspace:*",
|
|
11
11
|
"@hypit/generation": "workspace:*",
|
|
12
|
+
"@hypit/minimax-h3": "workspace:*",
|
|
12
13
|
"@hypit/protocol": "workspace:*",
|
|
13
14
|
"@hypit/runtime": "workspace:*",
|
|
14
|
-
"@hypit/runtime-kit": "workspace:*"
|
|
15
|
+
"@hypit/runtime-kit": "workspace:*",
|
|
16
|
+
"@hypit/wan": "workspace:*"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
19
|
"@hypit/seedance": "workspace:*"
|
|
@@ -2,13 +2,22 @@ import type { ModuleRef } from "@hypit/protocol";
|
|
|
2
2
|
import type { GenerationWireMapping } from "@hypit/generation";
|
|
3
3
|
|
|
4
4
|
const SEEDANCE: ModuleRef = { name: "@hypit/seedance", version: "1" };
|
|
5
|
+
const MINIMAX_H3: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
|
|
6
|
+
const WAN: ModuleRef = { name: "@hypit/wan", version: "1" };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One mapping plus the Monid provider that relays the endpoint. Monid addresses an endpoint by
|
|
10
|
+
* provider and path, so the provider travels with the mapping rather than being assumed.
|
|
11
|
+
*/
|
|
12
|
+
export type MonidMapping = GenerationWireMapping & { readonly service: string };
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* Monid `bytedance` endpoints and the fields their ModelArk request body takes. Media fields name
|
|
8
16
|
* the `role` of a `content` item; routes.ts folds them into that array. `personReference` is
|
|
9
17
|
* accepted on visual references and not transmitted: the endpoint has no field for it.
|
|
10
18
|
*/
|
|
11
|
-
const seedance = (name: string, endpoint: string):
|
|
19
|
+
const seedance = (name: string, endpoint: string): MonidMapping => ({
|
|
20
|
+
service: "bytedance",
|
|
12
21
|
capability: { module: SEEDANCE, name }, result: "video", routes: [{ model: endpoint }],
|
|
13
22
|
fields: {
|
|
14
23
|
prompt: { as: "value", field: "text" },
|
|
@@ -25,9 +34,57 @@ const seedance = (name: string, endpoint: string): GenerationWireMapping => ({
|
|
|
25
34
|
},
|
|
26
35
|
});
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Monid's `minimax` MiniMax-H3 endpoint. It takes the same role-tagged `content` array as the
|
|
39
|
+
* ModelArk endpoints above, names the model in the body, and carries neither a generated-audio nor
|
|
40
|
+
* a web-search field. `resolution` is required by the endpoint while the model's port is optional,
|
|
41
|
+
* so an unstated resolution is sent as the 2K the HypiHub Provider also selects.
|
|
42
|
+
*/
|
|
43
|
+
const minimaxH3: MonidMapping = {
|
|
44
|
+
service: "minimax",
|
|
45
|
+
capability: { module: MINIMAX_H3, name: "minimax-h3" }, result: "video",
|
|
46
|
+
routes: [{ model: "/v1/video/minimax-h3" }],
|
|
47
|
+
constants: { model: "MiniMax-H3" },
|
|
48
|
+
fields: {
|
|
49
|
+
prompt: { as: "value", field: "text" },
|
|
50
|
+
referenceImage: { as: "urlArray", field: "reference_image" },
|
|
51
|
+
referenceVideo: { as: "urlArray", field: "reference_video" },
|
|
52
|
+
referenceAudio: { as: "urlArray", field: "reference_audio" },
|
|
53
|
+
firstFrame: { as: "url", field: "first_frame" },
|
|
54
|
+
lastFrame: { as: "url", field: "last_frame" },
|
|
55
|
+
resolution: { as: "value", field: "resolution", whenAbsent: "2K" },
|
|
56
|
+
aspectRatio: { as: "value", field: "ratio" },
|
|
57
|
+
duration: { as: "value", field: "duration" },
|
|
58
|
+
},
|
|
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
|
+
|
|
82
|
+
export const monidMappings: readonly MonidMapping[] = [
|
|
29
83
|
seedance("seedance-2", "/v1/video/seedance-2.0"),
|
|
30
84
|
seedance("seedance-2-fast", "/v1/video/seedance-2.0-fast"),
|
|
31
85
|
seedance("seedance-2-mini", "/v1/video/seedance-2.0-mini"),
|
|
32
86
|
seedance("seedance-2.5", "/v1/video/seedance-2.5"),
|
|
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"),
|
|
33
90
|
];
|
|
@@ -31,7 +31,7 @@ type Handle = {
|
|
|
31
31
|
readonly runId: string;
|
|
32
32
|
readonly route: string;
|
|
33
33
|
readonly startedAt: number;
|
|
34
|
-
readonly
|
|
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); }
|
|
@@ -65,11 +65,22 @@ function httpsUrl(value: unknown, subject: string): string {
|
|
|
65
65
|
assert(typeof value === "string" && /^https?:\/\//u.test(value), `${subject} has no URL`);
|
|
66
66
|
return value;
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
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.
|
|
72
|
+
*/
|
|
73
|
+
function outputUrls(run: Record<string, unknown>): readonly string[] {
|
|
70
74
|
const output = object(run.output, "Monid run output");
|
|
71
|
-
const
|
|
72
|
-
|
|
75
|
+
const content = output.content === undefined ? undefined : object(output.content, "Monid run output content");
|
|
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")];
|
|
73
84
|
}
|
|
74
85
|
const extensions: Readonly<Record<string, string>> = {
|
|
75
86
|
"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "video/mp4": "mp4", "video/quicktime": "mov",
|
|
@@ -160,7 +171,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
|
|
|
160
171
|
const route = monidRouteForCapability(context.need.capability);
|
|
161
172
|
assert(route !== undefined, "Monid does not implement this exact capability");
|
|
162
173
|
const request = route.prepare(context.need.constraints);
|
|
163
|
-
await context.reportProgress?.({ phase: `Preparing Monid request:
|
|
174
|
+
await context.reportProgress?.({ phase: `Preparing Monid request: ${request.service} ${request.endpoint}` });
|
|
164
175
|
let input: Record<string, unknown>;
|
|
165
176
|
try {
|
|
166
177
|
input = await request.compile(resolverFor(client, context, publicAssetUrl));
|
|
@@ -168,8 +179,8 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
|
|
|
168
179
|
throw new MonidServiceError(error instanceof MonidServiceError ? error.code : "MONID_ERROR",
|
|
169
180
|
`Monid request preparation failed; endpoint=${request.endpoint}; generation not submitted: ${failureMessage(error)}`);
|
|
170
181
|
}
|
|
171
|
-
await context.reportProgress?.({ phase: `Submitting Monid request:
|
|
172
|
-
const { body: run } = await client.run({ provider:
|
|
182
|
+
await context.reportProgress?.({ phase: `Submitting Monid request: ${request.service} ${request.endpoint}` });
|
|
183
|
+
const { body: run } = await client.run({ provider: request.service, endpoint: request.endpoint, input }, apiKey(context.credentials));
|
|
173
184
|
const handle: Handle = { contract: "hypit.monid-operation@1", runId: runId(run), route: route.key, startedAt: Date.now() };
|
|
174
185
|
const receipt = { id: handle.runId };
|
|
175
186
|
const ended = monidTerminalStatuses.includes(String(run.status) as typeof monidTerminalStatuses[number]);
|
|
@@ -177,7 +188,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
|
|
|
177
188
|
if (!ended) return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(run.status) }), receipt };
|
|
178
189
|
const rejected = monidRunFailure(run, handle.runId);
|
|
179
190
|
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
180
|
-
return { status: "ready", handle: canonicalize({ ...handle,
|
|
191
|
+
return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
|
|
181
192
|
} catch (error) {
|
|
182
193
|
return failure(error);
|
|
183
194
|
}
|
|
@@ -198,7 +209,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
|
|
|
198
209
|
}
|
|
199
210
|
const rejected = monidRunFailure(run, handle.runId);
|
|
200
211
|
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
201
|
-
return { status: "ready", handle: canonicalize({ ...handle,
|
|
212
|
+
return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
|
|
202
213
|
} catch (error) {
|
|
203
214
|
return failure(error);
|
|
204
215
|
}
|
|
@@ -208,10 +219,15 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
|
|
|
208
219
|
const handle = object(context.handle, "Monid handle") as unknown as Handle;
|
|
209
220
|
const route = monidRouteForCapability(context.need.capability);
|
|
210
221
|
assert(route !== undefined && handle.route === route.key, "Monid collection route differs");
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 } };
|
|
215
231
|
} catch (error) {
|
|
216
232
|
return failure(error);
|
|
217
233
|
}
|
|
@@ -230,7 +246,7 @@ export function createMonidProvider(options: CreateMonidProviderOptions = {}) {
|
|
|
230
246
|
const asyncEndpoint = endpoint(client, pollIntervalMs, operationTimeoutMs, options.publicAssetUrl);
|
|
231
247
|
return defineEndpointPackage({
|
|
232
248
|
module: monidProviderModuleRef, facet: "gateway", instance: options.instance ?? "monid.default", pool: options.pool ?? options.instance ?? "monid.default",
|
|
233
|
-
pricing: { kind: "page", url: "https://monid.ai/tools
|
|
249
|
+
pricing: { kind: "page", url: "https://monid.ai/tools" },
|
|
234
250
|
credentials: { apiKey: options.apiKey ?? credentialRef("os", "monid.api-key") },
|
|
235
251
|
credentialInputs: { apiKey: { label: "Monid API key" } },
|
|
236
252
|
defaultConcurrency: options.defaultConcurrency ?? 4,
|
|
@@ -2,23 +2,29 @@ import {
|
|
|
2
2
|
compileWireRequest,
|
|
3
3
|
selectWireModelForRequest,
|
|
4
4
|
generationTypes,
|
|
5
|
+
sealGeneratedImageSet,
|
|
5
6
|
sealGeneratedVideoSet,
|
|
6
7
|
} from "@hypit/generation";
|
|
7
|
-
import type { GenerationArtifactUrlResolver, GenerationRequest
|
|
8
|
+
import type { GenerationArtifactUrlResolver, GenerationRequest } from "@hypit/generation";
|
|
8
9
|
import { canonicalize } from "@hypit/protocol";
|
|
9
10
|
import type { BlobRef, CapabilityRef, CanonicalValue, StoredValue, TypeRef } from "@hypit/protocol";
|
|
10
11
|
import type { EndpointRequest, EndpointSupport } from "@hypit/endpoint-kit";
|
|
11
12
|
import { monidMappings } from "./mapping.js";
|
|
13
|
+
import type { MonidMapping } from "./mapping.js";
|
|
12
14
|
|
|
13
15
|
export type MonidPreparedRequest = {
|
|
14
|
-
/** Monid
|
|
16
|
+
/** Monid provider relaying the endpoint. */
|
|
17
|
+
readonly service: string;
|
|
18
|
+
/** Monid endpoint path under that provider. */
|
|
15
19
|
readonly endpoint: string;
|
|
16
20
|
readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
|
-
export type MonidRoute =
|
|
23
|
+
export type MonidRoute = MonidMapping & {
|
|
20
24
|
readonly key: string;
|
|
21
25
|
readonly returns: TypeRef;
|
|
26
|
+
/** What the run produces, which decides how many results collection expects. */
|
|
27
|
+
readonly media: "image" | "video";
|
|
22
28
|
readonly supports: (request: EndpointRequest) => EndpointSupport;
|
|
23
29
|
readonly prepare: (constraints: CanonicalValue) => MonidPreparedRequest;
|
|
24
30
|
readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
|
|
@@ -35,7 +41,34 @@ function strings(value: unknown): readonly string[] {
|
|
|
35
41
|
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
function
|
|
44
|
+
function referenceToVideo(request: GenerationRequest): boolean {
|
|
45
|
+
return present(request, "referenceImage") || present(request, "referenceVideo") || present(request, "referenceAudio");
|
|
46
|
+
}
|
|
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
|
+
|
|
60
|
+
function rejection(mapping: MonidMapping, request: GenerationRequest): string | undefined {
|
|
61
|
+
if (mapping.capability.module.name === "@hypit/wan") return wanRejection(mapping, request);
|
|
62
|
+
if (mapping.capability.name === "minimax-h3") {
|
|
63
|
+
// MiniMax-H3 resolves the framing from the uploaded image in frame mode and defaults
|
|
64
|
+
// reference-to-video to adaptive. Text-to-video carries no such source, and the endpoint
|
|
65
|
+
// takes no adaptive ratio there, so the aspect ratio has to be stated.
|
|
66
|
+
if (!present(request, "firstFrame") && !present(request, "lastFrame") && !referenceToVideo(request)
|
|
67
|
+
&& scalar(request, "aspectRatio") === undefined) {
|
|
68
|
+
return "MiniMax H3 text-to-video on Monid requires an aspect ratio";
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
39
72
|
if (scalar(request, "webSearch") === true) {
|
|
40
73
|
return "Monid's Seedance endpoints document no web search field";
|
|
41
74
|
}
|
|
@@ -54,6 +87,7 @@ function arkInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
|
54
87
|
const first = input.first_frame;
|
|
55
88
|
const last = input.last_frame;
|
|
56
89
|
return {
|
|
90
|
+
...(typeof input.model === "string" ? { model: input.model } : {}),
|
|
57
91
|
content: [
|
|
58
92
|
{ type: "text", text: input.text },
|
|
59
93
|
...(typeof first === "string" ? [contentItem("image_url", first, "first_frame")] : []),
|
|
@@ -69,6 +103,15 @@ function arkInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
|
69
103
|
};
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
/**
|
|
107
|
+
* MiniMax-H3 takes a required ratio: frame mode and reference-to-video both resolve to adaptive,
|
|
108
|
+
* and rejection() has already required a stated ratio for text-to-video.
|
|
109
|
+
*/
|
|
110
|
+
function minimaxH3Input(input: Record<string, unknown>): Record<string, unknown> {
|
|
111
|
+
const body = arkInput(input);
|
|
112
|
+
return { ...body, ratio: body.ratio ?? "adaptive" };
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
function capabilityKey(capability: CapabilityRef): string {
|
|
73
116
|
return `${capability.module.name}@${capability.module.version}#${capability.name}`;
|
|
74
117
|
}
|
|
@@ -76,7 +119,8 @@ function capabilityKey(capability: CapabilityRef): string {
|
|
|
76
119
|
export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) => ({
|
|
77
120
|
...mapping,
|
|
78
121
|
key: capabilityKey(mapping.capability),
|
|
79
|
-
returns: generationTypes.videoSet,
|
|
122
|
+
returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
|
|
123
|
+
media: mapping.result === "image" ? "image" as const : "video" as const,
|
|
80
124
|
supports: (request) => {
|
|
81
125
|
const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
|
|
82
126
|
return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
|
|
@@ -85,14 +129,21 @@ export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) =>
|
|
|
85
129
|
const request = constraints as unknown as GenerationRequest;
|
|
86
130
|
const reason = rejection(mapping, request);
|
|
87
131
|
if (reason !== undefined) throw new Error(reason);
|
|
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;
|
|
88
136
|
return {
|
|
137
|
+
service: mapping.service,
|
|
89
138
|
endpoint: selectWireModelForRequest(mapping, request),
|
|
90
|
-
compile: async (resolve) =>
|
|
139
|
+
compile: async (resolve) => body((await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
|
|
91
140
|
};
|
|
92
141
|
},
|
|
93
142
|
packageResult: (artifacts) => ({
|
|
94
143
|
kind: "inline",
|
|
95
|
-
value: canonicalize(
|
|
144
|
+
value: canonicalize(mapping.result === "image"
|
|
145
|
+
? sealGeneratedImageSet({ images: artifacts })
|
|
146
|
+
: sealGeneratedVideoSet({ videos: artifacts })),
|
|
96
147
|
}),
|
|
97
148
|
}));
|
|
98
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!);
|