feat(portrait-clearance): 肖像排查 - 增加画布插件与本机检测链路
This commit is contained in:
32
web/test/portrait-clearance-input-bindings.test.ts
Normal file
32
web/test/portrait-clearance-input-bindings.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { isPortraitImageInput, reconcilePortraitClearanceInputBindings, swapPortraitClearanceDirectBindings } from "@/lib/portrait-clearance/input-bindings";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
function image(id: string, metadata: CanvasNodeData["metadata"] = {}) {
|
||||
return { id, type: CanvasNodeType.Image, title: id, position: { x: 0, y: 0 }, width: 420, height: 236, metadata: { content: `data:image/png;base64,${id}`, ...metadata } } satisfies CanvasNodeData;
|
||||
}
|
||||
|
||||
function connection(id: string, fromNodeId: string, toNodeId = "clearance") {
|
||||
return { id, fromNodeId, toNodeId } satisfies CanvasConnection;
|
||||
}
|
||||
|
||||
describe("portrait clearance input bindings", () => {
|
||||
it("preserves existing roles, fills new connections deterministically, and removes stale ids", () => {
|
||||
const nodes = [image("a"), image("b"), image("c")];
|
||||
const bindings = reconcilePortraitClearanceInputBindings("direct-compare", "clearance", [connection("conn-2", "b"), connection("conn-1", "a"), connection("conn-3", "c")], nodes, [{ nodeId: "b", role: "reference" }, { nodeId: "missing", role: "query" }]);
|
||||
expect(bindings).toEqual([{ nodeId: "b", role: "reference" }, { nodeId: "a", role: "query" }]);
|
||||
});
|
||||
|
||||
it("uses candidate roles for additional network inputs and swaps only A/B", () => {
|
||||
const nodes = [image("a"), image("b"), image("c")];
|
||||
const bindings = reconcilePortraitClearanceInputBindings("network-search", "clearance", [connection("1", "a"), connection("2", "b"), connection("3", "c")], nodes);
|
||||
expect(bindings).toEqual([{ nodeId: "a", role: "query" }, { nodeId: "b", role: "candidate" }, { nodeId: "c", role: "candidate" }]);
|
||||
expect(swapPortraitClearanceDirectBindings([{ nodeId: "a", role: "query" }, { nodeId: "b", role: "reference" }, { nodeId: "c", role: "candidate" }])).toEqual([{ nodeId: "a", role: "reference" }, { nodeId: "b", role: "query" }, { nodeId: "c", role: "candidate" }]);
|
||||
});
|
||||
|
||||
it("accepts a persisted storage key as usable image data but rejects empty image nodes", () => {
|
||||
expect(isPortraitImageInput(image("storage", { content: undefined, storageKey: "image:user:one" }))).toBe(true);
|
||||
expect(isPortraitImageInput(image("empty", { content: undefined }))).toBe(false);
|
||||
});
|
||||
});
|
||||
49
web/test/portrait-clearance-model-selection.test.ts
Normal file
49
web/test/portrait-clearance-model-selection.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { portraitVisionModelError, portraitVisionModels, resolvePortraitVisionModel } from "@/lib/portrait-clearance/vision";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
function config(): AiConfig {
|
||||
return {
|
||||
channelMode: "local",
|
||||
baseUrl: "https://example.test",
|
||||
apiKey: "key",
|
||||
apiFormat: "openai",
|
||||
model: "system::vision-text",
|
||||
imageModel: "system::image",
|
||||
videoModel: "",
|
||||
textModel: "system::vision-text",
|
||||
audioModel: "",
|
||||
audioVoice: "",
|
||||
audioFormat: "mp3",
|
||||
audioSpeed: "1",
|
||||
audioInstructions: "",
|
||||
videoSeconds: "6",
|
||||
vquality: "720",
|
||||
videoGenerateAudio: "true",
|
||||
videoWatermark: "false",
|
||||
videoArkPrivateAssetUpload: "true",
|
||||
systemPrompt: "",
|
||||
models: [],
|
||||
imageModels: [],
|
||||
videoModels: [],
|
||||
textModels: [],
|
||||
audioModels: [],
|
||||
quality: "auto",
|
||||
size: "1:1",
|
||||
transparentBackground: "false",
|
||||
count: "1",
|
||||
canvasImageCount: "1",
|
||||
channels: [{ id: "system", name: "系统", baseUrl: "https://example.test", apiKey: "key", apiFormat: "openai", models: ["vision-text", "text-only"], modelCosts: [{ model: "vision-text", capability: "text", billingMode: "fixed_request", unitPriceMicrocredits: 1, capabilityConfig: { version: 1, text: { references: { promptMaxChars: 10000, maxImages: 2, maxImageBytes: 12_000_000, maxVideos: 0, maxVideoBytes: 0 } } } }, { model: "text-only", capability: "text", billingMode: "fixed_request", unitPriceMicrocredits: 1 }] }],
|
||||
};
|
||||
}
|
||||
|
||||
describe("portrait clearance model selection", () => {
|
||||
it("uses any selectable text model without requiring a separate image capability flag", () => {
|
||||
const value = config();
|
||||
expect(portraitVisionModels(value)).toEqual(["system::vision-text", "system::text-only"]);
|
||||
expect(resolvePortraitVisionModel(value, { mode: "project-default" })).toBe("system::vision-text");
|
||||
expect(resolvePortraitVisionModel(value, { mode: "pinned", modelRef: "system::text-only" })).toBe("system::text-only");
|
||||
expect(portraitVisionModelError(value, { mode: "pinned", modelRef: "system::text-only" })).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user