@hypit/hypit 0.2.8 → 0.2.10
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 +6 -0
- package/package.json +3 -2
- 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-beatapi/README.md +84 -0
- package/packages/provider-beatapi/package.json +23 -0
- package/packages/provider-beatapi/src/activation.ts +62 -0
- package/packages/provider-beatapi/src/errors.ts +46 -0
- package/packages/provider-beatapi/src/index.ts +2 -0
- package/packages/provider-beatapi/src/mapping.ts +89 -0
- package/packages/provider-beatapi/src/provider.ts +238 -0
- package/packages/provider-beatapi/src/routes.ts +127 -0
- package/packages/provider-hiapi/README.md +1 -3
- package/packages/provider-hypihub/README.md +9 -1
- package/packages/provider-hypihub/src/mapping.ts +23 -0
- package/packages/provider-monid/README.md +0 -2
- package/packages/studio/src/server.ts +17 -2
- package/packages/video-cli/src/snapshot.ts +18 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# `@hypit/provider-beatapi`
|
|
2
|
+
|
|
3
|
+
Hypit Runtime Provider for a [BeatAPI](https://docs.beatapi.io/quick-guide) account. Each capability
|
|
4
|
+
posts one task to `https://api.beatapi.io` with a `Bearer` API key, polls `GET /v1/tasks/{task_id}`
|
|
5
|
+
until it reaches `succeeded`, downloads every entry of `output.media` and stores the files in the
|
|
6
|
+
current Build.
|
|
7
|
+
|
|
8
|
+
| Capability | BeatAPI model | Task path |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `@hypit/seedance@1#seedance-2` | `seedance-2` | `/v1/videos/tasks` |
|
|
11
|
+
| `@hypit/seedance@1#seedance-2-fast` | `seedance-2-fast` | `/v1/videos/tasks` |
|
|
12
|
+
| `@hypit/seedance@1#seedance-2-mini` | `seedance-2-mini` | `/v1/videos/tasks` |
|
|
13
|
+
| `@hypit/seedance@1#seedance-2.5` | `seedance-2.5` | `/v1/videos/tasks` |
|
|
14
|
+
| `@hypit/minimax-h3@1#minimax-h3` | `minimax-h3` | `/v1/videos/tasks` |
|
|
15
|
+
| `@hypit/grok-imagine@1#grok-imagine-video-1.5-preview` | `grok-imagine-video-1.5` | `/v1/videos/tasks` |
|
|
16
|
+
| `@hypit/gpt-image@1#gpt-image-2` | `gpt-image-2` | `/v1/images/tasks` |
|
|
17
|
+
| `@hypit/nano-banana@1#nano-banana-2` | `nano-banana-2` | `/v1/images/tasks` |
|
|
18
|
+
| `@hypit/nano-banana@1#nano-banana-pro` | `nano-banana-pro` | `/v1/images/tasks` |
|
|
19
|
+
|
|
20
|
+
BeatAPI documents further models, including Veo, Kling, Wan 3.0 and HappyHorse; this Provider maps
|
|
21
|
+
only the models the Distribution already describes.
|
|
22
|
+
|
|
23
|
+
One BeatAPI alias serves every input mode, so the request shape alone decides which arrays travel:
|
|
24
|
+
`reference_images`, `reference_videos` and `reference_audios` carry subject references, while the
|
|
25
|
+
opening and closing frames travel as one ordered `images` array. `duration`, `aspect_ratio`,
|
|
26
|
+
`resolution` and Seedance's `generate_audio` keep their authored values. Nano Banana 2 spells its
|
|
27
|
+
JPEG output `jpeg`, so an authored `output-format="jpg"` is sent as that.
|
|
28
|
+
|
|
29
|
+
Service limits this Provider reports as unsupported before uploading any reference:
|
|
30
|
+
|
|
31
|
+
- Seedance has no `web_search` field, and `seedance-2-mini` renders no generated audio. An authored
|
|
32
|
+
`false` is dropped; an authored `true` is refused.
|
|
33
|
+
- Seedance 2 does not render 1080p together with reference images.
|
|
34
|
+
- Seedance 2.5 takes 4 to 30 seconds, so `duration="-1"` for an automatic length is refused.
|
|
35
|
+
- MiniMax H3 orders its two frames first then last, so a last frame needs a first frame.
|
|
36
|
+
- Grok Imagine 1.5 accepts at most one image at 1080p.
|
|
37
|
+
- GPT Image 2 has no `background` field.
|
|
38
|
+
- Nano Banana 2 accepts up to ten reference images and does not render `1:4`, `4:1`, `1:8` or `8:1`.
|
|
39
|
+
|
|
40
|
+
Seedance visual references carry `person-reference`; the Provider accepts the declaration and
|
|
41
|
+
transmits nothing for it, since BeatAPI's task body has no field for it.
|
|
42
|
+
|
|
43
|
+
Referenced Resources are uploaded to `POST /v1/files` with `purpose=input`, and the returned HTTPS
|
|
44
|
+
URL travels in the task body. The service accepts PNG, JPEG and WebP images and MP3, WAV, AAC and
|
|
45
|
+
M4A audio up to 50 MB each, and MP4 and MOV video up to 100 MB; a reference outside those types or
|
|
46
|
+
sizes is reported before submission. One Resource is uploaded once per Runtime operation. Embedded
|
|
47
|
+
callers may replace that transport with `publicAssetUrl(artifact, resources, fields)`, which must
|
|
48
|
+
return a URL BeatAPI can fetch.
|
|
49
|
+
|
|
50
|
+
Runtime Profile example:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"format": "hypit.runtime-local@1",
|
|
55
|
+
"dataRoot": ".hypit/runtimes/local",
|
|
56
|
+
"credentials": {
|
|
57
|
+
"platform": {
|
|
58
|
+
"use": "@hypit/credential-store-platform"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"endpoints": {
|
|
62
|
+
"beatapi.default": {
|
|
63
|
+
"use": "@hypit/provider-beatapi",
|
|
64
|
+
"pool": "beatapi.default",
|
|
65
|
+
"config": {
|
|
66
|
+
"apiKey": { "store": "platform", "key": "beatapi.api-key" },
|
|
67
|
+
"defaultConcurrency": 3,
|
|
68
|
+
"pollIntervalMs": 10000
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"bindings": {}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`baseUrl` defaults to `https://api.beatapi.io`. Store the API key with
|
|
77
|
+
`hypit auth login beatapi.default --runtime hypit.runtime.json`. Optional `requestTimeoutMs`,
|
|
78
|
+
`operationTimeoutMs` and `actionLimits` bound single HTTP calls, the whole remote task and action
|
|
79
|
+
concurrency. Each submission sends the Runtime operation as its `Idempotency-Key`, so a retried
|
|
80
|
+
submission of the same body resolves to the same task.
|
|
81
|
+
|
|
82
|
+
HTTP failures keep BeatAPI's `error.code`, message, `request_id` and any `retry_after_seconds`; a
|
|
83
|
+
`failed` task keeps its `error_code` and `error_message`. Any URL inside a surfaced reason is
|
|
84
|
+
redacted, since a hosted result URL is an access capability rather than diagnostic content.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypit/provider-beatapi",
|
|
3
|
+
"version": "0.0.0-dev",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": { ".": "./src/index.ts" },
|
|
8
|
+
"hypit": { "activation": "./src/activation.ts" },
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@hypit/endpoint-kit": "workspace:*",
|
|
11
|
+
"@hypit/generation": "workspace:*",
|
|
12
|
+
"@hypit/protocol": "workspace:*",
|
|
13
|
+
"@hypit/runtime": "workspace:*",
|
|
14
|
+
"@hypit/runtime-kit": "workspace:*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@hypit/gpt-image": "workspace:*",
|
|
18
|
+
"@hypit/grok-imagine": "workspace:*",
|
|
19
|
+
"@hypit/minimax-h3": "workspace:*",
|
|
20
|
+
"@hypit/nano-banana": "workspace:*",
|
|
21
|
+
"@hypit/seedance": "workspace:*"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeEndpointAdapterFacet,
|
|
3
|
+
runtimeConfigCredentialRef,
|
|
4
|
+
runtimeConfigActionLimits,
|
|
5
|
+
runtimeConfigExact,
|
|
6
|
+
runtimeConfigObject,
|
|
7
|
+
runtimeConfigPositiveInteger,
|
|
8
|
+
runtimeConfigString,
|
|
9
|
+
} from "@hypit/runtime-kit";
|
|
10
|
+
|
|
11
|
+
import { createBeatApiProvider } from "./provider.js";
|
|
12
|
+
|
|
13
|
+
const adapter = createRuntimeEndpointAdapterFacet({
|
|
14
|
+
use: "@hypit/provider-beatapi",
|
|
15
|
+
activate(context) {
|
|
16
|
+
if (context.pool === undefined) throw new Error("BeatAPI Provider Pool is required");
|
|
17
|
+
const config = runtimeConfigObject(context.config, "BeatAPI");
|
|
18
|
+
runtimeConfigExact(config, [
|
|
19
|
+
"baseUrl",
|
|
20
|
+
"apiKey",
|
|
21
|
+
"defaultConcurrency",
|
|
22
|
+
"actionLimits",
|
|
23
|
+
"pollIntervalMs",
|
|
24
|
+
"requestTimeoutMs",
|
|
25
|
+
"operationTimeoutMs",
|
|
26
|
+
], "BeatAPI");
|
|
27
|
+
const baseUrl = runtimeConfigString(config.baseUrl, "BeatAPI baseUrl");
|
|
28
|
+
if (baseUrl !== undefined) {
|
|
29
|
+
const url = new URL(baseUrl);
|
|
30
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
31
|
+
throw new Error("BeatAPI baseUrl must use HTTPS or loopback");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const apiKey = runtimeConfigCredentialRef(config.apiKey, "BeatAPI apiKey");
|
|
35
|
+
if (apiKey === undefined) throw new Error("BeatAPI apiKey CredentialRef is required");
|
|
36
|
+
const actionLimits = runtimeConfigActionLimits(config.actionLimits);
|
|
37
|
+
const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "BeatAPI defaultConcurrency");
|
|
38
|
+
const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "BeatAPI pollIntervalMs");
|
|
39
|
+
const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "BeatAPI requestTimeoutMs");
|
|
40
|
+
const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "BeatAPI operationTimeoutMs");
|
|
41
|
+
return {
|
|
42
|
+
endpoint: createBeatApiProvider({
|
|
43
|
+
instance: context.instance,
|
|
44
|
+
pool: context.pool,
|
|
45
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
46
|
+
apiKey,
|
|
47
|
+
...(defaultConcurrency === undefined ? {} : { defaultConcurrency }),
|
|
48
|
+
...(actionLimits === undefined ? {} : { actionLimits }),
|
|
49
|
+
...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
|
|
50
|
+
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
51
|
+
...(operationTimeoutMs === undefined ? {} : { operationTimeoutMs }),
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const hypitPackage = {
|
|
58
|
+
format: "hypit.node-package@1" as const,
|
|
59
|
+
hostFacets: [adapter],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default hypitPackage;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** BeatAPI's `{ error: { code, message, request_id, retry_after_seconds } }` envelope and terminal task errors. */
|
|
2
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
3
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
|
+
? value as Record<string, unknown> : undefined;
|
|
5
|
+
}
|
|
6
|
+
function text(value: unknown): string | undefined {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Responses may mention a hosted result URL. Keep the reason, not its access capability.
|
|
11
|
+
export function safeBeatApiReason(value: string): string {
|
|
12
|
+
return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BeatApiServiceError extends Error {
|
|
16
|
+
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BeatApiHttpError extends BeatApiServiceError {
|
|
20
|
+
constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
|
|
21
|
+
request: { readonly method: string; readonly path: string; readonly model?: string }) {
|
|
22
|
+
let body: Record<string, unknown> | undefined;
|
|
23
|
+
try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
|
|
24
|
+
const error = record(body?.error);
|
|
25
|
+
const code = text(error?.code) ?? "BEATAPI_HTTP_ERROR";
|
|
26
|
+
const reason = text(error?.message) ?? (error === undefined ? text(bodyText.slice(0, 2000)) : undefined);
|
|
27
|
+
const requestId = text(error?.request_id) ?? text(response.headers.get("x-request-id"));
|
|
28
|
+
const retryAfter = typeof error?.retry_after_seconds === "number" ? error.retry_after_seconds : undefined;
|
|
29
|
+
const facts = [
|
|
30
|
+
`BeatAPI HTTP ${status}`, code, `${request.method} ${request.path}`,
|
|
31
|
+
...(request.model === undefined ? [] : [`model=${request.model}`]),
|
|
32
|
+
...(requestId === undefined ? [] : [`request=${requestId}`]),
|
|
33
|
+
...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}s`]),
|
|
34
|
+
];
|
|
35
|
+
super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A terminal `failed` task; `undefined` otherwise. */
|
|
40
|
+
export function beatApiTaskFailure(task: Record<string, unknown>, id: string): BeatApiServiceError | undefined {
|
|
41
|
+
if (task.status !== "failed") return undefined;
|
|
42
|
+
const code = text(task.error_code) ?? "BEATAPI_TASK_FAILED";
|
|
43
|
+
const reason = text(task.error_message);
|
|
44
|
+
return new BeatApiServiceError(code,
|
|
45
|
+
`BeatAPI task ${id} failed; ${code}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ModuleRef } from "@hypit/protocol";
|
|
2
|
+
import type { GenerationWireMapping } from "@hypit/generation";
|
|
3
|
+
|
|
4
|
+
const SEEDANCE: ModuleRef = { name: "@hypit/seedance", version: "1" };
|
|
5
|
+
const MINIMAX: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
|
|
6
|
+
const GROK: ModuleRef = { name: "@hypit/grok-imagine", version: "1" };
|
|
7
|
+
const GPT_IMAGE: ModuleRef = { name: "@hypit/gpt-image", version: "1" };
|
|
8
|
+
const NANO_BANANA: ModuleRef = { name: "@hypit/nano-banana", version: "1" };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* BeatAPI model aliases and the body fields each one documents. One alias serves every input mode,
|
|
12
|
+
* so each Capability has a single route and the request shape alone decides which arrays travel.
|
|
13
|
+
*
|
|
14
|
+
* BeatAPI carries the opening and closing frames as one ordered `images` array rather than two
|
|
15
|
+
* fields. The mapping writes them to `first_frame` and `last_frame`, which routes.ts folds into
|
|
16
|
+
* that array; a mapping cannot write one wire field from two ports.
|
|
17
|
+
*/
|
|
18
|
+
const seedanceFields = {
|
|
19
|
+
prompt: { as: "value", field: "prompt" },
|
|
20
|
+
firstFrame: { as: "url", field: "first_frame", resourceFields: ["personReference"] },
|
|
21
|
+
lastFrame: { as: "url", field: "last_frame", resourceFields: ["personReference"] },
|
|
22
|
+
referenceImage: { as: "urlArray", field: "reference_images", resourceFields: ["personReference"] },
|
|
23
|
+
referenceVideo: { as: "urlArray", field: "reference_videos", resourceFields: ["personReference"] },
|
|
24
|
+
referenceAudio: { as: "urlArray", field: "reference_audios" },
|
|
25
|
+
duration: { as: "value", field: "duration" },
|
|
26
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
27
|
+
resolution: { as: "value", field: "resolution" },
|
|
28
|
+
generateAudio: { as: "value", field: "generate_audio" },
|
|
29
|
+
webSearch: { as: "value", field: "web_search" },
|
|
30
|
+
} as const satisfies GenerationWireMapping["fields"];
|
|
31
|
+
|
|
32
|
+
const seedance = (name: string): GenerationWireMapping => ({
|
|
33
|
+
capability: { module: SEEDANCE, name }, result: "video", routes: [{ model: name }], fields: seedanceFields,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const beatApiMappings: readonly GenerationWireMapping[] = [
|
|
37
|
+
seedance("seedance-2"),
|
|
38
|
+
seedance("seedance-2-fast"),
|
|
39
|
+
seedance("seedance-2-mini"),
|
|
40
|
+
seedance("seedance-2.5"),
|
|
41
|
+
{
|
|
42
|
+
capability: { module: MINIMAX, name: "minimax-h3" }, result: "video",
|
|
43
|
+
routes: [{ model: "minimax-h3" }],
|
|
44
|
+
fields: {
|
|
45
|
+
prompt: { as: "value", field: "prompt" },
|
|
46
|
+
firstFrame: { as: "url", field: "first_frame" },
|
|
47
|
+
lastFrame: { as: "url", field: "last_frame" },
|
|
48
|
+
referenceImage: { as: "urlArray", field: "reference_images" },
|
|
49
|
+
referenceVideo: { as: "urlArray", field: "reference_videos" },
|
|
50
|
+
referenceAudio: { as: "urlArray", field: "reference_audios" },
|
|
51
|
+
duration: { as: "value", field: "duration" },
|
|
52
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
53
|
+
resolution: { as: "value", field: "resolution" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
capability: { module: GROK, name: "grok-imagine-video-1.5-preview" }, result: "video",
|
|
58
|
+
routes: [{ model: "grok-imagine-video-1.5" }],
|
|
59
|
+
fields: {
|
|
60
|
+
prompt: { as: "value", field: "prompt" },
|
|
61
|
+
images: { as: "urlArray", field: "reference_images" },
|
|
62
|
+
duration: { as: "value", field: "duration" },
|
|
63
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
64
|
+
resolution: { as: "value", field: "resolution" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image",
|
|
69
|
+
routes: [{ model: "gpt-image-2" }],
|
|
70
|
+
fields: {
|
|
71
|
+
prompt: { as: "value", field: "prompt" },
|
|
72
|
+
images: { as: "urlArray", field: "images" },
|
|
73
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
74
|
+
resolution: { as: "value", field: "resolution" },
|
|
75
|
+
background: { as: "value", field: "background" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
...(["nano-banana-2", "nano-banana-pro"] as const).map((model) => ({
|
|
79
|
+
capability: { module: NANO_BANANA, name: model }, result: "image" as const,
|
|
80
|
+
routes: [{ model }],
|
|
81
|
+
fields: {
|
|
82
|
+
prompt: { as: "value" as const, field: "prompt" },
|
|
83
|
+
images: { as: "urlArray" as const, field: "images" },
|
|
84
|
+
aspectRatio: { as: "value" as const, field: "aspect_ratio" },
|
|
85
|
+
resolution: { as: "value" as const, field: "resolution" },
|
|
86
|
+
outputFormat: { as: "value" as const, field: "output_format" },
|
|
87
|
+
},
|
|
88
|
+
})),
|
|
89
|
+
];
|