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(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) })).data); if (!created.id) throw new Error("视频接口没有返回任务 ID"); for (;;) { const video = unwrapVideoResponse((await axios.get(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(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 || "视频下载失败"); }