diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..aa5d1bc
Binary files /dev/null and b/.DS_Store differ
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 🤖
@@ -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 79b2212..fdbdb7a 100644
Binary files a/backend/.DS_Store and b/backend/.DS_Store differ
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