kawaa99's picture
Update utils.py
640b0c6 verified
Raw History Blame Contribute Delete
15.4 kB
import json
import sqlite3
import tempfile
from gtts import gTTS
from datasets import load_dataset
from transformers import MarianMTModel, MarianTokenizer
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
from sentence_transformers import SentenceTransformer, util
import pathlib
import hashlib
import streamlit as st
import random
from functools import lru_cache
from transformers import pipeline
from lemminflect import getLemma
import re
def normalize_idiom(text):
"""
Normalize an idiom string for consistent dictionary keys / matching:
- lowercase
- strip leading/trailing whitespace
- strip a leading "to " (dataset idioms sometimes include the infinitive marker)
"""
text = text.strip().lower()
if text.startswith("to "):
text = text[3:].strip()
return text
# LOAD IDIOMS
@st.cache_data
def load_idioms(path="idiom2.json"):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
cleaned = {}
for idiom, info in data.items():
cleaned[normalize_idiom(idiom)] = {
"meaning": info.get("meaning", ""),
"topic": info.get("topic", "General"),
"gloss": info.get("gloss", ""),
"english_meaning": info.get("english_meaning", "")
}
return cleaned
# AUDIO
CACHE_DIR = pathlib.Path("audio_cache")
CACHE_DIR.mkdir(exist_ok=True)
@st.cache_data
def generate_audio(text, lang="en"):
safe_text = hashlib.md5(f"{text}_{lang}".encode("utf-8")).hexdigest()
file_path = CACHE_DIR / f"{safe_text}.mp3"
if not file_path.exists():
tts = gTTS(text=text, lang=lang)
tts.save(file_path)
return str(file_path)
# SQLITE DATABASE
def init_db():
conn = sqlite3.connect("idioms.db", check_same_thread=False)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS favorite (
idiom TEXT PRIMARY KEY,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS analytics (
idiom TEXT PRIMARY KEY,
attempts INTEGER DEFAULT 0,
correct INTEGER DEFAULT 0
)
""")
conn.commit()
return conn
def add_favorite(conn, idiom):
c = conn.cursor()
c.execute("INSERT OR REPLACE INTO favorite (idiom) VALUES (?)", (idiom,))
conn.commit()
def get_favorite(conn):
c = conn.cursor()
rows = c.execute("SELECT idiom FROM favorite ORDER BY timestamp DESC").fetchall()
return [r[0] for r in rows]
def remove_favorite(conn, idiom):
c = conn.cursor()
c.execute("DELETE FROM favorite WHERE idiom = ?", (idiom,))
conn.commit()
# ANALYTICS
def update_analytics(conn, idiom, is_correct):
c = conn.cursor()
c.execute("""
INSERT INTO analytics (idiom, attempts, correct)
VALUES (?, 0, 0)
ON CONFLICT(idiom) DO NOTHING
""", (idiom,))
c.execute("""
UPDATE analytics
SET attempts = attempts + 1,
correct = correct + ?
WHERE idiom = ?
""", (1 if is_correct else 0, idiom))
conn.commit()
def get_learning_stats(conn):
c = conn.cursor()
rows = c.execute("SELECT idiom, attempts, correct FROM analytics").fetchall()
stats = []
for idiom, attempts, correct in rows:
acc = correct / attempts if attempts else 0
stats.append((idiom, attempts, correct, acc))
return stats
def get_weak_idioms(conn):
c = conn.cursor()
rows = c.execute("""
SELECT idiom, attempts, correct
FROM analytics
WHERE attempts >= 2
""").fetchall()
weak = []
for idiom, attempts, correct in rows:
accuracy = correct / attempts if attempts else 0
if accuracy < 0.6:
weak.append(idiom)
return weak
# TRANSLATION MODEL
@st.cache_resource
def load_translation_model():
model_name = "Helsinki-NLP/opus-mt-en-es"
tokenizer = MarianTokenizer.from_pretrained(model_name)
model = MarianMTModel.from_pretrained(model_name)
return tokenizer, model
def translate_literal(text):
tokenizer, model = load_translation_model()
inputs = tokenizer(text, return_tensors="pt", truncation=True)
translated = model.generate(**inputs, max_new_tokens=100)
return tokenizer.decode(translated[0], skip_special_tokens=True)
# EXAMPLES DATASETS
@st.cache_data(show_spinner=True)
def build_examples_map():
examples_map = {}
def add_example(key, en, es):
#key = key.lower().strip()
key = normalize_idiom(key)
if key not in examples_map:
examples_map[key] = []
if en or es:
examples_map[key].append({"en": en or "", "es": es or ""})
try:
#ds1 = load_dataset("fdelucaf/IdioTS")
#for row in ds1["train"]:
# if row["sentence_has_idiom"]:
# add_example(row["idiom"], row.get("en", ""), row.get("es", ""))
ds1 = load_dataset("fdelucaf/IdioTS")
for row in ds1["train"]:
value = str(row["sentence_has_idiom"]).strip().lower()
if value == "true":
add_example(row["idiom"], row.get("en", ""), row.get("es", ""))
ds2 = load_dataset("UCSC-Admire/idiom-SFT-dataset-561-2024-12-06_00-40-30")
#for row in ds2["train"]:
# idiom = row.get("idiom") or row.get("Idiomatic Expression") or ""
# en = row.get("en") or row.get("English") or ""
# es = row.get("es") or row.get("Spanish") or ""
# if idiom:
# add_example(idiom, en, es)
for row in ds2["train"]:
usage_type = (row.get("type") or row.get("label") or "").lower()
if usage_type != "idiomatic":
continue # skip literal / distractor sentences
idiom = row.get("idiom") or row.get("Idiomatic Expression") or ""
en = row.get("en") or row.get("English") or ""
es = row.get("es") or row.get("Spanish") or ""
if idiom:
add_example(idiom, en, es)
except Exception:
pass
return examples_map
# QUIZ GENERATION
# Load once
#def load_generator():
# return pipeline(
# "text-generation",
# model="distilgpt2"
# )
#generator = load_generator()
@st.cache_resource
def load_generator():
model_name = "google/flan-t5-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
return tokenizer, model
#DETECTION IN SENTENCES
# return SentenceTransformer('all-MiniLM-L6-v2')
@st.cache_resource
def load_similarity_model():
return SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
def build_embedding_text(idiom, info):
gloss = info.get("gloss", "")
return f"{gloss}. {info['meaning']}" if gloss else info["meaning"]
@st.cache_data(show_spinner=False)
def _get_idiom_meaning_embeddings(idiom_map_tuple):
"""
Cached separately from the model itself, so meanings only get
re-embedded when idiom_map actually changes, not on every call.
idiom_map_tuple: tuple of (idiom, meaning) pairs, since dicts aren't hashable
for st.cache_data.
"""
model = load_similarity_model()
idioms = [pair[0] for pair in idiom_map_tuple]
meanings = [pair[1] for pair in idiom_map_tuple]
embeddings = model.encode(meanings, convert_to_tensor=True)
return idioms, embeddings
# DETECT IDIOMS
def detect_idioms_ai(text, idiom_map, threshold=0.3):
model = load_similarity_model()
idiom_map_tuple = tuple(
(idiom, build_embedding_text(idiom, info))
#(idiom, info["meaning"])
for idiom, info in idiom_map.items()
)
idioms, meaning_embeddings = _get_idiom_meaning_embeddings(idiom_map_tuple)
text_embedding = model.encode(text, convert_to_tensor=True)
scores = util.cos_sim(text_embedding, meaning_embeddings)[0]
best_idx = int(scores.argmax())
best_score = float(scores[best_idx])
if best_score < threshold:
return []
return [idioms[best_idx]]
def lemmatize_word(word):
lemmas = getLemma(word, upos='VERB')
return lemmas[0] if lemmas else word
def detect_idioms(text, idioms):
tokens = list(re.finditer(r"\b\w+\b", text))
text_words = [t.group().lower() for t in tokens]
text_lemmas = [lemmatize_word(w) for w in text_words]
found = []
spans = {}
for idiom in idioms:
idiom_words = idiom.lower().split()
idiom_lemmas = [lemmatize_word(w) for w in idiom_words]
n = len(idiom_lemmas)
for i in range(len(text_lemmas) - n + 1):
if text_lemmas[i:i+n] == idiom_lemmas:
found.append(idiom)
start = tokens[i].start()
end = tokens[i + n - 1].end()
spans[idiom] = text[start:end]
break
return found, spans
#text_lower = text.lower()
#return [i for i in idioms if i.lower() in text_lower]
#def detect_idioms_ai(text, idiom_map, top_k=3, threshold=0.35):
# model = load_similarity_model()
# idiom_map_tuple = tuple(
# (idiom, info["meaning"]) for idiom, info in idiom_map.items()
#)
# idioms, meaning_embeddings = _get_idiom_meaning_embeddings(idiom_map_tuple)
# text_embedding = model.encode(text, convert_to_tensor=True)
#scores = util.cos_sim(text_embedding, meaning_embeddings)[0]
# ranked = sorted(
# zip(idioms, scores.tolist()),
# key=lambda pair: pair[1],
# reverse=True
# )
# suggestions = [idiom for idiom, score in ranked[:top_k] if score >= threshold]
# return suggestions
def normalize_structure(sentence):
"""
Simplify sentence structure for repetition detection.
"""
sentence = sentence.lower()
# remove punctuation
sentence = re.sub(r"[^\w\s]", "", sentence)
# remove common subjects
starters = [
"i", "he", "she", "they", "we", "my boss",
"the company", "someone", "people", "our team"
]
words = sentence.split()
if len(words) >= 2:
first_two = " ".join(words[:2])
if first_two in starters:
return " ".join(words[2:5])
return " ".join(words[:3])
def generate_ai_sentence(idiom, examples_map, used_structures):
tokenizer, model = load_generator()
banned_phrases = [
"i decided", "he decided", "she decided", "decided to"
]
leakage_phrases = [
"keep the idiom", "exactly unchanged", "paraphrase",
"rules:", "do not explain", "return only one sentence",
"keep the same meaning", "following sentence"
]
examples = examples_map.get(normalize_idiom(idiom), [])
candidates = [ex["en"] for ex in examples if ex.get("en")]
if not candidates:
return None
random.shuffle(candidates)
idiom_pattern = re.compile(r"\b" + re.escape(idiom) + r"\b", re.IGNORECASE)
for base_sentence in candidates:
try:
prompt = f"""Paraphrase the following sentence.
Rules:
- Keep the idiom "{idiom}" exactly unchanged.
- Keep the same meaning.
- Return only one sentence.
- Do not explain anything.
Sentence:
{base_sentence}"""
inputs = tokenizer(prompt, return_tensors="pt", truncation=True)
outputs = model.generate(
**inputs,
max_new_tokens=50,
do_sample=True,
temperature=0.7,
top_p=0.9,
repetition_penalty=1.2,
)
sentence = tokenizer.decode(outputs[0], skip_special_tokens=True).strip()
if not sentence or len(sentence.split()) < 8:
continue
if len(sentence.split()) < len(base_sentence.split()) * 0.6:
continue
if not idiom_pattern.search(sentence):
continue
if sentence.strip().lower() == base_sentence.strip().lower():
continue
if any(leak in sentence.lower() for leak in leakage_phrases):
continue
structure = normalize_structure(sentence)
if structure in used_structures:
continue
used_structures.add(structure)
sentence = idiom_pattern.sub("_____", sentence)
return sentence
except Exception:
continue
# ---------- FALLBACK TO DATASET ----------
examples = examples_map.get(normalize_idiom(idiom), [])
valid_examples = []
for ex in examples:
text = ex.get("en", "")
if idiom_pattern.search(text):
if any(bad in text.lower() for bad in banned_phrases):
continue
structure = normalize_structure(text)
if structure not in used_structures:
valid_examples.append(text)
if valid_examples:
sentence = random.choice(valid_examples)
used_structures.add(normalize_structure(sentence))
sentence = idiom_pattern.sub("_____", sentence)
return sentence
return None
#def generate_distractors(correct_idiom, all_idioms):
# pool = [i for i in all_idioms if i != correct_idiom]
# distractors = random.sample(pool, min(3, len(pool)))
# options = distractors + [correct_idiom]
# random.shuffle(options)
# return options
def generate_distractors(correct_idiom, idiom_map, n=3):
correct_topic = idiom_map[correct_idiom].get("topic", "General")
same_topic_pool = [
i for i in idiom_map
if i != correct_idiom and idiom_map[i].get("topic", "General") == correct_topic
]
other_pool = [
i for i in idiom_map
if i != correct_idiom and idiom_map[i].get("topic", "General") != correct_topic
]
random.shuffle(same_topic_pool)
random.shuffle(other_pool)
distractors = same_topic_pool[:n]
if len(distractors) < n:
distractors += other_pool[: n - len(distractors)]
options = distractors + [correct_idiom]
random.shuffle(options)
return options
def generate_adaptive_quiz(
conn,
idiom_map,
examples_map,
used_questions,
used_structures,
max_ai_attempts=5 # bef:2
):
import time
idioms = list(idiom_map.keys())
available = [i for i in idioms if i not in used_questions]
if not available:
used_questions.clear()
available = idioms[:]
weak = get_weak_idioms(conn)
if weak:
weak_available = [i for i in available if i in weak]
if weak_available:
available = weak_available
random.shuffle(available)
t0 = time.time()
for idiom in available[:max_ai_attempts]:
sentence = generate_ai_sentence(idiom, examples_map, used_structures)
if not sentence:
continue
used_questions.add(idiom)
#wrong_pool = [i for i in idioms if i != idiom]
#wrong_options = random.sample(wrong_pool, min(3, len(wrong_pool)))
#options = wrong_options + [idiom]
#random.shuffle(options)
options = generate_distractors(idiom, idiom_map)
print(f"[TIMING] question generated: {time.time() - t0:.2f}s")
return {
"question": sentence,
"options": options,
"answer": idiom
}
print(f"[TIMING] all attempts failed: {time.time() - t0:.2f}s")
used_questions.clear()
used_structures.clear()
return None