Spaces:
Running
Running
| import re | |
| from typing import Dict, List, Any | |
| from datetime import datetime | |
| class StudentClassifier: | |
| """ | |
| Lightweight, fast NLP classifier for Student News that avoids heavy ML/LLM models. | |
| Uses keyword heuristics and regex to categorize, summarize, and extract metadata. | |
| """ | |
| CATEGORIES = { | |
| "Education Policy & Govt Updates": ["education policy", "syllabus", "ugc", "cbse", "aicte", "nep 2020", "ministry of education", "school board", "educational reform", "government school", "higher education"], | |
| "Exams & Results": ["exam date", "results declared", "scorecard", "admit card", "nta", "jee main", "neet ug", "upsc prelims", "cut-off marks", "answer key", "date sheet", "counseling", "mock test", "merit list", "board exams", "entrance exam", "examination", "test result", "passing marks"], | |
| "Scholarships & Internships": ["scholarship", "internship", "fellowship", "stipend", "student grant", "student funding", "financial assistance for students", "startup grant", "startup funding", "training program", "summer internship"], | |
| "Career & Placement News": ["campus placement", "fresher hiring", "graduate recruitment", "university placement", "off-campus drive", "fresher vacancy", "job for students", "career fair", "recruitment drive", "employment news"], | |
| "Study Abroad Updates": ["student visa", "study abroad", "ielts", "toefl", "gre", "international student", "foreign university", "overseas education", "visa news", "emigration for study"], | |
| "AI & Tech for Students": ["student hackathon", "coding competition", "aicte internship", "student bootcamp", "student certification", "campus ambassador", "hackathon", "robotics", "tech fest", "coding challenge"] | |
| } | |
| PROFILES = { | |
| "School Student (10th/12th)": ["cbse", "ncert", "class 10", "class 12", "board exam", "icse", "state board", "school student", "secondary education"], | |
| "Engineering Aspirant": ["jee", "b.tech", "engineering entrance", "gate exam", "iit", "nit", "bitsat", "josaa", "engineering student", "iitian"], | |
| "Medical Aspirant": ["neet", "mbbs admission", "aiims", "bds", "pharmacy entrance", "ayush counseling", "jipmer", "pgimer", "medical student"], | |
| "Govt Job Aspirant": ["upsc notification", "ssc cgl", "bank po entrance", "rrb ntpc", "ibps po", "nda exam", "cds notification", "civil services prelims", "sbi po", "state psc", "government job", "employment news"], | |
| "Graduate/Techie": ["fresher", "placement", "startup", "coding", "software engineer", "developer", "internship", "graduation"] | |
| } | |
| AUTHORITIES = ["UGC", "CBSE", "AICTE", "NTA", "UPSC", "SSC", "Ministry of Education", "State Board", "University Grants Commission"] | |
| URGENCY_KEYWORDS = { | |
| "High": ["deadline", "tomorrow", "today", "urgent", "breaking", "last date", "closing", "alert"], | |
| "Medium": ["upcoming", "soon", "next week", "announced", "scheduled", "notification"], | |
| "Low": ["proposed", "planned", "expected", "future", "report", "study"] | |
| } | |
| STRICT_KEYWORDS = [ | |
| "university admission", "college admission", "campus placement", "scholarship", | |
| "exam result", "admit card", "student visa", "study abroad", "cut-off marks", | |
| "board exam", "jee main", "neet ug", "education policy", "fellowship", "internship", | |
| "syllabus", "ncert", "ugc", "cbse", "nta", "upsc notification", "ssc cgl", | |
| "startup grant", "startup funding", "hackathon", "coding competition", | |
| "educational", "learning", "academic", "placement drive", "hiring fresher", | |
| "recruitment drive", "campus hiring", "student achievement", "campus life", | |
| "exam update", "university news", "school news", "career guide" | |
| ] | |
| def _extract_specific_exam(self, text: str) -> str | None: | |
| # Match standard Indian exams as whole words | |
| exams = ["NEET", "JEE", "UPSC", "SSC", "CBSE", "CAT", "CLAT", "GATE", "ICSE", "CUET", "NDA", "CDS", "IBPS"] | |
| for exam in exams: | |
| if re.search(rf'\b{exam}\b', text, re.IGNORECASE): | |
| return exam | |
| return None | |
| def process_article(self, title: str, content: str) -> Dict[str, Any] | None: | |
| """ | |
| Main pipeline to process a raw article into a structured student news item. | |
| Returns None if it does not strictly cover Student News keywords. | |
| """ | |
| combined_text = f"{title} {content}".lower() | |
| # Strict context guard | |
| strict_matches = 0 | |
| for kw in self.STRICT_KEYWORDS: | |
| if re.search(rf'\b{kw}\b', combined_text, re.IGNORECASE): | |
| strict_matches += 1 | |
| specific_exam = self._extract_specific_exam(combined_text) | |
| # Must have at least one very strong keyword, or an exact exam match | |
| if strict_matches == 0 and not specific_exam: | |
| return None | |
| category = self._assign_category(combined_text) | |
| tags = self._generate_tags(combined_text, category) | |
| profiles = self._assign_profiles(combined_text) | |
| direct_links = self._extract_links(text=content) # Extract links only from content | |
| # Inject exact matching exam if found | |
| if specific_exam: | |
| tags.insert(0, f"#{specific_exam}") | |
| dates = self._extract_dates(combined_text) | |
| authority = self._extract_authority(combined_text) | |
| urgency = self._determine_urgency(combined_text) | |
| summary = self._generate_summary(content) | |
| trend_score = self._calculate_trend_score(combined_text, urgency) | |
| return { | |
| "title": title, | |
| "summary": summary, | |
| "category": category, | |
| "tags": tags, | |
| "profiles": profiles, | |
| "direct_links": direct_links, | |
| "important_dates": dates, | |
| "authority": authority, | |
| "urgency": urgency, | |
| "trend_score": trend_score | |
| } | |
| def _assign_category(self, text: str) -> str: | |
| best_match = "General Student News" | |
| max_score = 0 | |
| for cat, keywords in self.CATEGORIES.items(): | |
| score = 0 | |
| for kw in keywords: | |
| if re.search(rf'\b{kw}\b', text, re.IGNORECASE): | |
| score += 1 | |
| if score > max_score: | |
| max_score = score | |
| best_match = cat | |
| return best_match | |
| def _generate_tags(self, text: str, category: str) -> List[str]: | |
| tags = set() | |
| # Base tag on category | |
| if "Exam" in category: tags.add("#Exam") | |
| if "Scholarship" in category: tags.add("#Scholarship") | |
| if "Job" in category or "Career" in category: tags.add("#Job") | |
| if "Policy" in category: tags.add("#Policy") | |
| if "Abroad" in category: tags.add("#StudyAbroad") | |
| if "Tech" in category: tags.add("#Tech") | |
| # Specific keyword tags | |
| if "nta" in text or "jee" in text or "neet" in text: tags.add("#CompetitiveExams") | |
| if "cbse" in text or "board" in text: tags.add("#BoardExams") | |
| if "internship" in text: tags.add("#Internship") | |
| if "hackathon" in text or "coding" in text: tags.add("#Coding") | |
| return list(tags)[:4] # max 4 tags | |
| def _extract_dates(self, text: str) -> List[str]: | |
| # Very lightweight date extraction regex (e.g. 15th Jan, March 20, 2024-05-12) | |
| dates = [] | |
| # Basic pattern for "DD Month" or "Month DD" | |
| month_pattern = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*' | |
| date_pattern = rf'\b(\d{{1,2}}(?:st|nd|rd|th)?\s+{month_pattern}|{month_pattern}\s+\d{{1,2}})\b' | |
| matches = re.findall(date_pattern, text, re.IGNORECASE) | |
| # matches returns tuples because of groups in regex, clean it up | |
| for match in re.finditer(date_pattern, text, re.IGNORECASE): | |
| dates.append(match.group(0).title()) | |
| return list(set(dates))[:2] # Top 2 dates extracted | |
| def _extract_authority(self, text: str) -> str: | |
| for auth in self.AUTHORITIES: | |
| if auth.lower() in text: | |
| return auth | |
| return "General" | |
| def _determine_urgency(self, text: str) -> str: | |
| for level, keywords in self.URGENCY_KEYWORDS.items(): | |
| if any(kw in text for kw in keywords): | |
| return level | |
| return "Low" | |
| def _generate_summary(self, content: str) -> str: | |
| # Fast 3-line summary approximation by splitting sentences | |
| # Not perfect NLP, but extremely fast | |
| if not content: | |
| return "" | |
| # Clean text slightly | |
| text = re.sub(r'\s+', ' ', content).strip() | |
| # Split by periods roughly | |
| sentences = [s.strip() + "." for s in text.split('.') if len(s.strip()) > 15] | |
| if len(sentences) <= 3: | |
| return " ".join(sentences) | |
| # Take first 2 sentences and try to find one with numbers/dates for the 3rd | |
| summary_sents = sentences[:2] | |
| best_third = sentences[2] | |
| for s in sentences[2:6]: | |
| if re.search(r'\d', s): | |
| best_third = s | |
| break | |
| summary_sents.append(best_third) | |
| return " ".join(summary_sents) | |
| def _calculate_trend_score(self, text: str, urgency: str) -> int: | |
| score = 10 | |
| if urgency == "High": | |
| score += 40 | |
| elif urgency == "Medium": | |
| score += 20 | |
| # Boost score based on strong keywords | |
| hot_keywords = ["deadline", "released", "declared", "announce", "breaking", "major"] | |
| score += sum(15 for kw in hot_keywords if kw in text) | |
| return min(score, 100) | |
| def _assign_profiles(self, text: str) -> List[str]: | |
| profiles = [] | |
| for profile, keywords in self.PROFILES.items(): | |
| if any(kw in text for kw in keywords): | |
| profiles.append(profile) | |
| # default to all if none matched | |
| if not profiles: | |
| profiles.append("General Student") | |
| return profiles | |
| def _extract_links(self, text: str) -> List[str]: | |
| # Extract http/https links | |
| urls = re.findall(r'https?://[^\s<>"]+|www\.[^\s<>"]+', text) | |
| return list(set(urls))[:2] | |