UNPKG

n8n-nodes-capivision

Version:

OCR multiengine com visão apurada de capivara — Tesseract, OCR.space, AWS Textract e suporte a layout inteligente.

587 lines (551 loc) 17.4 kB
import { IExecuteFunctions } from 'n8n-core'; import { INodeExecutionData, INodeType, INodeTypeDescription, IBinaryData, } from 'n8n-workflow'; import { createWorker } from 'tesseract.js'; import { TextractClient, DetectDocumentTextCommand, AnalyzeDocumentCommand } from '@aws-sdk/client-textract'; import OpenAI from 'openai'; import axios from 'axios'; import { PDFExtract } from 'pdf.js-extract'; import { PDFDocument } from 'pdf-lib'; interface PdfPage { content: Array<{ str: string }>; } interface PdfExtractResult { pages: PdfPage[]; } class OcrExtractor { private pdfExtract = new PDFExtract(); async extractFromPdf(pdfBuffer: Buffer): Promise<string> { try { const data = await this.pdfExtract.extractBuffer(pdfBuffer) as PdfExtractResult; return data.pages.map(page => page.content.map(item => item.str).join(' ')).join('\n\n'); } catch (error: any) { throw new Error(`Erro ao extrair texto do PDF: ${error.message}`); } } async validateAndProcessFile(binaryData: IBinaryData): Promise<{ type: string; data: Buffer }> { try { const mimeType = binaryData.mimeType || ''; if (!binaryData.data) { throw new Error('Dados binários não encontrados'); } const data = Buffer.from(binaryData.data, 'base64'); if (!data || data.length === 0) { throw new Error('Falha ao converter dados base64 para buffer'); } if (mimeType.startsWith('image/')) { return { type: 'image', data }; } else if (mimeType === 'application/pdf') { return { type: 'pdf', data }; } else { throw new Error(`Tipo de arquivo não suportado: ${mimeType}. Use PDF ou imagem.`); } } catch (error: any) { throw new Error(`Erro ao validar arquivo: ${error.message}`); } } async extractText(result: any, engine: string, fileType: string): Promise<string> { try { switch (engine) { case 'tesseract': if (fileType === 'pdf') { throw new Error('Tesseract.js não suporta PDF diretamente no n8n. Use OCR.space ou AWS Textract para PDFs.'); } return result.text || ''; case 'ocrspace': if (!result.ParsedResults || !result.ParsedResults[0]) { throw new Error('OCR.space não retornou resultados válidos'); } return result.ParsedResults[0].ParsedText || ''; case 'textract': if (!result.Blocks) { throw new Error('AWS Textract não retornou blocos de texto'); } return result.Blocks .filter((block: any) => block.BlockType === 'LINE') .map((block: any) => block.Text) .join('\n') || ''; default: throw new Error(`Engine "${engine}" não suportada`); } } catch (error: any) { throw new Error(`Erro ao extrair texto: ${error.message}`); } } async extractJson(result: any, engine: string, layout: any): Promise<any> { try { const text = await this.extractText(result, engine, 'image'); if (Object.keys(layout).length === 0) { return { text }; } const structuredData: any = {}; for (const [field, coords] of Object.entries(layout)) { const { x, y, w, h } = coords as any; structuredData[field] = `Valor extraído para ${field}`; } return structuredData; } catch (error: any) { throw new Error(`Erro ao extrair JSON: ${error.message}`); } } async extractCsv(result: any, engine: string, layout: any): Promise<string> { try { const jsonData = await this.extractJson(result, engine, layout); if (Object.keys(layout).length === 0) { return jsonData.text; } const headers = Object.keys(jsonData); const values = Object.values(jsonData); return `${headers.join(',')}\n${values.join(',')}`; } catch (error: any) { throw new Error(`Erro ao extrair CSV: ${error.message}`); } } } async function analyzeWithAI(text: string, credentials: any): Promise<string> { try { const openai = new OpenAI({ apiKey: credentials.apiKey, }); const response = await openai.chat.completions.create({ model: 'gpt-4', messages: [ { role: 'system', content: 'Você é um assistente especializado em análise de texto extraído por OCR. Analise o texto fornecido e forneça um resumo estruturado com os principais pontos e informações relevantes.', }, { role: 'user', content: text, }, ], temperature: 0.7, }); return response.choices[0]?.message?.content || 'Não foi possível analisar o texto com IA.'; } catch (error: any) { throw new Error(`Erro na análise com IA: ${error.message}`); } } export class CapivisionOcr implements INodeType { description: INodeTypeDescription = { displayName: 'CAPIVISION OCR', name: 'capivisionOcr', icon: 'file:icon.svg', group: ['transform'], version: 1, description: 'OCR multiengine com visão apurada de capivara', defaults: { name: 'CAPIVISION OCR', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'ocrSpaceApi', required: true, displayOptions: { show: { engine: ['ocrspace'], }, }, }, { name: 'awsTextractApi', required: true, displayOptions: { show: { engine: ['textract'], }, }, }, { name: 'openAiApi', required: true, displayOptions: { show: { treatmentMethod: ['ocr_ai'], }, }, }, ], properties: [ { displayName: 'Mecanismo OCR', name: 'engine', type: 'options', options: [ { name: 'Tesseract.js (apenas imagens)', value: 'tesseract', description: 'Melhor para imagens simples e texto bem definido', }, { name: 'OCR.space (imagens e PDF)', value: 'ocrspace', description: 'Suporta PDF e vários idiomas', }, { name: 'AWS Textract (imagens e PDF)', value: 'textract', description: 'Melhor para documentos complexos e formulários', }, ], default: 'tesseract', required: true, }, { displayName: 'Tipo de Entrada', name: 'imageFormat', type: 'options', options: [ { name: 'Binário', value: 'binary', }, { name: 'Base64', value: 'base64', }, ], default: 'binary', required: true, }, { displayName: 'Input Binário', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, displayOptions: { show: { imageFormat: ['binary'], }, }, description: 'Nome do campo que contém a imagem/PDF', }, { displayName: 'String Base64', name: 'base64String', type: 'string', typeOptions: { rows: 4, }, default: '', required: true, displayOptions: { show: { imageFormat: ['base64'], }, }, description: 'String base64 da imagem (pode incluir ou não o cabeçalho data:image)', }, { displayName: 'Método de Tratamento', name: 'treatmentMethod', type: 'options', options: [ { name: 'Apenas OCR (Sem IA)', value: 'ocr_only', }, { name: 'OCR + IA', value: 'ocr_ai', }, ], default: 'ocr_only', required: true, }, { displayName: 'Mecanismo IA', name: 'aiEngine', type: 'options', displayOptions: { show: { treatmentMethod: ['ocr_ai'], }, }, options: [ { name: 'ChatGPT', value: 'chatgpt', }, ], default: 'chatgpt', required: true, }, { displayName: 'Modelo IA', name: 'aiModel', type: 'options', displayOptions: { show: { treatmentMethod: ['ocr_ai'], aiEngine: ['chatgpt'], }, }, options: [ { name: 'GPT-4o-mini', value: 'gpt-4-mini', }, { name: 'GPT-4o', value: 'gpt-4', }, ], default: 'gpt-4-mini', required: true, }, { displayName: 'Formato de Saída', name: 'outputFormat', type: 'options', options: [ { name: 'Texto Puro', value: 'text', }, { name: 'JSON Estruturado', value: 'json', }, { name: 'CSV', value: 'csv', }, ], default: 'text', required: true, }, { displayName: 'Preset de Layout (opcional)', name: 'layoutPreset', type: 'json', default: '{}', required: false, description: 'JSON com estrutura de coordenadas para extração', }, ], }; async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { try { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; const extractor = new OcrExtractor(); for (let i = 0; i < items.length; i++) { let engine: string = ''; let treatmentMethod: string = ''; let outputFormat: string = ''; let fileType: string = ''; try { engine = this.getNodeParameter('engine', i) as string; outputFormat = this.getNodeParameter('outputFormat', i) as string; treatmentMethod = this.getNodeParameter('treatmentMethod', i) as string; const imageFormat = this.getNodeParameter('imageFormat', i) as string; let imageData: Buffer; let processedFile: { type: string; data: Buffer }; if (imageFormat === 'binary') { const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; const binaryData = items[i].binary; if (!binaryData) { throw new Error('Nenhum dado binário encontrado!'); } const binaryProperty = binaryData[binaryPropertyName]; if (!binaryProperty) { throw new Error(`Nenhum dado binário encontrado no campo "${binaryPropertyName}"!`); } processedFile = await extractor.validateAndProcessFile(binaryProperty); imageData = processedFile.data; fileType = processedFile.type; } else { const base64String = this.getNodeParameter('base64String', i) as string; if (!base64String) { throw new Error('String base64 não fornecida!'); } const base64Clean = base64String.replace(/^data:image\/[a-zA-Z+]+;base64,/, ''); imageData = Buffer.from(base64Clean, 'base64'); fileType = 'image'; } let extractedText: string = ''; switch (engine) { case 'tesseract': if (fileType === 'pdf') { throw new Error('Tesseract.js não suporta PDF diretamente no n8n. Use OCR.space ou AWS Textract para PDFs.'); } console.log('Iniciando processamento Tesseract...'); console.log('Tamanho da imagem:', imageData.length, 'bytes'); try { const worker = await createWorker({ langPath: 'https://tessdata.projectnaptha.com/4.0.0', logger: m => console.log('Tesseract Log:', JSON.stringify(m)), errorHandler: e => console.error('Tesseract Error:', e), }); console.log('Worker Tesseract criado'); try { await worker.loadLanguage('por'); console.log('Idioma carregado'); await worker.initialize('por'); console.log('Worker inicializado'); const result = await worker.recognize(imageData); console.log('Reconhecimento concluído'); if (!result || !result.data) { throw new Error('Resultado do Tesseract inválido'); } extractedText = result.data.text || ''; if (!extractedText.trim()) { throw new Error('Nenhum texto extraído da imagem'); } console.log('Texto extraído com sucesso'); } finally { await worker.terminate(); console.log('Worker terminado'); } } catch (tesseractError: any) { console.error('Erro detalhado do Tesseract:', tesseractError); throw new Error(`Erro no processamento do Tesseract: ${tesseractError.message || 'Erro desconhecido'}\nStack: ${tesseractError.stack || 'Sem stack trace'}`); } break; case 'ocrspace': const credentials = await this.getCredentials('ocrSpaceApi'); const formData = new FormData(); formData.append('apikey', credentials.apiKey as string); formData.append('language', 'por'); formData.append('isOverlayRequired', 'true'); formData.append('detectOrientation', 'true'); formData.append('scale', 'true'); formData.append('OCREngine', '2'); if (fileType === 'pdf') { formData.append('file', new Blob([imageData], { type: 'application/pdf' })); } else { formData.append('file', new Blob([imageData])); } const response = await axios.post('https://api.ocr.space/parse/image', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); extractedText = response.data.ParsedResults[0].ParsedText || ''; break; case 'textract': const awsCredentials = await this.getCredentials('awsTextractApi'); const textract = new TextractClient({ region: awsCredentials.region as string, credentials: { accessKeyId: awsCredentials.accessKeyId as string, secretAccessKey: awsCredentials.secretAccessKey as string, }, }); if (fileType === 'pdf') { const command = new AnalyzeDocumentCommand({ Document: { Bytes: imageData, }, FeatureTypes: ['FORMS', 'TABLES', 'QUERIES'], }); const result = await textract.send(command); extractedText = result.Blocks?.filter(block => block.BlockType === 'LINE') .map(block => block.Text) .join('\n') || ''; } else { const command = new DetectDocumentTextCommand({ Document: { Bytes: imageData, }, }); const result = await textract.send(command); extractedText = result.Blocks?.filter(block => block.BlockType === 'LINE') .map(block => block.Text) .join('\n') || ''; } break; } let finalOutput: any = extractedText; if (treatmentMethod === 'ocr_ai' && extractedText) { try { const credentials = await this.getCredentials('openAiApi'); const aiEngine = this.getNodeParameter('aiEngine', i) as string; const aiModel = this.getNodeParameter('aiModel', i) as string; if (aiEngine === 'chatgpt') { const openai = new OpenAI({ apiKey: credentials.apiKey as string, }); const aiResponse = await openai.chat.completions.create({ model: aiModel === 'gpt-4-mini' ? 'gpt-4-1106-preview' : 'gpt-4', messages: [ { role: 'system', content: 'Você é um especialista em análise de documentos. Analise o texto fornecido e extraia as informações mais relevantes.', }, { role: 'user', content: `Analise este texto e extraia as informações mais importantes:\n\n${extractedText}`, }, ], temperature: 0.3, }); const aiAnalysis = aiResponse.choices[0]?.message?.content || 'Não foi possível analisar o texto com IA.'; if (outputFormat === 'json') { finalOutput = { original_text: extractedText, ai_analysis: aiAnalysis, }; } else if (outputFormat === 'csv') { finalOutput = `Texto Original,Análise IA\n"${extractedText.replace(/"/g, '""')}","${aiAnalysis.replace(/"/g, '""')}"`; } else { finalOutput = `=== Texto Original ===\n${extractedText}\n\n=== Análise IA ===\n${aiAnalysis}`; } } } catch (aiError: any) { throw new Error(`Erro na análise com IA: ${aiError.message}`); } } returnData.push({ json: { success: true, timestamp: new Date().toISOString(), engine, fileType, treatmentMethod, outputFormat, data: finalOutput, metadata: { processedAt: new Date().toISOString(), engineVersion: { tesseract: '4.1.1', openai: '4.97.0', }, }, }, }); } catch (error: any) { console.error('Erro completo:', error); returnData.push({ json: { success: false, timestamp: new Date().toISOString(), error: { message: error.message || 'Erro desconhecido', type: error.name || 'ProcessingError', stack: error.stack || 'Sem stack trace', context: { engine, fileType, treatmentMethod, outputFormat, originalError: error.toString(), details: typeof error === 'object' ? JSON.stringify(error) : 'Erro não é um objeto' }, }, }, }); } } return [returnData]; } catch (error: any) { throw new Error(`Erro global na execução: ${error.message}\nDetalhes: ${error.stack}`); } } }