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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|