FinSightAI / backend /services /ingestion.py
Aniket2003333333's picture
start
7248d39
Raw
History Blame Contribute Delete
6.11 kB
"""Document ingestion pipeline."""
import logging
import uuid
from datetime import datetime, timezone
from typing import Dict, List
from config import settings
from db.faiss_client import FaissDB
from models.embedder import MiniCPMEmbedder
from models.ocr import MiniCPMVOCR
from utils.chunker import FinanceAwareChunker
from utils.liteparse_parser import parse_document
from utils.pdf_parser import extract_pdf_spatial_pages, render_page_image
logger = logging.getLogger(__name__)
class IngestionService:
def __init__(
self,
embedder: MiniCPMEmbedder,
ocr: MiniCPMVOCR,
db: FaissDB,
):
self.embedder = embedder
self.ocr = ocr
self.db = db
self.chunker = FinanceAwareChunker()
def _embed_texts(self, texts: List[str]) -> List[List[float]]:
batch_size = settings.EMBED_BATCH_SIZE
vectors: List[List[float]] = []
total = len(texts)
for start in range(0, total, batch_size):
batch = texts[start : start + batch_size]
logger.info(
"Embedding batch %d–%d of %d",
start + 1,
min(start + len(batch), total),
total,
)
vectors.extend(self.embedder.embed_documents(batch))
return vectors
def ingest_pdf(self, file_bytes: bytes, filename: str) -> Dict:
doc_id = str(uuid.uuid4())
now = datetime.now(timezone.utc).isoformat()
all_chunks: List[Dict] = []
logger.info("Parsing %s ...", filename)
parse_result = parse_document(file_bytes, filename, self.ocr)
sparse_pages = {
page_num
for page_num, _, is_sparse in extract_pdf_spatial_pages(file_bytes)
if is_sparse
}
logger.info(
"Parsed %d pages (%d OCR pages) from %s",
len(parse_result.pages),
len(sparse_pages),
filename,
)
chart_ocr_count = 0
for parsed_page in parse_result.pages:
page_text = parsed_page.text.strip()
if not page_text:
continue
page_num = parsed_page.page_num
source = "liteparse" if page_num in sparse_pages else "embedded"
page_chunks = self.chunker.chunk(
page_text, page_num=page_num, source=source
)
if (
chart_ocr_count < settings.CHART_OCR_MAX_PAGES
and self.chunker.should_extract_chart(page_text)
):
try:
logger.info(
"Chart OCR page %d (%d/%d cap)",
page_num,
chart_ocr_count + 1,
settings.CHART_OCR_MAX_PAGES,
)
page_image = render_page_image(file_bytes, page_num)
chart_desc = self.ocr.describe_chart(page_image)
if chart_desc and len(chart_desc.strip()) > 20:
chart_chunks = self.chunker.chunk(
chart_desc,
page_num=page_num,
source="ocr_chart",
section_override="chart_data",
)
page_chunks.extend(chart_chunks)
chart_ocr_count += 1
except Exception as e:
logger.warning(
"Chart extraction failed on page %d: %s", page_num, e
)
for chunk in page_chunks:
chunk["document_id"] = doc_id
chunk["document_name"] = filename
chunk["document_type"] = "pdf"
chunk["page_number"] = page_num
chunk["created_at"] = now
all_chunks.extend(page_chunks)
if not all_chunks:
return {"document_id": doc_id, "chunks_ingested": 0, "filename": filename}
texts = [c["text"] for c in all_chunks]
logger.info("Embedding %d chunks from %s ...", len(texts), filename)
vectors = self._embed_texts(texts)
logger.info("Saving %d chunks to FAISS ...", len(all_chunks))
self.db.upsert_chunks(all_chunks, vectors)
logger.info(
"Ingestion complete: %s (%d chunks, %d chart OCR pages)",
filename,
len(all_chunks),
chart_ocr_count,
)
return {
"document_id": doc_id,
"chunks_ingested": len(all_chunks),
"filename": filename,
}
def ingest_image(self, file_bytes: bytes, filename: str) -> Dict:
doc_id = str(uuid.uuid4())
now = datetime.now(timezone.utc).isoformat()
logger.info("Parsing image %s ...", filename)
parse_result = parse_document(file_bytes, filename, self.ocr)
ocr_text = parse_result.text.strip()
chunks = self.chunker.chunk(ocr_text, page_num=1, source="liteparse")
try:
chart_desc = self.ocr.describe_chart(file_bytes)
if chart_desc and len(chart_desc.strip()) > 20:
chart_chunks = self.chunker.chunk(
chart_desc,
page_num=1,
source="ocr_chart",
section_override="chart_data",
)
chunks.extend(chart_chunks)
except Exception as e:
logger.warning("Chart extraction failed: %s", e)
for chunk in chunks:
chunk["document_id"] = doc_id
chunk["document_name"] = filename
chunk["document_type"] = "image"
chunk["page_number"] = 1
chunk["created_at"] = now
texts = [c["text"] for c in chunks]
logger.info("Embedding %d chunks from %s ...", len(texts), filename)
vectors = self._embed_texts(texts)
self.db.upsert_chunks(chunks, vectors)
return {
"document_id": doc_id,
"chunks_ingested": len(chunks),
"filename": filename,
}