部分模型不遵守 JSON Schema:直接返回顶层数组、数组外再包一层、每个角色单独包一层 characters、或正文先列出角色名数组再给出契约对象。原解析只取第一个可解析 JSON 值,容易命中旁枝片段并被判为解析失败。 - prompt_template.go: extractPreferredJSONText 优先选取包含 preferKey 的顶层对象候选;角色卡校验按形态归一处理,结果为空仍提示未识别到角色,字段缺失报出具体角色序号与字段名 - storyboard.go: normalizeStoryboardRootJSON 兼容单元素 plan 数组解包与镜头对象数组自动包装为完整 plan - canvas-character-reference.ts: 前端角色卡提取同步统一归一逻辑 - 补充前后端形态兼容回归测试与 CHANGELOG
107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import { parseCharacterBreakdown } from "../src/lib/canvas/canvas-character-reference";
|
||
|
||
describe("parseCharacterBreakdown", () => {
|
||
test("忽略推理文字中的 JSON 碎片,读取最终角色契约对象", () => {
|
||
const result = `让我先分析正文。\n\naliases: [](正文未提供别名)\n\n最终结果:\n\n\`\`\`json
|
||
{
|
||
"characters": [
|
||
{
|
||
"name": "林夏",
|
||
"aliases": [],
|
||
"role": "女主角,26岁,加班晚归的都市上班族",
|
||
"appearance": "26岁东亚女性,面带疲态",
|
||
"clothing": "米色风衣,背深色帆布工作包",
|
||
"physique": "年轻女性常规体态",
|
||
"personality": "善良内敛,观察力强",
|
||
"props": "深色帆布工作包,磁吸小夜灯",
|
||
"consistencyPrompt": "26岁东亚女性,米色风衣,深色帆布工作包",
|
||
"multiViewPrompt": "正面、侧面、背面保持风衣和帆布包一致",
|
||
"voiceLanguage": "中文普通话",
|
||
"voiceAge": "26岁青年女性",
|
||
"voiceTimbre": "温和、略带疲惫感"
|
||
},
|
||
{
|
||
"name": "吴奶奶",
|
||
"aliases": [],
|
||
"role": "林夏的对门邻居,七十岁出头老人",
|
||
"appearance": "七十岁出头东亚老年女性,银灰短发",
|
||
"clothing": "深绿色针织开衫",
|
||
"physique": "老年女性体态",
|
||
"personality": "独立谨慎,感恩含蓄",
|
||
"props": "米色布袋和钥匙",
|
||
"consistencyPrompt": "银灰短发,深绿色针织开衫,米色布袋",
|
||
"multiViewPrompt": "正面、侧面、背面保持短发和开衫一致",
|
||
"voiceLanguage": "中文普通话",
|
||
"voiceAge": "七十岁出头老年女性",
|
||
"voiceTimbre": "语速偏慢的老年女性声线"
|
||
}
|
||
]
|
||
}
|
||
\`\`\``;
|
||
|
||
expect(parseCharacterBreakdown(result).map((character) => character.name)).toEqual(["林夏", "吴奶奶"]);
|
||
});
|
||
});
|
||
|
||
const characterCard = {
|
||
name: "林夏",
|
||
aliases: ["夏夏"],
|
||
role: "女主角,加班晚归的都市上班族",
|
||
appearance: "26岁东亚女性,短发",
|
||
clothing: "米色风衣",
|
||
physique: "年轻女性常规体态",
|
||
personality: "善良内敛",
|
||
props: "磁吸小夜灯",
|
||
consistencyPrompt: "短发,米色风衣",
|
||
multiViewPrompt: "正面、侧面、背面保持一致",
|
||
voiceLanguage: "中文普通话",
|
||
voiceAge: "26岁青年女性",
|
||
voiceTimbre: "温和",
|
||
};
|
||
|
||
describe("parseCharacterBreakdown 顶层形态兼容", () => {
|
||
test("接受顶层角色数组", () => {
|
||
expect(parseCharacterBreakdown(JSON.stringify([characterCard])).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("接受数组外再包一层", () => {
|
||
expect(parseCharacterBreakdown(JSON.stringify([[characterCard]])).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("接受每个角色单独包一层 characters", () => {
|
||
const payload = [{ characters: [characterCard] }];
|
||
expect(parseCharacterBreakdown(JSON.stringify(payload)).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("接受 characters 写成以角色名为键的对象", () => {
|
||
const payload = { characters: { 林夏: characterCard } };
|
||
expect(parseCharacterBreakdown(JSON.stringify(payload)).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("跳过数组里的非角色元素", () => {
|
||
const payload = [{ index: 1 }, characterCard];
|
||
expect(parseCharacterBreakdown(JSON.stringify(payload)).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("正文先列角色名数组时仍取到最终契约对象", () => {
|
||
const result = `本章角色:["林夏","陈默"]\n\n${JSON.stringify({ characters: [characterCard] })}`;
|
||
expect(parseCharacterBreakdown(result).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("顶层数组被 markdown 代码块包裹", () => {
|
||
const result = "```json\n" + JSON.stringify([characterCard]) + "\n```";
|
||
expect(parseCharacterBreakdown(result).map((character) => character.name)).toEqual(["林夏"]);
|
||
});
|
||
|
||
test("角色名单数组不是角色卡,继续向后寻找契约对象", () => {
|
||
const result = `旁白提到了名字:["林夏"]`;
|
||
expect(() => parseCharacterBreakdown(result)).toThrow(/符合契约的 JSON/);
|
||
});
|
||
|
||
test("角色缺少设定字段时报出角色名", () => {
|
||
const payload = [{ name: "林夏", role: "女主角" }];
|
||
expect(() => parseCharacterBreakdown(JSON.stringify(payload))).toThrow(/林夏/);
|
||
});
|
||
});
|