import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import * as os from 'os'; const PADDLEOCR_URL = 'http://localhost:5000'; interface IOCRResult { text: string; confidence: number; box: number[][]; } interface IOCRResponse { success: boolean; results: IOCRResult[]; error?: string; } interface IHealthResponse { status: string; model: string; language: string; gpu_enabled: boolean; } /** * Convert PDF first page to PNG using ImageMagick */ function convertPdfToImage(pdfPath: string): string { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdf-convert-')); const outputPath = path.join(tempDir, 'page.png'); try { execSync( `convert -density 200 -quality 90 "${pdfPath}[0]" -background white -alpha remove "${outputPath}"`, { stdio: 'pipe' } ); const imageData = fs.readFileSync(outputPath); return imageData.toString('base64'); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } } /** * Create a simple test image with text using ImageMagick */ function createTestImage(text: string): string { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-image-')); const outputPath = path.join(tempDir, 'test.png'); try { execSync( `convert -size 400x100 xc:white -font DejaVu-Sans -pointsize 24 -fill black -gravity center -annotate 0 "${text}" "${outputPath}"`, { stdio: 'pipe' } ); const imageData = fs.readFileSync(outputPath); return imageData.toString('base64'); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } } // Health check test tap.test('should respond to health check', async () => { const response = await fetch(`${PADDLEOCR_URL}/health`); expect(response.ok).toBeTrue(); const data: IHealthResponse = await response.json(); expect(data.status).toEqual('healthy'); expect(data.model).toEqual('PP-OCRv4'); expect(data.language).toBeTypeofString(); expect(data.gpu_enabled).toBeTypeofBoolean(); console.log(`PaddleOCR Status: ${data.status}`); console.log(` Model: ${data.model}`); console.log(` Language: ${data.language}`); console.log(` GPU Enabled: ${data.gpu_enabled}`); }); // Base64 OCR test tap.test('should perform OCR on base64 image', async () => { // Create a test image with known text const testText = 'Hello World 12345'; console.log(`Creating test image with text: "${testText}"`); const imageBase64 = createTestImage(testText); const response = await fetch(`${PADDLEOCR_URL}/ocr`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: imageBase64 }), }); expect(response.ok).toBeTrue(); const data: IOCRResponse = await response.json(); expect(data.success).toBeTrue(); expect(data.results).toBeArray(); const extractedText = data.results.map((r) => r.text).join(' '); console.log(`Extracted text: "${extractedText}"`); // Check that we got some text back expect(data.results.length).toBeGreaterThan(0); // Check that at least some of the expected text was found const normalizedExtracted = extractedText.toLowerCase().replace(/\s+/g, ''); const normalizedExpected = testText.toLowerCase().replace(/\s+/g, ''); const hasPartialMatch = normalizedExtracted.includes('hello') || normalizedExtracted.includes('world') || normalizedExtracted.includes('12345'); expect(hasPartialMatch).toBeTrue(); }); // File upload OCR test tap.test('should perform OCR via file upload', async () => { const testText = 'Invoice Number 98765'; console.log(`Creating test image with text: "${testText}"`); const imageBase64 = createTestImage(testText); const imageBuffer = Buffer.from(imageBase64, 'base64'); const formData = new FormData(); const blob = new Blob([imageBuffer], { type: 'image/png' }); formData.append('img', blob, 'test.png'); const response = await fetch(`${PADDLEOCR_URL}/ocr/upload`, { method: 'POST', body: formData, }); expect(response.ok).toBeTrue(); const data: IOCRResponse = await response.json(); expect(data.success).toBeTrue(); expect(data.results).toBeArray(); const extractedText = data.results.map((r) => r.text).join(' '); console.log(`Extracted text: "${extractedText}"`); // Check that we got some text back expect(data.results.length).toBeGreaterThan(0); }); // OCR result structure test tap.test('should return proper OCR result structure', async () => { const testText = 'Test 123'; const imageBase64 = createTestImage(testText); const response = await fetch(`${PADDLEOCR_URL}/ocr`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: imageBase64 }), }); const data: IOCRResponse = await response.json(); if (data.results.length > 0) { const result = data.results[0]; // Check result has required fields expect(result.text).toBeTypeofString(); expect(result.confidence).toBeTypeofNumber(); expect(result.box).toBeArray(); // Check bounding box structure (4 points, each with x,y) expect(result.box.length).toEqual(4); for (const point of result.box) { expect(point.length).toEqual(2); expect(point[0]).toBeTypeofNumber(); expect(point[1]).toBeTypeofNumber(); } // Confidence should be between 0 and 1 expect(result.confidence).toBeGreaterThan(0); expect(result.confidence).toBeLessThanOrEqual(1); console.log(`Result structure valid:`); console.log(` Text: "${result.text}"`); console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`); console.log(` Box: ${JSON.stringify(result.box)}`); } }); // Test with actual invoice if available const invoiceDir = path.join(process.cwd(), '.nogit/invoices'); if (fs.existsSync(invoiceDir)) { const pdfFiles = fs.readdirSync(invoiceDir).filter((f) => f.endsWith('.pdf')); if (pdfFiles.length > 0) { const testPdf = pdfFiles[0]; tap.test(`should extract text from invoice: ${testPdf}`, async () => { const pdfPath = path.join(invoiceDir, testPdf); console.log(`Converting ${testPdf} to image...`); const imageBase64 = convertPdfToImage(pdfPath); console.log(`Image size: ${(imageBase64.length / 1024).toFixed(1)} KB`); const startTime = Date.now(); const response = await fetch(`${PADDLEOCR_URL}/ocr`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: imageBase64 }), }); const endTime = Date.now(); const elapsedMs = endTime - startTime; expect(response.ok).toBeTrue(); const data: IOCRResponse = await response.json(); expect(data.success).toBeTrue(); console.log(`OCR completed in ${(elapsedMs / 1000).toFixed(2)}s`); console.log(`Found ${data.results.length} text regions`); // Print first 10 results const preview = data.results.slice(0, 10); console.log(`\nFirst ${preview.length} results:`); for (const result of preview) { console.log(` [${(result.confidence * 100).toFixed(0)}%] ${result.text}`); } if (data.results.length > 10) { console.log(` ... and ${data.results.length - 10} more`); } // Should find text in an invoice expect(data.results.length).toBeGreaterThan(5); }); } } // Error handling test tap.test('should handle invalid base64 gracefully', async () => { const response = await fetch(`${PADDLEOCR_URL}/ocr`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: 'not-valid-base64!!!' }), }); const data: IOCRResponse = await response.json(); // Should return success: false with error message expect(data.success).toBeFalse(); expect(data.error).toBeTypeofString(); console.log(`Error handling works: ${data.error}`); }); export default tap.start();