Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -30,7 +30,9 @@ from google.genai import types
|
|
| 30 |
import azure.cognitiveservices.speech as speechsdk
|
| 31 |
|
| 32 |
from korean_rules import rule_engine
|
| 33 |
-
from content_pack import get_active_pack, replace_active_pack
|
|
|
|
|
|
|
| 34 |
from learner_model import get_or_create_session, get_session, delete_session, purge_stale_sessions
|
| 35 |
from question_generator import QuestionGenerator, QTYPE_TO_RULE
|
| 36 |
|
|
@@ -671,6 +673,145 @@ def handle_writing_verification(data):
|
|
| 671 |
emit('writing_result', {"correct": False, "detected_text": "Error", "feedback": "Server error"})
|
| 672 |
|
| 673 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
@socketio.on('get_grammar_rules')
|
| 675 |
def handle_get_grammar_rules(data):
|
| 676 |
pack = get_active_pack()
|
|
|
|
| 30 |
import azure.cognitiveservices.speech as speechsdk
|
| 31 |
|
| 32 |
from korean_rules import rule_engine
|
| 33 |
+
from content_pack import (get_active_pack, replace_active_pack,
|
| 34 |
+
get_explanation, get_example_sentences,
|
| 35 |
+
get_speaking_sentences, get_nouns)
|
| 36 |
from learner_model import get_or_create_session, get_session, delete_session, purge_stale_sessions
|
| 37 |
from question_generator import QuestionGenerator, QTYPE_TO_RULE
|
| 38 |
|
|
|
|
| 673 |
emit('writing_result', {"correct": False, "detected_text": "Error", "feedback": "Server error"})
|
| 674 |
|
| 675 |
|
| 676 |
+
# ===========================================================================
|
| 677 |
+
# 9. LESSON FLOW — Explanation, Example Sentences, Speaking Practice
|
| 678 |
+
# ===========================================================================
|
| 679 |
+
# These three endpoints serve the 6-frame lesson structure:
|
| 680 |
+
# Frame 1+2 → get_lesson_explanation (title, body, pattern)
|
| 681 |
+
# Frame 3 → get_example_sentences (positive/negative pairs)
|
| 682 |
+
# Frame 4 → request_question (existing — MCQ / fill-in / assemble)
|
| 683 |
+
# Frame 4.1 → submit_answer (existing — feedback)
|
| 684 |
+
# Frame 5 → get_speaking_sentence (sentence to repeat aloud)
|
| 685 |
+
# Frame 6 → assess_pronunciation (existing — accuracy + word scores)
|
| 686 |
+
# ===========================================================================
|
| 687 |
+
|
| 688 |
+
@socketio.on('get_lesson_explanation')
|
| 689 |
+
def handle_get_lesson_explanation(data):
|
| 690 |
+
"""
|
| 691 |
+
Frame 1+2: Return explanation and grammar pattern for a rule.
|
| 692 |
+
Emit: { "grammar_rule": "copula" }
|
| 693 |
+
Response: { "grammar_rule", "title", "body", "pattern", "notes", "difficulty", "lesson" }
|
| 694 |
+
"""
|
| 695 |
+
grammar_rule = (data or {}).get("grammar_rule", "")
|
| 696 |
+
if not grammar_rule:
|
| 697 |
+
emit('lesson_explanation', {"error": "grammar_rule is required"})
|
| 698 |
+
return
|
| 699 |
+
explanation = get_explanation(grammar_rule)
|
| 700 |
+
if not explanation.get("title"):
|
| 701 |
+
emit('lesson_explanation', {"error": f"No lesson content for rule: {grammar_rule}"})
|
| 702 |
+
return
|
| 703 |
+
logger.info(f"📖 Explanation: rule={grammar_rule}")
|
| 704 |
+
emit('lesson_explanation', explanation)
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
@socketio.on('get_example_sentences')
|
| 708 |
+
def handle_get_example_sentences(data):
|
| 709 |
+
"""
|
| 710 |
+
Frame 3: Return positive/negative sentence pairs for a grammar rule.
|
| 711 |
+
Emit: { "grammar_rule": "copula", "use_ai": false }
|
| 712 |
+
Response: { "grammar_rule", "sentences": [{positive, negative, focus_word, rule_shown}], "source" }
|
| 713 |
+
|
| 714 |
+
use_ai=true generates fresh pairs via Gemini (slower, falls back to hardcoded on error).
|
| 715 |
+
use_ai=false (default) returns hardcoded pairs instantly.
|
| 716 |
+
"""
|
| 717 |
+
grammar_rule = (data or {}).get("grammar_rule", "")
|
| 718 |
+
use_ai = (data or {}).get("use_ai", False)
|
| 719 |
+
if not grammar_rule:
|
| 720 |
+
emit('example_sentences', {"error": "grammar_rule is required"})
|
| 721 |
+
return
|
| 722 |
+
|
| 723 |
+
logger.info(f"📝 Example sentences: rule={grammar_rule} use_ai={use_ai}")
|
| 724 |
+
sentences = get_example_sentences(grammar_rule)
|
| 725 |
+
|
| 726 |
+
if not use_ai or not client:
|
| 727 |
+
emit('example_sentences', {"grammar_rule": grammar_rule, "sentences": sentences, "source": "hardcoded"})
|
| 728 |
+
return
|
| 729 |
+
|
| 730 |
+
try:
|
| 731 |
+
pack = get_active_pack()
|
| 732 |
+
nouns = get_nouns(pack)[:10]
|
| 733 |
+
noun_list = ", ".join(f"{n['korean']}({n['english']})" for n in nouns)
|
| 734 |
+
rule_info = get_explanation(grammar_rule)
|
| 735 |
+
prompt = (
|
| 736 |
+
f"You are a Korean language teacher generating example sentences.\n"
|
| 737 |
+
f"Grammar rule: {grammar_rule}\n"
|
| 738 |
+
f"Explanation: {rule_info.get('body', '')}\n"
|
| 739 |
+
f"Pattern: {rule_info.get('pattern', '')}\n"
|
| 740 |
+
f"Vocabulary: {noun_list}\n\n"
|
| 741 |
+
f"Generate exactly 2 positive/negative sentence pairs demonstrating the rule.\n"
|
| 742 |
+
f"Return ONLY valid JSON: {{\"sentences\": ["
|
| 743 |
+
f"{{\"positive\": {{\"korean\": \"...\", \"english\": \"...\"}}, "
|
| 744 |
+
f"\"negative\": {{\"korean\": \"...\", \"english\": \"...\"}}, "
|
| 745 |
+
f"\"focus_word\": \"...\", \"rule_shown\": \"...\"}}]}}"
|
| 746 |
+
)
|
| 747 |
+
response = client.models.generate_content(model=GEMINI_MODEL, contents=prompt)
|
| 748 |
+
text = response.text.strip()
|
| 749 |
+
if "```" in text:
|
| 750 |
+
text = text.split("```")[1]
|
| 751 |
+
if text.startswith("json"):
|
| 752 |
+
text = text[4:]
|
| 753 |
+
ai_sentences = json.loads(text.strip()).get("sentences", [])
|
| 754 |
+
if ai_sentences:
|
| 755 |
+
emit('example_sentences', {"grammar_rule": grammar_rule, "sentences": ai_sentences, "source": "gemini"})
|
| 756 |
+
return
|
| 757 |
+
except Exception as e:
|
| 758 |
+
logger.warning(f"Gemini example gen failed: {e} — fallback to hardcoded")
|
| 759 |
+
|
| 760 |
+
emit('example_sentences', {"grammar_rule": grammar_rule, "sentences": sentences, "source": "hardcoded_fallback"})
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
@socketio.on('get_speaking_sentence')
|
| 764 |
+
def handle_get_speaking_sentence(data):
|
| 765 |
+
"""
|
| 766 |
+
Frame 5: Return a sentence for the user to repeat aloud.
|
| 767 |
+
The reference_text field is ready to pass directly to assess_pronunciation.
|
| 768 |
+
|
| 769 |
+
Emit: { "grammar_rule": "copula", "difficulty": 1, "exclude_ids": ["0"] }
|
| 770 |
+
Response: { "grammar_rule", "sentence": {korean, english, difficulty},
|
| 771 |
+
"sentence_index", "total_sentences",
|
| 772 |
+
"instruction", "reference_text" }
|
| 773 |
+
"""
|
| 774 |
+
grammar_rule = (data or {}).get("grammar_rule", "")
|
| 775 |
+
difficulty = (data or {}).get("difficulty", None)
|
| 776 |
+
exclude_ids = set(str(i) for i in (data or {}).get("exclude_ids", []))
|
| 777 |
+
if not grammar_rule:
|
| 778 |
+
emit('speaking_sentence', {"error": "grammar_rule is required"})
|
| 779 |
+
return
|
| 780 |
+
|
| 781 |
+
logger.info(f"🗣️ Speaking sentence: rule={grammar_rule} diff={difficulty}")
|
| 782 |
+
sentences = get_speaking_sentences(grammar_rule, difficulty=difficulty)
|
| 783 |
+
|
| 784 |
+
if not sentences:
|
| 785 |
+
examples = get_example_sentences(grammar_rule)
|
| 786 |
+
if examples:
|
| 787 |
+
first = examples[0]["positive"]
|
| 788 |
+
emit('speaking_sentence', {
|
| 789 |
+
"grammar_rule": grammar_rule,
|
| 790 |
+
"sentence": {"korean": first["korean"], "english": first["english"], "difficulty": 1},
|
| 791 |
+
"sentence_index": "ex_0", "total_sentences": len(examples),
|
| 792 |
+
"instruction": "듣고 따라 말해 보세요! Listen and repeat.",
|
| 793 |
+
"reference_text": first["korean"],
|
| 794 |
+
})
|
| 795 |
+
return
|
| 796 |
+
emit('speaking_sentence', {"error": f"No speaking sentences for rule: {grammar_rule}"})
|
| 797 |
+
return
|
| 798 |
+
|
| 799 |
+
import random
|
| 800 |
+
available = [(i, s) for i, s in enumerate(sentences) if str(i) not in exclude_ids]
|
| 801 |
+
if not available:
|
| 802 |
+
available = list(enumerate(sentences))
|
| 803 |
+
idx, sentence = random.choice(available)
|
| 804 |
+
|
| 805 |
+
emit('speaking_sentence', {
|
| 806 |
+
"grammar_rule": grammar_rule,
|
| 807 |
+
"sentence": sentence,
|
| 808 |
+
"sentence_index": str(idx),
|
| 809 |
+
"total_sentences": len(sentences),
|
| 810 |
+
"instruction": "듣고 따라 말해 보세요! Listen and repeat.",
|
| 811 |
+
"reference_text": sentence["korean"],
|
| 812 |
+
})
|
| 813 |
+
|
| 814 |
+
|
| 815 |
@socketio.on('get_grammar_rules')
|
| 816 |
def handle_get_grammar_rules(data):
|
| 817 |
pack = get_active_pack()
|