Files
ttstd-infinite-canvas/web/test/ai-art-critique.test.ts
QAyong a6504c0e00 feat(plugin): AI 审美批改 - 增加结构化视觉分析与精准标注 (#387)
* feat(portrait-clearance): 肖像排查 - 增加画布插件与本机检测链路

* fix(web): 插件中心 - 修复应用插件筛选与自动定位

* feat(plugin): AI审美批改 - 完成分析报告与问题标注闭环
2026-09-02 20:32:58 +08:00

661 lines
30 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import "../src/lib/plugins/builtin/ai-art-critique";
import { getNodeAcceptedInputKind, getNodeDefinition } from "../src/lib/canvas/node-registry";
import { resolveAddNodeMenuCommands, type AddNodeMenuContext } from "../src/lib/canvas/tool-registry";
import { ART_CRITIQUE_NODE_TYPE, ART_CRITIQUE_RUBRIC_VERSION } from "../src/lib/art-critique/contracts";
import { ART_CRITIQUE_SEVERITY_COLORS, isRenderableArtCritiqueTarget, referenceTargetForIssue, repairIssueTarget, targetBounds, targetPolygon } from "../src/lib/art-critique/annotation";
import { artCritiqueMessages, parseArtCritiqueResponse } from "../src/lib/art-critique/review";
import {
applyGrounding,
applyEditPrompts,
applyReferenceCoordinates,
applyVerification,
artCritiquePipelineTools,
deduplicateAggregateIssues,
filterAggregateAgainstCandidates,
parseAggregatePayload,
parseEditPromptPayload,
parseGroundingPayload,
parseSceneReviewPayload,
parseVerificationPayload,
} from "../src/lib/art-critique/pipeline";
import { ART_CRITIQUE_RUBRIC, buildArtCritiqueRubricPrompt } from "../src/lib/art-critique/rubrics";
import type { ToolResponseResult } from "../src/services/api/image";
function response(value: unknown): ToolResponseResult {
return {
content: "",
toolCalls: [{ id: "call-1", type: "function", function: { name: "submit_art_critique", arguments: JSON.stringify(value) } }],
};
}
const input = { dataUrl: "data:image/png;base64,placeholder", title: "测试图片", sourceFingerprint: "v1-test" };
describe("AI 审美批改插件注册", () => {
test("节点只接受图片并隐藏输出端口", () => {
expect(getNodeAcceptedInputKind(ART_CRITIQUE_NODE_TYPE)).toBe("image");
expect(getNodeDefinition(ART_CRITIQUE_NODE_TYPE)?.showOutputConnection).toBe(false);
});
test("启用后出现在添加节点菜单", () => {
const baseContext = {
workspaceMode: "simple" as const,
isProjectLinked: false,
handlers: {} as AddNodeMenuContext["handlers"],
};
const enabledCommands = resolveAddNodeMenuCommands({ ...baseContext, enabledPluginIds: new Set(["ai-art-critique"]) });
const disabledCommands = resolveAddNodeMenuCommands({ ...baseContext, enabledPluginIds: new Set<string>() });
expect(enabledCommands.some((command) => command.id === ART_CRITIQUE_NODE_TYPE)).toBe(true);
expect(disabledCommands.some((command) => command.id === ART_CRITIQUE_NODE_TYPE)).toBe(false);
});
});
describe("AI 审美批改报告解析", () => {
test("解析 AI 生成的局部编辑提示词并按问题 ID 应用", () => {
const issue = {
id: "issue-1",
category: "lighting",
title: "主体脸部受光不足",
explanation: "脸部暗部过重,表情与互动的可读性下降。",
severity: "high",
confidence: 0.9,
target: {
type: "box",
points: [
{ x: 0.3, y: 0.2 },
{ x: 0.6, y: 0.6 },
],
},
targetDescription: "女性主体的脸部",
suggestion: {
goal: "提升人物表情的可读性",
actions: ["提高脸部局部亮度", "保留原有光线方向"],
preserve: ["保留夜景氛围"],
expectedEffect: "人物重新成为视觉焦点",
},
} satisfies import("../src/lib/art-critique/contracts").ArtCritiqueIssue;
const result = parseEditPromptPayload({
prompts: [{ issueId: "issue-1", editPrompt: "只提高女性主体脸部的局部亮度,保留窗光方向、人物身份、构图和夜景氛围。" }],
});
const warnings: string[] = [];
const applied = applyEditPrompts([issue], result.prompts, warnings);
expect(applied[0].editPrompt).toContain("女性主体脸部");
expect(warnings).toHaveLength(0);
});
test("AI 提示词缺失时不使用本地拼接替代", () => {
const issue = {
id: "issue-1",
category: "lighting" as const,
title: "主体脸部受光不足",
explanation: "脸部暗部过重。",
severity: "high" as const,
confidence: 0.9,
target: { type: "global" as const, points: [] },
suggestion: { goal: "提升可读性", actions: ["提高局部亮度"], preserve: ["保留夜景氛围"], expectedEffect: "人物更突出" },
};
const warnings: string[] = [];
const applied = applyEditPrompts([issue], [], warnings);
expect(applied[0].editPrompt).toBeUndefined();
expect(warnings[0]).toContain("不会使用本地拼接替代");
});
test("使用版本化 Rubric 并把高优先级问题排在前面", () => {
const report = parseArtCritiqueResponse(
response({
summary: "画面有明确主体。",
strengths: ["色彩统一"],
issues: [
{
id: "low-first",
category: "color",
title: "颜色略杂",
explanation: "次要区域饱和度略高。",
severity: "low",
confidence: 0.98,
target: { type: "global", points: [] },
suggestion: { goal: "收拢色彩", actions: ["降低次要区域饱和度"], preserve: ["保留主体强调色"], expectedEffect: "主体更突出" },
},
{
id: "high-second",
category: "composition",
title: "主体方向感不足",
explanation: "主体与留白方向关系不清。",
severity: "high",
confidence: 0.7,
target: {
type: "box",
points: [
{ x: 0.2, y: 0.2 },
{ x: 0.6, y: 0.8 },
],
},
suggestion: { goal: "增强方向感", actions: ["调整主体位置"], preserve: ["保留人物姿态"], expectedEffect: "视线移动更自然" },
},
],
}),
input,
);
expect(report.rubricVersion).toBe(ART_CRITIQUE_RUBRIC_VERSION);
expect(report.issues.map((issue) => issue.id)).toEqual(["high-second", "low-first"]);
});
test("Prompt 包含 Rubric 版本、参考方法和构图意图约束", () => {
const systemText = artCritiqueMessages(input)[0].content;
expect(typeof systemText).toBe("string");
expect(systemText).toContain(ART_CRITIQUE_RUBRIC_VERSION);
expect(systemText).toContain("CADB / SAMP-Net");
expect(systemText).toContain("不能仅因没有使用三分法就报告");
expect(buildArtCritiqueRubricPrompt()).toContain("HumanAesExpert");
expect(ART_CRITIQUE_RUBRIC).toHaveLength(4);
});
test("窄 Reviewer Prompt 只注入自己的规则并允许没有问题", () => {
const prompt = buildArtCritiqueRubricPrompt({ categories: ["color"], includeReferenceMapping: false });
expect(prompt).toContain("【色彩|color】");
expect(prompt).not.toContain("【构图与视觉层级|composition】");
expect(prompt).not.toContain("参考方法映射");
expect(prompt).toContain("允许没有问题");
});
test("保留报告合同并把不完整的局部目标降级为全局问题", () => {
const report = parseArtCritiqueResponse(
response({
summary: "主体明确,但画面层次还可以更集中。",
strengths: ["主体轮廓清楚"],
issues: [
{
id: "composition-1",
category: "composition",
title: "主体偏离视觉重心",
explanation: "主体周围留白不均。",
severity: "high",
confidence: 0.86,
target: { type: "box", points: [{ x: 0.2, y: 0.3 }] },
suggestion: { goal: "集中注意力", actions: ["收紧左侧留白"], preserve: ["保留主体姿态"], expectedEffect: "视觉重心更稳定" },
},
],
}),
input,
);
expect(report.sourceFingerprint).toBe("v1-test");
expect(report.issues).toHaveLength(1);
expect(report.issues[0].target).toEqual({ type: "global", points: [] });
});
test("最多保留五个问题并裁剪越界坐标", () => {
const issues = Array.from({ length: 8 }, (_, index) => ({
id: `issue-${index}`,
category: "color",
title: `问题 ${index}`,
explanation: "需要调整。",
severity: "low",
confidence: 2,
target: { type: "point", points: [{ x: -1, y: 2 }] },
suggestion: { goal: "改善", actions: ["调整"], preserve: [], expectedEffect: "更统一" },
}));
const report = parseArtCritiqueResponse(response({ summary: "总结", strengths: [], issues }), input);
expect(report.issues).toHaveLength(5);
expect(report.issues[0].confidence).toBe(1);
expect(report.issues[0].target.points[0]).toEqual({ x: 0, y: 1 });
});
});
describe("AI 审美批改标注几何", () => {
test("box 目标转换为四边形并计算边界", () => {
const target = {
type: "box" as const,
points: [
{ x: 0.8, y: 0.7 },
{ x: 0.2, y: 0.1 },
],
};
expect(targetPolygon(target)).toEqual([
{ x: 0.2, y: 0.1 },
{ x: 0.8, y: 0.1 },
{ x: 0.8, y: 0.7 },
{ x: 0.2, y: 0.7 },
]);
expect(targetBounds(target)).toEqual({ x: 0.2, y: 0.1, width: 0.6000000000000001, height: 0.6 });
});
test("severity 使用固定颜色角色", () => {
expect(ART_CRITIQUE_SEVERITY_COLORS.high).toBe("#ef4444");
expect(ART_CRITIQUE_SEVERITY_COLORS.medium).toBe("#f59e0b");
});
test("定位失败时用问题描述生成参考区域或多个参考点", () => {
const foreground = referenceTargetForIssue({
category: "composition",
title: "前景人物压过主体",
explanation: "左侧前景的视觉重量过重。",
targetDescription: "左侧前景人物与主体区域",
});
const table = referenceTargetForIssue({
category: "proportion",
title: "桌沿切断人物关系",
explanation: "桌沿和桌腿位于画面下方。",
targetDescription: "桌沿、桌腿和桌面静物",
});
const highlights = referenceTargetForIssue({
category: "color",
title: "多处高饱和暖色亮点分散焦点",
explanation: "画面中存在多个分散的暖色亮点。",
targetDescription: "多处高饱和暖色亮点",
});
expect(foreground.type).toBe("box");
expect(targetBounds(foreground).width).toBeCloseTo(0.36);
expect(table.type).toBe("box");
expect(targetBounds(table).y).toBe(0.5);
expect(highlights.type).toBe("points");
expect(highlights.points.length).toBeGreaterThan(1);
});
test("无效框坐标会被识别并修复为可见参考框", () => {
const issue = {
id: "invalid-box",
category: "composition" as const,
title: "前景暗部压过主体",
explanation: "左侧前景形成连续暗部。",
severity: "high" as const,
confidence: 0.9,
target: {
type: "box" as const,
points: [
{ x: 0.2, y: 0.1 },
{ x: 0.8, y: 0.1 },
],
},
targetDescription: "左侧前景人物",
suggestion: { goal: "减轻干扰", actions: ["降低前景对比"], preserve: [], expectedEffect: "主体更突出" },
};
expect(isRenderableArtCritiqueTarget(issue.target)).toBe(false);
const repaired = repairIssueTarget(issue);
expect(repaired.targetSource).toBe("reference");
expect(repaired.target.type).toBe("box");
expect(targetBounds(repaired.target).height).toBeGreaterThan(0.8);
});
});
describe("AI 审美批改多阶段管线", () => {
test("阶段工具覆盖场景、Reviewer、聚合、定位、提示词生成和复核", () => {
expect(Object.values(artCritiquePipelineTools).map((tool) => tool.function.name)).toEqual([
"analyze_art_scene",
"review_art_composition",
"review_art_color",
"review_art_lighting",
"review_art_structure",
"aggregate_art_critique",
"ground_art_critique_issues",
"generate_art_edit_prompts",
"verify_art_critique",
]);
});
test("解析场景和候选问题", () => {
const result = parseSceneReviewPayload({
scene: {
imageType: "portrait",
style: ["电影感"],
subjects: [{ id: "person", description: "坐在桌边的人物", importance: "primary" }],
intendedFocus: "人物表情",
compositionType: ["对角线"],
lightingType: ["窗光"],
mood: "安静",
estimatedIntent: "表现人物的孤独感",
sceneDepth: "medium",
},
candidates: [
{
id: "c-1",
checkId: "subject-placement",
category: "composition",
title: "主体被左侧前景压住",
observation: "左侧前景占据很大面积",
reason: "人物视觉重量被削弱",
evidence: ["前景轮廓覆盖画面左侧"],
severity: 0.8,
confidence: 0.9,
targetDescription: "左侧前景与人物区域",
},
],
});
expect(result.scene.imageType).toBe("portrait");
expect(result.candidates[0].reviewer).toBe("composition");
expect(result.candidates[0].confidence).toBe(0.9);
});
test("按规则 ID、Reviewer、图片类型、证据和置信度过滤候选", () => {
const result = parseSceneReviewPayload({
scene: {
imageType: "landscape",
style: [],
subjects: [],
intendedFocus: "风景层次",
compositionType: [],
lightingType: [],
mood: "平静",
estimatedIntent: "表现空间感",
sceneDepth: "deep",
},
candidates: [
{ id: "valid", checkId: "visual-balance", category: "composition", title: "有效问题", observation: "有证据", reason: "影响表达", evidence: ["左侧重量明显"], severity: 0.8, confidence: 0.8, targetDescription: "左侧" },
{ id: "no-rule", checkId: "made-up-rule", category: "composition", title: "未知规则", observation: "有证据", reason: "影响表达", evidence: ["证据"], severity: 0.8, confidence: 0.9, targetDescription: "画面" },
{ id: "no-evidence", checkId: "visual-balance", category: "composition", title: "没有证据", observation: "看起来不平衡", reason: "影响表达", evidence: [], severity: 0.8, confidence: 0.9, targetDescription: "画面" },
{ id: "low-confidence", checkId: "visual-balance", category: "composition", title: "置信度低", observation: "证据不足", reason: "影响表达", evidence: ["模糊"], severity: 0.8, confidence: 0.2, targetDescription: "画面" },
{ id: "wrong-reviewer", checkId: "visual-balance", category: "color", title: "类别不匹配", observation: "有证据", reason: "影响表达", evidence: ["证据"], severity: 0.8, confidence: 0.9, targetDescription: "画面" },
],
});
expect(result.candidates.map((candidate) => candidate.id)).toEqual(["valid"]);
expect(result.candidates[0].checkId).toBe("visual-balance");
});
test("聚合结果保留建议并按优先级排序", () => {
const result = parseAggregatePayload({
summary: "主体明确,但层级还可以更集中。",
strengths: ["色彩统一"],
issues: [
{
id: "low",
category: "color",
title: "次要区域略鲜艳",
explanation: "背景有局部高饱和色块。",
severity: "low",
confidence: 0.95,
targetDescription: "背景右上角",
suggestion: { goal: "降低干扰", actions: ["降低饱和度"], preserve: ["保留主体强调色"], expectedEffect: "主体更突出" },
sourceCandidateIds: ["c-2"],
},
{
id: "high",
category: "lighting",
title: "人物和背景分离不足",
explanation: "人物脸部和墙面明度接近。",
severity: "high",
confidence: 0.72,
targetDescription: "人物脸部和后方墙面",
suggestion: { goal: "增强分离", actions: ["提高脸部局部亮度"], preserve: ["保留窗光方向"], expectedEffect: "焦点更清楚" },
sourceCandidateIds: ["c-3"],
},
],
});
expect(result.issues.map((issue) => issue.id)).toEqual(["high", "low"]);
expect(result.issues[0].suggestion.actions).toEqual(["提高脸部局部亮度"]);
});
test("聚合结果只能引用真实候选并过滤无来源问题", () => {
const candidates = [
{
id: "c-valid",
checkId: "palette",
kind: "issue" as const,
category: "color" as const,
title: "背景颜色干扰",
observation: "背景右上角有一块高饱和色块",
reason: "它抢走了主体的注意力",
evidence: ["右上角色块与主体形成强烈饱和度对比"],
severity: 0.8,
confidence: 0.85,
targetDescription: "背景右上角",
reviewer: "color" as const,
},
];
const aggregate = parseAggregatePayload({
summary: "主体明确。",
strengths: ["主体清楚"],
issues: [
{
id: "valid",
category: "color",
title: "背景颜色干扰",
explanation: "背景高饱和色块抢走注意力。",
severity: "medium",
confidence: 0.8,
targetDescription: "背景右上角",
suggestion: { goal: "降低干扰", actions: ["降低饱和度"], preserve: ["保留主体颜色"], expectedEffect: "主体更突出" },
sourceCandidateIds: ["c-valid"],
},
{
id: "invented",
category: "lighting",
title: "凭空新增的问题",
explanation: "输入候选中不存在这个问题。",
severity: "high",
confidence: 0.9,
targetDescription: "画面",
suggestion: { goal: "改善", actions: ["调整"], preserve: [], expectedEffect: "更好" },
sourceCandidateIds: ["missing-candidate"],
},
],
});
const warnings: string[] = [];
const filtered = filterAggregateAgainstCandidates(aggregate, candidates, warnings);
expect(filtered.issues.map((issue) => issue.id)).toEqual(["valid"]);
expect(warnings).toHaveLength(1);
});
test("可选方向不能伪装成问题,且只能引用同类候选", () => {
const candidates = [
{
id: "c-option",
checkId: "temperature",
kind: "option" as const,
category: "color" as const,
title: "尝试更冷的氛围",
observation: "画面当前冷暖平衡,存在向冷色偏移的空间",
reason: "这可能让情绪更克制,但不是确定性缺陷",
evidence: ["主体和背景的冷暖关系清楚"],
severity: 0.6,
confidence: 0.82,
targetDescription: "整体色调",
reviewer: "color" as const,
},
];
const aggregate = parseAggregatePayload({
summary: "画面整体成立。",
strengths: ["色彩关系清楚"],
issues: [
{
id: "wrong-issue",
category: "color",
title: "色彩错误",
explanation: "把可选方向误写成了问题。",
severity: "medium",
confidence: 0.9,
targetDescription: "整体色调",
suggestion: { goal: "改善", actions: ["调整"], preserve: [], expectedEffect: "更好" },
sourceCandidateIds: ["c-option"],
},
],
options: [
{
id: "valid-option",
category: "color",
title: "尝试更冷的氛围",
explanation: "如果想强化克制感,可以考虑向冷色移动。",
confidence: 0.84,
suggestion: { goal: "尝试冷色氛围", actions: ["小幅降低色温"], preserve: ["保留主体肤色"], expectedEffect: "情绪更克制" },
sourceCandidateIds: ["c-option"],
},
],
});
const warnings: string[] = [];
const filtered = filterAggregateAgainstCandidates(aggregate, candidates, warnings);
expect(filtered.issues).toHaveLength(0);
expect(filtered.options.map((option) => option.id)).toEqual(["valid-option"]);
expect(warnings).toHaveLength(1);
});
test("低置信度定位降级为全局,复核拒绝项被过滤", () => {
const issues = [
{
id: "confirmed",
category: "composition" as const,
title: "主体位置",
explanation: "主体偏向一侧。",
severity: "high" as const,
confidence: 0.9,
target: { type: "global" as const, points: [] },
suggestion: { goal: "改善", actions: ["调整位置"], preserve: [], expectedEffect: "更平衡" },
},
{
id: "rejected",
category: "color" as const,
title: "颜色问题",
explanation: "颜色关系不清。",
severity: "medium" as const,
confidence: 0.8,
target: { type: "global" as const, points: [] },
suggestion: { goal: "改善", actions: ["调整颜色"], preserve: [], expectedEffect: "更统一" },
},
];
const grounded = applyGrounding(issues, [
{
issueId: "confirmed",
target: {
type: "box",
points: [
{ x: 0.2, y: 0.2 },
{ x: 0.6, y: 0.8 },
],
},
groundingConfidence: 0.9,
},
{
issueId: "rejected",
target: {
type: "box",
points: [
{ x: 0.1, y: 0.1 },
{ x: 0.4, y: 0.4 },
],
},
groundingConfidence: 0.4,
},
]);
expect(grounded[0].target.type).toBe("box");
expect(grounded[1].target.type).toBe("global");
const verified = applyVerification(grounded, [
{ issueId: "confirmed", verification: { verdict: "confirmed", confidence: 0.92, reason: "图像证据清楚" } },
{ issueId: "rejected", verification: { verdict: "rejected", confidence: 0.9, reason: "与图像不符" } },
]);
expect(verified.issues.map((issue) => issue.id)).toEqual(["confirmed"]);
expect(verified.summary).toEqual({ checked: 2, confirmed: 1, uncertain: 0, rejected: 1 });
});
test("定位服务失败时应用参考坐标并标记来源", () => {
const issues = [
{
id: "reference-1",
category: "composition" as const,
title: "左侧前景人物过重",
explanation: "左侧前景人物形成连续暗色块。",
severity: "high" as const,
confidence: 0.9,
target: { type: "global" as const, points: [] },
targetDescription: "左侧前景人物",
suggestion: { goal: "减轻干扰", actions: ["收窄前景人物"], preserve: [], expectedEffect: "主体更突出" },
},
];
const grounded = applyReferenceCoordinates(issues);
expect(grounded[0].target.type).toBe("box");
expect(grounded[0].targetSource).toBe("reference");
});
test("定位和复核解析保留置信度", () => {
const grounding = parseGroundingPayload({ targets: [{ issueId: "i-1", target: { type: "global", points: [] }, groundingConfidence: 0.61 }] });
const verification = parseVerificationPayload({ decisions: [{ issueId: "i-1", verdict: "uncertain", confidence: 0.77, reason: "局部证据不够明确" }] });
expect(grounding.targets[0].groundingConfidence).toBe(0.61);
expect(verification.decisions[0].verification.verdict).toBe("uncertain");
});
test("定位解析支持分散问题的多个参考点", () => {
const grounding = parseGroundingPayload({
targets: [
{
issueId: "i-2",
target: {
type: "points",
points: [
{ x: 0.2, y: 0.2 },
{ x: 0.8, y: 0.7 },
],
},
groundingConfidence: 0.8,
},
],
});
expect(grounding.targets[0].target.type).toBe("points");
expect(targetPolygon(grounding.targets[0].target)).toHaveLength(2);
});
test("模型返回无效框时改用参考区域而不是退化横线", () => {
const issues = [
{
id: "grounding-invalid-box",
category: "composition" as const,
title: "前景暗部过重",
explanation: "左侧前景压过主体动线。",
severity: "high" as const,
confidence: 0.9,
target: { type: "global" as const, points: [] },
targetDescription: "左侧前景人物",
suggestion: { goal: "减轻前景重量", actions: ["缩小前景人物"], preserve: [], expectedEffect: "主体更突出" },
},
];
const grounded = applyGrounding(issues, [
{
issueId: "grounding-invalid-box",
target: {
type: "box",
points: [
{ x: 0.2, y: 0.1 },
{ x: 0.8, y: 0.1 },
],
},
groundingConfidence: 0.92,
},
]);
expect(grounded[0].targetSource).toBe("reference");
expect(grounded[0].target.type).toBe("box");
expect(targetBounds(grounded[0].target).height).toBeGreaterThan(0.8);
});
test("同类同根因问题会合并并保留来源与建议", () => {
const issues = [
{
id: "face-light",
category: "lighting" as const,
title: "主体脸部受光不足",
explanation: "脸部处于暗部,表情不清晰。",
severity: "high" as const,
confidence: 0.9,
targetDescription: "主体脸部",
suggestion: { goal: "提升表情可读性", actions: ["提高脸部局部亮度"], preserve: ["保留冷色氛围"], expectedEffect: "表情更清楚" },
sourceCandidateIds: ["lighting-exposure"],
},
{
id: "subject-separation",
category: "lighting" as const,
title: "人物暗部融入背景",
explanation: "人物亮度与背景接近,主体分离不足。",
severity: "medium" as const,
confidence: 0.86,
targetDescription: "人物头发和右肩",
suggestion: { goal: "加强主体分离", actions: ["补充克制的轮廓光"], preserve: ["保留人物轮廓"], expectedEffect: "空间层次更清楚" },
sourceCandidateIds: ["lighting-separation"],
},
];
const warnings: string[] = [];
const result = deduplicateAggregateIssues(issues, warnings);
expect(result).toHaveLength(1);
expect(result[0].sourceCandidateIds).toEqual(["lighting-exposure", "lighting-separation"]);
expect(result[0].suggestion.actions).toEqual(["提高脸部局部亮度", "补充克制的轮廓光"]);
expect(warnings).toEqual(["聚合阶段合并了 1 个重复或同根因问题。"]);
});
});