- 添加 resolveRequestSize 函数处理尺寸参数解析逻辑 - 将 pixelSize 变量重命名为 requestSize 以提高代码可读性 - 修复当尺寸为 auto 时不发送 size 参数的功能 - 确保所有非 auto 尺寸值都会正确传递到请求中 - 更新图片生成和编辑函数中的尺寸参数处理逻辑
101 lines
4.8 KiB
TypeScript
101 lines
4.8 KiB
TypeScript
import axios from "axios";
|
||
|
||
import { dataUrlToFile } from "@/lib/image-utils";
|
||
import { imageToDataUrl } from "@/services/image-storage";
|
||
import { buildApiUrl, type AiConfig } from "@/stores/use-config-store";
|
||
import { useUserStore } from "@/stores/use-user-store";
|
||
import type { ReferenceImage } from "@/types/image";
|
||
|
||
type VideoResponse = { id: string; status?: string; error?: { message?: string } };
|
||
type ApiVideoResponse = VideoResponse | { code?: number; data?: VideoResponse | null; msg?: string };
|
||
|
||
function aiApiUrl(config: AiConfig, path: string) {
|
||
return config.channelMode === "remote" ? `/api/v1${path}` : buildApiUrl(config.baseUrl, path);
|
||
}
|
||
|
||
function aiHeaders(config: AiConfig) {
|
||
const token = useUserStore.getState().token;
|
||
return config.channelMode === "remote" ? (token ? { Authorization: `Bearer ${token}` } : undefined) : { Authorization: `Bearer ${config.apiKey}` };
|
||
}
|
||
|
||
function refreshRemoteUser(config: AiConfig) {
|
||
if (config.channelMode === "remote") void useUserStore.getState().hydrateUser();
|
||
}
|
||
|
||
export async function requestVideoGeneration(config: AiConfig, prompt: string, references: ReferenceImage[] = []) {
|
||
const model = config.model || config.videoModel;
|
||
const body = new FormData();
|
||
body.append("model", model);
|
||
body.append("prompt", prompt);
|
||
body.append("seconds", normalizeVideoSeconds(config.videoSeconds));
|
||
if (normalizeVideoSize(config.size)) body.append("size", normalizeVideoSize(config.size)!);
|
||
body.append("resolution_name", normalizeVideoResolution(config.vquality));
|
||
body.append("preset", "normal");
|
||
const files = await Promise.all(references.slice(0, 7).map(async (image) => dataUrlToFile({ ...image, dataUrl: await imageToDataUrl(image) })));
|
||
files.forEach((file) => body.append("input_reference[]", file));
|
||
try {
|
||
const created = unwrapVideoResponse((await axios.post<ApiVideoResponse>(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) })).data);
|
||
if (!created.id) throw new Error("视频接口没有返回任务 ID");
|
||
for (;;) {
|
||
const video = unwrapVideoResponse((await axios.get<ApiVideoResponse>(aiApiUrl(config, `/videos/${created.id}`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined })).data);
|
||
if (video.status === "completed") break;
|
||
if (video.status === "failed" || video.status === "cancelled") throw new Error(video.error?.message || "视频生成失败");
|
||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||
}
|
||
const content = await axios.get<Blob>(aiApiUrl(config, `/videos/${created.id}/content`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined, responseType: "blob" });
|
||
await assertVideoBlob(content.data);
|
||
refreshRemoteUser(config);
|
||
return content.data;
|
||
} catch (error) {
|
||
throw new Error(readAxiosError(error, "视频生成失败"));
|
||
}
|
||
}
|
||
|
||
function normalizeVideoSeconds(value: string) {
|
||
const seconds = Math.floor(Number(value) || 6);
|
||
return String(Math.max(1, Math.min(20, seconds)));
|
||
}
|
||
|
||
function normalizeVideoSize(value: string) {
|
||
if (value === "auto") return null;
|
||
const size = value || "1280x720";
|
||
if (/^\d+x\d+$/.test(size)) return size;
|
||
return ["9:16", "2:3", "3:4"].includes(size) ? "720x1280" : "1280x720";
|
||
}
|
||
|
||
function normalizeVideoResolution(value: string) {
|
||
if (value === "low") return "480p";
|
||
if (value === "auto" || value === "high" || value === "medium") return "720p";
|
||
const resolution = value.replace(/p$/i, "") || "720";
|
||
return `${resolution}p`;
|
||
}
|
||
|
||
function unwrapVideoResponse(payload: ApiVideoResponse) {
|
||
if (!payload) throw new Error("接口没有返回视频任务");
|
||
if ("code" in payload && typeof payload.code === "number") {
|
||
if (payload.code !== 0) throw new Error(payload.msg || "请求失败");
|
||
if (!payload.data) throw new Error("接口没有返回视频任务");
|
||
return payload.data;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function readAxiosError(error: unknown, fallback: string) {
|
||
if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
|
||
const responseData = error.response?.data;
|
||
return responseData?.msg || responseData?.error?.message || (error.response?.status ? `${fallback}:${error.response.status}` : fallback);
|
||
}
|
||
return error instanceof Error ? error.message : fallback;
|
||
}
|
||
|
||
async function assertVideoBlob(blob: Blob) {
|
||
if (!blob.type.includes("json")) return;
|
||
let payload: { code?: number; msg?: string };
|
||
try {
|
||
payload = JSON.parse(await blob.text()) as { code?: number; msg?: string };
|
||
} catch {
|
||
return;
|
||
}
|
||
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "视频下载失败");
|
||
}
|