Files
ttstd-infinite-canvas/web/src/services/api/video-provider-newapi.ts

74 lines
5.0 KiB
TypeScript
Raw 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 { 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 || "");
}