feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
@@ -7,12 +7,13 @@ export const meta = {
|
||||
en: "Google Veo video generation on Vertex AI (text-to-video and image-to-video)",
|
||||
zh: "Google Veo 视频生成(文生视频、图生视频),Vertex AI 版本",
|
||||
},
|
||||
version: "1.0.2",
|
||||
version: "1.1.0",
|
||||
channelTypes: [41],
|
||||
author: { name: "QuantumNous" },
|
||||
models: ["veo-3.0-generate-001", "veo-3.0-fast-generate-001", "veo-3.1-generate-preview", "veo-3.1-fast-generate-preview"],
|
||||
fetchMode: "per_task",
|
||||
auth: { type: "oauth2_jwt" },
|
||||
upstreams: ["vendor", "new_api"],
|
||||
usageSchema: {
|
||||
// Requested video duration in seconds. Allowed values: 4, 6, 8.
|
||||
seconds: {
|
||||
@@ -38,12 +39,40 @@ export const meta = {
|
||||
{ label: "8s 1080p audio", facts: { seconds: 8, resolution: "1080p", generate_audio: true } },
|
||||
{ label: "8s 4k audio", facts: { seconds: 8, resolution: "4k", generate_audio: true } },
|
||||
],
|
||||
// Vertex AI request and response bodies, for clients and for a downstream New
|
||||
// API gateway. The model segment carries the method: predictLongRunning
|
||||
// submits and fetchPredictOperation polls. The gateway runs every request in
|
||||
// its own channel's project; the operation names it returns echo the path's
|
||||
// project and location and end with the gateway task id.
|
||||
routes: [
|
||||
{
|
||||
method: "POST",
|
||||
path: "/vertex-ai/v1/projects/:project/locations/:location/publishers/google/models/:model",
|
||||
type: "dynamic",
|
||||
decode: "modelMethod",
|
||||
render: "operation",
|
||||
},
|
||||
],
|
||||
protocols: [{ name: "openai_responses", supports: ["stream", "sync", "background"] }, "openai_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 Vertex AI wire format on the prefixed native routes, takes its own
|
||||
// token as a Bearer header and runs the request in its own project.
|
||||
function viaGateway(ctx) {
|
||||
return Boolean(ctx.upstream && ctx.upstream.kind === "new_api");
|
||||
}
|
||||
function requestHeaders(ctx) {
|
||||
const headers = { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader };
|
||||
if (!viaGateway(ctx)) headers["x-goog-user-project"] = ctx.auth.projectId;
|
||||
return headers;
|
||||
}
|
||||
function responsesInput(req) {
|
||||
const texts = [],
|
||||
images = [];
|
||||
@@ -125,7 +154,7 @@ function apiBase(baseUrl, project, location) {
|
||||
return "https://" + (location === "global" ? "" : location + "-") + "aiplatform.googleapis.com/v1/projects/" + project + "/locations/" + location;
|
||||
}
|
||||
function modelURL(ctx, project, location, model, action) {
|
||||
return apiBase(ctx.baseUrl, project, location) + "/publishers/google/models/" + model + ":" + action;
|
||||
return apiBase(ctx.baseUrl + (viaGateway(ctx) ? "/vertex-ai" : ""), project, location) + "/publishers/google/models/" + model + ":" + action;
|
||||
}
|
||||
function decodeTaskId(value) {
|
||||
return utils.base64URLDecode(value);
|
||||
@@ -144,6 +173,71 @@ function dataVideo(response) {
|
||||
return "data:" + (String(enc).includes("/") ? enc : "video/" + enc) + ";base64," + data;
|
||||
}
|
||||
|
||||
export const native = {
|
||||
modelMethod: function (ctx) {
|
||||
const segment = String((ctx.params || {}).model || "");
|
||||
const separator = segment.lastIndexOf(":");
|
||||
const method = separator > 0 ? segment.slice(separator + 1) : "";
|
||||
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");
|
||||
if (method === "fetchPredictOperation") {
|
||||
const name = String(body.operationName || "");
|
||||
const marker = name.lastIndexOf("/operations/");
|
||||
if (marker < 0) throw new Error("operationName must name an operation");
|
||||
return { kind: "query", taskIds: [name.slice(marker + "/operations/".length)] };
|
||||
}
|
||||
if (method !== "predictLongRunning") throw new Error("the path must name models/{model}:predictLongRunning or :fetchPredictOperation");
|
||||
const model = segment.slice(0, separator);
|
||||
const instances = Array.isArray(body.instances) ? body.instances : [];
|
||||
const instance = plainObject(instances[0]);
|
||||
if (instances.length !== 1 || !instance) throw new Error("instances must hold exactly one instance");
|
||||
// buildSubmitRequest sends only the prompt and the first frame.
|
||||
for (const key of Object.keys(instance)) {
|
||||
if (key !== "prompt" && key !== "image") throw new Error("instances[0]." + key + " is not supported");
|
||||
}
|
||||
const metadata = Object.assign({}, plainObject(body.parameters) || {});
|
||||
const requestBody = { model: model, prompt: instance.prompt, metadata: metadata };
|
||||
if (metadata.durationSeconds !== undefined) {
|
||||
requestBody.duration = metadata.durationSeconds;
|
||||
delete metadata.durationSeconds;
|
||||
if ([4, 6, 8].indexOf(Number(requestBody.duration)) < 0) throw new Error("durationSeconds must be one of 4, 6, or 8");
|
||||
}
|
||||
if (metadata.resolution !== undefined && ["720p", "1080p", "4k"].indexOf(String(metadata.resolution).toLowerCase()) < 0)
|
||||
throw new Error("resolution must be one of 720p, 1080p, or 4k");
|
||||
if (instance.image !== undefined) {
|
||||
const image = plainObject(instance.image) || {};
|
||||
const mime = trimmed(image.mimeType).toLowerCase();
|
||||
if (!trimmed(image.bytesBase64Encoded)) throw new Error("instances[0].image must carry bytesBase64Encoded");
|
||||
if (mime !== "image/jpeg" && mime !== "image/png") throw new Error("input image must be image/jpeg or image/png");
|
||||
requestBody.images = ["data:" + mime + ";base64," + image.bytesBase64Encoded];
|
||||
}
|
||||
return { kind: "submit", model: model, action: requestBody.images ? "image_to_video" : "text_to_video", requestBody: requestBody };
|
||||
},
|
||||
// Submission renders one task and fetchPredictOperation a list of one. The
|
||||
// poller can fail a task whose operation was still running; that operation
|
||||
// is rendered done with the failure.
|
||||
operation: function (ctx, tasks) {
|
||||
const task = Array.isArray(tasks) ? tasks[0] : tasks;
|
||||
const params = ctx.params || {};
|
||||
const data = plainObject(task.data) || {};
|
||||
const named = /\/models\/([^/]+)\/operations\//.exec(String(data.name || ""));
|
||||
const model = named ? named[1] : String(params.model || "").split(":")[0];
|
||||
const output = Object.assign({}, data, {
|
||||
name: "projects/" + params.project + "/locations/" + params.location + "/publishers/google/models/" + model + "/operations/" + task.task_id,
|
||||
});
|
||||
if (task.status === "FAILURE" && !(plainObject(data.error) && data.error.message)) {
|
||||
output.done = true;
|
||||
output.error = { message: task.fail_reason || "task failed" };
|
||||
delete output.response;
|
||||
}
|
||||
return output;
|
||||
},
|
||||
error: function (ctx, error) {
|
||||
return { error: { code: error.httpStatus, message: error.message } };
|
||||
},
|
||||
};
|
||||
|
||||
export function buildSubmitRequest(ctx) {
|
||||
if (ctx.authError) throw new Error(ctx.authError);
|
||||
const req = ctx.requestBody || {},
|
||||
@@ -183,9 +277,10 @@ export function buildSubmitRequest(ctx) {
|
||||
instance.image = { bytesBase64Encoded: data, mimeType: mime };
|
||||
}
|
||||
return {
|
||||
url: modelURL(ctx, ctx.auth.projectId, region(ctx, model), model, "predictLongRunning"),
|
||||
// A gateway runs the request in its own project, so the path names none.
|
||||
url: modelURL(ctx, viaGateway(ctx) ? "-" : ctx.auth.projectId, region(ctx, model), model, "predictLongRunning"),
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader, "x-goog-user-project": ctx.auth.projectId },
|
||||
headers: requestHeaders(ctx),
|
||||
body: { instances: [instance], parameters: metadata },
|
||||
action: image ? "image_to_video" : "text_to_video",
|
||||
};
|
||||
@@ -217,7 +312,7 @@ export function buildQueryRequest(ctx) {
|
||||
return {
|
||||
url: modelURL(ctx, project, location || "us-central1", model, "fetchPredictOperation"),
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader, "x-goog-user-project": ctx.auth.projectId },
|
||||
headers: requestHeaders(ctx),
|
||||
body: { operationName: name },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user