Add explicit error handling for invalid or inconsistent pred_boxes and logits array lengths in runLayoutModel to prevent silent failures. Expand test suite to verify correct region filtering and error scenarios, ensuring only labeled regions are returned and malformed outputs are handled robustly.
175 lines
4.2 KiB
TypeScript
175 lines
4.2 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
const mockState = vi.hoisted(() => ({
|
|
runOutput: {
|
|
logits: { data: new Float32Array() },
|
|
pred_boxes: { data: new Float32Array() },
|
|
},
|
|
}));
|
|
|
|
vi.mock('onnxruntime-node', () => ({
|
|
InferenceSession: {
|
|
create: vi.fn(async () => ({
|
|
run: vi.fn(async () => mockState.runOutput),
|
|
})),
|
|
},
|
|
Tensor: class Tensor {
|
|
constructor(
|
|
public type: string,
|
|
public data: Float32Array,
|
|
public dims: number[],
|
|
) {}
|
|
},
|
|
}));
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
readFile: vi.fn(async (path: string) => {
|
|
if (path === '/tmp/model-config.json') {
|
|
return JSON.stringify({
|
|
id2label: {
|
|
0: 'text',
|
|
1: 'table',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (path === '/tmp/model-preprocessor.json') {
|
|
return JSON.stringify({
|
|
size: { width: 2, height: 2 },
|
|
rescale_factor: 1 / 255,
|
|
image_mean: [0, 0, 0],
|
|
image_std: [1, 1, 1],
|
|
});
|
|
}
|
|
|
|
throw new Error(`unexpected readFile path: ${path}`);
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@napi-rs/canvas', () => {
|
|
const createCanvas = (width: number, height: number) => ({
|
|
getContext: () => ({
|
|
fillStyle: '#ffffff',
|
|
fillRect: () => {},
|
|
drawImage: () => {},
|
|
imageSmoothingEnabled: true,
|
|
getImageData: () => ({
|
|
data: new Uint8ClampedArray(width * height * 4).fill(255),
|
|
}),
|
|
}),
|
|
});
|
|
const loadImage = vi.fn(async () => ({ width: 2, height: 2 }));
|
|
|
|
return {
|
|
createCanvas,
|
|
loadImage,
|
|
default: {
|
|
createCanvas,
|
|
loadImage,
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock('../../src/pdf/model', () => ({
|
|
ensureModel: vi.fn(async () => '/tmp/model.onnx'),
|
|
MODEL_CONFIG_PATH: '/tmp/model-config.json',
|
|
MODEL_PREPROCESSOR_PATH: '/tmp/model-preprocessor.json',
|
|
}));
|
|
|
|
vi.mock('../../src/config/cpu-budget', () => ({
|
|
getOnnxThreadsPerJob: vi.fn(() => 1),
|
|
}));
|
|
|
|
describe('runLayoutModel', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mockState.runOutput = {
|
|
logits: { data: new Float32Array() },
|
|
pred_boxes: { data: new Float32Array() },
|
|
};
|
|
});
|
|
|
|
test('keeps one winner per query instead of dropping later queries behind duplicate class rows', async () => {
|
|
mockState.runOutput = {
|
|
logits: {
|
|
data: new Float32Array([
|
|
3,
|
|
4,
|
|
2.5,
|
|
0.1,
|
|
]),
|
|
},
|
|
pred_boxes: {
|
|
data: new Float32Array([
|
|
0.25, 0.25, 0.3, 0.3,
|
|
0.75, 0.75, 0.3, 0.3,
|
|
]),
|
|
},
|
|
};
|
|
|
|
const { runLayoutModel } = await import('../../src/pdf/runLayoutModel');
|
|
const regions = await runLayoutModel({
|
|
pageWidth: 100,
|
|
pageHeight: 100,
|
|
textItems: [{} as never],
|
|
pageImage: Buffer.from([1]),
|
|
});
|
|
|
|
expect(regions).toHaveLength(2);
|
|
expect(regions[0]?.label).toBe('text');
|
|
expect(regions[0]?.confidence).toBeCloseTo(0.9168273, 6);
|
|
expect(regions[0]?.bbox).toEqual([
|
|
expect.closeTo(60, 5),
|
|
expect.closeTo(60, 5),
|
|
expect.closeTo(90, 5),
|
|
expect.closeTo(90, 5),
|
|
]);
|
|
expect(regions[1]?.label).toBe('table');
|
|
expect(regions[1]?.confidence).toBeCloseTo(0.73105858, 6);
|
|
expect(regions[1]?.bbox).toEqual([
|
|
expect.closeTo(10, 5),
|
|
expect.closeTo(10, 5),
|
|
expect.closeTo(40, 5),
|
|
expect.closeTo(40, 5),
|
|
]);
|
|
});
|
|
|
|
test('drops unlabeled query winners and keeps only labeled regions', async () => {
|
|
mockState.runOutput = {
|
|
logits: {
|
|
data: new Float32Array([
|
|
0.1,
|
|
0.2,
|
|
5,
|
|
4,
|
|
0.1,
|
|
0.1,
|
|
]),
|
|
},
|
|
pred_boxes: {
|
|
data: new Float32Array([
|
|
0.25, 0.25, 0.3, 0.3,
|
|
0.75, 0.75, 0.3, 0.3,
|
|
]),
|
|
},
|
|
};
|
|
|
|
const { runLayoutModel } = await import('../../src/pdf/runLayoutModel');
|
|
const regions = await runLayoutModel({
|
|
pageWidth: 100,
|
|
pageHeight: 100,
|
|
textItems: [{} as never],
|
|
pageImage: Buffer.from([1]),
|
|
});
|
|
|
|
expect(regions).toHaveLength(1);
|
|
expect(regions[0]?.label).toBe('text');
|
|
expect(regions[0]?.confidence).toBeCloseTo(0.96109135, 5);
|
|
expect(regions[0]?.bbox).toEqual([
|
|
expect.closeTo(60, 5),
|
|
expect.closeTo(60, 5),
|
|
expect.closeTo(90, 5),
|
|
expect.closeTo(90, 5),
|
|
]);
|
|
});
|
|
});
|