74 lines
5.0 KiB
TypeScript
74 lines
5.0 KiB
TypeScript
import { modelCapabilityConfigFor, videoResolutionRequest } from "@/lib/model-capabilities";
|
||
import { boolConfig } from "@/lib/seedance-video";
|
||
import { getResourceOSSUrl } from "@/services/api/resources";
|
||
import { modelOptionName } from "@/stores/use-config-store";
|
||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||
import type { ReferenceImage } from "@/types/image";
|
||
|
||
import type { ApiEnvelope, ApiVideoResponse, RequestOptions, ResolvedAiConfig, VideoGenerationTask, VideoGenerationTaskState } from "./video-contracts";
|
||
import type { VideoProviderDeps } from "./video-provider-deps";
|
||
import { normalizeVideoSeconds, normalizeVideoSize } from "./video-validation";
|
||
|
||
export async function createVideoGenerationsTask(deps: VideoProviderDeps, config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], videoReferences: ReferenceVideo[], audioReferences: ReferenceAudio[], options?: RequestOptions): Promise<VideoGenerationTask> {
|
||
if (references.length > 9 || videoReferences.length > 3 || audioReferences.length > 3) throw new Error("NewAPI Video Generations 最多支持 9 张参考图、3 个参考视频和 3 个参考音频");
|
||
if (audioReferences.length > 0 && videoReferences.length === 0) throw new Error("NewAPI Video Generations 的参考音频必须同时提供至少 1 个参考视频;纯音频生视频请切换到支持该模式的渠道");
|
||
const [imageUrls, videoUrls, audioUrls] = await Promise.all([
|
||
Promise.all(references.map((item) => resolveVideoGenerationsUrl(item.url || item.dataUrl, item.storageKey))),
|
||
Promise.all(videoReferences.map((item) => resolveVideoGenerationsUrl(item.url, item.storageKey))),
|
||
Promise.all(audioReferences.map((item) => resolveVideoGenerationsUrl(item.url, item.storageKey))),
|
||
]);
|
||
const profile = modelCapabilityConfigFor(config, model).video!;
|
||
const resolution = newAPIVideoResolutionRequest(profile, config.vquality, modelOptionName(model));
|
||
const payload = {
|
||
model: modelOptionName(model),
|
||
prompt: prompt.trim(),
|
||
seconds: normalizeVideoSeconds(config.videoSeconds),
|
||
aspect_ratio: normalizeVideoSize(config.size) || "16:9",
|
||
...(resolution ? { resolution } : {}),
|
||
...(profile.generateAudio.supported ? { generate_audio: boolConfig(config.videoGenerateAudio, profile.generateAudio.default) } : {}),
|
||
...(imageUrls.length ? { image_urls: imageUrls } : {}),
|
||
...(videoUrls.length ? { video_urls: videoUrls } : {}),
|
||
...(audioUrls.length ? { audio_urls: audioUrls } : {}),
|
||
};
|
||
try {
|
||
const created = deps.response.unwrapVideoResponse(await deps.transport.post<ApiVideoResponse>(deps.transport.apiUrl("/video/generations"), payload, options));
|
||
const id = deps.response.videoTaskId(created);
|
||
if (!id) throw new Error("NewAPI Video Generations 没有返回任务 ID");
|
||
return { id, provider: "video-generations", model };
|
||
} catch (error) {
|
||
throw new Error(deps.response.readAxiosError(error, "NewAPI Video Generations 任务创建失败"));
|
||
}
|
||
}
|
||
|
||
export async function pollVideoGenerationsTask(deps: VideoProviderDeps, task: VideoGenerationTask, options?: RequestOptions): Promise<VideoGenerationTaskState> {
|
||
try {
|
||
const raw = await deps.transport.get<ApiEnvelope<Record<string, unknown>>>(deps.transport.apiUrl(`/video/generations/${encodeURIComponent(task.id)}`), options);
|
||
const state = deps.response.unwrapEnvelopeRecord(raw);
|
||
const status = String(state.status || "").toUpperCase();
|
||
if (status === "SUCCESS" || status === "SUCCEEDED" || status === "COMPLETED") {
|
||
const url = String(state.result_url || state.video_url || state.url || "");
|
||
if (!url) return { status: "failed", error: "视频任务已完成但没有返回视频地址" };
|
||
return { status: "completed", result: await deps.response.videoResultFromUrl(url, options) };
|
||
}
|
||
if (status === "FAILURE" || status === "FAILED" || status === "CANCELLED") return { status: "failed", error: String(state.fail_reason || state.error || "视频生成失败") };
|
||
return { status: "pending" };
|
||
} catch (error) {
|
||
throw new Error(deps.response.readAxiosError(error, "NewAPI Video Generations 任务查询失败"));
|
||
}
|
||
}
|
||
|
||
function newAPIVideoResolutionRequest(profile: NonNullable<ReturnType<typeof modelCapabilityConfigFor>["video"]>, value: string, model: string) {
|
||
if (model.trim().toLowerCase() === "grok-video-1.5-1080p") return "1080p";
|
||
return videoResolutionRequest(profile, value);
|
||
}
|
||
|
||
async function resolveVideoGenerationsUrl(value: string | undefined, storageKey?: string) {
|
||
if (storageKey?.startsWith("resource:")) return getResourceOSSUrl(storageKey);
|
||
if (isPublicMediaUrl(value || "")) return String(value);
|
||
throw new Error("NewAPI Video Generations 的参考素材需要公网 URL;请先把素材保存到对象存储");
|
||
}
|
||
|
||
function isPublicMediaUrl(value: string) {
|
||
return /^https?:\/\//i.test(value || "");
|
||
}
|