Spaces:
Running
Running
| """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() | |