rairo commited on
Commit
3922b40
·
verified ·
1 Parent(s): fe5a9a9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -1
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()