feat(model): enhance model configuration and script handling for audio and video capabilities
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import axios from "axios";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { dataUrlToFile } from "@/lib/image-utils";
|
||||
import { getMediaBlob, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||
import { imageToDataUrl } from "@/services/image-storage";
|
||||
import { boolConfig, buildSeedancePromptText, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceVideoReferenceError, SEEDANCE_REFERENCE_LIMITS } from "@/lib/seedance-video";
|
||||
import { buildApiUrl, modelOptionName, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { buildApiUrl, modelOptionName, resolveModelRequestConfig, resolveModelScript, type AiConfig } from "@/stores/use-config-store";
|
||||
import { runModelPlugin } from "./model-plugin";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
|
||||
@@ -23,9 +25,12 @@ type ApiEnvelope<T> = T | { code?: number | string; data?: T | null; msg?: strin
|
||||
type RequestOptions = { signal?: AbortSignal };
|
||||
|
||||
export type VideoGenerationResult = { blob?: Blob; url?: string; mimeType?: string };
|
||||
export type VideoGenerationTask = { id: string; provider: "openai" | "seedance"; model: string };
|
||||
export type VideoGenerationTask = { id: string; provider: "openai" | "seedance" | "plugin"; model: string };
|
||||
export type VideoGenerationTaskState = { status: "pending" } | { status: "completed"; result: VideoGenerationResult } | { status: "failed"; error: string };
|
||||
|
||||
/** Results for scripted (plugin) video models, which run their own create+poll in one shot at task creation. */
|
||||
const pluginVideoResults = new Map<string, VideoGenerationResult>();
|
||||
|
||||
function aiApiUrl(config: AiConfig, path: string) {
|
||||
return buildApiUrl(config.baseUrl, path);
|
||||
}
|
||||
@@ -54,6 +59,8 @@ export async function requestVideoGeneration(config: AiConfig, prompt: string, r
|
||||
export async function createVideoGenerationTask(config: AiConfig, prompt: string, references: ReferenceImage[] = [], videoReferences: ReferenceVideo[] = [], audioReferences: ReferenceAudio[] = [], options?: RequestOptions): Promise<VideoGenerationTask> {
|
||||
const selectedModel = (config.model || config.videoModel).trim();
|
||||
const requestConfig = resolveModelRequestConfig(config, selectedModel);
|
||||
const script = resolveModelScript(config, selectedModel);
|
||||
if (script) return createPluginVideoTask(requestConfig, selectedModel, script, prompt, references, options);
|
||||
assertVideoConfig(requestConfig, requestConfig.model);
|
||||
if (isSeedanceVideoConfig(requestConfig)) {
|
||||
return createSeedanceTask(requestConfig, selectedModel, prompt, references, videoReferences, audioReferences, options);
|
||||
@@ -65,11 +72,56 @@ export async function createVideoGenerationTask(config: AiConfig, prompt: string
|
||||
}
|
||||
|
||||
export async function pollVideoGenerationTask(config: AiConfig, task: VideoGenerationTask, options?: RequestOptions): Promise<VideoGenerationTaskState> {
|
||||
if (task.provider === "plugin") {
|
||||
const result = pluginVideoResults.get(task.id);
|
||||
return result ? { status: "completed", result } : { status: "failed", error: "插件视频任务已失效,请重新生成" };
|
||||
}
|
||||
const requestConfig = resolveModelRequestConfig(config, task.model);
|
||||
assertVideoConfig(requestConfig, requestConfig.model);
|
||||
return task.provider === "seedance" ? pollSeedanceTask(requestConfig, task, options) : pollOpenAIVideoTask(requestConfig, task, options);
|
||||
}
|
||||
|
||||
async function createPluginVideoTask(config: AiConfig, model: string, script: string, prompt: string, references: ReferenceImage[], options?: RequestOptions): Promise<VideoGenerationTask> {
|
||||
if (!config.baseUrl.trim()) throw new Error("请先配置 Base URL");
|
||||
if (!config.apiKey.trim()) throw new Error("请先配置 API Key");
|
||||
const refs = await Promise.all(references.map((image) => imageToDataUrl(image)));
|
||||
const result = videoPluginResult(
|
||||
await runModelPlugin({
|
||||
capability: "video",
|
||||
script,
|
||||
config,
|
||||
input: {
|
||||
prompt,
|
||||
references: refs,
|
||||
params: {
|
||||
seconds: normalizeVideoSeconds(config.videoSeconds),
|
||||
size: normalizeVideoSize(config.size),
|
||||
resolution: normalizeVideoResolution(config.vquality),
|
||||
ratio: config.size,
|
||||
generateAudio: boolConfig(config.videoGenerateAudio, true),
|
||||
watermark: boolConfig(config.videoWatermark, false),
|
||||
},
|
||||
},
|
||||
signal: options?.signal,
|
||||
}),
|
||||
);
|
||||
const id = nanoid();
|
||||
pluginVideoResults.set(id, result);
|
||||
return { id, provider: "plugin", model };
|
||||
}
|
||||
|
||||
function videoPluginResult(result: unknown): VideoGenerationResult {
|
||||
if (result instanceof Blob) return { blob: result };
|
||||
if (typeof result === "string") return { url: result, mimeType: "video/mp4" };
|
||||
if (result && typeof result === "object") {
|
||||
const record = result as Record<string, unknown>;
|
||||
if (record.blob instanceof Blob) return { blob: record.blob };
|
||||
const url = [record.url, record.video_url, record.result_url].find((value) => typeof value === "string" && value) as string | undefined;
|
||||
if (url) return { url, mimeType: "video/mp4" };
|
||||
}
|
||||
throw new Error("模型调用脚本没有返回视频");
|
||||
}
|
||||
|
||||
export async function storeGeneratedVideo(result: VideoGenerationResult): Promise<UploadedFile> {
|
||||
if (result.blob) return uploadMediaFile(result.blob, "video");
|
||||
if (result.url) {
|
||||
|
||||
Reference in New Issue
Block a user