fix(cefr): resolve authentic context sentence per term and repair legacy SRS cards
- Add resolve_context_sentence_for_term to replace placeholder blanks and lookup authentic sentences from cefr_questions or books - Fix dual mistake ingestion in process_cefr_answer to assign distinct context to target and chosen distractor - Provide authentic context_sentence in options_breakdown and pass it via data-context in cefr.js - Add repair_mismatched_cefr_contexts in database.py to automatically repair cards with placeholder blanks - Bump Service Worker cache to notes-v5.9 - Add test_cefr_context_resolution_and_repair covering resolution, dual ingestion, manual add, and DB repair
This commit is contained in:
1 parent
82b7a654ca
commit
3098d3016d
5 files changed
+230
-10
No files matched your search
+77
-7
@@ -9,6 +9,7 @@ Features:
|
||||
- Per-level progress tracking (mastered, learning, percentage).
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import sqlite3
|
||||
import random
|
||||
@@ -2039,6 +2040,64 @@ def get_cefr_quiz_batch(
|
||||
return quiz_items
|
||||
|
||||
|
||||
def resolve_context_sentence_for_term(
|
||||
conn: sqlite3.Connection,
|
||||
term: str,
|
||||
candidate_sentence: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
Resolves the best authentic context sentence for a given term:
|
||||
1. If candidate_sentence contains the term, use it (with ___ replaced).
|
||||
2. Look up in cefr_questions specifically for this term.
|
||||
3. Look up in books table for an authentic sentence containing this term.
|
||||
4. Fallback to clean natural sentence.
|
||||
"""
|
||||
clean_term = term.strip()
|
||||
if not clean_term:
|
||||
return ""
|
||||
|
||||
# 1. If candidate_sentence already contains this term (case-insensitive word boundary)
|
||||
if candidate_sentence:
|
||||
cand_clean = candidate_sentence.strip()
|
||||
if re.search(rf"\b{re.escape(clean_term)}\b", cand_clean, re.IGNORECASE):
|
||||
return re.sub(r"\s+", " ", cand_clean.replace("___", clean_term)).strip()
|
||||
|
||||
# 2. Look up in cefr_questions specifically for this term
|
||||
try:
|
||||
q_row = conn.execute(
|
||||
"SELECT sentence FROM cefr_questions WHERE lower(trim(term)) = lower(trim(?)) ORDER BY id ASC LIMIT 1",
|
||||
(clean_term,),
|
||||
).fetchone()
|
||||
if q_row and q_row["sentence"]:
|
||||
return re.sub(r"\s+", " ", q_row["sentence"].replace("___", clean_term)).strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Look up in books table
|
||||
try:
|
||||
books = conn.execute("SELECT chapters_json FROM books ORDER BY id DESC LIMIT 10").fetchall()
|
||||
for b in books:
|
||||
try:
|
||||
chapters = json.loads(b["chapters_json"])
|
||||
except Exception:
|
||||
continue
|
||||
for ch in chapters:
|
||||
content = ch.get("content", "")
|
||||
if clean_term.lower() in content.lower():
|
||||
for s in re.split(r"(?<=[.!?])\s+", content):
|
||||
s_clean = s.strip()
|
||||
if 20 <= len(s_clean) <= 180 and re.search(rf"\b{re.escape(clean_term)}\b", s_clean, re.IGNORECASE):
|
||||
return re.sub(r"\s+", " ", s_clean).strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 4. If candidate_sentence had '___' and came from this term
|
||||
if candidate_sentence and "___" in candidate_sentence:
|
||||
return re.sub(r"\s+", " ", candidate_sentence.replace("___", clean_term)).strip()
|
||||
|
||||
return f"Using '{clean_term}' in meaningful sentences reinforces active recall and long-term fluency."
|
||||
|
||||
|
||||
def process_cefr_answer(
|
||||
conn: sqlite3.Connection,
|
||||
user_id: int,
|
||||
@@ -2105,9 +2164,10 @@ def process_cefr_answer(
|
||||
if not is_correct:
|
||||
# =====================================================================
|
||||
# DUAL AUTOMATED GAP INGESTION (Crucial Pedagogical Requirement)
|
||||
# 1. Correct target word -> goes 100% to vocabulary
|
||||
# 2. Chosen wrong word -> also goes to vocabulary!
|
||||
# 1. Correct target word -> goes 100% to vocabulary with its sentence
|
||||
# 2. Chosen wrong word -> also goes to vocabulary with its OWN context!
|
||||
# =====================================================================
|
||||
target_ctx = resolve_context_sentence_for_term(conn, target_term, sentence_with_term)
|
||||
target_card_info = _ingest_card_for_user(
|
||||
conn,
|
||||
user_id=user_id,
|
||||
@@ -2115,7 +2175,7 @@ def process_cefr_answer(
|
||||
translation=translation,
|
||||
phonetic=phonetic,
|
||||
pos=pos,
|
||||
context_sentence=sentence_with_term,
|
||||
context_sentence=target_ctx,
|
||||
explanation=explanation,
|
||||
level=level,
|
||||
tag_suffix="test-mistake",
|
||||
@@ -2130,6 +2190,7 @@ def process_cefr_answer(
|
||||
sel_trans = sel_row["translation"] if sel_row else selected_clean
|
||||
sel_phon = sel_row["phonetic"] if sel_row else ""
|
||||
sel_pos = sel_row["pos"] if sel_row else ""
|
||||
sel_ctx = resolve_context_sentence_for_term(conn, selected_clean)
|
||||
|
||||
selected_card_info = _ingest_card_for_user(
|
||||
conn,
|
||||
@@ -2138,7 +2199,7 @@ def process_cefr_answer(
|
||||
translation=sel_trans,
|
||||
phonetic=sel_phon,
|
||||
pos=sel_pos,
|
||||
context_sentence=sentence_with_term,
|
||||
context_sentence=sel_ctx,
|
||||
explanation=f"Выбрано по ошибке в вопросе про '{target_term}'",
|
||||
level=level,
|
||||
tag_suffix="test-mistake-chosen",
|
||||
@@ -2193,11 +2254,14 @@ def process_cefr_answer(
|
||||
if not is_correct and (is_target or was_selected):
|
||||
is_auto_added = True
|
||||
|
||||
opt_ctx = resolve_context_sentence_for_term(conn, opt_clean, sentence_with_term if is_target else "")
|
||||
|
||||
options_breakdown.append({
|
||||
"term": opt_clean,
|
||||
"translation": opt_trans,
|
||||
"phonetic": opt_phon,
|
||||
"pos": opt_pos,
|
||||
"context_sentence": opt_ctx,
|
||||
"is_correct": is_target,
|
||||
"was_selected": was_selected,
|
||||
"in_library": bool(user_card) or is_auto_added,
|
||||
@@ -2287,8 +2351,11 @@ def _ingest_card_for_user(
|
||||
existing_tags.add(tag_suffix)
|
||||
updated_tags = ", ".join(sorted(existing_tags))
|
||||
|
||||
updated_context = existing_card["context_sentence"] or ""
|
||||
if context_sentence and context_sentence not in updated_context:
|
||||
updated_context = (existing_card["context_sentence"] or "").strip()
|
||||
# If existing context has "___" or lacks the term, overwrite with new authentic context!
|
||||
if ("___" in updated_context or not re.search(rf"\b{re.escape(term_clean)}\b", updated_context, re.IGNORECASE)) and context_sentence:
|
||||
updated_context = context_sentence
|
||||
elif context_sentence and context_sentence not in updated_context:
|
||||
updated_context = f"{updated_context}\n\n{context_sentence}".strip()
|
||||
|
||||
conn.execute(
|
||||
@@ -2337,6 +2404,8 @@ def ingest_cefr_word(
|
||||
clean_term = term.strip()
|
||||
clean_level = level.upper().strip()
|
||||
|
||||
resolved_ctx = resolve_context_sentence_for_term(conn, clean_term, context_sentence)
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT term, translation, phonetic, pos FROM offline_dictionary WHERE term = ? COLLATE NOCASE",
|
||||
(clean_term,),
|
||||
@@ -2352,7 +2421,7 @@ def ingest_cefr_word(
|
||||
translation=trans,
|
||||
phonetic=phon,
|
||||
pos=pos,
|
||||
context_sentence=context_sentence,
|
||||
context_sentence=resolved_ctx,
|
||||
explanation=f"Добавлено вручную из вариантов вопроса теста CEFR ({clean_level})",
|
||||
level=clean_level,
|
||||
tag_suffix="manual-test-add",
|
||||
@@ -2364,6 +2433,7 @@ def ingest_cefr_word(
|
||||
"term": clean_term,
|
||||
"translation": trans,
|
||||
"phonetic": phon,
|
||||
"context_sentence": resolved_ctx,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user