fix(canvas): 角色卡引用 - 修复三视图读取与本地图片生成(#650)
fix(canvas): 角色卡引用 - 修复三视图读取与本地图片生成
This commit is contained in:
@@ -10,7 +10,7 @@ import { isNeutralColorGrade, resolveCanvasColorGradeReference } from "@/lib/can
|
||||
import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
import { mediaConversionSourceFingerprint } from "@/lib/media-conversion/contracts";
|
||||
import { resolveCanvasDrawingReference } from "@/lib/canvas/canvas-drawing-reference";
|
||||
import { compileCharacterReferencePrompt } from "@/lib/canvas/canvas-character-reference";
|
||||
import { compileCharacterReferencePrompt, normalizeCharacterImageMentions } from "@/lib/canvas/canvas-character-reference";
|
||||
import { nodeReferenceImage } from "@/lib/canvas/canvas-project-generation";
|
||||
import { isCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
|
||||
import { audioFileExtension } from "@/lib/character-voice-formats";
|
||||
@@ -301,11 +301,13 @@ export function generationInputMentionLabel(input: NodeGenerationInput, inputs:
|
||||
}
|
||||
|
||||
export function normalizeGenerationNodeMentionTokens(prompt: string, inputs: NodeGenerationInput[]) {
|
||||
const labelByNodeId = new Map(generationSlotEntries(inputs).map(({ input, label }) => [input.nodeId, label]));
|
||||
return prompt.replace(/@\[node:([^\]]+)\]/g, (token, nodeId: string) => {
|
||||
const slots = generationSlotEntries(inputs);
|
||||
const labelByNodeId = new Map(slots.map(({ input, label }) => [input.nodeId, label]));
|
||||
const normalized = prompt.replace(/@\[node:([^\]]+)\]/g, (token, nodeId: string) => {
|
||||
const label = labelByNodeId.get(nodeId);
|
||||
return label ? `@${label}` : token;
|
||||
});
|
||||
return normalizeCharacterImageMentions(normalized, slots.filter(({ input }) => input.type === "image").length, slots.filter(({ input }) => input.type === "character").map(({ label }) => label));
|
||||
}
|
||||
|
||||
function hasResolvableGenerationMention(prompt: string, inputs: NodeGenerationInput[]) {
|
||||
|
||||
@@ -18,6 +18,14 @@ export type CharacterBreakdown = {
|
||||
voiceTimbre: string;
|
||||
};
|
||||
|
||||
export function normalizeCharacterImageMentions(prompt: string, imageCount: number, characterLabels: string[]) {
|
||||
if (imageCount > 0 || !characterLabels.length) return prompt;
|
||||
return prompt.replace(/@图片([1-9]\d*)(?=$|\s|[,.!?;:,。!?;:、)\]}】)])/g, (token, index: string) => {
|
||||
const label = characterLabels[Number(index) - 1];
|
||||
return label ? "@" + label : token;
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshCanvasCharacterReferenceNodes(nodes: CanvasNodeData[], assets: ProjectAsset[]) {
|
||||
const characters = new Map(assets.filter((asset) => asset.category === "character" && asset.character).map((asset) => [asset.id, asset]));
|
||||
let changed = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { imageReferenceLabel } from "@/lib/image-reference-prompt";
|
||||
import { normalizeCharacterImageMentions } from "@/lib/canvas/canvas-character-reference";
|
||||
import { canvasNodeVideoPreviewUrl, canvasVideoAssetPreviewUrl } from "@/lib/canvas/canvas-media-preview";
|
||||
import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt";
|
||||
import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
@@ -113,10 +114,12 @@ export function canvasResourceMentionToken(reference: CanvasResourceReference) {
|
||||
}
|
||||
|
||||
export function normalizeCanvasNodeMentionTokens(prompt: string, references: CanvasResourceReference[]) {
|
||||
return references.reduce((value, reference) => {
|
||||
const normalized = references.reduce((value, reference) => {
|
||||
if (!reference.nodeId || reference.assetId || reference.kind === "skill" || reference.kind === "tool") return value;
|
||||
return value.split(canvasNodeMentionToken(reference.nodeId)).join(`@${reference.label}`);
|
||||
}, prompt);
|
||||
const active = references.filter((reference) => reference.active && !reference.assetId);
|
||||
return normalizeCharacterImageMentions(normalized, active.filter((reference) => reference.kind === "image").length, active.filter((reference) => reference.kind === "character").map((reference) => reference.label));
|
||||
}
|
||||
|
||||
/** 将提示词内已连接素材的名称替换为对应引用;同名的每处指代都会保留为独立引用。 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { buildNodeGenerationContext } from "../src/components/canvas/canvas-node-generation";
|
||||
import { buildCanvasResourceReferences, getGenerationResourceNodes } from "../src/lib/canvas/canvas-resource-references";
|
||||
import { buildCanvasResourceReferences, getGenerationResourceNodes, normalizeCanvasNodeMentionTokens } from "../src/lib/canvas/canvas-resource-references";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
|
||||
|
||||
function node(id: string, type: CanvasNodeType, content: string): CanvasNodeData {
|
||||
@@ -33,6 +33,36 @@ function connection(fromNodeId: string): CanvasConnection {
|
||||
}
|
||||
|
||||
describe("canvas node generation position mentions", () => {
|
||||
test("角色卡三视图可解析图片别名,编辑器与生成统一显示角色引用", () => {
|
||||
const target = targetNode();
|
||||
const character = node("character", CanvasNodeType.Text, "");
|
||||
character.metadata = { workflowKind: "character", characterAssetId: "character-asset", characterVersionPolicy: "current" };
|
||||
const nodes = [target, character];
|
||||
const connections = [connection(character.id)];
|
||||
const references = buildCanvasResourceReferences(nodes, connections, target.id);
|
||||
const prompt = "严格参考 @图片1,保持服装一致。";
|
||||
expect(normalizeCanvasNodeMentionTokens(prompt, references)).toBe("严格参考 @角色1,保持服装一致。");
|
||||
const context = buildNodeGenerationContext(target.id, nodes, connections, prompt, []);
|
||||
expect(context.prompt).toBe("严格参考 @角色1,保持服装一致。");
|
||||
expect(context.characterReferences).toEqual([{ nodeId: character.id, assetId: "character-asset", requestedVersionId: undefined }]);
|
||||
expect(() => buildNodeGenerationContext(target.id, nodes, connections, "参考 @图片2", [])).toThrow("@图片2");
|
||||
});
|
||||
|
||||
test("角色卡与普通图片混合时,图片编号不指向角色卡", () => {
|
||||
const target = targetNode();
|
||||
const character = node("character", CanvasNodeType.Text, "");
|
||||
character.metadata = { workflowKind: "character", characterAssetId: "character-asset" };
|
||||
const image = node("scene", CanvasNodeType.Image, "data:image/png;base64,AA==");
|
||||
const nodes = [target, character, image];
|
||||
const connections = [connection(character.id), connection(image.id)];
|
||||
const prompt = "@角色1 站在 @图片1 场景内。";
|
||||
const references = buildCanvasResourceReferences(nodes, connections, target.id);
|
||||
expect(normalizeCanvasNodeMentionTokens(prompt, references)).toBe(prompt);
|
||||
const context = buildNodeGenerationContext(target.id, nodes, connections, prompt, []);
|
||||
expect(context.referenceImages.map((item) => item.id)).toEqual([image.id]);
|
||||
expect(context.characterReferences.map((item) => item.nodeId)).toEqual([character.id]);
|
||||
});
|
||||
|
||||
test("父图无预览时仍可继承输入,显式子图输入优先且去重", () => {
|
||||
const a = node("a", CanvasNodeType.Image, "");
|
||||
a.metadata = { storageKey: "resource:a" };
|
||||
|
||||
Reference in New Issue
Block a user