File size: 6,407 Bytes
c3c2e5f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """
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,
}
|