fix(canvas): 短剧分镜 - 支持多镜头并行生成并正确引用角色图声资产
This commit is contained in:
@@ -124,6 +124,20 @@ export async function runBackendGenerationTask(
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate }, prepared, dependencies);
|
||||
}
|
||||
|
||||
// 分镜等后台生产流程只需要可靠提交任务;任务状态与产物由项目工作区轮询和
|
||||
// 后端自动回填负责,不能让页面 mutation 一直等待供应商完成。
|
||||
export async function submitBackendGenerationTask(
|
||||
options: BackendGenerationTaskOptions,
|
||||
dependencies: GenerationTaskDependencies = defaultDependencies,
|
||||
): Promise<GenerationTask> {
|
||||
throwIfAborted(options.signal);
|
||||
assertClientPromptLimit(options.mode, options.prompt, options.config, options.metadata);
|
||||
if (usesLocalDreamina(options.config)) throw new Error("本机即梦任务暂不支持后台提交");
|
||||
const prepared = await prepareGenerationReferences(options);
|
||||
throwIfAborted(options.signal);
|
||||
return createBackendGenerationTask(options, prepared, dependencies);
|
||||
}
|
||||
|
||||
export async function runBackendToolGenerationTask(options: {
|
||||
prompt: string;
|
||||
config: AiConfig;
|
||||
@@ -401,7 +415,14 @@ async function prepareGenerationReferences({
|
||||
}
|
||||
|
||||
async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {
|
||||
const { projectId, mode, prompt, config, signal, metadata, onTaskUpdate, onTextDelta } = options;
|
||||
const task = await createBackendGenerationTask(options, prepared, dependencies);
|
||||
const { signal, onTaskUpdate, onTextDelta } = options;
|
||||
const completed = await dependencies.waitTask(task.id, { signal, initialTask: task, onTaskUpdate, onTextDelta });
|
||||
return parseBackendGenerationResult(completed);
|
||||
}
|
||||
|
||||
async function createBackendGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {
|
||||
const { projectId, mode, prompt, config, metadata, onTaskUpdate } = options;
|
||||
const videoOperation = generationOperation(options);
|
||||
const workflow = resolveGenerationWorkflowExecution(config, mode);
|
||||
const logicalModelId = workflow ? "" : logicalModelIDForConfig(config);
|
||||
@@ -428,8 +449,7 @@ async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions
|
||||
},
|
||||
});
|
||||
onTaskUpdate?.(task);
|
||||
const completed = await dependencies.waitTask(task.id, { signal, initialTask: task, onTaskUpdate, onTextDelta });
|
||||
return parseBackendGenerationResult(completed);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function prepareBackendMediaReference(media: ReferenceVideo | ReferenceAudio) {
|
||||
|
||||
@@ -38,7 +38,12 @@ export type SeedanceTask = {
|
||||
};
|
||||
|
||||
export type ApiEnvelope<T> = T | { code?: number; data?: T | null; msg?: string };
|
||||
export type RequestOptions = { signal?: AbortSignal };
|
||||
export type RequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
videoEditOperation?: string;
|
||||
videoStartFrameNodeId?: string;
|
||||
videoEndFrameNodeId?: string;
|
||||
};
|
||||
|
||||
export type VideoGenerationResult = { blob?: Blob; url?: string; mimeType?: string };
|
||||
export type VideoGenerationTask = { id: string; provider: "openai" | "agnes" | "seedance" | "video-generations" | "gemini-veo" | "novita" | "minimax"; model: string };
|
||||
|
||||
@@ -15,14 +15,16 @@ export async function createMiniMaxVideoTask(deps: VideoProviderDeps, config: Re
|
||||
const videoUrls = await Promise.all(videoReferences.slice(0, 3).map((video) => resolveMiniMaxMediaUrl(video, "参考视频")));
|
||||
const audioUrls = await Promise.all(audioReferences.slice(0, 3).map((audio) => resolveMiniMaxMediaUrl(audio, "参考音频")));
|
||||
const content: Array<Record<string, unknown>> = [{ type: "text", text: prompt.trim() }];
|
||||
const explicitFrameMode = options?.videoEditOperation !== "reference_to_video" && Boolean(options?.videoStartFrameNodeId || options?.videoEndFrameNodeId);
|
||||
imageUrls.forEach((url, index) => {
|
||||
// 首尾帧只允许成对出现;三张及以上图片统一作为多模态参考图,避免提交非法组合。
|
||||
const role = videoUrls.length || audioUrls.length || imageUrls.length > 2 ? "reference_image" : index === 0 ? "first_frame" : "last_frame";
|
||||
const reference = references[index];
|
||||
const role = miniMaxImageRole(reference?.id, options);
|
||||
if (explicitFrameMode && role === "reference_image") throw new Error("MiniMax 首尾帧模式不能混合未标记的参考图");
|
||||
content.push({ type: "image_url", image_url: { url }, role });
|
||||
});
|
||||
videoUrls.forEach((url) => content.push({ type: "video_url", video_url: { url }, role: "reference_video" }));
|
||||
audioUrls.forEach((url) => content.push({ type: "audio_url", audio_url: { url }, role: "reference_audio" }));
|
||||
const frameMode = imageUrls.length > 0 && imageUrls.length <= 2 && videoUrls.length === 0 && audioUrls.length === 0;
|
||||
const frameMode = explicitFrameMode;
|
||||
const payload = {
|
||||
model: modelOptionName(model),
|
||||
content,
|
||||
@@ -41,6 +43,13 @@ export async function createMiniMaxVideoTask(deps: VideoProviderDeps, config: Re
|
||||
}
|
||||
}
|
||||
|
||||
function miniMaxImageRole(imageId: string | undefined, options?: RequestOptions) {
|
||||
if (options?.videoEditOperation === "reference_to_video") return "reference_image";
|
||||
if (imageId && imageId === options?.videoStartFrameNodeId) return "first_frame";
|
||||
if (imageId && imageId === options?.videoEndFrameNodeId) return "last_frame";
|
||||
return "reference_image";
|
||||
}
|
||||
|
||||
export async function pollMiniMaxVideoTask(deps: VideoProviderDeps, config: ResolvedAiConfig, task: VideoGenerationTask, options?: RequestOptions): Promise<VideoGenerationTaskState> {
|
||||
try {
|
||||
const response = await deps.transport.get<{ task?: MiniMaxVideoTask }>(miniMaxVideoUrl(config, `/query/video_generation/${encodeURIComponent(task.id)}`), options);
|
||||
|
||||
Reference in New Issue
Block a user