Ali Hashhash commited on
Commit
4db2bb6
·
1 Parent(s): c29c4d4

feat: implement summarization engine with Pydantic schemas and map-reduce processing pipeline

Browse files
src/categorization/topic_classifier.py CHANGED
@@ -9,9 +9,9 @@ Usage:
9
  # => "Technology & AI"
10
 
11
  Categories:
12
- Technology & AI | Business & Finance | Education & Science
13
- Productivity & Self-Growth | News & Politics
14
- Entertainment & Lifestyle | Health & Sports
15
  """
16
 
17
  from typing import List, Set
@@ -28,11 +28,15 @@ logger = setup_logger(__name__)
28
  CATEGORIES = [
29
  "Technology & AI",
30
  "Business & Finance",
31
- "Education & Science",
 
32
  "Productivity & Self-Growth",
33
- "News & Politics",
34
- "Entertainment & Lifestyle",
35
- "Health & Sports",
 
 
 
36
  ]
37
 
38
 
@@ -98,12 +102,22 @@ _register("Business & Finance", [
98
  "ربح", "دخل", "ميزانية",
99
  ])
100
 
101
- # ── Education & Science ──
102
- _register("Education & Science", [
103
  # English
104
  "education", "learning", "teaching", "school", "university", "college",
105
- "academic", "research", "study", "studying", "exam", "exams", "course",
106
  "tutorial", "lecture", "scholarship", "degree", "phd", "thesis",
 
 
 
 
 
 
 
 
 
 
107
  "science", "physics", "chemistry", "biology", "math", "mathematics",
108
  "statistics", "calculus", "algebra", "geometry", "astronomy", "space",
109
  "nasa", "quantum", "quantum physics", "quantum computing",
@@ -111,12 +125,10 @@ _register("Education & Science", [
111
  "climate", "climate change", "environment", "engineering",
112
  "mechanical engineering", "electrical engineering", "civil engineering",
113
  "experiment", "laboratory", "lab", "hypothesis", "theory",
114
- "history", "philosophy", "psychology", "sociology", "linguistics",
115
- "anthropology", "archaeology", "literature", "language", "grammar",
116
  # Arabic
117
- "تعليم", "تعلم", "مدرسة", "جامعة", "علوم", "فيزياء", "كيمياء",
118
- "أحياء", "رياضيات", "بحث", "دراسة", "امتحان", نهج", "محاضرة",
119
- "هندسة", "تاريخ", "فلسفة", "علم نفس", "فلك", "بيئة",
120
  ])
121
 
122
  # ── Productivity & Self-Growth ──
@@ -128,74 +140,92 @@ _register("Productivity & Self-Growth", [
128
  "mindset", "focus", "concentration", "efficiency", "organization",
129
  "planning", "journaling", "morning routine", "routine", "success",
130
  "self help", "self-help", "life coaching", "coaching", "mentoring",
131
- "mentor", "stoicism", "minimalism", "mindfulness", "meditation",
132
  "emotional intelligence", "communication skills", "public speaking",
133
  "negotiation", "critical thinking", "problem solving", "creativity",
134
  "decision making", "confidence", "resilience", "work-life balance",
135
  "burnout", "career", "career development", "skill building",
136
  # Arabic
137
  "إنتاجية", "تطوير ذات", "تحفيز", "عادات", "إدارة الوقت",
138
- "أهداف", "تركيز", "نجاح", "تخطيط", "تأمل", "ثقة بالنفس",
139
  "مهارات", "تفكير", "إبداع",
140
  ])
141
 
142
- # ── News & Politics ──
143
- _register("News & Politics", [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  # English
145
- "news", "politics", "political", "government", "policy", "election",
146
- "elections", "democracy", "geopolitics", "diplomacy", "war", "conflict",
147
- "military", "defense", "law", "legal", "legislation", "regulation",
148
- "human rights", "immigration", "refugee", "sanctions", "united nations",
149
- "nato", "eu", "european union", "congress", "parliament", "senate",
150
- "president", "prime minister", "foreign policy", "domestic policy",
151
- "protest", "activism", "corruption", "media", "journalism",
152
- "press", "freedom of speech", "censorship", "propaganda",
153
- "international relations", "treaty", "nuclear",
154
  # Arabic
155
- "أخبار", "سياسة", "حكومة", "انتخابات", "ديمقراطية", "حرب",
156
- "قانون", "حقوق إنسان", "دبلوماسية", "برلمان", "رئيس",
157
- "إعلام", "صحافة",
158
  ])
159
 
160
- # ── Entertainment & Lifestyle ──
161
- _register("Entertainment & Lifestyle", [
162
  # English
163
  "entertainment", "movie", "movies", "film", "films", "cinema",
164
  "tv", "television", "series", "netflix", "streaming", "anime",
165
  "manga", "gaming", "video games", "esports", "twitch", "youtube",
166
- "podcast", "music", "song", "album", "concert", "artist",
167
- "celebrity", "fashion", "style", "beauty", "makeup", "skincare",
168
- "travel", "tourism", "food", "cooking", "recipe", "restaurant",
169
- "cuisine", "vlog", "vlogging", "photography", "art", "design",
170
- "graphic design", "illustration", "architecture", "interior design",
171
- "diy", "crafts", "comedy", "humor", "drama", "reality tv",
172
  "social media", "tiktok", "instagram", "influencer", "content creator",
173
- "lifestyle", "luxury", "culture", "pop culture",
174
  # Arabic
175
  "ترفيه", "أفلام", "سينما", "مسلسلات", "ألعاب", "موسيقى",
176
- "سفر", "طبخ", "أزياء", "جمال", "تصوير", "فن", "تصميم",
177
- "ثقافة", "كوميديا", "يوتيوب",
178
  ])
179
 
180
- # ── Health & Sports ──
181
- _register("Health & Sports", [
182
  # English
183
- "health", "fitness", "exercise", "workout", "gym", "bodybuilding",
184
- "weight loss", "diet", "nutrition", "calories", "protein", "vitamins",
185
- "supplements", "wellness", "mental health", "therapy", "depression",
186
- "anxiety", "stress", "sleep", "yoga", "pilates", "crossfit",
187
- "running", "marathon", "swimming", "cycling", "hiking",
188
- "sports", "football", "soccer", "basketball", "tennis", "baseball",
189
- "cricket", "rugby", "boxing", "mma", "ufc", "wrestling",
190
- "olympics", "world cup", "premier league", "nba", "nfl",
191
- "medicine", "medical", "doctor", "hospital", "surgery", "disease",
192
- "virus", "vaccine", "pandemic", "covid", "cancer", "diabetes",
193
- "heart", "cardio", "physical therapy", "rehabilitation",
194
- "first aid", "pharmacy", "drug", "prescription",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  # Arabic
196
- "صحة", "رياضة", "تمارين", "لياقة", "تغذية", "حمية",
197
- "صحة نفسية", "علاج", "طب", "مستشفى", "كرة قدم", "سباحة",
198
- "يوغا", "نوم", "فيتامينات",
199
  ])
200
 
201
 
@@ -239,14 +269,14 @@ def classify_topic(topics: List[str]) -> str:
239
  break
240
 
241
  if not matched:
242
- return "Education & Science"
243
 
244
  # Return the first match in CATEGORIES order for consistency
245
  for cat in CATEGORIES:
246
  if cat in matched:
247
  return cat
248
 
249
- return "Education & Science"
250
 
251
 
252
  def classify_topics(topics: List[str]) -> List[str]:
@@ -269,7 +299,7 @@ def classify_topic_groq(title: str, summary: str) -> str:
269
  Bypasses the local Zero-Shot classification model entirely.
270
  """
271
  if not title and not summary:
272
- return "Education & Science"
273
 
274
  try:
275
  from src.utils.model_loader import get_groq_client
@@ -319,11 +349,11 @@ def classify_topic_groq(title: str, summary: str) -> str:
319
  return cat
320
 
321
  logger.warning("⚠️ Groq returned invalid category: %s — falling back", reply)
322
- return "Education & Science"
323
 
324
  except Exception as e:
325
  logger.error("❌ Groq category classification failed: %s", e, exc_info=True)
326
- return "Education & Science"
327
 
328
 
329
  # ─────────────────────────────────────────────────────────────────────────────
@@ -341,7 +371,7 @@ def classify_topic_zeroshot(text: str) -> str:
341
  The best-matching category string from CATEGORIES.
342
  """
343
  if not text or not text.strip():
344
- return "Education & Science"
345
 
346
  try:
347
  from src.utils.model_loader import get_classifier_pipeline
@@ -364,14 +394,14 @@ def classify_topic_zeroshot(text: str) -> str:
364
 
365
  except Exception as e:
366
  logger.warning("⚠️ Zero-shot classification failed: %s — falling back", e)
367
- return "Education & Science"
368
 
369
 
370
  def classify_topic_hybrid(topics: List[str], text: str = "") -> str:
371
  """Best-of-both-worlds classifier.
372
 
373
  1. First tries fast keyword matching via ``classify_topic(topics)``.
374
- 2. If the result is the generic fallback ("Education & Science") AND
375
  ``text`` is provided, runs the mDeBERTa zero-shot classifier on
376
  the text for a more nuanced result.
377
 
@@ -385,7 +415,7 @@ def classify_topic_hybrid(topics: List[str], text: str = "") -> str:
385
  keyword_result = classify_topic(topics)
386
 
387
  # If keyword matching gave a confident answer, use it
388
- if keyword_result != "Education & Science":
389
  return keyword_result
390
 
391
  # If we have text, try zero-shot as a fallback
 
9
  # => "Technology & AI"
10
 
11
  Categories:
12
+ Technology & AI | Business & Finance | Education | Science
13
+ Productivity & Self-Growth | Health & Wellness | Sports & Fitness
14
+ Entertainment | History | Philosophy | Arts & Culture
15
  """
16
 
17
  from typing import List, Set
 
28
  CATEGORIES = [
29
  "Technology & AI",
30
  "Business & Finance",
31
+ "Education",
32
+ "Science",
33
  "Productivity & Self-Growth",
34
+ "Health & Wellness",
35
+ "Sports & Fitness",
36
+ "Entertainment",
37
+ "History",
38
+ "Philosophy",
39
+ "Arts & Culture",
40
  ]
41
 
42
 
 
102
  "ربح", "دخل", "ميزانية",
103
  ])
104
 
105
+ # ── Education ──
106
+ _register("Education", [
107
  # English
108
  "education", "learning", "teaching", "school", "university", "college",
109
+ "academic", "study", "studying", "exam", "exams", "course",
110
  "tutorial", "lecture", "scholarship", "degree", "phd", "thesis",
111
+ "curriculum", "pedagogy", "classroom", "student", "teacher",
112
+ "grammar", "language", "linguistics",
113
+ # Arabic
114
+ "تعليم", "تعلم", "مدرسة", "جامعة", "دراسة", "امتحان", "منهج",
115
+ "محاضرة", "طالب", "معلم",
116
+ ])
117
+
118
+ # ── Science ──
119
+ _register("Science", [
120
+ # English
121
  "science", "physics", "chemistry", "biology", "math", "mathematics",
122
  "statistics", "calculus", "algebra", "geometry", "astronomy", "space",
123
  "nasa", "quantum", "quantum physics", "quantum computing",
 
125
  "climate", "climate change", "environment", "engineering",
126
  "mechanical engineering", "electrical engineering", "civil engineering",
127
  "experiment", "laboratory", "lab", "hypothesis", "theory",
128
+ "research", "psychology", "sociology", "anthropology",
 
129
  # Arabic
130
+ "علوم", "فيزياء", "كيمياء", "أحياء", "رياضيات", "بحث",
131
+ "هندسة", "فلك", "بيئة", "علم نفس",
 
132
  ])
133
 
134
  # ── Productivity & Self-Growth ──
 
140
  "mindset", "focus", "concentration", "efficiency", "organization",
141
  "planning", "journaling", "morning routine", "routine", "success",
142
  "self help", "self-help", "life coaching", "coaching", "mentoring",
143
+ "mentor", "minimalism", "mindfulness",
144
  "emotional intelligence", "communication skills", "public speaking",
145
  "negotiation", "critical thinking", "problem solving", "creativity",
146
  "decision making", "confidence", "resilience", "work-life balance",
147
  "burnout", "career", "career development", "skill building",
148
  # Arabic
149
  "إنتاجية", "تطوير ذات", "تحفيز", "عادات", "إدارة الوقت",
150
+ "أهداف", "تركيز", "نجاح", "تخطيط", "ثقة بالنفس",
151
  "مهارات", "تفكير", "إبداع",
152
  ])
153
 
154
+ # ── Health & Wellness ──
155
+ _register("Health & Wellness", [
156
+ # English
157
+ "health", "wellness", "mental health", "therapy", "depression",
158
+ "anxiety", "stress", "sleep", "yoga", "pilates", "meditation",
159
+ "diet", "nutrition", "calories", "protein", "vitamins",
160
+ "supplements", "weight loss", "fitness",
161
+ "medicine", "medical", "doctor", "hospital", "surgery", "disease",
162
+ "virus", "vaccine", "pandemic", "covid", "cancer", "diabetes",
163
+ "heart", "cardio", "physical therapy", "rehabilitation",
164
+ "first aid", "pharmacy", "drug", "prescription",
165
+ # Arabic
166
+ "صحة", "تغذية", "حمية", "صحة نفسية", "علاج", "طب",
167
+ "مستشفى", "نوم", "فيتامينات", "يوغا",
168
+ ])
169
+
170
+ # ── Sports & Fitness ──
171
+ _register("Sports & Fitness", [
172
  # English
173
+ "sports", "football", "soccer", "basketball", "tennis", "baseball",
174
+ "cricket", "rugby", "boxing", "mma", "ufc", "wrestling",
175
+ "olympics", "world cup", "premier league", "nba", "nfl",
176
+ "exercise", "workout", "gym", "bodybuilding", "crossfit",
177
+ "running", "marathon", "swimming", "cycling", "hiking",
 
 
 
 
178
  # Arabic
179
+ "رياضة", "تمارين", "لياقة", "كرة قدم", "سباحة",
 
 
180
  ])
181
 
182
+ # ── Entertainment ──
183
+ _register("Entertainment", [
184
  # English
185
  "entertainment", "movie", "movies", "film", "films", "cinema",
186
  "tv", "television", "series", "netflix", "streaming", "anime",
187
  "manga", "gaming", "video games", "esports", "twitch", "youtube",
188
+ "podcast", "music", "song", "album", "concert",
189
+ "celebrity", "comedy", "humor", "drama", "reality tv",
 
 
 
 
190
  "social media", "tiktok", "instagram", "influencer", "content creator",
191
+ "vlog", "vlogging", "pop culture",
192
  # Arabic
193
  "ترفيه", "أفلام", "سينما", "مسلسلات", "ألعاب", "موسيقى",
194
+ "كوميديا", "يوتيوب",
 
195
  ])
196
 
197
+ # ── History ──
198
+ _register("History", [
199
  # English
200
+ "history", "ancient", "medieval", "civilization", "empire",
201
+ "world war", "revolution", "archaeology", "historical",
202
+ "dynasty", "colonialism", "independence", "heritage",
203
+ # Arabic
204
+ "تاريخ", "حضارة", "إمبراطورية", "ثورة", "آثار", "تراث",
205
+ ])
206
+
207
+ # ── Philosophy ──
208
+ _register("Philosophy", [
209
+ # English
210
+ "philosophy", "ethics", "morality", "existentialism", "stoicism",
211
+ "metaphysics", "epistemology", "logic", "consciousness",
212
+ "free will", "determinism", "nihilism", "virtue",
213
+ # Arabic
214
+ "فلسفة", "أخلاق", "وجودية", "منطق", "وعي",
215
+ ])
216
+
217
+ # ── Arts & Culture ──
218
+ _register("Arts & Culture", [
219
+ # English
220
+ "art", "artist", "painting", "sculpture", "gallery", "museum",
221
+ "photography", "design", "graphic design", "illustration",
222
+ "architecture", "interior design", "fashion", "style", "beauty",
223
+ "makeup", "skincare", "travel", "tourism", "food", "cooking",
224
+ "recipe", "restaurant", "cuisine", "diy", "crafts",
225
+ "culture", "literature", "lifestyle", "luxury",
226
  # Arabic
227
+ "فن", "تصميم", "تصوير", "سفر", "طبخ", "أزياء",
228
+ "جمال", "ثقافة",
 
229
  ])
230
 
231
 
 
269
  break
270
 
271
  if not matched:
272
+ return "Education"
273
 
274
  # Return the first match in CATEGORIES order for consistency
275
  for cat in CATEGORIES:
276
  if cat in matched:
277
  return cat
278
 
279
+ return "Education"
280
 
281
 
282
  def classify_topics(topics: List[str]) -> List[str]:
 
299
  Bypasses the local Zero-Shot classification model entirely.
300
  """
301
  if not title and not summary:
302
+ return "Education"
303
 
304
  try:
305
  from src.utils.model_loader import get_groq_client
 
349
  return cat
350
 
351
  logger.warning("⚠️ Groq returned invalid category: %s — falling back", reply)
352
+ return "Education"
353
 
354
  except Exception as e:
355
  logger.error("❌ Groq category classification failed: %s", e, exc_info=True)
356
+ return "Education"
357
 
358
 
359
  # ─────────────────────────────────────────────────────────────────────────────
 
371
  The best-matching category string from CATEGORIES.
372
  """
373
  if not text or not text.strip():
374
+ return "Education"
375
 
376
  try:
377
  from src.utils.model_loader import get_classifier_pipeline
 
394
 
395
  except Exception as e:
396
  logger.warning("⚠️ Zero-shot classification failed: %s — falling back", e)
397
+ return "Education"
398
 
399
 
400
  def classify_topic_hybrid(topics: List[str], text: str = "") -> str:
401
  """Best-of-both-worlds classifier.
402
 
403
  1. First tries fast keyword matching via ``classify_topic(topics)``.
404
+ 2. If the result is the generic fallback ("Education") AND
405
  ``text`` is provided, runs the mDeBERTa zero-shot classifier on
406
  the text for a more nuanced result.
407
 
 
415
  keyword_result = classify_topic(topics)
416
 
417
  # If keyword matching gave a confident answer, use it
418
+ if keyword_result != "Education":
419
  return keyword_result
420
 
421
  # If we have text, try zero-shot as a fallback
src/summarization/note_generator.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import re
3
  from typing import Dict, List
@@ -34,51 +35,6 @@ _LANG_MATCH_INSTRUCTION = (
34
  "Identify the primary language of the input text. You MUST generate your entire response strictly in that exact same language. DO NOT mix languages under any circumstances."
35
  )
36
 
37
- def parse_key_points_from_markdown(markdown_text: str) -> List[str]:
38
- """Extract bullet points under the Key Points header in Markdown."""
39
- if not markdown_text:
40
- return []
41
-
42
- lines = markdown_text.splitlines()
43
- key_points = []
44
- in_key_points_section = False
45
-
46
- for line in lines:
47
- line_strip = line.strip()
48
- if not line_strip:
49
- continue
50
- # Check for headers like ## 💡 Key Points or ## 💡 أهم النقاط or ## Key Points
51
- if line_strip.startswith("##") and ("Key Points" in line_strip or "النقاط" in line_strip):
52
- in_key_points_section = True
53
- continue
54
- elif line_strip.startswith("##") and in_key_points_section:
55
- # Reached a new section header, stop parsing
56
- break
57
-
58
- if in_key_points_section:
59
- # Match list items starting with -, *, or numbering
60
- if line_strip.startswith("-") or line_strip.startswith("*"):
61
- cleaned_point = line_strip.lstrip("-* ").strip()
62
- if cleaned_point:
63
- key_points.append(cleaned_point)
64
- elif line_strip[0].isdigit() and (line_strip[1] == "." or (len(line_strip) > 2 and line_strip[2] == ".")):
65
- parts = line_strip.split(".", 1)
66
- if len(parts) > 1:
67
- cleaned_point = parts[1].strip()
68
- if cleaned_point:
69
- key_points.append(cleaned_point)
70
-
71
- # Fallback if parsing failed or section wasn't found
72
- if not key_points:
73
- for line in lines:
74
- line_strip = line.strip()
75
- if line_strip.startswith("- ") or line_strip.startswith("* "):
76
- cleaned = line_strip[2:].strip()
77
- if cleaned and not any(h in cleaned for h in ["General Summary", "الملخص العام", "Key Questions", "الأسئلة", "Key Points", "النقاط"]):
78
- key_points.append(cleaned)
79
-
80
- return key_points[:5]
81
-
82
 
83
 
84
 
@@ -248,51 +204,153 @@ class NoteGenerator:
248
  )
249
  return segments_list
250
 
251
- def _reduce_summary(self, segments_list: List[Dict], video_title: str) -> str:
252
- """REDUCE step: summarize the combined chunk-summaries into one overall summary."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  combined_text = " ".join(seg["summary"] for seg in segments_list)
254
  clean_combined = WHITESPACE_RE.sub(" ", combined_text.strip())[:3000]
255
 
256
  if not clean_combined:
257
- return f"Summary of: {video_title}."
 
 
 
 
 
 
 
258
 
259
  if INFERENCE_MODE == "groq":
260
  messages = [
261
  {
262
  "role": "system",
263
  "content": (
264
- "You are a professional summarizer compiling a final overview of a video.\n"
265
- f"{_LANG_MATCH_INSTRUCTION}\n\n"
266
- "Below are partial summaries from different sections of the video. "
267
- "Combine and structure them into a highly cohesive study note. "
268
- "You MUST structure your response EXACTLY as follows:\n\n"
269
- "## 📋 [General Summary / الملخص العام]\n"
270
- "(Write a brief 2-3 sentence overview of the entire video here.)\n\n"
271
- "## ❓ [Key Questions & Answers / أبرز الأسئلة والأجوبة]\n"
272
- "(Extract exactly the 5 most important insights and format them as Q&A. Use exactly: Q: [Question]? \\n A: [Answer] for each turn. Do not use bold/italic formatting on 'Q:' or 'A:' to keep it clean.)\n\n"
273
- "## 💡 [Key Points / أهم النقاط]\n"
274
- "(Write 3 to 5 actual, factual bullet points extracted directly from the text. DO NOT use generic placeholder text like 'الاستنتاج الجوهري' or generic statements. Ensure they are concrete details.)\n\n"
275
- "CRITICAL Rules:\n"
276
- "1. Translate all section headers to match the detected primary language of the video transcript automatically. "
277
- "For example, if the video transcript is in Arabic, all section headers must be in Arabic (e.g. '## 📋 الملخص العام', '## ❓ أبرز الأسئلة والأجوبة', '## 💡 أهم النقاط'). "
278
- "If the video transcript is in English, all section headers must be in English (e.g. '## 📋 General Summary', '## ❓ Key Questions & Answers', '## 💡 Key Points').\n"
279
- "2. DO NOT use placeholder text under any circumstances.\n"
280
- "3. Do not repeat headers unnecessarily. Ensure the output is highly cohesive.\n"
281
- "4. Output ONLY the structured Markdown notes, nothing else."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  ),
283
  },
284
  {"role": "user", "content": clean_combined},
285
  ]
286
- logger.info("🟢 Reducing summaries via Groq API (llama-3.3-70b-versatile) to Q&A Markdown...")
287
  groq_client = get_groq_client()
288
  chat_completion = groq_client.chat.completions.create(
289
  model="llama-3.3-70b-versatile",
290
  messages=messages,
291
- max_tokens=1000,
292
  temperature=0.0,
 
293
  )
294
- overall = chat_completion.choices[0].message.content or ""
 
 
295
  else:
 
296
  messages = [
297
  {
298
  "role": "system",
@@ -309,10 +367,15 @@ class NoteGenerator:
309
  logger.info("🤖 Reducing summaries via local Qwen pipeline...")
310
  overall = _generate_text_local(messages, max_new_tokens=250)
311
 
312
- if not overall or len(overall.strip()) < 5:
313
- overall = f"استعراض شامل ومناقشة تفصيلية لموضوع: {video_title}."
314
 
315
- return overall
 
 
 
 
 
316
 
317
  def generateSummary(self, transcript_text: str, video_title: str) -> Dict:
318
  """Generates a structured AI summary, validated against SummarySchema."""
 
1
+ import json
2
  import os
3
  import re
4
  from typing import Dict, List
 
35
  "Identify the primary language of the input text. You MUST generate your entire response strictly in that exact same language. DO NOT mix languages under any circumstances."
36
  )
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
 
 
204
  )
205
  return segments_list
206
 
207
+ # ── Valid categories for the reduce prompt ────────────────────────────
208
+ VALID_CATEGORIES = [
209
+ "Technology & AI",
210
+ "Business & Finance",
211
+ "Education",
212
+ "Science",
213
+ "Productivity & Self-Growth",
214
+ "Health & Wellness",
215
+ "Sports & Fitness",
216
+ "Entertainment",
217
+ "History",
218
+ "Philosophy",
219
+ "Arts & Culture",
220
+ ]
221
+
222
+ def _parse_reduce_json(self, raw_text: str, video_title: str) -> Dict:
223
+ """Safely parse the strict JSON returned by the reduce LLM call.
224
+
225
+ Returns a dict with keys: markdown_summary, key_points, category, language.
226
+ Falls back gracefully if parsing fails.
227
+ """
228
+ # Try to extract JSON from the response (handle markdown code fences)
229
+ text = raw_text.strip()
230
+ if text.startswith("```"):
231
+ # Remove ```json ... ``` wrappers
232
+ text = re.sub(r"^```(?:json)?\s*", "", text)
233
+ text = re.sub(r"\s*```$", "", text)
234
+
235
+ try:
236
+ data = json.loads(text)
237
+ except json.JSONDecodeError:
238
+ logger.warning("⚠️ Failed to parse reduce JSON. Falling back to raw text.")
239
+ return {
240
+ "markdown_summary": raw_text,
241
+ "key_points": [],
242
+ "category": "Education",
243
+ "language": "ar",
244
+ }
245
+
246
+ # Validate and sanitize each field
247
+ markdown_summary = data.get("markdown_summary", "").strip()
248
+ if not markdown_summary:
249
+ markdown_summary = raw_text
250
+
251
+ key_points = data.get("key_points", [])
252
+ if not isinstance(key_points, list):
253
+ key_points = []
254
+ key_points = [str(p).strip() for p in key_points if str(p).strip()][:5]
255
+
256
+ category = data.get("category", "").strip()
257
+ if category not in self.VALID_CATEGORIES:
258
+ # Attempt fuzzy match
259
+ category_lower = category.lower()
260
+ matched = False
261
+ for valid_cat in self.VALID_CATEGORIES:
262
+ if valid_cat.lower() in category_lower or category_lower in valid_cat.lower():
263
+ category = valid_cat
264
+ matched = True
265
+ break
266
+ if not matched:
267
+ category = "Education"
268
+
269
+ language = data.get("language", "ar").strip().lower()
270
+ if language not in ("ar", "en"):
271
+ language = "ar"
272
+
273
+ return {
274
+ "markdown_summary": markdown_summary,
275
+ "key_points": key_points,
276
+ "category": category,
277
+ "language": language,
278
+ }
279
+
280
+ def _reduce_summary(self, segments_list: List[Dict], video_title: str) -> Dict:
281
+ """REDUCE step: combine chunk-summaries into a strict JSON with
282
+ markdown_summary, key_points, category, and language.
283
+
284
+ Returns a dict (parsed JSON), NOT a raw string.
285
+ """
286
  combined_text = " ".join(seg["summary"] for seg in segments_list)
287
  clean_combined = WHITESPACE_RE.sub(" ", combined_text.strip())[:3000]
288
 
289
  if not clean_combined:
290
+ return {
291
+ "markdown_summary": f"Summary of: {video_title}.",
292
+ "key_points": [],
293
+ "category": "Education",
294
+ "language": "ar",
295
+ }
296
+
297
+ categories_str = ", ".join(f'"{c}"' for c in self.VALID_CATEGORIES)
298
 
299
  if INFERENCE_MODE == "groq":
300
  messages = [
301
  {
302
  "role": "system",
303
  "content": (
304
+ "You are a professional summarizer compiling a final overview of a video.\n\n"
305
+ "STRICT RULES — follow every one exactly:\n\n"
306
+ "1. LANGUAGE: Detect the primary language of the text below. "
307
+ "If Arabic, write EVERYTHING (headers, body, questions, answers, key points) in Arabic. "
308
+ "If English, write EVERYTHING in English. "
309
+ "NEVER mix languages.\n\n"
310
+ "2. OUTPUT FORMAT: You MUST return a single valid JSON object with this exact schema — no extra text, no markdown fences, ONLY the JSON:\n"
311
+ '{\n'
312
+ ' "language": "ar" or "en",\n'
313
+ ' "category": "one of the categories listed below",\n'
314
+ ' "markdown_summary": "the formatted markdown string",\n'
315
+ ' "key_points": ["point 1", "point 2", "point 3", "point 4", "point 5"]\n'
316
+ '}\n\n'
317
+ '3. MARKDOWN_SUMMARY FORMATTING this is critical for readability:\n'
318
+ ' - Start with: ## 📋 الملخص العام (or ## 📋 General Summary for English)\n'
319
+ ' - Then TWO newlines (\\n\\n)\n'
320
+ ' - Write a clear, concise 1-paragraph overview of the entire video.\n'
321
+ ' - Then: \\n\\n---\\n\\n\n'
322
+ ' - Then: ## ❓ أبرز الأسئلة والأجوبة (or ## ❓ Key Questions & Answers for English)\n'
323
+ ' - Then TWO newlines (\\n\\n)\n'
324
+ ' - Write exactly 5 Q&A pairs. Format each pair as:\n'
325
+ ' **س: [Question]?**\\nج: [Answer]\\n\\n (for Arabic)\n'
326
+ ' **Q: [Question]?**\\nA: [Answer]\\n\\n (for English)\n'
327
+ ' - IMPORTANT: Put \\n\\n (double newline) between EVERY Q&A pair for spacious layout.\n'
328
+ ' - Use relevant modern emojis sparingly in questions to make it engaging.\n'
329
+ ' - DO NOT include key points, bullet lists, or any other sections in markdown_summary.\n\n'
330
+ '4. KEY_POINTS: Exactly 5 concise, factual strings in a JSON array. "
331
+ "These must be concrete insights from the content, NOT generic text. "
332
+ "Do NOT repeat these inside markdown_summary.\n\n"
333
+ f"5. CATEGORY: Must be exactly one of: [{categories_str}]. "
334
+ "Choose the most accurate one based on the actual content.\n\n"
335
+ "6. Do NOT wrap the output in markdown code fences. Return raw JSON only."
336
  ),
337
  },
338
  {"role": "user", "content": clean_combined},
339
  ]
340
+ logger.info("🟢 Reducing summaries via Groq API (strict JSON mode)...")
341
  groq_client = get_groq_client()
342
  chat_completion = groq_client.chat.completions.create(
343
  model="llama-3.3-70b-versatile",
344
  messages=messages,
345
+ max_tokens=1500,
346
  temperature=0.0,
347
+ response_format={"type": "json_object"},
348
  )
349
+ raw_response = chat_completion.choices[0].message.content or ""
350
+ logger.info(f"🔎 Reduce raw response (len={len(raw_response)}): {raw_response[:300]!r}")
351
+ return self._parse_reduce_json(raw_response, video_title)
352
  else:
353
+ # Local Qwen mode — keep simple text-based reduce (no JSON)
354
  messages = [
355
  {
356
  "role": "system",
 
367
  logger.info("🤖 Reducing summaries via local Qwen pipeline...")
368
  overall = _generate_text_local(messages, max_new_tokens=250)
369
 
370
+ if not overall or len(overall.strip()) < 5:
371
+ overall = f"استعراض شامل ومناقشة تفصيلية لموضوع: {video_title}."
372
 
373
+ return {
374
+ "markdown_summary": overall,
375
+ "key_points": [],
376
+ "category": "Education",
377
+ "language": "ar",
378
+ }
379
 
380
  def generateSummary(self, transcript_text: str, video_title: str) -> Dict:
381
  """Generates a structured AI summary, validated against SummarySchema."""
src/summarization/schemas.py CHANGED
@@ -73,3 +73,10 @@ class SummarySchema(BaseModel):
73
  " Examples: ['Python', 'Machine Learning', 'Neural Networks']."
74
  ),
75
  )
 
 
 
 
 
 
 
 
73
  " Examples: ['Python', 'Machine Learning', 'Neural Networks']."
74
  ),
75
  )
76
+ key_points: List[str] = Field(
77
+ default_factory=list,
78
+ description=(
79
+ "Exactly 5 concise key points extracted from the video content."
80
+ " Used by the UI to render a separate key-points list."
81
+ ),
82
+ )