amanyelfiky commited on
Commit
d4e9430
·
verified ·
1 Parent(s): 59717a4

Upload 3 files

Browse files
Files changed (3) hide show
  1. src/README.md +116 -0
  2. src/note_generator.py +282 -0
  3. 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
+ )