@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { requestDeadline } from "@hypit/runtime-kit";
|
|
2
|
+
import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
|
|
3
|
+
import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
|
|
4
|
+
import type { GenerationArtifactUrlResolver } from "@hypit/generation";
|
|
5
|
+
import { canonicalize } from "@hypit/protocol";
|
|
6
|
+
import type { BlobRef, CapabilityRef } from "@hypit/protocol";
|
|
7
|
+
import { credentialRef } from "@hypit/runtime";
|
|
8
|
+
import type { CredentialRef, ResourceStore } from "@hypit/runtime";
|
|
9
|
+
import { beatApiRouteForCapability, beatApiRoutes } from "./routes.js";
|
|
10
|
+
import { BeatApiHttpError, BeatApiServiceError, beatApiTaskFailure } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
export const beatApiProviderModuleRef = { name: "@hypit/provider-beatapi", version: "1" } as const;
|
|
13
|
+
|
|
14
|
+
export type CreateBeatApiProviderOptions = {
|
|
15
|
+
readonly instance?: string;
|
|
16
|
+
readonly pool?: string;
|
|
17
|
+
readonly baseUrl?: string;
|
|
18
|
+
readonly apiKey?: CredentialRef;
|
|
19
|
+
readonly defaultConcurrency?: number;
|
|
20
|
+
readonly actionLimits?: import("@hypit/endpoint-kit").EndpointActionLimits;
|
|
21
|
+
readonly pollIntervalMs?: number;
|
|
22
|
+
readonly requestTimeoutMs?: number;
|
|
23
|
+
readonly operationTimeoutMs?: number;
|
|
24
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
25
|
+
/** Publish a referenced Resource at a URL the service can fetch; replaces the `/v1/files` upload. */
|
|
26
|
+
readonly publicAssetUrl?: (artifact: BlobRef, artifacts: ResourceStore, fields?: Readonly<Record<string, string | number | boolean>>) => Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Handle = {
|
|
30
|
+
readonly contract: "hypit.beatapi-operation@1";
|
|
31
|
+
readonly taskId: string;
|
|
32
|
+
readonly route: string;
|
|
33
|
+
readonly startedAt: number;
|
|
34
|
+
readonly urls?: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MB = 1_000_000;
|
|
38
|
+
|
|
39
|
+
/** Per-file sizes and containers `POST /v1/files` accepts, by media kind. */
|
|
40
|
+
const uploadLimits: Readonly<Record<string, { readonly bytes: number; readonly mediaTypes: readonly string[] }>> = {
|
|
41
|
+
image: { bytes: 50 * MB, mediaTypes: ["image/png", "image/jpeg", "image/jpg", "image/webp"] },
|
|
42
|
+
audio: { bytes: 50 * MB, mediaTypes: ["audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav", "audio/aac", "audio/mp4", "audio/x-m4a"] },
|
|
43
|
+
video: { bytes: 100 * MB, mediaTypes: ["video/mp4", "video/quicktime"] },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const extensions: Readonly<Record<string, string>> = {
|
|
47
|
+
"image/png": "png", "image/jpeg": "jpg", "image/jpg": "jpg", "image/webp": "webp",
|
|
48
|
+
"audio/mpeg": "mp3", "audio/mp3": "mp3", "audio/wav": "wav", "audio/x-wav": "wav",
|
|
49
|
+
"audio/aac": "aac", "audio/mp4": "m4a", "audio/x-m4a": "m4a",
|
|
50
|
+
"video/mp4": "mp4", "video/quicktime": "mov",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
|
|
54
|
+
function object(value: unknown, subject: string): Record<string, unknown> {
|
|
55
|
+
assert(value !== null && typeof value === "object" && !Array.isArray(value), `${subject} must be an object`);
|
|
56
|
+
return value as Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
function capabilityKey(capability: CapabilityRef): string { return `${capability.module.name}@${capability.module.version}#${capability.name}`; }
|
|
59
|
+
function apiBaseUrl(value: string): string {
|
|
60
|
+
let trimmed = value.trim();
|
|
61
|
+
while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
|
|
62
|
+
assert(trimmed.length > 0, "BeatAPI base URL is empty");
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
function apiKey(credentials: Readonly<Record<string, EndpointCredential>>): string {
|
|
66
|
+
const value = credentials.apiKey?.secret;
|
|
67
|
+
assert(typeof value === "string" && value.length > 0, "BeatAPI apiKey credential is unavailable; store a BeatAPI API key for this Endpoint");
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function httpsUrl(value: unknown, subject: string): string {
|
|
71
|
+
assert(typeof value === "string" && /^https:\/\//u.test(value), `${subject} has no HTTPS URL`);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function failureMessage(error: unknown): string {
|
|
75
|
+
return error instanceof Error ? error.message : String(error);
|
|
76
|
+
}
|
|
77
|
+
function failure(error: unknown): EndpointOutcome {
|
|
78
|
+
return { status: "failed", failure: { code: error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR", message: failureMessage(error) } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class BeatApiClient {
|
|
82
|
+
constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
|
|
83
|
+
async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
|
|
84
|
+
const deadline = requestDeadline(this.timeout);
|
|
85
|
+
try {
|
|
86
|
+
const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
|
|
87
|
+
...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
|
|
88
|
+
}));
|
|
89
|
+
const text = await deadline.wait(response.text());
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
|
|
92
|
+
throw new BeatApiHttpError(response.status, response, text, {
|
|
93
|
+
method: init.method ?? "GET", path, ...(typeof input?.model === "string" ? { model: input.model } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
let body: unknown;
|
|
97
|
+
try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`BeatAPI returned invalid JSON (${response.status})`); }
|
|
98
|
+
return object(object(body, "BeatAPI response").data, "BeatAPI response data");
|
|
99
|
+
} finally { deadline.finish(); }
|
|
100
|
+
}
|
|
101
|
+
/** Upload one referenced Resource and return the HTTPS URL the task request carries. */
|
|
102
|
+
async upload(artifact: BlobRef, resources: ResourceStore, key: string): Promise<string> {
|
|
103
|
+
const kind = artifact.mediaType.split("/", 1)[0] ?? "";
|
|
104
|
+
const limits = uploadLimits[kind];
|
|
105
|
+
assert(limits !== undefined, `BeatAPI accepts image, audio and video references, not ${artifact.mediaType}`);
|
|
106
|
+
assert(limits.mediaTypes.includes(artifact.mediaType),
|
|
107
|
+
`BeatAPI accepts ${limits.mediaTypes.join(", ")} for ${kind} references, not ${artifact.mediaType}`);
|
|
108
|
+
assert(artifact.size <= limits.bytes,
|
|
109
|
+
`BeatAPI accepts ${kind} references up to ${limits.bytes / MB} MB; ${artifact.resource} is ${artifact.size} bytes`);
|
|
110
|
+
const bytes = await resources.get(artifact.resource);
|
|
111
|
+
assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
|
|
112
|
+
const form = new FormData();
|
|
113
|
+
form.append("file", new Blob([new Uint8Array(bytes)], { type: artifact.mediaType }),
|
|
114
|
+
`${artifact.resource}.${extensions[artifact.mediaType] ?? "bin"}`);
|
|
115
|
+
form.append("purpose", "input");
|
|
116
|
+
const file = await this.json("/v1/files", key, { method: "POST", body: form });
|
|
117
|
+
return httpsUrl(file.url, "BeatAPI file upload");
|
|
118
|
+
}
|
|
119
|
+
async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
|
|
120
|
+
const deadline = requestDeadline(this.timeout);
|
|
121
|
+
try {
|
|
122
|
+
const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
|
|
123
|
+
if (!response.ok) throw new Error(`BeatAPI asset returned HTTP ${response.status}`);
|
|
124
|
+
return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
|
|
125
|
+
} finally { deadline.finish(); }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolverFor(client: BeatApiClient, context: EndpointInvocationContext, publicAssetUrl: CreateBeatApiProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
|
|
130
|
+
const resolved = new Map<string, Promise<string>>();
|
|
131
|
+
return (artifact, fields) => {
|
|
132
|
+
const existing = resolved.get(artifact.resource);
|
|
133
|
+
if (existing !== undefined) return existing;
|
|
134
|
+
const promise = publicAssetUrl === undefined
|
|
135
|
+
? client.upload(artifact, context.resources, apiKey(context.credentials))
|
|
136
|
+
: publicAssetUrl(artifact, context.resources, fields);
|
|
137
|
+
resolved.set(artifact.resource, promise);
|
|
138
|
+
return promise;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Statuses a queued image or video task passes through before it is terminal. */
|
|
143
|
+
const pendingStatuses: readonly string[] = ["queued", "processing"];
|
|
144
|
+
|
|
145
|
+
function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreateBeatApiProviderOptions["publicAssetUrl"]): AsyncEndpoint {
|
|
146
|
+
return {
|
|
147
|
+
async start(context) {
|
|
148
|
+
try {
|
|
149
|
+
const route = beatApiRouteForCapability(context.need.capability);
|
|
150
|
+
assert(route !== undefined, "BeatAPI does not implement this exact capability");
|
|
151
|
+
const request = route.prepare(context.need.constraints);
|
|
152
|
+
await context.reportProgress?.({ phase: `Preparing BeatAPI request: ${request.model}` });
|
|
153
|
+
let body: Record<string, unknown>;
|
|
154
|
+
try {
|
|
155
|
+
body = await request.compile(resolverFor(client, context, publicAssetUrl));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new BeatApiServiceError(error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR",
|
|
158
|
+
`BeatAPI request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
|
|
159
|
+
}
|
|
160
|
+
await context.reportProgress?.({ phase: `Submitting BeatAPI request: ${request.model}` });
|
|
161
|
+
const response = await client.json(`/v1/${request.media}s/tasks`, apiKey(context.credentials), {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json", "idempotency-key": context.operation },
|
|
164
|
+
body: JSON.stringify(body),
|
|
165
|
+
});
|
|
166
|
+
assert(typeof response.id === "string" && response.id.length > 0, "BeatAPI response has no task id");
|
|
167
|
+
const handle: Handle = { contract: "hypit.beatapi-operation@1", taskId: response.id, route: route.key, startedAt: Date.now() };
|
|
168
|
+
const receipt = { id: handle.taskId };
|
|
169
|
+
await context.checkpoint?.({ handle: canonicalize(handle), receipt });
|
|
170
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "submitted" }), receipt };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return failure(error);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
async poll(context) {
|
|
176
|
+
try {
|
|
177
|
+
const handle = object(context.handle, "BeatAPI handle") as unknown as Handle;
|
|
178
|
+
const route = beatApiRouteForCapability(context.need.capability);
|
|
179
|
+
assert(route !== undefined && handle.contract === "hypit.beatapi-operation@1" && handle.route === route.key, "BeatAPI handle is invalid");
|
|
180
|
+
const receipt = { id: handle.taskId };
|
|
181
|
+
if (Date.now() - handle.startedAt > maxOperationMs) {
|
|
182
|
+
return { status: "failed", receipt, failure: { code: "BEATAPI_OPERATION_TIMEOUT", message: `BeatAPI task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
|
|
183
|
+
}
|
|
184
|
+
const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
|
|
185
|
+
const status = String(task.status);
|
|
186
|
+
if (pendingStatuses.includes(status)) {
|
|
187
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
|
|
188
|
+
}
|
|
189
|
+
const rejected = beatApiTaskFailure(task, handle.taskId);
|
|
190
|
+
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
191
|
+
assert(status === "succeeded", `BeatAPI returned unknown task status ${status}`);
|
|
192
|
+
const media = object(task.output, "BeatAPI task output").media;
|
|
193
|
+
assert(Array.isArray(media) && media.length > 0, "BeatAPI task succeeded without output media");
|
|
194
|
+
const urls = media.map((item, index) => httpsUrl(object(item, `BeatAPI output ${index + 1}`).url, `BeatAPI output ${index + 1}`));
|
|
195
|
+
return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return failure(error);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async collect(context) {
|
|
201
|
+
try {
|
|
202
|
+
const handle = object(context.handle, "BeatAPI handle") as unknown as Handle;
|
|
203
|
+
const route = beatApiRouteForCapability(context.need.capability);
|
|
204
|
+
assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "BeatAPI collection route differs");
|
|
205
|
+
await context.reportProgress?.({ phase: "Receiving generated files" });
|
|
206
|
+
const blobs: BlobRef[] = [];
|
|
207
|
+
for (const url of handle.urls) {
|
|
208
|
+
const downloaded = await client.download(url);
|
|
209
|
+
blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
|
|
210
|
+
}
|
|
211
|
+
return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return failure(error);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createBeatApiProvider(options: CreateBeatApiProviderOptions = {}) {
|
|
220
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
|
|
221
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
|
|
222
|
+
for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
|
|
223
|
+
assert(Number.isSafeInteger(value) && value > 0, `BeatAPI ${name} must be a positive integer`);
|
|
224
|
+
}
|
|
225
|
+
const client = new BeatApiClient(apiBaseUrl(options.baseUrl ?? "https://api.beatapi.io"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
|
|
226
|
+
const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
|
|
227
|
+
return defineEndpointPackage({
|
|
228
|
+
module: beatApiProviderModuleRef, facet: "gateway", instance: options.instance ?? "beatapi.default", pool: options.pool ?? options.instance ?? "beatapi.default",
|
|
229
|
+
pricing: { kind: "page", url: "https://docs.beatapi.io/pricing" },
|
|
230
|
+
credentials: { apiKey: options.apiKey ?? credentialRef("os", "beatapi.api-key") },
|
|
231
|
+
credentialInputs: { apiKey: { label: "BeatAPI API key" } },
|
|
232
|
+
defaultConcurrency: options.defaultConcurrency ?? 4,
|
|
233
|
+
...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
|
|
234
|
+
capabilities: beatApiRoutes.map((route) => ({
|
|
235
|
+
capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
|
|
236
|
+
})),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compileWireRequest,
|
|
3
|
+
selectWireModelForRequest,
|
|
4
|
+
generationTypes,
|
|
5
|
+
sealGeneratedImageSet,
|
|
6
|
+
sealGeneratedVideoSet,
|
|
7
|
+
} from "@hypit/generation";
|
|
8
|
+
import type { GenerationArtifactUrlResolver, GenerationRequest, GenerationWireMapping } from "@hypit/generation";
|
|
9
|
+
import { canonicalize } from "@hypit/protocol";
|
|
10
|
+
import type { BlobRef, CapabilityRef, CanonicalValue, StoredValue, TypeRef } from "@hypit/protocol";
|
|
11
|
+
import type { EndpointRequest, EndpointSupport } from "@hypit/endpoint-kit";
|
|
12
|
+
import { beatApiMappings } from "./mapping.js";
|
|
13
|
+
|
|
14
|
+
export type BeatApiPreparedRequest = {
|
|
15
|
+
readonly model: string;
|
|
16
|
+
readonly media: "image" | "video";
|
|
17
|
+
readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BeatApiRoute = GenerationWireMapping & {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly returns: TypeRef;
|
|
23
|
+
readonly supports: (request: EndpointRequest) => EndpointSupport;
|
|
24
|
+
readonly prepare: (constraints: CanonicalValue) => BeatApiPreparedRequest;
|
|
25
|
+
readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function scalar(request: GenerationRequest, port: string): string | number | boolean | undefined {
|
|
29
|
+
const value = request.ports[port]?.[0];
|
|
30
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
function count(request: GenerationRequest, port: string): number {
|
|
33
|
+
return request.ports[port]?.length ?? 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Ratios the Nano Banana 2 model package offers that BeatAPI's schema for it does not list. */
|
|
37
|
+
const NANO_BANANA_2_UNAVAILABLE: readonly string[] = ["1:4", "4:1", "1:8", "8:1"];
|
|
38
|
+
|
|
39
|
+
/** BeatAPI's documented input ranges per model, checked before any reference is uploaded. */
|
|
40
|
+
function rejection(mapping: GenerationWireMapping, request: GenerationRequest): string | undefined {
|
|
41
|
+
const { name } = mapping.capability;
|
|
42
|
+
const model = selectWireModelForRequest(mapping, request);
|
|
43
|
+
const ratio = scalar(request, "aspectRatio");
|
|
44
|
+
const resolution = scalar(request, "resolution");
|
|
45
|
+
if (mapping.capability.module.name === "@hypit/seedance") {
|
|
46
|
+
if (scalar(request, "webSearch") === true) return `BeatAPI ${model} has no web_search field`;
|
|
47
|
+
if (name === "seedance-2-mini" && scalar(request, "generateAudio") === true) {
|
|
48
|
+
return "BeatAPI seedance-2-mini renders no generated audio";
|
|
49
|
+
}
|
|
50
|
+
if (name === "seedance-2" && resolution === "1080p" && count(request, "referenceImage") > 0) {
|
|
51
|
+
return "BeatAPI seedance-2 does not render 1080p with reference images";
|
|
52
|
+
}
|
|
53
|
+
if (name === "seedance-2.5" && scalar(request, "duration") === -1) {
|
|
54
|
+
return "BeatAPI seedance-2.5 takes a duration of 4 to 30 seconds, not automatic";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (name === "minimax-h3" && count(request, "lastFrame") > 0 && count(request, "firstFrame") === 0) {
|
|
58
|
+
return "BeatAPI minimax-h3 orders its images first then last, so a last frame needs a first frame";
|
|
59
|
+
}
|
|
60
|
+
if (name === "grok-imagine-video-1.5-preview" && resolution === "1080p" && count(request, "images") > 1) {
|
|
61
|
+
return "BeatAPI grok-imagine-video-1.5 accepts at most one image at 1080p";
|
|
62
|
+
}
|
|
63
|
+
if (name === "gpt-image-2" && scalar(request, "background") !== undefined) {
|
|
64
|
+
return "BeatAPI gpt-image-2 has no background field";
|
|
65
|
+
}
|
|
66
|
+
if (name === "nano-banana-2") {
|
|
67
|
+
if (count(request, "images") > 10) return "BeatAPI nano-banana-2 accepts up to ten reference images";
|
|
68
|
+
if (NANO_BANANA_2_UNAVAILABLE.includes(String(ratio))) {
|
|
69
|
+
return `BeatAPI nano-banana-2 does not render ${String(ratio)}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Fields the model package states that BeatAPI names differently, spells differently or omits. */
|
|
76
|
+
function normalize(mapping: GenerationWireMapping, input: Record<string, unknown>): Record<string, unknown> {
|
|
77
|
+
const { name } = mapping.capability;
|
|
78
|
+
// BeatAPI carries the opening and closing frames as one ordered array.
|
|
79
|
+
const frames = [input.first_frame, input.last_frame].filter((item): item is string => typeof item === "string");
|
|
80
|
+
delete input.first_frame;
|
|
81
|
+
delete input.last_frame;
|
|
82
|
+
if (frames.length > 0) input.images = frames;
|
|
83
|
+
if (input.web_search === false) delete input.web_search;
|
|
84
|
+
if (name === "seedance-2-mini" && input.generate_audio === false) delete input.generate_audio;
|
|
85
|
+
if (name === "nano-banana-2" && input.output_format === "jpg") input.output_format = "jpeg";
|
|
86
|
+
return input;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function capabilityKey(capability: CapabilityRef): string {
|
|
90
|
+
return `${capability.module.name}@${capability.module.version}#${capability.name}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const beatApiRoutes: readonly BeatApiRoute[] = beatApiMappings.map((mapping) => ({
|
|
94
|
+
...mapping,
|
|
95
|
+
key: capabilityKey(mapping.capability),
|
|
96
|
+
returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
|
|
97
|
+
supports: (request) => {
|
|
98
|
+
const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
|
|
99
|
+
return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
|
|
100
|
+
},
|
|
101
|
+
prepare: (constraints) => {
|
|
102
|
+
const request = constraints as unknown as GenerationRequest;
|
|
103
|
+
const reason = rejection(mapping, request);
|
|
104
|
+
if (reason !== undefined) throw new Error(reason);
|
|
105
|
+
const model = selectWireModelForRequest(mapping, request);
|
|
106
|
+
return {
|
|
107
|
+
model,
|
|
108
|
+
media: mapping.result === "image" ? "image" : "video",
|
|
109
|
+
compile: async (resolve) => ({
|
|
110
|
+
model,
|
|
111
|
+
...normalize(mapping, (await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
packageResult: (artifacts) => ({
|
|
116
|
+
kind: "inline",
|
|
117
|
+
value: canonicalize(mapping.result === "image"
|
|
118
|
+
? sealGeneratedImageSet({ images: artifacts })
|
|
119
|
+
: sealGeneratedVideoSet({ videos: artifacts })),
|
|
120
|
+
}),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const byCapability = new Map(beatApiRoutes.map((route) => [route.key, route]));
|
|
124
|
+
|
|
125
|
+
export function beatApiRouteForCapability(capability: CapabilityRef): BeatApiRoute | undefined {
|
|
126
|
+
return byCapability.get(capabilityKey(capability));
|
|
127
|
+
}
|
|
@@ -38,9 +38,7 @@ Service limits this Provider reports as unsupported before submitting:
|
|
|
38
38
|
- Grok Imagine renders 480p or 720p; `grok-imagine-1.5/image-to-video` animates exactly one image.
|
|
39
39
|
|
|
40
40
|
Seedance visual references require `person-reference`; the Provider accepts the declaration and
|
|
41
|
-
transmits nothing for it, since HiAPI has no field for it.
|
|
42
|
-
videos that contain a real human face; HiAPI offers no way to register authorized portrait material,
|
|
43
|
-
so such a request fails with the service's moderation error.
|
|
41
|
+
transmits nothing for it, since HiAPI has no field for it.
|
|
44
42
|
|
|
45
43
|
Reference images and audio travel inline as `data:` URLs, which HiAPI documents for its Seedance
|
|
46
44
|
inputs, within the per-file sizes each model page states: 30 MB images and 15 MB audio for
|
|
@@ -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: {
|
|
@@ -27,8 +27,6 @@ The Seedance endpoints add `generate_audio`. Monid documents no web search field
|
|
|
27
27
|
`web-search="true"` is unsupported, and Seedance 2.5 frame mode (`first-frame` present) requires
|
|
28
28
|
`aspect-ratio="adaptive"`. Seedance visual references require `person-reference`; the Provider
|
|
29
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
30
|
|
|
33
31
|
MiniMax H3 names its model in the body and takes neither of those two fields. The endpoint requires
|
|
34
32
|
a resolution, so a request that states none is sent at `2K`, the resolution the HypiHub Provider
|
|
@@ -722,12 +722,27 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
722
722
|
response.end();
|
|
723
723
|
return;
|
|
724
724
|
}
|
|
725
|
-
|
|
725
|
+
const size = file.bytes.byteLength;
|
|
726
|
+
let range: BuildResultFileRange | undefined;
|
|
727
|
+
try {
|
|
728
|
+
range = requestedByteRange(request.headers.range, size);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (!(error instanceof RangeError)) throw error;
|
|
731
|
+
response.statusCode = 416;
|
|
732
|
+
response.setHeader("content-range", `bytes */${size}`);
|
|
733
|
+
response.end();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
response.statusCode = range === undefined ? 200 : 206;
|
|
726
737
|
response.setHeader("content-type", file.mediaType);
|
|
738
|
+
response.setHeader("content-length", String(range === undefined ? size : range.endExclusive - range.start));
|
|
727
739
|
response.setHeader("cache-control", "no-store");
|
|
728
740
|
response.setHeader("accept-ranges", "bytes");
|
|
741
|
+
if (range !== undefined) {
|
|
742
|
+
response.setHeader("content-range", `bytes ${range.start}-${range.endExclusive - 1}/${size}`);
|
|
743
|
+
}
|
|
729
744
|
if (request.method === "HEAD") response.end();
|
|
730
|
-
else response.end(
|
|
745
|
+
else response.end(range === undefined ? file.bytes : file.bytes.subarray(range.start, range.endExclusive));
|
|
731
746
|
return;
|
|
732
747
|
}
|
|
733
748
|
if (url.pathname === "/__studio/artifact") {
|
|
@@ -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();
|