Spaces:
Paused
Paused
| """ | |
| process_pipeline.py — Main MCQ generation pipeline | |
| Changes from original: | |
| 1. Lemmatizer integrated — answer and all options are root forms, not inflected | |
| 2. Stronger sentence quality filter (anaphora, blank position, min word length) | |
| 3. Candidate selection prefers longer, more specific nouns over generic ones | |
| 4. build_question blanks the surface (inflected) form in the sentence but | |
| stores the root form as the answer — so the question looks natural | |
| """ | |
| from preprocessing.tokenizer import tokenize_text | |
| from nlp.pos_tagger import get_pos_tags | |
| from nlp.ner_tagger import get_entities_from_sentence | |
| from lemmatizer import get_root | |
| from question_generation.option_generator import generate_options | |
| # --------------------------------------------------------------------------- | |
| # SENTENCE QUALITY FILTER | |
| # --------------------------------------------------------------------------- | |
| # Words that make a sentence self-referential or anaphoric — skip these | |
| _ANAPHORIC_STARTS = { | |
| "இது", "அது", "இவை", "அவை", "இதனால்", "அதனால்", | |
| "இதன்", "அதன்", "இவர்", "அவர்", "இவர்கள்", "அவர்கள்", | |
| "எனவே", "ஆகவே", "இதனை", "அதனை", | |
| } | |
| # Words so generic they make bad blanks even if tagged NOUN | |
| _GENERIC_NOUNS = { | |
| "மனித", "இது", "அது", "உடல்", "பொருள்", "விஷயம்", | |
| "காரணம்", "நிலை", "வகை", "முறை", "பகுதி", "இடம்", | |
| "நேரம்", "செயல்", "தன்மை", "வழி", | |
| } | |
| # Placeholder stopwords set; can be populated from a resource file later. | |
| STOPWORDS = set() | |
| def is_valid_sentence(sentence: str) -> bool: | |
| """ | |
| Returns True only if the sentence is suitable for MCQ generation. | |
| Rules | |
| ----- | |
| 1. At least 6 words (shorter sentences produce trivial blanks). | |
| 2. Must not start with an anaphoric/pronoun word. | |
| 3. Must not start with a conjunctive adverb (sentence is incomplete alone). | |
| 4. Must contain at least one non-generic noun or proper noun. | |
| """ | |
| words = sentence.split() | |
| # Rule 1: length | |
| if len(words) < 3: | |
| return False | |
| # Rule 2 & 3: anaphoric / dependent start | |
| if words[0] in _ANAPHORIC_STARTS: | |
| return False | |
| # Rule 4: sentence must have informational content | |
| # (quick heuristic — full POS check happens later in pipeline) | |
| tamil_words = [w for w in words if len(w) >= 3 and w not in _GENERIC_NOUNS] | |
| if len(tamil_words) < 1: | |
| return False | |
| return True | |
| # --------------------------------------------------------------------------- | |
| # CANDIDATE SELECTION | |
| # --------------------------------------------------------------------------- | |
| def select_candidate(pos_tags, entities, sentence): | |
| from question_generation.cluster_store import CLUSTERS | |
| words_in_sentence = set(sentence.split()) | |
| def valid(word): | |
| return ( | |
| len(word) >= 3 | |
| and word not in STOPWORDS | |
| and word in words_in_sentence | |
| ) | |
| def score(word, tag): | |
| s = 0 | |
| # POS priority | |
| if tag == "PROPN": s += 5 | |
| elif tag == "NOUN": s += 3 | |
| # NER bonus | |
| if word in entities: s += 10 | |
| # Cluster bonus — can we find distractors? | |
| for cluster in CLUSTERS.values(): | |
| if word in cluster: | |
| s += 2 | |
| break | |
| return s | |
| # Build scored candidates from all nouns/propns | |
| candidates = [ | |
| (word, tag, score(word, tag)) | |
| for word, tag in pos_tags | |
| if tag in ("NOUN", "PROPN") and valid(word) | |
| ] | |
| # Also add NER entities not caught by POS | |
| for e in entities: | |
| if valid(e) and not any(w == e for w, _, _ in candidates): | |
| candidates.append((e, "PROPN", score(e, "PROPN"))) | |
| if not candidates: | |
| return None | |
| # Pick highest scoring | |
| best = max(candidates, key=lambda x: x[2]) | |
| surface = best[0] | |
| root = get_root(surface) | |
| return surface, root | |
| # --------------------------------------------------------------------------- | |
| # QUESTION BUILDER | |
| # --------------------------------------------------------------------------- | |
| def build_question(sentence: str, surface_word: str) -> str | None: | |
| """ | |
| Replace the first occurrence of surface_word in sentence with _____. | |
| We blank the *surface* (inflected) form so the sentence stays | |
| grammatically natural. The answer stored is the root form. | |
| """ | |
| if not surface_word or surface_word not in sentence: | |
| return None | |
| # Guard: blank should not be the first or last token (bad question shape) | |
| words = sentence.split() | |
| if words[0] == surface_word or words[-1] == surface_word: | |
| return None | |
| return sentence.replace(surface_word, "_____", 1) | |
| # --------------------------------------------------------------------------- | |
| # MAIN PIPELINE | |
| # --------------------------------------------------------------------------- | |
| def generate_mcq_pipeline(sentence: str) -> dict | None: | |
| """ | |
| Full pipeline: sentence → MCQ dict or None. | |
| Returns | |
| ------- | |
| { | |
| "question": "... _____ ...", | |
| "options": ["A", "B", "C", "D"], # shuffled, root forms | |
| "answer": "root_form_of_answer" | |
| } | |
| or None if the sentence is unsuitable. | |
| """ | |
| sentence = sentence.replace("\n", " ").strip() | |
| # Step 1: quality gate | |
| if not is_valid_sentence(sentence): | |
| return None | |
| # Step 2: NLP | |
| tokens = tokenize_text(sentence) | |
| pos_tags = get_pos_tags(sentence) | |
| entities = get_entities_from_sentence(sentence) | |
| # Step 3: pick candidate (surface + root) | |
| candidate = select_candidate(pos_tags, entities, sentence) | |
| if not candidate: | |
| return None | |
| surface, root = candidate | |
| # Step 4: build question (blank uses surface form for natural Tamil) | |
| question = build_question(sentence, surface) | |
| if not question: | |
| return None | |
| # Step 5: generate options — all in root form | |
| options = generate_options( | |
| answer_word=root, | |
| sentence_words=tokens, | |
| pos_tags=pos_tags, | |
| question=question, | |
| context=sentence, | |
| ) | |
| return { | |
| "question": question, | |
| "options": options, | |
| "answer": root, | |
| } | |