feat(model): enhance model configuration and script handling for audio and video capabilities

This commit is contained in:
HouYunFei
2026-07-15 10:13:32 +08:00
parent a4dcc679c9
commit c57f7d61a7
13 changed files with 690 additions and 266 deletions
+54 -2
View File
@@ -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) {