This commit is contained in:
urieljarethbusiness-cpu
2026-05-17 10:14:14 -06:00
parent d8773b2508
commit 64b3d15b90
61 changed files with 7612 additions and 0 deletions
@@ -0,0 +1,20 @@
from __future__ import annotations
from pathlib import Path
from backend.app.services.markdown_builder import title_from_path
from backend.app.services.processors.base import ProcessedContent
from backend.app.services.providers.deepgram_client import DeepgramClient
class AudioProcessor:
def process(self, path: Path) -> ProcessedContent:
client = DeepgramClient()
data = client.transcribe_file(path)
transcript, confidence = client.transcript_from_response(data)
confidence_line = f"\n\n_Confianza estimada: {confidence * 100:.1f}%_" if isinstance(confidence, (int, float)) else ""
return ProcessedContent(
title=f"Transcripcion {title_from_path(path)}",
body=f"## Transcripcion\n\n{transcript}{confidence_line}",
processor="deepgram",
)
+10
View File
@@ -0,0 +1,10 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class ProcessedContent:
title: str
body: str
processor: str
@@ -0,0 +1,30 @@
from __future__ import annotations
from pathlib import Path
from docx import Document
from backend.app.services.markdown_builder import title_from_path
from backend.app.services.processors.base import ProcessedContent
class DocxProcessor:
def process(self, path: Path) -> ProcessedContent:
doc = Document(path)
blocks: list[str] = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text:
blocks.append(text)
for table in doc.tables:
rows = [[cell.text.strip().replace("\n", " ") for cell in row.cells] for row in table.rows]
if rows:
blocks.append(self._table_to_markdown(rows))
return ProcessedContent(title=title_from_path(path), body="\n\n".join(blocks), processor="docx")
def _table_to_markdown(self, rows: list[list[str]]) -> str:
header = rows[0]
lines = [f"| {' | '.join(header)} |", f"| {' | '.join(['---'] * len(header))} |"]
for row in rows[1:]:
lines.append(f"| {' | '.join(row)} |")
return "\n".join(lines)
@@ -0,0 +1,81 @@
from __future__ import annotations
from pathlib import Path
import fitz
from backend.app.services.markdown_builder import title_from_path
from backend.app.services.processors.base import ProcessedContent
from backend.app.services.providers.mistral_client import MistralClient
class PdfProcessor:
def __init__(self, use_ocr: bool = False, page_ranges: str | None = None):
self.use_ocr = use_ocr
self.page_ranges = page_ranges
def process(self, path: Path) -> ProcessedContent:
if self.use_ocr:
return self._process_ocr(path)
doc = fitz.open(path)
pages: list[str] = []
for index in self._selected_pages(len(doc)):
page = doc[index - 1]
text = page.get_text("text").strip()
if text:
pages.append(f"## Pagina {index}\n\n{text}")
body = "\n\n".join(pages).strip()
if len(body) < 80:
raise RuntimeError("El PDF tiene poco texto extraible; procesalo con OCR")
return ProcessedContent(title=title_from_path(path), body=body, processor="pdf-text")
def _process_ocr(self, path: Path) -> ProcessedContent:
doc = fitz.open(path)
client = MistralClient()
parts: list[str] = []
temp_paths: list[Path] = []
try:
for index in self._selected_pages(len(doc)):
page = doc[index - 1]
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2), alpha=False)
image_path = path.with_name(f"{path.stem}-page-{index}.jpg")
pix.save(image_path)
temp_paths.append(image_path)
parts.append(f"## Pagina {index}\n\n{client.ocr_image(image_path)}")
finally:
for temp_path in temp_paths:
temp_path.unlink(missing_ok=True)
return ProcessedContent(title=title_from_path(path), body="\n\n".join(parts), processor="mistral-ocr")
def _selected_pages(self, total_pages: int) -> list[int]:
if total_pages < 1:
raise RuntimeError("El PDF no tiene paginas")
if not self.page_ranges or not self.page_ranges.strip():
return list(range(1, total_pages + 1))
selected: list[int] = []
for raw_part in self.page_ranges.split(","):
part = raw_part.strip()
if not part:
continue
if "-" in part:
start_text, end_text = [piece.strip() for piece in part.split("-", 1)]
if not start_text.isdigit() or not end_text.isdigit():
raise RuntimeError(f"Rango de paginas invalido: {part}")
start = int(start_text)
end = int(end_text)
if start > end:
raise RuntimeError(f"Rango de paginas invertido: {part}")
selected.extend(range(start, end + 1))
else:
if not part.isdigit():
raise RuntimeError(f"Pagina invalida: {part}")
selected.append(int(part))
unique_pages = sorted(set(selected))
invalid = [page for page in unique_pages if page < 1 or page > total_pages]
if invalid:
raise RuntimeError(f"Paginas fuera de rango: {', '.join(map(str, invalid))}. El PDF tiene {total_pages} paginas")
if not unique_pages:
raise RuntimeError("No se seleccionaron paginas para procesar")
return unique_pages
@@ -0,0 +1,15 @@
from __future__ import annotations
from pathlib import Path
from backend.app.services.markdown_builder import title_from_path
from backend.app.services.processors.base import ProcessedContent
class TextProcessor:
def process(self, path: Path) -> ProcessedContent:
return ProcessedContent(
title=title_from_path(path),
body=path.read_text(encoding="utf-8", errors="replace"),
processor="text",
)
@@ -0,0 +1,46 @@
from __future__ import annotations
import subprocess
from pathlib import Path
from backend.app.services.processors.audio_processor import AudioProcessor
from backend.app.services.processors.base import ProcessedContent
class VideoProcessor:
def __init__(self, tmp_dir: Path):
self.tmp_dir = tmp_dir
def process(self, path: Path) -> ProcessedContent:
self.tmp_dir.mkdir(parents=True, exist_ok=True)
audio_path = self.tmp_dir / f"{path.stem}.wav"
try:
subprocess.run(
[
"ffmpeg",
"-y",
"-i",
str(path),
"-vn",
"-acodec",
"pcm_s16le",
"-ar",
"16000",
"-ac",
"1",
str(audio_path),
],
check=True,
capture_output=True,
text=True,
)
processed = AudioProcessor().process(audio_path)
processed.title = f"Transcripcion {path.stem}"
processed.processor = "ffmpeg+deepgram"
return processed
except FileNotFoundError as exc:
raise RuntimeError("ffmpeg no esta instalado o no esta disponible en PATH") from exc
except subprocess.CalledProcessError as exc:
raise RuntimeError(f"No se pudo extraer audio del video: {exc.stderr}") from exc
finally:
audio_path.unlink(missing_ok=True)