feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+89 -16
View File
@@ -7,11 +7,12 @@ export const meta = {
en: "Shengshu Vidu video generation (text-to-video, image-to-video, first-and-last-frame, and reference-to-video)",
zh: "生数 Vidu 视频生成(文生视频、图生视频、首尾帧、参考生视频)",
},
version: "1.0.2",
version: "1.1.0",
author: { name: "QuantumNous" },
channelTypes: [52],
models: ["viduq2", "viduq1", "vidu2.0", "vidu1.5"],
fetchMode: "per_task",
upstreams: ["vendor", "new_api"],
usageSchema: {
// estimated/actual Vidu credits
credits: {
@@ -46,15 +47,52 @@ export const meta = {
{ label: "2.0 4s 720p", facts: { credits: 0, duration: 4, resolution: "720p" } },
{ label: "2.0 8s 720p", facts: { credits: 0, duration: 8, resolution: "720p" } },
],
// Vidu request and response bodies, for clients and for a downstream New API
// gateway.
routes: [
{ method: "POST", path: "/vidu/ent/v2/text2video", type: "submit", decode: "createTask", render: "taskCreated" },
{ method: "POST", path: "/vidu/ent/v2/img2video", type: "submit", decode: "createTask", render: "taskCreated" },
{ method: "POST", path: "/vidu/ent/v2/start-end2video", type: "submit", decode: "createTask", render: "taskCreated" },
{ method: "POST", path: "/vidu/ent/v2/reference2video", type: "submit", decode: "createTask", render: "taskCreated" },
{ method: "GET", path: "/vidu/ent/v2/tasks/:task_id/creations", type: "query", render: "taskStatus" },
],
protocols: [{ name: "openai_responses", supports: ["stream", "sync", "background"] }, "openai_video"],
};
const RESOLUTIONS = ["360p", "540p", "720p", "1080p"];
// The submit route names the action; one or two reference images would
// otherwise read as image-to-video or first-and-last-frame.
const ROUTE_ACTIONS = {
text2video: "text_to_video",
img2video: "image_to_video",
"start-end2video": "first_tail_to_video",
reference2video: "reference_to_video",
};
function trimmed(value) {
return String(value || "").trim();
}
function plainObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
// On a New API channel the same plugin answers on another gateway, which
// serves the Vidu wire format on the prefixed native routes and takes its own
// token as a Bearer header.
function viaGateway(ctx) {
return Boolean(ctx.upstream && ctx.upstream.kind === "new_api");
}
function apiRoot(ctx) {
return ctx.baseUrl + (viaGateway(ctx) ? "/vidu" : "") + "/ent/v2";
}
function authorization(ctx) {
return (viaGateway(ctx) ? "Bearer " : "Token ") + ctx.apiKey;
}
function isQ2Model(model) {
return String(model || "").indexOf("viduq2") === 0;
}
@@ -219,6 +257,45 @@ function pathFor(action) {
return "/text2video";
}
export const native = {
createTask: function (ctx) {
if (!ctx.body || ctx.body.kind !== "json") throw new Error("JSON body required");
const body = plainObject(ctx.body.value);
if (!body) throw new Error("request body must be an object");
const model = trimmed(body.model);
if (!model) throw new Error("model is required");
const action = ROUTE_ACTIONS[ctx.path.slice(ctx.path.lastIndexOf("/") + 1)];
// The other Vidu fields ride in metadata, which buildSubmitRequest
// spreads back into the body.
const metadata = Object.assign({}, body, { action: action });
const requestBody = { model: model, metadata: metadata };
delete metadata.model;
for (const key of ["images", "prompt", "duration", "resolution"]) {
delete metadata[key];
if (body[key] !== undefined && body[key] !== null) requestBody[key] = body[key];
}
const duration = requestBody.duration === undefined ? undefined : Number(requestBody.duration);
validateViduCombo(model, duration, outboundResolution(requestBody, model), hasViduImages(requestBody, false));
return { kind: "submit", model: model, action: action, requestBody: requestBody };
},
taskCreated: function (ctx, task) {
return Object.assign({}, plainObject(task.data) || {}, { task_id: task.task_id });
},
// The poller can fail a task whose snapshot was still processing; that
// snapshot is rendered failed so a downstream gateway reads the same state.
taskStatus: function (ctx, task) {
const output = Object.assign({}, plainObject(task.data) || {}, { id: task.task_id });
if (task.status === "FAILURE" && output.state !== "failed") {
output.state = "failed";
output.err_code = output.err_code || task.fail_reason || "task failed";
}
return output;
},
error: function (ctx, error) {
return { code: error.code, message: error.message };
},
};
export function buildSubmitRequest(ctx) {
const req = ctx.requestBody;
const action = actionFor(req);
@@ -226,25 +303,21 @@ export function buildSubmitRequest(ctx) {
let model = ctx.upstreamModel || "viduq1";
if (action === "reference_to_video" && model.includes("viduq2")) model = "viduq2";
const images = Array.isArray(req.images) ? req.images.map(filePlaceholder) : null;
const body = Object.assign(
{
model: model,
images: images,
prompt: req.prompt || null,
duration: outboundDuration(req, model),
resolution: outboundResolution(req, model),
movement_amplitude: "auto",
},
metadata
);
// metadata carries the other Vidu fields and cannot replace the billed
// model, duration and resolution.
const body = Object.assign({ images: images, prompt: req.prompt || null, movement_amplitude: "auto" }, metadata, {
model: model,
duration: outboundDuration(req, model),
resolution: outboundResolution(req, model),
});
delete body.action;
if (!body.prompt) delete body.prompt;
if (!body.bgm) delete body.bgm;
if (Array.isArray(body.images)) body.images = body.images.map(filePlaceholder);
return {
url: ctx.baseUrl + "/ent/v2" + pathFor(action),
url: apiRoot(ctx) + pathFor(action),
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: "Token " + ctx.apiKey },
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: authorization(ctx) },
body: body,
action: action,
};
@@ -265,9 +338,9 @@ export function extractUsage(ctx) {
export function buildQueryRequest(ctx) {
return {
url: ctx.baseUrl + "/ent/v2/tasks/" + ctx.taskId + "/creations",
url: apiRoot(ctx) + "/tasks/" + ctx.taskId + "/creations",
method: "GET",
headers: { Accept: "application/json", Authorization: "Token " + ctx.apiKey },
headers: { Accept: "application/json", Authorization: authorization(ctx) },
};
}