ai-chatbot/lib/ai/providers/aliyun-canvas-video.ts

94 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ProviderTaskFailedError,
type ProviderTaskLifecycle,
} from "@/lib/video-canvas/video-task-lifecycle";
import type { ExecuteDirectVideoGenerationParams } from "@/lib/ai/tools/generate-video";
// This adapter deliberately exposes only the official model whose contract we support.
export const ALIYUN_CANVAS_VIDEO_MODEL = "wan2.6-i2v-flash";
export function buildAliyunCanvasVideoRequest(params: ExecuteDirectVideoGenerationParams, modelId: string) {
if (modelId !== ALIYUN_CANVAS_VIDEO_MODEL) throw new Error("当前阿里云视频模型尚未接入画布");
if (params.mode !== "first-frame" || params.images?.length !== 1) throw new Error("万相首帧生视频需要一张参考图");
if (params.audioUrls?.length) throw new Error("此画布接入暂不支持参考音频");
const duration = params.duration ?? 5;
const resolution = params.resolution ?? "720p";
if (!Number.isInteger(duration) || duration < 2 || duration > 15) throw new Error("万相视频时长需为 2–15 秒整数");
if (!["720p", "1080p"].includes(resolution)) throw new Error("万相视频仅支持 720P / 1080P");
if (params.cameraFixed || params.returnLastFrame) throw new Error("此万相接入不支持固定镜头参数或返回尾帧参数");
return {
model: modelId,
input: { prompt: params.prompt, img_url: params.images[0] },
parameters: {
resolution: resolution.toUpperCase(), duration,
// The upstream derives aspect ratio from the input image, not a ratio parameter.
audio: params.generateAudio ?? false,
prompt_extend: false, watermark: params.watermark ?? false,
...(params.seed != null ? { seed: params.seed } : {}),
},
};
}
export async function executeAliyunCanvasVideo(
params: ExecuteDirectVideoGenerationParams & {
modelId: string;
apiKey: string;
baseURL: string;
lifecycle?: ProviderTaskLifecycle;
},
runtime = {
fetch: globalThis.fetch,
sleep: (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
},
) {
const body = buildAliyunCanvasVideoRequest(params, params.modelId);
const base = new URL(params.baseURL);
base.pathname = "/api/v1";
base.search = "";
base.hash = "";
const headers = { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json", "X-DashScope-Async": "enable" };
const deadline = Date.now() + 275_000;
let taskId = params.lifecycle?.resume?.taskId || "";
const request = async (path: string, init: RequestInit = {}) => {
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error(`阿里云视频查询超时,任务 ${taskId},请勿重复生成`);
const response = await runtime.fetch(`${base.toString().replace(/\/$/, "")}${path}`, {
...init, headers, signal: AbortSignal.timeout(Math.min(30_000, remaining)),
});
const data = await response.json();
if (!response.ok) throw new Error(`阿里云视频请求失败(${response.status}):${data.code || data.error?.code || "UnknownError"}${taskId ? `,任务 ${taskId}` : ""}`);
return data;
};
await params.lifecycle?.assertActive();
if (!taskId) {
const submitted = await request("/services/aigc/video-generation/video-synthesis", {
method: "POST",
body: JSON.stringify(body),
});
taskId = submitted.output?.task_id;
if (!taskId) throw new Error("阿里云未返回视频任务 ID");
await params.lifecycle?.accepted({ taskId });
}
await params.onProgress?.(`阿里云视频任务已提交:${taskId}`);
while (Date.now() < deadline) {
await runtime.sleep(Math.min(5_000, Math.max(0, deadline - Date.now())));
await params.lifecycle?.assertActive();
const result = await request(`/tasks/${encodeURIComponent(taskId)}`);
const output = result.output || {};
if (output.task_status === "SUCCEEDED") {
if (!/^https?:\/\//i.test(output.video_url || "")) throw new Error("阿里云任务完成但没有返回视频");
return { url: output.video_url as string, metadata: {
provider: "aliyun", taskId, requestId: result.request_id,
duration: body.parameters.duration, resolution: params.resolution ?? "720p",
generateAudio: body.parameters.audio,
} };
}
if (["FAILED", "CANCELED", "UNKNOWN"].includes(output.task_status)) {
throw new ProviderTaskFailedError(
`阿里云视频生成失败:${output.code || result.code || output.task_status},任务 ${taskId}`,
);
}
}
throw new Error(`阿里云视频查询超时,任务 ${taskId},请勿重复生成`);
}