Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
521
extensions/pixverse/video-generation-provider.ts
Normal file
521
extensions/pixverse/video-generation-provider.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
// Pixverse provider module implements model/runtime integration.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
createProviderOperationDeadline,
|
||||
pollProviderOperationJson,
|
||||
postJsonRequest,
|
||||
postMultipartRequest,
|
||||
readProviderJsonResponse,
|
||||
resolveProviderOperationTimeoutMs,
|
||||
resolveProviderHttpRequestConfig,
|
||||
sanitizeConfiguredModelProviderRequest,
|
||||
type ProviderOperationDeadline,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
asFiniteNumber,
|
||||
asSafeIntegerInRange,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
VideoGenerationSourceAsset,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
import {
|
||||
DEFAULT_PIXVERSE_MODEL_ID,
|
||||
DEFAULT_PIXVERSE_REGION,
|
||||
PIXVERSE_BASE_URL_BY_REGION,
|
||||
PIXVERSE_PROVIDER_ID,
|
||||
type PixVerseApiRegion,
|
||||
} from "./constants.js";
|
||||
|
||||
const DEFAULT_PIXVERSE_BASE_URL = PIXVERSE_BASE_URL_BY_REGION[DEFAULT_PIXVERSE_REGION];
|
||||
const DEFAULT_PIXVERSE_QUALITY = "540p";
|
||||
const DEFAULT_TIMEOUT_MS = 300_000;
|
||||
const POLL_INTERVAL_MS = 5_000;
|
||||
const MAX_POLL_ATTEMPTS = 180;
|
||||
const MAX_DURATION_SECONDS = 15;
|
||||
const PIXVERSE_SEED_MAX = 2_147_483_647;
|
||||
const PIXVERSE_VIDEO_MODELS = ["v6", "c1"] as const;
|
||||
const PIXVERSE_TEXT_ASPECT_RATIOS = [
|
||||
"16:9",
|
||||
"4:3",
|
||||
"1:1",
|
||||
"3:4",
|
||||
"9:16",
|
||||
"2:3",
|
||||
"3:2",
|
||||
"21:9",
|
||||
] as const;
|
||||
const PIXVERSE_QUALITIES = ["360p", "540p", "720p", "1080p"] as const;
|
||||
|
||||
type PixVerseEnvelope<T> = {
|
||||
ErrCode?: unknown;
|
||||
ErrMsg?: unknown;
|
||||
Resp?: T;
|
||||
};
|
||||
|
||||
type PixVerseUploadImageResponse = {
|
||||
img_id?: unknown;
|
||||
img_url?: unknown;
|
||||
};
|
||||
|
||||
type PixVerseVideoCreateResponse = {
|
||||
video_id?: unknown;
|
||||
};
|
||||
|
||||
type PixVerseVideoResultResponse = {
|
||||
id?: unknown;
|
||||
status?: unknown;
|
||||
url?: unknown;
|
||||
outputWidth?: unknown;
|
||||
outputHeight?: unknown;
|
||||
seed?: unknown;
|
||||
size?: unknown;
|
||||
};
|
||||
|
||||
function resolvePixVerseBaseUrl(req: VideoGenerationRequest): string {
|
||||
const provider = req.cfg?.models?.providers?.[PIXVERSE_PROVIDER_ID];
|
||||
const configuredBaseUrl = normalizeOptionalString(provider?.baseUrl);
|
||||
if (configuredBaseUrl) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
const region = resolvePixVerseApiRegion(provider?.region);
|
||||
return PIXVERSE_BASE_URL_BY_REGION[region];
|
||||
}
|
||||
|
||||
function resolvePixVerseApiRegion(value: unknown): PixVerseApiRegion {
|
||||
const region = normalizeOptionalString(value)?.toLowerCase();
|
||||
switch (region) {
|
||||
case "cn":
|
||||
case "china":
|
||||
case "mainland":
|
||||
case "pai":
|
||||
return "cn";
|
||||
case "global":
|
||||
case "intl":
|
||||
case "international":
|
||||
case undefined:
|
||||
return DEFAULT_PIXVERSE_REGION;
|
||||
default:
|
||||
throw new Error(`Unsupported PixVerse API region "${region}". Use "international" or "cn".`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePixVerseModel(model: string | undefined): string {
|
||||
const normalized = normalizeOptionalString(model)?.replace(/^pixverse\//iu, "");
|
||||
return normalized?.toLowerCase() || DEFAULT_PIXVERSE_MODEL_ID;
|
||||
}
|
||||
|
||||
function resolvePixVerseQuality(req: VideoGenerationRequest): string {
|
||||
const optionQuality =
|
||||
normalizeOptionalString(req.providerOptions?.quality) ??
|
||||
normalizeOptionalString(req.resolution);
|
||||
const requested = optionQuality ?? normalizeOptionalString(req.size);
|
||||
if (!requested) {
|
||||
return DEFAULT_PIXVERSE_QUALITY;
|
||||
}
|
||||
const normalized = requested.toLowerCase() === "480p" ? "540p" : requested.toLowerCase();
|
||||
return PIXVERSE_QUALITIES.includes(normalized as (typeof PIXVERSE_QUALITIES)[number])
|
||||
? normalized
|
||||
: DEFAULT_PIXVERSE_QUALITY;
|
||||
}
|
||||
|
||||
function resolvePixVerseDurationSeconds(value: number | undefined): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return 5;
|
||||
}
|
||||
return Math.max(1, Math.min(MAX_DURATION_SECONDS, Math.round(value)));
|
||||
}
|
||||
|
||||
function appendOptionalNumber(body: Record<string, unknown>, key: string, value: unknown): void {
|
||||
const numberValue = asFiniteNumber(value);
|
||||
if (numberValue != null) {
|
||||
body[key] = numberValue;
|
||||
}
|
||||
}
|
||||
|
||||
function appendOptionalInt32Seed(body: Record<string, unknown>, value: unknown): void {
|
||||
const seed = asSafeIntegerInRange(value, { min: 0, max: PIXVERSE_SEED_MAX });
|
||||
if (seed !== undefined) {
|
||||
body.seed = seed;
|
||||
}
|
||||
}
|
||||
|
||||
function readPixVerseSeed(value: unknown): number | undefined {
|
||||
return asSafeIntegerInRange(value, { min: 0, max: PIXVERSE_SEED_MAX });
|
||||
}
|
||||
|
||||
function appendOptionalString(body: Record<string, unknown>, key: string, value: unknown): void {
|
||||
const stringValue = normalizeOptionalString(value);
|
||||
if (stringValue) {
|
||||
body[key] = stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
function buildPixVerseHeaders(headers: Headers, contentType?: string): Headers {
|
||||
const next = new Headers(headers);
|
||||
next.set("Ai-trace-id", randomUUID());
|
||||
if (contentType) {
|
||||
next.set("Content-Type", contentType);
|
||||
} else {
|
||||
next.delete("Content-Type");
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function readPixVerseSuccess<T>(payload: PixVerseEnvelope<T>, label: string): T {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
throw new Error(`${label}: malformed JSON response`);
|
||||
}
|
||||
const code = asFiniteNumber(payload.ErrCode);
|
||||
if (code !== 0) {
|
||||
const message = normalizeOptionalString(payload.ErrMsg) ?? `ErrCode ${String(payload.ErrCode)}`;
|
||||
throw new Error(`${label}: ${message}`);
|
||||
}
|
||||
if (payload.Resp === undefined || payload.Resp === null) {
|
||||
throw new Error(`${label}: response missing Resp`);
|
||||
}
|
||||
return payload.Resp;
|
||||
}
|
||||
|
||||
// Reads a PixVerse JSON response through the shared provider JSON reader so a
|
||||
// provider that streams an unbounded body cannot force the runtime to buffer the
|
||||
// whole payload before parsing it on the success path. The shared helper applies
|
||||
// the established 16 MiB provider JSON cap and the standard malformed-JSON
|
||||
// wrapping; PixVerse envelope validation stays local via readPixVerseSuccess.
|
||||
async function readPixVerseJson<T>(response: Response, label: string): Promise<T> {
|
||||
const payload = await readProviderJsonResponse(response, label);
|
||||
return readPixVerseSuccess(payload as PixVerseEnvelope<T>, label);
|
||||
}
|
||||
|
||||
function readPixVerseVideoId(payload: PixVerseVideoCreateResponse): number {
|
||||
const videoId = asSafeIntegerInRange(payload.video_id, { min: 0 });
|
||||
if (videoId == null) {
|
||||
throw new Error("PixVerse video generation response missing video_id");
|
||||
}
|
||||
return videoId;
|
||||
}
|
||||
|
||||
function readPixVerseImageId(payload: PixVerseUploadImageResponse): number {
|
||||
const imageId = asSafeIntegerInRange(payload.img_id, { min: 0 });
|
||||
if (imageId == null) {
|
||||
throw new Error("PixVerse image upload response missing img_id");
|
||||
}
|
||||
return imageId;
|
||||
}
|
||||
|
||||
function readPixVerseStatus(payload: PixVerseVideoResultResponse): number {
|
||||
const status = asSafeIntegerInRange(payload.status, { min: 0 });
|
||||
if (status == null) {
|
||||
throw new Error("PixVerse video status response missing status");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function buildUploadImageForm(asset: VideoGenerationSourceAsset): FormData {
|
||||
const form = new FormData();
|
||||
const url = normalizeOptionalString(asset.url);
|
||||
if (url) {
|
||||
form.set("image_url", url);
|
||||
return form;
|
||||
}
|
||||
if (!asset.buffer) {
|
||||
throw new Error("PixVerse image-to-video input is missing image data.");
|
||||
}
|
||||
const mimeType = normalizeOptionalString(asset.mimeType) ?? "image/png";
|
||||
const extension = extensionForMime(mimeType)?.slice(1) ?? "png";
|
||||
const fileName = normalizeOptionalString(asset.fileName) ?? `image.${extension}`;
|
||||
const bytes = new Uint8Array(asset.buffer.byteLength);
|
||||
bytes.set(asset.buffer);
|
||||
form.set("image", new File([bytes], fileName, { type: mimeType }));
|
||||
return form;
|
||||
}
|
||||
|
||||
function buildVideoBody(
|
||||
req: VideoGenerationRequest,
|
||||
model: string,
|
||||
imageId?: number,
|
||||
): Record<string, unknown> {
|
||||
const options = req.providerOptions ?? {};
|
||||
const body: Record<string, unknown> = {
|
||||
duration: resolvePixVerseDurationSeconds(req.durationSeconds),
|
||||
model,
|
||||
prompt: req.prompt,
|
||||
quality: resolvePixVerseQuality(req),
|
||||
};
|
||||
if (imageId !== undefined) {
|
||||
body.img_id = imageId;
|
||||
body.motion_mode =
|
||||
normalizeOptionalString(options.motion_mode) ??
|
||||
normalizeOptionalString(options.motionMode) ??
|
||||
"normal";
|
||||
} else {
|
||||
body.aspect_ratio = normalizeOptionalString(req.aspectRatio) ?? "16:9";
|
||||
}
|
||||
appendOptionalString(
|
||||
body,
|
||||
"negative_prompt",
|
||||
normalizeOptionalString(options.negative_prompt) ??
|
||||
normalizeOptionalString(options.negativePrompt),
|
||||
);
|
||||
appendOptionalString(
|
||||
body,
|
||||
"camera_movement",
|
||||
normalizeOptionalString(options.camera_movement) ??
|
||||
normalizeOptionalString(options.cameraMovement),
|
||||
);
|
||||
appendOptionalNumber(
|
||||
body,
|
||||
"template_id",
|
||||
asFiniteNumber(options.template_id) ?? asFiniteNumber(options.templateId),
|
||||
);
|
||||
appendOptionalInt32Seed(body, options.seed);
|
||||
if (req.audio !== undefined) {
|
||||
body.generate_audio_switch = req.audio;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function readPixVerseFailureMessage(payload: PixVerseVideoResultResponse): string | undefined {
|
||||
switch (readPixVerseStatus(payload)) {
|
||||
case 7:
|
||||
return "PixVerse video generation failed content moderation";
|
||||
case 8:
|
||||
return "PixVerse video generation failed";
|
||||
case 6:
|
||||
return "PixVerse video generation was deleted before completion";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollPixVerseVideo(params: {
|
||||
videoId: number;
|
||||
baseUrl: string;
|
||||
deadline: ProviderOperationDeadline;
|
||||
fetchFn: typeof fetch;
|
||||
allowPrivateNetwork: boolean;
|
||||
dispatcherPolicy?: Parameters<typeof postJsonRequest>[0]["dispatcherPolicy"];
|
||||
headers: Headers;
|
||||
}): Promise<PixVerseVideoResultResponse> {
|
||||
const readResult = (payload: PixVerseEnvelope<PixVerseVideoResultResponse>) =>
|
||||
readPixVerseSuccess(payload, "PixVerse video status request failed");
|
||||
const payload = await pollProviderOperationJson<PixVerseEnvelope<PixVerseVideoResultResponse>>({
|
||||
url: `${params.baseUrl}/video/result/${params.videoId}`,
|
||||
headers: () => buildPixVerseHeaders(params.headers),
|
||||
deadline: params.deadline,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
fetchFn: params.fetchFn,
|
||||
maxAttempts: MAX_POLL_ATTEMPTS,
|
||||
pollIntervalMs: POLL_INTERVAL_MS,
|
||||
requestFailedMessage: "PixVerse video status request failed",
|
||||
timeoutMessage: `PixVerse video generation task ${params.videoId} did not finish in time`,
|
||||
isComplete: (candidate) => readPixVerseStatus(readResult(candidate)) === 1,
|
||||
getFailureMessage: (candidate) => readPixVerseFailureMessage(readResult(candidate)),
|
||||
allowPrivateNetwork: params.allowPrivateNetwork,
|
||||
dispatcherPolicy: params.dispatcherPolicy,
|
||||
});
|
||||
return readResult(payload);
|
||||
}
|
||||
|
||||
function extractPixVerseVideo(payload: PixVerseVideoResultResponse): GeneratedVideoAsset {
|
||||
const url = normalizeOptionalString(payload.url);
|
||||
if (!url) {
|
||||
throw new Error("PixVerse video generation completed without output URL");
|
||||
}
|
||||
return {
|
||||
url,
|
||||
mimeType: "video/mp4",
|
||||
fileName: "video-1.mp4",
|
||||
metadata: {
|
||||
sourceUrl: url,
|
||||
outputWidth: asFiniteNumber(payload.outputWidth),
|
||||
outputHeight: asFiniteNumber(payload.outputHeight),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider {
|
||||
return {
|
||||
id: PIXVERSE_PROVIDER_ID,
|
||||
label: "PixVerse",
|
||||
defaultModel: DEFAULT_PIXVERSE_MODEL_ID,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
models: [...PIXVERSE_VIDEO_MODELS],
|
||||
isConfigured: ({ agentDir }) =>
|
||||
isProviderApiKeyConfigured({
|
||||
provider: PIXVERSE_PROVIDER_ID,
|
||||
agentDir,
|
||||
}),
|
||||
capabilities: {
|
||||
generate: {
|
||||
maxVideos: 1,
|
||||
maxDurationSeconds: MAX_DURATION_SECONDS,
|
||||
supportedDurationSeconds: Array.from(
|
||||
{ length: MAX_DURATION_SECONDS },
|
||||
(_, index) => index + 1,
|
||||
),
|
||||
aspectRatios: [...PIXVERSE_TEXT_ASPECT_RATIOS],
|
||||
resolutions: ["360P", "540P", "720P", "1080P"],
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
providerOptions: {
|
||||
seed: "number",
|
||||
negative_prompt: "string",
|
||||
negativePrompt: "string",
|
||||
quality: "string",
|
||||
camera_movement: "string",
|
||||
cameraMovement: "string",
|
||||
template_id: "number",
|
||||
templateId: "number",
|
||||
},
|
||||
},
|
||||
imageToVideo: {
|
||||
enabled: true,
|
||||
maxVideos: 1,
|
||||
maxInputImages: 1,
|
||||
maxDurationSeconds: MAX_DURATION_SECONDS,
|
||||
supportedDurationSeconds: Array.from(
|
||||
{ length: MAX_DURATION_SECONDS },
|
||||
(_, index) => index + 1,
|
||||
),
|
||||
resolutions: ["360P", "540P", "720P", "1080P"],
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
providerOptions: {
|
||||
seed: "number",
|
||||
negative_prompt: "string",
|
||||
negativePrompt: "string",
|
||||
quality: "string",
|
||||
motion_mode: "string",
|
||||
motionMode: "string",
|
||||
camera_movement: "string",
|
||||
cameraMovement: "string",
|
||||
template_id: "number",
|
||||
templateId: "number",
|
||||
},
|
||||
},
|
||||
videoToVideo: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
async generateVideo(req) {
|
||||
if ((req.inputVideos?.length ?? 0) > 0) {
|
||||
throw new Error("PixVerse video generation does not support video reference inputs.");
|
||||
}
|
||||
if ((req.inputImages?.length ?? 0) > 1) {
|
||||
throw new Error("PixVerse image-to-video supports at most one input image.");
|
||||
}
|
||||
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
provider: PIXVERSE_PROVIDER_ID,
|
||||
cfg: req.cfg,
|
||||
agentDir: req.agentDir,
|
||||
store: req.authStore,
|
||||
});
|
||||
if (!auth.apiKey) {
|
||||
throw new Error("PixVerse API key missing");
|
||||
}
|
||||
|
||||
const model = normalizePixVerseModel(req.model);
|
||||
const fetchFn = fetch;
|
||||
const providerConfig = req.cfg?.models?.providers?.[PIXVERSE_PROVIDER_ID];
|
||||
const deadline = createProviderOperationDeadline({
|
||||
timeoutMs: req.timeoutMs,
|
||||
label: "PixVerse video generation",
|
||||
});
|
||||
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
|
||||
resolveProviderHttpRequestConfig({
|
||||
baseUrl: resolvePixVerseBaseUrl(req),
|
||||
defaultBaseUrl: DEFAULT_PIXVERSE_BASE_URL,
|
||||
request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
|
||||
defaultHeaders: {
|
||||
"API-KEY": auth.apiKey,
|
||||
},
|
||||
provider: PIXVERSE_PROVIDER_ID,
|
||||
capability: "video",
|
||||
transport: "http",
|
||||
});
|
||||
|
||||
const image = req.inputImages?.[0];
|
||||
let imageId: number | undefined;
|
||||
if (image) {
|
||||
const upload = await postMultipartRequest({
|
||||
url: `${baseUrl}/image/upload`,
|
||||
headers: buildPixVerseHeaders(headers),
|
||||
body: buildUploadImageForm(image),
|
||||
timeoutMs: resolveProviderOperationTimeoutMs({
|
||||
deadline,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
}),
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(upload.response, "PixVerse image upload failed");
|
||||
imageId = readPixVerseImageId(
|
||||
await readPixVerseJson<PixVerseUploadImageResponse>(
|
||||
upload.response,
|
||||
"PixVerse image upload failed",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await upload.release();
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = imageId === undefined ? "/video/text/generate" : "/video/img/generate";
|
||||
const create = await postJsonRequest({
|
||||
url: `${baseUrl}${endpoint}`,
|
||||
headers: buildPixVerseHeaders(headers, "application/json"),
|
||||
body: buildVideoBody(req, model, imageId),
|
||||
timeoutMs: resolveProviderOperationTimeoutMs({
|
||||
deadline,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
}),
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(create.response, "PixVerse video generation failed");
|
||||
const videoId = readPixVerseVideoId(
|
||||
await readPixVerseJson<PixVerseVideoCreateResponse>(
|
||||
create.response,
|
||||
"PixVerse video generation failed",
|
||||
),
|
||||
);
|
||||
const completed = await pollPixVerseVideo({
|
||||
videoId,
|
||||
baseUrl,
|
||||
deadline,
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
headers,
|
||||
});
|
||||
return {
|
||||
videos: [extractPixVerseVideo(completed)],
|
||||
model,
|
||||
metadata: {
|
||||
endpoint,
|
||||
videoId,
|
||||
status: readPixVerseStatus(completed),
|
||||
seed: readPixVerseSeed(completed.seed),
|
||||
size: asFiniteNumber(completed.size),
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await create.release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user