| """ |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| |
| _ANAPHORIC_STARTS = { |
| "இது", "அது", "இவை", "அவை", "இதனால்", "அதனால்", |
| "இதன்", "அதன்", "இவர்", "அவர்", "இவர்கள்", "அவர்கள்", |
| "எனவே", "ஆகவே", "இதனை", "அதனை", |
| } |
|
|
| |
| _GENERIC_NOUNS = { |
| "மனித", "இது", "அது", "உடல்", "பொருள்", "விஷயம்", |
| "காரணம்", "நிலை", "வகை", "முறை", "பகுதி", "இடம்", |
| "நேரம்", "செயல்", "தன்மை", "வழி", |
| } |
|
|
| |
| 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() |
|
|
| |
| if len(words) < 3: |
| return False |
|
|
| |
| if words[0] in _ANAPHORIC_STARTS: |
| return False |
|
|
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| |
| if tag == "PROPN": s += 5 |
| elif tag == "NOUN": s += 3 |
| |
| if word in entities: s += 10 |
| |
| for cluster in CLUSTERS.values(): |
| if word in cluster: |
| s += 2 |
| break |
| return s |
|
|
| |
| candidates = [ |
| (word, tag, score(word, tag)) |
| for word, tag in pos_tags |
| if tag in ("NOUN", "PROPN") and valid(word) |
| ] |
|
|
| |
| 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 |
|
|
| |
| best = max(candidates, key=lambda x: x[2]) |
| surface = best[0] |
| root = get_root(surface) |
| return surface, root |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| words = sentence.split() |
| if words[0] == surface_word or words[-1] == surface_word: |
| return None |
|
|
| return sentence.replace(surface_word, "_____", 1) |
|
|
|
|
| |
| |
| |
|
|
| 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() |
|
|
| |
| if not is_valid_sentence(sentence): |
| return None |
|
|
| |
| tokens = tokenize_text(sentence) |
| pos_tags = get_pos_tags(sentence) |
| entities = get_entities_from_sentence(sentence) |
|
|
| |
| candidate = select_candidate(pos_tags, entities, sentence) |
| if not candidate: |
| return None |
|
|
| surface, root = candidate |
|
|
| |
| question = build_question(sentence, surface) |
| if not question: |
| return None |
|
|
| |
| options = generate_options( |
| answer_word=root, |
| sentence_words=tokens, |
| pos_tags=pos_tags, |
| question=question, |
| context=sentence, |
| ) |
|
|
| return { |
| "question": question, |
| "options": options, |
| "answer": root, |
| } |
|
|