"""Author RAG Chatbot SaaS — Book Summarizer. Generates a concise summary for each book using Facebook's BART model. This runs ONCE after embedding — result stored on the Book record as ai_summary. RULE: Summarizer runs async after embedding completes — never blocks ingestion. """ import structlog logger = structlog.get_logger(__name__) _summarizer_pipeline = None async def get_summarizer(): """Lazily load and cache the BART summarization pipeline. Returns: HuggingFace pipeline for summarization. """ global _summarizer_pipeline if _summarizer_pipeline is None: from transformers import pipeline logger.info("Loading BART summarizer model (first load may take a moment)...") _summarizer_pipeline = pipeline( "summarization", model="facebook/bart-large-cnn", device=-1, # CPU (-1), use 0 for GPU ) logger.info("BART summarizer loaded successfully") return _summarizer_pipeline async def summarize_book(text: str, max_length: int = 300) -> str: """Generate a concise summary of a book's content using BART. Uses the first 3000 characters as representative input (BART has input limits). Falls back gracefully if model fails. Args: text: Full extracted book text. max_length: Maximum summary length in tokens. Returns: Summary string (or empty string on failure). """ if not text.strip(): return "" # BART works best with ~1024 tokens input — use beginning of book input_text = text[:4000].strip() try: summarizer = await get_summarizer() result = summarizer( input_text, max_length=max_length, min_length=60, do_sample=False, truncation=True, ) summary = result[0]["summary_text"].strip() logger.info("Book summary generated", length=len(summary)) return summary except Exception as e: logger.error("BART summarization failed", error=str(e)) return _extract_first_paragraph(text) def _extract_first_paragraph(text: str) -> str: """Fallback: extract the first meaningful paragraph as a summary. Args: text: Full document text. Returns: First non-empty paragraph, truncated to 500 chars. """ for paragraph in text.split("\n\n"): stripped = paragraph.strip() if len(stripped) > 100: return stripped[:500] + ("..." if len(stripped) > 500 else "") return text[:300].strip()