@hypit/hypit 0.2.8 → 0.2.9
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/package.json +1 -1
- package/packages/media-execution/src/execute.ts +1 -1
- package/packages/pixverse/README.md +22 -13
- package/packages/pixverse/src/activation.ts +3 -1
- package/packages/pixverse/src/index.ts +181 -67
- package/packages/pixverse/src/surface.ts +181 -72
- package/packages/pixverse/src/validation.ts +13 -0
- package/packages/provider-hypihub/README.md +9 -1
- package/packages/provider-hypihub/src/mapping.ts +23 -0
- package/packages/video-cli/src/snapshot.ts +18 -4
package/package.json
CHANGED
|
@@ -802,6 +802,7 @@ export async function executeRenderStillVideo(
|
|
|
802
802
|
"select=eq(n\\,0)",
|
|
803
803
|
"loop=loop=-1:size=1:start=0",
|
|
804
804
|
`trim=start_frame=0:end_frame=${frames}`,
|
|
805
|
+
`settb=expr=${denominator}/${numerator}`,
|
|
805
806
|
`setpts=N*${denominator}/(${numerator}*TB)`,
|
|
806
807
|
].join(",");
|
|
807
808
|
let argv: string[];
|
|
@@ -1278,7 +1279,6 @@ export async function executeMuxProgramMedia(
|
|
|
1278
1279
|
"-y", "-i", visualPath, "-i", audioPath,
|
|
1279
1280
|
"-map", `0:${visual.index}`, "-map", "1:0",
|
|
1280
1281
|
"-c:v", "copy", "-c:a", "aac", "-ar", "48000", "-ac", "2",
|
|
1281
|
-
"-frames:v", String(need.visual.frameCount),
|
|
1282
1282
|
"-movflags", "+faststart", output,
|
|
1283
1283
|
],
|
|
1284
1284
|
timeoutMs: env.processTimeoutMs,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# `@hypit/pixverse`
|
|
2
2
|
|
|
3
|
-
Exact author/compute contracts and
|
|
3
|
+
Exact author/compute contracts and package-owned author Surfaces for PixVerse. The package carries
|
|
4
|
+
two exact models, `pixverse-v6` and `pixverse-c1`, and selects no Provider, API key or network
|
|
5
|
+
execution. The selected Provider implements the exact capability.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Connect prompt and frames as ordinary graph edges:
|
|
7
|
+
Both models render 1 to 15 seconds at `360p`, `540p`, `720p` or `1080p` from a prompt of up to
|
|
8
|
+
5,000 characters. `<pix:Video>` generates from the prompt, from a first frame, or from a first and
|
|
9
|
+
last frame; `<pix:ReferenceVideo>` generates from the image and video subjects its `Reference`
|
|
10
|
+
children carry.
|
|
10
11
|
|
|
11
12
|
```xml
|
|
12
13
|
<pix:Video
|
|
13
14
|
id="opening"
|
|
15
|
+
model="v6"
|
|
14
16
|
prompt={line}
|
|
15
17
|
duration="5"
|
|
16
18
|
quality="720p"
|
|
@@ -18,17 +20,24 @@ Connect prompt and frames as ordinary graph edges:
|
|
|
18
20
|
generate-audio="true"
|
|
19
21
|
/>
|
|
20
22
|
|
|
21
|
-
<pix:Video id="bridge" prompt={motion} duration="5" quality="720p"
|
|
23
|
+
<pix:Video id="bridge" model="c1" prompt={motion} duration="5" quality="720p"
|
|
22
24
|
first-frame={hero.image} last-frame={product.image}/>
|
|
25
|
+
|
|
26
|
+
<pix:ReferenceVideo id="fusion" model="v6" prompt={outfit} duration="5" quality="720p" aspect-ratio="16:9">
|
|
27
|
+
<pix:Reference image={character.image}/>
|
|
28
|
+
<pix:Reference image={clothes.image}/>
|
|
29
|
+
</pix:ReferenceVideo>
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
The
|
|
26
|
-
|
|
32
|
+
The prompt addresses the references in the order they appear, as `@ref_1`, `@ref_2` and so on. V6
|
|
33
|
+
takes up to ten image references and C1 up to seven. V6 also takes up to two video references
|
|
34
|
+
totalling 15 seconds; those carry the length of the run, so that element states no `duration`, and
|
|
35
|
+
`aspect-ratio="auto"` takes their shape.
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`multi-clip
|
|
37
|
+
A prompt-only run states its `aspect-ratio`; a run that starts from a `first-frame` takes that
|
|
38
|
+
frame's shape instead. A `last-frame` bridges from the first frame into one continuous shot, as do
|
|
39
|
+
subject references, so neither is combined with V6's `multi-clip`, which renders the prompt as
|
|
40
|
+
several cuts. `seed` and `multi-clip` are V6's own switches.
|
|
32
41
|
|
|
33
42
|
`generate-audio` renders an audio track alongside the picture, including speech the prompt asks a
|
|
34
43
|
character to say. The model exposes no separate voice, language or dialogue field, so a spoken line
|
|
@@ -2,10 +2,12 @@ import { createMarkupSurfaceHostFacet } from "@hypit/markup";
|
|
|
2
2
|
import {
|
|
3
3
|
pixverseComponent, pixverseDefinition, pixverseManifest, pixverseModuleRef, pixverseMarkupSurfaces,
|
|
4
4
|
} from "./index.js";
|
|
5
|
-
import { decodePixverseVideoSurface } from "./surface.js";
|
|
5
|
+
import { decodePixverseReferenceVideoSurface, decodePixverseVideoSurface } from "./surface.js";
|
|
6
6
|
export const hypitPackage = { format: "hypit.node-package@1" as const, modules: [{ manifest: pixverseManifest }], components: [pixverseComponent], hostFacets: [
|
|
7
7
|
pixverseDefinition.hostFacet,
|
|
8
8
|
createMarkupSurfaceHostFacet({ module: pixverseModuleRef,
|
|
9
9
|
declaration: pixverseMarkupSurfaces.find((item) => item.name === "video")!, handler: decodePixverseVideoSurface }),
|
|
10
|
+
createMarkupSurfaceHostFacet({ module: pixverseModuleRef,
|
|
11
|
+
declaration: pixverseMarkupSurfaces.find((item) => item.name === "reference-video")!, handler: decodePixverseReferenceVideoSurface }),
|
|
10
12
|
] };
|
|
11
13
|
export default hypitPackage;
|
|
@@ -1,117 +1,231 @@
|
|
|
1
1
|
import { artifactTypes } from "@hypit/artifact";
|
|
2
2
|
import { sealGenerationPortRequest, sealGenerationPortTable } from "@hypit/generation";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
GenerationPort,
|
|
5
|
+
GenerationPortRequirement,
|
|
6
|
+
GenerationPortTable,
|
|
7
|
+
GenerationPortValue,
|
|
8
|
+
GenerationRequest,
|
|
9
|
+
} from "@hypit/generation";
|
|
10
|
+
import type { SurfaceAttributeVocabulary, SurfacePortVocabulary } from "@hypit/markup";
|
|
5
11
|
import { defineExactModelModule } from "@hypit/model-kit";
|
|
6
12
|
import { textTypes } from "@hypit/text";
|
|
7
13
|
|
|
14
|
+
import { pixverseRequestValidator } from "./validation.js";
|
|
15
|
+
|
|
8
16
|
export const pixverseModuleRef = { name: "@hypit/pixverse", version: "1" } as const;
|
|
9
|
-
export const pixverseModels = ["pixverse-v6"] as const;
|
|
17
|
+
export const pixverseModels = ["pixverse-v6", "pixverse-c1"] as const;
|
|
10
18
|
export type PixverseModel = typeof pixverseModels[number];
|
|
11
19
|
|
|
12
20
|
const PIXVERSE_QUALITIES = ["360p", "540p", "720p", "1080p"] as const;
|
|
13
21
|
const PIXVERSE_ASPECT_RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"] as const;
|
|
22
|
+
/** V6 reads `auto` as the shape of the reference videos it generates from. */
|
|
23
|
+
const PIXVERSE_V6_ASPECT_RATIOS = [...PIXVERSE_ASPECT_RATIOS, "auto"] as const;
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ name: "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Exact PixVerse model inputs. V6 takes up to ten reference images and up to two reference videos,
|
|
27
|
+
* and carries the sampling seed and multi-clip switch its endpoints declare. C1 takes up to seven
|
|
28
|
+
* reference images and generates from a prompt, a frame or a pair of frames.
|
|
29
|
+
*/
|
|
30
|
+
function pixversePortTable(model: PixverseModel): GenerationPortTable {
|
|
31
|
+
const v6 = model === "pixverse-v6";
|
|
32
|
+
const referenceVideo: readonly GenerationPort[] = v6
|
|
33
|
+
? [{ name: "referenceVideo", value: { kind: "media", accepts: ["video"] }, minItems: 0, maxItems: 2 }]
|
|
34
|
+
: [];
|
|
35
|
+
const v6Switches: readonly GenerationPort[] = v6
|
|
36
|
+
? [
|
|
37
|
+
{ name: "multiClip", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
|
|
38
|
+
{ name: "seed", value: { kind: "number", integer: true, minimum: 0, maximum: 2_147_483_647 }, minItems: 0, maxItems: 1 },
|
|
39
|
+
]
|
|
40
|
+
: [];
|
|
41
|
+
const v6Requires: readonly GenerationPortRequirement[] = v6
|
|
42
|
+
? [
|
|
43
|
+
// A run that bridges two frames, and one that carries subject references, is a single
|
|
44
|
+
// continuous shot rather than several cuts.
|
|
45
|
+
{ kind: "atMostOneOf", ports: ["multiClip", "lastFrame"] },
|
|
46
|
+
{ kind: "atMostOneOf", ports: ["multiClip", "referenceImage"] },
|
|
47
|
+
// The reference videos carry the length of the generated run.
|
|
48
|
+
{ kind: "atMostOneOf", ports: ["duration", "referenceVideo"] },
|
|
49
|
+
]
|
|
50
|
+
: [];
|
|
51
|
+
return sealGenerationPortTable({
|
|
52
|
+
model,
|
|
53
|
+
result: "video",
|
|
54
|
+
ports: [
|
|
55
|
+
{ name: "prompt", value: { kind: "text", maxChars: 5_000 }, minItems: 1, maxItems: 1 },
|
|
56
|
+
{ name: "firstFrame", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: 1 },
|
|
57
|
+
{ name: "lastFrame", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: 1 },
|
|
58
|
+
{ name: "referenceImage", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: v6 ? 10 : 7 },
|
|
59
|
+
...referenceVideo,
|
|
60
|
+
// Required except in the reference-video mode, which pixverseRequestValidator states.
|
|
61
|
+
{ name: "duration", value: { kind: "number", integer: true, minimum: 1, maximum: 15 }, minItems: 0, maxItems: 1 },
|
|
62
|
+
{ name: "quality", value: { kind: "enum", values: [...PIXVERSE_QUALITIES] }, minItems: 1, maxItems: 1 },
|
|
63
|
+
{
|
|
64
|
+
name: "aspectRatio",
|
|
65
|
+
value: { kind: "enum", values: v6 ? [...PIXVERSE_V6_ASPECT_RATIOS] : [...PIXVERSE_ASPECT_RATIOS] },
|
|
66
|
+
minItems: 0,
|
|
67
|
+
maxItems: 1,
|
|
68
|
+
},
|
|
69
|
+
{ name: "generateAudio", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
|
|
70
|
+
...v6Switches,
|
|
71
|
+
],
|
|
72
|
+
requires: [
|
|
73
|
+
// A last frame states where a run that already has a first frame ends.
|
|
74
|
+
{ kind: "requiresPresent", port: "lastFrame", needs: ["firstFrame"] },
|
|
75
|
+
// A run that starts from a frame inherits that frame's shape.
|
|
76
|
+
{ kind: "atMostOneOf", ports: ["aspectRatio", "firstFrame"] },
|
|
77
|
+
// Frames and subject references are separate ways of placing an image in the run.
|
|
78
|
+
{ kind: "atMostOneOf", ports: ["referenceImage", "firstFrame"] },
|
|
79
|
+
...v6Requires,
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
38
83
|
|
|
39
84
|
export const pixversePorts: Readonly<Record<PixverseModel, GenerationPortTable>> = {
|
|
40
|
-
"pixverse-v6":
|
|
85
|
+
"pixverse-v6": pixversePortTable("pixverse-v6"),
|
|
86
|
+
"pixverse-c1": pixversePortTable("pixverse-c1"),
|
|
41
87
|
};
|
|
42
88
|
|
|
43
89
|
export function sealPixverseRequest(
|
|
90
|
+
model: PixverseModel,
|
|
44
91
|
ports: Readonly<Record<string, readonly GenerationPortValue[]>>,
|
|
45
92
|
): GenerationRequest {
|
|
46
|
-
return sealGenerationPortRequest(
|
|
93
|
+
return sealGenerationPortRequest(pixversePorts[model], ports);
|
|
47
94
|
}
|
|
48
95
|
|
|
49
96
|
const pixverseBaseDefinition = defineExactModelModule({
|
|
50
97
|
module: pixverseModuleRef,
|
|
51
|
-
endpoints: [{
|
|
52
|
-
key
|
|
53
|
-
requestTypeName: "PixverseV6Request",
|
|
54
|
-
producerName:
|
|
55
|
-
ports:
|
|
56
|
-
|
|
98
|
+
endpoints: ([["v6", "pixverse-v6"], ["c1", "pixverse-c1"]] as const).map(([key, model]) => ({
|
|
99
|
+
key,
|
|
100
|
+
requestTypeName: model === "pixverse-v6" ? "PixverseV6Request" : "PixverseC1Request",
|
|
101
|
+
producerName: `request-${model}`,
|
|
102
|
+
ports: pixversePorts[model],
|
|
103
|
+
validateRequest: pixverseRequestValidator(model),
|
|
104
|
+
})),
|
|
57
105
|
});
|
|
58
106
|
|
|
59
107
|
export const pixverseEndpoints = pixverseBaseDefinition.endpoints;
|
|
108
|
+
export const pixverseEndpointsByModel = {
|
|
109
|
+
"pixverse-v6": pixverseEndpoints.v6!,
|
|
110
|
+
"pixverse-c1": pixverseEndpoints.c1!,
|
|
111
|
+
} as const;
|
|
60
112
|
export const pixverseComponent = pixverseBaseDefinition.component;
|
|
61
|
-
const endpoint = pixverseEndpoints.video!;
|
|
62
113
|
|
|
63
|
-
const
|
|
114
|
+
const surfaceOutputs = Object.values(pixverseEndpoints).flatMap((endpoint) => [
|
|
115
|
+
endpoint.draftType,
|
|
116
|
+
...Object.values(endpoint.mediaBindings).map((binding) => binding.type),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const pixverseCommonAttributes: readonly SurfaceAttributeVocabulary[] = [
|
|
64
120
|
{ name: "id", kind: "identifier", required: true,
|
|
65
121
|
summary: "Names this generation so its video Artifact can be referenced elsewhere in the Source." },
|
|
122
|
+
{ name: "model", kind: "literal", required: true, values: ["v6", "pixverse-v6", "c1", "pixverse-c1"],
|
|
123
|
+
summary: "Chooses the exact PixVerse model that renders the video." },
|
|
66
124
|
{ name: "prompt", kind: "reference", required: true, accepts: [textTypes.text],
|
|
67
125
|
summary: "The Text edge describing the shot, including any spoken line the model should voice." },
|
|
68
|
-
{ name: "first-frame", kind: "reference", required: false, accepts: [artifactTypes.blob],
|
|
69
|
-
summary: "Starts the video from one image Artifact." },
|
|
70
|
-
{ name: "last-frame", kind: "reference", required: false, accepts: [artifactTypes.blob],
|
|
71
|
-
summary: "Ends the video on one image Artifact, bridging from the first frame." },
|
|
72
|
-
{ name: "duration", kind: "literal", required: true,
|
|
73
|
-
summary: "How many seconds of video to render, from 1 to 15." },
|
|
74
126
|
{ name: "quality", kind: "literal", required: true, values: [...PIXVERSE_QUALITIES],
|
|
75
127
|
summary: "The size band the model renders at." },
|
|
76
|
-
{ name: "aspect-ratio", kind: "literal", required: false, values: [...PIXVERSE_ASPECT_RATIOS],
|
|
77
|
-
summary: "The Frame shape of the generated video." },
|
|
78
128
|
{ name: "generate-audio", kind: "literal", required: false, values: ["true", "false"],
|
|
79
129
|
summary: "Renders an audio track alongside the picture." },
|
|
80
|
-
{ name: "multi-clip", kind: "literal", required: false, values: ["true", "false"],
|
|
81
|
-
summary: "Renders the prompt as several cuts instead of one continuous shot." },
|
|
82
130
|
{ name: "seed", kind: "literal", required: false,
|
|
83
|
-
summary: "Seeds
|
|
131
|
+
summary: "Seeds V6's sampling so a rerun stays close to this one." },
|
|
84
132
|
];
|
|
85
133
|
|
|
86
|
-
|
|
134
|
+
const pixverseAspectRatio: SurfaceAttributeVocabulary = {
|
|
135
|
+
name: "aspect-ratio", kind: "literal", required: false, values: [...PIXVERSE_V6_ASPECT_RATIOS],
|
|
136
|
+
summary: "The Frame shape of the generated video.",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const pixverseVideoPort: readonly SurfacePortVocabulary[] = [{
|
|
87
140
|
name: "video",
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
type: artifactTypes.blob,
|
|
142
|
+
summary: "The generated video, addressed as `<id>.video`.",
|
|
143
|
+
}];
|
|
144
|
+
|
|
145
|
+
const pixverseQualityNote = "`quality` is `360p`, `540p`, `720p` or `1080p`, and `duration` is 1 to 15 seconds.";
|
|
146
|
+
const pixversePromptNote = "A spoken line belongs in the prompt; the model exposes no separate voice, language or dialogue field.";
|
|
147
|
+
const pixverseModelNote = "V6 accepts `seed` and up to ten references; C1 accepts up to seven references.";
|
|
148
|
+
|
|
149
|
+
export const pixverseMarkupSurfaces = [
|
|
150
|
+
{
|
|
151
|
+
name: "video",
|
|
152
|
+
tag: "Video",
|
|
153
|
+
mode: "structured" as const,
|
|
154
|
+
outputs: surfaceOutputs,
|
|
155
|
+
vocabulary: {
|
|
156
|
+
summary: "Generates one video with an exact PixVerse model from a Text prompt, optionally starting from a frame or bridging two.",
|
|
157
|
+
attributes: [
|
|
158
|
+
...pixverseCommonAttributes,
|
|
159
|
+
{ name: "duration", kind: "literal" as const, required: true,
|
|
160
|
+
summary: "How many seconds of video to render, from 1 to 15." },
|
|
161
|
+
{ name: "first-frame", kind: "reference" as const, required: false, accepts: [artifactTypes.blob],
|
|
162
|
+
summary: "Starts the video from one image Artifact." },
|
|
163
|
+
{ name: "last-frame", kind: "reference" as const, required: false, accepts: [artifactTypes.blob],
|
|
164
|
+
summary: "Ends the video on one image Artifact, bridging from the first frame." },
|
|
165
|
+
pixverseAspectRatio,
|
|
166
|
+
{ name: "multi-clip", kind: "literal" as const, required: false, values: ["true", "false"],
|
|
167
|
+
summary: "Renders the prompt as several cuts instead of one continuous shot, on V6." },
|
|
168
|
+
],
|
|
169
|
+
ports: pixverseVideoPort,
|
|
170
|
+
example: `<pix:Video
|
|
100
171
|
id="opening"
|
|
172
|
+
model="v6"
|
|
101
173
|
prompt={line}
|
|
102
174
|
duration="5"
|
|
103
175
|
quality="720p"
|
|
104
176
|
aspect-ratio="9:16"
|
|
105
177
|
generate-audio="true"
|
|
106
178
|
/>`,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
notes: [
|
|
180
|
+
pixverseQualityNote,
|
|
181
|
+
pixverseModelNote,
|
|
182
|
+
"A run that starts from a frame takes its shape from that frame, so `aspect-ratio` states the shape only for a prompt-only run.",
|
|
183
|
+
"A `last-frame` bridges from the `first-frame` into one continuous shot, so it is not combined with `multi-clip`.",
|
|
184
|
+
pixversePromptNote,
|
|
185
|
+
"The element accepts no children and no text content.",
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "reference-video",
|
|
191
|
+
tag: "ReferenceVideo",
|
|
192
|
+
mode: "structured" as const,
|
|
193
|
+
outputs: surfaceOutputs,
|
|
194
|
+
vocabulary: {
|
|
195
|
+
summary: "Generates one video with an exact PixVerse model from a Text prompt and the image or video subjects it carries.",
|
|
196
|
+
attributes: [
|
|
197
|
+
...pixverseCommonAttributes,
|
|
198
|
+
{ name: "duration", kind: "literal" as const, required: false,
|
|
199
|
+
summary: "How many seconds of video to render, from 1 to 15; video references carry their own length." },
|
|
200
|
+
pixverseAspectRatio,
|
|
201
|
+
],
|
|
202
|
+
children: [{
|
|
203
|
+
tag: "Reference",
|
|
204
|
+
cardinality: "many" as const,
|
|
205
|
+
summary: "Attaches one subject Artifact the model generates from, chosen by an `image` or `video` reference.",
|
|
206
|
+
attributes: [
|
|
207
|
+
{ name: "image", kind: "reference" as const, required: false, accepts: [artifactTypes.blob],
|
|
208
|
+
summary: "Selects the image Artifact whose subject the generated video carries." },
|
|
209
|
+
{ name: "video", kind: "reference" as const, required: false, accepts: [artifactTypes.blob],
|
|
210
|
+
summary: "Selects the video Artifact whose motion and subject the generated video carries." },
|
|
211
|
+
],
|
|
212
|
+
}],
|
|
213
|
+
ports: pixverseVideoPort,
|
|
214
|
+
example: `<pix:ReferenceVideo id="fusion" model="v6" prompt={outfit} duration="5" quality="720p" aspect-ratio="16:9">
|
|
215
|
+
<pix:Reference image={character.image}/>
|
|
216
|
+
<pix:Reference image={clothes.image}/>
|
|
217
|
+
</pix:ReferenceVideo>`,
|
|
218
|
+
notes: [
|
|
219
|
+
pixverseQualityNote,
|
|
220
|
+
pixverseModelNote,
|
|
221
|
+
"The prompt addresses the references in order as `@ref_1`, `@ref_2` and so on.",
|
|
222
|
+
"`Reference` carries exactly one of `image` or `video`, and is empty.",
|
|
223
|
+
"V6 accepts up to two video references totalling 15 seconds, which carry the length of the run in place of `duration`; `aspect-ratio=\"auto\"` takes their shape.",
|
|
224
|
+
pixversePromptNote,
|
|
225
|
+
],
|
|
226
|
+
},
|
|
113
227
|
},
|
|
114
|
-
|
|
228
|
+
] as const;
|
|
115
229
|
|
|
116
230
|
export const pixverseManifest = {
|
|
117
231
|
...pixverseBaseDefinition.manifest,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { artifactTypes } from "@hypit/artifact";
|
|
2
2
|
import { generationPort, sealGenerationMediaBinding, sealGenerationRequestDraft } from "@hypit/generation";
|
|
3
|
-
import type { GenerationMediaPort, GenerationPortValue } from "@hypit/generation";
|
|
3
|
+
import type { GenerationMediaPort, GenerationMediaRole, GenerationPortValue } from "@hypit/generation";
|
|
4
4
|
import {
|
|
5
5
|
createExactModelPrimaryGenerationFragment,
|
|
6
6
|
exactModelMediaInputNames,
|
|
@@ -16,23 +16,31 @@ import type {
|
|
|
16
16
|
import type { CanonicalValue, TypeRef } from "@hypit/protocol";
|
|
17
17
|
import { textTypes, verifyText } from "@hypit/text";
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import { pixverseEndpointsByModel } from "./index.js";
|
|
20
|
+
import type { PixverseModel } from "./index.js";
|
|
21
|
+
|
|
22
|
+
type MediaInput = {
|
|
23
|
+
readonly port: "firstFrame" | "lastFrame" | "referenceImage" | "referenceVideo";
|
|
24
|
+
readonly role: GenerationMediaRole;
|
|
25
|
+
readonly source: SurfaceResolvedReference;
|
|
26
|
+
};
|
|
20
27
|
|
|
21
28
|
function assert(condition: unknown, message: string): asserts condition {
|
|
22
29
|
if (!condition) throw new Error(message);
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function localName(name: string): string {
|
|
33
|
+
return name.includes(":") ? name.slice(name.lastIndexOf(":") + 1) : name;
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
function sameType(left: TypeRef, right: TypeRef): boolean {
|
|
26
37
|
return left.name === right.name && left.module.name === right.module.name && left.module.version === right.module.version;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function exact(element: StructuredElement): void {
|
|
33
|
-
const unknown = Object.keys(element.attributes).filter((name) => !attributes.includes(name as typeof attributes[number]));
|
|
40
|
+
function exact(element: StructuredElement, allowed: readonly string[], required: readonly string[]): void {
|
|
41
|
+
const unknown = Object.keys(element.attributes).filter((name) => !allowed.includes(name));
|
|
34
42
|
assert(unknown.length === 0, `${element.name} does not accept ${unknown[0]}`);
|
|
35
|
-
const missing =
|
|
43
|
+
const missing = required.filter((name) => element.attributes[name] === undefined);
|
|
36
44
|
assert(missing.length === 0, `${element.name} requires ${missing.join(", ")}`);
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -60,6 +68,10 @@ function integer(element: StructuredElement, name: string): readonly GenerationP
|
|
|
60
68
|
return [Number(value)];
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
function optionalInteger(element: StructuredElement, name: string): readonly GenerationPortValue[] | undefined {
|
|
72
|
+
return element.attributes[name] === undefined ? undefined : integer(element, name);
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
function ref(
|
|
64
76
|
element: StructuredElement,
|
|
65
77
|
name: string,
|
|
@@ -73,74 +85,171 @@ function ref(
|
|
|
73
85
|
return result;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
function
|
|
88
|
+
function media(
|
|
77
89
|
element: StructuredElement,
|
|
78
90
|
name: string,
|
|
91
|
+
role: GenerationMediaRole,
|
|
79
92
|
resolve: (path: string) => SurfaceResolvedReference | undefined,
|
|
80
|
-
): SurfaceResolvedReference
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`${element.name}.${name} must reference image media`);
|
|
93
|
+
): SurfaceResolvedReference {
|
|
94
|
+
const artifact = ref(element, name, artifactTypes.blob, resolve);
|
|
95
|
+
if (artifact.record !== undefined) {
|
|
96
|
+
assert(artifact.record.value.kind === "blob" && artifact.record.value.mediaType.startsWith(`${role}/`),
|
|
97
|
+
`${element.name}.${name} must reference ${role} media`);
|
|
86
98
|
}
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
99
|
+
return artifact;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function empty(element: StructuredElement): void {
|
|
103
|
+
assert(!element.children.some((item) => item.kind === "element" || item.value.trim()),
|
|
104
|
+
`${element.name} accepts no children`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function selectModel(element: StructuredElement): { endpoint: ExactModelEndpoint; model: PixverseModel } {
|
|
108
|
+
const requested = text(element, "model");
|
|
109
|
+
if (requested === "v6" || requested === "pixverse-v6") {
|
|
110
|
+
return { endpoint: pixverseEndpointsByModel["pixverse-v6"], model: "pixverse-v6" };
|
|
111
|
+
}
|
|
112
|
+
if (requested === "c1" || requested === "pixverse-c1") {
|
|
113
|
+
return { endpoint: pixverseEndpointsByModel["pixverse-c1"], model: "pixverse-c1" };
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`${element.name}.model must be v6 or c1`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A switch one model carries and the other does not is refused where it was authored. */
|
|
119
|
+
function modelScoped(
|
|
120
|
+
element: StructuredElement,
|
|
121
|
+
endpoint: ExactModelEndpoint,
|
|
122
|
+
attribute: string,
|
|
123
|
+
port: string,
|
|
124
|
+
read: (element: StructuredElement, name: string) => readonly GenerationPortValue[] | undefined,
|
|
125
|
+
): readonly GenerationPortValue[] | undefined {
|
|
126
|
+
if (element.attributes[attribute] === undefined) return undefined;
|
|
127
|
+
assert(endpoint.ports.ports.some((item) => item.name === port),
|
|
128
|
+
`${element.name}.${attribute} is not accepted by ${endpoint.ports.model}`);
|
|
129
|
+
return read(element, attribute);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function capacity(element: StructuredElement, endpoint: ExactModelEndpoint, port: string, used: number): void {
|
|
133
|
+
if (used === 0) return;
|
|
134
|
+
const declared = endpoint.ports.ports.find((item) => item.name === port);
|
|
135
|
+
assert(declared !== undefined, `${element.name} references are not accepted by ${endpoint.ports.model}`);
|
|
136
|
+
assert(used <= declared.maxItems,
|
|
137
|
+
`${element.name} accepts at most ${declared.maxItems} ${port === "referenceVideo" ? "video" : "image"} references on ${endpoint.ports.model}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assemble(
|
|
141
|
+
element: StructuredElement,
|
|
142
|
+
endpoint: ExactModelEndpoint,
|
|
143
|
+
prompt: SurfaceResolvedReference,
|
|
144
|
+
inputs: readonly MediaInput[],
|
|
145
|
+
stated: Readonly<Record<string, readonly GenerationPortValue[] | undefined>>,
|
|
146
|
+
) {
|
|
147
|
+
const id = text(element, "id");
|
|
148
|
+
const draft = sealGenerationRequestDraft(endpoint.ports, Object.fromEntries(
|
|
149
|
+
Object.entries(stated).filter((entry): entry is [string, readonly GenerationPortValue[]] => entry[1] !== undefined),
|
|
150
|
+
));
|
|
151
|
+
const records: Array<{ id: string; type: TypeRef; value: { kind: "inline"; value: CanonicalValue }; range: StructuredElement["range"] }> = [{
|
|
152
|
+
id: `${id}.draft`, type: endpoint.draftType,
|
|
153
|
+
value: { kind: "inline", value: draft as unknown as CanonicalValue }, range: element.range,
|
|
154
|
+
}];
|
|
155
|
+
const componentInputs: Record<string, SurfaceResolvedReference["ref"] | { kind: "record"; id: string }> = {
|
|
156
|
+
draft: { kind: "record", id: `${id}.draft` }, [exactModelTextInputName("prompt")]: prompt.ref,
|
|
143
157
|
};
|
|
158
|
+
const attached = inputs.map(({ port, role, source }, index) => {
|
|
159
|
+
const name = `media-${String(index + 1).padStart(4, "0")}`;
|
|
160
|
+
const bindingId = `${id}.${name}.binding`;
|
|
161
|
+
const mediaPort = generationPort(endpoint.ports, port);
|
|
162
|
+
assert(mediaPort.value.kind === "media", `PixVerse port ${port} is not media`);
|
|
163
|
+
records.push({
|
|
164
|
+
id: bindingId, type: endpoint.mediaBindings[port]!.type,
|
|
165
|
+
value: { kind: "inline", value: sealGenerationMediaBinding(mediaPort as GenerationMediaPort, { role }) as unknown as CanonicalValue },
|
|
166
|
+
range: element.range,
|
|
167
|
+
});
|
|
168
|
+
const names = exactModelMediaInputNames(name);
|
|
169
|
+
componentInputs[names.binding] = { kind: "record", id: bindingId };
|
|
170
|
+
componentInputs[names.artifact] = source.ref;
|
|
171
|
+
return { name, port } as const;
|
|
172
|
+
});
|
|
173
|
+
const fragment = createExactModelPrimaryGenerationFragment(endpoint, attached, [{ name: "prompt", port: "prompt" }]);
|
|
174
|
+
return { records, fragments: [fragment], components: [{
|
|
175
|
+
id, fragment: fragment.id, inputs: componentInputs, outputs: { video: `${id}.video` }, range: element.range,
|
|
176
|
+
}] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function promptReference(
|
|
180
|
+
element: StructuredElement,
|
|
181
|
+
resolveReference: (path: string) => SurfaceResolvedReference | undefined,
|
|
182
|
+
): SurfaceResolvedReference {
|
|
183
|
+
const prompt = ref(element, "prompt", textTypes.text, resolveReference);
|
|
184
|
+
if (prompt.record !== undefined) {
|
|
185
|
+
assert(prompt.record.value.kind === "inline", `${element.name}.prompt must reference Text`);
|
|
186
|
+
verifyText(prompt.record.value.value);
|
|
187
|
+
}
|
|
188
|
+
return prompt;
|
|
144
189
|
}
|
|
145
190
|
|
|
146
|
-
|
|
191
|
+
const VIDEO_ATTRIBUTES = ["id", "model", "prompt", "first-frame", "last-frame", "duration", "quality",
|
|
192
|
+
"aspect-ratio", "generate-audio", "multi-clip", "seed"] as const;
|
|
193
|
+
|
|
194
|
+
export const decodePixverseVideoSurface: StructuredSurfaceHandler = ({ element, resolveReference }) => {
|
|
195
|
+
exact(element, VIDEO_ATTRIBUTES, ["id", "model", "prompt", "duration", "quality"]);
|
|
196
|
+
empty(element);
|
|
197
|
+
const { endpoint } = selectModel(element);
|
|
198
|
+
const frames = ([["firstFrame", "first-frame"], ["lastFrame", "last-frame"]] as const)
|
|
199
|
+
.filter(([, attribute]) => element.attributes[attribute] !== undefined)
|
|
200
|
+
.map(([port, attribute]) => ({
|
|
201
|
+
port, role: "image" as const, source: media(element, attribute, "image", resolveReference),
|
|
202
|
+
}));
|
|
203
|
+
return assemble(element, endpoint, promptReference(element, resolveReference), frames, {
|
|
204
|
+
duration: integer(element, "duration"),
|
|
205
|
+
quality: [text(element, "quality")],
|
|
206
|
+
aspectRatio: optionalText(element, "aspect-ratio"),
|
|
207
|
+
generateAudio: optionalFlag(element, "generate-audio"),
|
|
208
|
+
multiClip: modelScoped(element, endpoint, "multi-clip", "multiClip", optionalFlag),
|
|
209
|
+
seed: modelScoped(element, endpoint, "seed", "seed", optionalInteger),
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const REFERENCE_ATTRIBUTES = ["id", "model", "prompt", "duration", "quality",
|
|
214
|
+
"aspect-ratio", "generate-audio", "seed"] as const;
|
|
215
|
+
|
|
216
|
+
export const decodePixverseReferenceVideoSurface: StructuredSurfaceHandler = ({ element, resolveReference }) => {
|
|
217
|
+
exact(element, REFERENCE_ATTRIBUTES, ["id", "model", "prompt", "quality"]);
|
|
218
|
+
const { endpoint } = selectModel(element);
|
|
219
|
+
const references: MediaInput[] = [];
|
|
220
|
+
for (const child of element.children) {
|
|
221
|
+
if (child.kind === "text") {
|
|
222
|
+
assert(child.value.trim().length === 0, `${element.name} accepts only Reference children`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
assert(localName(child.name) === "Reference", `${element.name} accepts only Reference children`);
|
|
226
|
+
empty(child);
|
|
227
|
+
const roles = (["image", "video"] as const).filter((role) => child.attributes[role] !== undefined);
|
|
228
|
+
exact(child, ["image", "video"], []);
|
|
229
|
+
assert(roles.length === 1, `${child.name} requires exactly one of image, video`);
|
|
230
|
+
const role = roles[0]!;
|
|
231
|
+
references.push({
|
|
232
|
+
port: role === "image" ? "referenceImage" : "referenceVideo",
|
|
233
|
+
role,
|
|
234
|
+
source: media(child, role, role, resolveReference),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
assert(references.length > 0, `${element.name} requires at least one Reference`);
|
|
238
|
+
for (const port of ["referenceImage", "referenceVideo"] as const) {
|
|
239
|
+
capacity(element, endpoint, port, references.filter((item) => item.port === port).length);
|
|
240
|
+
}
|
|
241
|
+
const videos = references.some((item) => item.port === "referenceVideo");
|
|
242
|
+
if (videos) {
|
|
243
|
+
assert(element.attributes["duration"] === undefined,
|
|
244
|
+
`${element.name} takes no duration; its video references carry the length of the run`);
|
|
245
|
+
} else {
|
|
246
|
+
assert(element.attributes["duration"] !== undefined, `${element.name} requires duration`);
|
|
247
|
+
}
|
|
248
|
+
return assemble(element, endpoint, promptReference(element, resolveReference), references, {
|
|
249
|
+
duration: optionalInteger(element, "duration"),
|
|
250
|
+
quality: [text(element, "quality")],
|
|
251
|
+
aspectRatio: optionalText(element, "aspect-ratio"),
|
|
252
|
+
generateAudio: optionalFlag(element, "generate-audio"),
|
|
253
|
+
seed: modelScoped(element, endpoint, "seed", "seed", optionalInteger),
|
|
254
|
+
});
|
|
255
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GenerationRequest } from "@hypit/generation";
|
|
2
|
+
import type { PixverseModel } from "./index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reference videos carry the length of the run, so that mode states no duration.
|
|
6
|
+
* Every other mode states its own.
|
|
7
|
+
*/
|
|
8
|
+
export function pixverseRequestValidator(model: PixverseModel): (request: GenerationRequest) => void {
|
|
9
|
+
return (request) => {
|
|
10
|
+
if (request.ports.referenceVideo !== undefined || request.ports.duration !== undefined) return;
|
|
11
|
+
throw new Error(`${model} requires duration`);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -20,6 +20,13 @@ Seedance 2.5 (`@hypit/seedance` model `2.5`) maps to `seedance-2.5` and supports
|
|
|
20
20
|
`480p`, `720p` and `1080p`. The Provider passes the authored `resolution` to `POST /v1/videos`;
|
|
21
21
|
omitting it in the Seedance Surface defaults to `720p`.
|
|
22
22
|
|
|
23
|
+
`@hypit/pixverse` models `pixverse-v6` and `pixverse-c1` map to `pixverse/v6` and `pixverse/c1` on
|
|
24
|
+
`POST /v1/videos`. The model's own `quality` band travels as `resolution` and its duration as
|
|
25
|
+
`seconds`; frames use `first_frame` and `last_frame`, image references use `reference_image_urls`,
|
|
26
|
+
and V6's video references use `reference_videos`. A reference-video request carries no `seconds`.
|
|
27
|
+
This body has no field for V6's `seed` or `multi-clip`, so a request that states either is refused
|
|
28
|
+
by name before any reference is uploaded.
|
|
29
|
+
|
|
23
30
|
The current HypiHub GPT Image 2 route has these service-specific limits:
|
|
24
31
|
|
|
25
32
|
| Resolution | Ratios unavailable at this Endpoint | `background` |
|
|
@@ -32,7 +39,8 @@ HypiHub owns this support check independently: it leaves the GPT Image model pac
|
|
|
32
39
|
the model or another Provider.
|
|
33
40
|
|
|
34
41
|
Model identity and input mode are separate. The mapping uses HypiHub's canonical model names:
|
|
35
|
-
`gpt-image-2`, `seedream-5-lite`, `minimax-h3`, `grok-imagine-video`
|
|
42
|
+
`gpt-image-2`, `seedream-5-lite`, `minimax-h3`, `grok-imagine-video`, `pixverse/v6`,
|
|
43
|
+
`pixverse/c1` and the individual Seedance names.
|
|
36
44
|
An image request without references uses `/images/generations`; image edits use `/images/edits`
|
|
37
45
|
with the same model name. Video requests use `/videos`, preserving reference images, reference
|
|
38
46
|
videos and first/last frames in their distinct fields. Old operation-specific names are not needed
|
|
@@ -8,6 +8,7 @@ const NANO_BANANA: ModuleRef = { name: "@hypit/nano-banana", version: "1" };
|
|
|
8
8
|
const SEEDREAM: ModuleRef = { name: "@hypit/seedream", version: "1" };
|
|
9
9
|
const MINIMAX: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
|
|
10
10
|
const GROK: ModuleRef = { name: "@hypit/grok-imagine", version: "1" };
|
|
11
|
+
const PIXVERSE: ModuleRef = { name: "@hypit/pixverse", version: "1" };
|
|
11
12
|
const MIMO_SPEECH: ModuleRef = { name: "@hypit/mimo-speech", version: "1" };
|
|
12
13
|
const FISHAUDIO_SPEECH: ModuleRef = { name: "@hypit/fishaudio-speech", version: "1" };
|
|
13
14
|
const ELEVENLABS_SPEECH: ModuleRef = { name: "@hypit/elevenlabs-speech", version: "1" };
|
|
@@ -29,6 +30,26 @@ const seedance = (name: string): GenerationWireMapping => ({
|
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* PixVerse V6 and C1 on `POST /v1/videos`. Both take the same body; V6 additionally accepts
|
|
35
|
+
* reference videos, which carry the length of the run in place of `seconds`. The model's own
|
|
36
|
+
* `quality` band is HypiHub's `resolution`.
|
|
37
|
+
*/
|
|
38
|
+
const pixverse = (name: string, model: string): GenerationWireMapping => ({
|
|
39
|
+
capability: { module: PIXVERSE, name }, result: "video", routes: [{ model }],
|
|
40
|
+
fields: {
|
|
41
|
+
prompt: { as: "value", field: "prompt" },
|
|
42
|
+
firstFrame: { as: "url", field: "first_frame" },
|
|
43
|
+
lastFrame: { as: "url", field: "last_frame" },
|
|
44
|
+
referenceImage: { as: "urlArray", field: "reference_image_urls" },
|
|
45
|
+
...(name === "pixverse-v6" ? { referenceVideo: { as: "urlArray" as const, field: "reference_videos" } } : {}),
|
|
46
|
+
duration: { as: "value", field: "seconds" },
|
|
47
|
+
quality: { as: "value", field: "resolution" },
|
|
48
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
49
|
+
generateAudio: { as: "value", field: "generate_audio" },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
32
53
|
export const hypiHubMappings: readonly GenerationWireMapping[] = [
|
|
33
54
|
{
|
|
34
55
|
capability: { module: { name: "@hypit/volcengine-matting", version: "1" }, name: "matte-portrait-video" },
|
|
@@ -42,6 +63,8 @@ export const hypiHubMappings: readonly GenerationWireMapping[] = [
|
|
|
42
63
|
seedance("seedance-2-fast"),
|
|
43
64
|
seedance("seedance-2-mini"),
|
|
44
65
|
seedance("seedance-2.5"),
|
|
66
|
+
pixverse("pixverse-v6", "pixverse/v6"),
|
|
67
|
+
pixverse("pixverse-c1", "pixverse/c1"),
|
|
45
68
|
{
|
|
46
69
|
capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image", routes: [{ model: "gpt-image-2" }],
|
|
47
70
|
fields: {
|
|
@@ -23,7 +23,7 @@ const MEDIA_TYPES: Readonly<Record<string, string>> = {
|
|
|
23
23
|
|
|
24
24
|
async function readProject(source: string, html: string, resources: FileResourceStore): Promise<HyperframesHtmlProject> {
|
|
25
25
|
const assets = [];
|
|
26
|
-
const base =
|
|
26
|
+
const base = isSnapshotHtmlUrl(source) ? new URL(source) : pathToFileURL(source);
|
|
27
27
|
for (const url of hyperframesHtmlAssetUrls(html)) {
|
|
28
28
|
const address = new URL(url, base);
|
|
29
29
|
let mediaType: string | undefined;
|
|
@@ -52,6 +52,20 @@ async function readProject(source: string, html: string, resources: FileResource
|
|
|
52
52
|
|
|
53
53
|
const OPTIONS = ["--studio", "--to", "--at-frame", "--start-frame", "--end-frame-exclusive", "--step-frames", "--grid", "--cell", "--runtime", "--workspace"];
|
|
54
54
|
|
|
55
|
+
/** Capture already treats HTTP(S) case-insensitively; snapshot must not turn HTTPS:// into a local path. */
|
|
56
|
+
export function isSnapshotHtmlUrl(value: string): boolean {
|
|
57
|
+
return /^https?:\/\//iu.test(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `--studio` is a base URL, not a host:port token. `new URL` otherwise throws TypeError. */
|
|
61
|
+
export function studioDocumentUrl(studio: string): string {
|
|
62
|
+
try {
|
|
63
|
+
return new URL("/__studio/document", studio).href;
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`--studio needs an http(s) Studio URL, got ${studio}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
export function writeSnapshotHelp(io: CliIo): void {
|
|
56
70
|
io.write(`hypit snapshot\nCapture exact frames from an existing compiled HyperFrames HTML programme through the selected Runtime Profile.\n\n`
|
|
57
71
|
+ ` hypit snapshot --studio <studio-url> --at-frame <n[,n,…]> --to <directory>\n`
|
|
@@ -87,8 +101,8 @@ export async function runSnapshotCli(argv: readonly string[], io: CliIo, environ
|
|
|
87
101
|
return Number(raw);
|
|
88
102
|
};
|
|
89
103
|
const source = studio === undefined
|
|
90
|
-
?
|
|
91
|
-
:
|
|
104
|
+
? isSnapshotHtmlUrl(positionals[0]!) ? positionals[0]! : resolve(environment.cwd, positionals[0]!)
|
|
105
|
+
: studioDocumentUrl(studio);
|
|
92
106
|
let document: HyperframesDocument | undefined;
|
|
93
107
|
let html: string;
|
|
94
108
|
if (studio !== undefined) {
|
|
@@ -97,7 +111,7 @@ export async function runSnapshotCli(argv: readonly string[], io: CliIo, environ
|
|
|
97
111
|
document = await response.json() as HyperframesDocument;
|
|
98
112
|
assertHyperframesDocument(document);
|
|
99
113
|
html = document.html;
|
|
100
|
-
} else if (
|
|
114
|
+
} else if (isSnapshotHtmlUrl(source)) {
|
|
101
115
|
const response = await fetch(source);
|
|
102
116
|
if (!response.ok) throw new Error(`Snapshot HTML: HTTP ${response.status} ${await response.text()}`);
|
|
103
117
|
html = await response.text();
|