From a3ef26ff29a33cd129122b98c60b79a1e93c6001 Mon Sep 17 00:00:00 2001 From: Drakonis96 <41027003+Drakonis96@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:38:12 +0100 Subject: [PATCH] 0.5.2 Bug fixes. --- .DS_Store | Bin 0 -> 6148 bytes Dockerfile | 6 +- README.md | 8 +- backend/.DS_Store | Bin 6148 -> 6148 bytes backend/utils.py | 235 ++++++++++++++++++++++++---------------------- 5 files changed, 132 insertions(+), 117 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aa5d1bcddd676d17176f6f4d497f0f754c532ece GIT binary patch literal 6148 zcmeHKO^ee&7=EW)ZMut4L}4#Nz-z7TcKukqWV`KQK|O3l4=Od;WHoFyQ} zS%*HM6{~%3sT;ckURk$o8kU}MMiIpnu5rYdlsOiiQ3_T<`DM9bKmHW|&$Dyz02z7I zyJB`KI;`cVK~L$Jj%Y;B$)(Y%s&u%fQj<;)d5C;G19p%*eP%6lR}*vgaA+G?yI=ZEyE`AyW$JXd*EEHk{#uOE(s7zlm zn4+WK)^U!;LZPA))0YpX-^}z4g~_+$`L+xv<|s6!QNSp$s=$_Nw#4~A`u+WXm1J5* z0i(c1DZuJOcR0Y5^x0aOoH%Q3_&c~T5mzXb6cqY6mKAXn@4}^F%oY`(qp?tk9+>$N NkTRIUDDYPm_yyT^+}!{G literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile index b1480cb..54ddfc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build the frontend -FROM node:16-alpine as frontend-build +FROM node:16-alpine AS frontend-build WORKDIR /app/frontend COPY frontend/package.json frontend/package-lock.json* ./ RUN npm install @@ -16,6 +16,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ poppler-utils \ tesseract-ocr \ ghostscript \ + qpdf \ + unpaper \ + libxml2 \ + libxslt1.1 \ libffi-dev \ libssl-dev \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 8f0da9c..1b61a35 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
- ocrAI Logo -
+
+ ocrAI Logo +
# ocrAI 🤖 @@ -78,4 +78,4 @@ Frontend: React, Axios Backend: Flask, Python OCR: Tesseract, pdf2image, OCRmyPDF AI: OpenAI, Gemini, Mistral APIs -Containerization: Docker, Docker Compose \ No newline at end of file +Containerization: Docker, Docker Compose diff --git a/backend/.DS_Store b/backend/.DS_Store index 79b22124f752124be8bc830f0ff4801f3d9bcfd0..fdbdb7a0746d6b346046a26683c1abcebde12c73 100644 GIT binary patch delta 179 zcmZoMXfc@J&&a(oU^g=(_hcTH{>iy4{5(nJ#RW+@`AG~64BICwuokL`S63UGo9ZZ- znAGYhR9hMu0GVbcwY8iaqRRT#LGjr+xq11WlXtSp%Xa}au`}c|lroes6oAR%vf!e; focz3WpbX>W87%FLq#DZDwfQEiE#qc(j=%f>U(qrq delta 88 zcmV-e0H^lO}I2%R) diff --git a/backend/utils.py b/backend/utils.py index 0309e06..d28a816 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -29,8 +29,9 @@ def encode_image(file_path): def run_tesseract(file_path): """ - If the file is a PDF, perform OCR page by page and add a "Page X:" header; - otherwise, perform OCR normally. + Si el archivo es un PDF, se realiza OCR página a página añadiendo al principio de cada una + la cabecera con el formato [Page 0001], [Page 0002], etc. + Para otros formatos se realiza OCR normal. """ extracted_text = "" if file_path.lower().endswith(".pdf"): @@ -38,7 +39,7 @@ def run_tesseract(file_path): pages = convert_from_path(file_path) for i, page in enumerate(pages, start=1): page_text = pytesseract.image_to_string(page, lang='eng') - extracted_text += f"Page {i}:\n{page_text}\n\n" + extracted_text += f"[Page {i:04d}]\n{page_text}\n\n" except Exception as e: extracted_text = f"❌ Error processing PDF: {str(e)}" else: @@ -115,7 +116,7 @@ def ocr_file_by_pages(file_path, api, model, prompt_key, update_progress, is_can temp_filename = os.path.join(OUTPUT_FOLDER, f"temp_page_{uuid.uuid4().hex}.png") page.save(temp_filename, "PNG") page_text = call_api_ocr(api, model, temp_filename, prompt_key) - final_text += f"Page {i}:\n{page_text}\n\n" + final_text += f"[Page {i:04d}]\n{page_text}\n\n" os.remove(temp_filename) progress = int((i / total) * 100) update_progress(progress, f"📄 Processed page {i} of {total}.") @@ -140,7 +141,7 @@ def translate_file_by_pages(file_path, api, model, target_language, prompt_key, page.save(temp_filename, "PNG") page_text = pytesseract.image_to_string(page, lang='eng') translated_page = call_api_translation(api, model, page_text, target_language, prompt_key) - final_translation += f"Page {i}:\n{translated_page}\n\n" + final_translation += f"[Page {i:04d}]\n{translated_page}\n\n" os.remove(temp_filename) progress = int((i / total) * 100) update_progress(progress, f"📄 Processed page {i} of {total}.") @@ -151,11 +152,14 @@ def translate_file_by_pages(file_path, api, model, target_language, prompt_key, text = f.read() translated = call_api_translation(api, model, text, target_language, prompt_key) update_progress(100, "🎉 Process completed") - return f"Page 1:\n{translated}" + return f"[Page {1:04d}]\n{translated}" else: return "Unsupported file type for translation." def process_file(file_path, api, model, mode, prompt_key, update_progress, is_cancelled): + """ + NOTA: Ahora solo usamos [Page XXXX] al inicio de cada página (por Tesseract). + """ if is_cancelled(): update_progress(0, "⏹️ Cancelled") return "Process cancelled." @@ -163,13 +167,11 @@ def process_file(file_path, api, model, mode, prompt_key, update_progress, is_ca base_name = os.path.splitext(os.path.basename(file_path))[0] if mode == "OCR": - # Process using tesseract with page-structure if PDF. processed_text = run_tesseract(file_path) if is_cancelled(): update_progress(25, "⏹️ Cancelled") return "Process cancelled." update_progress(50, "✅ Tesseract OCR completed.") - # Generate PDF copy as before. pdf_output = os.path.join(OUTPUT_FOLDER, base_name + "_ocr.pdf") if file_path.lower().endswith(".pdf"): if embed_ocr_in_pdf(file_path, pdf_output): @@ -179,6 +181,7 @@ def process_file(file_path, api, model, mode, prompt_key, update_progress, is_ca update_progress(95, "⚠️ Failed to embed OCR; original PDF copied.") else: shutil.copy(file_path, pdf_output) + elif mode == "OCR + AI": processed_text = run_tesseract(file_path) if is_cancelled(): @@ -195,13 +198,14 @@ def process_file(file_path, api, model, mode, prompt_key, update_progress, is_ca update_progress(95, "⚠️ Failed to embed OCR; original PDF copied.") else: shutil.copy(file_path, pdf_output) + elif mode == "AI": update_progress(25, "📂 File ready for full AI processing.") if file_path.lower().endswith(".pdf"): processed_text = ocr_file_by_pages(file_path, api, model, prompt_key, update_progress, is_cancelled) else: processed_text = call_api_ocr(api, model, file_path, prompt_key) - # In AI mode, do not generate a new PDF. + else: processed_text = "Unrecognized processing mode." update_progress(25, "❌ Error: Unrecognized mode.") @@ -212,58 +216,90 @@ def process_file(file_path, api, model, mode, prompt_key, update_progress, is_ca update_progress(75, "🤖 API processing completed.") - # Write the output TXT file (it is already structured by page in OCR and OCR+AI modes) + # Guardar el TXT final txt_file = os.path.join(OUTPUT_FOLDER, base_name + ".txt") with open(txt_file, "w", encoding="utf-8") as f: f.write(processed_text) + update_progress(100, "🎉 Process completed") return processed_text -def process_text(text): - lines = text.splitlines() - processed_lines = [] - buffer = "" - for line in lines: - stripped = line.strip() - if not stripped: - if buffer: - processed_lines.append(buffer) - buffer = "" +def organize_paragraphs(text): + """ + Organiza el texto plano en párrafos de forma más flexible. + Si se detecta doble salto de línea se usa como separador; + si no, se procesa línea a línea para unirlas en párrafos, creando uno nuevo cuando: + - Se encuentra una línea vacía, o + - La línea actual termina en punto. + Dentro de cada párrafo se unen las líneas; se inserta un
si la lĂ­nea termina en punto. + """ + text = text.strip() + paragraphs = [] + if "\n\n" in text: + blocks = re.split(r'\n\s*\n', text) + else: + lines = text.splitlines() + blocks = [] + buffer = "" + for line in lines: + stripped = line.strip() + if not stripped: + if buffer: + blocks.append(buffer) + buffer = "" else: - processed_lines.append("") - continue + if buffer and buffer.endswith("."): + blocks.append(buffer) + buffer = stripped + else: + if buffer: + buffer += " " + stripped + else: + buffer = stripped if buffer: - if buffer.endswith('.'): - processed_lines.append(buffer) - buffer = stripped - else: - buffer += " " + stripped + blocks.append(buffer) + for block in blocks: + line_list = block.splitlines() + if len(line_list) == 1: + paragraphs.append(line_list[0].strip()) else: - buffer = stripped - if buffer: - processed_lines.append(buffer) - return "\n".join(processed_lines) + new_block = "" + for i, line in enumerate(line_list): + line = line.strip() + if not line: + continue + if i < len(line_list) - 1: + if line.endswith("."): + new_block += line + "
" + else: + new_block += line + " " + else: + new_block += line + paragraphs.append(new_block.strip()) + return paragraphs def convert_txt_to_pdf(txt_file_path): """ - Se ha modificado para que, si el contenido del TXT está formateado en HTML, - se interpreten los tags que indiquen títulos (

,

, etc.), párrafos (

) - y saltos de página ( o div con clase "page-break"). - - En caso de que el contenido sea texto plano y contenga patrones en el formato - [Page X] (entre corchetes), se usará ese separador para dividir las páginas. - El encabezado (sin los corchetes) se incluirá en la parte superior de cada página. + Convierte un archivo TXT a PDF siguiendo estas reglas: + - Se detecta el patrón [Page XXXX] para separar páginas (solo al inicio). + - Este marcador se convierte en un encabezado (h1). + - Si el contenido proviene de un bloque markdown con ```html se elimina ese marcador y + se parsea con BeautifulSoup para generar párrafos independientes. + - Para contenido en texto plano se organiza en párrafos con organize_paragraphs. + - Se inserta un PageBreak después de cada bloque de página. """ with open(txt_file_path, "r", encoding="utf-8") as f: content = f.read() base_name = os.path.splitext(os.path.basename(txt_file_path))[0] output_pdf = os.path.join(OUTPUT_FOLDER, base_name + "_txt.pdf") - doc = SimpleDocTemplate(output_pdf, pagesize=A4, - rightMargin=40, leftMargin=40, - topMargin=40, bottomMargin=40) + doc = SimpleDocTemplate( + output_pdf, + pagesize=A4, + rightMargin=40, leftMargin=40, + topMargin=40, bottomMargin=40 + ) styles = getSampleStyleSheet() - # Estilos para encabezados header_styles = { "h1": ParagraphStyle('Heading1', parent=styles['Heading1'], alignment=TA_CENTER), "h2": ParagraphStyle('Heading2', parent=styles['Heading2'], alignment=TA_CENTER), @@ -277,82 +313,57 @@ def convert_txt_to_pdf(txt_file_path): parent=styles['Normal'], alignment=TA_JUSTIFY, leading=15, - leftIndent=20 # Sangría al inicio de cada párrafo + leftIndent=20 ) flowables = [] - # Detectamos si el contenido es HTML (buscando etiquetas comunes) - is_html = any(tag in content.lower() for tag in [""), normal_style) - flowables.append(para) - flowables.append(Spacer(1, 12)) - flowables.append(PageBreak()) + first_page_encountered = True + header_para = Paragraph(part, styles['Heading1']) + flowables.append(header_para) + flowables.append(Spacer(1, 12)) else: - # Si no se detectan separadores, se usa el método anterior basado en "Page X:" sin corchetes. - pages = re.split(r'(?i)Page\s+\d+:\s*', content) - if pages and pages[0].strip() == "": - pages = pages[1:] - if len(pages) <= 1: - pages = content.split("\n\n") - processed_pages = [process_text(page) for page in pages if page.strip() != ""] - for i, page_text in enumerate(processed_pages, start=1): - header = Paragraph(f"PAGE {i}", styles['Heading1']) - flowables.append(header) - flowables.append(Spacer(1, 12)) - para = Paragraph(page_text.replace("\n", "
"), normal_style) - flowables.append(para) - if i < len(processed_pages): - flowables.append(Spacer(1, 24)) - flowables.append(PageBreak()) + if part.startswith("```"): + lines = part.splitlines() + if lines and lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + part = "\n".join(lines) + # Si parece HTML, se procesa para separar cada etiqueta de interés + if re.search(r'<\s*html', part, re.IGNORECASE) or re.search(r'<\s*(p|h[1-6])', part, re.IGNORECASE): + if not part.lower().startswith("" + soup = BeautifulSoup(part, "html.parser") + for element in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p']): + if element.name.lower() in ["h1", "h2", "h3", "h4", "h5", "h6"]: + style = header_styles.get(element.name.lower(), styles['Heading1']) + else: + style = normal_style + text_content = element.get_text().strip() + if text_content: + flowables.append(Paragraph(text_content, style)) + flowables.append(Spacer(1, 12)) + else: + # Texto plano -> organizar en párrafos + paragraphs = organize_paragraphs(part) + for para_text in paragraphs: + if para_text: + flowables.append(Paragraph(para_text, normal_style)) + flowables.append(Spacer(1, 12)) doc.build(flowables) return output_pdf