import { imageToDataUrl } from "@/services/image-storage"; import { modelOptionName } from "@/stores/use-config-store"; import type { ReferenceImage } from "@/types/image"; import type { ReferenceAudio, ReferenceVideo } from "@/types/media"; import { normalizeVideoResolution, normalizeVideoSeconds, normalizeVideoSize } from "./video-validation"; import type { RequestOptions, ResolvedAiConfig, VideoGenerationTask, VideoGenerationTaskState } from "./video-contracts"; import type { VideoProviderDeps } from "./video-provider-deps"; type GeminiVeoOperation = { name?: string; done?: boolean; error?: { message?: string }; response?: Record; }; export async function createGeminiVeoTask(deps: VideoProviderDeps, config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], videoReferences: ReferenceVideo[], audioReferences: ReferenceAudio[], options?: RequestOptions): Promise { if (references.length > 1 || videoReferences.length || audioReferences.length) throw new Error("Gemini Veo 当前只支持 1 张起始图,不支持参考视频或音频"); const instance: Record = { prompt: prompt.trim() }; if (references[0]) { const dataUrl = await imageToDataUrl(references[0]); const matched = /^data:([^;,]+);base64,(.+)$/s.exec(dataUrl); if (!matched) throw new Error("Gemini Veo 起始图读取失败"); instance.image = { bytesBase64Encoded: matched[2], mimeType: matched[1] }; } const payload = { instances: [instance], parameters: { aspectRatio: normalizeVideoSize(config.size) || "16:9", durationSeconds: Number.parseInt(normalizeVideoSeconds(config.videoSeconds), 10) || 6, resolution: normalizeVideoResolution(config.vquality), sampleCount: 1, }, }; try { const response = await deps.transport.post(geminiVeoCreateUrl(config, modelOptionName(model)), payload, options); if (!response.name) throw new Error("Gemini Veo 没有返回 operation name"); return { id: response.name, provider: "gemini-veo", model }; } catch (error) { throw new Error(deps.response.readAxiosError(error, "Gemini Veo 任务创建失败")); } } export async function pollGeminiVeoTask(deps: VideoProviderDeps, config: ResolvedAiConfig, task: VideoGenerationTask, options?: RequestOptions): Promise { try { const operation = await deps.transport.get(geminiVeoOperationUrl(config, task.id), options); if (operation.error?.message) return { status: "failed", error: operation.error.message }; if (!operation.done) return { status: "pending" }; const url = findGeminiVideoURL(operation.response); if (!url) return { status: "failed", error: "Gemini Veo 任务已完成但没有返回视频地址" }; const blob = await deps.transport.getExternalBlob(url, geminiVeoHeaders(config), options); await deps.response.assertVideoBlob(blob); return { status: "completed", result: { blob } }; } catch (error) { throw new Error(deps.response.readAxiosError(error, "Gemini Veo 任务查询失败")); } } function geminiVeoCreateUrl(config: ResolvedAiConfig, model: string) { return `${geminiVeoBaseUrl(config)}/models/${encodeURIComponent(model)}:predictLongRunning`; } function geminiVeoOperationUrl(config: ResolvedAiConfig, operationName: string) { return `${geminiVeoBaseUrl(config)}/${operationName.replace(/^\/+/, "")}`; } function geminiVeoBaseUrl(config: ResolvedAiConfig) { const base = config.baseUrl.replace(/\/+$/, ""); return /\/v1beta$/i.test(base) ? base : `${base}/v1beta`; } function geminiVeoHeaders(config: ResolvedAiConfig) { return { "x-goog-api-key": config.apiKey }; } function findGeminiVideoURL(value: unknown): string { if (!value || typeof value !== "object") return ""; if (Array.isArray(value)) { for (const item of value) { const found = findGeminiVideoURL(item); if (found) return found; } return ""; } const record = value as Record; for (const key of ["uri", "url", "videoUri", "video_url"]) { if (typeof record[key] === "string" && /^https?:\/\//i.test(record[key])) return record[key]; } for (const child of Object.values(record)) { const found = findGeminiVideoURL(child); if (found) return found; } return ""; }