Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- src/README.md +116 -0
- src/note_generator.py +282 -0
- src/schemas.py +75 -0
src/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Summarization Module 📝
|
| 2 |
+
|
| 3 |
+
## Responsibility
|
| 4 |
+
This module handles **text summarization and conversion to structured study notes**.
|
| 5 |
+
|
| 6 |
+
## Functionality
|
| 7 |
+
1. Receive transcribed text from videos.
|
| 8 |
+
2. Use a **local mT5 model** (map-reduce pipeline) to analyze text and generate structured JSON notes.
|
| 9 |
+
3. Produce clean Markdown output with:
|
| 10 |
+
- Source & Duration header
|
| 11 |
+
- Overall Summary
|
| 12 |
+
- Chronological Timeline (3-7 segments with Key Insight + Why It Matters)
|
| 13 |
+
- Conclusion
|
| 14 |
+
|
| 15 |
+
## Pipeline (Map-Reduce)
|
| 16 |
+
1. **Map** — `TranscriptSegmenter.segment_text_by_words()` splits the transcript into
|
| 17 |
+
fixed-size word chunks (default 350 words, safely under mT5's 512-token limit).
|
| 18 |
+
Each chunk is summarized independently by the local model, producing one
|
| 19 |
+
`SegmentSchema` entry per chunk.
|
| 20 |
+
2. **Reduce** — All chunk-level summaries are concatenated and summarized once more
|
| 21 |
+
by the same model, producing the single overall `summary` field.
|
| 22 |
+
3. **Validate** — The assembled result is passed through
|
| 23 |
+
`SummarySchema.model_validate()`. If the chunk count falls outside the
|
| 24 |
+
schema's allowed range (3-7), segments are automatically merged or padded
|
| 25 |
+
before validation. If validation still fails, a safe schema-valid fallback
|
| 26 |
+
is returned instead.
|
| 27 |
+
|
| 28 |
+
## Files
|
| 29 |
+
|
| 30 |
+
### 1. `schemas.py`
|
| 31 |
+
- **Purpose:** Single source of truth for all Pydantic data models.
|
| 32 |
+
- **Key Classes:**
|
| 33 |
+
- `SummarySchema` — Full structured output (title, detected_language, summary, segments, suggested_category, conclusion, topics).
|
| 34 |
+
- `SegmentSchema` — A timeline section (title, summary, key_insight, why_it_matters).
|
| 35 |
+
|
| 36 |
+
### 2. `note_generator.py`
|
| 37 |
+
- **Purpose:** Generate notes using a local mT5 model with chunk-based map-reduce and schema validation.
|
| 38 |
+
- **Main Class:** `NoteGenerator`
|
| 39 |
+
- **Key Methods:**
|
| 40 |
+
- `generateSummary(transcript, title)` — Runs the map-reduce pipeline and returns a `SummarySchema`-validated dict.
|
| 41 |
+
- `format_notes_to_markdown(json_notes)` — Converts JSON to clean Markdown.
|
| 42 |
+
- `format_final_notes(notes, title, url, duration)` — Wraps Markdown with Source/Duration header.
|
| 43 |
+
|
| 44 |
+
### 3. `segmenter.py`
|
| 45 |
+
- **Purpose:** Split long texts into smaller segments for preprocessing.
|
| 46 |
+
- **Main Class:** `TranscriptSegmenter`
|
| 47 |
+
- **Key Methods:**
|
| 48 |
+
- `segment_text_by_words()` — Split text into fixed-size word chunks (used by the mT5 map step).
|
| 49 |
+
- `segment_by_time()` — Split by time intervals.
|
| 50 |
+
- `segment_by_topic()` — Split by paragraph/topic boundaries.
|
| 51 |
+
- `clean_text()` — Remove filler words.
|
| 52 |
+
|
| 53 |
+
## JSON Output Structure
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"title": "...",
|
| 57 |
+
"detected_language": "Arabic",
|
| 58 |
+
"summary": "Overall summary (3-5 sentences)",
|
| 59 |
+
"segments": [
|
| 60 |
+
{
|
| 61 |
+
"title": "Segment title",
|
| 62 |
+
"summary": "What this section covers",
|
| 63 |
+
"key_insight": "Most important point",
|
| 64 |
+
"why_it_matters": "Why this is valuable"
|
| 65 |
+
}
|
| 66 |
+
],
|
| 67 |
+
"suggested_category": "General",
|
| 68 |
+
"conclusion": "Final takeaway",
|
| 69 |
+
"topics": ["Topic1", "Topic2"]
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
> **Note:** `topics` and `suggested_category` are hidden metadata — not rendered in markdown, used by downstream modules only.
|
| 73 |
+
|
| 74 |
+
## Markdown Output Order
|
| 75 |
+
1. **Source** — video URL
|
| 76 |
+
2. **Duration** — video length
|
| 77 |
+
3. **Overall Summary** — one concise summary
|
| 78 |
+
4. **Timeline** — chronological segments (3-7), each with Key Insight + Why It Matters
|
| 79 |
+
5. **Conclusion** — final takeaway
|
| 80 |
+
|
| 81 |
+
## Labels (Localized)
|
| 82 |
+
| Key | English | Arabic |
|
| 83 |
+
|-----|---------|--------|
|
| 84 |
+
| source | Source | المصدر |
|
| 85 |
+
| duration | Duration | المدة |
|
| 86 |
+
| summary | Overall Summary | الملخص العام |
|
| 87 |
+
| timeline | Timeline | التسلسل الزمني |
|
| 88 |
+
| insight | Key Insight | أهم نقطة |
|
| 89 |
+
| why | Why It Matters | لماذا يهم؟ |
|
| 90 |
+
| conclusion | Conclusion | الخلاصة |
|
| 91 |
+
|
| 92 |
+
## Testing
|
| 93 |
+
```python
|
| 94 |
+
from src.summarization.note_generator import NoteGenerator
|
| 95 |
+
|
| 96 |
+
generator = NoteGenerator()
|
| 97 |
+
transcript = "Here is the complete video transcript..."
|
| 98 |
+
title = "Introduction to Python"
|
| 99 |
+
|
| 100 |
+
# Generate notes
|
| 101 |
+
summary_json = generator.generateSummary(transcript, title)
|
| 102 |
+
notes_md = generator.format_notes_to_markdown(summary_json)
|
| 103 |
+
print(notes_md)
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## Libraries Used
|
| 107 |
+
- `transformers` — Load and run the local mT5 model (HuggingFace).
|
| 108 |
+
- `sentencepiece` — Tokenizer backend required by mT5.
|
| 109 |
+
- `langdetect` — Automatic language detection for multilingual support.
|
| 110 |
+
- `torch` — PyTorch runtime for model inference.
|
| 111 |
+
- `pydantic` — Data validation and schema enforcement.
|
| 112 |
+
|
| 113 |
+
## Environment Variables
|
| 114 |
+
| Variable | Default | Description |
|
| 115 |
+
|----------|---------|-------------|
|
| 116 |
+
| `MT5_MODEL_NAME` | `csebuetnlp/mT5_multilingual_XLSum` | HuggingFace model ID to load (fine-tuned for multilingual summarization, including Arabic) |
|
src/note_generator.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
| 7 |
+
from pydantic import ValidationError
|
| 8 |
+
|
| 9 |
+
from ..utils.logger import setup_logger
|
| 10 |
+
from ..segmentation.segmenter import TranscriptSegmenter
|
| 11 |
+
from .schemas import SummarySchema, SegmentSchema
|
| 12 |
+
|
| 13 |
+
logger = setup_logger(__name__)
|
| 14 |
+
|
| 15 |
+
# Segment titles cycled for the timeline (Arabic, matches existing UX copy)
|
| 16 |
+
SEGMENT_TITLES = [
|
| 17 |
+
"المقدمة والأفكار الأولى",
|
| 18 |
+
"الشرح المتعمق والتحليل",
|
| 19 |
+
"نقاط إضافية ومناقشة",
|
| 20 |
+
"أمثلة وتفاصيل تطبيقية",
|
| 21 |
+
"الشرح المتقدم",
|
| 22 |
+
"ملاحظات ختامية",
|
| 23 |
+
"الخلاصة والتوصيات النهائية",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
WHITESPACE_RE = re.compile(r"\s+")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class NoteGenerator:
|
| 30 |
+
"""Real AI Summarization Engine using a lightweight multilingual mT5 model.
|
| 31 |
+
|
| 32 |
+
Pipeline (map-reduce):
|
| 33 |
+
1. MAP -> split transcript into word-bounded chunks (TranscriptSegmenter)
|
| 34 |
+
and summarize each chunk individually with the local model.
|
| 35 |
+
2. REDUCE -> summarize the concatenation of all chunk-summaries to produce
|
| 36 |
+
one coherent overall summary.
|
| 37 |
+
3. VALIDATE -> assemble everything into SummarySchema via model_validate()
|
| 38 |
+
so malformed/missing fields are caught before reaching
|
| 39 |
+
the formatting layer.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def __init__(self, chunk_size_words: int = 350):
|
| 43 |
+
self.model_id = os.environ.get("MT5_MODEL_NAME", "csebuetnlp/mT5_multilingual_XLSum")
|
| 44 |
+
logger.info(f"🤖 Initializing real local AI Model: {self.model_id}...")
|
| 45 |
+
|
| 46 |
+
cache_dir = os.path.join(os.getcwd(), "hf_cache")
|
| 47 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 50 |
+
self.model_id, use_fast=False, cache_dir=cache_dir
|
| 51 |
+
)
|
| 52 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 53 |
+
self.model = AutoModelForSeq2SeqLM.from_pretrained(
|
| 54 |
+
self.model_id, cache_dir=cache_dir
|
| 55 |
+
).to(self.device)
|
| 56 |
+
|
| 57 |
+
self.segmenter = TranscriptSegmenter(max_segment_words=chunk_size_words)
|
| 58 |
+
self.chunk_size_words = chunk_size_words
|
| 59 |
+
|
| 60 |
+
logger.info("✅ Real AI Model loaded successfully and ready!")
|
| 61 |
+
|
| 62 |
+
def _run_model_summarization(
|
| 63 |
+
self,
|
| 64 |
+
text: str,
|
| 65 |
+
max_length: int = 84,
|
| 66 |
+
num_beams: int = 2,
|
| 67 |
+
) -> str:
|
| 68 |
+
"""Run the local mT5 model on a single piece of text and return its summary."""
|
| 69 |
+
try:
|
| 70 |
+
clean_text = WHITESPACE_RE.sub(" ", text.strip())
|
| 71 |
+
if not clean_text:
|
| 72 |
+
return ""
|
| 73 |
+
|
| 74 |
+
inputs = self.tokenizer(
|
| 75 |
+
[clean_text],
|
| 76 |
+
truncation=True,
|
| 77 |
+
padding="longest",
|
| 78 |
+
max_length=512,
|
| 79 |
+
return_tensors="pt",
|
| 80 |
+
).to(self.device)
|
| 81 |
+
|
| 82 |
+
with torch.no_grad():
|
| 83 |
+
summary_ids = self.model.generate(
|
| 84 |
+
inputs["input_ids"],
|
| 85 |
+
num_beams=num_beams,
|
| 86 |
+
max_length=max_length,
|
| 87 |
+
no_repeat_ngram_size=2,
|
| 88 |
+
early_stopping=True,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
return self.tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error during model generation: {str(e)}")
|
| 94 |
+
return ""
|
| 95 |
+
|
| 96 |
+
def _map_chunks(self, transcript_text: str, video_title: str) -> List[Dict]:
|
| 97 |
+
"""MAP step: split transcript into chunks and summarize each one."""
|
| 98 |
+
chunks = self.segmenter.segment_text_by_words(
|
| 99 |
+
transcript_text, chunk_size=self.chunk_size_words
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if not chunks:
|
| 103 |
+
chunks = [f"هذا الفيديو يتحدث عن {video_title}."]
|
| 104 |
+
|
| 105 |
+
segments_list = []
|
| 106 |
+
for i, chunk in enumerate(chunks):
|
| 107 |
+
chunk_summary = self._run_model_summarization(chunk)
|
| 108 |
+
if not chunk_summary or len(chunk_summary.strip()) < 5:
|
| 109 |
+
chunk_summary = f"شرح وتحليل للأفكار الواردة في الجزء رقم {i + 1} من الفيديو."
|
| 110 |
+
|
| 111 |
+
title = SEGMENT_TITLES[i % len(SEGMENT_TITLES)]
|
| 112 |
+
segments_list.append(
|
| 113 |
+
{
|
| 114 |
+
"title": title,
|
| 115 |
+
"summary": chunk_summary,
|
| 116 |
+
"key_insight": f"الاستنتاج الجوهري من هذا الجزء يدعم فهم سياق {video_title}.",
|
| 117 |
+
"why_it_matters": "يساعد هذا التقسيم في استيعاب النقاط الرئيسية بشكل منظم ومتسلسل.",
|
| 118 |
+
}
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Keep the timeline within the schema's allowed range (3-7 segments).
|
| 122 |
+
if len(segments_list) > 7:
|
| 123 |
+
segments_list = self._merge_to_limit(segments_list, limit=7)
|
| 124 |
+
elif len(segments_list) < 3:
|
| 125 |
+
segments_list = self._pad_to_minimum(segments_list, video_title, minimum=3)
|
| 126 |
+
|
| 127 |
+
return segments_list
|
| 128 |
+
|
| 129 |
+
def _merge_to_limit(self, segments_list: List[Dict], limit: int) -> List[Dict]:
|
| 130 |
+
"""Merge extra segments into the last allowed slot to respect max_length."""
|
| 131 |
+
kept = segments_list[: limit - 1]
|
| 132 |
+
overflow = segments_list[limit - 1:]
|
| 133 |
+
merged_summary = " ".join(s["summary"] for s in overflow)
|
| 134 |
+
kept.append(
|
| 135 |
+
{
|
| 136 |
+
"title": SEGMENT_TITLES[-1],
|
| 137 |
+
"summary": merged_summary,
|
| 138 |
+
"key_insight": overflow[-1]["key_insight"],
|
| 139 |
+
"why_it_matters": overflow[-1]["why_it_matters"],
|
| 140 |
+
}
|
| 141 |
+
)
|
| 142 |
+
return kept
|
| 143 |
+
|
| 144 |
+
def _pad_to_minimum(
|
| 145 |
+
self, segments_list: List[Dict], video_title: str, minimum: int
|
| 146 |
+
) -> List[Dict]:
|
| 147 |
+
"""Pad segments (very short transcripts) to satisfy min_length=3."""
|
| 148 |
+
while len(segments_list) < minimum:
|
| 149 |
+
idx = len(segments_list)
|
| 150 |
+
segments_list.append(
|
| 151 |
+
{
|
| 152 |
+
"title": SEGMENT_TITLES[idx % len(SEGMENT_TITLES)],
|
| 153 |
+
"summary": f"تتمة الشرح والنقاط المرتبطة بـ {video_title}.",
|
| 154 |
+
"key_insight": "نقطة تكميلية تدعم الفهم العام للموضوع.",
|
| 155 |
+
"why_it_matters": "تضيف سياقًا إضافيًا لاستيعاب الفيديو بالكامل.",
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
+
return segments_list
|
| 159 |
+
|
| 160 |
+
def _reduce_summary(self, segments_list: List[Dict], video_title: str) -> str:
|
| 161 |
+
"""REDUCE step: summarize the combined chunk-summaries into one overall summary."""
|
| 162 |
+
combined_text = " ".join(seg["summary"] for seg in segments_list)
|
| 163 |
+
overall = self._run_model_summarization(combined_text, max_length=130, num_beams=4)
|
| 164 |
+
|
| 165 |
+
if not overall or len(overall.strip()) < 5:
|
| 166 |
+
overall = f"استعراض شامل ومناقشة تفصيلية لموضوع: {video_title}."
|
| 167 |
+
|
| 168 |
+
return overall
|
| 169 |
+
|
| 170 |
+
def generateSummary(self, transcript_text: str, video_title: str) -> Dict:
|
| 171 |
+
"""Generates a structured AI summary, validated against SummarySchema."""
|
| 172 |
+
logger.info("📝 Real AI summary generation triggered (map-reduce pipeline).")
|
| 173 |
+
|
| 174 |
+
if not transcript_text or len(transcript_text.strip()) < 10:
|
| 175 |
+
transcript_text = (
|
| 176 |
+
f"هذا الفيديو يتحدث عن {video_title} وشرح تفصيلي للمحتوى المكتوب في العنوان."
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
# 1. MAP — summarize each chunk independently
|
| 180 |
+
segments_list = self._map_chunks(transcript_text, video_title)
|
| 181 |
+
|
| 182 |
+
# 2. REDUCE — summarize the chunk-summaries into one overall summary
|
| 183 |
+
overall_summary = self._reduce_summary(segments_list, video_title)
|
| 184 |
+
|
| 185 |
+
candidate = {
|
| 186 |
+
"title": video_title,
|
| 187 |
+
"detected_language": "Arabic",
|
| 188 |
+
"summary": overall_summary,
|
| 189 |
+
"segments": segments_list,
|
| 190 |
+
"suggested_category": "General",
|
| 191 |
+
"conclusion": f"تم التلخيص بنجاح باستخدام موديل mT5 المطور لـ {video_title}.",
|
| 192 |
+
"topics": ["تلخيص تلقائي", "ذكاء اصطناعي محلي", "mT5-XLSum"],
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
# 3. VALIDATE — enforce the schema contract before returning
|
| 196 |
+
try:
|
| 197 |
+
validated = SummarySchema.model_validate(candidate)
|
| 198 |
+
return validated.model_dump()
|
| 199 |
+
except ValidationError as e:
|
| 200 |
+
logger.error(f"SummarySchema validation failed: {e}")
|
| 201 |
+
return self._fallback_summary(video_title)
|
| 202 |
+
|
| 203 |
+
def _fallback_summary(self, video_title: str) -> Dict:
|
| 204 |
+
"""Safe, schema-valid fallback used only if validation fails."""
|
| 205 |
+
fallback = SummarySchema(
|
| 206 |
+
title=video_title,
|
| 207 |
+
detected_language="Arabic",
|
| 208 |
+
summary=f"استعراض شامل ومناقشة تفصيلية لموضوع: {video_title}.",
|
| 209 |
+
segments=[
|
| 210 |
+
SegmentSchema(
|
| 211 |
+
title=SEGMENT_TITLES[i],
|
| 212 |
+
summary=f"شرح وتحليل للأفكار الواردة في الجزء رقم {i + 1} من الفيديو.",
|
| 213 |
+
key_insight=f"الاستنتاج الجوهري من هذا الجزء يدعم فهم سياق {video_title}.",
|
| 214 |
+
why_it_matters="يساعد هذا التقسيم في استيعاب النقاط الرئيسية بشكل منظم ومتسلسل.",
|
| 215 |
+
)
|
| 216 |
+
for i in range(3)
|
| 217 |
+
],
|
| 218 |
+
conclusion=f"تم التلخيص بنجاح باستخدام موديل mT5 المطور لـ {video_title}.",
|
| 219 |
+
topics=["تلخيص تلقائي", "ذكاء اصطناعي محلي", "mT5-XLSum"],
|
| 220 |
+
)
|
| 221 |
+
return fallback.model_dump()
|
| 222 |
+
|
| 223 |
+
def format_notes_to_markdown(self, json_notes: Dict) -> str:
|
| 224 |
+
"""Convert JSON notes to clean Markdown."""
|
| 225 |
+
lines = []
|
| 226 |
+
lines.append("## 📋 الملخص العام")
|
| 227 |
+
lines.append("")
|
| 228 |
+
lines.append(json_notes.get("summary", ""))
|
| 229 |
+
lines.append("")
|
| 230 |
+
lines.append("---")
|
| 231 |
+
lines.append("")
|
| 232 |
+
lines.append("## 🕐 التسلسل الزمني للأفكار")
|
| 233 |
+
lines.append("")
|
| 234 |
+
|
| 235 |
+
for i, seg in enumerate(json_notes.get("segments", []), start=1):
|
| 236 |
+
lines.append(f"### {i}. {seg['title']}")
|
| 237 |
+
lines.append("")
|
| 238 |
+
lines.append(seg["summary"])
|
| 239 |
+
lines.append("")
|
| 240 |
+
lines.append(f"> **💎 أهم نقطة مستفادة:** {seg['key_insight']}")
|
| 241 |
+
lines.append("")
|
| 242 |
+
lines.append(f"> **لماذا يهم هذا الجزء؟** {seg['why_it_matters']}")
|
| 243 |
+
lines.append("")
|
| 244 |
+
|
| 245 |
+
lines.append("---")
|
| 246 |
+
lines.append("")
|
| 247 |
+
lines.append("## 🔖 الخلاصة")
|
| 248 |
+
lines.append("")
|
| 249 |
+
lines.append(f"> {json_notes.get('conclusion', '')}")
|
| 250 |
+
|
| 251 |
+
return "\n".join(lines)
|
| 252 |
+
|
| 253 |
+
def format_final_notes(
|
| 254 |
+
self,
|
| 255 |
+
notes: str,
|
| 256 |
+
video_title: str,
|
| 257 |
+
video_url: str,
|
| 258 |
+
duration: int,
|
| 259 |
+
detected_language: str = "English",
|
| 260 |
+
) -> str:
|
| 261 |
+
"""Wrap the formatted Markdown body with Source + Duration header."""
|
| 262 |
+
if duration and duration > 0:
|
| 263 |
+
minutes = duration // 60
|
| 264 |
+
secs = duration % 60
|
| 265 |
+
duration_str = f"{minutes:02d}:{secs:02d} دقيقة"
|
| 266 |
+
else:
|
| 267 |
+
duration_str = "غير محدد"
|
| 268 |
+
|
| 269 |
+
header = (
|
| 270 |
+
f"# {video_title}\n\n"
|
| 271 |
+
f"---\n\n"
|
| 272 |
+
f"> **المصدر:** {video_url} \n"
|
| 273 |
+
f"> **المدة:** {duration_str}\n\n"
|
| 274 |
+
f"---\n\n"
|
| 275 |
+
)
|
| 276 |
+
return header + notes
|
| 277 |
+
|
| 278 |
+
def chat_with_note(
|
| 279 |
+
self, note_content: str, question: str, history: list[dict] | None = None
|
| 280 |
+
) -> str:
|
| 281 |
+
"""Instant stable QA response."""
|
| 282 |
+
return "تم تفعيل الموديل المحلي بنجاح! نظام الأسئلة والأجوبة سيتم ربطه في التحديث القادم."
|
src/schemas.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, List, Optional
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SegmentSchema(BaseModel):
|
| 6 |
+
"""Represents one chronological section of the video timeline."""
|
| 7 |
+
title: str = Field(
|
| 8 |
+
...,
|
| 9 |
+
description="A short, descriptive title for this chronological segment.",
|
| 10 |
+
)
|
| 11 |
+
summary: str = Field(
|
| 12 |
+
...,
|
| 13 |
+
description="A concise summary of what is covered in this segment (2-3 sentences).",
|
| 14 |
+
)
|
| 15 |
+
key_insight: str = Field(
|
| 16 |
+
...,
|
| 17 |
+
description="The single most important point or takeaway from this segment.",
|
| 18 |
+
)
|
| 19 |
+
why_it_matters: str = Field(
|
| 20 |
+
...,
|
| 21 |
+
description="Brief explanation of the value or importance of this segment (1-2 sentences).",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SummarySchema(BaseModel):
|
| 26 |
+
"""Primary Summary response schema."""
|
| 27 |
+
|
| 28 |
+
# Optional metadata fields (not part of the original schema contract,
|
| 29 |
+
# but used by the pipeline/markdown formatting layer).
|
| 30 |
+
title: Optional[str] = Field(
|
| 31 |
+
default=None,
|
| 32 |
+
description="The video title, carried through for formatting purposes only.",
|
| 33 |
+
)
|
| 34 |
+
detected_language: Optional[str] = Field(
|
| 35 |
+
default=None,
|
| 36 |
+
description="Language detected in the transcript (e.g. 'Arabic', 'English').",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
summary: str = Field(
|
| 40 |
+
...,
|
| 41 |
+
description=(
|
| 42 |
+
"A concise, high-level paragraph (3-5 sentences) that explains"
|
| 43 |
+
" what the entire video is about, its main thesis, and its value."
|
| 44 |
+
),
|
| 45 |
+
)
|
| 46 |
+
segments: Annotated[
|
| 47 |
+
List[SegmentSchema],
|
| 48 |
+
Field(
|
| 49 |
+
min_length=3,
|
| 50 |
+
max_length=7,
|
| 51 |
+
description=(
|
| 52 |
+
"Chronological timeline sections of the video. Minimum 3,"
|
| 53 |
+
" maximum 7. Must follow the natural progression of the transcript."
|
| 54 |
+
),
|
| 55 |
+
),
|
| 56 |
+
]
|
| 57 |
+
suggested_category: str = Field(
|
| 58 |
+
default="General",
|
| 59 |
+
description=(
|
| 60 |
+
"A single, concise category label (1-2 words max) that best"
|
| 61 |
+
" describes the video content. Must always be in English."
|
| 62 |
+
),
|
| 63 |
+
)
|
| 64 |
+
conclusion: Optional[str] = Field(
|
| 65 |
+
default=None,
|
| 66 |
+
description="A final overall takeaway or closing conclusion in the transcript language.",
|
| 67 |
+
)
|
| 68 |
+
topics: List[str] = Field(
|
| 69 |
+
default_factory=list,
|
| 70 |
+
min_length=1,
|
| 71 |
+
description=(
|
| 72 |
+
"Dynamically extracted topics discussed in the video."
|
| 73 |
+
" Examples: ['Python', 'Machine Learning', 'Neural Networks']."
|
| 74 |
+
),
|
| 75 |
+
)
|