const path = require('path'); const fs = require('fs'); const os = require('os'); const { parsePdf } = require('./pdfParser'); const { parseDocx } = require('./docxParser'); const { cleanText } = require('./textCleaner'); const { LOW_TEXT_DENSITY_THRESHOLD, isTesseractAvailable, ocrFallbackForPdf, } = require('./ocrFallbackService'); const ApiError = require('../utils/ApiError'); const { getFileStream } = require('./gridFsStorage'); const SUPPORTED_EXTENSIONS = ['.pdf', '.docx']; /** * Detects file type and delegates to the correct parser. * Reads the file from GridFS, writes to an ephemeral tmp file, and extracts text. * Returns cleaned raw text as a single string. * Throws ApiError 422 for corrupted/unreadable files. */ const extractText = async (fileId, originalName) => { const ext = path.extname(originalName).toLowerCase(); if (!SUPPORTED_EXTENSIONS.includes(ext)) { throw new ApiError( 400, `Unsupported file type "${ext}". Only PDF (.pdf) and Word (.docx) files are accepted.` ); } let rawText; // Create a temporary file path const tmpFilePath = path.join(os.tmpdir(), `lexguard-${Date.now()}-${originalName}`); try { // Stream file from GridFS to local tmp storage for parsing await new Promise((resolve, reject) => { const readStream = getFileStream(fileId); const writeStream = fs.createWriteStream(tmpFilePath); readStream.pipe(writeStream); writeStream.on('finish', resolve); writeStream.on('error', reject); readStream.on('error', reject); }); if (ext === '.pdf') { try { const { parseWithLlama } = require('./llamaParseService'); console.log('📄 Attempting LlamaParse extraction...'); rawText = await parseWithLlama(tmpFilePath); if (rawText && rawText.trim().length > 0) { console.log(`✅ LlamaParse extraction successful.`); } else { throw new Error('LlamaParse returned empty text'); } } catch (llamaErr) { console.warn(`⚠️ LlamaParse failed or not configured (${llamaErr.message}). Falling back to pdf-parse...`); rawText = await parsePdf(tmpFilePath); } } else if (ext === '.docx') { rawText = await parseDocx(tmpFilePath); } // ── OCR Fallback for Scanned PDFs ───────────────────────────────────────── if (ext === '.pdf' && (!rawText || rawText.trim().length < LOW_TEXT_DENSITY_THRESHOLD)) { console.log(`⚠️ Low text density detected (${(rawText || '').trim().length} chars). Checking for OCR fallback...`); const tesseractReady = await isTesseractAvailable(); if (tesseractReady) { try { console.log('📄 Scanned PDF detected. Initializing Tesseract OCR pipeline...'); rawText = await ocrFallbackForPdf(tmpFilePath); console.log(`✅ OCR extraction complete. Recovered ${rawText.trim().length} characters.`); } catch (ocrErr) { console.error(`❌ OCR fallback failed: ${ocrErr.message}`); throw new ApiError( 422, 'The uploaded PDF appears to be scanned/image-based. ' + 'OCR extraction was attempted but failed. Please upload a text-based PDF or DOCX file instead. ' + `(${ocrErr.message})` ); } } else { throw new ApiError( 422, 'Could not extract text from the uploaded PDF. The file appears to be scanned or image-based. ' + 'OCR processing is not available on this server. Please upload a text-based PDF or DOCX file instead.' ); } } } catch (parseErr) { // Re-throw ApiError instances as-is; wrap unknown errors if (parseErr instanceof ApiError) throw parseErr; throw new ApiError( 422, `Could not parse the uploaded file. It may be corrupted, password-protected, or not a valid ${ext.toUpperCase()} file. (${parseErr.message})` ); } finally { // ALWAYS clean up the temp file — even on errors — to prevent disk leaks on serverless try { if (fs.existsSync(tmpFilePath)) { fs.unlinkSync(tmpFilePath); } } catch (_cleanupErr) { // Swallow cleanup errors to avoid masking the original error } } if (!rawText || rawText.trim().length === 0) { throw new ApiError( 422, 'Could not extract any text from the uploaded file. ' + 'The file may be scanned/image-based, password-protected, or empty.' ); } return cleanText(rawText); }; module.exports = { extractText };