0.5.2
Bug fixes.
This commit is contained in:
parent
ee77b2989f
commit
a3ef26ff29
5 changed files with 132 additions and 117 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
# Stage 1: Build the frontend
|
# Stage 1: Build the frontend
|
||||||
FROM node:16-alpine as frontend-build
|
FROM node:16-alpine AS frontend-build
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
COPY frontend/package.json frontend/package-lock.json* ./
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
@ -16,6 +16,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
tesseract-ocr \
|
tesseract-ocr \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
qpdf \
|
||||||
|
unpaper \
|
||||||
|
libxml2 \
|
||||||
|
libxslt1.1 \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<center>
|
<div align="center">
|
||||||
<img src="logo.png" alt="ocrAI Logo" width="150">
|
<img src="/backend/static/logo.png" alt="ocrAI Logo" width="250">
|
||||||
</center>
|
</div>
|
||||||
|
|
||||||
# ocrAI 🤖
|
# ocrAI 🤖
|
||||||
|
|
||||||
|
|
|
||||||
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
233
backend/utils.py
233
backend/utils.py
|
|
@ -29,8 +29,9 @@ def encode_image(file_path):
|
||||||
|
|
||||||
def run_tesseract(file_path):
|
def run_tesseract(file_path):
|
||||||
"""
|
"""
|
||||||
If the file is a PDF, perform OCR page by page and add a "Page X:" header;
|
Si el archivo es un PDF, se realiza OCR página a página añadiendo al principio de cada una
|
||||||
otherwise, perform OCR normally.
|
la cabecera con el formato [Page 0001], [Page 0002], etc.
|
||||||
|
Para otros formatos se realiza OCR normal.
|
||||||
"""
|
"""
|
||||||
extracted_text = ""
|
extracted_text = ""
|
||||||
if file_path.lower().endswith(".pdf"):
|
if file_path.lower().endswith(".pdf"):
|
||||||
|
|
@ -38,7 +39,7 @@ def run_tesseract(file_path):
|
||||||
pages = convert_from_path(file_path)
|
pages = convert_from_path(file_path)
|
||||||
for i, page in enumerate(pages, start=1):
|
for i, page in enumerate(pages, start=1):
|
||||||
page_text = pytesseract.image_to_string(page, lang='eng')
|
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:
|
except Exception as e:
|
||||||
extracted_text = f"❌ Error processing PDF: {str(e)}"
|
extracted_text = f"❌ Error processing PDF: {str(e)}"
|
||||||
else:
|
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")
|
temp_filename = os.path.join(OUTPUT_FOLDER, f"temp_page_{uuid.uuid4().hex}.png")
|
||||||
page.save(temp_filename, "PNG")
|
page.save(temp_filename, "PNG")
|
||||||
page_text = call_api_ocr(api, model, temp_filename, prompt_key)
|
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)
|
os.remove(temp_filename)
|
||||||
progress = int((i / total) * 100)
|
progress = int((i / total) * 100)
|
||||||
update_progress(progress, f"📄 Processed page {i} of {total}.")
|
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.save(temp_filename, "PNG")
|
||||||
page_text = pytesseract.image_to_string(page, lang='eng')
|
page_text = pytesseract.image_to_string(page, lang='eng')
|
||||||
translated_page = call_api_translation(api, model, page_text, target_language, prompt_key)
|
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)
|
os.remove(temp_filename)
|
||||||
progress = int((i / total) * 100)
|
progress = int((i / total) * 100)
|
||||||
update_progress(progress, f"📄 Processed page {i} of {total}.")
|
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()
|
text = f.read()
|
||||||
translated = call_api_translation(api, model, text, target_language, prompt_key)
|
translated = call_api_translation(api, model, text, target_language, prompt_key)
|
||||||
update_progress(100, "🎉 Process completed")
|
update_progress(100, "🎉 Process completed")
|
||||||
return f"Page 1:\n{translated}"
|
return f"[Page {1:04d}]\n{translated}"
|
||||||
else:
|
else:
|
||||||
return "Unsupported file type for translation."
|
return "Unsupported file type for translation."
|
||||||
|
|
||||||
def process_file(file_path, api, model, mode, prompt_key, update_progress, is_cancelled):
|
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():
|
if is_cancelled():
|
||||||
update_progress(0, "⏹️ Cancelled")
|
update_progress(0, "⏹️ Cancelled")
|
||||||
return "Process 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]
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
|
||||||
if mode == "OCR":
|
if mode == "OCR":
|
||||||
# Process using tesseract with page-structure if PDF.
|
|
||||||
processed_text = run_tesseract(file_path)
|
processed_text = run_tesseract(file_path)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
update_progress(25, "⏹️ Cancelled")
|
update_progress(25, "⏹️ Cancelled")
|
||||||
return "Process cancelled."
|
return "Process cancelled."
|
||||||
update_progress(50, "✅ Tesseract OCR completed.")
|
update_progress(50, "✅ Tesseract OCR completed.")
|
||||||
# Generate PDF copy as before.
|
|
||||||
pdf_output = os.path.join(OUTPUT_FOLDER, base_name + "_ocr.pdf")
|
pdf_output = os.path.join(OUTPUT_FOLDER, base_name + "_ocr.pdf")
|
||||||
if file_path.lower().endswith(".pdf"):
|
if file_path.lower().endswith(".pdf"):
|
||||||
if embed_ocr_in_pdf(file_path, pdf_output):
|
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.")
|
update_progress(95, "⚠️ Failed to embed OCR; original PDF copied.")
|
||||||
else:
|
else:
|
||||||
shutil.copy(file_path, pdf_output)
|
shutil.copy(file_path, pdf_output)
|
||||||
|
|
||||||
elif mode == "OCR + AI":
|
elif mode == "OCR + AI":
|
||||||
processed_text = run_tesseract(file_path)
|
processed_text = run_tesseract(file_path)
|
||||||
if is_cancelled():
|
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.")
|
update_progress(95, "⚠️ Failed to embed OCR; original PDF copied.")
|
||||||
else:
|
else:
|
||||||
shutil.copy(file_path, pdf_output)
|
shutil.copy(file_path, pdf_output)
|
||||||
|
|
||||||
elif mode == "AI":
|
elif mode == "AI":
|
||||||
update_progress(25, "📂 File ready for full AI processing.")
|
update_progress(25, "📂 File ready for full AI processing.")
|
||||||
if file_path.lower().endswith(".pdf"):
|
if file_path.lower().endswith(".pdf"):
|
||||||
processed_text = ocr_file_by_pages(file_path, api, model, prompt_key, update_progress, is_cancelled)
|
processed_text = ocr_file_by_pages(file_path, api, model, prompt_key, update_progress, is_cancelled)
|
||||||
else:
|
else:
|
||||||
processed_text = call_api_ocr(api, model, file_path, prompt_key)
|
processed_text = call_api_ocr(api, model, file_path, prompt_key)
|
||||||
# In AI mode, do not generate a new PDF.
|
|
||||||
else:
|
else:
|
||||||
processed_text = "Unrecognized processing mode."
|
processed_text = "Unrecognized processing mode."
|
||||||
update_progress(25, "❌ Error: Unrecognized 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.")
|
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")
|
txt_file = os.path.join(OUTPUT_FOLDER, base_name + ".txt")
|
||||||
with open(txt_file, "w", encoding="utf-8") as f:
|
with open(txt_file, "w", encoding="utf-8") as f:
|
||||||
f.write(processed_text)
|
f.write(processed_text)
|
||||||
|
|
||||||
update_progress(100, "🎉 Process completed")
|
update_progress(100, "🎉 Process completed")
|
||||||
return processed_text
|
return processed_text
|
||||||
|
|
||||||
def process_text(text):
|
def organize_paragraphs(text):
|
||||||
lines = text.splitlines()
|
"""
|
||||||
processed_lines = []
|
Organiza el texto plano en párrafos de forma más flexible.
|
||||||
buffer = ""
|
Si se detecta doble salto de línea se usa como separador;
|
||||||
for line in lines:
|
si no, se procesa línea a línea para unirlas en párrafos, creando uno nuevo cuando:
|
||||||
stripped = line.strip()
|
- Se encuentra una línea vacía, o
|
||||||
if not stripped:
|
- La línea actual termina en punto.
|
||||||
if buffer:
|
Dentro de cada párrafo se unen las líneas; se inserta un <br/> si la línea termina en punto.
|
||||||
processed_lines.append(buffer)
|
"""
|
||||||
buffer = ""
|
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:
|
else:
|
||||||
processed_lines.append("")
|
if buffer and buffer.endswith("."):
|
||||||
continue
|
blocks.append(buffer)
|
||||||
|
buffer = stripped
|
||||||
|
else:
|
||||||
|
if buffer:
|
||||||
|
buffer += " " + stripped
|
||||||
|
else:
|
||||||
|
buffer = stripped
|
||||||
if buffer:
|
if buffer:
|
||||||
if buffer.endswith('.'):
|
blocks.append(buffer)
|
||||||
processed_lines.append(buffer)
|
for block in blocks:
|
||||||
buffer = stripped
|
line_list = block.splitlines()
|
||||||
else:
|
if len(line_list) == 1:
|
||||||
buffer += " " + stripped
|
paragraphs.append(line_list[0].strip())
|
||||||
else:
|
else:
|
||||||
buffer = stripped
|
new_block = ""
|
||||||
if buffer:
|
for i, line in enumerate(line_list):
|
||||||
processed_lines.append(buffer)
|
line = line.strip()
|
||||||
return "\n".join(processed_lines)
|
if not line:
|
||||||
|
continue
|
||||||
|
if i < len(line_list) - 1:
|
||||||
|
if line.endswith("."):
|
||||||
|
new_block += line + "<br/>"
|
||||||
|
else:
|
||||||
|
new_block += line + " "
|
||||||
|
else:
|
||||||
|
new_block += line
|
||||||
|
paragraphs.append(new_block.strip())
|
||||||
|
return paragraphs
|
||||||
|
|
||||||
def convert_txt_to_pdf(txt_file_path):
|
def convert_txt_to_pdf(txt_file_path):
|
||||||
"""
|
"""
|
||||||
Se ha modificado para que, si el contenido del TXT está formateado en HTML,
|
Convierte un archivo TXT a PDF siguiendo estas reglas:
|
||||||
se interpreten los tags que indiquen títulos (<h1>, <h2>, etc.), párrafos (<p>)
|
- Se detecta el patrón [Page XXXX] para separar páginas (solo al inicio).
|
||||||
y saltos de página (<pagebreak> o div con clase "page-break").
|
- Este marcador se convierte en un encabezado (h1).
|
||||||
|
- Si el contenido proviene de un bloque markdown con ```html se elimina ese marcador y
|
||||||
En caso de que el contenido sea texto plano y contenga patrones en el formato
|
se parsea con BeautifulSoup para generar párrafos independientes.
|
||||||
[Page X] (entre corchetes), se usará ese separador para dividir las páginas.
|
- Para contenido en texto plano se organiza en párrafos con organize_paragraphs.
|
||||||
El encabezado (sin los corchetes) se incluirá en la parte superior de cada página.
|
- Se inserta un PageBreak después de cada bloque de página.
|
||||||
"""
|
"""
|
||||||
with open(txt_file_path, "r", encoding="utf-8") as f:
|
with open(txt_file_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
base_name = os.path.splitext(os.path.basename(txt_file_path))[0]
|
base_name = os.path.splitext(os.path.basename(txt_file_path))[0]
|
||||||
output_pdf = os.path.join(OUTPUT_FOLDER, base_name + "_txt.pdf")
|
output_pdf = os.path.join(OUTPUT_FOLDER, base_name + "_txt.pdf")
|
||||||
doc = SimpleDocTemplate(output_pdf, pagesize=A4,
|
doc = SimpleDocTemplate(
|
||||||
rightMargin=40, leftMargin=40,
|
output_pdf,
|
||||||
topMargin=40, bottomMargin=40)
|
pagesize=A4,
|
||||||
|
rightMargin=40, leftMargin=40,
|
||||||
|
topMargin=40, bottomMargin=40
|
||||||
|
)
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
# Estilos para encabezados
|
|
||||||
header_styles = {
|
header_styles = {
|
||||||
"h1": ParagraphStyle('Heading1', parent=styles['Heading1'], alignment=TA_CENTER),
|
"h1": ParagraphStyle('Heading1', parent=styles['Heading1'], alignment=TA_CENTER),
|
||||||
"h2": ParagraphStyle('Heading2', parent=styles['Heading2'], 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'],
|
parent=styles['Normal'],
|
||||||
alignment=TA_JUSTIFY,
|
alignment=TA_JUSTIFY,
|
||||||
leading=15,
|
leading=15,
|
||||||
leftIndent=20 # Sangría al inicio de cada párrafo
|
leftIndent=20
|
||||||
)
|
)
|
||||||
|
|
||||||
flowables = []
|
flowables = []
|
||||||
# Detectamos si el contenido es HTML (buscando etiquetas comunes)
|
# Patrón para detectar el marcador [Page XXXX]
|
||||||
is_html = any(tag in content.lower() for tag in ["<html", "<p", "<h1", "<h2", "<h3"])
|
page_pattern = re.compile(r'\[Page\s+\d{4}\]')
|
||||||
|
parts = re.split(r'(\[Page\s+\d{4}\])', content)
|
||||||
|
first_page_encountered = False
|
||||||
|
|
||||||
if is_html:
|
for part in parts:
|
||||||
soup = BeautifulSoup(content, "html.parser")
|
part = part.strip()
|
||||||
body = soup.body if soup.body else soup
|
if not part:
|
||||||
for element in body.children:
|
continue
|
||||||
if element.name is None:
|
|
||||||
text = element.strip()
|
if page_pattern.fullmatch(part):
|
||||||
if text:
|
if first_page_encountered:
|
||||||
para = Paragraph(text, normal_style)
|
|
||||||
flowables.append(para)
|
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
elif element.name.lower() in ["h1", "h2", "h3", "h4", "h5", "h6"]:
|
|
||||||
tag = element.name.lower()
|
|
||||||
style = header_styles.get(tag, styles['Heading1'])
|
|
||||||
para = Paragraph(element.decode_contents(), style)
|
|
||||||
flowables.append(para)
|
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
elif element.name.lower() == "p":
|
|
||||||
para = Paragraph(element.decode_contents(), normal_style)
|
|
||||||
flowables.append(para)
|
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
elif element.name.lower() == "pagebreak" or (element.name.lower() == "div" and "page-break" in element.get("class", [])):
|
|
||||||
flowables.append(PageBreak())
|
flowables.append(PageBreak())
|
||||||
else:
|
else:
|
||||||
text = element.get_text().strip()
|
first_page_encountered = True
|
||||||
if text:
|
header_para = Paragraph(part, styles['Heading1'])
|
||||||
para = Paragraph(text, normal_style)
|
flowables.append(header_para)
|
||||||
flowables.append(para)
|
flowables.append(Spacer(1, 12))
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
else:
|
|
||||||
# Si se detecta el patrón [Page X] en el contenido, se usa para dividir las páginas.
|
|
||||||
if re.search(r'\[Page\s+\d+\]', content, re.IGNORECASE):
|
|
||||||
# Dividir incluyendo el separador (usamos grupo de captura)
|
|
||||||
parts = re.split(r'(\[Page\s+\d+\])', content, flags=re.IGNORECASE)
|
|
||||||
current_header = ""
|
|
||||||
for part in parts:
|
|
||||||
part = part.strip()
|
|
||||||
if not part:
|
|
||||||
continue
|
|
||||||
# Si es un encabezado [Page X]
|
|
||||||
if re.match(r'\[Page\s+\d+\]', part, re.IGNORECASE):
|
|
||||||
# Extraemos el número o texto sin los corchetes para usarlo como encabezado
|
|
||||||
current_header = part.strip("[]")
|
|
||||||
header_para = Paragraph(current_header, styles['Heading1'])
|
|
||||||
flowables.append(header_para)
|
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
else:
|
|
||||||
# Es el contenido de la página
|
|
||||||
processed = process_text(part)
|
|
||||||
para = Paragraph(processed.replace("\n", "<br/>"), normal_style)
|
|
||||||
flowables.append(para)
|
|
||||||
flowables.append(Spacer(1, 12))
|
|
||||||
flowables.append(PageBreak())
|
|
||||||
else:
|
else:
|
||||||
# Si no se detectan separadores, se usa el método anterior basado en "Page X:" sin corchetes.
|
if part.startswith("```"):
|
||||||
pages = re.split(r'(?i)Page\s+\d+:\s*', content)
|
lines = part.splitlines()
|
||||||
if pages and pages[0].strip() == "":
|
if lines and lines[0].startswith("```"):
|
||||||
pages = pages[1:]
|
lines = lines[1:]
|
||||||
if len(pages) <= 1:
|
if lines and lines[-1].strip() == "```":
|
||||||
pages = content.split("\n\n")
|
lines = lines[:-1]
|
||||||
processed_pages = [process_text(page) for page in pages if page.strip() != ""]
|
part = "\n".join(lines)
|
||||||
for i, page_text in enumerate(processed_pages, start=1):
|
# Si parece HTML, se procesa para separar cada etiqueta de interés
|
||||||
header = Paragraph(f"PAGE {i}", styles['Heading1'])
|
if re.search(r'<\s*html', part, re.IGNORECASE) or re.search(r'<\s*(p|h[1-6])', part, re.IGNORECASE):
|
||||||
flowables.append(header)
|
if not part.lower().startswith("<html"):
|
||||||
flowables.append(Spacer(1, 12))
|
part = "<html>" + part + "</html>"
|
||||||
para = Paragraph(page_text.replace("\n", "<br/>"), normal_style)
|
soup = BeautifulSoup(part, "html.parser")
|
||||||
flowables.append(para)
|
for element in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p']):
|
||||||
if i < len(processed_pages):
|
if element.name.lower() in ["h1", "h2", "h3", "h4", "h5", "h6"]:
|
||||||
flowables.append(Spacer(1, 24))
|
style = header_styles.get(element.name.lower(), styles['Heading1'])
|
||||||
flowables.append(PageBreak())
|
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)
|
doc.build(flowables)
|
||||||
return output_pdf
|
return output_pdf
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue