idildogaturkmen commited on
Commit
5f694cd
·
verified ·
1 Parent(s): dcfad77

Upload 8 files

Browse files
Files changed (8) hide show
  1. app.py +0 -0
  2. custom_audio_recorder.py +1057 -0
  3. database.py +420 -0
  4. example_sentences.py +1341 -0
  5. gamification.py +1501 -0
  6. quiz_system.py +865 -0
  7. requirements.txt +19 -3
  8. 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
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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