Upload 8 files
Browse files- app.py +0 -0
- custom_audio_recorder.py +1057 -0
- database.py +420 -0
- example_sentences.py +1341 -0
- gamification.py +1501 -0
- quiz_system.py +865 -0
- requirements.txt +19 -3
- vocam_ui.py +501 -0
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
custom_audio_recorder.py
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# custom_audio_recorder.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import streamlit.components.v1 as components
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import base64
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
# Define the component directory
|
| 10 |
+
COMPONENT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "components")
|
| 11 |
+
|
| 12 |
+
# Create the directory if it doesn't exist
|
| 13 |
+
os.makedirs(COMPONENT_DIR, exist_ok=True)
|
| 14 |
+
|
| 15 |
+
# Create the HTML file for the component
|
| 16 |
+
HTML_FILE = os.path.join(COMPONENT_DIR, "audio_recorder.html")
|
| 17 |
+
|
| 18 |
+
# Write the HTML/JavaScript code to the file
|
| 19 |
+
with open(HTML_FILE, "w") as f:
|
| 20 |
+
f.write("""
|
| 21 |
+
<!DOCTYPE html>
|
| 22 |
+
<html>
|
| 23 |
+
<head>
|
| 24 |
+
<title>Audio Recorder with Real-time Feedback</title>
|
| 25 |
+
<style>
|
| 26 |
+
.recorder-container {
|
| 27 |
+
display: flex;
|
| 28 |
+
flex-direction: column;
|
| 29 |
+
align-items: center;
|
| 30 |
+
padding: 15px;
|
| 31 |
+
background-color: #f5f5f5;
|
| 32 |
+
border-radius: 8px;
|
| 33 |
+
margin: 10px 0;
|
| 34 |
+
}
|
| 35 |
+
.record-button {
|
| 36 |
+
background-color: #ff4b4b;
|
| 37 |
+
color: white;
|
| 38 |
+
border: none;
|
| 39 |
+
padding: 10px 20px;
|
| 40 |
+
border-radius: 24px;
|
| 41 |
+
cursor: pointer;
|
| 42 |
+
font-size: 16px;
|
| 43 |
+
margin: 10px;
|
| 44 |
+
display: flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
}
|
| 47 |
+
.record-button.recording {
|
| 48 |
+
background-color: #ff0000;
|
| 49 |
+
animation: pulse 1.5s infinite;
|
| 50 |
+
}
|
| 51 |
+
@keyframes pulse {
|
| 52 |
+
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); }
|
| 53 |
+
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
|
| 54 |
+
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
|
| 55 |
+
}
|
| 56 |
+
.status {
|
| 57 |
+
margin-top: 10px;
|
| 58 |
+
font-style: italic;
|
| 59 |
+
}
|
| 60 |
+
#recordingDisplay {
|
| 61 |
+
margin-top: 15px;
|
| 62 |
+
width: 100%;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Real-time feedback styles */
|
| 66 |
+
.visualization-container {
|
| 67 |
+
width: 100%;
|
| 68 |
+
margin: 10px 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.feedback-meter {
|
| 72 |
+
width: 100%;
|
| 73 |
+
height: 8px;
|
| 74 |
+
background-color: #e0e0e0;
|
| 75 |
+
border-radius: 4px;
|
| 76 |
+
margin: 5px 0;
|
| 77 |
+
overflow: hidden;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.feedback-bar {
|
| 81 |
+
height: 100%;
|
| 82 |
+
width: 0%;
|
| 83 |
+
transition: width 0.2s ease, background-color 0.2s ease;
|
| 84 |
+
border-radius: 4px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.feedback-label {
|
| 88 |
+
display: flex;
|
| 89 |
+
justify-content: space-between;
|
| 90 |
+
font-size: 12px;
|
| 91 |
+
color: #666;
|
| 92 |
+
margin-bottom: 2px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.feedback-value {
|
| 96 |
+
font-weight: bold;
|
| 97 |
+
text-align: right;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.realtime-feedback {
|
| 101 |
+
margin-top: 10px;
|
| 102 |
+
padding: 8px;
|
| 103 |
+
border-radius: 4px;
|
| 104 |
+
font-size: 14px;
|
| 105 |
+
text-align: center;
|
| 106 |
+
background-color: #f0f0f0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Waveform canvas */
|
| 110 |
+
#waveform, #spectrogram {
|
| 111 |
+
width: 100%;
|
| 112 |
+
height: 60px;
|
| 113 |
+
background-color: #f0f0f0;
|
| 114 |
+
border-radius: 4px;
|
| 115 |
+
margin-top: 5px;
|
| 116 |
+
}
|
| 117 |
+
</style>
|
| 118 |
+
</head>
|
| 119 |
+
<body>
|
| 120 |
+
<div class="recorder-container">
|
| 121 |
+
<button id="recordButton" class="record-button">
|
| 122 |
+
<span id="buttonText">Start Recording</span>
|
| 123 |
+
</button>
|
| 124 |
+
<div id="status" class="status">Ready to record</div>
|
| 125 |
+
|
| 126 |
+
<!-- Real-time feedback visualizations -->
|
| 127 |
+
<div class="visualization-container" id="visualizationContainer" style="display: none;">
|
| 128 |
+
<!-- Volume meter -->
|
| 129 |
+
<div class="feedback-label">
|
| 130 |
+
<span>Volume</span>
|
| 131 |
+
<span class="feedback-value" id="volumeValue">0%</span>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="feedback-meter">
|
| 134 |
+
<div class="feedback-bar" id="volumeBar" style="background-color: #4CAF50;"></div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Clarity meter -->
|
| 138 |
+
<div class="feedback-label">
|
| 139 |
+
<span>Clarity</span>
|
| 140 |
+
<span class="feedback-value" id="clarityValue">0%</span>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="feedback-meter">
|
| 143 |
+
<div class="feedback-bar" id="clarityBar" style="background-color: #2196F3;"></div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<!-- Pitch accuracy meter -->
|
| 147 |
+
<div class="feedback-label">
|
| 148 |
+
<span>Pitch</span>
|
| 149 |
+
<span class="feedback-value" id="pitchValue">0%</span>
|
| 150 |
+
</div>
|
| 151 |
+
<div class="feedback-meter">
|
| 152 |
+
<div class="feedback-bar" id="pitchBar" style="background-color: #FF9800;"></div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<!-- Real-time feedback message -->
|
| 156 |
+
<div class="realtime-feedback" id="feedbackMessage">
|
| 157 |
+
Speak clearly into the microphone
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<!-- Audio visualizations -->
|
| 161 |
+
<canvas id="waveform"></canvas>
|
| 162 |
+
<canvas id="spectrogram"></canvas>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<!-- Recording playback area -->
|
| 166 |
+
<div id="recordingDisplay"></div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<script>
|
| 170 |
+
// Audio recording functionality
|
| 171 |
+
let audioChunks = [];
|
| 172 |
+
let mediaRecorder;
|
| 173 |
+
let audioBlob;
|
| 174 |
+
let isRecording = false;
|
| 175 |
+
let audioUrl = null;
|
| 176 |
+
let audioContext;
|
| 177 |
+
let analyser;
|
| 178 |
+
let scriptProcessor;
|
| 179 |
+
let targetWord = ""; // Will be set from Streamlit
|
| 180 |
+
|
| 181 |
+
// Elements
|
| 182 |
+
const recordButton = document.getElementById('recordButton');
|
| 183 |
+
const buttonText = document.getElementById('buttonText');
|
| 184 |
+
const statusText = document.getElementById('status');
|
| 185 |
+
const recordingDisplay = document.getElementById('recordingDisplay');
|
| 186 |
+
const visualizationContainer = document.getElementById('visualizationContainer');
|
| 187 |
+
|
| 188 |
+
// Feedback meters
|
| 189 |
+
const volumeBar = document.getElementById('volumeBar');
|
| 190 |
+
const clarityBar = document.getElementById('clarityBar');
|
| 191 |
+
const pitchBar = document.getElementById('pitchBar');
|
| 192 |
+
const volumeValue = document.getElementById('volumeValue');
|
| 193 |
+
const clarityValue = document.getElementById('clarityValue');
|
| 194 |
+
const pitchValue = document.getElementById('pitchValue');
|
| 195 |
+
const feedbackMessage = document.getElementById('feedbackMessage');
|
| 196 |
+
|
| 197 |
+
// Canvas elements
|
| 198 |
+
const waveformCanvas = document.getElementById('waveform');
|
| 199 |
+
const spectrogramCanvas = document.getElementById('spectrogram');
|
| 200 |
+
const waveformCtx = waveformCanvas.getContext('2d');
|
| 201 |
+
const spectrogramCtx = spectrogramCanvas.getContext('2d');
|
| 202 |
+
|
| 203 |
+
// Set up event listeners
|
| 204 |
+
recordButton.addEventListener('click', toggleRecording);
|
| 205 |
+
|
| 206 |
+
// Function to receive data from Streamlit
|
| 207 |
+
function onDataFromStreamlit(event) {
|
| 208 |
+
const data = event.data;
|
| 209 |
+
|
| 210 |
+
// Check if it's a message from Streamlit
|
| 211 |
+
if (data.type === 'streamlit:setComponentValue') {
|
| 212 |
+
const value = data.value;
|
| 213 |
+
|
| 214 |
+
// Check if the message contains target word data
|
| 215 |
+
if (value && value.targetWord) {
|
| 216 |
+
targetWord = value.targetWord;
|
| 217 |
+
console.log('Target word set:', targetWord);
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Listen for messages from Streamlit
|
| 223 |
+
window.addEventListener('message', onDataFromStreamlit);
|
| 224 |
+
|
| 225 |
+
// Toggle recording state
|
| 226 |
+
function toggleRecording() {
|
| 227 |
+
if (!isRecording) {
|
| 228 |
+
startRecording();
|
| 229 |
+
} else {
|
| 230 |
+
stopRecording();
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Initialize audio analysis
|
| 235 |
+
function initAudioAnalysis(stream) {
|
| 236 |
+
try {
|
| 237 |
+
// Create audio context
|
| 238 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 239 |
+
|
| 240 |
+
// Create analyser node
|
| 241 |
+
analyser = audioContext.createAnalyser();
|
| 242 |
+
analyser.fftSize = 2048;
|
| 243 |
+
analyser.smoothingTimeConstant = 0.8;
|
| 244 |
+
|
| 245 |
+
// Create script processor for detailed analysis
|
| 246 |
+
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
| 247 |
+
|
| 248 |
+
// Connect audio source to analyser and script processor
|
| 249 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 250 |
+
source.connect(analyser);
|
| 251 |
+
analyser.connect(scriptProcessor);
|
| 252 |
+
scriptProcessor.connect(audioContext.destination);
|
| 253 |
+
|
| 254 |
+
// Set up buffers for analysis
|
| 255 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 256 |
+
const timeDataArray = new Uint8Array(bufferLength);
|
| 257 |
+
const frequencyDataArray = new Uint8Array(bufferLength);
|
| 258 |
+
|
| 259 |
+
// Process audio data
|
| 260 |
+
scriptProcessor.onaudioprocess = function(audioProcessingEvent) {
|
| 261 |
+
if (!isRecording) return;
|
| 262 |
+
|
| 263 |
+
// Get time domain and frequency data
|
| 264 |
+
analyser.getByteTimeDomainData(timeDataArray);
|
| 265 |
+
analyser.getByteFrequencyData(frequencyDataArray);
|
| 266 |
+
|
| 267 |
+
// Calculate audio metrics
|
| 268 |
+
const metrics = calculateAudioMetrics(timeDataArray, frequencyDataArray);
|
| 269 |
+
|
| 270 |
+
// Update visualizations
|
| 271 |
+
updateVisualizations(timeDataArray, frequencyDataArray, metrics);
|
| 272 |
+
|
| 273 |
+
// Send data to Streamlit
|
| 274 |
+
sendRealtimeDataToStreamlit(metrics);
|
| 275 |
+
};
|
| 276 |
+
|
| 277 |
+
// Show visualization container
|
| 278 |
+
visualizationContainer.style.display = 'block';
|
| 279 |
+
|
| 280 |
+
return { analyser, scriptProcessor };
|
| 281 |
+
} catch (err) {
|
| 282 |
+
console.error('Error initializing audio analysis:', err);
|
| 283 |
+
return null;
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Enhance the calculateAudioMetrics function in the JavaScript code
|
| 288 |
+
function calculateAudioMetrics(timeDataArray, frequencyDataArray) {
|
| 289 |
+
// Calculate volume (amplitude)
|
| 290 |
+
let sumSquares = 0;
|
| 291 |
+
for (let i = 0; i < timeDataArray.length; i++) {
|
| 292 |
+
// Convert from 0-255 to -1 to 1
|
| 293 |
+
const amplitude = (timeDataArray[i] - 128) / 128;
|
| 294 |
+
sumSquares += amplitude * amplitude;
|
| 295 |
+
}
|
| 296 |
+
const rms = Math.sqrt(sumSquares / timeDataArray.length);
|
| 297 |
+
const volume = Math.min(100, Math.max(0, rms * 300)); // Scale to 0-100%
|
| 298 |
+
|
| 299 |
+
// Enhanced clarity calculation - analyze mid-frequency energy
|
| 300 |
+
const lowFreqSum = frequencyDataArray.slice(5, 20).reduce((a, b) => a + b, 0);
|
| 301 |
+
const midFreqSum = frequencyDataArray.slice(20, 80).reduce((a, b) => a + b, 0);
|
| 302 |
+
const highFreqSum = frequencyDataArray.slice(80, 200).reduce((a, b) => a + b, 0) + 1; // Add 1 to avoid division by zero
|
| 303 |
+
|
| 304 |
+
// Clarity is based on the balance of frequencies - speech needs good mid frequencies
|
| 305 |
+
const freqBalance = midFreqSum / (lowFreqSum + highFreqSum + 1);
|
| 306 |
+
const clarity = Math.min(100, Math.max(0, freqBalance * 200));
|
| 307 |
+
|
| 308 |
+
// Enhanced pitch calculation - estimate based on zero-crossings in time domain
|
| 309 |
+
let zeroCrossings = 0;
|
| 310 |
+
let prevSample = timeDataArray[0];
|
| 311 |
+
for (let i = 1; i < timeDataArray.length; i++) {
|
| 312 |
+
if ((prevSample < 128 && timeDataArray[i] >= 128) ||
|
| 313 |
+
(prevSample >= 128 && timeDataArray[i] < 128)) {
|
| 314 |
+
zeroCrossings++;
|
| 315 |
+
}
|
| 316 |
+
prevSample = timeDataArray[i];
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// Convert zero-crossings to a rough estimate of fundamental frequency
|
| 320 |
+
const sampleRate = 44100; // Typical sample rate
|
| 321 |
+
const audioLength = timeDataArray.length / sampleRate; // Length in seconds
|
| 322 |
+
const estimatedFreq = zeroCrossings / (2 * audioLength);
|
| 323 |
+
|
| 324 |
+
// For pitch accuracy, we compare to an expected range based on the target word
|
| 325 |
+
// This is a simplified approach - in a real app, you'd use pre-computed expected pitch for each word
|
| 326 |
+
let targetFreqMin, targetFreqMax;
|
| 327 |
+
if (targetWord) {
|
| 328 |
+
// Set expected frequency range based on word properties
|
| 329 |
+
// This is just an example - ideally this would come from a database of target words
|
| 330 |
+
const firstChar = targetWord.charAt(0).toLowerCase();
|
| 331 |
+
if ('aeiou'.includes(firstChar)) {
|
| 332 |
+
// Vowels typically have higher pitch
|
| 333 |
+
targetFreqMin = 180;
|
| 334 |
+
targetFreqMax = 300;
|
| 335 |
+
} else {
|
| 336 |
+
// Consonants typically have lower pitch
|
| 337 |
+
targetFreqMin = 120;
|
| 338 |
+
targetFreqMax = 250;
|
| 339 |
+
}
|
| 340 |
+
} else {
|
| 341 |
+
// Default range
|
| 342 |
+
targetFreqMin = 100;
|
| 343 |
+
targetFreqMax = 400;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Calculate pitch accuracy based on estimated frequency
|
| 347 |
+
let pitchAccuracy;
|
| 348 |
+
if (estimatedFreq < targetFreqMin) {
|
| 349 |
+
// Too low
|
| 350 |
+
pitchAccuracy = Math.max(30, 70 - (targetFreqMin - estimatedFreq) / 10);
|
| 351 |
+
} else if (estimatedFreq > targetFreqMax) {
|
| 352 |
+
// Too high
|
| 353 |
+
pitchAccuracy = Math.max(30, 70 - (estimatedFreq - targetFreqMax) / 10);
|
| 354 |
+
} else {
|
| 355 |
+
// Within range - calculate how close to ideal center
|
| 356 |
+
const idealCenter = (targetFreqMin + targetFreqMax) / 2;
|
| 357 |
+
const distanceFromIdeal = Math.abs(estimatedFreq - idealCenter);
|
| 358 |
+
const rangeHalfWidth = (targetFreqMax - targetFreqMin) / 2;
|
| 359 |
+
pitchAccuracy = 80 + 20 * (1 - distanceFromIdeal / rangeHalfWidth);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// Ensure values are in valid range
|
| 363 |
+
pitchAccuracy = Math.min(100, Math.max(0, pitchAccuracy));
|
| 364 |
+
|
| 365 |
+
// Generate more specific feedback based on metrics
|
| 366 |
+
let feedback = '';
|
| 367 |
+
|
| 368 |
+
if (volume < 20) {
|
| 369 |
+
feedback = 'Speak louder for better recognition';
|
| 370 |
+
} else if (volume > 85) {
|
| 371 |
+
feedback = 'Speaking too loudly, please lower your voice';
|
| 372 |
+
} else if (clarity < 40) {
|
| 373 |
+
feedback = 'Try to speak more clearly and articulate each sound';
|
| 374 |
+
} else if (pitchAccuracy < 50) {
|
| 375 |
+
if (estimatedFreq < targetFreqMin) {
|
| 376 |
+
feedback = 'Try speaking with a slightly higher pitch';
|
| 377 |
+
} else if (estimatedFreq > targetFreqMax) {
|
| 378 |
+
feedback = 'Try speaking with a slightly lower pitch';
|
| 379 |
+
} else {
|
| 380 |
+
feedback = 'Focus on the correct pronunciation';
|
| 381 |
+
}
|
| 382 |
+
} else if (volume >= 20 && clarity >= 40 && pitchAccuracy >= 60) {
|
| 383 |
+
feedback = 'Great pronunciation! Keep it up';
|
| 384 |
+
} else {
|
| 385 |
+
feedback = 'Continue speaking naturally';
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
return {
|
| 389 |
+
volume,
|
| 390 |
+
clarity,
|
| 391 |
+
pitchAccuracy,
|
| 392 |
+
feedback,
|
| 393 |
+
estimatedFrequency: estimatedFreq
|
| 394 |
+
};
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
// Update visual elements with metrics
|
| 398 |
+
function updateVisualizations(timeDataArray, frequencyDataArray, metrics) {
|
| 399 |
+
// Update meter bars
|
| 400 |
+
volumeBar.style.width = `${metrics.volume}%`;
|
| 401 |
+
clarityBar.style.width = `${metrics.clarity}%`;
|
| 402 |
+
pitchBar.style.width = `${metrics.pitchAccuracy}%`;
|
| 403 |
+
|
| 404 |
+
// Update numeric values
|
| 405 |
+
volumeValue.textContent = `${Math.round(metrics.volume)}%`;
|
| 406 |
+
clarityValue.textContent = `${Math.round(metrics.clarity)}%`;
|
| 407 |
+
pitchValue.textContent = `${Math.round(metrics.pitchAccuracy)}%`;
|
| 408 |
+
|
| 409 |
+
// Update feedback message
|
| 410 |
+
feedbackMessage.textContent = metrics.feedback;
|
| 411 |
+
|
| 412 |
+
// Color code the feedback based on overall quality
|
| 413 |
+
const overallQuality = (metrics.volume + metrics.clarity + metrics.pitchAccuracy) / 3;
|
| 414 |
+
if (overallQuality >= 70) {
|
| 415 |
+
feedbackMessage.style.backgroundColor = '#e8f5e9'; // Light green
|
| 416 |
+
feedbackMessage.style.color = '#2e7d32'; // Dark green
|
| 417 |
+
} else if (overallQuality >= 50) {
|
| 418 |
+
feedbackMessage.style.backgroundColor = '#fff8e1'; // Light amber
|
| 419 |
+
feedbackMessage.style.color = '#ff8f00'; // Dark amber
|
| 420 |
+
} else {
|
| 421 |
+
feedbackMessage.style.backgroundColor = '#ffebee'; // Light red
|
| 422 |
+
feedbackMessage.style.color = '#c62828'; // Dark red
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// Draw waveform
|
| 426 |
+
drawWaveform(timeDataArray);
|
| 427 |
+
|
| 428 |
+
// Draw spectrogram
|
| 429 |
+
drawSpectrogram(frequencyDataArray);
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// Draw waveform on canvas
|
| 433 |
+
function drawWaveform(timeDataArray) {
|
| 434 |
+
const width = waveformCanvas.width;
|
| 435 |
+
const height = waveformCanvas.height;
|
| 436 |
+
|
| 437 |
+
waveformCtx.fillStyle = '#f0f0f0';
|
| 438 |
+
waveformCtx.fillRect(0, 0, width, height);
|
| 439 |
+
|
| 440 |
+
waveformCtx.lineWidth = 2;
|
| 441 |
+
waveformCtx.strokeStyle = '#4CAF50';
|
| 442 |
+
waveformCtx.beginPath();
|
| 443 |
+
|
| 444 |
+
const sliceWidth = width / timeDataArray.length;
|
| 445 |
+
let x = 0;
|
| 446 |
+
|
| 447 |
+
for (let i = 0; i < timeDataArray.length; i++) {
|
| 448 |
+
const v = timeDataArray[i] / 128.0;
|
| 449 |
+
const y = v * height / 2;
|
| 450 |
+
|
| 451 |
+
if (i === 0) {
|
| 452 |
+
waveformCtx.moveTo(x, y);
|
| 453 |
+
} else {
|
| 454 |
+
waveformCtx.lineTo(x, y);
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
x += sliceWidth;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
waveformCtx.lineTo(width, height / 2);
|
| 461 |
+
waveformCtx.stroke();
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
// Draw spectrogram on canvas
|
| 465 |
+
function drawSpectrogram(frequencyDataArray) {
|
| 466 |
+
const width = spectrogramCanvas.width;
|
| 467 |
+
const height = spectrogramCanvas.height;
|
| 468 |
+
|
| 469 |
+
spectrogramCtx.fillStyle = '#f0f0f0';
|
| 470 |
+
spectrogramCtx.fillRect(0, 0, width, height);
|
| 471 |
+
|
| 472 |
+
// We'll use the first 256 frequency bins (or fewer if not available)
|
| 473 |
+
const numBins = Math.min(256, frequencyDataArray.length);
|
| 474 |
+
const barWidth = width / numBins;
|
| 475 |
+
|
| 476 |
+
for (let i = 0; i < numBins; i++) {
|
| 477 |
+
const value = frequencyDataArray[i];
|
| 478 |
+
const percent = value / 255;
|
| 479 |
+
const barHeight = height * percent;
|
| 480 |
+
const x = i * barWidth;
|
| 481 |
+
const y = height - barHeight;
|
| 482 |
+
|
| 483 |
+
// Create gradient color based on frequency
|
| 484 |
+
const hue = (i / numBins) * 240; // blue to red
|
| 485 |
+
spectrogramCtx.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
| 486 |
+
spectrogramCtx.fillRect(x, y, barWidth, barHeight);
|
| 487 |
+
}
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
// Send real-time data to Streamlit
|
| 491 |
+
function sendRealtimeDataToStreamlit(metrics) {
|
| 492 |
+
if (window.Streamlit) {
|
| 493 |
+
window.Streamlit.setComponentValue({
|
| 494 |
+
state: 'analyzing',
|
| 495 |
+
metrics: metrics,
|
| 496 |
+
timestamp: Date.now()
|
| 497 |
+
});
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// Start recording function
|
| 502 |
+
async function startRecording() {
|
| 503 |
+
audioChunks = [];
|
| 504 |
+
|
| 505 |
+
// Clear previous recording display
|
| 506 |
+
recordingDisplay.innerHTML = '';
|
| 507 |
+
|
| 508 |
+
try {
|
| 509 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 510 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 511 |
+
|
| 512 |
+
mediaRecorder.ondataavailable = (event) => {
|
| 513 |
+
audioChunks.push(event.data);
|
| 514 |
+
};
|
| 515 |
+
|
| 516 |
+
mediaRecorder.onstop = processRecording;
|
| 517 |
+
|
| 518 |
+
// Initialize audio analysis
|
| 519 |
+
const analysis = initAudioAnalysis(stream);
|
| 520 |
+
|
| 521 |
+
// Start recording
|
| 522 |
+
mediaRecorder.start();
|
| 523 |
+
isRecording = true;
|
| 524 |
+
recordButton.classList.add('recording');
|
| 525 |
+
buttonText.textContent = 'Stop Recording';
|
| 526 |
+
statusText.textContent = 'Recording in progress...';
|
| 527 |
+
} catch (err) {
|
| 528 |
+
console.error('Error accessing microphone:', err);
|
| 529 |
+
statusText.textContent = 'Error: Could not access microphone';
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// Stop recording function
|
| 534 |
+
function stopRecording() {
|
| 535 |
+
if (mediaRecorder && isRecording) {
|
| 536 |
+
mediaRecorder.stop();
|
| 537 |
+
isRecording = false;
|
| 538 |
+
recordButton.classList.remove('recording');
|
| 539 |
+
buttonText.textContent = 'Start Recording';
|
| 540 |
+
statusText.textContent = 'Processing recording...';
|
| 541 |
+
|
| 542 |
+
// Hide visualization container
|
| 543 |
+
visualizationContainer.style.display = 'none';
|
| 544 |
+
|
| 545 |
+
// Stop all tracks in the stream to release microphone
|
| 546 |
+
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
| 547 |
+
|
| 548 |
+
// Clean up audio processing
|
| 549 |
+
if (scriptProcessor) {
|
| 550 |
+
scriptProcessor.disconnect();
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
if (analyser) {
|
| 554 |
+
analyser.disconnect();
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
if (audioContext) {
|
| 558 |
+
audioContext.close().catch(console.error);
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Process the recording after stopping
|
| 564 |
+
function processRecording() {
|
| 565 |
+
audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 566 |
+
audioUrl = URL.createObjectURL(audioBlob);
|
| 567 |
+
|
| 568 |
+
// Create and display audio player
|
| 569 |
+
const audioPlayer = document.createElement('audio');
|
| 570 |
+
audioPlayer.controls = true;
|
| 571 |
+
audioPlayer.src = audioUrl;
|
| 572 |
+
audioPlayer.style.width = '100%';
|
| 573 |
+
audioPlayer.style.marginTop = '10px';
|
| 574 |
+
audioPlayer.style.borderRadius = '4px';
|
| 575 |
+
|
| 576 |
+
// Clear previous content and add the player
|
| 577 |
+
recordingDisplay.innerHTML = '';
|
| 578 |
+
recordingDisplay.appendChild(audioPlayer);
|
| 579 |
+
|
| 580 |
+
// Create download button
|
| 581 |
+
const downloadButton = document.createElement('button');
|
| 582 |
+
downloadButton.textContent = 'Download Recording';
|
| 583 |
+
downloadButton.style.marginTop = '10px';
|
| 584 |
+
downloadButton.style.padding = '5px 10px';
|
| 585 |
+
downloadButton.style.borderRadius = '4px';
|
| 586 |
+
downloadButton.style.backgroundColor = '#2196F3';
|
| 587 |
+
downloadButton.style.color = 'white';
|
| 588 |
+
downloadButton.style.border = 'none';
|
| 589 |
+
downloadButton.style.cursor = 'pointer';
|
| 590 |
+
|
| 591 |
+
downloadButton.addEventListener('click', () => {
|
| 592 |
+
const a = document.createElement('a');
|
| 593 |
+
a.href = audioUrl;
|
| 594 |
+
a.download = 'pronunciation-recording.wav';
|
| 595 |
+
a.click();
|
| 596 |
+
});
|
| 597 |
+
|
| 598 |
+
recordingDisplay.appendChild(downloadButton);
|
| 599 |
+
|
| 600 |
+
// Convert to base64 for passing to Streamlit
|
| 601 |
+
const reader = new FileReader();
|
| 602 |
+
reader.readAsDataURL(audioBlob);
|
| 603 |
+
reader.onloadend = function() {
|
| 604 |
+
const base64data = reader.result.split(',')[1];
|
| 605 |
+
// Send to Streamlit
|
| 606 |
+
sendToStreamlit(base64data);
|
| 607 |
+
statusText.textContent = 'Recording complete!';
|
| 608 |
+
};
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// Send data to Streamlit
|
| 612 |
+
function sendToStreamlit(base64AudioData) {
|
| 613 |
+
if (window.Streamlit) {
|
| 614 |
+
window.Streamlit.setComponentValue({
|
| 615 |
+
state: 'complete',
|
| 616 |
+
data: base64AudioData,
|
| 617 |
+
format: 'audio/wav'
|
| 618 |
+
});
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
// Initialize communication with Streamlit
|
| 623 |
+
if (window.Streamlit) {
|
| 624 |
+
window.Streamlit.componentReady();
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
// Add real-time visualization function
|
| 628 |
+
function setupVisualization() {
|
| 629 |
+
// Create visualization canvas if it doesn't exist
|
| 630 |
+
if (!document.getElementById('audio-visualizer')) {
|
| 631 |
+
const canvas = document.createElement('canvas');
|
| 632 |
+
canvas.id = 'audio-visualizer';
|
| 633 |
+
canvas.width = 300;
|
| 634 |
+
canvas.height = 60;
|
| 635 |
+
canvas.style.marginTop = '10px';
|
| 636 |
+
canvas.style.width = '100%';
|
| 637 |
+
canvas.style.borderRadius = '4px';
|
| 638 |
+
canvas.style.backgroundColor = '#f0f0f0';
|
| 639 |
+
|
| 640 |
+
// Add it to the container
|
| 641 |
+
recordingDisplay.appendChild(canvas);
|
| 642 |
+
|
| 643 |
+
// Add volume level indicator
|
| 644 |
+
const volumeIndicator = document.createElement('div');
|
| 645 |
+
volumeIndicator.id = 'volume-level';
|
| 646 |
+
volumeIndicator.style.width = '100%';
|
| 647 |
+
volumeIndicator.style.height = '4px';
|
| 648 |
+
volumeIndicator.style.backgroundColor = '#e0e0e0';
|
| 649 |
+
volumeIndicator.style.position = 'relative';
|
| 650 |
+
volumeIndicator.style.marginTop = '5px';
|
| 651 |
+
volumeIndicator.style.borderRadius = '2px';
|
| 652 |
+
|
| 653 |
+
const volumeMeter = document.createElement('div');
|
| 654 |
+
volumeMeter.id = 'volume-meter';
|
| 655 |
+
volumeMeter.style.height = '100%';
|
| 656 |
+
volumeMeter.style.width = '0%';
|
| 657 |
+
volumeMeter.style.backgroundColor = '#4CAF50';
|
| 658 |
+
volumeMeter.style.borderRadius = '2px';
|
| 659 |
+
volumeMeter.style.transition = 'width 0.1s ease';
|
| 660 |
+
|
| 661 |
+
volumeIndicator.appendChild(volumeMeter);
|
| 662 |
+
recordingDisplay.appendChild(volumeIndicator);
|
| 663 |
+
|
| 664 |
+
// Add feedback text area
|
| 665 |
+
const feedbackText = document.createElement('div');
|
| 666 |
+
feedbackText.id = 'feedback-text';
|
| 667 |
+
feedbackText.style.marginTop = '5px';
|
| 668 |
+
feedbackText.style.fontSize = '14px';
|
| 669 |
+
feedbackText.style.color = '#666';
|
| 670 |
+
feedbackText.style.textAlign = 'center';
|
| 671 |
+
feedbackText.textContent = 'Ready to record';
|
| 672 |
+
|
| 673 |
+
recordingDisplay.appendChild(feedbackText);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
return document.getElementById('audio-visualizer');
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
// Start audio visualization
|
| 680 |
+
function startVisualization(stream) {
|
| 681 |
+
const canvas = setupVisualization();
|
| 682 |
+
const canvasCtx = canvas.getContext('2d');
|
| 683 |
+
const volumeMeter = document.getElementById('volume-meter');
|
| 684 |
+
const feedbackText = document.getElementById('feedback-text');
|
| 685 |
+
|
| 686 |
+
// Set up audio context and analyzer
|
| 687 |
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 688 |
+
const analyser = audioContext.createAnalyser();
|
| 689 |
+
analyser.fftSize = 256;
|
| 690 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 691 |
+
const dataArray = new Uint8Array(bufferLength);
|
| 692 |
+
|
| 693 |
+
// Connect the audio stream
|
| 694 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 695 |
+
source.connect(analyser);
|
| 696 |
+
|
| 697 |
+
// Visualization function
|
| 698 |
+
function draw() {
|
| 699 |
+
if (!isRecording) return;
|
| 700 |
+
|
| 701 |
+
requestAnimationFrame(draw);
|
| 702 |
+
|
| 703 |
+
// Get frequency data
|
| 704 |
+
analyser.getByteFrequencyData(dataArray);
|
| 705 |
+
|
| 706 |
+
// Calculate volume level (0-100)
|
| 707 |
+
let sum = 0;
|
| 708 |
+
for(let i = 0; i < bufferLength; i++) {
|
| 709 |
+
sum += dataArray[i];
|
| 710 |
+
}
|
| 711 |
+
const average = sum / bufferLength;
|
| 712 |
+
const volumePercentage = (average / 255) * 100;
|
| 713 |
+
|
| 714 |
+
// Update volume meter
|
| 715 |
+
volumeMeter.style.width = `${volumePercentage}%`;
|
| 716 |
+
|
| 717 |
+
// Update feedback text based on volume
|
| 718 |
+
if (volumePercentage < 5) {
|
| 719 |
+
feedbackText.textContent = 'Speak louder...';
|
| 720 |
+
feedbackText.style.color = '#F44336';
|
| 721 |
+
} else if (volumePercentage > 80) {
|
| 722 |
+
feedbackText.textContent = 'Too loud!';
|
| 723 |
+
feedbackText.style.color = '#F44336';
|
| 724 |
+
} else if (volumePercentage > 40) {
|
| 725 |
+
feedbackText.textContent = 'Good volume!';
|
| 726 |
+
feedbackText.style.color = '#4CAF50';
|
| 727 |
+
} else {
|
| 728 |
+
feedbackText.textContent = 'Speak a bit louder';
|
| 729 |
+
feedbackText.style.color = '#FFC107';
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// Draw visualization
|
| 733 |
+
canvasCtx.fillStyle = 'rgb(240, 240, 240)';
|
| 734 |
+
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
| 735 |
+
|
| 736 |
+
const barWidth = (canvas.width / bufferLength) * 2.5;
|
| 737 |
+
let x = 0;
|
| 738 |
+
|
| 739 |
+
for(let i = 0; i < bufferLength; i++) {
|
| 740 |
+
const barHeight = (dataArray[i] / 255) * canvas.height;
|
| 741 |
+
|
| 742 |
+
// Create gradient color based on frequency
|
| 743 |
+
const hue = i / bufferLength * 240;
|
| 744 |
+
canvasCtx.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
| 745 |
+
|
| 746 |
+
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
| 747 |
+
x += barWidth + 1;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
// Send data to Streamlit
|
| 751 |
+
if (window.Streamlit && isRecording) {
|
| 752 |
+
window.Streamlit.setComponentValue({
|
| 753 |
+
status: 'recording',
|
| 754 |
+
volume: volumePercentage,
|
| 755 |
+
frequencyData: Array.from(dataArray).slice(0, 10) // Send just first 10 values
|
| 756 |
+
});
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// Start visualization
|
| 761 |
+
draw();
|
| 762 |
+
|
| 763 |
+
// Save audio context for cleanup
|
| 764 |
+
window.audioContext = audioContext;
|
| 765 |
+
|
| 766 |
+
return analyser;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
// Modify startRecording to include visualization
|
| 770 |
+
async function startRecording() {
|
| 771 |
+
audioChunks = [];
|
| 772 |
+
|
| 773 |
+
// Clear previous recording display
|
| 774 |
+
recordingDisplay.innerHTML = '';
|
| 775 |
+
|
| 776 |
+
try {
|
| 777 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 778 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 779 |
+
|
| 780 |
+
mediaRecorder.ondataavailable = (event) => {
|
| 781 |
+
audioChunks.push(event.data);
|
| 782 |
+
};
|
| 783 |
+
|
| 784 |
+
mediaRecorder.onstop = processRecording;
|
| 785 |
+
|
| 786 |
+
// Start recording
|
| 787 |
+
mediaRecorder.start();
|
| 788 |
+
isRecording = true;
|
| 789 |
+
recordButton.classList.add('recording');
|
| 790 |
+
buttonText.textContent = 'Stop Recording';
|
| 791 |
+
statusText.textContent = 'Recording in progress...';
|
| 792 |
+
|
| 793 |
+
// Start visualization
|
| 794 |
+
startVisualization(stream);
|
| 795 |
+
} catch (err) {
|
| 796 |
+
console.error('Error accessing microphone:', err);
|
| 797 |
+
statusText.textContent = 'Error: Could not access microphone';
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
// Modify stopRecording to clean up
|
| 802 |
+
function stopRecording() {
|
| 803 |
+
if (mediaRecorder && isRecording) {
|
| 804 |
+
mediaRecorder.stop();
|
| 805 |
+
isRecording = false;
|
| 806 |
+
recordButton.classList.remove('recording');
|
| 807 |
+
buttonText.textContent = 'Start Recording';
|
| 808 |
+
statusText.textContent = 'Processing recording...';
|
| 809 |
+
|
| 810 |
+
// Stop all tracks in the stream to release microphone
|
| 811 |
+
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
| 812 |
+
|
| 813 |
+
// Clean up audio context
|
| 814 |
+
if (window.audioContext && window.audioContext.state !== 'closed') {
|
| 815 |
+
window.audioContext.close().catch(console.error);
|
| 816 |
+
}
|
| 817 |
+
}
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
// Add real-time visualization function
|
| 822 |
+
function setupVisualization() {
|
| 823 |
+
// Create visualization canvas if it doesn't exist
|
| 824 |
+
if (!document.getElementById('audio-visualizer')) {
|
| 825 |
+
const canvas = document.createElement('canvas');
|
| 826 |
+
canvas.id = 'audio-visualizer';
|
| 827 |
+
canvas.width = 300;
|
| 828 |
+
canvas.height = 60;
|
| 829 |
+
canvas.style.marginTop = '10px';
|
| 830 |
+
canvas.style.width = '100%';
|
| 831 |
+
canvas.style.borderRadius = '4px';
|
| 832 |
+
canvas.style.backgroundColor = '#f0f0f0';
|
| 833 |
+
|
| 834 |
+
// Add it to the container
|
| 835 |
+
recordingDisplay.appendChild(canvas);
|
| 836 |
+
|
| 837 |
+
// Add volume level indicator
|
| 838 |
+
const volumeIndicator = document.createElement('div');
|
| 839 |
+
volumeIndicator.id = 'volume-level';
|
| 840 |
+
volumeIndicator.style.width = '100%';
|
| 841 |
+
volumeIndicator.style.height = '4px';
|
| 842 |
+
volumeIndicator.style.backgroundColor = '#e0e0e0';
|
| 843 |
+
volumeIndicator.style.position = 'relative';
|
| 844 |
+
volumeIndicator.style.marginTop = '5px';
|
| 845 |
+
volumeIndicator.style.borderRadius = '2px';
|
| 846 |
+
|
| 847 |
+
const volumeMeter = document.createElement('div');
|
| 848 |
+
volumeMeter.id = 'volume-meter';
|
| 849 |
+
volumeMeter.style.height = '100%';
|
| 850 |
+
volumeMeter.style.width = '0%';
|
| 851 |
+
volumeMeter.style.backgroundColor = '#4CAF50';
|
| 852 |
+
volumeMeter.style.borderRadius = '2px';
|
| 853 |
+
volumeMeter.style.transition = 'width 0.1s ease';
|
| 854 |
+
|
| 855 |
+
volumeIndicator.appendChild(volumeMeter);
|
| 856 |
+
recordingDisplay.appendChild(volumeIndicator);
|
| 857 |
+
|
| 858 |
+
// Add feedback text area
|
| 859 |
+
const feedbackText = document.createElement('div');
|
| 860 |
+
feedbackText.id = 'feedback-text';
|
| 861 |
+
feedbackText.style.marginTop = '5px';
|
| 862 |
+
feedbackText.style.fontSize = '14px';
|
| 863 |
+
feedbackText.style.color = '#666';
|
| 864 |
+
feedbackText.style.textAlign = 'center';
|
| 865 |
+
feedbackText.textContent = 'Ready to record';
|
| 866 |
+
|
| 867 |
+
recordingDisplay.appendChild(feedbackText);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
return document.getElementById('audio-visualizer');
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
// Start audio visualization
|
| 874 |
+
function startVisualization(stream) {
|
| 875 |
+
const canvas = setupVisualization();
|
| 876 |
+
const canvasCtx = canvas.getContext('2d');
|
| 877 |
+
const volumeMeter = document.getElementById('volume-meter');
|
| 878 |
+
const feedbackText = document.getElementById('feedback-text');
|
| 879 |
+
|
| 880 |
+
// Set up audio context and analyzer
|
| 881 |
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 882 |
+
const analyser = audioContext.createAnalyser();
|
| 883 |
+
analyser.fftSize = 256;
|
| 884 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 885 |
+
const dataArray = new Uint8Array(bufferLength);
|
| 886 |
+
|
| 887 |
+
// Connect the audio stream
|
| 888 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 889 |
+
source.connect(analyser);
|
| 890 |
+
|
| 891 |
+
// Visualization function
|
| 892 |
+
function draw() {
|
| 893 |
+
if (!isRecording) return;
|
| 894 |
+
|
| 895 |
+
requestAnimationFrame(draw);
|
| 896 |
+
|
| 897 |
+
// Get frequency data
|
| 898 |
+
analyser.getByteFrequencyData(dataArray);
|
| 899 |
+
|
| 900 |
+
// Calculate volume level (0-100)
|
| 901 |
+
let sum = 0;
|
| 902 |
+
for(let i = 0; i < bufferLength; i++) {
|
| 903 |
+
sum += dataArray[i];
|
| 904 |
+
}
|
| 905 |
+
const average = sum / bufferLength;
|
| 906 |
+
const volumePercentage = (average / 255) * 100;
|
| 907 |
+
|
| 908 |
+
// Update volume meter
|
| 909 |
+
volumeMeter.style.width = `${volumePercentage}%`;
|
| 910 |
+
|
| 911 |
+
// Update feedback text based on volume
|
| 912 |
+
if (volumePercentage < 5) {
|
| 913 |
+
feedbackText.textContent = 'Speak louder...';
|
| 914 |
+
feedbackText.style.color = '#F44336';
|
| 915 |
+
} else if (volumePercentage > 80) {
|
| 916 |
+
feedbackText.textContent = 'Too loud!';
|
| 917 |
+
feedbackText.style.color = '#F44336';
|
| 918 |
+
} else if (volumePercentage > 40) {
|
| 919 |
+
feedbackText.textContent = 'Good volume!';
|
| 920 |
+
feedbackText.style.color = '#4CAF50';
|
| 921 |
+
} else {
|
| 922 |
+
feedbackText.textContent = 'Speak a bit louder';
|
| 923 |
+
feedbackText.style.color = '#FFC107';
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
// Draw visualization
|
| 927 |
+
canvasCtx.fillStyle = 'rgb(240, 240, 240)';
|
| 928 |
+
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
| 929 |
+
|
| 930 |
+
const barWidth = (canvas.width / bufferLength) * 2.5;
|
| 931 |
+
let x = 0;
|
| 932 |
+
|
| 933 |
+
for(let i = 0; i < bufferLength; i++) {
|
| 934 |
+
const barHeight = (dataArray[i] / 255) * canvas.height;
|
| 935 |
+
|
| 936 |
+
// Create gradient color based on frequency
|
| 937 |
+
const hue = i / bufferLength * 240;
|
| 938 |
+
canvasCtx.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
| 939 |
+
|
| 940 |
+
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
| 941 |
+
x += barWidth + 1;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
// Send data to Streamlit
|
| 945 |
+
if (window.Streamlit && isRecording) {
|
| 946 |
+
window.Streamlit.setComponentValue({
|
| 947 |
+
status: 'recording',
|
| 948 |
+
volume: volumePercentage,
|
| 949 |
+
frequencyData: Array.from(dataArray).slice(0, 10) // Send just first 10 values
|
| 950 |
+
});
|
| 951 |
+
}
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
// Start visualization
|
| 955 |
+
draw();
|
| 956 |
+
|
| 957 |
+
// Save audio context for cleanup
|
| 958 |
+
window.audioContext = audioContext;
|
| 959 |
+
|
| 960 |
+
return analyser;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
// Modify startRecording to include visualization
|
| 964 |
+
async function startRecording() {
|
| 965 |
+
audioChunks = [];
|
| 966 |
+
|
| 967 |
+
// Clear previous recording display
|
| 968 |
+
recordingDisplay.innerHTML = '';
|
| 969 |
+
|
| 970 |
+
try {
|
| 971 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 972 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 973 |
+
|
| 974 |
+
mediaRecorder.ondataavailable = (event) => {
|
| 975 |
+
audioChunks.push(event.data);
|
| 976 |
+
};
|
| 977 |
+
|
| 978 |
+
mediaRecorder.onstop = processRecording;
|
| 979 |
+
|
| 980 |
+
// Start recording
|
| 981 |
+
mediaRecorder.start();
|
| 982 |
+
isRecording = true;
|
| 983 |
+
recordButton.classList.add('recording');
|
| 984 |
+
buttonText.textContent = 'Stop Recording';
|
| 985 |
+
statusText.textContent = 'Recording in progress...';
|
| 986 |
+
|
| 987 |
+
// Start visualization
|
| 988 |
+
startVisualization(stream);
|
| 989 |
+
} catch (err) {
|
| 990 |
+
console.error('Error accessing microphone:', err);
|
| 991 |
+
statusText.textContent = 'Error: Could not access microphone';
|
| 992 |
+
}
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
// Modify stopRecording to clean up
|
| 996 |
+
function stopRecording() {
|
| 997 |
+
if (mediaRecorder && isRecording) {
|
| 998 |
+
mediaRecorder.stop();
|
| 999 |
+
isRecording = false;
|
| 1000 |
+
recordButton.classList.remove('recording');
|
| 1001 |
+
buttonText.textContent = 'Start Recording';
|
| 1002 |
+
statusText.textContent = 'Processing recording...';
|
| 1003 |
+
|
| 1004 |
+
// Stop all tracks in the stream to release microphone
|
| 1005 |
+
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
| 1006 |
+
|
| 1007 |
+
// Clean up audio context
|
| 1008 |
+
if (window.audioContext && window.audioContext.state !== 'closed') {
|
| 1009 |
+
window.audioContext.close().catch(console.error);
|
| 1010 |
+
}
|
| 1011 |
+
}
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
</script>
|
| 1015 |
+
</body>
|
| 1016 |
+
</html>
|
| 1017 |
+
""")
|
| 1018 |
+
|
| 1019 |
+
# Create the custom component function
|
| 1020 |
+
def audio_recorder():
|
| 1021 |
+
"""Custom audio recorder component with JavaScript"""
|
| 1022 |
+
try:
|
| 1023 |
+
# Get the component value
|
| 1024 |
+
component_value = components.html(
|
| 1025 |
+
open(HTML_FILE, "r").read(),
|
| 1026 |
+
height=200
|
| 1027 |
+
)
|
| 1028 |
+
|
| 1029 |
+
# Process the returned value
|
| 1030 |
+
if component_value and isinstance(component_value, dict) and 'data' in component_value:
|
| 1031 |
+
# Decode the base64 audio data
|
| 1032 |
+
audio_bytes = base64.b64decode(component_value['data'])
|
| 1033 |
+
|
| 1034 |
+
# Store in session state for persistence
|
| 1035 |
+
st.session_state.audio_data = audio_bytes
|
| 1036 |
+
st.session_state.audio_data_received = True
|
| 1037 |
+
|
| 1038 |
+
# Display the audio player in Streamlit
|
| 1039 |
+
audio_container = st.container()
|
| 1040 |
+
with audio_container:
|
| 1041 |
+
st.audio(audio_bytes, format="audio/wav")
|
| 1042 |
+
st.success("Recording saved! You can replay it using the player above.")
|
| 1043 |
+
|
| 1044 |
+
# Return the audio bytes
|
| 1045 |
+
return audio_bytes
|
| 1046 |
+
|
| 1047 |
+
# If we already have audio data in session state, display it
|
| 1048 |
+
elif 'audio_data' in st.session_state and st.session_state.audio_data_received:
|
| 1049 |
+
st.audio(st.session_state.audio_data, format="audio/wav")
|
| 1050 |
+
st.success("Previous recording available. You can replay it or record a new one.")
|
| 1051 |
+
return st.session_state.audio_data
|
| 1052 |
+
|
| 1053 |
+
return None
|
| 1054 |
+
except Exception as e:
|
| 1055 |
+
st.error(f"Error in audio recorder component: {e}")
|
| 1056 |
+
st.exception(e) # Show full exception for debugging
|
| 1057 |
+
return None
|
database.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
import datetime
|
| 4 |
+
|
| 5 |
+
class LanguageLearningDB:
|
| 6 |
+
def __init__(self, db_path):
|
| 7 |
+
"""Initialize the database connection."""
|
| 8 |
+
self.db_path = db_path
|
| 9 |
+
self.conn = sqlite3.connect(db_path)
|
| 10 |
+
self.conn.row_factory = sqlite3.Row
|
| 11 |
+
self.cursor = self.conn.cursor()
|
| 12 |
+
|
| 13 |
+
# Create tables if they don't exist
|
| 14 |
+
self._create_tables()
|
| 15 |
+
|
| 16 |
+
def _create_tables(self):
|
| 17 |
+
"""Create necessary database tables if they don't exist."""
|
| 18 |
+
self.cursor.executescript('''
|
| 19 |
+
CREATE TABLE IF NOT EXISTS vocabulary (
|
| 20 |
+
id INTEGER PRIMARY KEY,
|
| 21 |
+
word_original TEXT NOT NULL,
|
| 22 |
+
word_translated TEXT NOT NULL,
|
| 23 |
+
language_translated TEXT NOT NULL,
|
| 24 |
+
category TEXT,
|
| 25 |
+
image_path TEXT,
|
| 26 |
+
date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 27 |
+
source TEXT DEFAULT 'manual'
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
CREATE TABLE IF NOT EXISTS user_progress (
|
| 31 |
+
id INTEGER PRIMARY KEY,
|
| 32 |
+
vocabulary_id INTEGER,
|
| 33 |
+
review_count INTEGER DEFAULT 0,
|
| 34 |
+
correct_count INTEGER DEFAULT 0,
|
| 35 |
+
last_reviewed TIMESTAMP,
|
| 36 |
+
proficiency_level INTEGER DEFAULT 0,
|
| 37 |
+
FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 41 |
+
id INTEGER PRIMARY KEY,
|
| 42 |
+
start_time TIMESTAMP,
|
| 43 |
+
end_time TIMESTAMP,
|
| 44 |
+
words_studied INTEGER DEFAULT 0,
|
| 45 |
+
words_learned INTEGER DEFAULT 0
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
CREATE TABLE IF NOT EXISTS camera_translations (
|
| 49 |
+
id INTEGER PRIMARY KEY,
|
| 50 |
+
image_path TEXT,
|
| 51 |
+
detected_text TEXT,
|
| 52 |
+
translated_text TEXT,
|
| 53 |
+
source_language TEXT,
|
| 54 |
+
target_language TEXT,
|
| 55 |
+
date_captured TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 56 |
+
is_saved_to_vocabulary BOOLEAN DEFAULT 0
|
| 57 |
+
);
|
| 58 |
+
''')
|
| 59 |
+
self.conn.commit()
|
| 60 |
+
|
| 61 |
+
def add_vocabulary(self, word_original, word_translated, language_translated, category=None, image_path=None, source='manual'):
|
| 62 |
+
"""Add a new vocabulary entry to the database."""
|
| 63 |
+
try:
|
| 64 |
+
vocab_id = self.cursor.execute('''
|
| 65 |
+
INSERT INTO vocabulary
|
| 66 |
+
(word_original, word_translated, language_translated, category, image_path, source)
|
| 67 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 68 |
+
''', (word_original, word_translated, language_translated, category, image_path, source)).lastrowid
|
| 69 |
+
|
| 70 |
+
# Initialize user progress for this vocabulary
|
| 71 |
+
self.cursor.execute('''
|
| 72 |
+
INSERT INTO user_progress (vocabulary_id, last_reviewed)
|
| 73 |
+
VALUES (?, ?)
|
| 74 |
+
''', (vocab_id, datetime.datetime.now()))
|
| 75 |
+
|
| 76 |
+
self.conn.commit()
|
| 77 |
+
return vocab_id
|
| 78 |
+
except sqlite3.Error as e:
|
| 79 |
+
print(f"Error adding vocabulary: {e}")
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def get_vocabulary(self, vocabulary_id):
|
| 83 |
+
"""Get a specific vocabulary entry by ID."""
|
| 84 |
+
try:
|
| 85 |
+
self.cursor.execute('''
|
| 86 |
+
SELECT * FROM vocabulary WHERE id = ?
|
| 87 |
+
''', (vocabulary_id,))
|
| 88 |
+
|
| 89 |
+
return self.cursor.fetchone()
|
| 90 |
+
except sqlite3.Error as e:
|
| 91 |
+
print(f"Error getting vocabulary: {e}")
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
def get_all_vocabulary(self, category=None, language=None):
|
| 95 |
+
"""Get all vocabulary entries, optionally filtered by category and/or language."""
|
| 96 |
+
try:
|
| 97 |
+
query = "SELECT * FROM vocabulary"
|
| 98 |
+
params = []
|
| 99 |
+
|
| 100 |
+
if category and language:
|
| 101 |
+
query += " WHERE category = ? AND language_translated = ?"
|
| 102 |
+
params = [category, language]
|
| 103 |
+
elif category:
|
| 104 |
+
query += " WHERE category = ?"
|
| 105 |
+
params = [category]
|
| 106 |
+
elif language:
|
| 107 |
+
query += " WHERE language_translated = ?"
|
| 108 |
+
params = [language]
|
| 109 |
+
|
| 110 |
+
query += " ORDER BY date_added DESC"
|
| 111 |
+
|
| 112 |
+
self.cursor.execute(query, params)
|
| 113 |
+
return self.cursor.fetchall()
|
| 114 |
+
except sqlite3.Error as e:
|
| 115 |
+
print(f"Error getting vocabulary: {e}")
|
| 116 |
+
return []
|
| 117 |
+
|
| 118 |
+
def update_vocabulary(self, vocabulary_id, word_original=None, word_translated=None,
|
| 119 |
+
language_translated=None, category=None, image_path=None):
|
| 120 |
+
"""Update an existing vocabulary entry."""
|
| 121 |
+
try:
|
| 122 |
+
# Get current values first
|
| 123 |
+
current = self.get_vocabulary(vocabulary_id)
|
| 124 |
+
if not current:
|
| 125 |
+
return False
|
| 126 |
+
|
| 127 |
+
# Use current values for any parameter not provided
|
| 128 |
+
word_original = word_original if word_original is not None else current['word_original']
|
| 129 |
+
word_translated = word_translated if word_translated is not None else current['word_translated']
|
| 130 |
+
language_translated = language_translated if language_translated is not None else current['language_translated']
|
| 131 |
+
category = category if category is not None else current['category']
|
| 132 |
+
image_path = image_path if image_path is not None else current['image_path']
|
| 133 |
+
|
| 134 |
+
self.cursor.execute('''
|
| 135 |
+
UPDATE vocabulary
|
| 136 |
+
SET word_original = ?, word_translated = ?,
|
| 137 |
+
language_translated = ?, category = ?, image_path = ?
|
| 138 |
+
WHERE id = ?
|
| 139 |
+
''', (word_original, word_translated, language_translated,
|
| 140 |
+
category, image_path, vocabulary_id))
|
| 141 |
+
|
| 142 |
+
self.conn.commit()
|
| 143 |
+
return True
|
| 144 |
+
except sqlite3.Error as e:
|
| 145 |
+
print(f"Error updating vocabulary: {e}")
|
| 146 |
+
return False
|
| 147 |
+
|
| 148 |
+
def delete_vocabulary(self, vocabulary_id):
|
| 149 |
+
"""Delete a vocabulary entry by ID and associated progress."""
|
| 150 |
+
try:
|
| 151 |
+
# Delete associated progress first (due to foreign key constraint)
|
| 152 |
+
self.cursor.execute('''
|
| 153 |
+
DELETE FROM user_progress WHERE vocabulary_id = ?
|
| 154 |
+
''', (vocabulary_id,))
|
| 155 |
+
|
| 156 |
+
# Delete vocabulary entry
|
| 157 |
+
self.cursor.execute('''
|
| 158 |
+
DELETE FROM vocabulary WHERE id = ?
|
| 159 |
+
''', (vocabulary_id,))
|
| 160 |
+
|
| 161 |
+
self.conn.commit()
|
| 162 |
+
return True
|
| 163 |
+
except sqlite3.Error as e:
|
| 164 |
+
print(f"Error deleting vocabulary: {e}")
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
def search_vocabulary(self, search_term, language=None):
|
| 168 |
+
"""Search vocabulary entries for a term in original or translated word."""
|
| 169 |
+
try:
|
| 170 |
+
query = '''
|
| 171 |
+
SELECT * FROM vocabulary
|
| 172 |
+
WHERE (word_original LIKE ? OR word_translated LIKE ?)
|
| 173 |
+
'''
|
| 174 |
+
params = [f'%{search_term}%', f'%{search_term}%']
|
| 175 |
+
|
| 176 |
+
if language:
|
| 177 |
+
query += " AND language_translated = ?"
|
| 178 |
+
params.append(language)
|
| 179 |
+
|
| 180 |
+
self.cursor.execute(query, params)
|
| 181 |
+
return self.cursor.fetchall()
|
| 182 |
+
except sqlite3.Error as e:
|
| 183 |
+
print(f"Error searching vocabulary: {e}")
|
| 184 |
+
return []
|
| 185 |
+
|
| 186 |
+
def update_word_progress(self, vocabulary_id, is_correct):
|
| 187 |
+
"""Update the progress for a vocabulary word after review."""
|
| 188 |
+
try:
|
| 189 |
+
# Get current progress
|
| 190 |
+
self.cursor.execute('''
|
| 191 |
+
SELECT * FROM user_progress WHERE vocabulary_id = ?
|
| 192 |
+
''', (vocabulary_id,))
|
| 193 |
+
|
| 194 |
+
progress = self.cursor.fetchone()
|
| 195 |
+
if not progress:
|
| 196 |
+
# Create new progress entry if it doesn't exist
|
| 197 |
+
self.cursor.execute('''
|
| 198 |
+
INSERT INTO user_progress
|
| 199 |
+
(vocabulary_id, review_count, correct_count, last_reviewed, proficiency_level)
|
| 200 |
+
VALUES (?, ?, ?, ?, ?)
|
| 201 |
+
''', (vocabulary_id, 1, 1 if is_correct else 0, datetime.datetime.now(), 0))
|
| 202 |
+
else:
|
| 203 |
+
# Update existing progress
|
| 204 |
+
review_count = progress['review_count'] + 1
|
| 205 |
+
correct_count = progress['correct_count'] + (1 if is_correct else 0)
|
| 206 |
+
|
| 207 |
+
# Calculate new proficiency level (0-5)
|
| 208 |
+
# Simple algorithm: proficiency is percentage of correct answers, mapped to 0-5 scale
|
| 209 |
+
proficiency = min(5, int((correct_count / review_count) * 5))
|
| 210 |
+
|
| 211 |
+
self.cursor.execute('''
|
| 212 |
+
UPDATE user_progress
|
| 213 |
+
SET review_count = ?, correct_count = ?,
|
| 214 |
+
last_reviewed = ?, proficiency_level = ?
|
| 215 |
+
WHERE vocabulary_id = ?
|
| 216 |
+
''', (review_count, correct_count, datetime.datetime.now(),
|
| 217 |
+
proficiency, vocabulary_id))
|
| 218 |
+
|
| 219 |
+
self.conn.commit()
|
| 220 |
+
return True
|
| 221 |
+
except sqlite3.Error as e:
|
| 222 |
+
print(f"Error updating word progress: {e}")
|
| 223 |
+
return False
|
| 224 |
+
|
| 225 |
+
def get_word_progress(self, vocabulary_id):
|
| 226 |
+
"""Get the progress for a specific vocabulary word."""
|
| 227 |
+
try:
|
| 228 |
+
self.cursor.execute('''
|
| 229 |
+
SELECT * FROM user_progress WHERE vocabulary_id = ?
|
| 230 |
+
''', (vocabulary_id,))
|
| 231 |
+
|
| 232 |
+
return self.cursor.fetchone()
|
| 233 |
+
except sqlite3.Error as e:
|
| 234 |
+
print(f"Error getting word progress: {e}")
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
def get_words_for_review(self, limit=10, min_proficiency=None, max_proficiency=None):
|
| 238 |
+
"""Get words for review based on proficiency level."""
|
| 239 |
+
try:
|
| 240 |
+
query = '''
|
| 241 |
+
SELECT v.*, p.proficiency_level, p.last_reviewed, p.review_count, p.correct_count
|
| 242 |
+
FROM vocabulary v
|
| 243 |
+
JOIN user_progress p ON v.id = p.vocabulary_id
|
| 244 |
+
WHERE 1=1
|
| 245 |
+
'''
|
| 246 |
+
params = []
|
| 247 |
+
|
| 248 |
+
if min_proficiency is not None:
|
| 249 |
+
query += " AND p.proficiency_level >= ?"
|
| 250 |
+
params.append(min_proficiency)
|
| 251 |
+
|
| 252 |
+
if max_proficiency is not None:
|
| 253 |
+
query += " AND p.proficiency_level <= ?"
|
| 254 |
+
params.append(max_proficiency)
|
| 255 |
+
|
| 256 |
+
# Order by last reviewed (oldest first) and proficiency level (lowest first)
|
| 257 |
+
query += " ORDER BY p.last_reviewed ASC, p.proficiency_level ASC LIMIT ?"
|
| 258 |
+
params.append(limit)
|
| 259 |
+
|
| 260 |
+
self.cursor.execute(query, params)
|
| 261 |
+
return self.cursor.fetchall()
|
| 262 |
+
except sqlite3.Error as e:
|
| 263 |
+
print(f"Error getting words for review: {e}")
|
| 264 |
+
return []
|
| 265 |
+
|
| 266 |
+
def start_session(self):
|
| 267 |
+
"""Start a new learning session."""
|
| 268 |
+
try:
|
| 269 |
+
session_id = self.cursor.execute('''
|
| 270 |
+
INSERT INTO sessions (start_time, words_studied, words_learned)
|
| 271 |
+
VALUES (?, 0, 0)
|
| 272 |
+
''', (datetime.datetime.now(),)).lastrowid
|
| 273 |
+
|
| 274 |
+
self.conn.commit()
|
| 275 |
+
return session_id
|
| 276 |
+
except sqlite3.Error as e:
|
| 277 |
+
print(f"Error starting session: {e}")
|
| 278 |
+
return None
|
| 279 |
+
|
| 280 |
+
def end_session(self, session_id, words_studied, words_learned):
|
| 281 |
+
"""End a learning session with statistics."""
|
| 282 |
+
try:
|
| 283 |
+
self.cursor.execute('''
|
| 284 |
+
UPDATE sessions
|
| 285 |
+
SET end_time = ?, words_studied = ?, words_learned = ?
|
| 286 |
+
WHERE id = ?
|
| 287 |
+
''', (datetime.datetime.now(), words_studied, words_learned, session_id))
|
| 288 |
+
|
| 289 |
+
self.conn.commit()
|
| 290 |
+
return True
|
| 291 |
+
except sqlite3.Error as e:
|
| 292 |
+
print(f"Error ending session: {e}")
|
| 293 |
+
return False
|
| 294 |
+
|
| 295 |
+
def get_session_stats(self, days=30):
|
| 296 |
+
"""Get statistics from sessions in the last N days."""
|
| 297 |
+
try:
|
| 298 |
+
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=days)
|
| 299 |
+
|
| 300 |
+
self.cursor.execute('''
|
| 301 |
+
SELECT
|
| 302 |
+
COUNT(*) as total_sessions,
|
| 303 |
+
SUM(words_studied) as total_words_studied,
|
| 304 |
+
SUM(words_learned) as total_words_learned,
|
| 305 |
+
AVG(words_studied) as avg_words_per_session,
|
| 306 |
+
AVG(words_learned) as avg_learned_per_session,
|
| 307 |
+
SUM(CAST(strftime('%s', end_time) - strftime('%s', start_time) AS REAL)) / 60 as total_minutes
|
| 308 |
+
FROM sessions
|
| 309 |
+
WHERE start_time >= ?
|
| 310 |
+
''', (cutoff_date,))
|
| 311 |
+
|
| 312 |
+
return self.cursor.fetchone()
|
| 313 |
+
except sqlite3.Error as e:
|
| 314 |
+
print(f"Error getting session stats: {e}")
|
| 315 |
+
return None
|
| 316 |
+
|
| 317 |
+
def save_camera_translation(self, image_path, detected_text, translated_text,
|
| 318 |
+
source_language, target_language):
|
| 319 |
+
"""Save a translation from camera capture."""
|
| 320 |
+
try:
|
| 321 |
+
translation_id = self.cursor.execute('''
|
| 322 |
+
INSERT INTO camera_translations
|
| 323 |
+
(image_path, detected_text, translated_text, source_language, target_language)
|
| 324 |
+
VALUES (?, ?, ?, ?, ?)
|
| 325 |
+
''', (image_path, detected_text, translated_text, source_language, target_language)).lastrowid
|
| 326 |
+
|
| 327 |
+
self.conn.commit()
|
| 328 |
+
return translation_id
|
| 329 |
+
except sqlite3.Error as e:
|
| 330 |
+
print(f"Error saving camera translation: {e}")
|
| 331 |
+
return None
|
| 332 |
+
|
| 333 |
+
def get_camera_translations(self, limit=50):
|
| 334 |
+
"""Get recent camera translations."""
|
| 335 |
+
try:
|
| 336 |
+
self.cursor.execute('''
|
| 337 |
+
SELECT * FROM camera_translations
|
| 338 |
+
ORDER BY date_captured DESC
|
| 339 |
+
LIMIT ?
|
| 340 |
+
''', (limit,))
|
| 341 |
+
|
| 342 |
+
return self.cursor.fetchall()
|
| 343 |
+
except sqlite3.Error as e:
|
| 344 |
+
print(f"Error getting camera translations: {e}")
|
| 345 |
+
return []
|
| 346 |
+
|
| 347 |
+
def add_camera_translation_to_vocabulary(self, translation_id, category=None):
|
| 348 |
+
"""Save a camera translation to vocabulary."""
|
| 349 |
+
try:
|
| 350 |
+
# Get the translation
|
| 351 |
+
self.cursor.execute('''
|
| 352 |
+
SELECT * FROM camera_translations WHERE id = ?
|
| 353 |
+
''', (translation_id,))
|
| 354 |
+
|
| 355 |
+
translation = self.cursor.fetchone()
|
| 356 |
+
if not translation:
|
| 357 |
+
return False
|
| 358 |
+
|
| 359 |
+
# Add to vocabulary
|
| 360 |
+
vocab_id = self.add_vocabulary(
|
| 361 |
+
translation['detected_text'],
|
| 362 |
+
translation['translated_text'],
|
| 363 |
+
translation['target_language'],
|
| 364 |
+
category,
|
| 365 |
+
translation['image_path'],
|
| 366 |
+
'camera'
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Mark as saved to vocabulary
|
| 370 |
+
if vocab_id:
|
| 371 |
+
self.cursor.execute('''
|
| 372 |
+
UPDATE camera_translations
|
| 373 |
+
SET is_saved_to_vocabulary = 1
|
| 374 |
+
WHERE id = ?
|
| 375 |
+
''', (translation_id,))
|
| 376 |
+
|
| 377 |
+
self.conn.commit()
|
| 378 |
+
return vocab_id
|
| 379 |
+
|
| 380 |
+
return None
|
| 381 |
+
except sqlite3.Error as e:
|
| 382 |
+
print(f"Error adding camera translation to vocabulary: {e}")
|
| 383 |
+
return None
|
| 384 |
+
|
| 385 |
+
def close(self):
|
| 386 |
+
"""Close the database connection."""
|
| 387 |
+
if self.conn:
|
| 388 |
+
self.conn.close()
|
| 389 |
+
|
| 390 |
+
# Example usage
|
| 391 |
+
if __name__ == "__main__":
|
| 392 |
+
# Create a test database in memory
|
| 393 |
+
db = LanguageLearningDB(":memory:")
|
| 394 |
+
|
| 395 |
+
# Add some test vocabulary
|
| 396 |
+
db.add_vocabulary("apple", "manzana", "es", "food")
|
| 397 |
+
db.add_vocabulary("book", "libro", "es", "objects")
|
| 398 |
+
|
| 399 |
+
# Update progress
|
| 400 |
+
db.update_word_progress(1, True) # Correct answer for word ID 1
|
| 401 |
+
|
| 402 |
+
# Save a camera translation
|
| 403 |
+
translation_id = db.save_camera_translation(
|
| 404 |
+
"/path/to/image.jpg",
|
| 405 |
+
"hello world",
|
| 406 |
+
"hola mundo",
|
| 407 |
+
"en",
|
| 408 |
+
"es"
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Add translation to vocabulary
|
| 412 |
+
db.add_camera_translation_to_vocabulary(translation_id, "phrases")
|
| 413 |
+
|
| 414 |
+
# Get all vocabulary
|
| 415 |
+
vocab = db.get_all_vocabulary()
|
| 416 |
+
for word in vocab:
|
| 417 |
+
print(f"{word['word_original']} -> {word['word_translated']} ({word['language_translated']})")
|
| 418 |
+
|
| 419 |
+
# Close the connection
|
| 420 |
+
db.close()
|
example_sentences.py
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Example Sentence Generator
|
| 3 |
+
------------------------------------------
|
| 4 |
+
A sophisticated approach to generating diverse, natural example sentences
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
import os
|
| 9 |
+
import random
|
| 10 |
+
import re
|
| 11 |
+
import time
|
| 12 |
+
import json
|
| 13 |
+
from functools import lru_cache
|
| 14 |
+
|
| 15 |
+
class ExampleSentenceGenerator:
|
| 16 |
+
def __init__(self, translate_func=None, debug=False):
|
| 17 |
+
"""Initialize the generator with a multi-stage filter pipeline and diverse templates."""
|
| 18 |
+
print("\n>>> ENHANCED SENTENCE GENERATOR LOADED <<<\n")
|
| 19 |
+
self.translate_func = translate_func
|
| 20 |
+
self.debug = debug # Set to True to see detailed rejection reasons
|
| 21 |
+
self.setup_cache_dir()
|
| 22 |
+
self._initialize_templates()
|
| 23 |
+
self.recent_templates = {} # Store recently used templates to avoid repetition
|
| 24 |
+
self.max_recent_templates = 10 # How many recent templates to remember per category
|
| 25 |
+
|
| 26 |
+
def setup_cache_dir(self):
|
| 27 |
+
"""Create cache directory if it doesn't exist."""
|
| 28 |
+
self.cache_dir = "sentence_cache"
|
| 29 |
+
os.makedirs(self.cache_dir, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
def _initialize_templates(self):
|
| 32 |
+
"""Initialize extensive template library with varied sentence structures."""
|
| 33 |
+
# CORE TEMPLATES
|
| 34 |
+
|
| 35 |
+
# NOUN TEMPLATES - Basic, Intermediate, Advanced
|
| 36 |
+
self.templates = {
|
| 37 |
+
# NOUN TEMPLATES - Various complexity levels and contexts
|
| 38 |
+
"noun": {
|
| 39 |
+
"basic": [
|
| 40 |
+
"The {word} is on the table.",
|
| 41 |
+
"I can see a {word} in the room.",
|
| 42 |
+
"She bought a new {word} yesterday.",
|
| 43 |
+
"This {word} is very useful.",
|
| 44 |
+
"Do you have a {word}?",
|
| 45 |
+
"Where is my {word}?",
|
| 46 |
+
"I need to buy a {word}.",
|
| 47 |
+
"That {word} belongs to me.",
|
| 48 |
+
"His {word} is very old.",
|
| 49 |
+
"Can I borrow your {word}?"
|
| 50 |
+
],
|
| 51 |
+
"intermediate": [
|
| 52 |
+
"I've been looking for a {word} like this for months.",
|
| 53 |
+
"She inherited an antique {word} from her grandmother.",
|
| 54 |
+
"The {word} was displayed prominently in the center of the room.",
|
| 55 |
+
"My brother accidentally broke my favorite {word} last week.",
|
| 56 |
+
"We should consider getting a different {word} for this purpose.",
|
| 57 |
+
"They sell handmade {word}s at that store downtown.",
|
| 58 |
+
"The museum has a rare {word} from the 16th century.",
|
| 59 |
+
"I'm thinking about replacing my {word} soon.",
|
| 60 |
+
"That {word} reminds me of my childhood home.",
|
| 61 |
+
"Would you mind passing me that {word} on the shelf?"
|
| 62 |
+
],
|
| 63 |
+
"advanced": [
|
| 64 |
+
"The antique {word} that I found at the flea market turned out to be quite valuable.",
|
| 65 |
+
"She explained that the unusual {word} had been in her family for generations.",
|
| 66 |
+
"Despite its age, the {word} functions perfectly after the restoration.",
|
| 67 |
+
"The artist incorporated a {word} into his latest sculpture, giving it an unexpected twist.",
|
| 68 |
+
"After searching through numerous stores, I finally found a {word} that matched my requirements.",
|
| 69 |
+
"The {word} caught my attention because of its unique design and craftsmanship.",
|
| 70 |
+
"They discussed replacing the old {word} with something more modern and practical.",
|
| 71 |
+
"I was surprised to discover that the {word} was actually made in my hometown.",
|
| 72 |
+
"The {word} would be perfect for our new apartment, if only it weren't so expensive.",
|
| 73 |
+
"When traveling abroad, he always brings back a traditional {word} as a souvenir."
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
# CLOTHING TEMPLATES - With proper context for clothing items
|
| 78 |
+
"clothing": {
|
| 79 |
+
"basic": [
|
| 80 |
+
"I like your {word}.",
|
| 81 |
+
"This {word} is too small.",
|
| 82 |
+
"She bought a new {word}.",
|
| 83 |
+
"The {word} is blue.",
|
| 84 |
+
"Can I borrow your {word}?",
|
| 85 |
+
"I need a warm {word}.",
|
| 86 |
+
"That {word} looks great on you.",
|
| 87 |
+
"My {word} is too big.",
|
| 88 |
+
"Where did you get that {word}?",
|
| 89 |
+
"He lost his favorite {word}."
|
| 90 |
+
],
|
| 91 |
+
"intermediate": [
|
| 92 |
+
"She wore her favorite {word} to the dinner party last night.",
|
| 93 |
+
"This {word} would look perfect with your new jeans.",
|
| 94 |
+
"I bought this {word} on sale last weekend.",
|
| 95 |
+
"The designer creates unique {word}s for celebrities.",
|
| 96 |
+
"I need to iron my {word} before the interview tomorrow.",
|
| 97 |
+
"This {word} is made of high-quality cotton.",
|
| 98 |
+
"That {word} really brings out the color of your eyes.",
|
| 99 |
+
"He spilled coffee on his new {word} this morning.",
|
| 100 |
+
"My mother gave me this {word} for my birthday.",
|
| 101 |
+
"Do you think this {word} is appropriate for a formal event?"
|
| 102 |
+
],
|
| 103 |
+
"advanced": [
|
| 104 |
+
"The intricate embroidery on her {word} caught everyone's attention at the gallery opening.",
|
| 105 |
+
"I've been searching for a {word} in this particular shade of blue for months.",
|
| 106 |
+
"The vintage {word} I found at the thrift store needed just minor alterations to fit perfectly.",
|
| 107 |
+
"She designs and sews her own {word}s using sustainable fabrics and traditional techniques.",
|
| 108 |
+
"After the washing machine incident, my favorite {word} was never quite the same.",
|
| 109 |
+
"When traveling to colder climates, I always pack my warmest {word} just in case.",
|
| 110 |
+
"The designer's latest collection features {word}s with bold geometric patterns and vibrant colors.",
|
| 111 |
+
"The celebrity was photographed wearing a custom-made {word} at the awards ceremony.",
|
| 112 |
+
"I've had this {word} for years, and it still remains one of my favorite pieces in my wardrobe.",
|
| 113 |
+
"The tailor suggested a different style of {word} that would better complement my body type."
|
| 114 |
+
]
|
| 115 |
+
},
|
| 116 |
+
|
| 117 |
+
# UNCOUNTABLE CLOTHING TEMPLATES - No articles
|
| 118 |
+
"uncountable_clothing": {
|
| 119 |
+
"basic": [
|
| 120 |
+
"The {word} feels very soft.",
|
| 121 |
+
"I need new {word} for winter.",
|
| 122 |
+
"This {word} is expensive.",
|
| 123 |
+
"She likes wearing {word} from that brand.",
|
| 124 |
+
"Where did you buy this {word}?",
|
| 125 |
+
"The {word} is made in Italy.",
|
| 126 |
+
"I prefer cotton {word}.",
|
| 127 |
+
"This {word} is too warm for summer.",
|
| 128 |
+
"The store sells {word} for all seasons.",
|
| 129 |
+
"That {word} looks comfortable."
|
| 130 |
+
],
|
| 131 |
+
"intermediate": [
|
| 132 |
+
"She designs {word} for a high-end fashion label in Milan.",
|
| 133 |
+
"I'm looking for {word} that's both stylish and comfortable.",
|
| 134 |
+
"The {word} she wore to the event received many compliments.",
|
| 135 |
+
"This brand specializes in {word} for outdoor activities.",
|
| 136 |
+
"The {word} in the display window caught my attention immediately.",
|
| 137 |
+
"Quality {word} is worth the investment in the long run.",
|
| 138 |
+
"They import {word} from several European countries.",
|
| 139 |
+
"The {word} feels incredibly soft and lightweight.",
|
| 140 |
+
"We need to buy warmer {word} before our trip to Norway.",
|
| 141 |
+
"The latest {word} collection features bold patterns and colors."
|
| 142 |
+
],
|
| 143 |
+
"advanced": [
|
| 144 |
+
"The designer's new collection features {word} made entirely from sustainable and recycled materials.",
|
| 145 |
+
"She has a keen eye for quality {word} that will last for many seasons.",
|
| 146 |
+
"The boutique specializes in handcrafted {word} using traditional techniques passed down for generations.",
|
| 147 |
+
"When traveling to varied climates, I always pack versatile {word} that can be layered as needed.",
|
| 148 |
+
"The exhibition showcased how {word} has evolved through different cultural contexts over the centuries.",
|
| 149 |
+
"The company has revolutionized athletic {word} with their innovative moisture-wicking fabrics.",
|
| 150 |
+
"After studying fashion design, she launched her own line of {word} inspired by architectural elements.",
|
| 151 |
+
"The {word} displayed in the museum demonstrates the craftsmanship of that historical period.",
|
| 152 |
+
"I prefer investing in timeless {word} rather than following fast-fashion trends.",
|
| 153 |
+
"The documentary explores how modern {word} production impacts environmental sustainability."
|
| 154 |
+
]
|
| 155 |
+
},
|
| 156 |
+
|
| 157 |
+
# PERSON TEMPLATES - For words describing people
|
| 158 |
+
"person": {
|
| 159 |
+
"basic": [
|
| 160 |
+
"The {word} is waiting outside.",
|
| 161 |
+
"I met a friendly {word} today.",
|
| 162 |
+
"She is a {word} at the university.",
|
| 163 |
+
"My neighbor is a {word}.",
|
| 164 |
+
"A {word} helped me find my way.",
|
| 165 |
+
"The {word} waved hello.",
|
| 166 |
+
"I saw a {word} at the store.",
|
| 167 |
+
"The {word} was very kind.",
|
| 168 |
+
"Do you know that {word}?",
|
| 169 |
+
"The {word} is reading a book."
|
| 170 |
+
],
|
| 171 |
+
"intermediate": [
|
| 172 |
+
"The {word} I met at the conference gave an interesting presentation about climate change.",
|
| 173 |
+
"My cousin recently became a {word} at the new hospital downtown.",
|
| 174 |
+
"The {word} who lives next door often helps me with my gardening questions.",
|
| 175 |
+
"We interviewed several {word}s for the position, but none had the right qualifications.",
|
| 176 |
+
"The {word} who wrote this article has a unique perspective on the issue.",
|
| 177 |
+
"A {word} approached me in the park asking for directions to the museum.",
|
| 178 |
+
"The community center needs more volunteer {word}s to help with the after-school program.",
|
| 179 |
+
"The {word} who served us at the restaurant was very attentive and friendly.",
|
| 180 |
+
"My daughter wants to become a {word} when she grows up.",
|
| 181 |
+
"The {word} standing by the entrance seemed to be waiting for someone."
|
| 182 |
+
],
|
| 183 |
+
"advanced": [
|
| 184 |
+
"The {word} who mentored me during my early career had a profound influence on my professional development.",
|
| 185 |
+
"After twenty years as a dedicated {word}, she decided to pursue a completely different career path.",
|
| 186 |
+
"The documentary features interviews with {word}s from diverse backgrounds sharing their unique experiences.",
|
| 187 |
+
"The renowned {word} delivered a compelling speech about the importance of community engagement.",
|
| 188 |
+
"The organization provides resources and support to {word}s who are navigating challenging circumstances.",
|
| 189 |
+
"My grandparent was a {word} during a pivotal time in our country's history.",
|
| 190 |
+
"Research suggests that {word}s who maintain strong social connections tend to live longer, healthier lives.",
|
| 191 |
+
"The book chronicles the journey of a {word} who overcame significant obstacles to achieve their dreams.",
|
| 192 |
+
"The panel discussion included perspectives from {word}s representing various fields and specialties.",
|
| 193 |
+
"As an experienced {word}, she offers valuable insights that only come with years of practice."
|
| 194 |
+
]
|
| 195 |
+
},
|
| 196 |
+
|
| 197 |
+
# ANIMAL TEMPLATES - For animal words (more natural, everyday examples)
|
| 198 |
+
"animal": {
|
| 199 |
+
"basic": [
|
| 200 |
+
"The {word} is sleeping.",
|
| 201 |
+
"I saw a {word} at the zoo.",
|
| 202 |
+
"My friend has a pet {word}.",
|
| 203 |
+
"The {word} runs very fast.",
|
| 204 |
+
"The {word} has beautiful fur.",
|
| 205 |
+
"I like watching {word}s.",
|
| 206 |
+
"The {word} is eating.",
|
| 207 |
+
"That {word} is very large.",
|
| 208 |
+
"The {word} lives in the forest.",
|
| 209 |
+
"Can you see the {word}?"
|
| 210 |
+
],
|
| 211 |
+
"intermediate": [
|
| 212 |
+
"We saw a {word} during our visit to the wildlife park.",
|
| 213 |
+
"My daughter loves learning about {word}s in her science class.",
|
| 214 |
+
"The TV show featured baby {word}s playing with their mother.",
|
| 215 |
+
"The zoo has three new {word}s in their collection this year.",
|
| 216 |
+
"Be careful if you see a wild {word} while hiking in the mountains.",
|
| 217 |
+
"The {word} we spotted on our hike was with its young.",
|
| 218 |
+
"My son has a book with beautiful pictures of {word}s.",
|
| 219 |
+
"The {word} at the zoo gets fed twice a day.",
|
| 220 |
+
"My neighbor's pet {word} escaped from their yard yesterday.",
|
| 221 |
+
"The children were excited to see the {word} at the zoo."
|
| 222 |
+
],
|
| 223 |
+
"advanced": [
|
| 224 |
+
"The documentary showed how {word}s care for their young in the wild.",
|
| 225 |
+
"Our safari guide pointed out a family of {word}s near the watering hole.",
|
| 226 |
+
"The {word} at the wildlife sanctuary has been trained to perform simple tricks.",
|
| 227 |
+
"My favorite exhibit at the natural history museum features a giant {word}.",
|
| 228 |
+
"We learned that {word}s are most active during the early morning hours.",
|
| 229 |
+
"In many stories and fables, the {word} is portrayed as clever and cunning.",
|
| 230 |
+
"The zoo provides special enrichment activities to keep their {word}s healthy and happy.",
|
| 231 |
+
"The wildlife photographer showed us amazing close-up photos of a {word} in its natural habitat.",
|
| 232 |
+
"She's studying to become a veterinarian specializing in treating {word}s and similar species.",
|
| 233 |
+
"The nature park has created a special protected area for the local {word} population."
|
| 234 |
+
]
|
| 235 |
+
},
|
| 236 |
+
|
| 237 |
+
# EYEWEAR TEMPLATES - For glasses, sunglasses, etc.
|
| 238 |
+
"eyewear": {
|
| 239 |
+
"basic": [
|
| 240 |
+
"I need my {word} to read.",
|
| 241 |
+
"She wears {word} all the time.",
|
| 242 |
+
"These {word} are new.",
|
| 243 |
+
"My {word} are broken.",
|
| 244 |
+
"Where are my {word}?",
|
| 245 |
+
"I lost my {word} yesterday.",
|
| 246 |
+
"These {word} help me see better.",
|
| 247 |
+
"The {word} are on the table.",
|
| 248 |
+
"His {word} have blue frames.",
|
| 249 |
+
"I need to clean my {word}."
|
| 250 |
+
],
|
| 251 |
+
"intermediate": [
|
| 252 |
+
"I finally found a pair of {word} that fit comfortably on my face.",
|
| 253 |
+
"She needs to wear prescription {word} while driving.",
|
| 254 |
+
"The optometrist recommended these {word} for reading and computer work.",
|
| 255 |
+
"My {word} have transition lenses that darken in the sunlight.",
|
| 256 |
+
"He's been wearing the same style of {word} for over twenty years.",
|
| 257 |
+
"I keep forgetting where I put my {word} down around the house.",
|
| 258 |
+
"These designer {word} were quite expensive but should last for years.",
|
| 259 |
+
"My new {word} have anti-glare coating which helps reduce eye strain.",
|
| 260 |
+
"She has several pairs of {word} that she coordinates with different outfits.",
|
| 261 |
+
"The {word} case protects them when I'm not wearing them."
|
| 262 |
+
],
|
| 263 |
+
"advanced": [
|
| 264 |
+
"After trying countless styles, I finally found {word} that complement my face shape and personal style.",
|
| 265 |
+
"The optometrist explained that my progressive {word} would take some time to adjust to, but would ultimately be more convenient.",
|
| 266 |
+
"She collects vintage {word} from the 1960s and has an impressive display of unique frames.",
|
| 267 |
+
"The technology behind these specialized {word} allows people with color blindness to perceive a wider range of colors.",
|
| 268 |
+
"My grandfather has worn the same tortoiseshell {word} for decades, refusing all suggestions to update his style.",
|
| 269 |
+
"The museum exhibition featured {word} worn by famous historical figures, showing how eyewear fashion has evolved.",
|
| 270 |
+
"Modern {word} manufacturing combines traditional craftsmanship with innovative materials for both durability and comfort.",
|
| 271 |
+
"The ophthalmologist suggested I try computer {word} specifically designed to filter blue light and reduce digital eye strain.",
|
| 272 |
+
"After laser surgery, she only needs to wear {word} occasionally for night driving or extended reading sessions.",
|
| 273 |
+
"The artisan creates custom {word} using sustainable materials and traditional techniques passed down for generations."
|
| 274 |
+
]
|
| 275 |
+
},
|
| 276 |
+
|
| 277 |
+
# JEWELRY TEMPLATES - For necklaces, rings, bracelets, etc.
|
| 278 |
+
"jewelry": {
|
| 279 |
+
"basic": [
|
| 280 |
+
"She wears a beautiful {word}.",
|
| 281 |
+
"The {word} is made of gold.",
|
| 282 |
+
"I lost my {word} yesterday.",
|
| 283 |
+
"This {word} was a gift.",
|
| 284 |
+
"The {word} has a diamond.",
|
| 285 |
+
"I like your {word}.",
|
| 286 |
+
"That {word} looks expensive.",
|
| 287 |
+
"My grandmother gave me this {word}.",
|
| 288 |
+
"She never takes off her {word}.",
|
| 289 |
+
"Where did you buy that {word}?"
|
| 290 |
+
],
|
| 291 |
+
"intermediate": [
|
| 292 |
+
"She wore her grandmother's antique {word} to the wedding ceremony.",
|
| 293 |
+
"The handcrafted {word} features stones collected from the local riverbed.",
|
| 294 |
+
"I had my {word} repaired at a specialty shop downtown.",
|
| 295 |
+
"He surprised her with a {word} for their anniversary celebration.",
|
| 296 |
+
"The museum displays {word}s from various historical periods and cultures.",
|
| 297 |
+
"She designs custom {word}s using traditional techniques she learned as an apprentice.",
|
| 298 |
+
"The {word} catches the light beautifully when she moves.",
|
| 299 |
+
"This {word} has been passed down through four generations of my family.",
|
| 300 |
+
"I'm looking for a {word} that I can wear with both casual and formal outfits.",
|
| 301 |
+
"The artisan market had a vendor selling handmade {word}s from recycled materials."
|
| 302 |
+
],
|
| 303 |
+
"advanced": [
|
| 304 |
+
"The intricate {word} she wore to the gala featured rare gemstones arranged in a pattern inspired by celestial constellations.",
|
| 305 |
+
"Archaeological discoveries revealed that ancient civilizations crafted {word}s using techniques that still challenge modern artisans.",
|
| 306 |
+
"The exhibition showcased how {word} designs have evolved across cultures while maintaining symbolic significance.",
|
| 307 |
+
"After studying under master craftspeople, she now creates custom {word}s that blend traditional methods with contemporary aesthetics.",
|
| 308 |
+
"The family heirloom {word} not only holds sentimental value but has been appraised as a rare historical piece.",
|
| 309 |
+
"The artisan's {word} collection features materials sourced ethically from sustainable mines and workshops.",
|
| 310 |
+
"Cultural traditions surrounding the gifting of {word}s reveal fascinating insights about historical social structures.",
|
| 311 |
+
"The delicate balance between craftsmanship and wearability is evident in every {word} the designer creates.",
|
| 312 |
+
"Her travels around the world have inspired a unique collection of {word}s that incorporate diverse cultural elements.",
|
| 313 |
+
"The restoration of the antique {word} required specialized techniques to preserve its original character and value."
|
| 314 |
+
]
|
| 315 |
+
},
|
| 316 |
+
|
| 317 |
+
# TOOLS TEMPLATES - For scissors, hammers, etc.
|
| 318 |
+
"tools": {
|
| 319 |
+
"basic": [
|
| 320 |
+
"I need the {word} to finish this.",
|
| 321 |
+
"The {word} is in the toolbox.",
|
| 322 |
+
"Can you pass me the {word}?",
|
| 323 |
+
"This {word} is very sharp.",
|
| 324 |
+
"Where are the {word}?",
|
| 325 |
+
"I bought a new {word} yesterday.",
|
| 326 |
+
"These {word} are old but work well.",
|
| 327 |
+
"Be careful with the {word}.",
|
| 328 |
+
"The {word} is broken.",
|
| 329 |
+
"I can't find my {word}."
|
| 330 |
+
],
|
| 331 |
+
"intermediate": [
|
| 332 |
+
"I need a specific type of {word} for this delicate repair work.",
|
| 333 |
+
"My grandfather's {word} has lasted for decades because of its quality craftsmanship.",
|
| 334 |
+
"The workshop has a specialized {word} that makes this process much easier.",
|
| 335 |
+
"She keeps her {word} organized in a custom-built storage system.",
|
| 336 |
+
"The artisan demonstrated how to properly use the {word} for best results.",
|
| 337 |
+
"I borrowed my neighbor's {word} to finish the project yesterday.",
|
| 338 |
+
"These professional-grade {word}s cost more but will last much longer.",
|
| 339 |
+
"The {word} needs to be sharpened before we continue with the project.",
|
| 340 |
+
"You'll need a different {word} for working with that material.",
|
| 341 |
+
"The antique {word} is still perfectly functional despite its age."
|
| 342 |
+
],
|
| 343 |
+
"advanced": [
|
| 344 |
+
"The craftsman hand-forges each {word} using techniques that have been passed down for generations.",
|
| 345 |
+
"Modern manufacturing has changed the design of the {word}, but professional artisans often prefer traditional models.",
|
| 346 |
+
"The museum's collection includes {word}s dating back to the industrial revolution, showing the evolution of tool design.",
|
| 347 |
+
"After apprenticing with a master, she developed her own modified {word} that addresses common issues with the standard design.",
|
| 348 |
+
"The specialized {word} was developed specifically for this type of precision work and requires significant training to use properly.",
|
| 349 |
+
"Restoring antique furniture requires period-appropriate {word}s to maintain historical accuracy and preservation standards.",
|
| 350 |
+
"Different regions developed unique variations of the {word} based on local materials and specific cultural needs.",
|
| 351 |
+
"The ergonomic design of this modern {word} reduces strain during extended periods of use.",
|
| 352 |
+
"The documentary explores how the invention of the {word} revolutionized this particular craft and industry.",
|
| 353 |
+
"Professional artisans often modify their {word}s to suit their specific working style and the unique demands of their specialty."
|
| 354 |
+
]
|
| 355 |
+
},
|
| 356 |
+
|
| 357 |
+
# TOYS TEMPLATES - For toys, games, etc. (more natural, child-focused examples)
|
| 358 |
+
"toys": {
|
| 359 |
+
"basic": [
|
| 360 |
+
"The child loves playing with the {word}.",
|
| 361 |
+
"This {word} is my favorite toy.",
|
| 362 |
+
"She sleeps with her {word} every night.",
|
| 363 |
+
"The {word} is sitting on the shelf.",
|
| 364 |
+
"He got a new {word} for his birthday.",
|
| 365 |
+
"This {word} is so soft and cuddly.",
|
| 366 |
+
"My daughter won't go anywhere without her {word}.",
|
| 367 |
+
"The {word} has a button that makes sounds.",
|
| 368 |
+
"We bought a new {word} at the toy store.",
|
| 369 |
+
"The children share their {word} with each other."
|
| 370 |
+
],
|
| 371 |
+
"intermediate": [
|
| 372 |
+
"My daughter takes her {word} with her everywhere she goes.",
|
| 373 |
+
"The child's {word} was a gift from her grandparents last Christmas.",
|
| 374 |
+
"When he can't find his {word}, he gets very upset at bedtime.",
|
| 375 |
+
"The little boy dragged his {word} behind him as he walked through the store.",
|
| 376 |
+
"She has a collection of {word}s displayed on her bedroom shelf.",
|
| 377 |
+
"His favorite {word} is getting worn out from being hugged so much.",
|
| 378 |
+
"The twins each have their own {word} that they take to preschool.",
|
| 379 |
+
"We had to go back to the hotel to get her {word} before bedtime.",
|
| 380 |
+
"The {word} has become his comfort object when he's feeling sad.",
|
| 381 |
+
"You can see how much she loves her {word} by how well she takes care of it."
|
| 382 |
+
],
|
| 383 |
+
"advanced": [
|
| 384 |
+
"The hospital gives each child a {word} to help them feel more comfortable during their stay.",
|
| 385 |
+
"My son still keeps his childhood {word} even though he's now in college.",
|
| 386 |
+
"The store specializes in handmade {word}s that are passed down through generations.",
|
| 387 |
+
"The family tradition involves giving each newborn child a special {word} on their first birthday.",
|
| 388 |
+
"The charity collects gently used {word}s to donate to children in need during the holidays.",
|
| 389 |
+
"The display case holds her collection of vintage {word}s from around the world.",
|
| 390 |
+
"Therapists often use {word}s to help children express their feelings during counseling sessions.",
|
| 391 |
+
"When we moved to a new country, his familiar {word} helped him adjust to all the changes.",
|
| 392 |
+
"The company designs {word}s specifically for children with sensory processing challenges.",
|
| 393 |
+
"My grandmother kept her childhood {word} for over seventy years before passing it on to me."
|
| 394 |
+
]
|
| 395 |
+
},
|
| 396 |
+
|
| 397 |
+
# VERB TEMPLATES - For action words
|
| 398 |
+
"verb": {
|
| 399 |
+
"basic": [
|
| 400 |
+
"I {word} every morning.",
|
| 401 |
+
"She {word}s regularly.",
|
| 402 |
+
"They {word} on weekends.",
|
| 403 |
+
"Do you {word} often?",
|
| 404 |
+
"He doesn't {word}.",
|
| 405 |
+
"We {word}ed yesterday.",
|
| 406 |
+
"I will {word} tomorrow.",
|
| 407 |
+
"She is {word}ing now.",
|
| 408 |
+
"Have you ever {word}ed?",
|
| 409 |
+
"They are {word}ing."
|
| 410 |
+
],
|
| 411 |
+
"intermediate": [
|
| 412 |
+
"I try to {word} at least three times a week for my health.",
|
| 413 |
+
"She {word}s whenever she has a free moment in her busy schedule.",
|
| 414 |
+
"The instructor taught us the proper technique to {word} effectively.",
|
| 415 |
+
"After the accident, he couldn't {word} for several months during recovery.",
|
| 416 |
+
"We {word} together every Saturday as part of our weekend routine.",
|
| 417 |
+
"The guidelines recommend that beginners {word} no more than twice weekly.",
|
| 418 |
+
"I've been {word}ing regularly since January and have noticed significant improvements.",
|
| 419 |
+
"Children naturally learn to {word} through observation and practice.",
|
| 420 |
+
"The app helps you track how often you {word} and measures your progress.",
|
| 421 |
+
"They {word} competitively at the national level."
|
| 422 |
+
],
|
| 423 |
+
"advanced": [
|
| 424 |
+
"Research indicates that those who {word} consistently throughout their lives maintain better cognitive function as they age.",
|
| 425 |
+
"After decades of {word}ing professionally, she now teaches advanced techniques to dedicated students.",
|
| 426 |
+
"The documentary explores how cultural differences influence the way people {word} across various societies.",
|
| 427 |
+
"The ancient practice of {word}ing has evolved considerably but maintains its core principles and benefits.",
|
| 428 |
+
"His innovative method of {word}ing challenged conventional wisdom and eventually transformed the entire field.",
|
| 429 |
+
"The rehabilitation program incorporates modified ways to {word} that accommodate various physical limitations.",
|
| 430 |
+
"Neuroscience research reveals that {word}ing activates multiple brain regions simultaneously, creating new neural pathways.",
|
| 431 |
+
"The philosophy behind this approach suggests that how you {word} reflects fundamental aspects of your worldview.",
|
| 432 |
+
"Traditional communities have {word}ed as part of seasonal rituals for countless generations.",
|
| 433 |
+
"The longitudinal study tracked participants who {word} regularly, finding significant long-term health benefits."
|
| 434 |
+
]
|
| 435 |
+
},
|
| 436 |
+
|
| 437 |
+
# ADJECTIVE TEMPLATES - For descriptive words
|
| 438 |
+
"adjective": {
|
| 439 |
+
"basic": [
|
| 440 |
+
"The sky is {word} today.",
|
| 441 |
+
"She looks very {word}.",
|
| 442 |
+
"That is a {word} book.",
|
| 443 |
+
"The food tastes {word}.",
|
| 444 |
+
"I feel {word} this morning.",
|
| 445 |
+
"The music sounds {word}.",
|
| 446 |
+
"What a {word} day!",
|
| 447 |
+
"The water is {word}.",
|
| 448 |
+
"His house is very {word}.",
|
| 449 |
+
"The flowers smell {word}."
|
| 450 |
+
],
|
| 451 |
+
"intermediate": [
|
| 452 |
+
"The critics described the film as surprisingly {word} despite its low budget.",
|
| 453 |
+
"After the renovation, the room feels much more {word} and welcoming.",
|
| 454 |
+
"The chef is known for creating {word} dishes that combine unexpected flavors.",
|
| 455 |
+
"The {word} atmosphere of the café makes it perfect for quiet conversation.",
|
| 456 |
+
"Her writing style is refreshingly {word} compared to other authors in the genre.",
|
| 457 |
+
"The landscape becomes increasingly {word} as you travel further north.",
|
| 458 |
+
"We were impressed by the {word} performance of the young musician.",
|
| 459 |
+
"The weather turned unexpectedly {word} during our vacation.",
|
| 460 |
+
"She chose a more {word} approach to solving the complex problem.",
|
| 461 |
+
"The {word} quality of the light at sunset inspired many of his paintings."
|
| 462 |
+
],
|
| 463 |
+
"advanced": [
|
| 464 |
+
"The architectural design achieves a harmonious balance between {word} elements and practical functionality.",
|
| 465 |
+
"The region's cuisine is characterized by {word} flavors that reflect its unique cultural heritage and local ingredients.",
|
| 466 |
+
"Critics have noted the increasingly {word} tone of the author's work as it evolved over decades.",
|
| 467 |
+
"The exhibition juxtaposes {word} contemporary pieces with classical works to create thought-provoking contrasts.",
|
| 468 |
+
"Researchers documented the {word} conditions that contribute to this rare ecological phenomenon.",
|
| 469 |
+
"The performance was notable for its {word} interpretation of the traditional composition.",
|
| 470 |
+
"The memoir reveals the {word} complexity of relationships formed during periods of historical upheaval.",
|
| 471 |
+
"Urban planners aim to create more {word} community spaces that encourage diverse interactions.",
|
| 472 |
+
"The documentary captures the {word} beauty of remote landscapes rarely witnessed by human eyes.",
|
| 473 |
+
"Medical researchers have identified several {word} biomarkers associated with early stages of the condition."
|
| 474 |
+
]
|
| 475 |
+
},
|
| 476 |
+
|
| 477 |
+
# GENERAL TEMPLATES - Fallback for everything else
|
| 478 |
+
"general": {
|
| 479 |
+
"basic": [
|
| 480 |
+
"Let's talk about {word}.",
|
| 481 |
+
"I'm interested in {word}.",
|
| 482 |
+
"Do you know about {word}?",
|
| 483 |
+
"{word} is important.",
|
| 484 |
+
"I learned about {word} yesterday.",
|
| 485 |
+
"She mentioned {word}.",
|
| 486 |
+
"They're studying {word}.",
|
| 487 |
+
"What do you think about {word}?",
|
| 488 |
+
"{word} has changed over time.",
|
| 489 |
+
"The book explains {word}."
|
| 490 |
+
],
|
| 491 |
+
"intermediate": [
|
| 492 |
+
"We discussed the implications of {word} during yesterday's meeting.",
|
| 493 |
+
"The article explores different perspectives on {word} in modern society.",
|
| 494 |
+
"She's been researching {word} for her upcoming paper.",
|
| 495 |
+
"Recent developments in {word} have changed our understanding of the field.",
|
| 496 |
+
"The podcast featured experts discussing the future of {word}.",
|
| 497 |
+
"Understanding {word} requires knowledge of several related concepts.",
|
| 498 |
+
"The course offers an introduction to the basics of {word}.",
|
| 499 |
+
"There are various approaches to addressing issues related to {word}.",
|
| 500 |
+
"The conference will focus on recent advances in {word}.",
|
| 501 |
+
"They published a comprehensive study about {word} last year."
|
| 502 |
+
],
|
| 503 |
+
"advanced": [
|
| 504 |
+
"The interdisciplinary approach to {word} reveals connections that weren't apparent within traditional academic frameworks.",
|
| 505 |
+
"Historical perspectives on {word} provide valuable context for understanding contemporary challenges in the field.",
|
| 506 |
+
"The symposium brought together experts from diverse backgrounds to address emerging questions about {word}.",
|
| 507 |
+
"Technological innovations have fundamentally transformed how researchers approach the study of {word}.",
|
| 508 |
+
"Cultural attitudes toward {word} vary significantly across different societies and historical periods.",
|
| 509 |
+
"The foundation funds projects that explore ethical implications of {word} in various contexts.",
|
| 510 |
+
"Policymakers must consider multiple stakeholder perspectives when developing regulations related to {word}.",
|
| 511 |
+
"The textbook presents a comprehensive framework for analyzing {word} across different scenarios.",
|
| 512 |
+
"Ongoing debates about {word} reflect deeper philosophical questions about our societal values.",
|
| 513 |
+
"The documentary examines how public understanding of {word} has evolved over the past century."
|
| 514 |
+
]
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
# Common words in each category
|
| 519 |
+
self.category_words = {
|
| 520 |
+
"person": [
|
| 521 |
+
"person", "man", "woman", "boy", "girl", "child", "teacher", "doctor",
|
| 522 |
+
"student", "friend", "neighbor", "parent", "employee", "worker", "artist",
|
| 523 |
+
"musician", "writer", "chef", "athlete", "scientist", "engineer", "nurse"
|
| 524 |
+
],
|
| 525 |
+
"animal": [
|
| 526 |
+
"dog", "cat", "bird", "fish", "horse", "cow", "elephant", "lion",
|
| 527 |
+
"tiger", "bear", "rabbit", "monkey", "mouse", "frog", "snake", "wolf",
|
| 528 |
+
"fox", "deer", "giraffe", "zebra", "penguin", "eagle", "owl", "turtle"
|
| 529 |
+
],
|
| 530 |
+
"clothing": [
|
| 531 |
+
"shirt", "pants", "dress", "jacket", "coat", "hat", "gloves", "socks",
|
| 532 |
+
"shoes", "boots", "sweater", "skirt", "jeans", "top", "scarf", "tie",
|
| 533 |
+
"blouse", "suit", "belt", "vest", "hoodie", "shorts", "pajamas", "uniform"
|
| 534 |
+
],
|
| 535 |
+
"uncountable_clothing": [
|
| 536 |
+
"clothing", "outerwear", "underwear", "sportswear", "footwear", "swimwear",
|
| 537 |
+
"knitwear", "loungewear", "sleepwear", "activewear", "winterwear", "beachwear"
|
| 538 |
+
],
|
| 539 |
+
"eyewear": [
|
| 540 |
+
"glasses", "sunglasses", "contacts", "goggles", "spectacles", "eyeglasses",
|
| 541 |
+
"shades", "reading glasses", "bifocals", "prescription glasses"
|
| 542 |
+
],
|
| 543 |
+
"jewelry": [
|
| 544 |
+
"necklace", "ring", "bracelet", "earrings", "watch", "pendant",
|
| 545 |
+
"brooch", "pin", "chain", "locket", "anklet", "cufflinks", "tiara",
|
| 546 |
+
"crown", "medallion", "choker", "bangle", "charm bracelet"
|
| 547 |
+
],
|
| 548 |
+
"tools": [
|
| 549 |
+
"scissors", "knife", "hammer", "screwdriver", "wrench", "pliers",
|
| 550 |
+
"saw", "drill", "tape measure", "level", "chisel", "clamp", "ruler",
|
| 551 |
+
"axe", "shovel", "rake", "trowel", "sander", "nail gun", "paintbrush"
|
| 552 |
+
],
|
| 553 |
+
"toys": [
|
| 554 |
+
"teddy bear", "doll", "ball", "blocks", "action figure", "puzzle",
|
| 555 |
+
"toy car", "stuffed animal", "plush toy", "game", "toy", "robot",
|
| 556 |
+
"kite", "yo-yo", "train set", "board game", "video game", "rattle",
|
| 557 |
+
"building set", "puppet", "play set", "model kit"
|
| 558 |
+
]
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
# Words that are typically used in plural form
|
| 562 |
+
self.plural_words = [
|
| 563 |
+
"glasses", "pants", "shorts", "jeans", "scissors", "trousers", "sunglasses",
|
| 564 |
+
"goggles", "spectacles", "eyeglasses", "headphones", "tights", "leggings",
|
| 565 |
+
"pliers", "binoculars", "tweezers", "pajamas", "shorts", "overalls", "trunks",
|
| 566 |
+
"boxers", "briefs", "clippers", "shears", "earrings", "earbuds", "headphones"
|
| 567 |
+
]
|
| 568 |
+
|
| 569 |
+
# Special cases that need specific handling
|
| 570 |
+
self.special_cases = {
|
| 571 |
+
"top": "clothing",
|
| 572 |
+
"glasses": "eyewear",
|
| 573 |
+
"teddy bear": "toys",
|
| 574 |
+
"teddy": "toys",
|
| 575 |
+
"clothing": "uncountable_clothing",
|
| 576 |
+
"bear": "animal",
|
| 577 |
+
"watch": "jewelry"
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
# Initialize the history of used templates
|
| 581 |
+
self.template_history = {}
|
| 582 |
+
|
| 583 |
+
def get_example_sentence(self, word, target_language, category=None):
|
| 584 |
+
"""Get an example sentence for a word using the enhanced algorithm."""
|
| 585 |
+
try:
|
| 586 |
+
# Normalize word
|
| 587 |
+
original_word = word
|
| 588 |
+
word = word.strip().lower()
|
| 589 |
+
|
| 590 |
+
if self.debug:
|
| 591 |
+
print(f"\n>>> Getting example for: '{word}' <<<\n")
|
| 592 |
+
|
| 593 |
+
# Try to get example from API with filter pipeline
|
| 594 |
+
examples = self._get_api_examples_with_pipeline(word, category)
|
| 595 |
+
|
| 596 |
+
# If we have valid examples, choose one randomly and return
|
| 597 |
+
if examples:
|
| 598 |
+
example = self._select_diverse_example(examples, word)
|
| 599 |
+
if self.debug:
|
| 600 |
+
print(f">>> Selected API example: '{example}'")
|
| 601 |
+
|
| 602 |
+
# Clean up the example
|
| 603 |
+
example = self._clean_sentence(example)
|
| 604 |
+
|
| 605 |
+
# Translate
|
| 606 |
+
translated = self._translate(example, target_language)
|
| 607 |
+
|
| 608 |
+
return {
|
| 609 |
+
"english": example,
|
| 610 |
+
"translated": translated,
|
| 611 |
+
"source": "filtered_api"
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
# If no valid API examples, use templates with complex selection
|
| 615 |
+
if self.debug:
|
| 616 |
+
print(f">>> No valid API examples found, using enhanced templates")
|
| 617 |
+
|
| 618 |
+
# Use category if provided, otherwise determine category
|
| 619 |
+
word_category = category if category else self._get_word_category(word)
|
| 620 |
+
if self.debug:
|
| 621 |
+
print(f">>> Word category: {word_category}")
|
| 622 |
+
|
| 623 |
+
# Get appropriate templates and select a diverse one
|
| 624 |
+
template, complexity = self._select_diverse_template(word, word_category)
|
| 625 |
+
|
| 626 |
+
# Create example from template with appropriate handling
|
| 627 |
+
example = self._create_example_from_template(template, word, word_category)
|
| 628 |
+
|
| 629 |
+
# Translate the example
|
| 630 |
+
translated = self._translate(example, target_language)
|
| 631 |
+
|
| 632 |
+
return {
|
| 633 |
+
"english": example,
|
| 634 |
+
"translated": translated,
|
| 635 |
+
"source": f"template_{word_category}_{complexity}"
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
except Exception as e:
|
| 639 |
+
if self.debug:
|
| 640 |
+
print(f">>> Error: {e}")
|
| 641 |
+
|
| 642 |
+
# Ultimate fallback
|
| 643 |
+
fallback = f"This is a {word}."
|
| 644 |
+
return {
|
| 645 |
+
"english": fallback,
|
| 646 |
+
"translated": self._translate(fallback, target_language),
|
| 647 |
+
"source": "error_fallback"
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
def _get_api_examples_with_pipeline(self, word, category=None):
|
| 651 |
+
"""Get examples from APIs and run them through the enhanced filter pipeline."""
|
| 652 |
+
# Get raw examples from all API sources
|
| 653 |
+
raw_examples = []
|
| 654 |
+
|
| 655 |
+
# Try Free Dictionary API
|
| 656 |
+
dict_examples = self._get_free_dictionary_examples(word)
|
| 657 |
+
if dict_examples:
|
| 658 |
+
raw_examples.extend(dict_examples)
|
| 659 |
+
|
| 660 |
+
# Try Wordnik API
|
| 661 |
+
wordnik_examples = self._get_wordnik_examples(word)
|
| 662 |
+
if wordnik_examples:
|
| 663 |
+
raw_examples.extend(wordnik_examples)
|
| 664 |
+
|
| 665 |
+
if not raw_examples:
|
| 666 |
+
if self.debug:
|
| 667 |
+
print(">>> No examples found from APIs")
|
| 668 |
+
return []
|
| 669 |
+
|
| 670 |
+
# Apply enhanced filter pipeline to raw examples
|
| 671 |
+
filtered_examples = []
|
| 672 |
+
for example in raw_examples:
|
| 673 |
+
# Run through all filters
|
| 674 |
+
if self._enhanced_filter_pipeline(example, word, category):
|
| 675 |
+
filtered_examples.append(example)
|
| 676 |
+
|
| 677 |
+
return filtered_examples
|
| 678 |
+
|
| 679 |
+
def _enhanced_filter_pipeline(self, example, word, category=None):
|
| 680 |
+
"""
|
| 681 |
+
Enhanced filter pipeline with improved context awareness.
|
| 682 |
+
Returns True if the example passes all filters, False otherwise.
|
| 683 |
+
"""
|
| 684 |
+
# Filter 1: Basic quality check
|
| 685 |
+
if not self._basic_quality_check(example):
|
| 686 |
+
if self.debug:
|
| 687 |
+
print(f">>> Rejected (Basic quality): '{example}'")
|
| 688 |
+
return False
|
| 689 |
+
|
| 690 |
+
# Filter 2: Exact word match
|
| 691 |
+
if not self._contains_exact_word(example, word):
|
| 692 |
+
if self.debug:
|
| 693 |
+
print(f">>> Rejected (No exact word match): '{example}'")
|
| 694 |
+
return False
|
| 695 |
+
|
| 696 |
+
# Filter 3: Compound word check
|
| 697 |
+
if self._contains_compound_words(example, word):
|
| 698 |
+
if self.debug:
|
| 699 |
+
print(f">>> Rejected (Contains compound word): '{example}'")
|
| 700 |
+
return False
|
| 701 |
+
|
| 702 |
+
# Filter 4: Variant form check
|
| 703 |
+
if self._contains_variant_forms(example, word):
|
| 704 |
+
if self.debug:
|
| 705 |
+
print(f">>> Rejected (Contains variant form): '{example}'")
|
| 706 |
+
return False
|
| 707 |
+
|
| 708 |
+
# Filter 5: Context appropriateness check based on category
|
| 709 |
+
identified_category = category if category else self._get_word_category(word)
|
| 710 |
+
if not self._check_advanced_context(example, word, identified_category):
|
| 711 |
+
if self.debug:
|
| 712 |
+
print(f">>> Rejected (Inappropriate context for {identified_category}): '{example}'")
|
| 713 |
+
return False
|
| 714 |
+
|
| 715 |
+
# Filter 6: Complexity and educational value check
|
| 716 |
+
if not self._check_complexity_value(example, word):
|
| 717 |
+
if self.debug:
|
| 718 |
+
print(f">>> Rejected (Not suitable complexity/value): '{example}'")
|
| 719 |
+
return False
|
| 720 |
+
|
| 721 |
+
# All filters passed
|
| 722 |
+
if self.debug:
|
| 723 |
+
print(f">>> Accepted: '{example}'")
|
| 724 |
+
return True
|
| 725 |
+
|
| 726 |
+
def _basic_quality_check(self, text):
|
| 727 |
+
"""Check if a sentence meets basic quality standards."""
|
| 728 |
+
# Must be a reasonable length
|
| 729 |
+
words = text.split()
|
| 730 |
+
if len(words) < 3 or len(words) > 25:
|
| 731 |
+
return False
|
| 732 |
+
|
| 733 |
+
# Shouldn't contain semicolons (often indicates a list, not a sentence)
|
| 734 |
+
if ';' in text:
|
| 735 |
+
return False
|
| 736 |
+
|
| 737 |
+
# Shouldn't be metadata
|
| 738 |
+
if "example of" in text.lower() or "examples of" in text.lower() or "example:" in text.lower():
|
| 739 |
+
return False
|
| 740 |
+
|
| 741 |
+
# Check if it has a proper end punctuation
|
| 742 |
+
if not text.strip().endswith(('.', '!', '?')):
|
| 743 |
+
return False
|
| 744 |
+
|
| 745 |
+
# Shouldn't contain inappropriate language
|
| 746 |
+
inappropriate_terms = ["kill", "die", "death", "murder", "suicide", "sex", "porn", "explicit", "violent"]
|
| 747 |
+
if any(term in text.lower() for term in inappropriate_terms):
|
| 748 |
+
return False
|
| 749 |
+
|
| 750 |
+
return True
|
| 751 |
+
|
| 752 |
+
def _contains_exact_word(self, text, word):
|
| 753 |
+
"""Check if text contains the exact word with proper word boundaries."""
|
| 754 |
+
pattern = r'\b' + re.escape(word) + r'\b'
|
| 755 |
+
return re.search(pattern, text.lower()) is not None
|
| 756 |
+
|
| 757 |
+
def _contains_compound_words(self, text, word):
|
| 758 |
+
"""Check if text contains compound words that include the target word."""
|
| 759 |
+
# Skip for very short words (2 chars or less) to avoid too many false positives
|
| 760 |
+
if len(word) <= 2:
|
| 761 |
+
return False
|
| 762 |
+
|
| 763 |
+
# Find all words in the text
|
| 764 |
+
all_words = re.findall(r'\b\w+\b', text.lower())
|
| 765 |
+
|
| 766 |
+
for text_word in all_words:
|
| 767 |
+
# Skip exact match
|
| 768 |
+
if text_word == word:
|
| 769 |
+
continue
|
| 770 |
+
|
| 771 |
+
# Check if the word is part of a longer word
|
| 772 |
+
if word in text_word:
|
| 773 |
+
# Check for special cases where it's ok
|
| 774 |
+
if word == "glass" and text_word == "glasses" and re.search(r'\b(eye|read|see|vision)', text.lower()):
|
| 775 |
+
continue
|
| 776 |
+
# It's a compound word
|
| 777 |
+
return True
|
| 778 |
+
|
| 779 |
+
return False
|
| 780 |
+
|
| 781 |
+
def _contains_variant_forms(self, text, word):
|
| 782 |
+
"""Check if text contains variant forms of the word."""
|
| 783 |
+
# Common variant forms for different word types
|
| 784 |
+
variants = set()
|
| 785 |
+
|
| 786 |
+
# Plural forms (for nouns)
|
| 787 |
+
if word.endswith('s'):
|
| 788 |
+
# If word ends with 's', consider singular form as variant
|
| 789 |
+
singular = word[:-1]
|
| 790 |
+
if len(singular) > 2: # Only add if meaningful
|
| 791 |
+
variants.add(singular)
|
| 792 |
+
else:
|
| 793 |
+
# Add potential plural forms
|
| 794 |
+
variants.add(word + "s")
|
| 795 |
+
if word.endswith('y'):
|
| 796 |
+
variants.add(word[:-1] + "ies")
|
| 797 |
+
elif word.endswith(('ch', 'sh', 'x', 'z', 's')):
|
| 798 |
+
variants.add(word + "es")
|
| 799 |
+
|
| 800 |
+
# Verb forms
|
| 801 |
+
variants.add(word + "ing")
|
| 802 |
+
variants.add(word + "ed")
|
| 803 |
+
if word.endswith('e'):
|
| 804 |
+
variants.add(word[:-1] + "ing")
|
| 805 |
+
variants.add(word + "d")
|
| 806 |
+
elif word.endswith(('y')):
|
| 807 |
+
variants.add(word[:-1] + "ied")
|
| 808 |
+
elif len(word) > 3 and word[-1] not in 'aeiou' and word[-2] in 'aeiou' and word[-3] not in 'aeiou':
|
| 809 |
+
variants.add(word + word[-1] + "ing")
|
| 810 |
+
variants.add(word + word[-1] + "ed")
|
| 811 |
+
|
| 812 |
+
# Adjective forms
|
| 813 |
+
variants.add(word + "er")
|
| 814 |
+
variants.add(word + "est")
|
| 815 |
+
if word.endswith('e'):
|
| 816 |
+
variants.add(word + "r")
|
| 817 |
+
variants.add(word + "st")
|
| 818 |
+
elif word.endswith('y'):
|
| 819 |
+
variants.add(word[:-1] + "ier")
|
| 820 |
+
variants.add(word[:-1] + "iest")
|
| 821 |
+
elif len(word) > 3 and word[-1] not in 'aeiou' and word[-2] in 'aeiou' and word[-3] not in 'aeiou':
|
| 822 |
+
variants.add(word + word[-1] + "er")
|
| 823 |
+
variants.add(word + word[-1] + "est")
|
| 824 |
+
|
| 825 |
+
# Special case handling for certain words
|
| 826 |
+
if word == "bear":
|
| 827 |
+
variants.update(["bore", "bearing", "bearable", "unbearable"])
|
| 828 |
+
elif word == "glasses":
|
| 829 |
+
variants.update(["glass", "eyeglass", "fiberglass", "fibreglass"])
|
| 830 |
+
elif word == "glass":
|
| 831 |
+
variants.update(["glasses", "fiberglass", "fibreglass"])
|
| 832 |
+
elif word == "top":
|
| 833 |
+
variants.update(["topping", "topped", "topmost"])
|
| 834 |
+
|
| 835 |
+
# Check for special case exceptions
|
| 836 |
+
if word == "glasses" and "eyeglasses" in text.lower():
|
| 837 |
+
return False
|
| 838 |
+
|
| 839 |
+
# Check if text contains any variants
|
| 840 |
+
for variant in variants:
|
| 841 |
+
if len(variant) <= 2:
|
| 842 |
+
continue # Skip very short variants
|
| 843 |
+
|
| 844 |
+
if self._contains_exact_word(text, variant):
|
| 845 |
+
if self.debug:
|
| 846 |
+
print(f">>> Found variant form: '{variant}'")
|
| 847 |
+
return True
|
| 848 |
+
|
| 849 |
+
return False
|
| 850 |
+
|
| 851 |
+
def _check_advanced_context(self, text, word, category):
|
| 852 |
+
"""Enhanced context check that considers word category and typical usage patterns."""
|
| 853 |
+
text_lower = text.lower()
|
| 854 |
+
|
| 855 |
+
# Special case for "bear" and other animals
|
| 856 |
+
if word == "bear" or category == "animal" and word == "bear":
|
| 857 |
+
# Check for animal context
|
| 858 |
+
animal_contexts = ["zoo", "animal", "wild", "fur", "cub", "paw", "den", "forest",
|
| 859 |
+
"grizzly", "polar", "pet", "wildlife", "nature", "animals"]
|
| 860 |
+
verb_contexts = ["burden", "weight", "load", "responsibility", "stand", "support",
|
| 861 |
+
"carry", "bore", "market", "stock", "bear with", "bear in mind"]
|
| 862 |
+
academic_contexts = ["research", "scientific", "theory", "journal", "symposium",
|
| 863 |
+
"academia", "philosophical", "evolution", "publication", "dissertation"]
|
| 864 |
+
|
| 865 |
+
has_animal_context = any(context in text_lower for context in animal_contexts)
|
| 866 |
+
has_verb_context = any(context in text_lower for context in verb_contexts)
|
| 867 |
+
has_academic_context = any(context in text_lower for context in academic_contexts)
|
| 868 |
+
|
| 869 |
+
# Prefer animal context, reject verb context and academic contexts
|
| 870 |
+
return has_animal_context and not has_verb_context and not has_academic_context
|
| 871 |
+
|
| 872 |
+
# Special case for teddy bear - ensure child-friendly context
|
| 873 |
+
elif "teddy" in word or word == "teddy bear":
|
| 874 |
+
child_contexts = ["child", "play", "toy", "soft", "cuddly", "sleep", "bed", "hug",
|
| 875 |
+
"stuffed", "plush", "gift", "favorite", "baby", "kid", "comfort"]
|
| 876 |
+
academic_contexts = ["research", "academic", "philosophical", "theory", "values",
|
| 877 |
+
"societal", "education", "cognitive", "development", "scholarly"]
|
| 878 |
+
|
| 879 |
+
has_child_context = any(context in text_lower for context in child_contexts)
|
| 880 |
+
has_academic_context = any(context in text_lower for context in academic_contexts)
|
| 881 |
+
|
| 882 |
+
# Strongly prefer child context for teddy bears, reject academic contexts
|
| 883 |
+
return has_child_context and not has_academic_context
|
| 884 |
+
|
| 885 |
+
# Special case for "top" (clothing vs. position)
|
| 886 |
+
elif word == "top" or category == "clothing" and word == "top":
|
| 887 |
+
# Check for clothing context
|
| 888 |
+
clothing_contexts = ["wear", "shirt", "outfit", "fashion", "dress", "color", "colour",
|
| 889 |
+
"style", "clothes", "wardrobe", "buy", "bought", "new", "fabric",
|
| 890 |
+
"cotton", "silk", "button", "sleeve", "collar", "blouse"]
|
| 891 |
+
position_contexts = ["mountain", "hill", "climb", "reached", "leadership", "ranked",
|
| 892 |
+
"ceiling", "position", "over", "above", "surface", "highest",
|
| 893 |
+
"best", "leading", "foremost", "premier", "superior", "chief"]
|
| 894 |
+
|
| 895 |
+
has_clothing_context = any(context in text_lower for context in clothing_contexts)
|
| 896 |
+
has_position_context = any(context in text_lower for context in position_contexts)
|
| 897 |
+
|
| 898 |
+
# For "top", strongly prefer clothing context for a language learning app
|
| 899 |
+
return has_clothing_context and not has_position_context
|
| 900 |
+
|
| 901 |
+
# For person words, check that context is not objectifying or inappropriate
|
| 902 |
+
elif category == "person":
|
| 903 |
+
objectifying_contexts = ["use", "using", "used", "utilize", "buy", "bought", "sell",
|
| 904 |
+
"sold", "cost", "price", "cheap", "expensive", "owned"]
|
| 905 |
+
|
| 906 |
+
# Check proximity of objectifying words to the person word
|
| 907 |
+
for context in objectifying_contexts:
|
| 908 |
+
if context in text_lower:
|
| 909 |
+
# Check if the objectifying word is close to the person word
|
| 910 |
+
context_index = text_lower.find(context)
|
| 911 |
+
word_index = text_lower.find(word)
|
| 912 |
+
if abs(context_index - word_index) < 15: # Within ~3-4 words
|
| 913 |
+
return False
|
| 914 |
+
|
| 915 |
+
# Special case for tools
|
| 916 |
+
elif category == "tools" and word == "scissors":
|
| 917 |
+
wrong_contexts = ["executed", "perfect", "jump", "kick", "position", "technique", "sports"]
|
| 918 |
+
tool_contexts = ["cut", "cutting", "paper", "fabric", "hair", "sharp", "blade", "trim"]
|
| 919 |
+
|
| 920 |
+
has_wrong_context = any(context in text_lower for context in wrong_contexts)
|
| 921 |
+
has_tool_context = any(context in text_lower for context in tool_contexts)
|
| 922 |
+
|
| 923 |
+
return has_tool_context or not has_wrong_context
|
| 924 |
+
|
| 925 |
+
# Special case for eyewear "glasses"
|
| 926 |
+
elif category == "eyewear":
|
| 927 |
+
# Check if it has eyewear context
|
| 928 |
+
eyewear_contexts = ["see", "vision", "read", "eye", "wear", "sight", "prescription",
|
| 929 |
+
"lens", "optician", "frame", "vision", "optometrist", "sunglasses"]
|
| 930 |
+
wrong_contexts = ["fill", "empty", "drink", "beverage", "water", "wine", "window",
|
| 931 |
+
"fiber", "fibre", "cup", "mug", "liquid", "pour"]
|
| 932 |
+
|
| 933 |
+
# Must have at least one eyewear context word or phrase
|
| 934 |
+
has_eyewear_context = any(context in text_lower for context in eyewear_contexts)
|
| 935 |
+
|
| 936 |
+
# Must not have conflicting contexts
|
| 937 |
+
has_wrong_context = any(context in text_lower for context in wrong_contexts)
|
| 938 |
+
|
| 939 |
+
return has_eyewear_context or not has_wrong_context
|
| 940 |
+
|
| 941 |
+
# Special case for jewelry
|
| 942 |
+
elif category == "jewelry":
|
| 943 |
+
jewelry_contexts = ["wear", "gold", "silver", "diamond", "gem", "stone", "gift",
|
| 944 |
+
"beautiful", "elegant", "accessory", "decorated", "adorned"]
|
| 945 |
+
|
| 946 |
+
has_jewelry_context = any(context in text_lower for context in jewelry_contexts)
|
| 947 |
+
return has_jewelry_context
|
| 948 |
+
|
| 949 |
+
# Clothing checks
|
| 950 |
+
elif category in ["clothing", "uncountable_clothing"]:
|
| 951 |
+
clothing_contexts = ["wear", "fashion", "style", "outfit", "dressed", "clothes",
|
| 952 |
+
"wardrobe", "fabric", "color", "comfortable", "fit", "size"]
|
| 953 |
+
|
| 954 |
+
has_clothing_context = any(context in text_lower for context in clothing_contexts)
|
| 955 |
+
return has_clothing_context
|
| 956 |
+
|
| 957 |
+
# For most words, more relaxed context check
|
| 958 |
+
return True
|
| 959 |
+
|
| 960 |
+
def _check_complexity_value(self, text, word):
|
| 961 |
+
"""Check if the example has appropriate complexity and educational value."""
|
| 962 |
+
# Count words for basic complexity check
|
| 963 |
+
word_count = len(text.split())
|
| 964 |
+
|
| 965 |
+
# Very short examples may not provide enough context
|
| 966 |
+
if word_count < 5:
|
| 967 |
+
return False
|
| 968 |
+
|
| 969 |
+
# Very long examples might be too complex for beginners
|
| 970 |
+
if word_count > 20:
|
| 971 |
+
return False
|
| 972 |
+
|
| 973 |
+
# Check for overly complex words that might be difficult for language learners
|
| 974 |
+
complex_words = 0
|
| 975 |
+
for w in text.split():
|
| 976 |
+
w = w.lower().strip('.,!?":;()')
|
| 977 |
+
if len(w) > 8 and w != word.lower(): # Don't count the target word as complex
|
| 978 |
+
complex_words += 1
|
| 979 |
+
|
| 980 |
+
# If more than 20% of words are complex, maybe too difficult
|
| 981 |
+
if complex_words / word_count > 0.2:
|
| 982 |
+
return False
|
| 983 |
+
|
| 984 |
+
# Prefer examples where the target word appears near the beginning or middle
|
| 985 |
+
# for better context understanding
|
| 986 |
+
word_position = text.lower().find(word.lower())
|
| 987 |
+
if word_position > len(text) * 0.7: # If word appears only near the end
|
| 988 |
+
return False
|
| 989 |
+
|
| 990 |
+
return True
|
| 991 |
+
|
| 992 |
+
def _get_word_category(self, word):
|
| 993 |
+
"""Determine the most appropriate category for a word with enhanced detection."""
|
| 994 |
+
# Check special cases first
|
| 995 |
+
if word in self.special_cases:
|
| 996 |
+
return self.special_cases[word]
|
| 997 |
+
|
| 998 |
+
# Check predefined categories
|
| 999 |
+
for category, words in self.category_words.items():
|
| 1000 |
+
if word in words:
|
| 1001 |
+
return category
|
| 1002 |
+
|
| 1003 |
+
# Handle compound words
|
| 1004 |
+
if "teddy bear" in word or "teddy" in word:
|
| 1005 |
+
return "toys"
|
| 1006 |
+
|
| 1007 |
+
# Check word endings and patterns
|
| 1008 |
+
if word in self.plural_words:
|
| 1009 |
+
# Check specific types of plurals
|
| 1010 |
+
if word in ["scissors", "pliers", "clippers", "shears"]:
|
| 1011 |
+
return "tools"
|
| 1012 |
+
elif word in ["glasses", "sunglasses", "spectacles", "eyeglasses"]:
|
| 1013 |
+
return "eyewear"
|
| 1014 |
+
elif word in ["pants", "shorts", "jeans", "trousers", "tights", "leggings"]:
|
| 1015 |
+
return "clothing"
|
| 1016 |
+
else:
|
| 1017 |
+
return "noun"
|
| 1018 |
+
elif word.endswith('ing') and len(word) > 5:
|
| 1019 |
+
if word in ["clothing"]:
|
| 1020 |
+
return "uncountable_clothing"
|
| 1021 |
+
return "verb"
|
| 1022 |
+
elif word.endswith(('er', 'est')) and len(word) > 4:
|
| 1023 |
+
# Check if it's actually a comparative/superlative adjective
|
| 1024 |
+
# or a noun ending in 'er' (like "computer")
|
| 1025 |
+
if word.endswith('er') and word[:-2] + 'e' in self.category_words.get("verb", []):
|
| 1026 |
+
return "person" # Like "teacher" from "teach"
|
| 1027 |
+
return "adjective"
|
| 1028 |
+
|
| 1029 |
+
# Default to noun for most words
|
| 1030 |
+
return "noun"
|
| 1031 |
+
|
| 1032 |
+
def _select_diverse_example(self, examples, word):
|
| 1033 |
+
"""Select a diverse example, avoiding recent patterns if possible."""
|
| 1034 |
+
if not examples:
|
| 1035 |
+
return None
|
| 1036 |
+
|
| 1037 |
+
# If only one example, return it
|
| 1038 |
+
if len(examples) == 1:
|
| 1039 |
+
return examples[0]
|
| 1040 |
+
|
| 1041 |
+
# Analyze examples for diversity factors
|
| 1042 |
+
analyzed_examples = []
|
| 1043 |
+
for example in examples:
|
| 1044 |
+
# Calculate distance from word to beginning
|
| 1045 |
+
word_pos = example.lower().find(word.lower())
|
| 1046 |
+
total_length = len(example)
|
| 1047 |
+
position_ratio = word_pos / total_length if total_length > 0 else 0
|
| 1048 |
+
|
| 1049 |
+
# Count sentence complexity (length, word variety)
|
| 1050 |
+
words = example.split()
|
| 1051 |
+
sentence_length = len(words)
|
| 1052 |
+
unique_words = len(set([w.lower() for w in words]))
|
| 1053 |
+
word_variety = unique_words / sentence_length if sentence_length > 0 else 0
|
| 1054 |
+
|
| 1055 |
+
# Detect sentence structure
|
| 1056 |
+
has_question = example.endswith('?')
|
| 1057 |
+
has_dialogue = '"' in example or "'" in example and "said" in example.lower()
|
| 1058 |
+
has_conjunction = any(conj in example.lower() for conj in [" and ", " but ", " or ", " because ", " however "])
|
| 1059 |
+
|
| 1060 |
+
# Create a feature vector for this example
|
| 1061 |
+
features = {
|
| 1062 |
+
"position_ratio": position_ratio,
|
| 1063 |
+
"sentence_length": sentence_length,
|
| 1064 |
+
"word_variety": word_variety,
|
| 1065 |
+
"has_question": has_question,
|
| 1066 |
+
"has_dialogue": has_dialogue,
|
| 1067 |
+
"has_conjunction": has_conjunction
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
analyzed_examples.append((example, features))
|
| 1071 |
+
|
| 1072 |
+
# Get previously used patterns if available
|
| 1073 |
+
prev_patterns = self.recent_templates.get(word, [])
|
| 1074 |
+
|
| 1075 |
+
# Score examples based on diversity from previous patterns
|
| 1076 |
+
scored_examples = []
|
| 1077 |
+
for example, features in analyzed_examples:
|
| 1078 |
+
# Start with base score
|
| 1079 |
+
score = 10
|
| 1080 |
+
|
| 1081 |
+
# Prefer moderate length sentences (not too short, not too long)
|
| 1082 |
+
length_score = 0
|
| 1083 |
+
if 7 <= features["sentence_length"] <= 15:
|
| 1084 |
+
length_score = 5
|
| 1085 |
+
elif 5 <= features["sentence_length"] < 7 or 15 < features["sentence_length"] <= 20:
|
| 1086 |
+
length_score = 3
|
| 1087 |
+
score += length_score
|
| 1088 |
+
|
| 1089 |
+
# Prefer good word variety
|
| 1090 |
+
if features["word_variety"] > 0.8:
|
| 1091 |
+
score += 3
|
| 1092 |
+
elif features["word_variety"] > 0.6:
|
| 1093 |
+
score += 2
|
| 1094 |
+
|
| 1095 |
+
# Slight bonus for different sentence structures
|
| 1096 |
+
if features["has_question"]:
|
| 1097 |
+
score += 1
|
| 1098 |
+
if features["has_dialogue"]:
|
| 1099 |
+
score += 1
|
| 1100 |
+
if features["has_conjunction"]:
|
| 1101 |
+
score += 2
|
| 1102 |
+
|
| 1103 |
+
# Major bonus for differing from recent patterns
|
| 1104 |
+
for prev_pattern in prev_patterns:
|
| 1105 |
+
# Compare this example to previous patterns
|
| 1106 |
+
pattern_similarity = 0
|
| 1107 |
+
|
| 1108 |
+
# Position similarity
|
| 1109 |
+
if abs(features["position_ratio"] - prev_pattern.get("position_ratio", 0)) < 0.2:
|
| 1110 |
+
pattern_similarity += 1
|
| 1111 |
+
|
| 1112 |
+
# Length similarity
|
| 1113 |
+
if abs(features["sentence_length"] - prev_pattern.get("sentence_length", 0)) < 3:
|
| 1114 |
+
pattern_similarity += 1
|
| 1115 |
+
|
| 1116 |
+
# Structure similarity
|
| 1117 |
+
if features["has_question"] == prev_pattern.get("has_question", False):
|
| 1118 |
+
pattern_similarity += 1
|
| 1119 |
+
if features["has_dialogue"] == prev_pattern.get("has_dialogue", False):
|
| 1120 |
+
pattern_similarity += 1
|
| 1121 |
+
if features["has_conjunction"] == prev_pattern.get("has_conjunction", False):
|
| 1122 |
+
pattern_similarity += 1
|
| 1123 |
+
|
| 1124 |
+
# Penalize similarity to recent patterns
|
| 1125 |
+
score -= pattern_similarity
|
| 1126 |
+
|
| 1127 |
+
scored_examples.append((example, features, score))
|
| 1128 |
+
|
| 1129 |
+
# Sort by score (highest first)
|
| 1130 |
+
scored_examples.sort(key=lambda x: x[2], reverse=True)
|
| 1131 |
+
|
| 1132 |
+
# Get the highest scored example
|
| 1133 |
+
best_example = scored_examples[0][0]
|
| 1134 |
+
best_features = scored_examples[0][1]
|
| 1135 |
+
|
| 1136 |
+
# Update recent patterns
|
| 1137 |
+
if word not in self.recent_templates:
|
| 1138 |
+
self.recent_templates[word] = []
|
| 1139 |
+
|
| 1140 |
+
self.recent_templates[word].append(best_features)
|
| 1141 |
+
|
| 1142 |
+
# Keep only the most recent patterns
|
| 1143 |
+
if len(self.recent_templates[word]) > self.max_recent_templates:
|
| 1144 |
+
self.recent_templates[word] = self.recent_templates[word][-self.max_recent_templates:]
|
| 1145 |
+
|
| 1146 |
+
return best_example
|
| 1147 |
+
|
| 1148 |
+
def _select_diverse_template(self, word, category):
|
| 1149 |
+
"""Select a diverse template based on complexity and avoiding recent patterns."""
|
| 1150 |
+
# Determine appropriate template category
|
| 1151 |
+
template_category = category
|
| 1152 |
+
|
| 1153 |
+
# If no templates for this category, use general
|
| 1154 |
+
if template_category not in self.templates:
|
| 1155 |
+
template_category = "noun" if category in ["clothing", "tools", "jewelry", "eyewear"] else "general"
|
| 1156 |
+
|
| 1157 |
+
# Get template subcategories for this type (basic, intermediate, advanced)
|
| 1158 |
+
if template_category in self.templates:
|
| 1159 |
+
subcats = list(self.templates[template_category].keys())
|
| 1160 |
+
else:
|
| 1161 |
+
subcats = ["basic", "intermediate", "advanced"]
|
| 1162 |
+
|
| 1163 |
+
# Adjust weights for specific categories to prefer simpler examples
|
| 1164 |
+
# For common everyday objects, animals and toys, prefer simpler examples
|
| 1165 |
+
if category in ["animal", "toys"] or word in ["bear", "teddy bear"]:
|
| 1166 |
+
# 60% basic, 30% intermediate, 10% advanced
|
| 1167 |
+
weights = [0.6, 0.3, 0.1]
|
| 1168 |
+
elif word in self.plural_words or len(word) <= 4:
|
| 1169 |
+
# For simple or plural words, prefer simpler examples
|
| 1170 |
+
# 60% basic, 30% intermediate, 10% advanced
|
| 1171 |
+
weights = [0.6, 0.3, 0.1]
|
| 1172 |
+
else:
|
| 1173 |
+
# Default weights: 50% basic, 30% intermediate, 20% advanced
|
| 1174 |
+
weights = [0.5, 0.3, 0.2]
|
| 1175 |
+
|
| 1176 |
+
complexity = random.choices(subcats, weights=weights[:len(subcats)])[0]
|
| 1177 |
+
|
| 1178 |
+
# Get templates for this category and complexity
|
| 1179 |
+
templates = self.templates.get(template_category, self.templates["general"]).get(complexity, self.templates["general"]["basic"])
|
| 1180 |
+
|
| 1181 |
+
# Initialize template history for this word if not exists
|
| 1182 |
+
word_key = f"{word}_{template_category}"
|
| 1183 |
+
if word_key not in self.template_history:
|
| 1184 |
+
self.template_history[word_key] = []
|
| 1185 |
+
|
| 1186 |
+
# Try to find a template that hasn't been used recently
|
| 1187 |
+
available_templates = [t for t in templates if t not in self.template_history[word_key]]
|
| 1188 |
+
|
| 1189 |
+
# If all templates have been used, reset history
|
| 1190 |
+
if not available_templates:
|
| 1191 |
+
available_templates = templates
|
| 1192 |
+
self.template_history[word_key] = []
|
| 1193 |
+
|
| 1194 |
+
# Select a template
|
| 1195 |
+
template = random.choice(available_templates)
|
| 1196 |
+
|
| 1197 |
+
# Update history
|
| 1198 |
+
self.template_history[word_key].append(template)
|
| 1199 |
+
if len(self.template_history[word_key]) > min(5, len(templates)):
|
| 1200 |
+
self.template_history[word_key] = self.template_history[word_key][-5:]
|
| 1201 |
+
|
| 1202 |
+
return template, complexity
|
| 1203 |
+
|
| 1204 |
+
def _create_example_from_template(self, template, word, category):
|
| 1205 |
+
"""Create an example from a template with proper handling of word forms."""
|
| 1206 |
+
# Special handling for plural words
|
| 1207 |
+
if word in self.plural_words:
|
| 1208 |
+
# Replace articles appropriately for plurals
|
| 1209 |
+
example = template.replace("a {word}", "{word}").replace("an {word}", "{word}")
|
| 1210 |
+
example = example.replace("the {word} is", "the {word} are")
|
| 1211 |
+
example = example.replace("This {word} is", "These {word} are")
|
| 1212 |
+
example = example.replace("this {word} is", "these {word} are")
|
| 1213 |
+
# Special handling for uncountable nouns
|
| 1214 |
+
elif category == "uncountable_clothing":
|
| 1215 |
+
# Remove articles for uncountable nouns
|
| 1216 |
+
example = template.replace("a {word}", "{word}").replace("an {word}", "{word}")
|
| 1217 |
+
# Regular handling for other words
|
| 1218 |
+
else:
|
| 1219 |
+
example = template
|
| 1220 |
+
|
| 1221 |
+
# Insert word into template
|
| 1222 |
+
example = example.format(word=word)
|
| 1223 |
+
|
| 1224 |
+
# Ensure first letter is capitalized
|
| 1225 |
+
if example and not example[0].isupper():
|
| 1226 |
+
example = example[0].upper() + example[1:]
|
| 1227 |
+
|
| 1228 |
+
# Ensure ending punctuation
|
| 1229 |
+
if example and not example[-1] in '.!?':
|
| 1230 |
+
example += '.'
|
| 1231 |
+
|
| 1232 |
+
return example
|
| 1233 |
+
|
| 1234 |
+
def _get_free_dictionary_examples(self, word):
|
| 1235 |
+
"""Get all examples from Free Dictionary API."""
|
| 1236 |
+
try:
|
| 1237 |
+
cache_path = os.path.join(self.cache_dir, f"freedict_{word}.json")
|
| 1238 |
+
|
| 1239 |
+
# Try to load from cache first
|
| 1240 |
+
if os.path.exists(cache_path):
|
| 1241 |
+
with open(cache_path, 'r') as f:
|
| 1242 |
+
cached_data = json.load(f)
|
| 1243 |
+
if cached_data.get('examples'):
|
| 1244 |
+
return cached_data['examples']
|
| 1245 |
+
|
| 1246 |
+
# If not in cache, fetch from API
|
| 1247 |
+
url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
| 1248 |
+
response = requests.get(url, timeout=5)
|
| 1249 |
+
|
| 1250 |
+
if response.status_code != 200:
|
| 1251 |
+
return []
|
| 1252 |
+
|
| 1253 |
+
data = response.json()
|
| 1254 |
+
|
| 1255 |
+
# Extract example sentences
|
| 1256 |
+
examples = []
|
| 1257 |
+
if isinstance(data, list) and len(data) > 0:
|
| 1258 |
+
for meaning in data[0].get('meanings', []):
|
| 1259 |
+
for definition in meaning.get('definitions', []):
|
| 1260 |
+
if 'example' in definition and definition['example']:
|
| 1261 |
+
examples.append(definition['example'])
|
| 1262 |
+
|
| 1263 |
+
# Cache results
|
| 1264 |
+
if examples:
|
| 1265 |
+
with open(cache_path, 'w') as f:
|
| 1266 |
+
json.dump({'examples': examples}, f)
|
| 1267 |
+
|
| 1268 |
+
return examples
|
| 1269 |
+
except Exception as e:
|
| 1270 |
+
if self.debug:
|
| 1271 |
+
print(f">>> Free Dictionary API error: {e}")
|
| 1272 |
+
return []
|
| 1273 |
+
|
| 1274 |
+
def _get_wordnik_examples(self, word):
|
| 1275 |
+
"""Get all examples from Wordnik API."""
|
| 1276 |
+
try:
|
| 1277 |
+
cache_path = os.path.join(self.cache_dir, f"wordnik_{word}.json")
|
| 1278 |
+
|
| 1279 |
+
# Try to load from cache first
|
| 1280 |
+
if os.path.exists(cache_path):
|
| 1281 |
+
with open(cache_path, 'r') as f:
|
| 1282 |
+
cached_data = json.load(f)
|
| 1283 |
+
if cached_data.get('examples'):
|
| 1284 |
+
return cached_data['examples']
|
| 1285 |
+
|
| 1286 |
+
# If not in cache, fetch from API
|
| 1287 |
+
# Note: This API might need an API key in practice
|
| 1288 |
+
url = f"https://api.wordnik.com/v4/word.json/{word}/examples"
|
| 1289 |
+
response = requests.get(url, timeout=5)
|
| 1290 |
+
|
| 1291 |
+
if response.status_code != 200:
|
| 1292 |
+
return []
|
| 1293 |
+
|
| 1294 |
+
data = response.json()
|
| 1295 |
+
|
| 1296 |
+
examples = []
|
| 1297 |
+
for example in data.get('examples', []):
|
| 1298 |
+
if 'text' in example and example['text']:
|
| 1299 |
+
examples.append(example['text'])
|
| 1300 |
+
|
| 1301 |
+
# Cache results
|
| 1302 |
+
if examples:
|
| 1303 |
+
with open(cache_path, 'w') as f:
|
| 1304 |
+
json.dump({'examples': examples}, f)
|
| 1305 |
+
|
| 1306 |
+
return examples
|
| 1307 |
+
except Exception as e:
|
| 1308 |
+
if self.debug:
|
| 1309 |
+
print(f">>> Wordnik API error: {e}")
|
| 1310 |
+
return []
|
| 1311 |
+
|
| 1312 |
+
def _clean_sentence(self, sentence):
|
| 1313 |
+
"""Clean and format a sentence."""
|
| 1314 |
+
# Remove quotes, extra spaces, etc.
|
| 1315 |
+
sentence = sentence.strip()
|
| 1316 |
+
sentence = re.sub(r'\s+', ' ', sentence)
|
| 1317 |
+
|
| 1318 |
+
# Remove metadata markers
|
| 1319 |
+
sentence = re.sub(r'\[.*?\]', '', sentence)
|
| 1320 |
+
|
| 1321 |
+
# Capitalize first letter
|
| 1322 |
+
if sentence and not sentence[0].isupper():
|
| 1323 |
+
sentence = sentence[0].upper() + sentence[1:]
|
| 1324 |
+
|
| 1325 |
+
# Ensure ending punctuation
|
| 1326 |
+
if sentence and not sentence[-1] in '.!?':
|
| 1327 |
+
sentence += '.'
|
| 1328 |
+
|
| 1329 |
+
return sentence
|
| 1330 |
+
|
| 1331 |
+
def _translate(self, text, target_language):
|
| 1332 |
+
"""Translate text using the provided translation function."""
|
| 1333 |
+
if self.translate_func:
|
| 1334 |
+
try:
|
| 1335 |
+
return self.translate_func(text, target_language)
|
| 1336 |
+
except Exception as e:
|
| 1337 |
+
if self.debug:
|
| 1338 |
+
print(f">>> Translation error: {e}")
|
| 1339 |
+
return ""
|
| 1340 |
+
else:
|
| 1341 |
+
return f"[Translation to {target_language}]"
|
gamification.py
ADDED
|
@@ -0,0 +1,1501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gamification Module for Vocam Language Learning App
|
| 3 |
+
|
| 4 |
+
This module implements gamification features:
|
| 5 |
+
1. Achievement Badges & Milestones
|
| 6 |
+
2. Daily Challenges
|
| 7 |
+
3. Streaks & Consistency Rewards
|
| 8 |
+
4. Progress Bars & Visual Feedback
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import streamlit as st
|
| 12 |
+
import os
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import base64
|
| 17 |
+
import json
|
| 18 |
+
import random
|
| 19 |
+
import tempfile
|
| 20 |
+
import time
|
| 21 |
+
from datetime import date, datetime, timedelta
|
| 22 |
+
from collections import defaultdict
|
| 23 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 24 |
+
import io
|
| 25 |
+
import sqlite3
|
| 26 |
+
|
| 27 |
+
class GamificationSystem:
|
| 28 |
+
def __init__(self, db_path="language_learning.db", translate_func=None):
|
| 29 |
+
"""Initialize the gamification system."""
|
| 30 |
+
self.db_path = db_path
|
| 31 |
+
self.translate_func = translate_func # Can be None initially
|
| 32 |
+
self.initialize_state()
|
| 33 |
+
self.load_game_state()
|
| 34 |
+
|
| 35 |
+
# Create asset directories if they don't exist
|
| 36 |
+
self.ensure_asset_directories()
|
| 37 |
+
|
| 38 |
+
# Check if we need to update streak and challenges for today
|
| 39 |
+
self.check_daily_updates()
|
| 40 |
+
|
| 41 |
+
def ensure_asset_directories(self):
|
| 42 |
+
"""Create necessary directories for gamification assets."""
|
| 43 |
+
os.makedirs("assets/badges", exist_ok=True)
|
| 44 |
+
os.makedirs("assets/tree", exist_ok=True)
|
| 45 |
+
|
| 46 |
+
def initialize_state(self):
|
| 47 |
+
"""Initialize session state variables for gamification."""
|
| 48 |
+
# Achievement and badge system
|
| 49 |
+
if 'achievements' not in st.session_state:
|
| 50 |
+
st.session_state['achievements'] = {}
|
| 51 |
+
if 'badges' not in st.session_state:
|
| 52 |
+
st.session_state['badges'] = {}
|
| 53 |
+
if 'displayed_achievements' not in st.session_state:
|
| 54 |
+
st.session_state['displayed_achievements'] = set()
|
| 55 |
+
|
| 56 |
+
# Daily challenges
|
| 57 |
+
if 'daily_challenges' not in st.session_state:
|
| 58 |
+
st.session_state['daily_challenges'] = []
|
| 59 |
+
if 'daily_challenges_completed' not in st.session_state:
|
| 60 |
+
st.session_state['daily_challenges_completed'] = set()
|
| 61 |
+
if 'word_of_the_day' not in st.session_state:
|
| 62 |
+
st.session_state['word_of_the_day'] = None
|
| 63 |
+
if 'word_of_the_day_date' not in st.session_state:
|
| 64 |
+
st.session_state['word_of_the_day_date'] = None
|
| 65 |
+
|
| 66 |
+
# Streak system
|
| 67 |
+
if 'streak_days' not in st.session_state:
|
| 68 |
+
st.session_state['streak_days'] = 0
|
| 69 |
+
if 'last_active_date' not in st.session_state:
|
| 70 |
+
st.session_state['last_active_date'] = None
|
| 71 |
+
if 'streak_savers' not in st.session_state:
|
| 72 |
+
st.session_state['streak_savers'] = 0
|
| 73 |
+
|
| 74 |
+
# Progress tracking
|
| 75 |
+
if 'points' not in st.session_state:
|
| 76 |
+
st.session_state['points'] = 0
|
| 77 |
+
if 'level' not in st.session_state:
|
| 78 |
+
st.session_state['level'] = 1
|
| 79 |
+
if 'category_progress' not in st.session_state:
|
| 80 |
+
st.session_state['category_progress'] = {}
|
| 81 |
+
if 'vocabulary_tree' not in st.session_state:
|
| 82 |
+
st.session_state['vocabulary_tree'] = {
|
| 83 |
+
'size': 1,
|
| 84 |
+
'leaves': 0,
|
| 85 |
+
'fruit': 0,
|
| 86 |
+
'level': 1
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
def check_daily_updates(self):
|
| 90 |
+
"""Check and update streak and challenges for the current day."""
|
| 91 |
+
self.check_streak()
|
| 92 |
+
self.generate_daily_challenges()
|
| 93 |
+
self.generate_word_of_the_day()
|
| 94 |
+
self.update_category_progress()
|
| 95 |
+
|
| 96 |
+
#=========================================================================
|
| 97 |
+
# 1. Achievement Badges & Milestones
|
| 98 |
+
#=========================================================================
|
| 99 |
+
|
| 100 |
+
def award_achievement(self, achievement_id, title, description, points=25):
|
| 101 |
+
"""Award an achievement to the user if they don't already have it."""
|
| 102 |
+
if achievement_id not in st.session_state.achievements:
|
| 103 |
+
st.session_state.achievements[achievement_id] = {
|
| 104 |
+
"id": achievement_id,
|
| 105 |
+
"title": title,
|
| 106 |
+
"description": description,
|
| 107 |
+
"date_earned": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 108 |
+
}
|
| 109 |
+
st.session_state.displayed_achievements.add(achievement_id)
|
| 110 |
+
|
| 111 |
+
# Award points
|
| 112 |
+
self.add_points(points, f"Achievement: {title}")
|
| 113 |
+
|
| 114 |
+
# Show notification
|
| 115 |
+
st.toast(f"🏆 Achievement Unlocked: {title}")
|
| 116 |
+
|
| 117 |
+
# Save game state
|
| 118 |
+
self.save_game_state()
|
| 119 |
+
return True
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
def award_badge(self, badge_id, title, category="general"):
|
| 123 |
+
"""Award a badge to the user if they don't already have it."""
|
| 124 |
+
if badge_id not in st.session_state.badges:
|
| 125 |
+
# Create badge image path
|
| 126 |
+
badge_image = self.create_badge_image(badge_id, title, category)
|
| 127 |
+
|
| 128 |
+
# Store badge info
|
| 129 |
+
st.session_state.badges[badge_id] = {
|
| 130 |
+
"id": badge_id,
|
| 131 |
+
"title": title,
|
| 132 |
+
"category": category,
|
| 133 |
+
"image_path": badge_image,
|
| 134 |
+
"date_earned": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
# Show notification
|
| 138 |
+
st.toast(f"🏅 Badge Earned: {title}")
|
| 139 |
+
|
| 140 |
+
# Save game state
|
| 141 |
+
self.save_game_state()
|
| 142 |
+
return True
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
def create_badge_image(self, badge_id, title, category):
|
| 146 |
+
"""Create a badge image and return the path."""
|
| 147 |
+
try:
|
| 148 |
+
badge_path = f"assets/badges/{badge_id}.png"
|
| 149 |
+
|
| 150 |
+
# If badge already exists, return path
|
| 151 |
+
if os.path.exists(badge_path):
|
| 152 |
+
return badge_path
|
| 153 |
+
|
| 154 |
+
# Create a new badge image
|
| 155 |
+
size = 200
|
| 156 |
+
img = Image.new('RGBA', (size, size), (255, 255, 255, 0))
|
| 157 |
+
draw = ImageDraw.Draw(img)
|
| 158 |
+
|
| 159 |
+
# Select color based on category
|
| 160 |
+
colors = {
|
| 161 |
+
"general": (52, 152, 219), # Blue
|
| 162 |
+
"streak": (231, 76, 60), # Red
|
| 163 |
+
"vocabulary": (46, 204, 113), # Green
|
| 164 |
+
"quiz": (155, 89, 182), # Purple
|
| 165 |
+
"challenge": (241, 196, 15) # Yellow
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
color = colors.get(category, colors["general"])
|
| 169 |
+
|
| 170 |
+
# Draw badge circle
|
| 171 |
+
draw.ellipse([(10, 10), (size-10, size-10)], fill=color, outline=(255, 255, 255), width=3)
|
| 172 |
+
|
| 173 |
+
# Add badge initial (first letter of title)
|
| 174 |
+
try:
|
| 175 |
+
# Try to load font, fallback to default if not available
|
| 176 |
+
try:
|
| 177 |
+
font = ImageFont.truetype("Arial.ttf", 80)
|
| 178 |
+
except:
|
| 179 |
+
font = ImageFont.load_default()
|
| 180 |
+
|
| 181 |
+
# Get initial from title
|
| 182 |
+
initial = title[0].upper()
|
| 183 |
+
|
| 184 |
+
# Calculate text position (center)
|
| 185 |
+
text_width, text_height = font.getsize(initial) if hasattr(font, 'getsize') else (50, 50)
|
| 186 |
+
text_position = ((size - text_width) // 2, (size - text_height) // 2)
|
| 187 |
+
|
| 188 |
+
# Draw text
|
| 189 |
+
draw.text(text_position, initial, fill=(255, 255, 255), font=font)
|
| 190 |
+
except Exception as e:
|
| 191 |
+
print(f"Error adding text to badge: {e}")
|
| 192 |
+
# Add a simple mark if text fails
|
| 193 |
+
draw.rectangle([(size//2-20, size//2-20), (size//2+20, size//2+20)], fill=(255, 255, 255))
|
| 194 |
+
|
| 195 |
+
# Save image
|
| 196 |
+
img.save(badge_path)
|
| 197 |
+
return badge_path
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
print(f"Error creating badge image: {e}")
|
| 201 |
+
return None
|
| 202 |
+
|
| 203 |
+
def check_achievements(self, action_type, **kwargs):
|
| 204 |
+
"""Check for achievements based on user actions."""
|
| 205 |
+
if action_type == "word_learned":
|
| 206 |
+
word = kwargs.get('word', '')
|
| 207 |
+
category = kwargs.get('category', '')
|
| 208 |
+
language = kwargs.get('language', '')
|
| 209 |
+
|
| 210 |
+
# First word achievement
|
| 211 |
+
if len(st.session_state.achievements) == 0:
|
| 212 |
+
self.award_achievement("first_word", "First Steps", "Learn your first word")
|
| 213 |
+
self.award_badge("first_word", "First Word", "vocabulary")
|
| 214 |
+
|
| 215 |
+
# Category-specific achievements
|
| 216 |
+
self.check_category_achievements(category)
|
| 217 |
+
|
| 218 |
+
# Language-specific achievements
|
| 219 |
+
self.check_language_achievements(language)
|
| 220 |
+
|
| 221 |
+
elif action_type == "quiz_completed":
|
| 222 |
+
score = kwargs.get('score', 0)
|
| 223 |
+
total = kwargs.get('total', 0)
|
| 224 |
+
|
| 225 |
+
# First quiz achievement
|
| 226 |
+
if "first_quiz" not in st.session_state.achievements:
|
| 227 |
+
self.award_achievement("first_quiz", "Quiz Taker", "Complete your first quiz")
|
| 228 |
+
self.award_badge("quiz_taker", "Quiz Taker", "quiz")
|
| 229 |
+
|
| 230 |
+
# Perfect quiz achievement
|
| 231 |
+
if score == total and total >= 5:
|
| 232 |
+
self.award_achievement("perfect_quiz", "Perfect Score", "Get a perfect score on a quiz")
|
| 233 |
+
self.award_badge("perfect_score", "Perfect Score", "quiz")
|
| 234 |
+
|
| 235 |
+
def check_category_achievements(self, category):
|
| 236 |
+
"""Check for category-specific achievements."""
|
| 237 |
+
if not category:
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
# Get all vocabulary
|
| 241 |
+
vocab_data = self.get_all_vocabulary()
|
| 242 |
+
|
| 243 |
+
# Count words in this category
|
| 244 |
+
category_count = sum(1 for word in vocab_data if word and 'category' in word and word['category'] == category)
|
| 245 |
+
|
| 246 |
+
# Award achievements based on count
|
| 247 |
+
if category_count >= 5:
|
| 248 |
+
self.award_achievement(
|
| 249 |
+
f"{category}_5",
|
| 250 |
+
f"{category.title()} Collector",
|
| 251 |
+
f"Learn 5 {category} words"
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
if category_count >= 10:
|
| 255 |
+
self.award_achievement(
|
| 256 |
+
f"{category}_10",
|
| 257 |
+
f"{category.title()} Master",
|
| 258 |
+
f"Learn 10 {category} words"
|
| 259 |
+
)
|
| 260 |
+
self.award_badge(f"{category}_master", f"{category.title()} Master", "vocabulary")
|
| 261 |
+
|
| 262 |
+
def check_language_achievements(self, language):
|
| 263 |
+
"""Check for language-specific achievements."""
|
| 264 |
+
if not language:
|
| 265 |
+
return
|
| 266 |
+
|
| 267 |
+
# Get all vocabulary
|
| 268 |
+
vocab_data = self.get_all_vocabulary()
|
| 269 |
+
|
| 270 |
+
# Count words in this language
|
| 271 |
+
language_count = sum(1 for word in vocab_data
|
| 272 |
+
if word and 'language_translated' in word and word['language_translated'] == language)
|
| 273 |
+
|
| 274 |
+
# Get language name
|
| 275 |
+
language_name = self.get_language_name(language)
|
| 276 |
+
|
| 277 |
+
# Award achievements based on count
|
| 278 |
+
if language_count >= 10:
|
| 279 |
+
self.award_achievement(
|
| 280 |
+
f"{language}_10",
|
| 281 |
+
f"{language_name} Beginner",
|
| 282 |
+
f"Learn 10 words in {language_name}"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
if language_count >= 25:
|
| 286 |
+
self.award_achievement(
|
| 287 |
+
f"{language}_25",
|
| 288 |
+
f"{language_name} Enthusiast",
|
| 289 |
+
f"Learn 25 words in {language_name}"
|
| 290 |
+
)
|
| 291 |
+
self.award_badge(f"{language}_enthusiast", f"{language_name} Enthusiast", "vocabulary")
|
| 292 |
+
|
| 293 |
+
if language_count >= 50:
|
| 294 |
+
self.award_achievement(
|
| 295 |
+
f"{language}_50",
|
| 296 |
+
f"{language_name} Speaker",
|
| 297 |
+
f"Learn 50 words in {language_name}"
|
| 298 |
+
)
|
| 299 |
+
self.award_badge(f"{language}_speaker", f"{language_name} Speaker", "vocabulary")
|
| 300 |
+
|
| 301 |
+
def display_achievements(self):
|
| 302 |
+
"""Display earned achievements in the UI."""
|
| 303 |
+
st.subheader("🏆 Achievements")
|
| 304 |
+
|
| 305 |
+
if not st.session_state.achievements:
|
| 306 |
+
st.info("You haven't earned any achievements yet. Keep learning to unlock them!")
|
| 307 |
+
return
|
| 308 |
+
|
| 309 |
+
# Group achievements by type
|
| 310 |
+
achievement_types = {
|
| 311 |
+
"Streaks": [],
|
| 312 |
+
"Vocabulary": [],
|
| 313 |
+
"Quizzes": [],
|
| 314 |
+
"Challenges": [],
|
| 315 |
+
"Other": []
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
for achievement_id, achievement in st.session_state.achievements.items():
|
| 319 |
+
if "streak" in achievement_id:
|
| 320 |
+
achievement_types["Streaks"].append(achievement)
|
| 321 |
+
elif "word" in achievement_id or "_5" in achievement_id or "_10" in achievement_id or "_25" in achievement_id or "_50" in achievement_id:
|
| 322 |
+
achievement_types["Vocabulary"].append(achievement)
|
| 323 |
+
elif "quiz" in achievement_id:
|
| 324 |
+
achievement_types["Quizzes"].append(achievement)
|
| 325 |
+
elif "challenge" in achievement_id:
|
| 326 |
+
achievement_types["Challenges"].append(achievement)
|
| 327 |
+
else:
|
| 328 |
+
achievement_types["Other"].append(achievement)
|
| 329 |
+
|
| 330 |
+
# Display achievements by type
|
| 331 |
+
for category, achievements in achievement_types.items():
|
| 332 |
+
if achievements:
|
| 333 |
+
with st.expander(f"{category} ({len(achievements)})", expanded=(category == "Recent")):
|
| 334 |
+
for achievement in sorted(achievements, key=lambda x: x.get("date_earned", ""), reverse=True):
|
| 335 |
+
st.markdown(f"**{achievement['title']}**: {achievement['description']}")
|
| 336 |
+
st.markdown(f"*Earned on {achievement.get('date_earned', 'Unknown').split()[0]}*")
|
| 337 |
+
st.markdown("---")
|
| 338 |
+
|
| 339 |
+
def display_badges(self):
|
| 340 |
+
"""Display earned badges in the UI."""
|
| 341 |
+
st.subheader("🏅 Badges")
|
| 342 |
+
|
| 343 |
+
if not st.session_state.badges:
|
| 344 |
+
st.info("You haven't earned any badges yet. Complete achievements to earn badges!")
|
| 345 |
+
return
|
| 346 |
+
|
| 347 |
+
# Display badges in a grid
|
| 348 |
+
badges = list(st.session_state.badges.values())
|
| 349 |
+
|
| 350 |
+
# Split badges into rows of 3
|
| 351 |
+
badge_rows = [badges[i:i+3] for i in range(0, len(badges), 3)]
|
| 352 |
+
|
| 353 |
+
for row in badge_rows:
|
| 354 |
+
cols = st.columns(3)
|
| 355 |
+
|
| 356 |
+
for i, badge in enumerate(row):
|
| 357 |
+
with cols[i]:
|
| 358 |
+
try:
|
| 359 |
+
if os.path.exists(badge["image_path"]):
|
| 360 |
+
st.image(badge["image_path"], width=100)
|
| 361 |
+
except:
|
| 362 |
+
st.markdown("🏅")
|
| 363 |
+
|
| 364 |
+
st.markdown(f"**{badge['title']}**")
|
| 365 |
+
earned_date = badge.get('date_earned', 'Unknown').split()[0]
|
| 366 |
+
st.markdown(f"*Earned: {earned_date}*")
|
| 367 |
+
|
| 368 |
+
#=========================================================================
|
| 369 |
+
# 2. Daily Challenges
|
| 370 |
+
#=========================================================================
|
| 371 |
+
|
| 372 |
+
def generate_daily_challenges(self):
|
| 373 |
+
"""Generate daily challenges based on user's vocabulary and progress."""
|
| 374 |
+
today = date.today().strftime("%Y-%m-%d")
|
| 375 |
+
|
| 376 |
+
# If we already generated challenges today, return those
|
| 377 |
+
if st.session_state.daily_challenges and len(st.session_state.daily_challenges) > 0:
|
| 378 |
+
if st.session_state.daily_challenges[0].get('date') == today:
|
| 379 |
+
return st.session_state.daily_challenges
|
| 380 |
+
|
| 381 |
+
# Get vocabulary data
|
| 382 |
+
vocab_data = self.get_all_vocabulary()
|
| 383 |
+
|
| 384 |
+
# Count words by category and language
|
| 385 |
+
categories = defaultdict(int)
|
| 386 |
+
languages = defaultdict(int)
|
| 387 |
+
|
| 388 |
+
for word in vocab_data:
|
| 389 |
+
if word and 'category' in word and word['category']:
|
| 390 |
+
categories[word['category']] += 1
|
| 391 |
+
|
| 392 |
+
if word and 'language_translated' in word:
|
| 393 |
+
languages[word['language_translated']] += 1
|
| 394 |
+
|
| 395 |
+
# Generate 3 daily challenges
|
| 396 |
+
challenges = []
|
| 397 |
+
|
| 398 |
+
# Challenge 1: Category-based challenge
|
| 399 |
+
if categories:
|
| 400 |
+
# Find categories with fewer items for targeted learning
|
| 401 |
+
sorted_categories = sorted(categories.items(), key=lambda x: x[1])
|
| 402 |
+
target_category = sorted_categories[0][0] if sorted_categories else "food"
|
| 403 |
+
target_count = random.randint(2, 4)
|
| 404 |
+
|
| 405 |
+
challenges.append({
|
| 406 |
+
"id": f"category_{today}_{target_category}",
|
| 407 |
+
"date": today,
|
| 408 |
+
"title": f"Learn {target_count} {target_category} words",
|
| 409 |
+
"description": f"Add {target_count} new {target_category} words to your vocabulary",
|
| 410 |
+
"type": "category",
|
| 411 |
+
"target": target_category,
|
| 412 |
+
"target_count": target_count,
|
| 413 |
+
"current_count": 0,
|
| 414 |
+
"completed": False,
|
| 415 |
+
"points": 20
|
| 416 |
+
})
|
| 417 |
+
|
| 418 |
+
# Challenge 2: Quiz challenge
|
| 419 |
+
challenges.append({
|
| 420 |
+
"id": f"quiz_{today}",
|
| 421 |
+
"date": today,
|
| 422 |
+
"title": "Ace a Quiz",
|
| 423 |
+
"description": "Complete a quiz with at least 80% accuracy",
|
| 424 |
+
"type": "quiz",
|
| 425 |
+
"target_accuracy": 80,
|
| 426 |
+
"completed": False,
|
| 427 |
+
"points": 25
|
| 428 |
+
})
|
| 429 |
+
|
| 430 |
+
# Challenge 3: Language-specific challenge
|
| 431 |
+
if languages:
|
| 432 |
+
user_language = st.session_state.get('target_language', 'es') # Default to Spanish
|
| 433 |
+
target_count = random.randint(3, 5)
|
| 434 |
+
|
| 435 |
+
challenges.append({
|
| 436 |
+
"id": f"language_{today}_{user_language}",
|
| 437 |
+
"date": today,
|
| 438 |
+
"title": f"Learn {target_count} words in {self.get_language_name(user_language)}",
|
| 439 |
+
"description": f"Add {target_count} new words in {self.get_language_name(user_language)}",
|
| 440 |
+
"type": "language",
|
| 441 |
+
"target": user_language,
|
| 442 |
+
"target_count": target_count,
|
| 443 |
+
"current_count": 0,
|
| 444 |
+
"completed": False,
|
| 445 |
+
"points": 15
|
| 446 |
+
})
|
| 447 |
+
|
| 448 |
+
# Save the challenges
|
| 449 |
+
st.session_state.daily_challenges = challenges
|
| 450 |
+
st.session_state.daily_challenges_completed = set()
|
| 451 |
+
|
| 452 |
+
# Save state
|
| 453 |
+
self.save_game_state()
|
| 454 |
+
|
| 455 |
+
return challenges
|
| 456 |
+
|
| 457 |
+
def complete_challenge(self, challenge_id):
|
| 458 |
+
"""Mark a challenge as completed and award points."""
|
| 459 |
+
for challenge in st.session_state.daily_challenges:
|
| 460 |
+
if challenge["id"] == challenge_id and not challenge["completed"]:
|
| 461 |
+
challenge["completed"] = True
|
| 462 |
+
st.session_state.daily_challenges_completed.add(challenge_id)
|
| 463 |
+
|
| 464 |
+
# Award points
|
| 465 |
+
self.add_points(challenge["points"], f"Challenge: {challenge['title']}")
|
| 466 |
+
|
| 467 |
+
# Show notification
|
| 468 |
+
st.toast(f"🎯 Challenge completed: {challenge['title']}")
|
| 469 |
+
|
| 470 |
+
# Check for achievement
|
| 471 |
+
if len(st.session_state.daily_challenges_completed) >= 10:
|
| 472 |
+
self.award_achievement(
|
| 473 |
+
"challenge_master",
|
| 474 |
+
"Challenge Master",
|
| 475 |
+
"Complete 10 daily challenges",
|
| 476 |
+
50
|
| 477 |
+
)
|
| 478 |
+
self.award_badge("challenge_master", "Challenge Master", "challenge")
|
| 479 |
+
|
| 480 |
+
# Add to vocabulary tree
|
| 481 |
+
self.update_vocabulary_tree(0.05)
|
| 482 |
+
|
| 483 |
+
# Save state
|
| 484 |
+
self.save_game_state()
|
| 485 |
+
return True
|
| 486 |
+
return False
|
| 487 |
+
|
| 488 |
+
def check_challenge_progress(self, word_original=None, word_translated=None, language=None, quiz_score=None, quiz_total=None):
|
| 489 |
+
"""Check if any challenges are completed by user actions."""
|
| 490 |
+
# Word learning challenges
|
| 491 |
+
if word_original and language:
|
| 492 |
+
category = self.get_object_category(word_original)
|
| 493 |
+
|
| 494 |
+
for challenge in st.session_state.daily_challenges:
|
| 495 |
+
if challenge["completed"]:
|
| 496 |
+
continue
|
| 497 |
+
|
| 498 |
+
if challenge["type"] == "category" and challenge["target"] == category:
|
| 499 |
+
challenge["current_count"] += 1
|
| 500 |
+
if challenge["current_count"] >= challenge["target_count"]:
|
| 501 |
+
self.complete_challenge(challenge["id"])
|
| 502 |
+
|
| 503 |
+
elif challenge["type"] == "language" and challenge["target"] == language:
|
| 504 |
+
challenge["current_count"] += 1
|
| 505 |
+
if challenge["current_count"] >= challenge["target_count"]:
|
| 506 |
+
self.complete_challenge(challenge["id"])
|
| 507 |
+
|
| 508 |
+
# Check word of the day
|
| 509 |
+
wotd = st.session_state.word_of_the_day
|
| 510 |
+
if wotd and not wotd["saved"] and word_original.lower() == wotd["original"].lower():
|
| 511 |
+
wotd["saved"] = True
|
| 512 |
+
self.add_points(wotd["points"], "Word of the Day bonus")
|
| 513 |
+
st.toast(f"🌟 Word of the Day bonus: +{wotd['points']} points!")
|
| 514 |
+
|
| 515 |
+
# Award achievement if this is first time
|
| 516 |
+
if "word_of_the_day" not in st.session_state.achievements:
|
| 517 |
+
self.award_achievement(
|
| 518 |
+
"word_of_the_day",
|
| 519 |
+
"Word Collector",
|
| 520 |
+
"Learn the Word of the Day"
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# Quiz challenges
|
| 524 |
+
if quiz_score is not None and quiz_total is not None and quiz_total > 0:
|
| 525 |
+
accuracy = (quiz_score / quiz_total) * 100
|
| 526 |
+
|
| 527 |
+
for challenge in st.session_state.daily_challenges:
|
| 528 |
+
if challenge["type"] == "quiz" and not challenge["completed"]:
|
| 529 |
+
if accuracy >= challenge["target_accuracy"]:
|
| 530 |
+
self.complete_challenge(challenge["id"])
|
| 531 |
+
|
| 532 |
+
def generate_word_of_the_day(self):
|
| 533 |
+
"""Generate a word of the day with guaranteed translation."""
|
| 534 |
+
today = date.today().strftime("%Y-%m-%d")
|
| 535 |
+
|
| 536 |
+
# Return existing word if already generated today
|
| 537 |
+
if st.session_state.word_of_the_day and st.session_state.word_of_the_day_date == today:
|
| 538 |
+
return st.session_state.word_of_the_day
|
| 539 |
+
|
| 540 |
+
# Words to potentially use as word of the day (common useful words)
|
| 541 |
+
potential_words = [
|
| 542 |
+
{"word": "hello", "category": "greeting"},
|
| 543 |
+
{"word": "thank you", "category": "courtesy"},
|
| 544 |
+
{"word": "please", "category": "courtesy"},
|
| 545 |
+
{"word": "friend", "category": "people"},
|
| 546 |
+
{"word": "food", "category": "essential"},
|
| 547 |
+
{"word": "water", "category": "essential"},
|
| 548 |
+
{"word": "help", "category": "emergency"},
|
| 549 |
+
{"word": "good", "category": "description"},
|
| 550 |
+
{"word": "bad", "category": "description"},
|
| 551 |
+
{"word": "yes", "category": "response"},
|
| 552 |
+
{"word": "no", "category": "response"},
|
| 553 |
+
{"word": "goodbye", "category": "greeting"},
|
| 554 |
+
{"word": "sorry", "category": "courtesy"},
|
| 555 |
+
{"word": "excuse me", "category": "courtesy"},
|
| 556 |
+
{"word": "love", "category": "emotion"},
|
| 557 |
+
{"word": "book", "category": "object"},
|
| 558 |
+
{"word": "car", "category": "vehicle"},
|
| 559 |
+
{"word": "house", "category": "place"},
|
| 560 |
+
{"word": "family", "category": "people"},
|
| 561 |
+
{"word": "work", "category": "activity"}
|
| 562 |
+
]
|
| 563 |
+
|
| 564 |
+
# Find words the user doesn't already have
|
| 565 |
+
vocab_data = self.get_all_vocabulary()
|
| 566 |
+
existing_words = set(word['word_original'].lower() for word in vocab_data if 'word_original' in word)
|
| 567 |
+
|
| 568 |
+
# Filter to words not already in vocabulary
|
| 569 |
+
available_words = [word for word in potential_words if word["word"].lower() not in existing_words]
|
| 570 |
+
|
| 571 |
+
# If all words are known, just pick any
|
| 572 |
+
if not available_words:
|
| 573 |
+
available_words = potential_words
|
| 574 |
+
|
| 575 |
+
# Pick a random word
|
| 576 |
+
word_of_the_day = random.choice(available_words)
|
| 577 |
+
|
| 578 |
+
# Get the target language from session state
|
| 579 |
+
target_language = st.session_state.get('target_language', 'es')
|
| 580 |
+
|
| 581 |
+
# Use our robust translation function to ensure we get a proper translation
|
| 582 |
+
translated_word = self.get_translation_for_word(word_of_the_day["word"], target_language)
|
| 583 |
+
|
| 584 |
+
# Set word of the day
|
| 585 |
+
st.session_state.word_of_the_day = {
|
| 586 |
+
"original": word_of_the_day["word"],
|
| 587 |
+
"translated": translated_word,
|
| 588 |
+
"category": word_of_the_day["category"],
|
| 589 |
+
"points": 10,
|
| 590 |
+
"saved": False
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
st.session_state.word_of_the_day_date = today
|
| 594 |
+
|
| 595 |
+
# Save state
|
| 596 |
+
self.save_game_state()
|
| 597 |
+
|
| 598 |
+
return st.session_state.word_of_the_day
|
| 599 |
+
|
| 600 |
+
def display_daily_challenges(self):
|
| 601 |
+
"""Display daily challenges in the UI."""
|
| 602 |
+
challenges = self.generate_daily_challenges()
|
| 603 |
+
|
| 604 |
+
st.subheader("📅 Today's Challenges")
|
| 605 |
+
|
| 606 |
+
# Display word of the day first
|
| 607 |
+
if st.session_state.word_of_the_day:
|
| 608 |
+
st.markdown("### ✨ Word of the Day")
|
| 609 |
+
wotd = st.session_state.word_of_the_day
|
| 610 |
+
|
| 611 |
+
st.markdown(f"**{wotd['original']}** → {wotd['translated']} ({wotd['category']})")
|
| 612 |
+
|
| 613 |
+
if wotd["saved"]:
|
| 614 |
+
st.success(f"✅ Added to vocabulary (+{wotd['points']} points)")
|
| 615 |
+
else:
|
| 616 |
+
st.info(f"Add this word to your vocabulary for {wotd['points']} bonus points!")
|
| 617 |
+
|
| 618 |
+
# Display challenges
|
| 619 |
+
if challenges:
|
| 620 |
+
for challenge in challenges:
|
| 621 |
+
col1, col2 = st.columns([3, 1])
|
| 622 |
+
|
| 623 |
+
with col1:
|
| 624 |
+
if challenge["completed"] or challenge["id"] in st.session_state.daily_challenges_completed:
|
| 625 |
+
st.markdown(f"✅ **{challenge['title']}**")
|
| 626 |
+
st.markdown(f"*{challenge['description']}* (Completed)")
|
| 627 |
+
else:
|
| 628 |
+
st.markdown(f"⭐ **{challenge['title']}**")
|
| 629 |
+
st.markdown(f"*{challenge['description']}*")
|
| 630 |
+
|
| 631 |
+
# Show progress for challenges with a count
|
| 632 |
+
if challenge["type"] in ["category", "language"]:
|
| 633 |
+
current = challenge.get("current_count", 0)
|
| 634 |
+
target = challenge.get("target_count", 1)
|
| 635 |
+
progress = min(1.0, current / target)
|
| 636 |
+
st.progress(progress)
|
| 637 |
+
st.markdown(f"Progress: {current}/{target}")
|
| 638 |
+
|
| 639 |
+
with col2:
|
| 640 |
+
st.markdown(f"+{challenge['points']} pts")
|
| 641 |
+
|
| 642 |
+
st.markdown("---")
|
| 643 |
+
else:
|
| 644 |
+
st.info("No challenges available for today.")
|
| 645 |
+
|
| 646 |
+
#=========================================================================
|
| 647 |
+
# 3. Streaks & Consistency Rewards
|
| 648 |
+
#=========================================================================
|
| 649 |
+
|
| 650 |
+
def check_streak(self):
|
| 651 |
+
"""Update user streak based on last active date."""
|
| 652 |
+
today = date.today()
|
| 653 |
+
|
| 654 |
+
# Initialize streak if first time
|
| 655 |
+
if st.session_state.last_active_date is None:
|
| 656 |
+
st.session_state.streak_days = 1
|
| 657 |
+
st.session_state.last_active_date = today
|
| 658 |
+
self.save_game_state()
|
| 659 |
+
return True
|
| 660 |
+
|
| 661 |
+
# Convert string date to date object if needed
|
| 662 |
+
last_date = st.session_state.last_active_date
|
| 663 |
+
if isinstance(last_date, str):
|
| 664 |
+
try:
|
| 665 |
+
last_date = datetime.strptime(last_date, "%Y-%m-%d").date()
|
| 666 |
+
except:
|
| 667 |
+
last_date = today
|
| 668 |
+
|
| 669 |
+
# Calculate days since last activity
|
| 670 |
+
days_passed = (today - last_date).days
|
| 671 |
+
|
| 672 |
+
# If already visited today, no streak change
|
| 673 |
+
if days_passed == 0:
|
| 674 |
+
return False
|
| 675 |
+
|
| 676 |
+
# Streak continues if visited yesterday
|
| 677 |
+
if days_passed == 1:
|
| 678 |
+
st.session_state.streak_days += 1
|
| 679 |
+
st.session_state.last_active_date = today
|
| 680 |
+
|
| 681 |
+
# Award streak achievements
|
| 682 |
+
if st.session_state.streak_days == 3:
|
| 683 |
+
self.award_achievement("3_day_streak", "On Fire!", "Maintained a 3-day learning streak")
|
| 684 |
+
self.award_badge("fire_starter", "Fire Starter", "streak")
|
| 685 |
+
elif st.session_state.streak_days == 7:
|
| 686 |
+
self.award_achievement("7_day_streak", "Week Warrior", "Maintained a 7-day learning streak")
|
| 687 |
+
self.award_badge("week_warrior", "Week Warrior", "streak")
|
| 688 |
+
# Award a streak saver at 7 days
|
| 689 |
+
st.session_state.streak_savers += 1
|
| 690 |
+
st.toast("🛟 You earned a Streak Saver! Use it to maintain your streak if you miss a day.")
|
| 691 |
+
elif st.session_state.streak_days == 30:
|
| 692 |
+
self.award_achievement("30_day_streak", "Monthly Master", "Maintained a 30-day learning streak")
|
| 693 |
+
self.award_badge("monthly", "Monthly Master", "streak")
|
| 694 |
+
# Award more streak savers at 30 days
|
| 695 |
+
st.session_state.streak_savers += 2
|
| 696 |
+
st.toast("🛟 You earned 2 Streak Savers for your amazing dedication!")
|
| 697 |
+
|
| 698 |
+
# Add bonus points for consecutive days
|
| 699 |
+
bonus_points = min(st.session_state.streak_days, 10) # Cap at 10 points
|
| 700 |
+
self.add_points(bonus_points, f"Streak bonus (Day {st.session_state.streak_days})")
|
| 701 |
+
|
| 702 |
+
# Update vocabulary tree for streak maintenance
|
| 703 |
+
self.update_vocabulary_tree(0.02)
|
| 704 |
+
|
| 705 |
+
self.save_game_state()
|
| 706 |
+
return True
|
| 707 |
+
|
| 708 |
+
# Missed a day but have a streak saver
|
| 709 |
+
elif days_passed == 2 and st.session_state.streak_savers > 0:
|
| 710 |
+
st.session_state.streak_savers -= 1
|
| 711 |
+
st.session_state.last_active_date = today
|
| 712 |
+
st.toast("🛟 Used a Streak Saver to maintain your streak!")
|
| 713 |
+
self.save_game_state()
|
| 714 |
+
return True
|
| 715 |
+
|
| 716 |
+
# Streak broken
|
| 717 |
+
else:
|
| 718 |
+
st.session_state.streak_days = 1
|
| 719 |
+
st.session_state.last_active_date = today
|
| 720 |
+
self.save_game_state()
|
| 721 |
+
return False
|
| 722 |
+
|
| 723 |
+
def add_points(self, amount, reason=""):
|
| 724 |
+
"""Add points to the user's account and check for level ups."""
|
| 725 |
+
st.session_state.points += amount
|
| 726 |
+
|
| 727 |
+
# Check for level up
|
| 728 |
+
old_level = st.session_state.level
|
| 729 |
+
new_level = 1 + int(st.session_state.points / 100) # Level up every 100 points
|
| 730 |
+
|
| 731 |
+
if new_level > old_level:
|
| 732 |
+
st.session_state.level = new_level
|
| 733 |
+
st.toast(f"🎉 Level Up! You're now level {new_level}")
|
| 734 |
+
|
| 735 |
+
# Award level-up achievement
|
| 736 |
+
if new_level == 5:
|
| 737 |
+
self.award_achievement("level_5", "Level 5", "Reach level 5 in your learning journey", 50)
|
| 738 |
+
self.award_badge("level_5", "Level 5", "general")
|
| 739 |
+
elif new_level == 10:
|
| 740 |
+
self.award_achievement("level_10", "Level 10", "Reach level 10 in your learning journey", 100)
|
| 741 |
+
self.award_badge("level_10", "Level 10", "general")
|
| 742 |
+
|
| 743 |
+
# Award streak saver on level up
|
| 744 |
+
if new_level > old_level:
|
| 745 |
+
st.session_state.streak_savers += 1
|
| 746 |
+
st.toast("🛟 You earned a Streak Saver for leveling up!")
|
| 747 |
+
|
| 748 |
+
# Update vocabulary tree
|
| 749 |
+
self.update_vocabulary_tree(0.2) # Big growth on level up
|
| 750 |
+
|
| 751 |
+
# Save state
|
| 752 |
+
self.save_game_state()
|
| 753 |
+
|
| 754 |
+
def display_streak_info(self):
|
| 755 |
+
"""Display streak information in the UI."""
|
| 756 |
+
st.subheader("🔥 Learning Streak")
|
| 757 |
+
|
| 758 |
+
col1, col2 = st.columns(2)
|
| 759 |
+
|
| 760 |
+
with col1:
|
| 761 |
+
st.markdown(f"**Current Streak**: {st.session_state.streak_days} days")
|
| 762 |
+
if st.session_state.streak_days >= 7:
|
| 763 |
+
st.success(f"Amazing! Keep up the great work! 🎉")
|
| 764 |
+
elif st.session_state.streak_days >= 3:
|
| 765 |
+
st.success(f"You're on fire! 🔥")
|
| 766 |
+
|
| 767 |
+
with col2:
|
| 768 |
+
if st.session_state.streak_savers > 0:
|
| 769 |
+
st.markdown(f"**Streak Savers**: {st.session_state.streak_savers}")
|
| 770 |
+
st.info("Streak savers help maintain your streak if you miss a day.")
|
| 771 |
+
|
| 772 |
+
# Next streak milestone
|
| 773 |
+
if st.session_state.streak_days < 3:
|
| 774 |
+
target = 3
|
| 775 |
+
elif st.session_state.streak_days < 7:
|
| 776 |
+
target = 7
|
| 777 |
+
elif st.session_state.streak_days < 30:
|
| 778 |
+
target = 30
|
| 779 |
+
else:
|
| 780 |
+
target = 100
|
| 781 |
+
|
| 782 |
+
# Progress to next milestone
|
| 783 |
+
progress = st.session_state.streak_days / target
|
| 784 |
+
st.markdown(f"**Next milestone**: {target} days")
|
| 785 |
+
st.progress(min(1.0, progress))
|
| 786 |
+
|
| 787 |
+
# Days to go
|
| 788 |
+
days_to_go = target - st.session_state.streak_days
|
| 789 |
+
if days_to_go > 0:
|
| 790 |
+
st.markdown(f"*{days_to_go} days to go!*")
|
| 791 |
+
|
| 792 |
+
#=========================================================================
|
| 793 |
+
# 4. Progress Bars & Visual Feedback
|
| 794 |
+
#=========================================================================
|
| 795 |
+
|
| 796 |
+
def update_category_progress(self):
|
| 797 |
+
"""Update progress for different categories based on vocabulary."""
|
| 798 |
+
vocab_data = self.get_all_vocabulary()
|
| 799 |
+
|
| 800 |
+
# Count totals by category and language
|
| 801 |
+
category_counts = defaultdict(int)
|
| 802 |
+
language_counts = defaultdict(lambda: defaultdict(int))
|
| 803 |
+
|
| 804 |
+
for word in vocab_data:
|
| 805 |
+
if word and 'category' in word and word['category']:
|
| 806 |
+
category_counts[word['category']] += 1
|
| 807 |
+
|
| 808 |
+
if word and 'language_translated' in word and 'category' in word and word['category']:
|
| 809 |
+
language_counts[word['language_translated']][word['category']] += 1
|
| 810 |
+
|
| 811 |
+
# Calculate progress for each category (assume targets)
|
| 812 |
+
category_targets = {
|
| 813 |
+
"food": 15,
|
| 814 |
+
"animals": 10,
|
| 815 |
+
"vehicles": 8,
|
| 816 |
+
"electronics": 10,
|
| 817 |
+
"furniture": 8,
|
| 818 |
+
"personal": 6,
|
| 819 |
+
"sports": 8,
|
| 820 |
+
"household": 10,
|
| 821 |
+
"other": 10,
|
| 822 |
+
"manual": 5,
|
| 823 |
+
"text": 10
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
# Calculate overall progress
|
| 827 |
+
total_words = sum(category_counts.values())
|
| 828 |
+
overall_target = sum(category_targets.values())
|
| 829 |
+
overall_progress = min(1.0, total_words / max(1, overall_target))
|
| 830 |
+
|
| 831 |
+
# Update progress state
|
| 832 |
+
st.session_state.category_progress = {
|
| 833 |
+
"categories": {cat: min(1.0, category_counts[cat] / max(1, category_targets.get(cat, 10)))
|
| 834 |
+
for cat in set(list(category_counts.keys()) + list(category_targets.keys()))},
|
| 835 |
+
"languages": {lang: {cat: count for cat, count in cats.items()}
|
| 836 |
+
for lang, cats in language_counts.items()},
|
| 837 |
+
"overall": overall_progress,
|
| 838 |
+
"total_words": total_words
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
# Save state
|
| 842 |
+
self.save_game_state()
|
| 843 |
+
|
| 844 |
+
def update_vocabulary_tree(self, growth_amount):
|
| 845 |
+
"""Update the vocabulary tree based on learning progress."""
|
| 846 |
+
tree = st.session_state.vocabulary_tree
|
| 847 |
+
|
| 848 |
+
# Add growth to the tree
|
| 849 |
+
tree['size'] += growth_amount
|
| 850 |
+
|
| 851 |
+
# Check for size thresholds to add leaves and fruit
|
| 852 |
+
if tree['size'] >= 1.5 and tree['leaves'] < 5:
|
| 853 |
+
# Add leaves as tree grows
|
| 854 |
+
leaves_to_add = min(5 - tree['leaves'], int(tree['size'] - 1))
|
| 855 |
+
if leaves_to_add > 0:
|
| 856 |
+
tree['leaves'] += leaves_to_add
|
| 857 |
+
st.toast(f"🌿 Your vocabulary tree is growing new leaves!")
|
| 858 |
+
|
| 859 |
+
if tree['size'] >= 3 and tree['fruit'] < 10:
|
| 860 |
+
# Add fruit as tree grows even larger
|
| 861 |
+
fruit_to_add = min(10 - tree['fruit'], int((tree['size'] - 2) / 0.5))
|
| 862 |
+
if fruit_to_add > 0:
|
| 863 |
+
tree['fruit'] += fruit_to_add
|
| 864 |
+
st.toast(f"🍎 Your vocabulary tree is growing fruit!")
|
| 865 |
+
|
| 866 |
+
# Level up tree at certain sizes
|
| 867 |
+
if tree['size'] >= 5 and tree['level'] == 1:
|
| 868 |
+
tree['level'] = 2
|
| 869 |
+
st.toast(f"🌳 Your vocabulary tree has grown to level 2!")
|
| 870 |
+
self.award_achievement("tree_level_2", "Growing Tree", "Grow your vocabulary tree to level 2", 30)
|
| 871 |
+
elif tree['size'] >= 10 and tree['level'] == 2:
|
| 872 |
+
tree['level'] = 3
|
| 873 |
+
st.toast(f"🌳 Your vocabulary tree has grown to level 3!")
|
| 874 |
+
self.award_achievement("tree_level_3", "Thriving Tree", "Grow your vocabulary tree to level 3", 50)
|
| 875 |
+
self.award_badge("tree_master", "Tree Master", "vocabulary")
|
| 876 |
+
|
| 877 |
+
# Save state
|
| 878 |
+
self.save_game_state()
|
| 879 |
+
|
| 880 |
+
def generate_tree_image(self):
|
| 881 |
+
"""Generate an image of the vocabulary tree."""
|
| 882 |
+
try:
|
| 883 |
+
tree = st.session_state.vocabulary_tree
|
| 884 |
+
|
| 885 |
+
# Create a base image
|
| 886 |
+
width, height = 400, 400
|
| 887 |
+
img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
| 888 |
+
draw = ImageDraw.Draw(img)
|
| 889 |
+
|
| 890 |
+
# Tree properties based on level and size
|
| 891 |
+
trunk_width = 20 + (tree['level'] * 10)
|
| 892 |
+
trunk_height = 100 + (tree['size'] * 20)
|
| 893 |
+
canopy_size = 50 + (tree['size'] * 30)
|
| 894 |
+
|
| 895 |
+
# Draw trunk
|
| 896 |
+
trunk_color = (139, 69, 19) # Brown
|
| 897 |
+
trunk_x = width // 2
|
| 898 |
+
trunk_bottom = height - 50
|
| 899 |
+
trunk_top = trunk_bottom - trunk_height
|
| 900 |
+
|
| 901 |
+
draw.rectangle(
|
| 902 |
+
[(trunk_x - trunk_width//2, trunk_top),
|
| 903 |
+
(trunk_x + trunk_width//2, trunk_bottom)],
|
| 904 |
+
fill=trunk_color
|
| 905 |
+
)
|
| 906 |
+
|
| 907 |
+
# Draw canopy (tree top)
|
| 908 |
+
canopy_color = (34, 139, 34) # Forest Green
|
| 909 |
+
draw.ellipse(
|
| 910 |
+
[(trunk_x - canopy_size, trunk_top - canopy_size),
|
| 911 |
+
(trunk_x + canopy_size, trunk_top + canopy_size//2)],
|
| 912 |
+
fill=canopy_color
|
| 913 |
+
)
|
| 914 |
+
|
| 915 |
+
# Add leaves
|
| 916 |
+
if tree['leaves'] > 0:
|
| 917 |
+
leaf_color = (50, 205, 50) # Lime Green
|
| 918 |
+
for i in range(tree['leaves']):
|
| 919 |
+
angle = (i / tree['leaves']) * 360
|
| 920 |
+
radius = canopy_size * 0.8
|
| 921 |
+
x = trunk_x + int(radius * np.cos(np.radians(angle)))
|
| 922 |
+
y = trunk_top - int(radius * np.sin(np.radians(angle)) * 0.5)
|
| 923 |
+
|
| 924 |
+
draw.ellipse(
|
| 925 |
+
[(x - 15, y - 10), (x + 15, y + 10)],
|
| 926 |
+
fill=leaf_color
|
| 927 |
+
)
|
| 928 |
+
|
| 929 |
+
# Add fruit
|
| 930 |
+
if tree['fruit'] > 0:
|
| 931 |
+
fruit_color = (255, 0, 0) # Red
|
| 932 |
+
for i in range(tree['fruit']):
|
| 933 |
+
angle = (i / tree['fruit']) * 360
|
| 934 |
+
radius = canopy_size * 0.6
|
| 935 |
+
x = trunk_x + int(radius * np.cos(np.radians(angle)))
|
| 936 |
+
y = trunk_top - int(radius * np.sin(np.radians(angle)) * 0.7)
|
| 937 |
+
|
| 938 |
+
draw.ellipse(
|
| 939 |
+
[(x - 8, y - 8), (x + 8, y + 8)],
|
| 940 |
+
fill=fruit_color
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
+
# Convert to bytes for Streamlit
|
| 944 |
+
buf = io.BytesIO()
|
| 945 |
+
img.save(buf, format='PNG')
|
| 946 |
+
byte_im = buf.getvalue()
|
| 947 |
+
|
| 948 |
+
return byte_im
|
| 949 |
+
except Exception as e:
|
| 950 |
+
print(f"Error generating tree image: {e}")
|
| 951 |
+
return None
|
| 952 |
+
|
| 953 |
+
def display_progress_visuals(self):
|
| 954 |
+
"""Display progress visualizations in the UI."""
|
| 955 |
+
st.subheader("📊 Learning Progress")
|
| 956 |
+
|
| 957 |
+
# Display overall progress
|
| 958 |
+
total_words = st.session_state.category_progress.get("total_words", 0)
|
| 959 |
+
st.markdown(f"### Total Words Learned: {total_words}")
|
| 960 |
+
|
| 961 |
+
# Target goals by level
|
| 962 |
+
target_words = 10 * st.session_state.level # Increases with level
|
| 963 |
+
progress = min(1.0, total_words / target_words)
|
| 964 |
+
|
| 965 |
+
st.markdown(f"**Level Goal**: {total_words}/{target_words} words")
|
| 966 |
+
st.progress(progress)
|
| 967 |
+
|
| 968 |
+
# Display category progress
|
| 969 |
+
st.markdown("### Category Progress")
|
| 970 |
+
|
| 971 |
+
categories = st.session_state.category_progress.get("categories", {})
|
| 972 |
+
if categories:
|
| 973 |
+
sorted_categories = sorted(
|
| 974 |
+
[(cat, prog) for cat, prog in categories.items()],
|
| 975 |
+
key=lambda x: x[1],
|
| 976 |
+
reverse=True
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
for category, progress in sorted_categories:
|
| 980 |
+
col1, col2 = st.columns([3, 1])
|
| 981 |
+
with col1:
|
| 982 |
+
st.markdown(f"**{category.title()}**")
|
| 983 |
+
st.progress(progress)
|
| 984 |
+
with col2:
|
| 985 |
+
percentage = int(progress * 100)
|
| 986 |
+
st.markdown(f"{percentage}%")
|
| 987 |
+
else:
|
| 988 |
+
st.info("Start learning words to see category progress!")
|
| 989 |
+
|
| 990 |
+
# Display vocabulary tree
|
| 991 |
+
st.markdown("### 🌳 Vocabulary Tree")
|
| 992 |
+
|
| 993 |
+
tree_img = self.generate_tree_image()
|
| 994 |
+
if tree_img:
|
| 995 |
+
st.image(tree_img, width=300)
|
| 996 |
+
|
| 997 |
+
# Tree stats
|
| 998 |
+
col1, col2 = st.columns(2)
|
| 999 |
+
with col1:
|
| 1000 |
+
st.markdown(f"**Tree Level**: {st.session_state.vocabulary_tree['level']}")
|
| 1001 |
+
st.markdown(f"**Size**: {st.session_state.vocabulary_tree['size']:.1f}")
|
| 1002 |
+
with col2:
|
| 1003 |
+
st.markdown(f"**Leaves**: {st.session_state.vocabulary_tree['leaves']}")
|
| 1004 |
+
st.markdown(f"**Fruit**: {st.session_state.vocabulary_tree['fruit']}")
|
| 1005 |
+
|
| 1006 |
+
st.info("Your tree grows as you learn more words, complete challenges, and maintain your streak!")
|
| 1007 |
+
else:
|
| 1008 |
+
st.info("Keep learning to grow your vocabulary tree!")
|
| 1009 |
+
|
| 1010 |
+
#=========================================================================
|
| 1011 |
+
# Gamification Dashboard & Integration
|
| 1012 |
+
#=========================================================================
|
| 1013 |
+
|
| 1014 |
+
def render_dashboard(self):
|
| 1015 |
+
"""Render the complete gamification dashboard."""
|
| 1016 |
+
st.title("🎮 My Progress")
|
| 1017 |
+
|
| 1018 |
+
# User level and points at the top
|
| 1019 |
+
col1, col2 = st.columns([1, 3])
|
| 1020 |
+
|
| 1021 |
+
with col1:
|
| 1022 |
+
# Show level badge
|
| 1023 |
+
if st.session_state.level >= 5 and "level_5" in st.session_state.badges:
|
| 1024 |
+
try:
|
| 1025 |
+
badge_path = st.session_state.badges["level_5"]["image_path"]
|
| 1026 |
+
if os.path.exists(badge_path):
|
| 1027 |
+
st.image(badge_path, width=100)
|
| 1028 |
+
except:
|
| 1029 |
+
st.markdown(f"## Level {st.session_state.level}")
|
| 1030 |
+
else:
|
| 1031 |
+
st.markdown(f"## Level {st.session_state.level}")
|
| 1032 |
+
|
| 1033 |
+
with col2:
|
| 1034 |
+
# Show progress to next level
|
| 1035 |
+
next_level = st.session_state.level + 1
|
| 1036 |
+
points_needed = (next_level - 1) * 100
|
| 1037 |
+
current_level_points = st.session_state.points - ((st.session_state.level - 1) * 100)
|
| 1038 |
+
progress_percentage = current_level_points / 100
|
| 1039 |
+
|
| 1040 |
+
st.markdown(f"### {st.session_state.points} total points")
|
| 1041 |
+
st.markdown(f"{current_level_points}/100 to level {next_level}")
|
| 1042 |
+
st.progress(progress_percentage)
|
| 1043 |
+
|
| 1044 |
+
# Create tabs for different sections
|
| 1045 |
+
tab1, tab2, tab3, tab4 = st.tabs(["Challenges", "Streaks", "Progress", "Achievements"])
|
| 1046 |
+
|
| 1047 |
+
with tab1:
|
| 1048 |
+
self.display_daily_challenges()
|
| 1049 |
+
|
| 1050 |
+
with tab2:
|
| 1051 |
+
self.display_streak_info()
|
| 1052 |
+
|
| 1053 |
+
with tab3:
|
| 1054 |
+
self.display_progress_visuals()
|
| 1055 |
+
|
| 1056 |
+
with tab4:
|
| 1057 |
+
col1, col2 = st.columns(2)
|
| 1058 |
+
with col1:
|
| 1059 |
+
self.display_achievements()
|
| 1060 |
+
with col2:
|
| 1061 |
+
self.display_badges()
|
| 1062 |
+
|
| 1063 |
+
def update_sidebar(self):
|
| 1064 |
+
"""Update the sidebar with gamification information."""
|
| 1065 |
+
try:
|
| 1066 |
+
with st.sidebar.expander("🏆 My Progress", expanded=True):
|
| 1067 |
+
# Use get() with default values to safely access session state
|
| 1068 |
+
level = st.session_state.get("level", 1) # Default to 1 if not exists
|
| 1069 |
+
points = st.session_state.get("points", 0) # Default to 0 if not exists
|
| 1070 |
+
|
| 1071 |
+
# Display level and points
|
| 1072 |
+
st.markdown(f"**Level {level}** | {points} points")
|
| 1073 |
+
|
| 1074 |
+
# Streak - check if it exists first
|
| 1075 |
+
streak_days = st.session_state.get("streak_days", 0)
|
| 1076 |
+
if streak_days > 0:
|
| 1077 |
+
st.markdown(f"🔥 **{streak_days}** day streak")
|
| 1078 |
+
|
| 1079 |
+
# Word of the day preview - check if it exists first
|
| 1080 |
+
wotd = st.session_state.get("word_of_the_day")
|
| 1081 |
+
if wotd and isinstance(wotd, dict): # Ensure it's a valid dictionary
|
| 1082 |
+
st.markdown("**✨ Word of the Day**")
|
| 1083 |
+
original = wotd.get('original', '?')
|
| 1084 |
+
|
| 1085 |
+
# Get current target language
|
| 1086 |
+
current_language = st.session_state.get('target_language', 'es')
|
| 1087 |
+
|
| 1088 |
+
# Check if language has changed since word of the day was generated
|
| 1089 |
+
wotd_language = wotd.get('language', None)
|
| 1090 |
+
|
| 1091 |
+
# If language changed or translation is a placeholder, get fresh translation
|
| 1092 |
+
translated = wotd.get('translated', '?')
|
| 1093 |
+
if wotd_language != current_language or translated.startswith('['):
|
| 1094 |
+
# Translation dictionary for common words
|
| 1095 |
+
translations = {
|
| 1096 |
+
"book": {
|
| 1097 |
+
"es": "libro", "fr": "livre", "de": "Buch", "it": "libro",
|
| 1098 |
+
"pt": "livro", "ru": "книга", "ja": "本", "zh-CN": "书"
|
| 1099 |
+
},
|
| 1100 |
+
"hello": {
|
| 1101 |
+
"es": "hola", "fr": "bonjour", "de": "hallo", "it": "ciao",
|
| 1102 |
+
"pt": "olá", "ru": "привет", "ja": "こんにちは", "zh-CN": "你好"
|
| 1103 |
+
},
|
| 1104 |
+
"thank you": {
|
| 1105 |
+
"es": "gracias", "fr": "merci", "de": "danke", "it": "grazie",
|
| 1106 |
+
"pt": "obrigado", "ru": "спасибо", "ja": "ありがとう", "zh-CN": "谢谢"
|
| 1107 |
+
},
|
| 1108 |
+
"please": {
|
| 1109 |
+
"es": "por favor", "fr": "s'il vous plaît", "de": "bitte", "it": "per favore",
|
| 1110 |
+
"pt": "por favor", "ru": "пожалуйста", "ja": "お願いします", "zh-CN": "请"
|
| 1111 |
+
},
|
| 1112 |
+
"friend": {
|
| 1113 |
+
"es": "amigo", "fr": "ami", "de": "Freund", "it": "amico",
|
| 1114 |
+
"pt": "amigo", "ru": "друг", "ja": "友達", "zh-CN": "朋友"
|
| 1115 |
+
},
|
| 1116 |
+
"food": {
|
| 1117 |
+
"es": "comida", "fr": "nourriture", "de": "Essen", "it": "cibo",
|
| 1118 |
+
"pt": "comida", "ru": "еда", "ja": "食べ物", "zh-CN": "食物"
|
| 1119 |
+
},
|
| 1120 |
+
"water": {
|
| 1121 |
+
"es": "agua", "fr": "eau", "de": "Wasser", "it": "acqua",
|
| 1122 |
+
"pt": "água", "ru": "вода", "ja": "水", "zh-CN": "水"
|
| 1123 |
+
},
|
| 1124 |
+
"help": {
|
| 1125 |
+
"es": "ayuda", "fr": "aide", "de": "Hilfe", "it": "aiuto",
|
| 1126 |
+
"pt": "ajuda", "ru": "помощь", "ja": "助けて", "zh-CN": "帮助"
|
| 1127 |
+
},
|
| 1128 |
+
"good": {
|
| 1129 |
+
"es": "bueno", "fr": "bon", "de": "gut", "it": "buono",
|
| 1130 |
+
"pt": "bom", "ru": "хороший", "ja": "良い", "zh-CN": "好"
|
| 1131 |
+
},
|
| 1132 |
+
"bad": {
|
| 1133 |
+
"es": "malo", "fr": "mauvais", "de": "schlecht", "it": "cattivo",
|
| 1134 |
+
"pt": "mau", "ru": "плохой", "ja": "悪い", "zh-CN": "坏"
|
| 1135 |
+
},
|
| 1136 |
+
"yes": {
|
| 1137 |
+
"es": "sí", "fr": "oui", "de": "ja", "it": "sì",
|
| 1138 |
+
"pt": "sim", "ru": "да", "ja": "はい", "zh-CN": "是"
|
| 1139 |
+
},
|
| 1140 |
+
"no": {
|
| 1141 |
+
"es": "no", "fr": "non", "de": "nein", "it": "no",
|
| 1142 |
+
"pt": "não", "ru": "нет", "ja": "いいえ", "zh-CN": "不"
|
| 1143 |
+
},
|
| 1144 |
+
"goodbye": {
|
| 1145 |
+
"es": "adiós", "fr": "au revoir", "de": "auf Wiedersehen", "it": "arrivederci",
|
| 1146 |
+
"pt": "adeus", "ru": "до свидания", "ja": "さようなら", "zh-CN": "再见"
|
| 1147 |
+
},
|
| 1148 |
+
"sorry": {
|
| 1149 |
+
"es": "lo siento", "fr": "désolé", "de": "Entschuldigung", "it": "scusa",
|
| 1150 |
+
"pt": "desculpe", "ru": "извините", "ja": "すみません", "zh-CN": "对不起"
|
| 1151 |
+
},
|
| 1152 |
+
"excuse me": {
|
| 1153 |
+
"es": "disculpe", "fr": "excusez-moi", "de": "Entschuldigung", "it": "scusi",
|
| 1154 |
+
"pt": "com licença", "ru": "извините", "ja": "すみません", "zh-CN": "打扰了"
|
| 1155 |
+
},
|
| 1156 |
+
"love": {
|
| 1157 |
+
"es": "amor", "fr": "amour", "de": "Liebe", "it": "amore",
|
| 1158 |
+
"pt": "amor", "ru": "любовь", "ja": "愛", "zh-CN": "爱"
|
| 1159 |
+
},
|
| 1160 |
+
"car": {
|
| 1161 |
+
"es": "coche", "fr": "voiture", "de": "Auto", "it": "macchina",
|
| 1162 |
+
"pt": "carro", "ru": "машина", "ja": "車", "zh-CN": "车"
|
| 1163 |
+
},
|
| 1164 |
+
"house": {
|
| 1165 |
+
"es": "casa", "fr": "maison", "de": "Haus", "it": "casa",
|
| 1166 |
+
"pt": "casa", "ru": "дом", "ja": "家", "zh-CN": "房子"
|
| 1167 |
+
},
|
| 1168 |
+
"family": {
|
| 1169 |
+
"es": "familia", "fr": "famille", "de": "Familie", "it": "famiglia",
|
| 1170 |
+
"pt": "família", "ru": "семья", "ja": "家族", "zh-CN": "家庭"
|
| 1171 |
+
},
|
| 1172 |
+
"work": {
|
| 1173 |
+
"es": "trabajo", "fr": "travail", "de": "Arbeit", "it": "lavoro",
|
| 1174 |
+
"pt": "trabalho", "ru": "работа", "ja": "仕事", "zh-CN": "工作"
|
| 1175 |
+
}
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
# Get translation for the current word and language
|
| 1179 |
+
if original.lower() in translations and current_language in translations[original.lower()]:
|
| 1180 |
+
translated = translations[original.lower()][current_language]
|
| 1181 |
+
else:
|
| 1182 |
+
# Fallback to a friendly format
|
| 1183 |
+
language_name = self.get_language_name(current_language)
|
| 1184 |
+
translated = f"{original} ({language_name})"
|
| 1185 |
+
|
| 1186 |
+
# Update the word of the day with the new translation and language
|
| 1187 |
+
wotd['translated'] = translated
|
| 1188 |
+
wotd['language'] = current_language
|
| 1189 |
+
st.session_state.word_of_the_day = wotd
|
| 1190 |
+
|
| 1191 |
+
# Display the word of the day with its (now correct) translation
|
| 1192 |
+
st.markdown(f"{original} → {translated}")
|
| 1193 |
+
|
| 1194 |
+
# Challenge preview - check if they exist first
|
| 1195 |
+
daily_challenges = st.session_state.get("daily_challenges", [])
|
| 1196 |
+
if daily_challenges and isinstance(daily_challenges, list) and len(daily_challenges) > 0:
|
| 1197 |
+
completed = sum(1 for c in daily_challenges if c.get("completed", False))
|
| 1198 |
+
total = len(daily_challenges)
|
| 1199 |
+
st.markdown(f"**📅 Challenges**: {completed}/{total} completed")
|
| 1200 |
+
except Exception as e:
|
| 1201 |
+
# Provide a fallback if anything goes wrong
|
| 1202 |
+
st.sidebar.markdown("🏆 **Progress system is initializing...**")
|
| 1203 |
+
print(f"Error in update_sidebar: {e}") # Log the error
|
| 1204 |
+
|
| 1205 |
+
#=========================================================================
|
| 1206 |
+
# Helper Functions
|
| 1207 |
+
#=========================================================================
|
| 1208 |
+
|
| 1209 |
+
def get_all_vocabulary_direct(self):
|
| 1210 |
+
"""Get all vocabulary items directly from SQLite."""
|
| 1211 |
+
try:
|
| 1212 |
+
# Connect to the database
|
| 1213 |
+
conn = sqlite3.connect(self.db_path)
|
| 1214 |
+
|
| 1215 |
+
# Use dictionary cursor for easier access
|
| 1216 |
+
conn.row_factory = sqlite3.Row
|
| 1217 |
+
cursor = conn.cursor()
|
| 1218 |
+
|
| 1219 |
+
# Get all vocabulary with user progress info
|
| 1220 |
+
cursor.execute('''
|
| 1221 |
+
SELECT v.id, v.word_original, v.word_translated, v.language_translated,
|
| 1222 |
+
v.category, v.image_path, v.date_added,
|
| 1223 |
+
up.proficiency_level, up.review_count, up.correct_count, up.last_reviewed
|
| 1224 |
+
FROM vocabulary v
|
| 1225 |
+
LEFT JOIN user_progress up ON v.id = up.vocabulary_id
|
| 1226 |
+
ORDER BY v.date_added DESC
|
| 1227 |
+
''')
|
| 1228 |
+
|
| 1229 |
+
# Fetch all results
|
| 1230 |
+
results = cursor.fetchall()
|
| 1231 |
+
|
| 1232 |
+
# Convert to list of dictionaries
|
| 1233 |
+
vocabulary = []
|
| 1234 |
+
for row in results:
|
| 1235 |
+
# Convert row to dictionary
|
| 1236 |
+
word = dict(row)
|
| 1237 |
+
vocabulary.append(word)
|
| 1238 |
+
|
| 1239 |
+
conn.close()
|
| 1240 |
+
return vocabulary
|
| 1241 |
+
except Exception as e:
|
| 1242 |
+
print(f"Error retrieving vocabulary: {str(e)}")
|
| 1243 |
+
return []
|
| 1244 |
+
|
| 1245 |
+
def get_all_vocabulary(self):
|
| 1246 |
+
"""Get all vocabulary, handling different potential sources."""
|
| 1247 |
+
# Try the direct method
|
| 1248 |
+
vocabulary = self.get_all_vocabulary_direct()
|
| 1249 |
+
|
| 1250 |
+
# If that fails, try the session state
|
| 1251 |
+
if not vocabulary and 'vocabulary' in st.session_state:
|
| 1252 |
+
vocabulary = st.session_state.vocabulary
|
| 1253 |
+
|
| 1254 |
+
return vocabulary
|
| 1255 |
+
|
| 1256 |
+
def get_object_category(self, label):
|
| 1257 |
+
"""Get the category for a detected object label."""
|
| 1258 |
+
label = label.lower()
|
| 1259 |
+
for category, items in OBJECT_CATEGORIES.items():
|
| 1260 |
+
if label in items:
|
| 1261 |
+
return category
|
| 1262 |
+
return "other"
|
| 1263 |
+
|
| 1264 |
+
def get_language_name(self, language_code):
|
| 1265 |
+
"""Get language name from code."""
|
| 1266 |
+
language_map = {
|
| 1267 |
+
"es": "Spanish",
|
| 1268 |
+
"fr": "French",
|
| 1269 |
+
"de": "German",
|
| 1270 |
+
"it": "Italian",
|
| 1271 |
+
"pt": "Portuguese",
|
| 1272 |
+
"ru": "Russian",
|
| 1273 |
+
"ja": "Japanese",
|
| 1274 |
+
"zh-CN": "Chinese"
|
| 1275 |
+
}
|
| 1276 |
+
return language_map.get(language_code, language_code)
|
| 1277 |
+
|
| 1278 |
+
def set_translate_func(self, translate_func):
|
| 1279 |
+
"""Set or update the translation function after initialization."""
|
| 1280 |
+
self.translate_func = translate_func
|
| 1281 |
+
|
| 1282 |
+
def translate_placeholder(self, text, target_language):
|
| 1283 |
+
"""Get translation using our robust translation system."""
|
| 1284 |
+
return self.get_translation_for_word(text, target_language)
|
| 1285 |
+
|
| 1286 |
+
#=========================================================================
|
| 1287 |
+
# State Management
|
| 1288 |
+
#=========================================================================
|
| 1289 |
+
|
| 1290 |
+
def save_game_state(self):
|
| 1291 |
+
"""Save game state to the database."""
|
| 1292 |
+
try:
|
| 1293 |
+
# Create a JSON representation of the gamification state
|
| 1294 |
+
game_state = {
|
| 1295 |
+
"achievements": st.session_state.achievements,
|
| 1296 |
+
"badges": st.session_state.badges,
|
| 1297 |
+
"streak_days": st.session_state.streak_days,
|
| 1298 |
+
"last_active_date": str(st.session_state.last_active_date) if st.session_state.last_active_date else None,
|
| 1299 |
+
"streak_savers": st.session_state.streak_savers,
|
| 1300 |
+
"points": st.session_state.points,
|
| 1301 |
+
"level": st.session_state.level,
|
| 1302 |
+
"daily_challenges": st.session_state.daily_challenges,
|
| 1303 |
+
"daily_challenges_completed": list(st.session_state.daily_challenges_completed),
|
| 1304 |
+
"word_of_the_day": st.session_state.word_of_the_day,
|
| 1305 |
+
"word_of_the_day_date": st.session_state.word_of_the_day_date,
|
| 1306 |
+
"category_progress": st.session_state.category_progress,
|
| 1307 |
+
"vocabulary_tree": st.session_state.vocabulary_tree
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
# Convert to JSON string
|
| 1311 |
+
game_state_json = json.dumps(game_state)
|
| 1312 |
+
|
| 1313 |
+
# Save to database
|
| 1314 |
+
conn = sqlite3.connect(self.db_path)
|
| 1315 |
+
cursor = conn.cursor()
|
| 1316 |
+
|
| 1317 |
+
# Check if game_state table exists, create if not
|
| 1318 |
+
cursor.execute('''
|
| 1319 |
+
CREATE TABLE IF NOT EXISTS game_state (
|
| 1320 |
+
id INTEGER PRIMARY KEY,
|
| 1321 |
+
state_json TEXT,
|
| 1322 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 1323 |
+
)
|
| 1324 |
+
''')
|
| 1325 |
+
|
| 1326 |
+
# Update or insert game state
|
| 1327 |
+
cursor.execute("SELECT id FROM game_state WHERE id = 1")
|
| 1328 |
+
if cursor.fetchone():
|
| 1329 |
+
cursor.execute("UPDATE game_state SET state_json = ?, updated_at = ? WHERE id = 1",
|
| 1330 |
+
(game_state_json, datetime.now()))
|
| 1331 |
+
else:
|
| 1332 |
+
cursor.execute("INSERT INTO game_state (id, state_json, updated_at) VALUES (1, ?, ?)",
|
| 1333 |
+
(game_state_json, datetime.now()))
|
| 1334 |
+
|
| 1335 |
+
conn.commit()
|
| 1336 |
+
conn.close()
|
| 1337 |
+
|
| 1338 |
+
except Exception as e:
|
| 1339 |
+
# If database operations fail, just log and continue
|
| 1340 |
+
print(f"Error saving game state: {e}")
|
| 1341 |
+
|
| 1342 |
+
def load_game_state(self):
|
| 1343 |
+
"""Load game state from the database."""
|
| 1344 |
+
try:
|
| 1345 |
+
# Connect to the database
|
| 1346 |
+
conn = sqlite3.connect(self.db_path)
|
| 1347 |
+
cursor = conn.cursor()
|
| 1348 |
+
|
| 1349 |
+
# Check if table exists
|
| 1350 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='game_state'")
|
| 1351 |
+
if not cursor.fetchone():
|
| 1352 |
+
conn.close()
|
| 1353 |
+
return False
|
| 1354 |
+
|
| 1355 |
+
# Get the state
|
| 1356 |
+
cursor.execute("SELECT state_json FROM game_state WHERE id = 1")
|
| 1357 |
+
result = cursor.fetchone()
|
| 1358 |
+
conn.close()
|
| 1359 |
+
|
| 1360 |
+
if not result:
|
| 1361 |
+
return False
|
| 1362 |
+
|
| 1363 |
+
# Parse the state
|
| 1364 |
+
game_state = json.loads(result[0])
|
| 1365 |
+
|
| 1366 |
+
# Update session state
|
| 1367 |
+
st.session_state.achievements = game_state.get("achievements", {})
|
| 1368 |
+
st.session_state.badges = game_state.get("badges", {})
|
| 1369 |
+
st.session_state.streak_days = game_state.get("streak_days", 0)
|
| 1370 |
+
st.session_state.last_active_date = game_state.get("last_active_date")
|
| 1371 |
+
st.session_state.streak_savers = game_state.get("streak_savers", 0)
|
| 1372 |
+
st.session_state.points = game_state.get("points", 0)
|
| 1373 |
+
st.session_state.level = game_state.get("level", 1)
|
| 1374 |
+
st.session_state.daily_challenges = game_state.get("daily_challenges", [])
|
| 1375 |
+
st.session_state.daily_challenges_completed = set(game_state.get("daily_challenges_completed", []))
|
| 1376 |
+
st.session_state.word_of_the_day = game_state.get("word_of_the_day")
|
| 1377 |
+
st.session_state.word_of_the_day_date = game_state.get("word_of_the_day_date")
|
| 1378 |
+
st.session_state.category_progress = game_state.get("category_progress", {})
|
| 1379 |
+
st.session_state.vocabulary_tree = game_state.get("vocabulary_tree", {
|
| 1380 |
+
'size': 1, 'leaves': 0, 'fruit': 0, 'level': 1
|
| 1381 |
+
})
|
| 1382 |
+
|
| 1383 |
+
return True
|
| 1384 |
+
except Exception as e:
|
| 1385 |
+
print(f"Error loading game state: {e}")
|
| 1386 |
+
return False
|
| 1387 |
+
|
| 1388 |
+
def get_translation_for_word(self, word, target_language):
|
| 1389 |
+
"""
|
| 1390 |
+
Get translation for word with multiple fallback options.
|
| 1391 |
+
Always returns a real translation, never a placeholder.
|
| 1392 |
+
"""
|
| 1393 |
+
# First fallback: Dictionary of common translations - extend this as needed
|
| 1394 |
+
common_translations = {
|
| 1395 |
+
"es": {
|
| 1396 |
+
"hello": "hola", "thank you": "gracias", "please": "por favor", "book": "libro",
|
| 1397 |
+
"goodbye": "adiós", "friend": "amigo", "food": "comida", "water": "agua",
|
| 1398 |
+
"help": "ayuda", "good": "bueno", "bad": "malo", "yes": "sí", "no": "no",
|
| 1399 |
+
"sorry": "lo siento", "excuse me": "disculpe", "love": "amor", "car": "coche",
|
| 1400 |
+
"house": "casa", "family": "familia", "work": "trabajo"
|
| 1401 |
+
},
|
| 1402 |
+
"fr": {
|
| 1403 |
+
"hello": "bonjour", "thank you": "merci", "please": "s'il vous plaît", "book": "livre",
|
| 1404 |
+
"goodbye": "au revoir", "friend": "ami", "food": "nourriture", "water": "eau",
|
| 1405 |
+
"help": "aide", "good": "bon", "bad": "mauvais", "yes": "oui", "no": "non",
|
| 1406 |
+
"sorry": "désolé", "excuse me": "excusez-moi", "love": "amour", "car": "voiture",
|
| 1407 |
+
"house": "maison", "family": "famille", "work": "travail"
|
| 1408 |
+
},
|
| 1409 |
+
"de": {
|
| 1410 |
+
"hello": "hallo", "thank you": "danke", "please": "bitte", "book": "Buch",
|
| 1411 |
+
"goodbye": "auf Wiedersehen", "friend": "Freund", "food": "Essen", "water": "Wasser",
|
| 1412 |
+
"help": "Hilfe", "good": "gut", "bad": "schlecht", "yes": "ja", "no": "nein",
|
| 1413 |
+
"sorry": "Entschuldigung", "excuse me": "Entschuldigung", "love": "Liebe",
|
| 1414 |
+
"car": "Auto", "house": "Haus", "family": "Familie", "work": "Arbeit"
|
| 1415 |
+
},
|
| 1416 |
+
"it": {
|
| 1417 |
+
"hello": "ciao", "thank you": "grazie", "please": "per favore", "book": "libro",
|
| 1418 |
+
"goodbye": "arrivederci", "friend": "amico", "food": "cibo", "water": "acqua",
|
| 1419 |
+
"help": "aiuto", "good": "buono", "bad": "cattivo", "yes": "sì", "no": "no",
|
| 1420 |
+
"sorry": "scusa", "excuse me": "scusi", "love": "amore", "car": "macchina",
|
| 1421 |
+
"house": "casa", "family": "famiglia", "work": "lavoro"
|
| 1422 |
+
},
|
| 1423 |
+
"pt": {
|
| 1424 |
+
"hello": "olá", "thank you": "obrigado", "please": "por favor", "book": "livro",
|
| 1425 |
+
"goodbye": "adeus", "friend": "amigo", "food": "comida", "water": "água",
|
| 1426 |
+
"help": "ajuda", "good": "bom", "bad": "mau", "yes": "sim", "no": "não",
|
| 1427 |
+
"sorry": "desculpe", "excuse me": "com licença", "love": "amor",
|
| 1428 |
+
"car": "carro", "house": "casa", "family": "família", "work": "trabalho"
|
| 1429 |
+
},
|
| 1430 |
+
"ru": {
|
| 1431 |
+
"hello": "привет", "thank you": "спасибо", "please": "пожалуйста", "book": "книга",
|
| 1432 |
+
"goodbye": "до свидания", "friend": "друг", "food": "еда", "water": "вода",
|
| 1433 |
+
"help": "помощь", "good": "хороший", "bad": "плохой", "yes": "да", "no": "нет",
|
| 1434 |
+
"sorry": "извините", "excuse me": "извините", "love": "любовь",
|
| 1435 |
+
"car": "машина", "house": "дом", "family": "семья", "work": "работа"
|
| 1436 |
+
},
|
| 1437 |
+
"ja": {
|
| 1438 |
+
"hello": "こんにちは", "thank you": "ありがとう", "please": "お願いします", "book": "本",
|
| 1439 |
+
"goodbye": "さようなら", "friend": "友達", "food": "食べ物", "water": "水",
|
| 1440 |
+
"help": "助けて", "good": "良い", "bad": "悪い", "yes": "はい", "no": "いいえ",
|
| 1441 |
+
"sorry": "すみません", "excuse me": "すみま��ん", "love": "愛",
|
| 1442 |
+
"car": "車", "house": "家", "family": "家族", "work": "仕事"
|
| 1443 |
+
},
|
| 1444 |
+
"zh-CN": {
|
| 1445 |
+
"hello": "你好", "thank you": "谢谢", "please": "请", "book": "书",
|
| 1446 |
+
"goodbye": "再见", "friend": "朋友", "food": "食物", "water": "水",
|
| 1447 |
+
"help": "帮助", "good": "好", "bad": "坏", "yes": "是", "no": "不",
|
| 1448 |
+
"sorry": "对不起", "excuse me": "打扰了", "love": "爱",
|
| 1449 |
+
"car": "车", "house": "房子", "family": "家庭", "work": "工作"
|
| 1450 |
+
}
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
+
# 1. Check if we have a translation in our common translations dictionary
|
| 1454 |
+
word_lower = word.lower()
|
| 1455 |
+
if target_language in common_translations and word_lower in common_translations[target_language]:
|
| 1456 |
+
return common_translations[target_language][word_lower]
|
| 1457 |
+
|
| 1458 |
+
# 2. Try using the provided translation function if available
|
| 1459 |
+
if hasattr(self, 'translate_func') and self.translate_func:
|
| 1460 |
+
try:
|
| 1461 |
+
result = self.translate_func(word, target_language)
|
| 1462 |
+
# Check if the result looks like a placeholder (contains brackets)
|
| 1463 |
+
if not (result.startswith('[') and result.endswith(']')):
|
| 1464 |
+
return result
|
| 1465 |
+
except Exception as e:
|
| 1466 |
+
print(f"Translation error: {e}")
|
| 1467 |
+
|
| 1468 |
+
# 3. If language is not in our dictionary or word is not found, return a more usable fallback
|
| 1469 |
+
# Append language suffix to show it's a fallback
|
| 1470 |
+
language_name = self.get_language_name(target_language)
|
| 1471 |
+
suffixes = {
|
| 1472 |
+
"es": "en español", "fr": "en français", "de": "auf Deutsch",
|
| 1473 |
+
"it": "in italiano", "pt": "em português", "ru": "по-русски",
|
| 1474 |
+
"ja": "日本語で", "zh-CN": "用中文"
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
suffix = suffixes.get(target_language, f"in {language_name}")
|
| 1478 |
+
return f"{word} ({suffix})"
|
| 1479 |
+
|
| 1480 |
+
# Define Object Categories (copied from main.py)
|
| 1481 |
+
OBJECT_CATEGORIES = {
|
| 1482 |
+
"food": ["apple", "banana", "orange", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl",
|
| 1483 |
+
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake"],
|
| 1484 |
+
|
| 1485 |
+
"animals": ["bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe"],
|
| 1486 |
+
|
| 1487 |
+
"vehicles": ["bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat"],
|
| 1488 |
+
|
| 1489 |
+
"electronics": ["tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven",
|
| 1490 |
+
"toaster", "sink", "refrigerator"],
|
| 1491 |
+
|
| 1492 |
+
"furniture": ["chair", "couch", "potted plant", "bed", "dining table", "toilet"],
|
| 1493 |
+
|
| 1494 |
+
"personal": ["backpack", "umbrella", "handbag", "tie", "suitcase"],
|
| 1495 |
+
|
| 1496 |
+
"sports": ["frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat",
|
| 1497 |
+
"baseball glove", "skateboard", "surfboard", "tennis racket"],
|
| 1498 |
+
|
| 1499 |
+
"household": ["bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "book", "clock",
|
| 1500 |
+
"vase", "scissors", "teddy bear", "hair drier", "toothbrush"]
|
| 1501 |
+
}
|
quiz_system.py
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Quiz System for Vocam Language Learning App
|
| 3 |
+
|
| 4 |
+
This module implements a robust, user-friendly quiz system with multiple question types,
|
| 5 |
+
detailed feedback, and an improved learning experience.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import streamlit as st
|
| 9 |
+
import os
|
| 10 |
+
import random
|
| 11 |
+
import io
|
| 12 |
+
import numpy as np
|
| 13 |
+
import matplotlib.pyplot as plt
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from collections import defaultdict
|
| 16 |
+
|
| 17 |
+
class QuizSystem:
|
| 18 |
+
def __init__(self, db_functions, text_to_speech, get_audio_html, get_example_sentence, get_pronunciation_guide):
|
| 19 |
+
"""Initialize the quiz system with required dependencies.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
db_functions: Dictionary containing database functions:
|
| 23 |
+
- get_all_vocabulary_direct: Function to get vocabulary
|
| 24 |
+
- update_word_progress_direct: Function to update word progress
|
| 25 |
+
text_to_speech: Function to convert text to speech
|
| 26 |
+
get_audio_html: Function to generate HTML for audio
|
| 27 |
+
get_example_sentence: Function to get example sentences
|
| 28 |
+
get_pronunciation_guide: Function to get pronunciation guide
|
| 29 |
+
"""
|
| 30 |
+
self.get_all_vocabulary_direct = db_functions['get_all_vocabulary_direct']
|
| 31 |
+
self.update_word_progress_direct = db_functions['update_word_progress_direct']
|
| 32 |
+
self.text_to_speech = text_to_speech
|
| 33 |
+
self.get_audio_html = get_audio_html
|
| 34 |
+
self.get_example_sentence = get_example_sentence
|
| 35 |
+
self.get_pronunciation_guide = get_pronunciation_guide
|
| 36 |
+
|
| 37 |
+
# Define question types
|
| 38 |
+
self.QUESTION_TYPES = [
|
| 39 |
+
"translation_to_target", # English → Target language
|
| 40 |
+
"translation_to_english", # Target language → English
|
| 41 |
+
"image_recognition", # Show image, select correct word
|
| 42 |
+
"category_match", # Match word to correct category
|
| 43 |
+
"sentence_completion", # Fill in blank in a sentence
|
| 44 |
+
"multiple_choice_category", # Choose words from same category
|
| 45 |
+
"audio_recognition" # Hear word, select correct option
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
def get_language_name_from_code(self, language_code, languages):
|
| 49 |
+
"""Get the language name from its code."""
|
| 50 |
+
for name, code in languages.items():
|
| 51 |
+
if code == language_code:
|
| 52 |
+
return name
|
| 53 |
+
return language_code
|
| 54 |
+
|
| 55 |
+
def get_possible_question_types(self, word, vocabulary):
|
| 56 |
+
"""Determine which question types are possible for the current word."""
|
| 57 |
+
possible_types = []
|
| 58 |
+
|
| 59 |
+
# All words can have basic translation questions
|
| 60 |
+
possible_types.append("translation_to_target")
|
| 61 |
+
possible_types.append("translation_to_english")
|
| 62 |
+
|
| 63 |
+
# Image recognition requires an image
|
| 64 |
+
if word.get('image_path') and os.path.exists(word.get('image_path', '')):
|
| 65 |
+
possible_types.append("image_recognition")
|
| 66 |
+
|
| 67 |
+
# Category matching requires a category
|
| 68 |
+
if word.get('category') and word['category'] != "other" and word['category'] != "manual":
|
| 69 |
+
possible_types.append("category_match")
|
| 70 |
+
|
| 71 |
+
# Find if there are other words in the same category
|
| 72 |
+
same_category_words = [w for w in vocabulary
|
| 73 |
+
if w.get('category') == word.get('category') and w['id'] != word['id']]
|
| 74 |
+
if len(same_category_words) >= 2: # Need at least 3 total including our word
|
| 75 |
+
possible_types.append("multiple_choice_category")
|
| 76 |
+
|
| 77 |
+
# Sentence completion is always possible
|
| 78 |
+
possible_types.append("sentence_completion")
|
| 79 |
+
|
| 80 |
+
# Audio recognition is always possible
|
| 81 |
+
possible_types.append("audio_recognition")
|
| 82 |
+
|
| 83 |
+
return possible_types
|
| 84 |
+
|
| 85 |
+
def setup_new_question(self, vocabulary, languages, current_question_num=0, total_questions=5):
|
| 86 |
+
"""Set up a new quiz question with enhanced functionality."""
|
| 87 |
+
if not vocabulary or len(vocabulary) < 4:
|
| 88 |
+
return False
|
| 89 |
+
|
| 90 |
+
# Store total questions for progress tracking
|
| 91 |
+
st.session_state.total_quiz_questions = total_questions
|
| 92 |
+
st.session_state.current_question_num = current_question_num + 1
|
| 93 |
+
|
| 94 |
+
# Select a random word as the question
|
| 95 |
+
st.session_state.current_quiz_word = random.choice(vocabulary)
|
| 96 |
+
word = st.session_state.current_quiz_word
|
| 97 |
+
|
| 98 |
+
# Determine what question types are possible with this word
|
| 99 |
+
possible_types = self.get_possible_question_types(word, vocabulary)
|
| 100 |
+
|
| 101 |
+
# For debugging: log the available question types
|
| 102 |
+
if st.session_state.debug_quiz:
|
| 103 |
+
st.sidebar.markdown(f"**Word:** {word.get('word_original', 'unknown')}")
|
| 104 |
+
st.sidebar.markdown(f"**Possible types:** {len(possible_types)}")
|
| 105 |
+
for qtype in possible_types:
|
| 106 |
+
st.sidebar.markdown(f"- {qtype}")
|
| 107 |
+
|
| 108 |
+
# Choose question type:
|
| 109 |
+
if hasattr(st.session_state, 'force_diverse') and st.session_state.force_diverse:
|
| 110 |
+
# Force diverse questions by cycling through types
|
| 111 |
+
# Based on the current_question_num, use a different question type
|
| 112 |
+
if 'question_type_history' not in st.session_state:
|
| 113 |
+
st.session_state.question_type_history = []
|
| 114 |
+
|
| 115 |
+
# Get types we've used less frequently
|
| 116 |
+
type_counts = {}
|
| 117 |
+
for qtype in self.QUESTION_TYPES:
|
| 118 |
+
used_count = sum(1 for q in st.session_state.question_type_history if q['type'] == qtype)
|
| 119 |
+
type_counts[qtype] = used_count
|
| 120 |
+
|
| 121 |
+
# Sort by least used
|
| 122 |
+
sorted_types = sorted(possible_types, key=lambda t: type_counts.get(t, 0))
|
| 123 |
+
|
| 124 |
+
# Pick the least used type that's possible
|
| 125 |
+
if sorted_types:
|
| 126 |
+
question_type = sorted_types[0]
|
| 127 |
+
else:
|
| 128 |
+
question_type = "translation_to_target" # Fallback
|
| 129 |
+
else:
|
| 130 |
+
# Normal selection logic
|
| 131 |
+
if current_question_num == 0:
|
| 132 |
+
question_type = "translation_to_target" # Start with basic translation
|
| 133 |
+
elif current_question_num == 1 and "translation_to_english" in possible_types:
|
| 134 |
+
question_type = "translation_to_english" # Second question type
|
| 135 |
+
else:
|
| 136 |
+
# For subsequent questions, choose randomly from possible types
|
| 137 |
+
question_type = random.choice(possible_types)
|
| 138 |
+
|
| 139 |
+
# Store the question type
|
| 140 |
+
st.session_state.current_question_type = question_type
|
| 141 |
+
|
| 142 |
+
# For debugging: record the question type chosen
|
| 143 |
+
if st.session_state.debug_quiz:
|
| 144 |
+
if 'question_type_history' not in st.session_state:
|
| 145 |
+
st.session_state.question_type_history = []
|
| 146 |
+
|
| 147 |
+
st.session_state.question_type_history.append({
|
| 148 |
+
'num': current_question_num,
|
| 149 |
+
'word': word.get('word_original', 'unknown'),
|
| 150 |
+
'type': question_type
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
# Try to set up the chosen question type
|
| 154 |
+
success = False
|
| 155 |
+
try:
|
| 156 |
+
# Create options based on question type
|
| 157 |
+
if question_type == "translation_to_target":
|
| 158 |
+
# Translation question: English → Target language
|
| 159 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=False)
|
| 160 |
+
success = True
|
| 161 |
+
|
| 162 |
+
elif question_type == "translation_to_english":
|
| 163 |
+
# Reverse translation: Target language → English
|
| 164 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=True)
|
| 165 |
+
success = True
|
| 166 |
+
|
| 167 |
+
elif question_type == "image_recognition":
|
| 168 |
+
# Image recognition: Show image, select correct word
|
| 169 |
+
if word.get('image_path') and os.path.exists(word.get('image_path', '')):
|
| 170 |
+
self.setup_image_recognition_question(word, vocabulary, languages)
|
| 171 |
+
success = True
|
| 172 |
+
|
| 173 |
+
elif question_type == "category_match":
|
| 174 |
+
# Category matching: Match word to correct category
|
| 175 |
+
if word.get('category'):
|
| 176 |
+
self.setup_category_match_question(word, vocabulary, languages)
|
| 177 |
+
success = True
|
| 178 |
+
|
| 179 |
+
elif question_type == "sentence_completion":
|
| 180 |
+
# Sentence completion: Fill in blank in sentence
|
| 181 |
+
example = self.get_example_sentence(word['word_original'], word['language_translated'])
|
| 182 |
+
if example and example.get('translated'):
|
| 183 |
+
self.setup_sentence_completion_question(word, vocabulary, languages)
|
| 184 |
+
success = True
|
| 185 |
+
|
| 186 |
+
elif question_type == "multiple_choice_category":
|
| 187 |
+
# Choose words from same category
|
| 188 |
+
if word.get('category'):
|
| 189 |
+
same_category_words = [w for w in vocabulary
|
| 190 |
+
if w.get('category') == word.get('category') and w['id'] != word['id']]
|
| 191 |
+
if len(same_category_words) >= 2:
|
| 192 |
+
self.setup_multiple_choice_category_question(word, vocabulary, languages)
|
| 193 |
+
success = True
|
| 194 |
+
|
| 195 |
+
elif question_type == "audio_recognition":
|
| 196 |
+
# Audio recognition: Hear word, select correct option
|
| 197 |
+
audio_bytes = self.text_to_speech(word['word_translated'], word['language_translated'])
|
| 198 |
+
if audio_bytes:
|
| 199 |
+
self.setup_audio_recognition_question(word, vocabulary, languages)
|
| 200 |
+
success = True
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
if st.session_state.debug_quiz:
|
| 204 |
+
st.sidebar.error(f"Error setting up {question_type}: {str(e)}")
|
| 205 |
+
success = False
|
| 206 |
+
|
| 207 |
+
# Fallback to translation if setup failed
|
| 208 |
+
if not success:
|
| 209 |
+
if st.session_state.debug_quiz:
|
| 210 |
+
st.sidebar.warning(f"Falling back to translation question")
|
| 211 |
+
|
| 212 |
+
# Update question type history if we're tracking
|
| 213 |
+
if st.session_state.debug_quiz and 'question_type_history' in st.session_state:
|
| 214 |
+
st.session_state.question_type_history[-1]['type'] = "translation_to_target (fallback)"
|
| 215 |
+
|
| 216 |
+
# Use translation as fallback
|
| 217 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=False)
|
| 218 |
+
st.session_state.current_question_type = "translation_to_target"
|
| 219 |
+
|
| 220 |
+
# Set up question elements
|
| 221 |
+
st.session_state.answered = False
|
| 222 |
+
st.session_state.feedback_message = ""
|
| 223 |
+
st.session_state.selected_option = None
|
| 224 |
+
|
| 225 |
+
return True
|
| 226 |
+
|
| 227 |
+
def setup_translation_question(self, word, vocabulary, languages, to_english=False):
|
| 228 |
+
"""Set up a basic translation question."""
|
| 229 |
+
if to_english:
|
| 230 |
+
# Target language → English
|
| 231 |
+
st.session_state.question_text = f"What is the English translation of '{word['word_translated']}'?"
|
| 232 |
+
|
| 233 |
+
# Create options (3 wrong + 1 correct)
|
| 234 |
+
options = [{'id': word['id'], 'word': word['word_original']}]
|
| 235 |
+
|
| 236 |
+
# Find wrong options with different original words
|
| 237 |
+
remaining_vocab = [w for w in vocabulary if w['id'] != word['id']]
|
| 238 |
+
while len(options) < 4 and remaining_vocab:
|
| 239 |
+
wrong_option = random.choice(remaining_vocab)
|
| 240 |
+
remaining_vocab.remove(wrong_option)
|
| 241 |
+
|
| 242 |
+
if not any(o['word'] == wrong_option['word_original'] for o in options):
|
| 243 |
+
options.append({
|
| 244 |
+
'id': wrong_option['id'],
|
| 245 |
+
'word': wrong_option['word_original']
|
| 246 |
+
})
|
| 247 |
+
else:
|
| 248 |
+
# English → Target language
|
| 249 |
+
st.session_state.question_text = f"What is the {self.get_language_name_from_code(word['language_translated'], languages)} translation of '{word['word_original']}'?"
|
| 250 |
+
|
| 251 |
+
# Create options (3 wrong + 1 correct)
|
| 252 |
+
options = [{'id': word['id'], 'word': word['word_translated']}]
|
| 253 |
+
|
| 254 |
+
# Find wrong options (preferably in the same language)
|
| 255 |
+
same_lang_vocab = [w for w in vocabulary
|
| 256 |
+
if w['id'] != word['id'] and w['language_translated'] == word['language_translated']]
|
| 257 |
+
|
| 258 |
+
# If not enough words in the same language, use words from any language
|
| 259 |
+
if len(same_lang_vocab) < 3:
|
| 260 |
+
remaining_vocab = [w for w in vocabulary if w['id'] != word['id']]
|
| 261 |
+
else:
|
| 262 |
+
remaining_vocab = same_lang_vocab
|
| 263 |
+
|
| 264 |
+
while len(options) < 4 and remaining_vocab:
|
| 265 |
+
wrong_option = random.choice(remaining_vocab)
|
| 266 |
+
remaining_vocab.remove(wrong_option)
|
| 267 |
+
|
| 268 |
+
if not any(o['word'] == wrong_option['word_translated'] for o in options):
|
| 269 |
+
options.append({
|
| 270 |
+
'id': wrong_option['id'],
|
| 271 |
+
'word': wrong_option['word_translated']
|
| 272 |
+
})
|
| 273 |
+
|
| 274 |
+
# Shuffle options
|
| 275 |
+
random.shuffle(options)
|
| 276 |
+
st.session_state.quiz_options = options
|
| 277 |
+
|
| 278 |
+
def setup_image_recognition_question(self, word, vocabulary, languages):
|
| 279 |
+
"""Set up a question that shows an image and asks for the correct word."""
|
| 280 |
+
st.session_state.question_text = f"What is this object called in {self.get_language_name_from_code(word['language_translated'], languages)}?"
|
| 281 |
+
|
| 282 |
+
# Create options (3 wrong + 1 correct)
|
| 283 |
+
options = [{'id': word['id'], 'word': word['word_translated']}]
|
| 284 |
+
|
| 285 |
+
# Find wrong options in the same language
|
| 286 |
+
same_lang_vocab = [w for w in vocabulary
|
| 287 |
+
if w['id'] != word['id'] and w['language_translated'] == word['language_translated']]
|
| 288 |
+
|
| 289 |
+
# If not enough words in the same language, use words from any language
|
| 290 |
+
if len(same_lang_vocab) < 3:
|
| 291 |
+
remaining_vocab = [w for w in vocabulary if w['id'] != word['id']]
|
| 292 |
+
else:
|
| 293 |
+
remaining_vocab = same_lang_vocab
|
| 294 |
+
|
| 295 |
+
while len(options) < 4 and remaining_vocab:
|
| 296 |
+
wrong_option = random.choice(remaining_vocab)
|
| 297 |
+
remaining_vocab.remove(wrong_option)
|
| 298 |
+
|
| 299 |
+
if not any(o['word'] == wrong_option['word_translated'] for o in options):
|
| 300 |
+
options.append({
|
| 301 |
+
'id': wrong_option['id'],
|
| 302 |
+
'word': wrong_option['word_translated']
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
# Shuffle options
|
| 306 |
+
random.shuffle(options)
|
| 307 |
+
st.session_state.quiz_options = options
|
| 308 |
+
|
| 309 |
+
def setup_category_match_question(self, word, vocabulary, languages):
|
| 310 |
+
"""Set up a question to match a word to its category."""
|
| 311 |
+
st.session_state.question_text = f"Which category does the word '{word['word_original']}' ({word['word_translated']}) belong to?"
|
| 312 |
+
|
| 313 |
+
# Correct category
|
| 314 |
+
correct_category = word.get('category', 'other')
|
| 315 |
+
|
| 316 |
+
# Get all possible categories from vocabulary
|
| 317 |
+
all_categories = set(w.get('category', 'other') for w in vocabulary
|
| 318 |
+
if w.get('category') and w.get('category') != 'manual')
|
| 319 |
+
|
| 320 |
+
# Remove the correct category
|
| 321 |
+
if correct_category in all_categories:
|
| 322 |
+
all_categories.remove(correct_category)
|
| 323 |
+
|
| 324 |
+
# Create options (3 wrong + 1 correct)
|
| 325 |
+
options = [{'id': word['id'], 'word': correct_category}]
|
| 326 |
+
|
| 327 |
+
# Add wrong categories
|
| 328 |
+
all_categories = list(all_categories)
|
| 329 |
+
while len(options) < 4 and all_categories:
|
| 330 |
+
wrong_category = random.choice(all_categories)
|
| 331 |
+
all_categories.remove(wrong_category)
|
| 332 |
+
|
| 333 |
+
options.append({
|
| 334 |
+
'id': f"wrong_{len(options)}",
|
| 335 |
+
'word': wrong_category
|
| 336 |
+
})
|
| 337 |
+
|
| 338 |
+
# If we don't have enough categories, add some generic ones
|
| 339 |
+
generic_categories = ["clothing", "colors", "emotions", "weather", "family", "places"]
|
| 340 |
+
while len(options) < 4:
|
| 341 |
+
for cat in generic_categories:
|
| 342 |
+
if cat not in [o['word'] for o in options]:
|
| 343 |
+
options.append({
|
| 344 |
+
'id': f"wrong_{len(options)}",
|
| 345 |
+
'word': cat
|
| 346 |
+
})
|
| 347 |
+
break
|
| 348 |
+
|
| 349 |
+
# Shuffle options
|
| 350 |
+
random.shuffle(options)
|
| 351 |
+
st.session_state.quiz_options = options
|
| 352 |
+
|
| 353 |
+
def setup_sentence_completion_question(self, word, vocabulary, languages):
|
| 354 |
+
"""Set up a sentence completion question."""
|
| 355 |
+
# Create a sentence with the word
|
| 356 |
+
if word.get('word_original'):
|
| 357 |
+
# Get an example sentence
|
| 358 |
+
example = self.get_example_sentence(word['word_original'], word['language_translated'])
|
| 359 |
+
|
| 360 |
+
if example and example.get('translated'):
|
| 361 |
+
# Use the translated example
|
| 362 |
+
full_sentence = example['translated']
|
| 363 |
+
|
| 364 |
+
# Replace the word with a blank
|
| 365 |
+
blank_sentence = full_sentence.replace(word['word_translated'], "______")
|
| 366 |
+
|
| 367 |
+
if blank_sentence != full_sentence: # Only if replacement worked
|
| 368 |
+
st.session_state.question_text = "Complete this sentence: " + blank_sentence
|
| 369 |
+
|
| 370 |
+
# Create options (3 wrong + 1 correct)
|
| 371 |
+
options = [{'id': word['id'], 'word': word['word_translated']}]
|
| 372 |
+
|
| 373 |
+
# Find wrong options in the same language
|
| 374 |
+
same_lang_vocab = [w for w in vocabulary
|
| 375 |
+
if w['id'] != word['id'] and w['language_translated'] == word['language_translated']]
|
| 376 |
+
|
| 377 |
+
# If not enough words, use words from any language
|
| 378 |
+
if len(same_lang_vocab) < 3:
|
| 379 |
+
remaining_vocab = [w for w in vocabulary if w['id'] != word['id']]
|
| 380 |
+
else:
|
| 381 |
+
remaining_vocab = same_lang_vocab
|
| 382 |
+
|
| 383 |
+
while len(options) < 4 and remaining_vocab:
|
| 384 |
+
wrong_option = random.choice(remaining_vocab)
|
| 385 |
+
remaining_vocab.remove(wrong_option)
|
| 386 |
+
|
| 387 |
+
if not any(o['word'] == wrong_option['word_translated'] for o in options):
|
| 388 |
+
options.append({
|
| 389 |
+
'id': wrong_option['id'],
|
| 390 |
+
'word': wrong_option['word_translated']
|
| 391 |
+
})
|
| 392 |
+
|
| 393 |
+
# Shuffle options
|
| 394 |
+
random.shuffle(options)
|
| 395 |
+
st.session_state.quiz_options = options
|
| 396 |
+
return
|
| 397 |
+
|
| 398 |
+
# Fallback to basic translation if sentence creation failed
|
| 399 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=False)
|
| 400 |
+
|
| 401 |
+
def setup_multiple_choice_category_question(self, word, vocabulary, languages):
|
| 402 |
+
"""Set up a question to identify words in the same category."""
|
| 403 |
+
category = word.get('category', 'other')
|
| 404 |
+
|
| 405 |
+
# Get all words in the same category
|
| 406 |
+
same_category_words = [w for w in vocabulary
|
| 407 |
+
if w.get('category') == category and w['id'] != word['id']]
|
| 408 |
+
|
| 409 |
+
if len(same_category_words) < 2:
|
| 410 |
+
# Not enough words in this category, fall back to translation
|
| 411 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=False)
|
| 412 |
+
return
|
| 413 |
+
|
| 414 |
+
# Select a random word from the same category
|
| 415 |
+
selected_cat_word = random.choice(same_category_words)
|
| 416 |
+
same_category_words.remove(selected_cat_word)
|
| 417 |
+
|
| 418 |
+
st.session_state.question_text = f"Which word belongs to the same category as '{word['word_original']}' and '{selected_cat_word['word_original']}'?"
|
| 419 |
+
|
| 420 |
+
# Create options with 1 correct (same category) and 3 wrong (different categories)
|
| 421 |
+
if same_category_words:
|
| 422 |
+
correct_option = random.choice(same_category_words)
|
| 423 |
+
options = [{'id': correct_option['id'], 'word': correct_option['word_original']}]
|
| 424 |
+
else:
|
| 425 |
+
# No more words in the same category, fall back to translation
|
| 426 |
+
self.setup_translation_question(word, vocabulary, languages, to_english=False)
|
| 427 |
+
return
|
| 428 |
+
|
| 429 |
+
# Get words from different categories
|
| 430 |
+
diff_category_words = [w for w in vocabulary
|
| 431 |
+
if w.get('category') != category and w.get('category') and w.get('category') != 'manual']
|
| 432 |
+
|
| 433 |
+
# Add wrong options
|
| 434 |
+
while len(options) < 4 and diff_category_words:
|
| 435 |
+
wrong_option = random.choice(diff_category_words)
|
| 436 |
+
diff_category_words.remove(wrong_option)
|
| 437 |
+
|
| 438 |
+
if not any(o['word'] == wrong_option['word_original'] for o in options):
|
| 439 |
+
options.append({
|
| 440 |
+
'id': wrong_option['id'],
|
| 441 |
+
'word': wrong_option['word_original']
|
| 442 |
+
})
|
| 443 |
+
|
| 444 |
+
# Shuffle options
|
| 445 |
+
random.shuffle(options)
|
| 446 |
+
st.session_state.quiz_options = options
|
| 447 |
+
|
| 448 |
+
def setup_audio_recognition_question(self, word, vocabulary, languages):
|
| 449 |
+
"""Set up an audio recognition question."""
|
| 450 |
+
st.session_state.question_text = "Listen to the pronunciation and select the correct word:"
|
| 451 |
+
|
| 452 |
+
# Generate audio for the word
|
| 453 |
+
audio_bytes = self.text_to_speech(word['word_translated'], word['language_translated'])
|
| 454 |
+
st.session_state.question_audio = audio_bytes
|
| 455 |
+
|
| 456 |
+
# Create options (3 wrong + 1 correct)
|
| 457 |
+
options = [{'id': word['id'], 'word': word['word_translated']}]
|
| 458 |
+
|
| 459 |
+
# Find wrong options in the same language
|
| 460 |
+
same_lang_vocab = [w for w in vocabulary
|
| 461 |
+
if w['id'] != word['id'] and w['language_translated'] == word['language_translated']]
|
| 462 |
+
|
| 463 |
+
# If not enough words in the same language, use words from any language
|
| 464 |
+
if len(same_lang_vocab) < 3:
|
| 465 |
+
remaining_vocab = [w for w in vocabulary if w['id'] != word['id']]
|
| 466 |
+
else:
|
| 467 |
+
remaining_vocab = same_lang_vocab
|
| 468 |
+
|
| 469 |
+
while len(options) < 4 and remaining_vocab:
|
| 470 |
+
wrong_option = random.choice(remaining_vocab)
|
| 471 |
+
remaining_vocab.remove(wrong_option)
|
| 472 |
+
|
| 473 |
+
if not any(o['word'] == wrong_option['word_translated'] for o in options):
|
| 474 |
+
options.append({
|
| 475 |
+
'id': wrong_option['id'],
|
| 476 |
+
'word': wrong_option['word_translated']
|
| 477 |
+
})
|
| 478 |
+
|
| 479 |
+
# Shuffle options
|
| 480 |
+
random.shuffle(options)
|
| 481 |
+
st.session_state.quiz_options = options
|
| 482 |
+
|
| 483 |
+
def check_answer(self, selected_index, languages, gamification=None):
|
| 484 |
+
"""Check if selected quiz answer is correct with enhanced feedback."""
|
| 485 |
+
if st.session_state.answered:
|
| 486 |
+
return
|
| 487 |
+
|
| 488 |
+
selected_option = st.session_state.quiz_options[selected_index]
|
| 489 |
+
st.session_state.selected_option = selected_option
|
| 490 |
+
|
| 491 |
+
word = st.session_state.current_quiz_word
|
| 492 |
+
question_type = st.session_state.current_question_type
|
| 493 |
+
|
| 494 |
+
# Determine if answer is correct based on question type
|
| 495 |
+
is_correct = False
|
| 496 |
+
|
| 497 |
+
if question_type in ["translation_to_target", "translation_to_english", "image_recognition", "audio_recognition"]:
|
| 498 |
+
# For translation and recognition questions, check if IDs match
|
| 499 |
+
is_correct = selected_option['id'] == word['id']
|
| 500 |
+
|
| 501 |
+
elif question_type == "category_match":
|
| 502 |
+
# For category matching, check if selected category matches word category
|
| 503 |
+
is_correct = selected_option['word'] == word.get('category', 'other')
|
| 504 |
+
|
| 505 |
+
elif question_type == "sentence_completion":
|
| 506 |
+
# For sentence completion, check if selected word matches target word
|
| 507 |
+
is_correct = selected_option['word'] == word['word_translated']
|
| 508 |
+
|
| 509 |
+
elif question_type == "multiple_choice_category":
|
| 510 |
+
# For multiple choice category, check if the selected word has the same category
|
| 511 |
+
# Get the vocab item for the selected word
|
| 512 |
+
selected_word = next((w for w in st.session_state.vocabulary if w['id'] == selected_option['id']), None)
|
| 513 |
+
is_correct = selected_word and selected_word.get('category') == word.get('category')
|
| 514 |
+
|
| 515 |
+
# Update database
|
| 516 |
+
self.update_word_progress_direct(word['id'], is_correct)
|
| 517 |
+
|
| 518 |
+
# Update session stats
|
| 519 |
+
st.session_state.words_studied += 1
|
| 520 |
+
if is_correct:
|
| 521 |
+
st.session_state.words_learned += 1
|
| 522 |
+
st.session_state.quiz_score += 1
|
| 523 |
+
|
| 524 |
+
st.session_state.quiz_total += 1
|
| 525 |
+
st.session_state.answered = True
|
| 526 |
+
|
| 527 |
+
# Create detailed feedback based on question type
|
| 528 |
+
if is_correct:
|
| 529 |
+
feedback = "Correct! "
|
| 530 |
+
else:
|
| 531 |
+
feedback = "Not quite. "
|
| 532 |
+
|
| 533 |
+
if question_type == "translation_to_target":
|
| 534 |
+
if is_correct:
|
| 535 |
+
feedback += f"'{word['word_original']}' translates to '{word['word_translated']}' in {self.get_language_name_from_code(word['language_translated'], languages)}."
|
| 536 |
+
else:
|
| 537 |
+
correct_option = next((o for o in st.session_state.quiz_options if o['id'] == word['id']), None)
|
| 538 |
+
if correct_option:
|
| 539 |
+
feedback += f"You selected '{selected_option['word']}', but the correct translation of '{word['word_original']}' is '{correct_option['word']}'."
|
| 540 |
+
else:
|
| 541 |
+
feedback += f"The correct translation of '{word['word_original']}' is '{word['word_translated']}'."
|
| 542 |
+
|
| 543 |
+
elif question_type == "translation_to_english":
|
| 544 |
+
if is_correct:
|
| 545 |
+
feedback += f"'{word['word_translated']}' in {self.get_language_name_from_code(word['language_translated'], languages)} translates to '{word['word_original']}' in English."
|
| 546 |
+
else:
|
| 547 |
+
correct_option = next((o for o in st.session_state.quiz_options if o['id'] == word['id']), None)
|
| 548 |
+
if correct_option:
|
| 549 |
+
feedback += f"You selected '{selected_option['word']}', but the correct English translation of '{word['word_translated']}' is '{correct_option['word']}'."
|
| 550 |
+
else:
|
| 551 |
+
feedback += f"The correct English translation of '{word['word_translated']}' is '{word['word_original']}'."
|
| 552 |
+
|
| 553 |
+
elif question_type == "image_recognition":
|
| 554 |
+
if is_correct:
|
| 555 |
+
feedback += f"The image shows '{word['word_original']}' which is '{word['word_translated']}' in {self.get_language_name_from_code(word['language_translated'], languages)}."
|
| 556 |
+
else:
|
| 557 |
+
correct_option = next((o for o in st.session_state.quiz_options if o['id'] == word['id']), None)
|
| 558 |
+
if correct_option:
|
| 559 |
+
feedback += f"You selected '{selected_option['word']}', but the image shows '{word['word_original']}' which is '{correct_option['word']}' in {self.get_language_name_from_code(word['language_translated'], languages)}."
|
| 560 |
+
else:
|
| 561 |
+
feedback += f"The image shows '{word['word_original']}' which is '{word['word_translated']}' in {self.get_language_name_from_code(word['language_translated'], languages)}."
|
| 562 |
+
|
| 563 |
+
elif question_type == "category_match":
|
| 564 |
+
if is_correct:
|
| 565 |
+
feedback += f"'{word['word_original']}' ({word['word_translated']}) belongs to the category '{word['category']}'."
|
| 566 |
+
else:
|
| 567 |
+
feedback += f"You selected '{selected_option['word']}', but '{word['word_original']}' ({word['word_translated']}) belongs to the category '{word['category']}'."
|
| 568 |
+
|
| 569 |
+
elif question_type == "sentence_completion":
|
| 570 |
+
if is_correct:
|
| 571 |
+
feedback += f"The correct word to complete the sentence is '{word['word_translated']}'."
|
| 572 |
+
else:
|
| 573 |
+
feedback += f"You selected '{selected_option['word']}', but the correct word to complete the sentence is '{word['word_translated']}'."
|
| 574 |
+
|
| 575 |
+
elif question_type == "multiple_choice_category":
|
| 576 |
+
if is_correct:
|
| 577 |
+
feedback += f"Correct! '{selected_option['word']}' belongs to the same category ('{word['category']}') as '{word['word_original']}'."
|
| 578 |
+
else:
|
| 579 |
+
selected_word = next((w for w in st.session_state.vocabulary if w['id'] == selected_option['id']), None)
|
| 580 |
+
if selected_word:
|
| 581 |
+
feedback += f"You selected '{selected_option['word']}' (category: '{selected_word.get('category', 'unknown')}'), but it's not in the '{word['category']}' category."
|
| 582 |
+
else:
|
| 583 |
+
feedback += f"The words are in the '{word['category']}' category."
|
| 584 |
+
|
| 585 |
+
elif question_type == "audio_recognition":
|
| 586 |
+
if is_correct:
|
| 587 |
+
feedback += f"The audio was saying '{word['word_translated']}', which means '{word['word_original']}' in English."
|
| 588 |
+
else:
|
| 589 |
+
correct_option = next((o for o in st.session_state.quiz_options if o['id'] == word['id']), None)
|
| 590 |
+
if correct_option:
|
| 591 |
+
feedback += f"You selected '{selected_option['word']}', but the audio was saying '{correct_option['word']}', which means '{word['word_original']}' in English."
|
| 592 |
+
else:
|
| 593 |
+
feedback += f"The audio was saying '{word['word_translated']}', which means '{word['word_original']}' in English."
|
| 594 |
+
|
| 595 |
+
# Store feedback
|
| 596 |
+
st.session_state.feedback_message = feedback
|
| 597 |
+
|
| 598 |
+
# Check if any challenges are completed - with error handling
|
| 599 |
+
if gamification:
|
| 600 |
+
try:
|
| 601 |
+
gamification.check_challenge_progress(
|
| 602 |
+
quiz_score=st.session_state.quiz_score,
|
| 603 |
+
quiz_total=st.session_state.quiz_total
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
# Check for quiz-related achievements
|
| 607 |
+
if st.session_state.quiz_total >= 5: # Only check if quiz is substantial
|
| 608 |
+
gamification.check_achievements(
|
| 609 |
+
"quiz_completed",
|
| 610 |
+
score=st.session_state.quiz_score,
|
| 611 |
+
total=st.session_state.quiz_total
|
| 612 |
+
)
|
| 613 |
+
except Exception as e:
|
| 614 |
+
print(f"Gamification error in check_answer: {e}")
|
| 615 |
+
|
| 616 |
+
return is_correct
|
| 617 |
+
|
| 618 |
+
def display_quiz_question(self, languages, manage_session=None):
|
| 619 |
+
"""Display the current quiz question with enhanced UI."""
|
| 620 |
+
word = st.session_state.current_quiz_word
|
| 621 |
+
question_type = st.session_state.current_question_type
|
| 622 |
+
|
| 623 |
+
# Display progress
|
| 624 |
+
current_q = st.session_state.current_question_num
|
| 625 |
+
total_q = st.session_state.total_quiz_questions
|
| 626 |
+
progress = current_q / total_q
|
| 627 |
+
|
| 628 |
+
st.progress(progress)
|
| 629 |
+
st.markdown(f"**Question {current_q}/{total_q}**")
|
| 630 |
+
|
| 631 |
+
# Display question text
|
| 632 |
+
st.markdown(f"## {st.session_state.question_text}")
|
| 633 |
+
|
| 634 |
+
# Display additional elements based on question type
|
| 635 |
+
if question_type == "image_recognition" and word['image_path'] and os.path.exists(word['image_path']):
|
| 636 |
+
try:
|
| 637 |
+
image = Image.open(word['image_path'])
|
| 638 |
+
st.image(image, caption="Identify this object", width=300)
|
| 639 |
+
except Exception as e:
|
| 640 |
+
st.error(f"Error displaying image: {e}")
|
| 641 |
+
|
| 642 |
+
elif question_type == "audio_recognition" and hasattr(st.session_state, 'question_audio'):
|
| 643 |
+
try:
|
| 644 |
+
st.markdown("🔊 **Listen to the word:**")
|
| 645 |
+
st.markdown(self.get_audio_html(st.session_state.question_audio), unsafe_allow_html=True)
|
| 646 |
+
st.markdown("*Click the play button to hear the word*")
|
| 647 |
+
except Exception as e:
|
| 648 |
+
st.error(f"Error playing audio: {e}")
|
| 649 |
+
|
| 650 |
+
# Create answer buttons with custom styling
|
| 651 |
+
cols = st.columns(len(st.session_state.quiz_options))
|
| 652 |
+
for i, option in enumerate(st.session_state.quiz_options):
|
| 653 |
+
with cols[i]:
|
| 654 |
+
# Format the display text based on question type
|
| 655 |
+
if question_type == "category_match" or question_type == "multiple_choice_category":
|
| 656 |
+
display_text = option['word'].title() # Capitalize category names
|
| 657 |
+
else:
|
| 658 |
+
display_text = option['word']
|
| 659 |
+
|
| 660 |
+
# Determine button appearance based on answer status
|
| 661 |
+
if st.session_state.answered:
|
| 662 |
+
is_selected = i == st.session_state.quiz_options.index(st.session_state.selected_option)
|
| 663 |
+
is_correct_option = (
|
| 664 |
+
(question_type in ["translation_to_target", "translation_to_english", "image_recognition", "audio_recognition"]
|
| 665 |
+
and option['id'] == word['id']) or
|
| 666 |
+
(question_type == "category_match" and option['word'] == word.get('category', 'other')) or
|
| 667 |
+
(question_type == "sentence_completion" and option['word'] == word['word_translated']) or
|
| 668 |
+
(question_type == "multiple_choice_category" and
|
| 669 |
+
next((w for w in st.session_state.vocabulary if w['id'] == option['id']), {}).get('category') == word.get('category'))
|
| 670 |
+
)
|
| 671 |
+
|
| 672 |
+
if is_correct_option:
|
| 673 |
+
if is_selected:
|
| 674 |
+
# Correct and selected - success with checkmark
|
| 675 |
+
st.success(f"✓ {display_text}")
|
| 676 |
+
else:
|
| 677 |
+
# Correct but not selected - just success
|
| 678 |
+
st.success(display_text)
|
| 679 |
+
else:
|
| 680 |
+
if is_selected:
|
| 681 |
+
# Wrong and selected - error with X
|
| 682 |
+
st.error(f"✗ {display_text}")
|
| 683 |
+
else:
|
| 684 |
+
# Wrong and not selected - neutral
|
| 685 |
+
st.button(display_text, key=f"option_{i}", disabled=True)
|
| 686 |
+
else:
|
| 687 |
+
# Not answered yet - clickable button
|
| 688 |
+
if st.button(display_text, key=f"option_{i}"):
|
| 689 |
+
is_correct = self.check_answer(i, languages, st.session_state.get('gamification', None))
|
| 690 |
+
# Force rerun to update UI
|
| 691 |
+
st.rerun()
|
| 692 |
+
|
| 693 |
+
# Display feedback after answering
|
| 694 |
+
if st.session_state.answered:
|
| 695 |
+
# Display feedback message
|
| 696 |
+
if st.session_state.feedback_message:
|
| 697 |
+
feedback_type = "success" if st.session_state.quiz_score > st.session_state.quiz_total - 1 else "error"
|
| 698 |
+
if feedback_type == "success":
|
| 699 |
+
st.success(st.session_state.feedback_message)
|
| 700 |
+
else:
|
| 701 |
+
st.error(st.session_state.feedback_message)
|
| 702 |
+
|
| 703 |
+
# Pronunciation section
|
| 704 |
+
if question_type != "audio_recognition": # Don't show if already played
|
| 705 |
+
st.markdown("### Pronunciation:")
|
| 706 |
+
audio_bytes = self.text_to_speech(word['word_translated'], word['language_translated'])
|
| 707 |
+
if audio_bytes:
|
| 708 |
+
st.markdown(self.get_audio_html(audio_bytes), unsafe_allow_html=True)
|
| 709 |
+
|
| 710 |
+
# Add pronunciation tips
|
| 711 |
+
pronunciation_tips = self.get_pronunciation_guide(word['word_translated'], word['language_translated'])
|
| 712 |
+
if pronunciation_tips:
|
| 713 |
+
st.markdown("**Tips:**")
|
| 714 |
+
for tip in pronunciation_tips:
|
| 715 |
+
st.markdown(f"- {tip}")
|
| 716 |
+
|
| 717 |
+
# Display an example if not already shown
|
| 718 |
+
if question_type != "sentence_completion":
|
| 719 |
+
example = self.get_example_sentence(word['word_original'], word['language_translated'])
|
| 720 |
+
if example and example.get('translated'):
|
| 721 |
+
st.markdown("**Example usage:**")
|
| 722 |
+
st.markdown(f"**English:** {example['english']}")
|
| 723 |
+
st.markdown(f"**{self.get_language_name_from_code(word['language_translated'], languages)}:** {example['translated']}")
|
| 724 |
+
|
| 725 |
+
# Next question or finish quiz button
|
| 726 |
+
if st.session_state.current_question_num < st.session_state.total_quiz_questions:
|
| 727 |
+
if st.button("Next Question", type="primary"):
|
| 728 |
+
# Setup next question
|
| 729 |
+
self.setup_new_question(
|
| 730 |
+
st.session_state.vocabulary,
|
| 731 |
+
languages,
|
| 732 |
+
current_question_num=st.session_state.current_question_num,
|
| 733 |
+
total_questions=st.session_state.total_quiz_questions
|
| 734 |
+
)
|
| 735 |
+
st.rerun()
|
| 736 |
+
else:
|
| 737 |
+
# Finish quiz button
|
| 738 |
+
if st.button("Finish Quiz", type="primary"):
|
| 739 |
+
st.session_state.current_quiz_word = None
|
| 740 |
+
st.session_state.quiz_options = []
|
| 741 |
+
st.session_state.quiz_completed = True
|
| 742 |
+
# End session
|
| 743 |
+
if st.session_state.session_id and manage_session:
|
| 744 |
+
manage_session("end")
|
| 745 |
+
st.rerun()
|
| 746 |
+
|
| 747 |
+
def display_quiz_results(self):
|
| 748 |
+
"""Display enhanced quiz results."""
|
| 749 |
+
st.success(f"Quiz completed! Your score: {st.session_state.quiz_score}/{st.session_state.quiz_total}")
|
| 750 |
+
|
| 751 |
+
# Calculate and display accuracy
|
| 752 |
+
accuracy = (st.session_state.quiz_score / st.session_state.quiz_total) * 100
|
| 753 |
+
|
| 754 |
+
# Create a gauge-like visualization
|
| 755 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 756 |
+
with col2:
|
| 757 |
+
# Create a pyplot figure for the gauge
|
| 758 |
+
fig, ax = plt.subplots(figsize=(8, 4))
|
| 759 |
+
|
| 760 |
+
# Define colors based on accuracy
|
| 761 |
+
if accuracy >= 90:
|
| 762 |
+
color = '#2ecc71' # Green
|
| 763 |
+
elif accuracy >= 70:
|
| 764 |
+
color = '#f39c12' # Orange
|
| 765 |
+
else:
|
| 766 |
+
color = '#e74c3c' # Red
|
| 767 |
+
|
| 768 |
+
# Create a simple gauge
|
| 769 |
+
ax.barh(0, accuracy, height=0.5, color=color)
|
| 770 |
+
ax.barh(0, 100, height=0.5, color='#ecf0f1', alpha=0.3)
|
| 771 |
+
|
| 772 |
+
# Add the percentage text
|
| 773 |
+
ax.text(accuracy/2, 0, f"{accuracy:.1f}%", ha='center', va='center', fontsize=20, color='white', fontweight='bold')
|
| 774 |
+
|
| 775 |
+
# Remove axes and set limits
|
| 776 |
+
ax.set_xlim(0, 100)
|
| 777 |
+
ax.set_ylim(-0.5, 0.5)
|
| 778 |
+
ax.axis('off')
|
| 779 |
+
|
| 780 |
+
# Add title
|
| 781 |
+
ax.set_title('Quiz Accuracy', fontsize=16, pad=20)
|
| 782 |
+
|
| 783 |
+
# Display the figure
|
| 784 |
+
st.pyplot(fig)
|
| 785 |
+
|
| 786 |
+
# Display feedback based on score
|
| 787 |
+
if accuracy >= 90:
|
| 788 |
+
st.balloons()
|
| 789 |
+
st.markdown("### 🎖️ Excellent job! You're mastering these words!")
|
| 790 |
+
st.markdown("Your fluency is improving rapidly! Keep up the great work.")
|
| 791 |
+
elif accuracy >= 70:
|
| 792 |
+
st.markdown("### 👍 Good work! Keep practicing to improve.")
|
| 793 |
+
st.markdown("You're making solid progress. A bit more practice will help these words stick.")
|
| 794 |
+
else:
|
| 795 |
+
st.markdown("### 📚 Keep practicing! You're on the right track.")
|
| 796 |
+
st.markdown("Learning a language takes time. Try reviewing these words again tomorrow.")
|
| 797 |
+
|
| 798 |
+
# Show learning suggestions
|
| 799 |
+
st.subheader("Learning Tips")
|
| 800 |
+
|
| 801 |
+
col1, col2 = st.columns(2)
|
| 802 |
+
|
| 803 |
+
with col1:
|
| 804 |
+
st.markdown("**Based on your quiz results:**")
|
| 805 |
+
if accuracy < 70:
|
| 806 |
+
st.markdown("- Try shorter, more frequent practice sessions")
|
| 807 |
+
st.markdown("- Focus on one category of words at a time")
|
| 808 |
+
st.markdown("- Use the audio pronunciation feature more often")
|
| 809 |
+
else:
|
| 810 |
+
st.markdown("- Challenge yourself with sentence completion")
|
| 811 |
+
st.markdown("- Try learning related words in groups")
|
| 812 |
+
st.markdown("- Practice your pronunciation by speaking aloud")
|
| 813 |
+
|
| 814 |
+
with col2:
|
| 815 |
+
st.markdown("**Next steps:**")
|
| 816 |
+
st.markdown("- Review any words you missed")
|
| 817 |
+
st.markdown("- Try a different quiz format tomorrow")
|
| 818 |
+
st.markdown("- Add more words to your vocabulary")
|
| 819 |
+
|
| 820 |
+
# Add a share/save results option
|
| 821 |
+
st.markdown("---")
|
| 822 |
+
st.markdown("**Save your progress:**")
|
| 823 |
+
|
| 824 |
+
col1, col2 = st.columns(2)
|
| 825 |
+
with col1:
|
| 826 |
+
if st.button("Practice these words again"):
|
| 827 |
+
# Start a new quiz with the same settings
|
| 828 |
+
st.session_state.quiz_score = 0
|
| 829 |
+
st.session_state.quiz_total = 0
|
| 830 |
+
st.session_state.quiz_completed = False
|
| 831 |
+
st.rerun()
|
| 832 |
+
|
| 833 |
+
with col2:
|
| 834 |
+
if st.button("Try a different language"):
|
| 835 |
+
# Reset quiz state completely
|
| 836 |
+
st.session_state.current_quiz_word = None
|
| 837 |
+
st.session_state.quiz_options = []
|
| 838 |
+
st.session_state.quiz_score = 0
|
| 839 |
+
st.session_state.quiz_total = 0
|
| 840 |
+
st.session_state.quiz_completed = False
|
| 841 |
+
st.rerun()
|
| 842 |
+
|
| 843 |
+
def start_new_quiz(self, vocabulary, languages, num_questions=5, manage_session=None):
|
| 844 |
+
"""Start a new quiz with enhanced functionality."""
|
| 845 |
+
# Reset quiz state
|
| 846 |
+
st.session_state.quiz_score = 0
|
| 847 |
+
st.session_state.quiz_total = 0
|
| 848 |
+
st.session_state.answered = False
|
| 849 |
+
st.session_state.quiz_completed = False
|
| 850 |
+
st.session_state.vocabulary = vocabulary # Store for later use
|
| 851 |
+
st.session_state.feedback_message = ""
|
| 852 |
+
|
| 853 |
+
if not vocabulary or len(vocabulary) < 4:
|
| 854 |
+
st.warning("Not enough vocabulary words for a quiz (need at least 4).")
|
| 855 |
+
return False
|
| 856 |
+
|
| 857 |
+
# Start a new session if needed
|
| 858 |
+
if not st.session_state.session_id and manage_session:
|
| 859 |
+
st.session_state.session_id = manage_session("start")
|
| 860 |
+
st.session_state.words_studied = 0
|
| 861 |
+
st.session_state.words_learned = 0
|
| 862 |
+
|
| 863 |
+
# Set up first question
|
| 864 |
+
self.setup_new_question(vocabulary, languages, 0, num_questions)
|
| 865 |
+
return True
|
requirements.txt
CHANGED
|
@@ -1,3 +1,19 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.29.0
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
pandas>=1.3.0
|
| 4 |
+
matplotlib>=3.7.0
|
| 5 |
+
Pillow>=9.0.0
|
| 6 |
+
opencv-python-headless>=4.5.0
|
| 7 |
+
gTTS>=2.2.0
|
| 8 |
+
deep-translator>=1.9.0
|
| 9 |
+
python-Levenshtein>=0.21.0
|
| 10 |
+
pydub>=0.25.1
|
| 11 |
+
rich>=10.14.0,<14.0.0
|
| 12 |
+
streamlit-webrtc==0.47.1
|
| 13 |
+
av>=10.0.0
|
| 14 |
+
SpeechRecognition>=3.8.1
|
| 15 |
+
soundfile>=0.13.1
|
| 16 |
+
librosa>=0.10.0
|
| 17 |
+
tensorflow==2.15.0
|
| 18 |
+
tensorflow-hub==0.16.1
|
| 19 |
+
transformers==4.35.2
|
vocam_ui.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import base64
|
| 3 |
+
|
| 4 |
+
# Define color palette
|
| 5 |
+
COLORS = {
|
| 6 |
+
"primary_dark": "#074173",
|
| 7 |
+
"primary_medium": "#1679AB",
|
| 8 |
+
"accent_light": "#5DEBD7",
|
| 9 |
+
"accent_lighter": "#C5FF95"
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
def apply_custom_css():
|
| 13 |
+
"""Apply custom CSS for Vocam UI enhancements."""
|
| 14 |
+
st.markdown("""
|
| 15 |
+
<style>
|
| 16 |
+
/* Main color palette */
|
| 17 |
+
:root {
|
| 18 |
+
--primary-dark: #074173;
|
| 19 |
+
--primary-medium: #1679AB;
|
| 20 |
+
--accent-light: #5DEBD7;
|
| 21 |
+
--accent-lighter: #C5FF95;
|
| 22 |
+
--text-light: #FFFFFF;
|
| 23 |
+
--text-dark: #333333;
|
| 24 |
+
--bg-light: #F8F9FA;
|
| 25 |
+
--bg-medium: #EFF3F6;
|
| 26 |
+
--card-bg: #FFFFFF;
|
| 27 |
+
--shadow: rgba(0, 0, 0, 0.1);
|
| 28 |
+
|
| 29 |
+
/* New highlight colors */
|
| 30 |
+
--highlight-orange: #FF9800;
|
| 31 |
+
--highlight-gold: #FFC107;
|
| 32 |
+
--highlight-aqua: #00FFFF;
|
| 33 |
+
--highlight-turquoise: #40E0D0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Base app styling */
|
| 37 |
+
.main .block-container {
|
| 38 |
+
padding-top: 1rem;
|
| 39 |
+
padding-bottom: 1rem;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Streamlit elements styling */
|
| 43 |
+
div.stButton > button {
|
| 44 |
+
background-color: var(--primary-medium);
|
| 45 |
+
color: white;
|
| 46 |
+
border-radius: 12px;
|
| 47 |
+
border: none;
|
| 48 |
+
padding: 0.5rem 1rem;
|
| 49 |
+
font-weight: 500;
|
| 50 |
+
box-shadow: 0 2px 4px var(--shadow);
|
| 51 |
+
transition: all 0.3s;
|
| 52 |
+
width: 100%; /* Make buttons full width of their container */
|
| 53 |
+
min-height: 38px; /* Consistent height */
|
| 54 |
+
text-align: center;
|
| 55 |
+
display: flex;
|
| 56 |
+
justify-content: center;
|
| 57 |
+
align-items: center;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
div.stButton > button:hover {
|
| 61 |
+
background-color: var(--primary-dark);
|
| 62 |
+
transform: translateY(-2px);
|
| 63 |
+
box-shadow: 0 4px 8px var(--shadow);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Fix session buttons spacing */
|
| 67 |
+
[data-testid="column"] > div.stButton,
|
| 68 |
+
[data-testid="column"] div.element-container {
|
| 69 |
+
margin-bottom: 0 !important;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/* Make tab content consistent height */
|
| 73 |
+
.stTabs [data-baseweb="tab-panel"] {
|
| 74 |
+
min-height: 250px; /* Adjust based on your needs */
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Fix camera/upload tabs */
|
| 78 |
+
button[role="tab"] {
|
| 79 |
+
font-weight: bold !important;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* More specific selectors for camera input to prevent jumping */
|
| 83 |
+
[data-testid="stCameraInput"] {
|
| 84 |
+
margin-bottom: 1rem !important;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Consistent height for file uploader */
|
| 88 |
+
[data-testid="stFileUploader"] {
|
| 89 |
+
margin-bottom: 1rem !important;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
div.stButton > button[data-baseweb="button"][kind="primary"] {
|
| 93 |
+
background-color: var(--accent-light);
|
| 94 |
+
color: var(--primary-dark);
|
| 95 |
+
font-weight: bold;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
div.stButton > button[data-baseweb="button"][kind="primary"]:hover {
|
| 99 |
+
background-color: var(--accent-lighter);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Info box styling for consistency */
|
| 103 |
+
.info-box {
|
| 104 |
+
background-color: #e6f7ff;
|
| 105 |
+
border-left: 5px solid #1890ff;
|
| 106 |
+
padding: 10px;
|
| 107 |
+
border-radius: 5px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Sidebar styling - FIXED with ROUNDED CORNERS */
|
| 111 |
+
section[data-testid="stSidebar"] {
|
| 112 |
+
background-color: var(--primary-dark);
|
| 113 |
+
}
|
| 114 |
+
section[data-testid="stSidebar"] > div {
|
| 115 |
+
background-color: var(--primary-dark);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Add rounded corners to all sidebar elements */
|
| 119 |
+
section[data-testid="stSidebar"] div[data-testid="stVerticalBlock"] > div {
|
| 120 |
+
border-radius: 12px !important;
|
| 121 |
+
margin-bottom: 8px;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* Special styling for My Progress section - BRIGHTER ORANGE */
|
| 125 |
+
section[data-testid="stSidebar"] div:has(h3:contains("My Progress")),
|
| 126 |
+
section[data-testid="stSidebar"] div:has(div:contains("My Progress")) {
|
| 127 |
+
background-color: var(--highlight-orange) !important;
|
| 128 |
+
border-radius: 12px !important;
|
| 129 |
+
padding: 10px !important;
|
| 130 |
+
margin-top: 12px !important;
|
| 131 |
+
margin-bottom: 12px !important;
|
| 132 |
+
box-shadow: 0 0 10px rgba(255, 152, 0, 0.5);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Make My Progress text pop */
|
| 136 |
+
section[data-testid="stSidebar"] div:has(h3:contains("My Progress")) h3,
|
| 137 |
+
section[data-testid="stSidebar"] div:has(div:contains("My Progress")) div {
|
| 138 |
+
color: var(--primary-dark) !important;
|
| 139 |
+
font-weight: bold !important;
|
| 140 |
+
text-shadow: 0 0 3px rgba(255, 255, 255, 0.5);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Special styling for Need Help section - BRIGHT AQUA */
|
| 144 |
+
section[data-testid="stSidebar"] div.stExpander:has(div:contains("Need Help")),
|
| 145 |
+
section[data-testid="stSidebar"] details:has(summary:contains("Need Help")) {
|
| 146 |
+
background-color: var(--highlight-aqua) !important;
|
| 147 |
+
border-radius: 12px !important;
|
| 148 |
+
margin-top: 12px !important;
|
| 149 |
+
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Make Need Help title more visible */
|
| 153 |
+
section[data-testid="stSidebar"] .streamlit-expanderHeader:contains("Need Help"),
|
| 154 |
+
section[data-testid="stSidebar"] summary:contains("Need Help") {
|
| 155 |
+
background-color: var(--highlight-turquoise) !important;
|
| 156 |
+
color: var(--primary-dark) !important;
|
| 157 |
+
font-weight: bold !important;
|
| 158 |
+
border-radius: 10px !important;
|
| 159 |
+
text-shadow: 0 0 3px rgba(255, 255, 255, 0.5);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* White text for standard elements */
|
| 163 |
+
section[data-testid="stSidebar"] h1,
|
| 164 |
+
section[data-testid="stSidebar"] h2,
|
| 165 |
+
section[data-testid="stSidebar"] h3,
|
| 166 |
+
section[data-testid="stSidebar"] h4,
|
| 167 |
+
section[data-testid="stSidebar"] h5,
|
| 168 |
+
section[data-testid="stSidebar"] p,
|
| 169 |
+
section[data-testid="stSidebar"] span:not(.st-emotion-cache-10trblm),
|
| 170 |
+
section[data-testid="stSidebar"] label,
|
| 171 |
+
section[data-testid="stSidebar"] .stMarkdown {
|
| 172 |
+
color: var(--text-light) !important;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Fix dropdown styling */
|
| 176 |
+
section[data-testid="stSidebar"] .stSelectbox label {
|
| 177 |
+
color: var(--text-light) !important;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* Style the selectbox itself with rounded corners */
|
| 181 |
+
section[data-testid="stSidebar"] .stSelectbox div[data-baseweb="select"] {
|
| 182 |
+
background-color: #ffffff !important;
|
| 183 |
+
border-radius: 10px !important;
|
| 184 |
+
overflow: hidden;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* Style the selectbox text */
|
| 188 |
+
section[data-testid="stSidebar"] .stSelectbox div[data-baseweb="select"] span {
|
| 189 |
+
color: var(--text-dark) !important;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* Ensure dropdown options are visible */
|
| 193 |
+
section[data-testid="stSidebar"] .stSelectbox option,
|
| 194 |
+
section[data-testid="stSidebar"] select option,
|
| 195 |
+
section[data-testid="stSidebar"] div[role="listbox"] *,
|
| 196 |
+
section[data-testid="stSidebar"] ul[role="listbox"] * {
|
| 197 |
+
color: var(--text-dark) !important;
|
| 198 |
+
background-color: #ffffff !important;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* Message box styling */
|
| 202 |
+
.success-box {
|
| 203 |
+
background-color: #f0fff4;
|
| 204 |
+
border-left: 5px solid #68d391;
|
| 205 |
+
padding: 10px;
|
| 206 |
+
border-radius: 5px;
|
| 207 |
+
margin-bottom: 10px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.info-box {
|
| 211 |
+
background-color: #e6f7ff;
|
| 212 |
+
border-left: 5px solid #1890ff;
|
| 213 |
+
padding: 10px;
|
| 214 |
+
border-radius: 5px;
|
| 215 |
+
margin-bottom: 10px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.warning-box {
|
| 219 |
+
background-color: #fff9e6;
|
| 220 |
+
border-left: 5px solid #ffc53d;
|
| 221 |
+
padding: 10px;
|
| 222 |
+
border-radius: 5px;
|
| 223 |
+
margin-bottom: 10px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.error-box {
|
| 227 |
+
background-color: #fff2f0;
|
| 228 |
+
border-left: 5px solid #ff4d4f;
|
| 229 |
+
padding: 10px;
|
| 230 |
+
border-radius: 5px;
|
| 231 |
+
margin-bottom: 10px;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* Better loading spinner - make it clearly visible but not intrusive */
|
| 235 |
+
.loading-container {
|
| 236 |
+
display: flex;
|
| 237 |
+
flex-direction: column;
|
| 238 |
+
align-items: center;
|
| 239 |
+
justify-content: center;
|
| 240 |
+
background-color: rgba(255, 255, 255, 0.95);
|
| 241 |
+
padding: 20px;
|
| 242 |
+
border-radius: 10px;
|
| 243 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 244 |
+
margin: 20px 0;
|
| 245 |
+
text-align: center;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.loading-spinner {
|
| 249 |
+
border: 5px solid #f3f3f3;
|
| 250 |
+
border-top: 5px solid var(--primary-medium);
|
| 251 |
+
border-radius: 50%;
|
| 252 |
+
width: 50px;
|
| 253 |
+
height: 50px;
|
| 254 |
+
animation: spin 1.5s linear infinite;
|
| 255 |
+
margin-bottom: 15px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
@keyframes spin {
|
| 259 |
+
0% { transform: rotate(0deg); }
|
| 260 |
+
100% { transform: rotate(360deg); }
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* Separator between input and results */
|
| 264 |
+
.result-separator {
|
| 265 |
+
border-top: 2px solid var(--accent-light);
|
| 266 |
+
margin: 20px 0;
|
| 267 |
+
width: 100%;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.scroll-indicator {
|
| 271 |
+
margin-top: 15px;
|
| 272 |
+
color: var(--primary-medium);
|
| 273 |
+
font-weight: bold;
|
| 274 |
+
animation: pulse 2s infinite;
|
| 275 |
+
display: none;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
@media (max-width: 768px) {
|
| 279 |
+
.scroll-indicator {
|
| 280 |
+
display: block;
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
@keyframes pulse {
|
| 285 |
+
0% { opacity: 0.6; }
|
| 286 |
+
50% { opacity: 1; }
|
| 287 |
+
100% { opacity: 0.6; }
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/* Card styling */
|
| 291 |
+
.vocam-card {
|
| 292 |
+
background-color: white;
|
| 293 |
+
border-radius: 10px;
|
| 294 |
+
padding: 15px;
|
| 295 |
+
margin-bottom: 15px;
|
| 296 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.vocam-card-accent {
|
| 300 |
+
background-color: #f0feff;
|
| 301 |
+
border-radius: 10px;
|
| 302 |
+
padding: 15px;
|
| 303 |
+
margin-bottom: 15px;
|
| 304 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
| 305 |
+
border-left: 5px solid var(--accent-light);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.word-card {
|
| 309 |
+
background-color: #f0feff;
|
| 310 |
+
border-radius: 10px;
|
| 311 |
+
padding: 10px 15px;
|
| 312 |
+
margin-bottom: 10px;
|
| 313 |
+
border-left: 4px solid var(--primary-medium);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.word-card h3 {
|
| 317 |
+
color: var(--primary-dark);
|
| 318 |
+
margin-top: 0;
|
| 319 |
+
margin-bottom: 5px;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.result-container {
|
| 323 |
+
border-top: 3px solid #f0f2f6;
|
| 324 |
+
margin: 20px 0;
|
| 325 |
+
padding-top: 10px;
|
| 326 |
+
width: 100%;
|
| 327 |
+
}
|
| 328 |
+
</style>
|
| 329 |
+
""", unsafe_allow_html=True)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# UI Helper Functions
|
| 333 |
+
def show_loading_spinner(text="Processing your image..."):
|
| 334 |
+
"""Display a custom loading spinner with scroll indication on mobile"""
|
| 335 |
+
spinner_html = f"""
|
| 336 |
+
<div class="loading-container">
|
| 337 |
+
<div class="loading-spinner"></div>
|
| 338 |
+
<p>{text}</p>
|
| 339 |
+
<div class="scroll-indicator">
|
| 340 |
+
<span>⬇️ Scroll down to view results ⬇️</span>
|
| 341 |
+
</div>
|
| 342 |
+
</div>
|
| 343 |
+
"""
|
| 344 |
+
return st.markdown(spinner_html, unsafe_allow_html=True)
|
| 345 |
+
|
| 346 |
+
def success_message(text):
|
| 347 |
+
"""Display a stylized success message"""
|
| 348 |
+
return st.markdown(f'<div class="success-box">✅ {text}</div>', unsafe_allow_html=True)
|
| 349 |
+
|
| 350 |
+
def info_message(text):
|
| 351 |
+
"""Display a stylized info message"""
|
| 352 |
+
return st.markdown(f'<div class="info-box">ℹ️ {text}</div>', unsafe_allow_html=True)
|
| 353 |
+
|
| 354 |
+
def warning_message(text):
|
| 355 |
+
"""Display a stylized warning message"""
|
| 356 |
+
return st.markdown(f'<div class="warning-box">⚠️ {text}</div>', unsafe_allow_html=True)
|
| 357 |
+
|
| 358 |
+
def error_message(text):
|
| 359 |
+
"""Display a stylized error message"""
|
| 360 |
+
return st.markdown(f'<div class="error-box">❌ {text}</div>', unsafe_allow_html=True)
|
| 361 |
+
|
| 362 |
+
def vocam_card(content_function, accent=False):
|
| 363 |
+
"""Create a card container with consistent styling"""
|
| 364 |
+
card_class = "vocam-card-accent" if accent else "vocam-card"
|
| 365 |
+
st.markdown(f'<div class="{card_class}">', unsafe_allow_html=True)
|
| 366 |
+
content_function()
|
| 367 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 368 |
+
|
| 369 |
+
def word_card(word_original, word_translated, audio_html=None, category=None):
|
| 370 |
+
"""Display a stylized word card"""
|
| 371 |
+
html = f"""
|
| 372 |
+
<div class="word-card">
|
| 373 |
+
<h3>{word_original} → {word_translated}</h3>
|
| 374 |
+
{f'<p>Category: {category}</p>' if category else ''}
|
| 375 |
+
{audio_html if audio_html else ''}
|
| 376 |
+
</div>
|
| 377 |
+
"""
|
| 378 |
+
return st.markdown(html, unsafe_allow_html=True)
|
| 379 |
+
|
| 380 |
+
def add_result_separator():
|
| 381 |
+
"""Add a visual separator between input and results for mobile"""
|
| 382 |
+
return st.markdown('<div class="result-container"></div>', unsafe_allow_html=True)
|
| 383 |
+
|
| 384 |
+
def add_scroll_indicator():
|
| 385 |
+
"""Add a mobile-friendly scroll indicator"""
|
| 386 |
+
return st.markdown('<div class="scroll-indicator">⬇️ Results Below ⬇️</div>', unsafe_allow_html=True)
|
| 387 |
+
|
| 388 |
+
def style_title(title_text):
|
| 389 |
+
"""Create a styled page title"""
|
| 390 |
+
return st.markdown(f'<h1 style="color: #074173;">{title_text}</h1>', unsafe_allow_html=True)
|
| 391 |
+
|
| 392 |
+
def style_section_title(title_text):
|
| 393 |
+
"""Create a styled section title"""
|
| 394 |
+
return st.markdown(f'<h3 style="color: #1679AB; margin-top: 20px;">{title_text}</h3>', unsafe_allow_html=True)
|
| 395 |
+
|
| 396 |
+
def style_audio_player(audio_bytes):
|
| 397 |
+
"""Get HTML for a styled audio player"""
|
| 398 |
+
if audio_bytes:
|
| 399 |
+
audio_base64 = base64.b64encode(audio_bytes).decode()
|
| 400 |
+
audio_tag = f'<audio src="data:audio/mp3;base64,{audio_base64}" controls style="width: 100%; max-width: 300px;"></audio>'
|
| 401 |
+
return audio_tag
|
| 402 |
+
return ""
|
| 403 |
+
|
| 404 |
+
def style_sidebar_header():
|
| 405 |
+
"""Style the sidebar header"""
|
| 406 |
+
st.sidebar.markdown('<h1 style="color: #5DEBD7;">🌍 Vocam</h1>', unsafe_allow_html=True)
|
| 407 |
+
|
| 408 |
+
def add_current_mode_indicator(app_mode):
|
| 409 |
+
"""Add a visual indicator for the current mode in the sidebar"""
|
| 410 |
+
st.sidebar.markdown(f"""
|
| 411 |
+
<div style="background-color: #5DEBD7; padding: 8px; border-radius: 5px; margin-top: 10px; margin-bottom: 20px;">
|
| 412 |
+
<p style="margin: 0; color: #074173; font-weight: bold;">Currently in: {app_mode}</p>
|
| 413 |
+
</div>
|
| 414 |
+
""", unsafe_allow_html=True)
|
| 415 |
+
|
| 416 |
+
def setup_sidebar_language_section(languages, st_session_state):
|
| 417 |
+
"""Set up the language selection section in the sidebar"""
|
| 418 |
+
st.sidebar.markdown('<h3 style="color: #5DEBD7; margin-top: 20px;">Language Settings</h3>', unsafe_allow_html=True)
|
| 419 |
+
selected_language = st.sidebar.selectbox(
|
| 420 |
+
"Select target language",
|
| 421 |
+
list(languages.keys()),
|
| 422 |
+
index=list(languages.values()).index(st_session_state.target_language) if st_session_state.target_language in languages.values() else 0
|
| 423 |
+
)
|
| 424 |
+
return selected_language
|
| 425 |
+
|
| 426 |
+
def display_session_info(st_session_state):
|
| 427 |
+
"""Display session information in the sidebar"""
|
| 428 |
+
st.sidebar.markdown('<h3 style="color: #5DEBD7; margin-top: 20px;">Session Info</h3>', unsafe_allow_html=True)
|
| 429 |
+
if st_session_state.session_id:
|
| 430 |
+
st.sidebar.markdown(f"""
|
| 431 |
+
<div style="background-color: #1679AB; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
|
| 432 |
+
<p style="color: white; margin: 0;"><strong>✅ Session active</strong></p>
|
| 433 |
+
</div>
|
| 434 |
+
<div style="background-color: #074173; padding: 10px; border-radius: 5px;">
|
| 435 |
+
<p style="color: white; margin: 0;">📚 Words studied: {st_session_state.words_studied}</p>
|
| 436 |
+
<p style="color: white; margin: 0;">🧠 Words learned: {st_session_state.words_learned}</p>
|
| 437 |
+
</div>
|
| 438 |
+
""", unsafe_allow_html=True)
|
| 439 |
+
else:
|
| 440 |
+
st.sidebar.markdown(f"""
|
| 441 |
+
<div style="background-color: #1679AB; padding: 10px; border-radius: 5px;">
|
| 442 |
+
<p style="color: white; margin: 0;">⚠️ No active session</p>
|
| 443 |
+
<p style="color: #C5FF95; margin-top: 5px; font-size: 0.9em;">Start a session in Camera Mode to track progress</p>
|
| 444 |
+
</div>
|
| 445 |
+
""", unsafe_allow_html=True)
|
| 446 |
+
|
| 447 |
+
def add_help_section():
|
| 448 |
+
"""Add a help section to the sidebar"""
|
| 449 |
+
with st.sidebar.expander("ℹ️ Need Help?"):
|
| 450 |
+
st.markdown("""
|
| 451 |
+
**Quick Tips:**
|
| 452 |
+
- 📸 Use **Camera Mode** to capture objects and learn new words
|
| 453 |
+
- 📚 Review your words in **My Vocabulary**
|
| 454 |
+
- 🎮 Test yourself in **Quiz Mode**
|
| 455 |
+
- 📊 Track your progress in **Statistics**
|
| 456 |
+
|
| 457 |
+
**On Mobile:**
|
| 458 |
+
- After taking a picture, scroll down to see results
|
| 459 |
+
- Tap buttons to navigate between sections
|
| 460 |
+
""")
|
| 461 |
+
|
| 462 |
+
def add_footer():
|
| 463 |
+
"""Add a footer to the page"""
|
| 464 |
+
st.markdown("""
|
| 465 |
+
<div style="text-align: center; margin-top: 40px; padding: 20px; color: #777;">
|
| 466 |
+
<p>Vocam | Created by İdil Doğa Türkmen</p>
|
| 467 |
+
</div>
|
| 468 |
+
""", unsafe_allow_html=True)
|
| 469 |
+
|
| 470 |
+
def empty_state(title, message, action_text=None, action_link=None):
|
| 471 |
+
"""Create a styled empty state container"""
|
| 472 |
+
html = f"""
|
| 473 |
+
<div style="text-align: center; padding: 50px 20px; background-color: #f8f9fa; border-radius: 10px; margin: 30px 0;">
|
| 474 |
+
<h3 style="color: #1679AB;">{title}</h3>
|
| 475 |
+
<p style="margin-bottom: 30px;">{message}</p>
|
| 476 |
+
"""
|
| 477 |
+
|
| 478 |
+
if action_text and action_link:
|
| 479 |
+
html += f"""
|
| 480 |
+
<button style="background-color: #5DEBD7; color: #074173; border: none; padding: 10px 20px; border-radius: 8px; font-weight: bold; cursor: pointer;"
|
| 481 |
+
onclick="document.querySelector('span:contains(\'{action_link}\')').click();">
|
| 482 |
+
{action_text}
|
| 483 |
+
</button>
|
| 484 |
+
"""
|
| 485 |
+
|
| 486 |
+
html += "</div>"
|
| 487 |
+
return st.markdown(html, unsafe_allow_html=True)
|
| 488 |
+
|
| 489 |
+
def style_plot(fig, title):
|
| 490 |
+
"""Apply consistent styling to matplotlib plots"""
|
| 491 |
+
ax = fig.gca()
|
| 492 |
+
|
| 493 |
+
# Style the chart
|
| 494 |
+
ax.spines['top'].set_visible(False)
|
| 495 |
+
ax.spines['right'].set_visible(False)
|
| 496 |
+
ax.spines['left'].set_color('#DDDDDD')
|
| 497 |
+
ax.spines['bottom'].set_color('#DDDDDD')
|
| 498 |
+
ax.set_xlabel('Category', fontsize=12, color='#333333')
|
| 499 |
+
ax.set_ylabel('Value', fontsize=12, color='#333333')
|
| 500 |
+
ax.set_title(title, fontsize=14, color='#074173')
|
| 501 |
+
return fig
|