| const express = require('express');
|
| const cors = require('cors');
|
| const multer = require('multer');
|
| const sharp = require('sharp');
|
| const { GoogleGenerativeAI } = require('@google/generative-ai');
|
| const fs = require('fs');
|
| const path = require('path');
|
|
|
|
|
| const GEMINI_API_KEY = process.env.GEMINI_API_KEY || null;
|
| const pdf = require('pdf-poppler');
|
|
|
| const app = express();
|
| const PORT = process.env.PORT || 7860;
|
|
|
|
|
| function cleanupTempFiles() {
|
| const uploadsDir = path.join(__dirname, 'uploads');
|
|
|
| try {
|
| if (!fs.existsSync(uploadsDir)) {
|
| fs.mkdirSync(uploadsDir, { recursive: true });
|
| console.log('📁 Created uploads directory');
|
| return;
|
| }
|
|
|
| const files = fs.readdirSync(uploadsDir);
|
| let cleanedCount = 0;
|
|
|
| files.forEach(file => {
|
| const filePath = path.join(uploadsDir, file);
|
| const stats = fs.statSync(filePath);
|
| const now = new Date();
|
| const fileAge = now - stats.mtime;
|
| const maxAge = 30 * 60 * 1000;
|
|
|
|
|
| if (fileAge > maxAge) {
|
| try {
|
| fs.unlinkSync(filePath);
|
| cleanedCount++;
|
| console.log(`🗑️ Cleaned up old temp file: ${file}`);
|
| } catch (error) {
|
| console.warn(`⚠️ Could not delete ${file}:`, error.message);
|
| }
|
| }
|
| });
|
|
|
| if (cleanedCount > 0) {
|
| console.log(`✅ Cleaned up ${cleanedCount} temporary files`);
|
| } else {
|
| console.log('✅ No temporary files to clean up');
|
| }
|
|
|
| } catch (error) {
|
| console.error('❌ Error during cleanup:', error.message);
|
| }
|
| }
|
|
|
|
|
| cleanupTempFiles();
|
|
|
|
|
| setInterval(() => {
|
| console.log('🔄 Running periodic cleanup...');
|
| cleanupTempFiles();
|
| }, 15 * 60 * 1000);
|
|
|
|
|
| app.use(cors());
|
| app.use(express.json());
|
|
|
|
|
| const upload = multer({
|
| dest: 'uploads/',
|
| limits: {
|
| fileSize: 10 * 1024 * 1024,
|
| },
|
| fileFilter: (req, file, cb) => {
|
| const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
|
| if (allowedTypes.includes(file.mimetype)) {
|
| cb(null, true);
|
| } else {
|
| cb(new Error('Invalid file type. Only JPEG, PNG, WebP, and PDF are allowed.'));
|
| }
|
| }
|
| });
|
|
|
|
|
| async function convertPdfToImages(pdfPath) {
|
| try {
|
| const outputDir = path.dirname(pdfPath);
|
|
|
| console.log('Converting PDF to images using pdf-poppler...');
|
|
|
| const options = {
|
| format: 'png',
|
| out_dir: outputDir,
|
| out_prefix: 'page',
|
| page: null,
|
| scale: 2048
|
| };
|
|
|
| console.log('PDF conversion options:', options);
|
|
|
|
|
| const results = await pdf.convert(pdfPath, options);
|
|
|
| console.log('PDF conversion results:', results);
|
|
|
|
|
| const imagePaths = [];
|
| if (Array.isArray(results)) {
|
| for (let i = 0; i < results.length; i++) {
|
| const imagePath = path.join(outputDir, `page-${i + 1}.png`);
|
| if (fs.existsSync(imagePath)) {
|
| imagePaths.push(imagePath);
|
| console.log(`Found converted page: ${imagePath}`);
|
| }
|
| }
|
| }
|
|
|
|
|
| if (imagePaths.length === 0) {
|
| console.log('Trying alternative file naming patterns...');
|
| const files = fs.readdirSync(outputDir);
|
| const pngFiles = files.filter(file => file.endsWith('.png') && file.startsWith('page'));
|
|
|
| for (const file of pngFiles) {
|
| const fullPath = path.join(outputDir, file);
|
| imagePaths.push(fullPath);
|
| console.log(`Found PNG file: ${fullPath}`);
|
| }
|
| }
|
|
|
| console.log(`Successfully converted ${imagePaths.length} pages to images`);
|
| return imagePaths;
|
| } catch (error) {
|
| console.error('PDF conversion error:', error);
|
| throw new Error(`PDF conversion failed: ${error.message}`);
|
| }
|
| }
|
|
|
|
|
| async function enhanceImageForOCR(imagePath) {
|
| try {
|
|
|
| const enhancedPath = imagePath + '_enhanced.png';
|
|
|
| await sharp(imagePath)
|
| .resize(null, 2000, {
|
| withoutEnlargement: false,
|
| kernel: sharp.kernel.lanczos3
|
| })
|
| .normalize()
|
| .sharpen({ sigma: 1.2, flat: 1, jagged: 2 })
|
| .gamma(1.1)
|
| .png({ quality: 95, compressionLevel: 6 })
|
| .toFile(enhancedPath);
|
|
|
| return enhancedPath;
|
| } catch (error) {
|
| console.error('Image enhancement error:', error);
|
| return imagePath;
|
| }
|
| }
|
|
|
|
|
| async function processWithGemini(imagePath, apiKey, mode = 'standard') {
|
| try {
|
| const genAI = new GoogleGenerativeAI(apiKey);
|
|
|
|
|
| const modelName = mode === 'structured' ? 'gemini-2.5-flash' : 'gemini-2.5-pro';
|
| const model = genAI.getGenerativeModel({ model: modelName });
|
|
|
|
|
| const imageBuffer = fs.readFileSync(imagePath);
|
| const imageBase64 = imageBuffer.toString('base64');
|
|
|
| const imagePart = {
|
| inlineData: {
|
| data: imageBase64,
|
| mimeType: 'image/png'
|
| }
|
| };
|
|
|
|
|
| let prompt;
|
| if (mode === 'structured') {
|
| prompt = `🇹🇭 **THAI-FOCUSED MARKDOWN OCR - อ่านข้อความไทยและจัดรูปแบบเป็น Markdown**
|
|
|
| **คำสั่งสำคัญ (CRITICAL INSTRUCTIONS):**
|
| 1. **อ่านทุกตัวอักษร** - แยกข้อความที่เขียนในรูปภาพทุกตัว
|
| 2. **ไม่ใช้ตัวอย่างทั่วไป** - ห้ามใช้ "Field | Value" ให้ใช้ข้อความจริงที่เห็น
|
| 3. **รักษาภาษาเดิม** - ถ้าเป็นไทยให้เป็นไทย ถ้าเป็นอังกฤษให้เป็นอังกฤษ
|
| 4. **ข้อความจริงเท่านั้น** - ห้ามตีความ แปล หรือสร้างคำอธิบายเอง
|
|
|
| **วิธีการสแกน:**
|
| - เริ่มจากซ้ายบน อ่านทีละบรรทัดไปขวาล่าง
|
| - รวมข้อความทั้งหมด: หัวข้อ เนื้อหา ตัวเลข วันที่ ข้อความเล็ก
|
| - **สำหรับข้อความไทย: เว้นวรรคระหว่างคำให้ถูกต้อง**
|
| - สำหรับตาราง: ใช้หัวตารางและข้อมูลจริงที่เห็น
|
| - สำหรับรายการ: ใช้รายการจริงที่เห็น
|
|
|
| **กฎการจัดรูปแบบ MARKDOWN:**
|
| - ## สำหรับหัวข้อใหญ่ (ใช้หัวข้อจริง)
|
| - ### สำหรับหัวข้อย่อย (ใช้หัวข้อจริง)
|
| - **ใช้ตารางเสมอสำหรับข้อมูลที่เป็นระบบ:**
|
| - เห็นตาราง → สร้างตารางด้วยหัวตารางและข้อมูลจริง
|
| - เห็นรายการ → แปลงเป็นตารางด้วยรายการจริง
|
| - เห็นคู่ข้อมูล → ใช้คีย์และค่าจริง
|
| - ใช้ **ตัวหนา** สำหรับข้อความที่เน้นในรูป
|
| - ใช้ > สำหรับหมายเหตุที่ปรากฏในรูป
|
|
|
| **ตัวอย่างตาราง:**
|
| | รายการ | จำนวน | ราคา |
|
| |--------|--------|------|
|
| | กาแฟ | 2 แก้ว | 60 บาท |
|
| | ขนมปัง | 1 ชิ้น | 25 บาท |
|
|
|
| **ผลลัพธ์: MARKDOWN ที่มีเนื้อหาจริง - ห้ามใช้แม่แบบทั่วไป**`;
|
| } else {
|
| prompt = `🇹🇭 **FAST MARKDOWN OCR - SAME FEATURES AS PRO MODE (2.5 FLASH)**
|
|
|
| **คำสั่งสำคัญ (IDENTICAL TO STRUCTURED MODE):**
|
| 1. **อ่านทุกตัวอักษร** - แยกข้อความที่เขียนในรูปภาพทุกตัว
|
| 2. **ไม่ใช้ตัวอย่างทั่วไป** - ห้ามใช้ "Field | Value" ให้ใช้ข้อความจริงที่เห็น
|
| 3. **รักษาภาษาเดิม** - ถ้าเป็นไทยให้เป็นไทย ถ้าเป็นอังกฤษให้เป็นอังกฤษ
|
| 4. **ข้อความจริงเท่านั้น** - ห้ามตีความ แปล หรือสร้างคำอธิบายเอง
|
|
|
| **วิธีการสแกน (SAME AS PRO MODE):**
|
| - เริ่มจากซ้ายบน อ่านทีละบรรทัดไปขวาล่าง
|
| - รวมข้อความทั้งหมด: หัวข้อ เนื้อหา ตัวเลข วันที่ ข้อความเล็ก
|
| - **สำหรับข้อความไทย: เว้นวรรคระหว่างคำให้ถูกต้อง**
|
| - สำหรับตาราง: ใช้หัวตารางและข้อมูลจริงที่เห็น
|
| - สำหรับรายการ: ใช้รายการจริงที่เห็น
|
|
|
| **MARKDOWN FORMATTING RULES (IDENTICAL TO PRO MODE):**
|
| - ## สำหรับหัวข้อใหญ่ (ใช้หัวข้อจริง)
|
| - ### สำหรับหัวข้อย่อย (ใช้หัวข้อจริง)
|
| - **ใช้ตารางเสมอสำหรับข้อมูลที่เป็นระบบ:**
|
| - เห็นตาราง → สร้างตาราง markdown ด้วยหัวตารางและข้อมูลจริง
|
| - เห็นรายการ → แปลงเป็นตาราง markdown ด้วยรายการจริง
|
| - เห็นคู่ข้อมูล → ใช้คีย์และค่าจริงในตาราง markdown
|
| - ใช้ **ตัวหนา** สำหรับข้อความที่เน้นในรูป
|
| - ใช้ > สำหรับหมายเหตุที่ปรากฏในรูป
|
| - ใช้ - สำหรับรายการเมื่อเหมาะสม
|
|
|
| **การประมวลผลภาษา (SAME AS PRO MODE):**
|
| - ข้อความไทย: เว้นวรรคให้ถูกต้อง แก้ไขข้อผิดพลาด OCR
|
| - ข้อความอังกฤษ: รักษาการสะกดและตัวพิมพ์เดิม
|
| - ตัวเลข: แยกตัวเลข ทศนิยม เปอร์เซ็นต์ รหัสให้แม่นยำ
|
| - ภาษาผสม: รักษาภาษาเดิมของแต่ละส่วน
|
|
|
| **ตัวอย่างตาราง MARKDOWN:**
|
| | รายการ | จำนวน | ราคา |
|
| |--------|--------|------|
|
| | กาแฟ อเมริกาโน่ | 2 แก้ว | 120 บาท |
|
| | ขนมปังโฮลวีท | 1 ชิ้น | 45 บาท |
|
|
|
| **ผลลัพธ์: MARKDOWN WITH SAME FEATURES AS PRO MODE - JUST FASTER WITH 2.5 FLASH**`;
|
| }
|
|
|
| const result = await model.generateContent([prompt, imagePart]);
|
| const response = await result.response;
|
| let extractedText = response.text();
|
|
|
|
|
|
|
|
|
| return extractedText;
|
|
|
| } catch (error) {
|
| console.error('Gemini processing error:', error);
|
| throw new Error(`OCR processing failed: ${error.message}`);
|
| }
|
| }
|
|
|
|
|
| function generateFormats(text, fileName, mode) {
|
| const baseFileName = fileName.replace(/\.[^/.]+$/, '');
|
| const timestamp = new Date().toISOString();
|
|
|
| const formats = {
|
|
|
| txt: text,
|
|
|
|
|
| md: mode === 'structured' ? text : `# ${baseFileName}\n\n${text}`,
|
|
|
|
|
| json: {
|
| metadata: {
|
| fileName: fileName,
|
| extractedAt: timestamp,
|
| characterCount: text.length,
|
| lineCount: text.split('\n').length,
|
| wordCount: text.split(/\s+/).filter(word => word.length > 0).length,
|
| processingMode: mode
|
| },
|
| content: {
|
| rawText: text,
|
| lines: text.split('\n'),
|
| paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
|
| }
|
| }
|
| };
|
|
|
| return formats;
|
| }
|
|
|
|
|
| const progressTracking = new Map();
|
| const consoleLogs = new Map();
|
|
|
|
|
| app.get('/api/progress/:sessionId', (req, res) => {
|
| const sessionId = req.params.sessionId;
|
| const progress = progressTracking.get(sessionId) || { current: 0, total: 0, status: 'Not started' };
|
| res.json(progress);
|
| });
|
|
|
|
|
| app.get('/api/progress-latest', (req, res) => {
|
| if (progressTracking.size === 0) {
|
| return res.json({ current: 0, total: 0, status: 'No active processing' });
|
| }
|
|
|
|
|
| const entries = Array.from(progressTracking.entries());
|
| const latestEntry = entries[entries.length - 1];
|
|
|
| res.json(latestEntry[1]);
|
| });
|
|
|
|
|
| app.post('/api/ocr', upload.single('file'), async (req, res) => {
|
| const sessionId = Date.now().toString();
|
|
|
| try {
|
| const { mode = 'standard' } = req.body;
|
|
|
|
|
| const apiKey = GEMINI_API_KEY || req.body.apiKey;
|
|
|
| if (!apiKey) {
|
| return res.status(400).json({
|
| success: false,
|
| error: GEMINI_API_KEY ? 'Server API key not configured' : 'Google API Key is required'
|
| });
|
| }
|
|
|
| if (!req.file) {
|
| return res.status(400).json({
|
| success: false,
|
| error: 'No file uploaded'
|
| });
|
| }
|
|
|
| let extractedText = '';
|
| let imagePaths = [];
|
|
|
|
|
| const addConsoleLog = (message) => {
|
| if (!consoleLogs.has(sessionId)) {
|
| consoleLogs.set(sessionId, []);
|
| }
|
| const logs = consoleLogs.get(sessionId);
|
| logs.push({
|
| timestamp: new Date().toISOString(),
|
| message: message
|
| });
|
|
|
| if (logs.length > 20) {
|
| logs.shift();
|
| }
|
| console.log(message);
|
| };
|
|
|
| addConsoleLog(`🚀 Processing file: ${req.file.originalname} in ${mode} mode`);
|
|
|
|
|
| const updateProgress = (current, total, status, details = {}) => {
|
| const progressData = {
|
| current,
|
| total,
|
| status,
|
| sessionId,
|
| fileName: req.file.originalname,
|
| consoleLogs: consoleLogs.get(sessionId) || [],
|
| ...details
|
| };
|
| progressTracking.set(sessionId, progressData);
|
| addConsoleLog(`Progress: ${current}/${total} - ${status}`);
|
| };
|
|
|
|
|
| if (req.file.mimetype === 'application/pdf') {
|
| addConsoleLog('🔄 Starting PDF processing...');
|
| addConsoleLog(`📄 PDF file: ${req.file.originalname}`);
|
| addConsoleLog(`📊 File size: ${(req.file.size / 1024).toFixed(2)} KB`);
|
|
|
| updateProgress(1, 10, '🖼️ Converting PDF to images...');
|
| const pdfImagePaths = await convertPdfToImages(req.file.path);
|
|
|
| if (pdfImagePaths.length === 0) {
|
| throw new Error('No pages could be extracted from PDF. The PDF might be corrupted or empty.');
|
| }
|
|
|
| addConsoleLog(`✅ Converted ${pdfImagePaths.length} pages to images`);
|
|
|
|
|
| const totalSteps = 2 + (pdfImagePaths.length * 2);
|
| updateProgress(2, totalSteps, `📋 Found ${pdfImagePaths.length} pages to process`, {
|
| totalPages: pdfImagePaths.length,
|
| currentPage: 0,
|
| totalCharacters: 0
|
| });
|
|
|
|
|
| for (let i = 0; i < pdfImagePaths.length; i++) {
|
| const currentPage = i + 1;
|
| const totalPages = pdfImagePaths.length;
|
|
|
| addConsoleLog(`🔍 Processing page ${currentPage}/${totalPages} (${Math.round((currentPage / totalPages) * 100)}%)`);
|
|
|
|
|
| const enhanceStep = 2 + (i * 2) + 1;
|
| updateProgress(enhanceStep, totalSteps, `🔧 Enhancing page ${currentPage}/${totalPages}`, {
|
| totalPages,
|
| currentPage,
|
| totalCharacters: extractedText.length,
|
| phase: 'enhancing'
|
| });
|
|
|
| addConsoleLog(`📝 Enhancing image: page-${currentPage}.png`);
|
| const enhancedImagePath = await enhanceImageForOCR(pdfImagePaths[i]);
|
|
|
|
|
| const ocrStep = 2 + (i * 2) + 2;
|
| updateProgress(ocrStep, totalSteps, `🤖 Running OCR on page ${currentPage}/${totalPages}`, {
|
| totalPages,
|
| currentPage,
|
| totalCharacters: extractedText.length,
|
| phase: 'ocr'
|
| });
|
|
|
| addConsoleLog(`🤖 Running OCR on page ${currentPage} with Gemini ${mode === 'structured' ? '2.5-Pro' : '2.5-Flash'}`);
|
| const pageText = await processWithGemini(enhancedImagePath, apiKey, mode);
|
|
|
| addConsoleLog(`✅ Page ${currentPage} processed - ${pageText.length} characters extracted`);
|
|
|
| if (mode === 'structured') {
|
| extractedText += `\n\n## Page ${currentPage}\n\n${pageText}`;
|
| } else {
|
| extractedText += `\n\n--- Page ${currentPage} ---\n\n${pageText}`;
|
| }
|
|
|
|
|
| updateProgress(ocrStep, totalSteps, `✅ Page ${currentPage}/${totalPages} completed - ${pageText.length} chars`, {
|
| totalPages,
|
| currentPage,
|
| totalCharacters: extractedText.length,
|
| pageCharacters: pageText.length,
|
| phase: 'completed'
|
| });
|
|
|
| addConsoleLog(`📊 Total extracted so far: ${extractedText.length} characters`);
|
|
|
| imagePaths.push(enhancedImagePath);
|
| }
|
|
|
| addConsoleLog('🧹 Cleaning up temporary files...');
|
|
|
| pdfImagePaths.forEach((imagePath, index) => {
|
| try {
|
| if (fs.existsSync(imagePath)) {
|
| fs.unlinkSync(imagePath);
|
| addConsoleLog(`🗑️ Cleaned up page-${index + 1}.png`);
|
| }
|
| } catch (error) {
|
| addConsoleLog(`⚠️ Cleanup warning: ${error.message}`);
|
| }
|
| });
|
|
|
| } else {
|
|
|
| addConsoleLog('🖼️ Processing single image file...');
|
| addConsoleLog(`📄 File: ${req.file.originalname}`);
|
| addConsoleLog(`📊 Size: ${(req.file.size / 1024).toFixed(2)} KB`);
|
|
|
| updateProgress(1, 3, '🔧 Enhancing image for better OCR...', {
|
| totalPages: 1,
|
| currentPage: 1,
|
| totalCharacters: 0,
|
| phase: 'enhancing'
|
| });
|
| const enhancedImagePath = await enhanceImageForOCR(req.file.path);
|
|
|
| updateProgress(2, 3, `🤖 Running OCR with Gemini ${mode === 'structured' ? '2.5-Pro' : '2.5-Flash'}...`, {
|
| totalPages: 1,
|
| currentPage: 1,
|
| totalCharacters: 0,
|
| phase: 'ocr'
|
| });
|
| extractedText = await processWithGemini(enhancedImagePath, apiKey, mode);
|
|
|
| updateProgress(3, 3, `✅ OCR completed - ${extractedText.length} characters extracted`, {
|
| totalPages: 1,
|
| currentPage: 1,
|
| totalCharacters: extractedText.length,
|
| pageCharacters: extractedText.length,
|
| phase: 'completed'
|
| });
|
| addConsoleLog(`✅ OCR completed - ${extractedText.length} characters extracted`);
|
| imagePaths.push(enhancedImagePath);
|
| }
|
|
|
|
|
| const formats = generateFormats(extractedText, req.file.originalname, mode);
|
|
|
|
|
| const filesToCleanup = [req.file.path, ...imagePaths];
|
| let cleanedFiles = 0;
|
|
|
| filesToCleanup.forEach(filePath => {
|
| if (filePath && fs.existsSync(filePath)) {
|
| try {
|
| fs.unlinkSync(filePath);
|
| cleanedFiles++;
|
| addConsoleLog(`🗑️ Cleaned up: ${path.basename(filePath)}`);
|
| } catch (error) {
|
| addConsoleLog(`⚠️ Cleanup warning for ${path.basename(filePath)}: ${error.message}`);
|
| }
|
| }
|
| });
|
|
|
| addConsoleLog(`✅ Cleanup complete: ${cleanedFiles} files removed`);
|
|
|
|
|
| const finalProgress = progressTracking.get(sessionId);
|
| if (finalProgress) {
|
| updateProgress(finalProgress.total, finalProgress.total, '✅ Processing complete!');
|
| }
|
|
|
|
|
| res.json({
|
| success: true,
|
| sessionId: sessionId,
|
| data: {
|
| fileName: req.file.originalname,
|
| fileSize: req.file.size,
|
| processingMode: mode,
|
| extractedText: extractedText,
|
| formats: formats,
|
| metadata: {
|
| characterCount: extractedText.length,
|
| wordCount: extractedText.split(/\s+/).filter(word => word.length > 0).length,
|
| lineCount: extractedText.split('\n').length,
|
| processedAt: new Date().toISOString()
|
| }
|
| }
|
| });
|
|
|
|
|
| setTimeout(() => {
|
| progressTracking.delete(sessionId);
|
| consoleLogs.delete(sessionId);
|
| }, 30000);
|
|
|
| } catch (error) {
|
| console.error('OCR processing error:', error);
|
|
|
|
|
| const filesToCleanup = [];
|
| if (req.file && req.file.path) filesToCleanup.push(req.file.path);
|
| if (imagePaths && imagePaths.length > 0) filesToCleanup.push(...imagePaths);
|
|
|
| let cleanedFiles = 0;
|
| filesToCleanup.forEach(filePath => {
|
| if (filePath && fs.existsSync(filePath)) {
|
| try {
|
| fs.unlinkSync(filePath);
|
| cleanedFiles++;
|
| console.log(`🗑️ Error cleanup: ${path.basename(filePath)}`);
|
| } catch (cleanupError) {
|
| console.warn(`⚠️ Error cleanup warning for ${path.basename(filePath)}:`, cleanupError.message);
|
| }
|
| }
|
| });
|
|
|
| if (cleanedFiles > 0) {
|
| console.log(`✅ Error cleanup complete: ${cleanedFiles} files removed`);
|
| }
|
|
|
| res.status(500).json({
|
| success: false,
|
| error: error.message || 'Internal server error during OCR processing'
|
| });
|
| }
|
| });
|
|
|
|
|
| app.get('/', (req, res) => {
|
| res.json({
|
| success: true,
|
| message: '🌙 Luna OCR Backend API',
|
| version: '1.0.0',
|
| endpoints: {
|
| health: '/api/health',
|
| ocr: '/api/ocr (POST)'
|
| },
|
| status: 'Running',
|
| port: PORT,
|
| timestamp: new Date().toISOString()
|
| });
|
| });
|
|
|
|
|
| app.get('/api/health', (req, res) => {
|
| res.json({
|
| success: true,
|
| message: 'Luna OCR Backend is running!',
|
| hasApiKey: !!GEMINI_API_KEY,
|
| requiresUserApiKey: !GEMINI_API_KEY,
|
| timestamp: new Date().toISOString()
|
| });
|
| });
|
|
|
|
|
| app.post('/api/cleanup', (req, res) => {
|
| try {
|
| console.log('🧹 Manual cleanup requested...');
|
| cleanupTempFiles();
|
|
|
| res.json({
|
| success: true,
|
| message: 'Cleanup completed successfully',
|
| timestamp: new Date().toISOString()
|
| });
|
| } catch (error) {
|
| console.error('Manual cleanup error:', error);
|
| res.status(500).json({
|
| success: false,
|
| error: 'Cleanup failed: ' + error.message
|
| });
|
| }
|
| });
|
|
|
|
|
| app.use((error, req, res, next) => {
|
| if (error instanceof multer.MulterError) {
|
| if (error.code === 'LIMIT_FILE_SIZE') {
|
| return res.status(400).json({
|
| success: false,
|
| error: 'File too large. Maximum size is 10MB.'
|
| });
|
| }
|
| }
|
|
|
| console.error('Unhandled error:', error);
|
| res.status(500).json({
|
| success: false,
|
| error: 'Internal server error'
|
| });
|
| });
|
|
|
|
|
| process.on('SIGINT', () => {
|
| console.log('\n🛑 Received SIGINT, cleaning up before shutdown...');
|
| cleanupTempFiles();
|
| console.log('� LuRna OCR Backend shutting down gracefully');
|
| process.exit(0);
|
| });
|
|
|
| process.on('SIGTERM', () => {
|
| console.log('\n🛑 Received SIGTERM, cleaning up before shutdown...');
|
| cleanupTempFiles();
|
| console.log('👋 Luna OCR Backend shutting down gracefully');
|
| process.exit(0);
|
| });
|
|
|
|
|
| app.listen(PORT, () => {
|
| console.log(`🚀 Luna OCR Backend running on port ${PORT}`);
|
| console.log(`📡 Health check: http://localhost:${PORT}/api/health`);
|
| console.log(`🔍 OCR endpoint: http://localhost:${PORT}/api/ocr`);
|
| console.log(`🧹 Cleanup endpoint: http://localhost:${PORT}/api/cleanup`);
|
| console.log(`⏰ Automatic cleanup runs every 15 minutes`);
|
| });
|
|
|
| module.exports = app; |