Spaces:
Sleeping
Sleeping
Update agent.py
Browse files
agent.py
CHANGED
@@ -2,15 +2,13 @@ import os
|
|
2 |
import requests
|
3 |
from smolagents.tools import tool
|
4 |
from difflib import SequenceMatcher
|
5 |
-
|
6 |
try:
|
7 |
from gradio_client import Client
|
8 |
except ImportError:
|
9 |
# Fallback import for older versions
|
10 |
import gradio_client
|
11 |
Client = gradio_client.Client
|
12 |
-
|
13 |
-
from google.genai import types
|
14 |
import json
|
15 |
import time
|
16 |
import numpy as np
|
@@ -26,9 +24,9 @@ TTS_API = os.getenv("TTS_API")
|
|
26 |
STT_API = os.getenv("STT_API")
|
27 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
28 |
|
29 |
-
# Configure Google Gemini
|
30 |
if GOOGLE_API_KEY:
|
31 |
-
|
32 |
|
33 |
@tool
|
34 |
def generate_story(name: str, grade: str, topic: str) -> str:
|
@@ -97,19 +95,20 @@ def generate_story(name: str, grade: str, topic: str) -> str:
|
|
97 |
"""
|
98 |
|
99 |
# Use Google Gemini
|
|
|
|
|
100 |
# Adjust generation parameters based on grade level
|
101 |
max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
|
102 |
|
103 |
-
generation_config =
|
104 |
-
temperature
|
105 |
-
max_output_tokens
|
106 |
-
top_p
|
107 |
-
|
108 |
|
109 |
-
response =
|
110 |
-
|
111 |
-
|
112 |
-
config=generation_config
|
113 |
)
|
114 |
|
115 |
return response.text.strip()
|
@@ -322,54 +321,23 @@ def compare_texts_for_feedback(original: str, spoken: str) -> str:
|
|
322 |
Returns:
|
323 |
str: Comprehensive, age-appropriate feedback with learning suggestions.
|
324 |
"""
|
325 |
-
# Check if the spoken text is too short to be meaningful
|
326 |
-
if not spoken or len(spoken.split()) < 3:
|
327 |
-
return "⚠️ Your reading was too short. Please try reading the complete story."
|
328 |
-
|
329 |
# Clean and process text
|
330 |
orig_words = [w.strip(".,!?;:\"'").lower() for w in original.split() if w.strip()]
|
331 |
spoken_words = [w.strip(".,!?;:\"'").lower() for w in spoken.split() if w.strip()]
|
332 |
|
333 |
-
# Set minimum threshold for overall matching - if nothing matches at all,
|
334 |
-
# it's likely the student read something completely different
|
335 |
-
common_words = set(orig_words).intersection(set(spoken_words))
|
336 |
-
if len(common_words) < max(2, len(orig_words) * 0.1): # At least 2 words or 10% must match
|
337 |
-
return "⚠️ I couldn't recognize enough words from the story. Please try reading the story text shown on the screen.\n\nReading accuracy: 0.0%"
|
338 |
-
|
339 |
# Calculate accuracy using sequence matching
|
340 |
matcher = SequenceMatcher(None, orig_words, spoken_words, autojunk=False)
|
341 |
accuracy = matcher.ratio() * 100
|
342 |
|
343 |
-
# Identify different types of errors
|
344 |
-
|
345 |
-
import difflib
|
346 |
-
d = difflib.Differ()
|
347 |
-
diff = list(d.compare([w.lower() for w in original.split() if w.strip()],
|
348 |
-
[w.lower() for w in spoken.split() if w.strip()]))
|
349 |
-
|
350 |
-
missed_words = []
|
351 |
-
for word in diff:
|
352 |
-
if word.startswith('- '): # Words in original but not in spoken
|
353 |
-
clean_word = word[2:].strip(".,!?;:\"'").lower()
|
354 |
-
if clean_word and len(clean_word) > 1: # Avoid punctuation
|
355 |
-
missed_words.append(clean_word)
|
356 |
-
|
357 |
-
# Convert to set to remove duplicates but preserve order for important words
|
358 |
-
missed_words_set = set(missed_words)
|
359 |
-
|
360 |
-
# Extra words (might be mispronunciations or additions)
|
361 |
extra_words = set(spoken_words) - set(orig_words)
|
362 |
|
363 |
# Find mispronounced words (words that sound similar but are different)
|
364 |
mispronounced = find_similar_words(orig_words, spoken_words)
|
365 |
|
366 |
-
# Prioritize important words (like nouns, longer words) if available
|
367 |
-
important_missed = [w for w in missed_words if len(w) > 4]
|
368 |
-
if important_missed:
|
369 |
-
missed_words_set = set(important_missed) | set([w for w in missed_words if w not in important_missed][:3])
|
370 |
-
|
371 |
# Generate age-appropriate feedback
|
372 |
-
return generate_adaptive_feedback(accuracy,
|
373 |
|
374 |
def find_similar_words(original_words: list, spoken_words: list) -> list:
|
375 |
"""
|
@@ -467,8 +435,6 @@ def get_pronunciation_tip(word: str) -> str:
|
|
467 |
return f"Sound it out: {'-'.join(word)}"
|
468 |
elif word.endswith('tion'):
|
469 |
return "Ends with 'shun' sound"
|
470 |
-
elif word.endswith('sion'):
|
471 |
-
return "Ends with 'zhun' or 'shun' sound"
|
472 |
elif word.endswith('ed'):
|
473 |
if word[-3] in 'td':
|
474 |
return "Past tense - ends with 'ed' sound"
|
@@ -476,24 +442,8 @@ def get_pronunciation_tip(word: str) -> str:
|
|
476 |
return "Past tense - ends with 'd' sound"
|
477 |
elif 'th' in word:
|
478 |
return "Put your tongue between your teeth for 'th'"
|
479 |
-
elif 'ch' in word:
|
480 |
-
return "Make the 'ch' sound like in 'cheese'"
|
481 |
-
elif 'sh' in word:
|
482 |
-
return "Make the 'sh' sound like in 'ship'"
|
483 |
-
elif word.startswith('kn'):
|
484 |
-
return "The 'k' is silent, start with the 'n' sound"
|
485 |
-
elif word.startswith('ph'):
|
486 |
-
return "The 'ph' makes an 'f' sound"
|
487 |
elif word.startswith('wh'):
|
488 |
return "Starts with 'w' sound (like 'when')"
|
489 |
-
elif word.endswith('igh'):
|
490 |
-
return "The 'igh' makes a long 'i' sound like in 'night'"
|
491 |
-
elif 'ou' in word:
|
492 |
-
return "The 'ou' often sounds like 'ow' in 'cow'"
|
493 |
-
elif 'ai' in word:
|
494 |
-
return "The 'ai' makes the long 'a' sound"
|
495 |
-
elif 'ea' in word:
|
496 |
-
return "The 'ea' usually makes the long 'e' sound"
|
497 |
elif len(word) >= 6:
|
498 |
# Break longer words into syllables
|
499 |
return f"Break it down: {break_into_syllables(word)}"
|
@@ -523,32 +473,12 @@ def get_pronunciation_correction(original: str, spoken: str) -> str:
|
|
523 |
return f"Starts with '{orig[0]}' sound, not '{spok[0]}'"
|
524 |
elif orig[-1] != spok[-1]:
|
525 |
return f"Ends with '{orig[-1]}' sound"
|
526 |
-
|
527 |
-
|
528 |
-
orig_vowels = [c for c in orig if c in 'aeiou']
|
529 |
-
spok_vowels = [c for c in spok if c in 'aeiou']
|
530 |
-
|
531 |
-
if orig_vowels != spok_vowels:
|
532 |
-
# Find the first different vowel
|
533 |
-
for i in range(min(len(orig_vowels), len(spok_vowels))):
|
534 |
-
if orig_vowels[i] != spok_vowels[i]:
|
535 |
-
vowel_map = {
|
536 |
-
'a': "ah (like in 'cat')",
|
537 |
-
'e': "eh (like in 'bed')",
|
538 |
-
'i': "ih (like in 'sit')",
|
539 |
-
'o': "oh (like in 'hot')",
|
540 |
-
'u': "uh (like in 'cup')"
|
541 |
-
}
|
542 |
-
correct_sound = vowel_map.get(orig_vowels[i], f"'{orig_vowels[i]}'")
|
543 |
-
wrong_sound = vowel_map.get(spok_vowels[i], f"'{spok_vowels[i]}'")
|
544 |
-
return f"Say the vowel sound as {correct_sound}, not {wrong_sound}"
|
545 |
-
|
546 |
-
# Default case
|
547 |
-
return f"Listen carefully: '{orig}' - try saying it slower"
|
548 |
|
549 |
def break_into_syllables(word: str) -> str:
|
550 |
"""
|
551 |
-
|
552 |
|
553 |
Args:
|
554 |
word (str): Word to break into syllables
|
@@ -556,97 +486,22 @@ def break_into_syllables(word: str) -> str:
|
|
556 |
Returns:
|
557 |
str: Word broken into syllables
|
558 |
"""
|
559 |
-
vowels = '
|
560 |
-
word = word.lower()
|
561 |
syllables = []
|
562 |
current_syllable = ''
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
syllables.append(prefix)
|
570 |
-
word = word[len(prefix):]
|
571 |
-
break
|
572 |
-
|
573 |
-
# Handle common suffixes
|
574 |
-
common_suffixes = ['ing', 'ed', 'er', 'est', 'ly', 'ful', 'ness', 'less', 'ment', 'able', 'ible']
|
575 |
-
for suffix in common_suffixes:
|
576 |
-
if word.endswith(suffix) and len(word) > len(suffix) + 1:
|
577 |
-
suffix_syllable = suffix
|
578 |
-
word = word[:-len(suffix)]
|
579 |
-
syllables.append(word)
|
580 |
-
syllables.append(suffix_syllable)
|
581 |
-
return '-'.join(syllables)
|
582 |
-
|
583 |
-
# Process the word character by character
|
584 |
-
i = 0
|
585 |
-
while i < len(word):
|
586 |
-
char = word[i]
|
587 |
-
|
588 |
-
# If we encounter a vowel
|
589 |
-
if char in vowels:
|
590 |
-
# Start or add to a syllable
|
591 |
-
if consonant_cluster:
|
592 |
-
# For consonant clusters, we generally add one consonant to the current syllable
|
593 |
-
# and move the rest to the next syllable
|
594 |
-
if len(consonant_cluster) > 1:
|
595 |
-
if current_syllable: # If we already have a syllable started
|
596 |
-
current_syllable += consonant_cluster[0]
|
597 |
-
syllables.append(current_syllable)
|
598 |
-
current_syllable = consonant_cluster[1:] + char
|
599 |
-
else: # For starting consonant clusters
|
600 |
-
current_syllable = consonant_cluster + char
|
601 |
-
else: # Single consonant
|
602 |
-
current_syllable += consonant_cluster + char
|
603 |
-
consonant_cluster = ''
|
604 |
-
else:
|
605 |
-
current_syllable += char
|
606 |
-
|
607 |
-
# Check for vowel pairs that should stay together
|
608 |
-
if i < len(word) - 1 and word[i+1] in vowels:
|
609 |
-
vowel_pairs = ['ea', 'ee', 'oo', 'ou', 'ie', 'ai', 'oa']
|
610 |
-
if word[i:i+2] in vowel_pairs:
|
611 |
-
current_syllable += word[i+1]
|
612 |
-
i += 1 # Skip the next vowel since we've added it
|
613 |
-
else: # Consonant
|
614 |
-
if current_syllable: # If we have an open syllable
|
615 |
-
if i < len(word) - 1 and word[i+1] not in vowels: # Consonant cluster
|
616 |
-
consonant_cluster += char
|
617 |
-
else: # Single consonant followed by vowel
|
618 |
-
current_syllable += char
|
619 |
-
else: # Starting with consonant or building consonant cluster
|
620 |
-
consonant_cluster += char
|
621 |
-
|
622 |
-
# Handle end of word or ready to break syllable
|
623 |
-
if i == len(word) - 1 or (char in vowels and i < len(word) - 1 and word[i+1] not in vowels):
|
624 |
-
if current_syllable:
|
625 |
syllables.append(current_syllable)
|
626 |
current_syllable = ''
|
627 |
-
|
628 |
-
i += 1
|
629 |
-
|
630 |
-
# Add any remaining parts
|
631 |
-
if consonant_cluster:
|
632 |
-
if syllables:
|
633 |
-
syllables[-1] += consonant_cluster
|
634 |
-
else:
|
635 |
-
syllables.append(consonant_cluster)
|
636 |
|
637 |
if current_syllable:
|
638 |
syllables.append(current_syllable)
|
639 |
|
640 |
-
|
641 |
-
result = '-'.join(syllables) if syllables else word
|
642 |
-
|
643 |
-
# If we ended up with no breaks, provide a simpler approach
|
644 |
-
if result == word and len(word) > 3:
|
645 |
-
# Simple fallback: break after every other letter
|
646 |
-
syllables = [word[i:i+2] for i in range(0, len(word), 2)]
|
647 |
-
result = '-'.join(syllables)
|
648 |
-
|
649 |
-
return result
|
650 |
|
651 |
@tool
|
652 |
def generate_targeted_story(previous_feedback: str, name: str, grade: str, missed_words: list = None) -> str:
|
@@ -666,38 +521,15 @@ def generate_targeted_story(previous_feedback: str, name: str, grade: str, misse
|
|
666 |
grade_num = int(''.join(filter(str.isdigit, grade)) or "3")
|
667 |
age = grade_num + 5
|
668 |
|
669 |
-
# Dynamically determine story parameters based on grade - match the same criteria as main story generation
|
670 |
-
if grade_num <= 2:
|
671 |
-
# Grades 1-2: Very simple stories
|
672 |
-
story_length = "2-3 short sentences"
|
673 |
-
vocabulary_level = "very simple words (mostly 1-2 syllables)"
|
674 |
-
sentence_structure = "short, simple sentences"
|
675 |
-
complexity = "basic concepts"
|
676 |
-
reading_level = "beginner"
|
677 |
-
elif grade_num <= 4:
|
678 |
-
# Grades 3-4: Intermediate stories
|
679 |
-
story_length = "1-2 short paragraphs"
|
680 |
-
vocabulary_level = "age-appropriate words with some longer words"
|
681 |
-
sentence_structure = "mix of simple and compound sentences"
|
682 |
-
complexity = "intermediate concepts with some detail"
|
683 |
-
reading_level = "intermediate"
|
684 |
-
else:
|
685 |
-
# Grades 5-6: More advanced stories
|
686 |
-
story_length = "2-3 paragraphs"
|
687 |
-
vocabulary_level = "varied vocabulary including descriptive words"
|
688 |
-
sentence_structure = "complex sentences with descriptive language"
|
689 |
-
complexity = "detailed concepts and explanations"
|
690 |
-
reading_level = "advanced elementary"
|
691 |
-
|
692 |
# Extract difficulty level from previous feedback
|
693 |
if "AMAZING" in previous_feedback or "accuracy: 9" in previous_feedback:
|
694 |
-
difficulty_adjustment = "slightly more challenging
|
695 |
focus_area = "new vocabulary and longer sentences"
|
696 |
elif "GOOD" in previous_feedback or "accuracy: 8" in previous_feedback:
|
697 |
difficulty_adjustment = "similar level with some new words"
|
698 |
focus_area = "reinforcing current skills"
|
699 |
else:
|
700 |
-
difficulty_adjustment = "
|
701 |
focus_area = "basic vocabulary and simple sentences"
|
702 |
|
703 |
# Create targeted practice words
|
@@ -711,16 +543,8 @@ def generate_targeted_story(previous_feedback: str, name: str, grade: str, misse
|
|
711 |
prompt = f"""
|
712 |
You are an expert reading coach creating a personalized story for {name}, a {age}-year-old in {grade}.
|
713 |
|
714 |
-
GRADE LEVEL: {grade} ({reading_level} level)
|
715 |
-
|
716 |
-
STORY SPECIFICATIONS:
|
717 |
-
- Length: {story_length}
|
718 |
-
- Vocabulary: {vocabulary_level}
|
719 |
-
- Sentence structure: {sentence_structure}
|
720 |
-
- Complexity: {complexity}
|
721 |
-
|
722 |
LEARNING ADAPTATION:
|
723 |
-
- Make this story {difficulty_adjustment}
|
724 |
- Focus on: {focus_area}
|
725 |
- {word_focus}
|
726 |
|
@@ -737,17 +561,17 @@ def generate_targeted_story(previous_feedback: str, name: str, grade: str, misse
|
|
737 |
"""
|
738 |
|
739 |
# Generate targeted story
|
|
|
740 |
max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
|
741 |
|
742 |
-
generation_config =
|
743 |
-
temperature
|
744 |
-
max_output_tokens
|
745 |
-
top_p
|
746 |
-
|
747 |
|
748 |
-
response =
|
749 |
-
|
750 |
-
contents=[prompt],
|
751 |
generation_config=generation_config
|
752 |
)
|
753 |
|
@@ -826,18 +650,6 @@ class ReadingCoachAgent:
|
|
826 |
# Transcribe the audio
|
827 |
transcribed_text = transcribe_audio(audio_path)
|
828 |
|
829 |
-
# Check if the transcribed text is an error message or empty
|
830 |
-
if transcribed_text.startswith("Error:") or transcribed_text.startswith("I couldn't hear") or len(transcribed_text.strip()) < 3:
|
831 |
-
# Return a helpful message instead of giving feedback with accuracy points
|
832 |
-
error_feedback = "⚠️ I couldn't hear your reading clearly. Please try again and make sure to:\n"
|
833 |
-
error_feedback += "• Speak clearly and at a normal pace\n"
|
834 |
-
error_feedback += "• Make sure your microphone is working properly\n"
|
835 |
-
error_feedback += "• Try reading in a quieter environment\n"
|
836 |
-
error_feedback += "• Read the complete story from beginning to end\n\n"
|
837 |
-
error_feedback += "Reading accuracy: 0.0%"
|
838 |
-
|
839 |
-
return transcribed_text, error_feedback, 0.0
|
840 |
-
|
841 |
# Compare with original story and get feedback
|
842 |
feedback = compare_texts_for_feedback(self.current_story, transcribed_text)
|
843 |
|
@@ -871,25 +683,8 @@ class ReadingCoachAgent:
|
|
871 |
name = self.student_info["name"]
|
872 |
grade = self.student_info["grade"]
|
873 |
|
874 |
-
# Get the last feedback to personalize the practice story
|
875 |
-
last_feedback = ""
|
876 |
-
missed_words_list = []
|
877 |
-
|
878 |
-
# Extract missed words from feedback if available
|
879 |
-
if self.current_session:
|
880 |
-
session_data = self.session_manager.get_session(self.current_session)
|
881 |
-
if session_data and "feedback_history" in session_data and session_data["feedback_history"]:
|
882 |
-
last_feedback = session_data["feedback_history"][-1]["feedback"]
|
883 |
-
|
884 |
-
# Extract missed words from the feedback
|
885 |
-
import re
|
886 |
-
if "PRACTICE THESE WORDS:" in last_feedback:
|
887 |
-
# Find all words that appear after bullet points
|
888 |
-
matches = re.findall(r'• ([A-Z]+)', last_feedback)
|
889 |
-
missed_words_list = [word.lower() for word in matches]
|
890 |
-
|
891 |
# Generate a new practice story using the targeted story function
|
892 |
-
practice_story = generate_targeted_story(
|
893 |
self.current_story = practice_story
|
894 |
|
895 |
return practice_story
|
@@ -912,39 +707,3 @@ class ReadingCoachAgent:
|
|
912 |
if match:
|
913 |
return float(match.group(1))
|
914 |
return 0.0
|
915 |
-
|
916 |
-
def _extract_missed_words_from_feedback(feedback: str) -> list:
|
917 |
-
"""
|
918 |
-
Extract missed words from feedback text.
|
919 |
-
|
920 |
-
Args:
|
921 |
-
feedback (str): Feedback text containing missed words
|
922 |
-
|
923 |
-
Returns:
|
924 |
-
list: List of missed words
|
925 |
-
"""
|
926 |
-
import re
|
927 |
-
missed_words = []
|
928 |
-
|
929 |
-
# Check if feedback contains practice words section
|
930 |
-
if "PRACTICE THESE WORDS:" in feedback:
|
931 |
-
# Extract the section with practice words
|
932 |
-
practice_section = feedback.split("PRACTICE THESE WORDS:")[1].split("\n")[1:]
|
933 |
-
# Extract words that appear after bullet points
|
934 |
-
for line in practice_section:
|
935 |
-
if "•" in line and "-" in line:
|
936 |
-
# Extract word before the dash
|
937 |
-
match = re.search(r'• ([A-Z]+) -', line)
|
938 |
-
if match:
|
939 |
-
missed_words.append(match.group(1).lower())
|
940 |
-
|
941 |
-
# If we also have mispronounced words, add them too
|
942 |
-
if "PRONUNCIATION PRACTICE:" in feedback:
|
943 |
-
pronun_section = feedback.split("PRONUNCIATION PRACTICE:")[1].split("\n")[1:]
|
944 |
-
for line in pronun_section:
|
945 |
-
if "•" in line and "(you said" in line:
|
946 |
-
match = re.search(r'• ([A-Z]+) \(you said', line)
|
947 |
-
if match:
|
948 |
-
missed_words.append(match.group(1).lower())
|
949 |
-
|
950 |
-
return missed_words
|
|
|
2 |
import requests
|
3 |
from smolagents.tools import tool
|
4 |
from difflib import SequenceMatcher
|
|
|
5 |
try:
|
6 |
from gradio_client import Client
|
7 |
except ImportError:
|
8 |
# Fallback import for older versions
|
9 |
import gradio_client
|
10 |
Client = gradio_client.Client
|
11 |
+
import google.generativeai as genai
|
|
|
12 |
import json
|
13 |
import time
|
14 |
import numpy as np
|
|
|
24 |
STT_API = os.getenv("STT_API")
|
25 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
26 |
|
27 |
+
# Configure Google Gemini
|
28 |
if GOOGLE_API_KEY:
|
29 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
30 |
|
31 |
@tool
|
32 |
def generate_story(name: str, grade: str, topic: str) -> str:
|
|
|
95 |
"""
|
96 |
|
97 |
# Use Google Gemini
|
98 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
99 |
+
|
100 |
# Adjust generation parameters based on grade level
|
101 |
max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
|
102 |
|
103 |
+
generation_config = {
|
104 |
+
"temperature": 0.8,
|
105 |
+
"max_output_tokens": max_tokens,
|
106 |
+
"top_p": 0.9,
|
107 |
+
}
|
108 |
|
109 |
+
response = model.generate_content(
|
110 |
+
contents=prompt,
|
111 |
+
generation_config=generation_config
|
|
|
112 |
)
|
113 |
|
114 |
return response.text.strip()
|
|
|
321 |
Returns:
|
322 |
str: Comprehensive, age-appropriate feedback with learning suggestions.
|
323 |
"""
|
|
|
|
|
|
|
|
|
324 |
# Clean and process text
|
325 |
orig_words = [w.strip(".,!?;:\"'").lower() for w in original.split() if w.strip()]
|
326 |
spoken_words = [w.strip(".,!?;:\"'").lower() for w in spoken.split() if w.strip()]
|
327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
# Calculate accuracy using sequence matching
|
329 |
matcher = SequenceMatcher(None, orig_words, spoken_words, autojunk=False)
|
330 |
accuracy = matcher.ratio() * 100
|
331 |
|
332 |
+
# Identify different types of errors
|
333 |
+
missed_words = set(orig_words) - set(spoken_words)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
extra_words = set(spoken_words) - set(orig_words)
|
335 |
|
336 |
# Find mispronounced words (words that sound similar but are different)
|
337 |
mispronounced = find_similar_words(orig_words, spoken_words)
|
338 |
|
|
|
|
|
|
|
|
|
|
|
339 |
# Generate age-appropriate feedback
|
340 |
+
return generate_adaptive_feedback(accuracy, missed_words, extra_words, mispronounced, len(orig_words))
|
341 |
|
342 |
def find_similar_words(original_words: list, spoken_words: list) -> list:
|
343 |
"""
|
|
|
435 |
return f"Sound it out: {'-'.join(word)}"
|
436 |
elif word.endswith('tion'):
|
437 |
return "Ends with 'shun' sound"
|
|
|
|
|
438 |
elif word.endswith('ed'):
|
439 |
if word[-3] in 'td':
|
440 |
return "Past tense - ends with 'ed' sound"
|
|
|
442 |
return "Past tense - ends with 'd' sound"
|
443 |
elif 'th' in word:
|
444 |
return "Put your tongue between your teeth for 'th'"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
elif word.startswith('wh'):
|
446 |
return "Starts with 'w' sound (like 'when')"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
447 |
elif len(word) >= 6:
|
448 |
# Break longer words into syllables
|
449 |
return f"Break it down: {break_into_syllables(word)}"
|
|
|
473 |
return f"Starts with '{orig[0]}' sound, not '{spok[0]}'"
|
474 |
elif orig[-1] != spok[-1]:
|
475 |
return f"Ends with '{orig[-1]}' sound"
|
476 |
+
else:
|
477 |
+
return f"Listen carefully: '{orig}' - try saying it slower"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
478 |
|
479 |
def break_into_syllables(word: str) -> str:
|
480 |
"""
|
481 |
+
Simple syllable breaking for pronunciation help.
|
482 |
|
483 |
Args:
|
484 |
word (str): Word to break into syllables
|
|
|
486 |
Returns:
|
487 |
str: Word broken into syllables
|
488 |
"""
|
489 |
+
vowels = 'aeiou'
|
|
|
490 |
syllables = []
|
491 |
current_syllable = ''
|
492 |
+
|
493 |
+
for i, char in enumerate(word):
|
494 |
+
current_syllable += char
|
495 |
+
# Simple rule: break after vowel if next char is consonant
|
496 |
+
if char.lower() in vowels and i < len(word) - 1:
|
497 |
+
if word[i + 1].lower() not in vowels:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
498 |
syllables.append(current_syllable)
|
499 |
current_syllable = ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
|
501 |
if current_syllable:
|
502 |
syllables.append(current_syllable)
|
503 |
|
504 |
+
return '-'.join(syllables) if len(syllables) > 1 else word
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
505 |
|
506 |
@tool
|
507 |
def generate_targeted_story(previous_feedback: str, name: str, grade: str, missed_words: list = None) -> str:
|
|
|
521 |
grade_num = int(''.join(filter(str.isdigit, grade)) or "3")
|
522 |
age = grade_num + 5
|
523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
524 |
# Extract difficulty level from previous feedback
|
525 |
if "AMAZING" in previous_feedback or "accuracy: 9" in previous_feedback:
|
526 |
+
difficulty_adjustment = "slightly more challenging"
|
527 |
focus_area = "new vocabulary and longer sentences"
|
528 |
elif "GOOD" in previous_feedback or "accuracy: 8" in previous_feedback:
|
529 |
difficulty_adjustment = "similar level with some new words"
|
530 |
focus_area = "reinforcing current skills"
|
531 |
else:
|
532 |
+
difficulty_adjustment = "simpler and shorter"
|
533 |
focus_area = "basic vocabulary and simple sentences"
|
534 |
|
535 |
# Create targeted practice words
|
|
|
543 |
prompt = f"""
|
544 |
You are an expert reading coach creating a personalized story for {name}, a {age}-year-old in {grade}.
|
545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
546 |
LEARNING ADAPTATION:
|
547 |
+
- Make this story {difficulty_adjustment} than the previous one
|
548 |
- Focus on: {focus_area}
|
549 |
- {word_focus}
|
550 |
|
|
|
561 |
"""
|
562 |
|
563 |
# Generate targeted story
|
564 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
565 |
max_tokens = 300 if grade_num <= 2 else 600 if grade_num <= 4 else 1000
|
566 |
|
567 |
+
generation_config = {
|
568 |
+
"temperature": 0.7,
|
569 |
+
"max_output_tokens": max_tokens,
|
570 |
+
"top_p": 0.9,
|
571 |
+
}
|
572 |
|
573 |
+
response = model.generate_content(
|
574 |
+
contents=prompt,
|
|
|
575 |
generation_config=generation_config
|
576 |
)
|
577 |
|
|
|
650 |
# Transcribe the audio
|
651 |
transcribed_text = transcribe_audio(audio_path)
|
652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
653 |
# Compare with original story and get feedback
|
654 |
feedback = compare_texts_for_feedback(self.current_story, transcribed_text)
|
655 |
|
|
|
683 |
name = self.student_info["name"]
|
684 |
grade = self.student_info["grade"]
|
685 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
686 |
# Generate a new practice story using the targeted story function
|
687 |
+
practice_story = generate_targeted_story("", name, grade)
|
688 |
self.current_story = practice_story
|
689 |
|
690 |
return practice_story
|
|
|
707 |
if match:
|
708 |
return float(match.group(1))
|
709 |
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|