Oviya commited on
Commit
0b92c10
·
1 Parent(s): 3f95782

update pron component

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. package.json +1 -0
  2. src/app/pronunciation/pronunciation.component.css +66 -3
  3. src/app/pronunciation/pronunciation.component.html +39 -16
  4. src/app/pronunciation/pronunciation.component.ts +274 -17
  5. src/app/pronunciation/pronunciation.service.ts +30 -58
  6. src/assets/audio/apple.mp3 +3 -0
  7. src/assets/audio/ball.mp3 +3 -0
  8. src/assets/audio/cat.mp3 +3 -0
  9. src/assets/audio/dog.mp3 +3 -0
  10. src/assets/audio/egg.mp3 +3 -0
  11. src/assets/audio/fish.mp3 +3 -0
  12. src/assets/audio/grapes.mp3 +3 -0
  13. src/assets/audio/hat.mp3 +3 -0
  14. src/assets/audio/ice_cream.mp3 +3 -0
  15. src/assets/audio/jar.mp3 +3 -0
  16. src/assets/audio/kite.mp3 +3 -0
  17. src/assets/audio/lion.mp3 +3 -0
  18. src/assets/audio/moon.mp3 +3 -0
  19. src/assets/audio/nest.mp3 +3 -0
  20. src/assets/audio/orange.mp3 +3 -0
  21. src/assets/audio/original/ball.m4a +0 -0
  22. src/assets/audio/original/cat.m4a +0 -0
  23. src/assets/audio/original/dog.m4a +0 -0
  24. src/assets/audio/original/egg.m4a +0 -0
  25. src/assets/audio/original/fish.m4a +0 -0
  26. src/assets/audio/original/grapes.m4a +0 -0
  27. src/assets/audio/original/hat.m4a +0 -0
  28. src/assets/audio/original/ice_cream.m4a +0 -0
  29. src/assets/audio/original/jar.m4a +0 -0
  30. src/assets/audio/original/kite.m4a +0 -0
  31. src/assets/audio/original/lion.m4a +0 -0
  32. src/assets/audio/original/moon.m4a +0 -0
  33. src/assets/audio/original/nest.m4a +0 -0
  34. src/assets/audio/original/orange.m4a +0 -0
  35. src/assets/audio/original/pig.m4a +0 -0
  36. src/assets/audio/original/queen.m4a +0 -0
  37. src/assets/audio/original/rabbit.m4a +0 -0
  38. src/assets/audio/original/sun.m4a +0 -0
  39. src/assets/audio/original/tree.m4a +0 -0
  40. src/assets/audio/original/van.m4a +0 -0
  41. src/assets/audio/original/watch.m4a +0 -0
  42. src/assets/audio/original/xylophone.m4a +0 -0
  43. src/assets/audio/original/yarn.m4a +0 -0
  44. src/assets/audio/original/zebra.m4a +0 -0
  45. src/assets/audio/pig.mp3 +3 -0
  46. src/assets/audio/queen.mp3 +3 -0
  47. src/assets/audio/rabbit.mp3 +3 -0
  48. src/assets/audio/sun.mp3 +3 -0
  49. src/assets/audio/tree.mp3 +3 -0
  50. src/assets/audio/umbrella.mp3 +3 -0
package.json CHANGED
@@ -30,6 +30,7 @@
30
  "rxjs": "~7.8.0",
31
  "tailwindcss": "^3.4.17",
32
  "tslib": "^2.3.0",
 
33
  "zone.js": "~0.14.3"
34
  },
35
  "devDependencies": {
 
30
  "rxjs": "~7.8.0",
31
  "tailwindcss": "^3.4.17",
32
  "tslib": "^2.3.0",
33
+ "wavesurfer.js": "^7.11.1",
34
  "zone.js": "~0.14.3"
35
  },
36
  "devDependencies": {
src/app/pronunciation/pronunciation.component.css CHANGED
@@ -8,8 +8,71 @@
8
  flex-direction: column;
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  .header {
12
- text-align: center;
 
 
13
  }
14
 
15
  .title {
@@ -53,8 +116,8 @@
53
  .controls-section {
54
  display: flex;
55
  flex-direction: column;
56
- justify-content: center;
57
- gap: 2vw;
58
  width: 30vw;
59
  }
60
 
 
8
  flex-direction: column;
9
  }
10
 
11
+ /* Keep the dropdown at the start */
12
+ .dropdown-row {
13
+ display: flex;
14
+ align-items: center;
15
+
16
+ }
17
+
18
+ /* Accessible helper — hide visually but keep available to screen readers */
19
+ .visually-hidden {
20
+ position: absolute !important;
21
+ height: 1px;
22
+ width: 1px;
23
+ overflow: hidden;
24
+ clip: rect(1px, 1px, 1px, 1px);
25
+ white-space: nowrap;
26
+ border: 0;
27
+ padding: 0;
28
+ margin: -1px;
29
+ }
30
+
31
+ /* Styled select that fits the app button styles */
32
+ .select-dropdown {
33
+ appearance: none;
34
+ -webkit-appearance: none;
35
+ -moz-appearance: none;
36
+ display: inline-block;
37
+ min-width: 180px;
38
+ height: 36px;
39
+ padding: 6px 36px 6px 12px; /* extra right padding for custom arrow */
40
+ font-size: 14px;
41
+ line-height: 1;
42
+ color: #111827;
43
+ background-color: #ffffff;
44
+ border: 1px solid #d1d5db;
45
+ border-radius: 8px;
46
+ background-image: linear-gradient(45deg, transparent 50%, #6b7280 50%), linear-gradient(135deg, #6b7280 50%, transparent 50%), linear-gradient(to right, #fff, #fff);
47
+ background-position: calc(100% - 18px) calc(1em + 2px), calc(100% - 13px) calc(1em + 2px), 100% 0;
48
+ background-size: 6px 6px, 6px 6px, 2.5em 2.5em;
49
+ background-repeat: no-repeat;
50
+ cursor: pointer;
51
+ }
52
+
53
+ /* Hover / focus states for accessibility */
54
+ .select-dropdown:hover {
55
+ border-color: #9ca3af;
56
+ }
57
+
58
+ .select-dropdown:focus {
59
+ outline: none;
60
+ box-shadow: 0 0 0 3px rgba(59,130,246,0.14);
61
+ border-color: #3b82f6;
62
+ }
63
+
64
+ /* Smaller variant if needed */
65
+ .select-dropdown.small {
66
+ min-width: 140px;
67
+ height: 32px;
68
+ font-size: 13px;
69
+ border-radius: 6px;
70
+ }
71
+
72
  .header {
73
+ /*text-align: center;*/
74
+ display:flex;
75
+ gap:13vw;
76
  }
77
 
78
  .title {
 
116
  .controls-section {
117
  display: flex;
118
  flex-direction: column;
119
+ justify-content: space-evenly;
120
+ gap: 0.5vw;
121
  width: 30vw;
122
  }
123
 
src/app/pronunciation/pronunciation.component.html CHANGED
@@ -1,7 +1,16 @@
1
  <div class="pron-container">
2
  <!-- Header -->
3
  <div class="header">
4
-
 
 
 
 
 
 
 
 
 
5
  <h2 class="title">Pronunciation Trainer</h2>
6
  </div>
7
 
@@ -44,7 +53,7 @@
44
  [class.active]="!isOriginal"
45
  (click)="isOriginal = false; updateSelection()">
46
  <span class="voice-text">Original Voice</span>
47
- </div>
48
 
49
  <div class="voice-label-group toggle-label-right"
50
  [class.active]="isOriginal"
@@ -94,6 +103,19 @@
94
  </div>
95
  </div>
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  <!-- Check Button -->
98
  <button class="btn btn-success full-width"
99
  id="checkBtn"
@@ -118,7 +140,7 @@
118
 
119
  <div class="results-section" id="resultsSection">
120
 
121
- <!-- Score Display: always show speakometer when result exists -->
122
  <div class="gauge-wrapper">
123
  <div class="gauge">
124
  <div class="gauge-arc"></div>
@@ -131,12 +153,12 @@
131
  </div>
132
  </div>
133
 
134
- <!-- Suggestions (paged, 2 per page) -->
135
- <div class="suggestions-section" *ngIf="result">
136
  <h3 class="suggestions-title">Feedback & Suggestions</h3>
137
 
138
- <!-- If structured suggestions exist, show current page (2 items) -->
139
- <div class="sugg-div" *ngIf="suggestions.length > 0; else legacySingleString">
140
  <div class="suggestions-page">
141
  <div class="suggestion-card" *ngFor="let s of pagedSuggestions; let i = index">
142
  <div class="suggestion-badge" aria-hidden="true">{{ s.id ?? (suggestionPage * suggestionsPerPage + i + 1) }}</div>
@@ -158,15 +180,16 @@
158
  <button class="btn btn-outline btn-nav pagebtn" (click)="nextSuggestion()" [disabled]="suggestionPage >= totalSuggestionPages - 1" aria-label="Next feedback">▶</button>
159
  </div>
160
  </div>
 
161
 
162
- <!-- Fallback when suggestion was provided as a single string -->
163
- <ng-template #legacySingleString>
164
- <div class="suggestion-item" *ngIf="result.suggestion">
165
- <div class="suggestion-content">
166
- <p class="suggestion-feedback">{{ result.suggestion }}</p>
167
- </div>
168
  </div>
169
- </ng-template>
170
  </div>
171
 
172
  <div>
@@ -188,6 +211,6 @@
188
 
189
  <button aria-label="Close" class="user-guide-close-icon" (click)="closePopup()">×</button>
190
  <!-- Add a hidden audio element for programmatic playback -->
191
-
192
- </div>
193
  </div>
 
 
1
  <div class="pron-container">
2
  <!-- Header -->
3
  <div class="header">
4
+ <div class="dropdown-row" aria-hidden="false">
5
+ <label for="pronModeSelect" class="visually-hidden">Pronunciation Mode</label>
6
+ <select id="pronModeSelect" class="select-dropdown" aria-label="Pronunciation Mode" (change)="onModeChange($any($event.target).value)">
7
+ <option value="phonetics">Phoneme-Level Accuracy Check</option>
8
+ <option value="waveform">Audio Similarity Check</option>
9
+ <option value="normaltext" disabled>Speech-to-Text Accuracy Check</option>
10
+ <option value="prosody" disabled>Prosody Check</option>
11
+ <option value="embedding" disabled>Embedding Similarity Check</option>
12
+ </select>
13
+ </div>
14
  <h2 class="title">Pronunciation Trainer</h2>
15
  </div>
16
 
 
53
  [class.active]="!isOriginal"
54
  (click)="isOriginal = false; updateSelection()">
55
  <span class="voice-text">Original Voice</span>
56
+ </div>
57
 
58
  <div class="voice-label-group toggle-label-right"
59
  [class.active]="isOriginal"
 
103
  </div>
104
  </div>
105
 
106
+ <!-- Waveform containers (shown only in Audio Similarity Check mode) -->
107
+ <div *ngIf="pronMode === 'waveform'" class="waveform-row">
108
+ <div class="waveform-block">
109
+ <div class="waveform-title">Teacher</div>
110
+ <div id="teacherWaveform" class="waveform-canvas"></div>
111
+ </div>
112
+
113
+ <div class="waveform-block">
114
+ <div class="waveform-title">Student</div>
115
+ <div id="studentWaveform" class="waveform-canvas"></div>
116
+ </div>
117
+ </div>
118
+
119
  <!-- Check Button -->
120
  <button class="btn btn-success full-width"
121
  id="checkBtn"
 
140
 
141
  <div class="results-section" id="resultsSection">
142
 
143
+ <!-- Score Display: always shown (initially 0) so speakometer is visible from start -->
144
  <div class="gauge-wrapper">
145
  <div class="gauge">
146
  <div class="gauge-arc"></div>
 
153
  </div>
154
  </div>
155
 
156
+ <!-- Suggestions: show whenever we have suggestions, regardless of result -->
157
+ <div class="suggestions-section" *ngIf="suggestions.length > 0">
158
  <h3 class="suggestions-title">Feedback & Suggestions</h3>
159
 
160
+ <!-- Structured suggestions -->
161
+ <div class="sugg-div">
162
  <div class="suggestions-page">
163
  <div class="suggestion-card" *ngFor="let s of pagedSuggestions; let i = index">
164
  <div class="suggestion-badge" aria-hidden="true">{{ s.id ?? (suggestionPage * suggestionsPerPage + i + 1) }}</div>
 
180
  <button class="btn btn-outline btn-nav pagebtn" (click)="nextSuggestion()" [disabled]="suggestionPage >= totalSuggestionPages - 1" aria-label="Next feedback">▶</button>
181
  </div>
182
  </div>
183
+ </div>
184
 
185
+ <!-- Legacy single-string suggestion: hide in select mode and only show when not using structured suggestions -->
186
+ <div class="suggestions-section" *ngIf="pronMode !== 'select' && (!suggestions || suggestions.length === 0) && result?.suggestion">
187
+ <h3 class="suggestions-title">Feedback & Suggestions</h3>
188
+ <div class="suggestion-item">
189
+ <div class="suggestion-content">
190
+ <p class="suggestion-feedback">{{ result?.suggestion }}</p>
191
  </div>
192
+ </div>
193
  </div>
194
 
195
  <div>
 
211
 
212
  <button aria-label="Close" class="user-guide-close-icon" (click)="closePopup()">×</button>
213
  <!-- Add a hidden audio element for programmatic playback -->
214
+
 
215
  </div>
216
+ </div>
src/app/pronunciation/pronunciation.component.ts CHANGED
@@ -1,8 +1,8 @@
1
- // (trimmed waveform/canvas logic; kept recording/playback, API and pagination)
2
  import { Component, OnDestroy, ChangeDetectorRef, Inject, OnInit } from '@angular/core';
3
  import { HttpClient } from '@angular/common/http';
4
  import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
5
  import { ApiService } from './pronunciation.service'; // adjust path if needed
 
6
 
7
  @Component({
8
  selector: 'app-pronunciation',
@@ -92,6 +92,11 @@ export class PronunciationComponent implements OnDestroy, OnInit {
92
  private recordingStopPromise?: Promise<void>;
93
  private recordingStopResolver?: (() => void) | null = null;
94
 
 
 
 
 
 
95
  constructor(private http: HttpClient, private cdr: ChangeDetectorRef,
96
  public dialogRef: MatDialogRef<PronunciationComponent>,
97
  @Inject(MAT_DIALOG_DATA) public data: any,
@@ -103,14 +108,69 @@ export class PronunciationComponent implements OnDestroy, OnInit {
103
  this.showQuestion(0);
104
  }
105
 
106
- // Called when the toggle changes
107
- updateSelection() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  // stop any current teacher playback and clear cache so selection change takes immediate effect
109
  this.stopTeacherPlayback();
110
  this.teacherAudioCache.clear();
111
  this.cdr.detectChanges();
112
  }
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  // Helper: build static asset path for a word (normalize to lowercase, underscores)
115
  private getStaticTeacherAudioPath(word: string): string {
116
  const clean = (word || '')
@@ -118,7 +178,7 @@ export class PronunciationComponent implements OnDestroy, OnInit {
118
  .trim()
119
  .replace(/\s+/g, '_') // spaces -> underscores
120
  .replace(/[^a-z0-9_]/g, ''); // remove other chars
121
- return `assets/audio/original/${clean}.m4a`;
122
  }
123
 
124
  // Play teacher audio: use local static file when original voice selected, otherwise fall back to existing flow.
@@ -200,10 +260,53 @@ export class PronunciationComponent implements OnDestroy, OnInit {
200
  });
201
  }
202
 
203
- // Simplified playback: no waveform/canvas support in finalized HTML
204
  public playAudioWithWaveform(src: string, type: 'teacher' | 'recorded'): void {
205
  if (!src) return;
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  if (type === 'teacher') {
208
  this.stopTeacherPlayback();
209
  this.teacherAudio = new Audio();
@@ -238,11 +341,32 @@ export class PronunciationComponent implements OnDestroy, OnInit {
238
  }
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  private stopTeacherPlayback(): void {
242
  if (this.teacherAudio) {
243
  try { this.teacherAudio.pause(); this.teacherAudio.onended = null; } catch { }
244
  this.teacherAudio = undefined;
245
  }
 
 
 
 
246
  }
247
 
248
  private stopRecordedPlayback(): void {
@@ -250,6 +374,9 @@ export class PronunciationComponent implements OnDestroy, OnInit {
250
  try { this.recordedAudio.pause(); this.recordedAudio.onended = null; } catch { }
251
  this.recordedAudio = undefined;
252
  }
 
 
 
253
  }
254
 
255
  // Loading state for check pronunciation request
@@ -269,15 +396,12 @@ export class PronunciationComponent implements OnDestroy, OnInit {
269
  }
270
  }
271
 
272
- // If there is an object URL for the finalized recording, prefer fetching it.
273
  let audioBlob: Blob | null = null;
274
 
275
  if (this.recordedBlobs && this.recordedBlobs.length > 0) {
276
- // merge recorded chunk blobs
277
  const inferredType = this.recordedBlobs[0]?.type || 'audio/webm';
278
  audioBlob = new Blob(this.recordedBlobs, { type: inferredType });
279
  } else if (this.audioBlobUrl) {
280
- // fetch blob from the object URL created in onstop
281
  try {
282
  const resp = await fetch(this.audioBlobUrl);
283
  audioBlob = await resp.blob();
@@ -313,17 +437,129 @@ export class PronunciationComponent implements OnDestroy, OnInit {
313
  form.append('audio', audioBlob, filename);
314
  form.append('word', this.word);
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  try {
317
  const res = await this.http.post<any>(`${this.backendURL}/pron/check_pronunciation`, form).toPromise();
318
 
319
- // Compute phonemic score (backend provides phoneme_similarity in range 0..1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  const phonemeSimilarity = Number(res?.phoneme_similarity ?? res?.phonemeSimilarity ?? 0);
321
  const phonemePct = Math.round(Math.max(0, Math.min(1, phonemeSimilarity)) * 100);
322
-
323
- // Keep acoustic score if you want to display/use later
324
  const acousticScore = Number(res?.acoustic_score ?? res?.score ?? res?.score_acoustic ?? 0);
325
 
326
- // Set the speakometer to show phonemic score scaled to 0..100
327
  this.result = {
328
  score: phonemePct,
329
  suggestion: (typeof res?.suggestion === 'string') ? res.suggestion : '',
@@ -332,7 +568,7 @@ export class PronunciationComponent implements OnDestroy, OnInit {
332
  acousticScore: isNaN(acousticScore) ? undefined : acousticScore
333
  };
334
 
335
- // Build structured suggestions with sanitized messages and derived titles
336
  if (Array.isArray(res?.suggestion)) {
337
  this.suggestions = res.suggestion.map((s: any, idx: number) => {
338
  const raw = (typeof s === 'string') ? s : (s.message ?? JSON.stringify(s));
@@ -352,8 +588,6 @@ export class PronunciationComponent implements OnDestroy, OnInit {
352
  } else {
353
  this.suggestions = [];
354
  }
355
-
356
- // Reset suggestion pagination
357
  this.suggestionPage = 0;
358
 
359
  } catch (err) {
@@ -406,6 +640,7 @@ export class PronunciationComponent implements OnDestroy, OnInit {
406
  ngOnDestroy(): void {
407
  this.stopTeacherPlayback();
408
  this.stopRecordedPlayback();
 
409
  if (this.micStream) {
410
  try { this.micStream.getTracks().forEach(t => t.stop()); } catch { }
411
  this.micStream = undefined;
@@ -427,6 +662,11 @@ export class PronunciationComponent implements OnDestroy, OnInit {
427
  requestTeacherAudio() {
428
  const cached = this.teacherAudioCache.get(this.word);
429
  if (cached) {
 
 
 
 
 
430
  const a = new Audio(cached);
431
  a.play().catch(err => console.warn('play failed', err));
432
  return;
@@ -435,8 +675,12 @@ export class PronunciationComponent implements OnDestroy, OnInit {
435
  this.api.generateTeacherAudio(this.word, this.selectedFile).subscribe({
436
  next: ({ audioUrl }) => {
437
  this.teacherAudioCache.set(this.word, audioUrl);
438
- const a = new Audio(audioUrl);
439
- a.play().catch(err => console.warn('play failed', err));
 
 
 
 
440
  },
441
  error: err => {
442
  console.error('generateTeacherAudio failed', err);
@@ -486,6 +730,11 @@ export class PronunciationComponent implements OnDestroy, OnInit {
486
  this.audioBlobUrl = URL.createObjectURL(audioBlob);
487
  this.recordedBlobUrl = this.audioBlobUrl;
488
 
 
 
 
 
 
489
  try { if (this.micStream) this.micStream.getTracks().forEach((t: any) => t.stop()); } catch { }
490
  this.micStream = undefined;
491
 
@@ -609,6 +858,14 @@ export class PronunciationComponent implements OnDestroy, OnInit {
609
  this.phonetics = q.phonetics ?? '';
610
  this.imgsrc = q.imgsrc ?? this.imgsrc;
611
 
 
 
 
 
 
 
 
 
612
  this.cdr.detectChanges();
613
  }
614
  }
 
 
1
  import { Component, OnDestroy, ChangeDetectorRef, Inject, OnInit } from '@angular/core';
2
  import { HttpClient } from '@angular/common/http';
3
  import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
4
  import { ApiService } from './pronunciation.service'; // adjust path if needed
5
+ import WaveSurfer from 'wavesurfer.js';
6
 
7
  @Component({
8
  selector: 'app-pronunciation',
 
92
  private recordingStopPromise?: Promise<void>;
93
  private recordingStopResolver?: (() => void) | null = null;
94
 
95
+ // WaveSurfer state
96
+ pronMode: string = 'phonetics'; // current dropdown mode
97
+ private teacherWaveSurfer: any;
98
+ private studentWaveSurfer: any;
99
+
100
  constructor(private http: HttpClient, private cdr: ChangeDetectorRef,
101
  public dialogRef: MatDialogRef<PronunciationComponent>,
102
  @Inject(MAT_DIALOG_DATA) public data: any,
 
108
  this.showQuestion(0);
109
  }
110
 
111
+ // handle dropdown mode change
112
+ public onModeChange(mode: string | null | undefined) {
113
+ const m = (mode ?? 'phonetics').toString().trim().toLowerCase();
114
+ console.log('[Pronunciation] onModeChange ->', { raw: mode, normalized: m });
115
+ this.pronMode = m;
116
+ if (m === 'waveform') {
117
+ this.initWaveSurfers();
118
+ const cached = this.teacherAudioCache.get(this.word);
119
+ if (cached && this.teacherWaveSurfer) {
120
+ try { this.teacherWaveSurfer.load(cached); } catch { /* ignore */ }
121
+ }
122
+ if (this.recordedBlobUrl && this.studentWaveSurfer) {
123
+ try { this.studentWaveSurfer.load(this.recordedBlobUrl); } catch { /* ignore */ }
124
+ }
125
+ } else {
126
+ this.destroyWaveSurfers();
127
+ }
128
+ }
129
+
130
+ // Added: updateSelection used by template toggle
131
+ public updateSelection(): void {
132
  // stop any current teacher playback and clear cache so selection change takes immediate effect
133
  this.stopTeacherPlayback();
134
  this.teacherAudioCache.clear();
135
  this.cdr.detectChanges();
136
  }
137
 
138
+ private initWaveSurfers(): void {
139
+ // avoid re-creating
140
+ if (!this.teacherWaveSurfer) {
141
+ try {
142
+ this.teacherWaveSurfer = WaveSurfer.create({
143
+ container: '#teacherWaveform',
144
+ waveColor: '#d0e9ff',
145
+ progressColor: '#007acc',
146
+ cursorColor: '#333',
147
+ height: 80
148
+
149
+ });
150
+ } catch (err) { console.warn('could not create teacher WaveSurfer', err); }
151
+ }
152
+
153
+ if (!this.studentWaveSurfer) {
154
+ try {
155
+ this.studentWaveSurfer = WaveSurfer.create({
156
+ container: '#studentWaveform',
157
+ waveColor: '#ffdcd0',
158
+ progressColor: '#ff6b3d',
159
+ cursorColor: '#333',
160
+ height: 80
161
+
162
+ });
163
+ } catch (err) { console.warn('could not create student WaveSurfer', err); }
164
+ }
165
+ }
166
+
167
+ private destroyWaveSurfers(): void {
168
+ try {
169
+ if (this.teacherWaveSurfer) { try { this.teacherWaveSurfer.destroy(); } catch { } this.teacherWaveSurfer = undefined; }
170
+ if (this.studentWaveSurfer) { try { this.studentWaveSurfer.destroy(); } catch { } this.studentWaveSurfer = undefined; }
171
+ } catch { /* ignore */ }
172
+ }
173
+
174
  // Helper: build static asset path for a word (normalize to lowercase, underscores)
175
  private getStaticTeacherAudioPath(word: string): string {
176
  const clean = (word || '')
 
178
  .trim()
179
  .replace(/\s+/g, '_') // spaces -> underscores
180
  .replace(/[^a-z0-9_]/g, ''); // remove other chars
181
+ return `assets/audio/${clean}.mp3`;
182
  }
183
 
184
  // Play teacher audio: use local static file when original voice selected, otherwise fall back to existing flow.
 
260
  });
261
  }
262
 
263
+ // Simplified playback: now supports WaveSurfer mode
264
  public playAudioWithWaveform(src: string, type: 'teacher' | 'recorded'): void {
265
  if (!src) return;
266
 
267
+ // If the dropdown is in waveform mode, use WaveSurfer instances
268
+ if (this.pronMode === 'waveform') {
269
+ // ensure wavesurfer instances exist
270
+ this.initWaveSurfers();
271
+
272
+ if (type === 'teacher') {
273
+ try {
274
+ if (this.teacherWaveSurfer) {
275
+ // load will accept blob/object URL or remote URL
276
+ this.teacherWaveSurfer.load(src);
277
+ // autoplay when ready
278
+ this.teacherWaveSurfer.once('ready', () => {
279
+ try { this.teacherWaveSurfer.play(); } catch { /* ignore */ }
280
+ });
281
+ } else {
282
+ // fallback to simple audio element
283
+ this.fallbackAudioPlay(src, 'teacher');
284
+ }
285
+ } catch (e) {
286
+ console.warn('WaveSurfer teacher play failed, fallback', e);
287
+ this.fallbackAudioPlay(src, 'teacher');
288
+ }
289
+ } else {
290
+ // recorded
291
+ try {
292
+ if (this.studentWaveSurfer) {
293
+ this.studentWaveSurfer.load(src);
294
+ this.studentWaveSurfer.once('ready', () => {
295
+ try { this.studentWaveSurfer.play(); } catch { /* ignore */ }
296
+ });
297
+ } else {
298
+ this.fallbackAudioPlay(src, 'recorded');
299
+ }
300
+ } catch (e) {
301
+ console.warn('WaveSurfer student play failed, fallback', e);
302
+ this.fallbackAudioPlay(src, 'recorded');
303
+ }
304
+ }
305
+
306
+ return;
307
+ }
308
+
309
+ // Non-waveform fallback (existing behavior)
310
  if (type === 'teacher') {
311
  this.stopTeacherPlayback();
312
  this.teacherAudio = new Audio();
 
341
  }
342
  }
343
 
344
+ // Simple fallback audio creation used when wavesurfer errors
345
+ private fallbackAudioPlay(src: string, type: 'teacher' | 'recorded') {
346
+ if (type === 'teacher') {
347
+ this.stopTeacherPlayback();
348
+ const a = new Audio(src);
349
+ try { if (!src.startsWith('blob:')) a.crossOrigin = 'anonymous'; } catch { }
350
+ a.play().catch(() => { /* ignore */ });
351
+ this.teacherAudio = a;
352
+ } else {
353
+ this.stopRecordedPlayback();
354
+ const a = new Audio(src);
355
+ try { if (!src.startsWith('blob:')) a.crossOrigin = 'anonymous'; } catch { }
356
+ a.play().catch(() => { /* ignore */ });
357
+ this.recordedAudio = a;
358
+ }
359
+ }
360
+
361
  private stopTeacherPlayback(): void {
362
  if (this.teacherAudio) {
363
  try { this.teacherAudio.pause(); this.teacherAudio.onended = null; } catch { }
364
  this.teacherAudio = undefined;
365
  }
366
+ // also pause wavesurfer if present
367
+ if (this.teacherWaveSurfer) {
368
+ try { this.teacherWaveSurfer.pause(); } catch { }
369
+ }
370
  }
371
 
372
  private stopRecordedPlayback(): void {
 
374
  try { this.recordedAudio.pause(); this.recordedAudio.onended = null; } catch { }
375
  this.recordedAudio = undefined;
376
  }
377
+ if (this.studentWaveSurfer) {
378
+ try { this.studentWaveSurfer.pause(); } catch { }
379
+ }
380
  }
381
 
382
  // Loading state for check pronunciation request
 
396
  }
397
  }
398
 
 
399
  let audioBlob: Blob | null = null;
400
 
401
  if (this.recordedBlobs && this.recordedBlobs.length > 0) {
 
402
  const inferredType = this.recordedBlobs[0]?.type || 'audio/webm';
403
  audioBlob = new Blob(this.recordedBlobs, { type: inferredType });
404
  } else if (this.audioBlobUrl) {
 
405
  try {
406
  const resp = await fetch(this.audioBlobUrl);
407
  audioBlob = await resp.blob();
 
437
  form.append('audio', audioBlob, filename);
438
  form.append('word', this.word);
439
 
440
+ // ensure mode normalized and appended
441
+ const modeToSend = (this.pronMode ?? 'phonetics').toString().trim().toLowerCase();
442
+ form.append('mode', modeToSend);
443
+
444
+ // optional: include reference file
445
+ if (this.selectedFile) {
446
+ form.append('reference', this.selectedFile);
447
+ }
448
+
449
+ // Debug: log mode and FormData entries (inspect network panel to confirm)
450
+ console.log('[Pronunciation] sending check_pronunciation', { word: this.word, mode: modeToSend });
451
+ try {
452
+ // enumerate FormData entries for verification
453
+ for (const [k, v] of (form as any).entries()) {
454
+ // for files, log name and size
455
+ if (v instanceof File) {
456
+ console.log(' FormData entry:', k, 'File name=', v.name, 'size=', v.size);
457
+ } else {
458
+ console.log(' FormData entry:', k, v);
459
+ }
460
+ }
461
+ } catch (e) {
462
+ console.warn('Could not enumerate FormData entries for logging', e);
463
+ }
464
+
465
  try {
466
  const res = await this.http.post<any>(`${this.backendURL}/pron/check_pronunciation`, form).toPromise();
467
 
468
+ // Waveform mode response handling
469
+ if (res?.mode === 'waveform' || typeof res?.waveform_similarity !== 'undefined') {
470
+ const sim = Number(res.waveform_similarity ?? 0);
471
+ const matched = Boolean(res.waveform_match);
472
+
473
+ // Put similarity on your speakometer / needle
474
+ this.result = {
475
+ score: Math.round(sim),
476
+ suggestion: matched ? 'Audio matches teacher' : 'Audio does not match teacher',
477
+ feedbackAudioUrl: undefined,
478
+ phonemeScore: sim,
479
+ acousticScore: undefined
480
+ };
481
+
482
+ // Show simple structured feedback
483
+ this.suggestions = [{
484
+ id: 1,
485
+ title: 'Waveform Match',
486
+ message: `Similarity: ${sim}% — Matched: ${matched ? 'Yes' : 'No'}`
487
+ }];
488
+
489
+ // If backend returned teacher audio path, load it into WaveSurfer; otherwise request teacher audio and load
490
+ if (this.pronMode === 'waveform') {
491
+ // load student into wavesurfer
492
+ if (this.recordedBlobUrl && this.studentWaveSurfer) {
493
+ try { this.studentWaveSurfer.load(this.recordedBlobUrl); } catch { /* ignore */ }
494
+ }
495
+
496
+ // prefer backend-provided teacher url if present
497
+ if (res.teacher_audio_url) {
498
+ const teacherUrl = res.teacher_audio_url.startsWith('http') ? res.teacher_audio_url : `${this.backendURL}/${res.teacher_audio_url}`;
499
+ this.teacherAudioCache.set(this.word, teacherUrl);
500
+ if (this.teacherWaveSurfer) {
501
+ try { this.teacherWaveSurfer.load(teacherUrl); } catch { /* ignore */ }
502
+ }
503
+ } else {
504
+ // otherwise try cached or request generation
505
+ const cached = this.teacherAudioCache.get(this.word);
506
+ if (cached && this.teacherWaveSurfer) {
507
+ try { this.teacherWaveSurfer.load(cached); } catch { /* ignore */ }
508
+ } else {
509
+ // call API to get teacher URL (generate_teacher_audio returns url)
510
+ this.api.generateTeacherAudio(this.word, this.selectedFile).subscribe({
511
+ next: ({ audioUrl }) => {
512
+ this.teacherAudioCache.set(this.word, audioUrl);
513
+ if (this.teacherWaveSurfer) {
514
+ try { this.teacherWaveSurfer.load(audioUrl); } catch { /* ignore */ }
515
+ }
516
+ },
517
+ error: () => { /* ignore */ }
518
+ });
519
+ }
520
+ }
521
+ }
522
+
523
+ this.suggestionPage = 0;
524
+ this.isChecking = false;
525
+ this.cdr.detectChanges();
526
+ return;
527
+ }
528
+
529
+ // If frontend is in "select" mode do NOT bind score or suggestion — only populate suggestions returned by backend.
530
+ const isSelectMode = modeToSend === 'select' || this.pronMode === 'select';
531
+ if (isSelectMode) {
532
+ if (Array.isArray(res?.suggestion)) {
533
+ this.suggestions = res.suggestion.map((s: any, idx: number) => {
534
+ const raw = (typeof s === 'string') ? s : (s.message ?? JSON.stringify(s));
535
+ const msg = this.sanitizeFeedbackText(raw);
536
+ const explicitTitle = (s && s.title) ? this.sanitizeFeedbackText(String(s.title)) : '';
537
+ const title = explicitTitle || this.deriveTitleFromMessage(msg, idx);
538
+ return {
539
+ id: s?.id ?? (idx + 1),
540
+ title,
541
+ type: s?.type ?? '',
542
+ message: msg
543
+ };
544
+ });
545
+ } else if (typeof res?.suggestion === 'string' && res.suggestion.trim()) {
546
+ const msg = this.sanitizeFeedbackText(res.suggestion);
547
+ this.suggestions = [{ id: 1, title: this.deriveTitleFromMessage(msg, 0), type: '', message: msg }];
548
+ } else {
549
+ this.suggestions = [];
550
+ }
551
+ // Do not set this.result.score or this.result.suggestion for select mode
552
+ this.suggestionPage = 0;
553
+ this.isChecking = false;
554
+ this.cdr.detectChanges();
555
+ return;
556
+ }
557
+
558
+ // --- existing phoneme/ASR handling (unchanged) ---
559
  const phonemeSimilarity = Number(res?.phoneme_similarity ?? res?.phonemeSimilarity ?? 0);
560
  const phonemePct = Math.round(Math.max(0, Math.min(1, phonemeSimilarity)) * 100);
 
 
561
  const acousticScore = Number(res?.acoustic_score ?? res?.score ?? res?.score_acoustic ?? 0);
562
 
 
563
  this.result = {
564
  score: phonemePct,
565
  suggestion: (typeof res?.suggestion === 'string') ? res.suggestion : '',
 
568
  acousticScore: isNaN(acousticScore) ? undefined : acousticScore
569
  };
570
 
571
+ // existing suggestion building...
572
  if (Array.isArray(res?.suggestion)) {
573
  this.suggestions = res.suggestion.map((s: any, idx: number) => {
574
  const raw = (typeof s === 'string') ? s : (s.message ?? JSON.stringify(s));
 
588
  } else {
589
  this.suggestions = [];
590
  }
 
 
591
  this.suggestionPage = 0;
592
 
593
  } catch (err) {
 
640
  ngOnDestroy(): void {
641
  this.stopTeacherPlayback();
642
  this.stopRecordedPlayback();
643
+ this.destroyWaveSurfers();
644
  if (this.micStream) {
645
  try { this.micStream.getTracks().forEach(t => t.stop()); } catch { }
646
  this.micStream = undefined;
 
662
  requestTeacherAudio() {
663
  const cached = this.teacherAudioCache.get(this.word);
664
  if (cached) {
665
+ // if in waveform mode, load into wavesurfer instead of creating new audio
666
+ if (this.pronMode === 'waveform' && this.teacherWaveSurfer) {
667
+ try { this.teacherWaveSurfer.load(cached); this.teacherWaveSurfer.once('ready', () => this.teacherWaveSurfer.play()); } catch { /* ignore */ }
668
+ return;
669
+ }
670
  const a = new Audio(cached);
671
  a.play().catch(err => console.warn('play failed', err));
672
  return;
 
675
  this.api.generateTeacherAudio(this.word, this.selectedFile).subscribe({
676
  next: ({ audioUrl }) => {
677
  this.teacherAudioCache.set(this.word, audioUrl);
678
+ if (this.pronMode === 'waveform' && this.teacherWaveSurfer) {
679
+ try { this.teacherWaveSurfer.load(audioUrl); this.teacherWaveSurfer.once('ready', () => this.teacherWaveSurfer.play()); } catch { /* ignore */ }
680
+ } else {
681
+ const a = new Audio(audioUrl);
682
+ a.play().catch(err => console.warn('play failed', err));
683
+ }
684
  },
685
  error: err => {
686
  console.error('generateTeacherAudio failed', err);
 
730
  this.audioBlobUrl = URL.createObjectURL(audioBlob);
731
  this.recordedBlobUrl = this.audioBlobUrl;
732
 
733
+ // If in waveform mode, load the student waveform for immediate playback/visualization
734
+ if (this.pronMode === 'waveform' && this.studentWaveSurfer) {
735
+ try { this.studentWaveSurfer.load(this.recordedBlobUrl); } catch { /* ignore */ }
736
+ }
737
+
738
  try { if (this.micStream) this.micStream.getTracks().forEach((t: any) => t.stop()); } catch { }
739
  this.micStream = undefined;
740
 
 
858
  this.phonetics = q.phonetics ?? '';
859
  this.imgsrc = q.imgsrc ?? this.imgsrc;
860
 
861
+ // If in waveform mode and teacher audio cached, load it for the new word
862
+ if (this.pronMode === 'waveform' && this.teacherWaveSurfer) {
863
+ const cached = this.teacherAudioCache.get(this.word);
864
+ if (cached) {
865
+ try { this.teacherWaveSurfer.load(cached); } catch { /* ignore */ }
866
+ }
867
+ }
868
+
869
  this.cdr.detectChanges();
870
  }
871
  }
src/app/pronunciation/pronunciation.service.ts CHANGED
@@ -18,34 +18,25 @@ export class ApiService {
18
 
19
  constructor(private http: HttpClient) { }
20
 
 
21
  generateTeacherAudio(word: string, reference?: File): Observable<{ audioUrl: string }> {
22
  const url = `${this.pronBase}/generate_teacher_audio`;
23
 
 
 
 
24
  if (reference) {
25
- const form = new FormData();
26
- form.append('word', word);
27
  form.append('reference', reference, reference.name);
28
-
29
- // Do NOT set Content-Type header for FormData; browser sets the boundary.
30
- return this.http.post<any>(url, form).pipe(
31
- map(res => ({ audioUrl: `${this.pronBase}/${res.audio_url}` })),
32
- tap(r => console.log('[API] generateTeacherAudio (with ref) ->', r)),
33
- catchError(err => {
34
- console.error('[API] generateTeacherAudio error', err);
35
- return throwError(() => err);
36
- })
37
- );
38
- } else {
39
- const headers = { 'Content-Type': 'application/json' };
40
- return this.http.post<any>(url, { word }, { headers }).pipe(
41
- map(res => ({ audioUrl: `${this.pronBase}/${res.audio_url}` })),
42
- tap(r => console.log('[API] generateTeacherAudio ->', r)),
43
- catchError(err => {
44
- console.error('[API] generateTeacherAudio error', err);
45
- return throwError(() => err);
46
- })
47
- );
48
  }
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
  // New: request teacher audio as raw bytes (no server-side persistent file)
@@ -53,30 +44,19 @@ export class ApiService {
53
  generateTeacherAudioStream(word: string, reference?: File): Observable<Blob> {
54
  const streamUrl = `${this.pronBase}/generate_teacher_audio_stream`;
55
 
56
- if (reference) {
57
- const form = new FormData();
58
- form.append('word', word);
59
- form.append('reference', reference, reference.name);
60
- // post form and expect blob response
61
- return this.http.post(streamUrl, form, { responseType: 'blob' }).pipe(
62
- tap(() => console.log('[API] generateTeacherAudioStream (with ref)')),
63
- map((b: any) => b as Blob),
64
- catchError(err => {
65
- console.error('[API] generateTeacherAudioStream error', err);
66
- return throwError(() => err);
67
- })
68
- );
69
- } else {
70
- const headers = { 'Content-Type': 'application/json' };
71
- return this.http.post(streamUrl, { word }, { headers, responseType: 'blob' as 'json' }).pipe(
72
- tap(() => console.log('[API] generateTeacherAudioStream')),
73
- map((b: any) => b as Blob),
74
- catchError(err => {
75
- console.error('[API] generateTeacherAudioStream error', err);
76
- return throwError(() => err);
77
- })
78
- );
79
- }
80
  }
81
 
82
  // Helper: generate teacher audio then download it as a Blob
@@ -84,19 +64,11 @@ export class ApiService {
84
  generateTeacherAudioBlob(word: string, reference?: File): Observable<{ audioUrl: string; blob: Blob }> {
85
  const postUrl = `${this.pronBase}/generate_teacher_audio`;
86
 
87
- const post$ = reference
88
- ? (() => {
89
- const form = new FormData();
90
- form.append('word', word);
91
- form.append('reference', reference, reference.name);
92
- return this.http.post<any>(postUrl, form);
93
- })()
94
- : (() => {
95
- const headers = { 'Content-Type': 'application/json' };
96
- return this.http.post<any>(postUrl, { word }, { headers });
97
- })();
98
 
99
- return post$.pipe(
100
  switchMap((res: any) => {
101
  // backend returns e.g. "audio/teacher-xxxx.wav" — build full URL under /pron
102
  const audioUrl = `${this.pronBase}/${res.audio_url}`;
 
18
 
19
  constructor(private http: HttpClient) { }
20
 
21
+
22
  generateTeacherAudio(word: string, reference?: File): Observable<{ audioUrl: string }> {
23
  const url = `${this.pronBase}/generate_teacher_audio`;
24
 
25
+ // Always send FormData so Flask can read request.form (and accept optional reference file).
26
+ const form = new FormData();
27
+ form.append('word', word);
28
  if (reference) {
 
 
29
  form.append('reference', reference, reference.name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
+
32
+ return this.http.post<any>(url, form).pipe(
33
+ map(res => ({ audioUrl: `${this.pronBase}/${res.audio_url}` })),
34
+ tap(r => console.log('[API] generateTeacherAudio ->', r)),
35
+ catchError(err => {
36
+ console.error('[API] generateTeacherAudio error', err);
37
+ return throwError(() => err);
38
+ })
39
+ );
40
  }
41
 
42
  // New: request teacher audio as raw bytes (no server-side persistent file)
 
44
  generateTeacherAudioStream(word: string, reference?: File): Observable<Blob> {
45
  const streamUrl = `${this.pronBase}/generate_teacher_audio_stream`;
46
 
47
+ const form = new FormData();
48
+ form.append('word', word);
49
+ if (reference) form.append('reference', reference, reference.name);
50
+
51
+ // Request blob response. Using 'blob' as 'json' keeps TS happy with HttpClient overloads.
52
+ return this.http.post(streamUrl, form, { responseType: 'blob' as 'json' }).pipe(
53
+ tap(() => console.log('[API] generateTeacherAudioStream')),
54
+ map((b: any) => b as Blob),
55
+ catchError(err => {
56
+ console.error('[API] generateTeacherAudioStream error', err);
57
+ return throwError(() => err);
58
+ })
59
+ );
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  // Helper: generate teacher audio then download it as a Blob
 
64
  generateTeacherAudioBlob(word: string, reference?: File): Observable<{ audioUrl: string; blob: Blob }> {
65
  const postUrl = `${this.pronBase}/generate_teacher_audio`;
66
 
67
+ const form = new FormData();
68
+ form.append('word', word);
69
+ if (reference) form.append('reference', reference, reference.name);
 
 
 
 
 
 
 
 
70
 
71
+ return this.http.post<any>(postUrl, form).pipe(
72
  switchMap((res: any) => {
73
  // backend returns e.g. "audio/teacher-xxxx.wav" — build full URL under /pron
74
  const audioUrl = `${this.pronBase}/${res.audio_url}`;
src/assets/audio/apple.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36b263be26c27afa3d24960fc53ce9d6b5363714dda95e22b230ce8eb8c607c9
3
+ size 12408
src/assets/audio/ball.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e04526715ad4a8ec31f85eed6db1c377bf669e00a9bd2d551d19daec5460418f
3
+ size 10392
src/assets/audio/cat.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f28d8a06857382b8e7cd0b51712696ecb17d9fcdfd2a6928afb7b199b081e23e
3
+ size 12912
src/assets/audio/dog.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:988790d50de59b4422a425aa3ee3bc0b38e5e03ec14354d525a57cd313f6a23a
3
+ size 11184
src/assets/audio/egg.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f50aecc9e38956f68572510ee93c00223f4a3f684125b4580635f13963ebaa8f
3
+ size 9936
src/assets/audio/fish.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bb0887b2bc028a2d454e6bd9bf4f3c80a5dc6bccfd9ea1417b3693f3424da24
3
+ size 9456
src/assets/audio/grapes.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa8a75e85c457f948c0b810c16665667c99c095c71224c267f222b5dceabdb2f
3
+ size 13008
src/assets/audio/hat.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2a6e1d6d94fdbfdf3615e352715c652d72bc104fa3ff5ce7eb20b987b1a5468
3
+ size 10800
src/assets/audio/ice_cream.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33cf0de8e2225a9eddfc0083c9a50e1f3ad5cf78a1b937d04694a250ed62ac83
3
+ size 14784
src/assets/audio/jar.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1cbb5f8dcc21cc4c78a3e2c61a6bb1a8f16eb2ed6e656a2b5b5c606a577d68cf
3
+ size 12432
src/assets/audio/kite.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7a1f086fbe0b46c4281d49cfdc4497259322f37c35aebee88e7ecba8bc507d47
3
+ size 12216
src/assets/audio/lion.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97e8dfeb933de825ddccb7ab23616f0544fb1bf13dfbe7174d79dcd5769c3493
3
+ size 14112
src/assets/audio/moon.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:08b408ab5d8d19c22a4ff80da95e954553473ace618347e67a65525a1296743d
3
+ size 11016
src/assets/audio/nest.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1a6d84024ed543fc47619bce66d258cc9d89ed86d0960c6b02b3ff984947d2cf
3
+ size 11376
src/assets/audio/orange.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:28e970be8938cdae9ba382519ad8df8f59006474ae0dccc3a1026537b6916f6a
3
+ size 15408
src/assets/audio/original/ball.m4a DELETED
Binary file (71.2 kB)
 
src/assets/audio/original/cat.m4a DELETED
Binary file (61.9 kB)
 
src/assets/audio/original/dog.m4a DELETED
Binary file (56.5 kB)
 
src/assets/audio/original/egg.m4a DELETED
Binary file (74.9 kB)
 
src/assets/audio/original/fish.m4a DELETED
Binary file (73.2 kB)
 
src/assets/audio/original/grapes.m4a DELETED
Binary file (61.4 kB)
 
src/assets/audio/original/hat.m4a DELETED
Binary file (75.3 kB)
 
src/assets/audio/original/ice_cream.m4a DELETED
Binary file (80.4 kB)
 
src/assets/audio/original/jar.m4a DELETED
Binary file (68.9 kB)
 
src/assets/audio/original/kite.m4a DELETED
Binary file (59.2 kB)
 
src/assets/audio/original/lion.m4a DELETED
Binary file (76 kB)
 
src/assets/audio/original/moon.m4a DELETED
Binary file (81.4 kB)
 
src/assets/audio/original/nest.m4a DELETED
Binary file (56.8 kB)
 
src/assets/audio/original/orange.m4a DELETED
Binary file (87.8 kB)
 
src/assets/audio/original/pig.m4a DELETED
Binary file (81.4 kB)
 
src/assets/audio/original/queen.m4a DELETED
Binary file (74.4 kB)
 
src/assets/audio/original/rabbit.m4a DELETED
Binary file (87.9 kB)
 
src/assets/audio/original/sun.m4a DELETED
Binary file (76.6 kB)
 
src/assets/audio/original/tree.m4a DELETED
Binary file (67.2 kB)
 
src/assets/audio/original/van.m4a DELETED
Binary file (69.7 kB)
 
src/assets/audio/original/watch.m4a DELETED
Binary file (76 kB)
 
src/assets/audio/original/xylophone.m4a DELETED
Binary file (75.6 kB)
 
src/assets/audio/original/yarn.m4a DELETED
Binary file (61.3 kB)
 
src/assets/audio/original/zebra.m4a DELETED
Binary file (56.9 kB)
 
src/assets/audio/pig.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b73eb1b22b1e2615a6336c813a974ddc71e6e9e047453a92b5ab7c1714318007
3
+ size 9504
src/assets/audio/queen.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f822d6e09e355dd10e6ec9a86567da8c8865a4e9207711dc6c6f91fe73a0fae4
3
+ size 11568
src/assets/audio/rabbit.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5fce73c31d372c8e310bc5d3e47bb973b203410a3b599bb6322ecdf2fd096672
3
+ size 10776
src/assets/audio/sun.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4e405e94eb7a7a9ce89b7b69fca109ccceac1b41c01fc165d882a8a9f1e7a8e
3
+ size 11904
src/assets/audio/tree.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9159be7faa03940ac783d589827efbf9666d3daf15e67a5953b7af8db2ac9703
3
+ size 13008
src/assets/audio/umbrella.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec6c59c06372515022b65731f2f22804b469d342e8d477d1b7c13a030ec33650
3
+ size 14472