100 lines
4.3 KiB
JavaScript
100 lines
4.3 KiB
JavaScript
const fs = require('fs/promises');
|
|
const path = require('path');
|
|
const { formatMarkedText, parseMarkedText, finalizeChapters, detectLayoutChapters, detectChapters, syntheticChapters } = require('../src/structure');
|
|
const { buildSamplePdf } = require('./helpers/samplePdf');
|
|
const { createLogger } = require('../src/logger');
|
|
|
|
const logger = createLogger('silent');
|
|
|
|
function pagesFrom(count) {
|
|
return Array.from({ length: count }, (_, index) => ({ page: index + 1, text: `Page ${index + 1} body` }));
|
|
}
|
|
|
|
describe('marked text', () => {
|
|
it('round-trips page text and chapter markers', () => {
|
|
const pages = [
|
|
{ page: 1, text: 'Title page' },
|
|
{ page: 2, text: 'Chapter body' },
|
|
];
|
|
const chapters = [
|
|
{ chapterNo: 0, title: 'Front matter', startPage: 1, endPage: 1 },
|
|
{ chapterNo: 1, title: 'The River', startPage: 2, endPage: 2 },
|
|
];
|
|
const marked = formatMarkedText(pages, chapters);
|
|
expect(marked.match(/\[Page \d+\]/g)).toEqual(['[Page 1]', '[Page 2]']);
|
|
expect(marked).toContain('=== CHAPTER 1: The River ===');
|
|
const parsed = parseMarkedText(marked);
|
|
expect(parsed.pages.map((page) => page.text)).toEqual(['Title page', 'Chapter body']);
|
|
expect(parsed.markers.map((marker) => marker.chapterNo)).toEqual([0, 1]);
|
|
expect(formatMarkedText(parsed.pages, chapters)).toBe(marked);
|
|
});
|
|
});
|
|
|
|
describe('chapter detection', () => {
|
|
it('uses a PDF outline and treats earlier pages as front matter', async () => {
|
|
const pdf = await buildSamplePdf();
|
|
await fs.mkdir(path.join(__dirname, 'fixtures'), { recursive: true });
|
|
await fs.writeFile(path.join(__dirname, 'fixtures/sample-5page.pdf'), pdf);
|
|
const pages = pagesFrom(5);
|
|
const detected = await detectChapters({ pdfBytes: pdf, blocks: [], pages, llm: null, logger });
|
|
expect(detected.chapterDetectionMethod).toBe('outline');
|
|
expect(detected.chapters[0]).toMatchObject({ chapterNo: 0, startPage: 1 });
|
|
expect(detected.chapters.map((chapter) => chapter.startPage)).toEqual([1, 2, 4]);
|
|
expect(detected.chapters[2].endPage).toBe(5);
|
|
const starts = detected.chapters.map((chapter) => chapter.startPage);
|
|
expect([...starts].sort((a, b) => a - b)).toEqual(starts);
|
|
});
|
|
|
|
it('uses layout headings near the top of the page', () => {
|
|
const blocks = [
|
|
{
|
|
Id: 'title',
|
|
BlockType: 'LAYOUT_TITLE',
|
|
Page: 3,
|
|
Text: 'Chapter 1 The River',
|
|
Geometry: { BoundingBox: { Top: 0.08, Left: 0.1, Width: 0.5, Height: 0.03 } },
|
|
},
|
|
{
|
|
Id: 'line',
|
|
BlockType: 'LINE',
|
|
Page: 3,
|
|
Text: 'Chapter 1 The River',
|
|
Geometry: { BoundingBox: { Top: 0.08, Left: 0.1, Width: 0.5, Height: 0.03 } },
|
|
},
|
|
];
|
|
const found = detectLayoutChapters(blocks, 6);
|
|
const chapters = finalizeChapters(found, 6);
|
|
expect(chapters.some((chapter) => chapter.title.includes('River'))).toBe(true);
|
|
expect(chapters[0].chapterNo).toBe(0);
|
|
expect(chapters[0].endPage).toBe(2);
|
|
});
|
|
|
|
it('asks the model when no outline or headings exist', async () => {
|
|
const pages = pagesFrom(4).map((page, index) => ({
|
|
...page,
|
|
text: index === 1 ? 'Chapter 2 Harbor\nShips leave.' : page.text,
|
|
}));
|
|
const llm = {
|
|
async complete() {
|
|
return { data: { chapters: [{ title: 'Harbor', startPage: 2 }] }, usage: { inputTokens: 1, outputTokens: 1 } };
|
|
},
|
|
};
|
|
const detected = await detectChapters({ pdfBytes: null, blocks: [], pages, llm, logger });
|
|
expect(detected.chapterDetectionMethod).toBe('llm');
|
|
expect(detected.chapters.map((chapter) => chapter.chapterNo)).toEqual([0, 1]);
|
|
});
|
|
|
|
it('builds synthetic sections when nothing else matches', async () => {
|
|
const pages = pagesFrom(22);
|
|
const detected = await detectChapters({ pdfBytes: null, blocks: [], pages, llm: null, logger });
|
|
expect(detected.chapterDetectionMethod).toBe('synthetic');
|
|
expect(detected.chapters.every((chapter) => chapter.synthetic)).toBe(true);
|
|
expect(detected.chapters[0].startPage).toBe(1);
|
|
expect(detected.chapters[detected.chapters.length - 1].endPage).toBe(22);
|
|
for (let index = 1; index < detected.chapters.length; index += 1) {
|
|
expect(detected.chapters[index].startPage).toBe(detected.chapters[index - 1].endPage + 1);
|
|
}
|
|
expect(syntheticChapters(22).length).toBeGreaterThan(1);
|
|
});
|
|
});
|