fix(canvas): 画布生成 - 隔离派生任务状态并修复视频模型切换
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { type GenerationTask } from "@/services/api/task-center";
|
||||
import { backendProviderConfig, runBackendGenerationTask } from "@/services/api/generation-task";
|
||||
import { defaultConfig, modelMatchesCapability, modelOptionName, normalizeModelOptionValue, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { configuredModelMatchesCapability, defaultConfig, normalizeModelOptionValue, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { resolveImageUrl, uploadImage } from "@/services/image-storage";
|
||||
import { resolveMediaUrl } from "@/services/file-storage";
|
||||
import { resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
@@ -316,7 +316,7 @@ export function resolveCanvasGenerationModel(config: AiConfig, model: string | u
|
||||
if (!model) return "";
|
||||
const normalized = normalizeModelOptionValue(model, config.channels);
|
||||
if (!normalized) return "";
|
||||
return modelMatchesCapability(modelOptionName(normalized), mode) ? normalized : "";
|
||||
return configuredModelMatchesCapability(config, normalized, mode) ? normalized : "";
|
||||
}
|
||||
|
||||
export function supportsVideoReferenceAudio(config: AiConfig) {
|
||||
|
||||
@@ -35,6 +35,7 @@ export async function executeVideoGeneration({
|
||||
const isEmptyVideoNode = sourceNode?.type === CanvasNodeType.Video && !sourceNode.metadata?.content;
|
||||
const isExistingVideoNode = sourceNode?.type === CanvasNodeType.Video && Boolean(sourceNode.metadata?.content);
|
||||
const videoId = isEmptyVideoNode ? nodeId : nanoid();
|
||||
const versionRootId = isExistingVideoNode && sourceNode ? sourceNode.metadata?.versionOfNodeId || sourceNode.id : undefined;
|
||||
const parent = sourceNode?.position || { x: 0, y: 0 };
|
||||
const videoGenerationMetadata = buildVideoGenerationMetadata(sourceNode, generationContext);
|
||||
const videoNode: CanvasNodeData = {
|
||||
@@ -63,17 +64,19 @@ export async function executeVideoGeneration({
|
||||
},
|
||||
};
|
||||
registerPendingNodeIds([videoId]);
|
||||
// 待生成版本先加入版本族,但只有成功结果才能替换当前主版本。
|
||||
setNodes((current) => {
|
||||
if (isEmptyVideoNode) return current.map((node) => (node.id === nodeId ? { ...node, ...videoNode } : node));
|
||||
if (!isExistingVideoNode || !sourceNode) return [...current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), videoNode];
|
||||
const rootId = sourceNode.metadata?.versionOfNodeId || sourceNode.id;
|
||||
const rootId = versionRootId!;
|
||||
const nextLabel = nextCanvasVersionLabel(rootId, current);
|
||||
const hasPrimaryVersion = current.some((node) => (node.metadata?.versionOfNodeId || node.id) === rootId && node.metadata?.versionPrimary);
|
||||
return [
|
||||
...current.map((node) => {
|
||||
if ((node.metadata?.versionOfNodeId || node.id) !== rootId) return node;
|
||||
return { ...node, metadata: { ...node.metadata, versionOfNodeId: rootId, versionLabel: node.metadata?.versionLabel || "A", versionPrimary: false, status: node.id === nodeId ? NODE_STATUS_SUCCESS : node.metadata?.status } };
|
||||
return { ...node, metadata: { ...node.metadata, versionOfNodeId: rootId, versionLabel: node.metadata?.versionLabel || "A", versionPrimary: node.metadata?.versionPrimary || (!hasPrimaryVersion && node.id === sourceNode.id), status: node.id === nodeId ? NODE_STATUS_SUCCESS : node.metadata?.status } };
|
||||
}),
|
||||
{ ...videoNode, metadata: { ...videoNode.metadata, versionOfNodeId: rootId, versionLabel: nextLabel, versionPrimary: true } },
|
||||
{ ...videoNode, metadata: { ...videoNode.metadata, versionOfNodeId: rootId, versionLabel: nextLabel, versionPrimary: false } },
|
||||
];
|
||||
});
|
||||
// 重新生成已有视频时,新节点继承源视频的上游连接,与源视频保持并行关系,而不是作为其下游子节点。
|
||||
@@ -91,9 +94,10 @@ export async function executeVideoGeneration({
|
||||
const video = await storeGeneratedVideo({ url: result.video.dataUrl, mimeType: result.video.mimeType || "video/mp4" });
|
||||
const videoSize = fitNodeSize(video.width || spec.width, video.height || spec.height, VIDEO_NODE_MAX_SIZE.width, VIDEO_NODE_MAX_SIZE.height);
|
||||
setNodes((current) => current.map((node) => {
|
||||
if (node.id !== videoId) return node;
|
||||
const isVersionSibling = Boolean(versionRootId && (node.metadata?.versionOfNodeId || node.id) === versionRootId);
|
||||
if (node.id !== videoId) return isVersionSibling ? { ...node, metadata: { ...node.metadata, versionPrimary: false } } : node;
|
||||
const geometry = node.metadata?.locked ? {} : { width: videoSize.width, height: videoSize.height, position: { x: node.position.x + node.width / 2 - videoSize.width / 2, y: node.position.y + node.height / 2 - videoSize.height / 2 } };
|
||||
return { ...node, ...geometry, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext), ...videoGenerationMetadata } };
|
||||
return { ...node, ...geometry, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext), ...videoGenerationMetadata, ...(versionRootId ? { versionPrimary: true } : {}) } };
|
||||
}));
|
||||
} finally {
|
||||
finishGenerationRequest(videoId, controller);
|
||||
|
||||
@@ -204,7 +204,8 @@ export function useCanvasGenerationExecutor({
|
||||
return;
|
||||
}
|
||||
|
||||
const markSourceStatus = sourceNode?.type !== CanvasNodeType.Image && !editingTextNode;
|
||||
// 已有内容节点只是本次生成的来源;任务状态归新目标所有,不能覆盖已成功结果。
|
||||
const markSourceStatus = !sourceNode?.metadata?.content && !editingTextNode;
|
||||
const statusPrompt = sourceNode?.type === CanvasNodeType.Config ? effectivePrompt : prompt;
|
||||
if (!effectivePrompt && (mode === "text" || mode === "audio")) {
|
||||
finishGenerationRequest(nodeId, controller);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
|
||||
import { buildGenerationConfig, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
|
||||
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
|
||||
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel } from "../src/lib/model-selection";
|
||||
import { defaultConfig, type AiConfig, type ModelChannel } from "../src/stores/use-config-store";
|
||||
@@ -52,6 +53,15 @@ function node(id: string, type: CanvasNodeType, generationMode?: "image" | "vide
|
||||
}
|
||||
|
||||
describe("逻辑模型选择", () => {
|
||||
test("后台标注的视频模型不因内部标识缺少视频关键词而回退", () => {
|
||||
const config = policyConfig();
|
||||
const selectedModel = config.videoModels[0]!;
|
||||
const videoNode = { ...node("video", CanvasNodeType.Video), metadata: { model: selectedModel } };
|
||||
|
||||
expect(resolveCanvasGenerationModel(config, selectedModel, "video")).toBe(selectedModel);
|
||||
expect(buildGenerationConfig(config, videoNode, "video").model).toBe(selectedModel);
|
||||
});
|
||||
|
||||
test("同渠道同显示名称合并为一个逻辑模型", () => {
|
||||
const config = policyConfig();
|
||||
const groups = groupModelsByDisplayName(config, config.videoModels);
|
||||
|
||||
Reference in New Issue
Block a user