Files
ttstd-infinite-canvas/web/test/media-conversion.test.ts

88 lines
5.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
import { findPendingMediaConversionInput } from "../src/components/canvas/canvas-node-generation";
import { getNodeAcceptedInputKinds, getNodeMaxInputCount, getNodeResourceKind } from "../src/lib/canvas/node-registry";
import { mediaConversionSourceFingerprint } from "../src/lib/media-conversion/contracts";
import { convertImageLocally, LocalImageConversionError, transformPixels } from "../src/lib/media-conversion/local-converter";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
import type { AiConfig } from "../src/stores/use-config-store";
function node(type: CanvasNodeType, id: string, metadata: CanvasNodeData["metadata"] = {}): CanvasNodeData {
return { id, type, title: id, position: { x: 0, y: 0 }, width: 380, height: 440, metadata };
}
const config = {} as AiConfig;
describe("本地转换节点注册", () => {
test("只允许一个图片或视频输入,完成后才作为图片素材", () => {
expect(getNodeAcceptedInputKinds(CanvasNodeType.MediaConversion)).toEqual(["image", "video"]);
expect(getNodeMaxInputCount(CanvasNodeType.MediaConversion)).toBe(1);
expect(getNodeResourceKind(node(CanvasNodeType.MediaConversion, "conversion"))).toBeNull();
expect(getNodeResourceKind(node(CanvasNodeType.MediaConversion, "conversion", {
mediaConversion: { schemaVersion: 1, operation: "grayscale", status: "completed", resultStorageKey: "image:scope:result" },
}))).toBe("image");
expect(getNodeResourceKind(node(CanvasNodeType.MediaConversion, "conversion", {
mediaConversion: { schemaVersion: 1, operation: "pose", status: "skipped", detectedPeople: 0 },
}))).toBeNull();
});
test("连接规则接受图片和视频,拒绝没有可读媒体的节点及第二个输入", () => {
const image = node(CanvasNodeType.Image, "image");
const video = node(CanvasNodeType.Video, "video");
const text = node(CanvasNodeType.Text, "text");
const conversion = node(CanvasNodeType.MediaConversion, "conversion");
const nodes = [image, video, text, conversion];
const candidate = (fromNodeId: string): CanvasConnection => ({ id: `candidate-${fromNodeId}`, fromNodeId, toNodeId: conversion.id });
expect(canvasConnectionError(config, nodes, [], candidate(image.id))).toBe("");
expect(canvasConnectionError(config, nodes, [], candidate(video.id))).toBe("");
expect(canvasConnectionError(config, nodes, [], candidate(text.id))).toContain("转换节点只接受图片或视频输入");
expect(canvasConnectionError(config, nodes, [{ id: "existing", fromNodeId: image.id, toNodeId: conversion.id }], candidate(video.id))).toContain("转换节点最多连接 1 个输入");
});
test("下游生成会阻止未完成或待更新的转换结果", () => {
const conversion = node(CanvasNodeType.MediaConversion, "conversion", {
mediaConversion: { schemaVersion: 1, operation: "depth", status: "unavailable" },
});
const input = node(CanvasNodeType.Image, "input", { content: "data:image/png;base64,source" });
const target = node(CanvasNodeType.Image, "target");
const connections: CanvasConnection[] = [
{ id: "input-conversion", fromNodeId: input.id, toNodeId: conversion.id },
{ id: "conversion-target", fromNodeId: conversion.id, toNodeId: target.id },
];
expect(findPendingMediaConversionInput(target.id, [input, conversion, target], connections)?.id).toBe(conversion.id);
conversion.metadata = { mediaConversion: { schemaVersion: 1, operation: "grayscale", status: "completed", sourceNodeId: input.id, sourceFingerprint: "v1-unknown", resultStorageKey: "image:scope:result" } };
expect(findPendingMediaConversionInput(target.id, [input, conversion, target], connections)?.id).toBe(conversion.id);
conversion.metadata.mediaConversion.sourceFingerprint = mediaConversionSourceFingerprint(input);
expect(findPendingMediaConversionInput(target.id, [input, conversion, target], connections)).toBeNull();
});
});
describe("图片本地转换算法", () => {
test("灰度转换保留 alpha 并使用感知亮度", () => {
const pixels = new Uint8ClampedArray([255, 0, 0, 200, 0, 255, 0, 100]);
const output = transformPixels(pixels, 2, 1, "grayscale");
expect(Array.from(output)).toEqual([76, 76, 76, 200, 150, 150, 150, 100]);
});
test("Canny 转换输出黑白边缘图并保留 alpha", () => {
const pixels = new Uint8ClampedArray([
0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255,
]);
const output = transformPixels(pixels, 3, 3, "edge-canny");
for (let index = 0; index < output.length; index += 4) {
expect([0, 255]).toContain(output[index]);
expect(output[index]).toBe(output[index + 1]);
expect(output[index]).toBe(output[index + 2]);
expect(output[index + 3]).toBe(255);
}
});
test("浏览器算法转换把模型操作交给本机 Runtime", async () => {
await expect(convertImageLocally("", "depth")).rejects.toMatchObject<LocalImageConversionError>({ code: "model_missing" });
});
});