Oviya commited on
Commit
00ec4c0
·
1 Parent(s): 39d4a08

add pronragupgrade

Browse files
Files changed (45) hide show
  1. src/app/app.module.ts +3 -1
  2. src/app/home/home.component.html +5 -0
  3. src/app/home/home.component.ts +10 -0
  4. src/app/pronunciationragupgrade/pronunciationragupgrade.component.css +920 -0
  5. src/app/pronunciationragupgrade/pronunciationragupgrade.component.html +165 -0
  6. src/app/pronunciationragupgrade/pronunciationragupgrade.component.ts +679 -0
  7. src/assets/pronvideo/animvideo/apple.mp4 +3 -0
  8. src/assets/pronvideo/animvideo/ball.mp4 +3 -0
  9. src/assets/pronvideo/animvideo/cat.mp4 +3 -0
  10. src/assets/pronvideo/animvideo/dog.mp4 +3 -0
  11. src/assets/pronvideo/animvideo/egg.mp4 +3 -0
  12. src/assets/pronvideo/animvideo/fish.mp4 +3 -0
  13. src/assets/pronvideo/animvideo/grapes.mp4 +3 -0
  14. src/assets/pronvideo/animvideo/hat.mp4 +3 -0
  15. src/assets/pronvideo/animvideo/icecream.mp4 +3 -0
  16. src/assets/pronvideo/animvideo/jar.mp4 +3 -0
  17. src/assets/pronvideo/animvideo/kite.mp4 +3 -0
  18. src/assets/pronvideo/animvideo/lion.mp4 +3 -0
  19. src/assets/pronvideo/animvideo/moon.mp4 +3 -0
  20. src/assets/pronvideo/animvideo/nest.mp4 +3 -0
  21. src/assets/pronvideo/animvideo/orange.mp4 +3 -0
  22. src/assets/pronvideo/animvideo/pig.mp4 +3 -0
  23. src/assets/pronvideo/animvideo/queen.mp4 +3 -0
  24. src/assets/pronvideo/animvideo/rabbit.mp4 +3 -0
  25. src/assets/pronvideo/animvideo/sun.mp4 +3 -0
  26. src/assets/pronvideo/animvideo/tree.mp4 +3 -0
  27. src/assets/pronvideo/animvideo/umbrella.mp4 +3 -0
  28. src/assets/pronvideo/animvideo/van.mp4 +3 -0
  29. src/assets/pronvideo/animvideo/watch.mp4 +3 -0
  30. src/assets/pronvideo/animvideo/xylophone.mp4 +3 -0
  31. src/assets/pronvideo/animvideo/yarn.mp4 +3 -0
  32. src/assets/pronvideo/animvideo/zebra.mp4 +3 -0
  33. src/assets/pronvideo/listening.mp4 +3 -0
  34. src/assets/pronvideo/slate.png +3 -0
  35. src/assets/pronvideo/videos/lion-old.mp4 +3 -0
  36. src/assets/pronvideo/videos/lion.mp4 +2 -2
  37. src/assets/pronvideo/videos/rabbit-old.mp4 +3 -0
  38. src/assets/pronvideo/videos/rabbit.mp4 +2 -2
  39. src/assets/pronvideo/videos/van-old.mp4 +3 -0
  40. src/assets/pronvideo/videos/van.mp4 +2 -2
  41. src/assets/pronvideo/videos/xylophone-old.mp4 +3 -0
  42. src/assets/pronvideo/videos/xylophone.mp4 +2 -2
  43. src/assets/pronvideo/videos/yarn-old.mp4 +3 -0
  44. src/assets/pronvideo/videos/yarn.mp4 +2 -2
  45. src/assets/pronvideo/videos/{zebra.mp4 → zebra-old.mp4} +0 -0
src/app/app.module.ts CHANGED
@@ -29,6 +29,7 @@ import { FooterComponent } from './footer/footer.component';
29
  import { ButtonComponent } from './shared/button/button.component';
30
  import { PronunciationVideoComponent } from './pronunciationvideo/pronunciationvideo.component';
31
  import { PronunciationRaggComponent } from './pronunciationragg/pronunciationragg.component';
 
32
 
33
 
34
  @NgModule({
@@ -40,7 +41,8 @@ import { PronunciationRaggComponent } from './pronunciationragg/pronunciationrag
40
  RootRedirectComponent,
41
  PronunciationComponent,
42
  PronunciationVideoComponent,
43
- PronunciationRaggComponent
 
44
  ],
45
  imports: [
46
  BrowserModule,
 
29
  import { ButtonComponent } from './shared/button/button.component';
30
  import { PronunciationVideoComponent } from './pronunciationvideo/pronunciationvideo.component';
31
  import { PronunciationRaggComponent } from './pronunciationragg/pronunciationragg.component';
32
+ import { PronunciationRagUpgradeComponent } from './pronunciationragupgrade/pronunciationragupgrade.component';
33
 
34
 
35
  @NgModule({
 
41
  RootRedirectComponent,
42
  PronunciationComponent,
43
  PronunciationVideoComponent,
44
+ PronunciationRaggComponent,
45
+ PronunciationRagUpgradeComponent
46
  ],
47
  imports: [
48
  BrowserModule,
src/app/home/home.component.html CHANGED
@@ -35,6 +35,11 @@
35
  Pronunciation Trainer Rag
36
  </a>
37
  </li>
 
 
 
 
 
38
  <li><a class="nav-link--disabled" routerLink="/personality-improvement" routerLinkActive="active-link">Personality Improvement</a></li>
39
  <li><a class="nav-link--disabled" routerLink="/body-language-improvement" routerLinkActive="active-link">Body Language Improvement</a></li>
40
  </ul>
 
35
  Pronunciation Trainer Rag
36
  </a>
37
  </li>
38
+ <li>
39
+ <a href="#" (click)="openPronunciationRagUpgrade(); $event.preventDefault()" role="button" aria-pressed="false">
40
+ Pronunciation Trainer Rag Upgrade
41
+ </a>
42
+ </li>
43
  <li><a class="nav-link--disabled" routerLink="/personality-improvement" routerLinkActive="active-link">Personality Improvement</a></li>
44
  <li><a class="nav-link--disabled" routerLink="/body-language-improvement" routerLinkActive="active-link">Body Language Improvement</a></li>
45
  </ul>
src/app/home/home.component.ts CHANGED
@@ -7,6 +7,7 @@ import { MatDialog } from '@angular/material/dialog';
7
  import { PronunciationComponent } from '../pronunciation/pronunciation.component';
8
  import { PronunciationVideoComponent } from '../pronunciationvideo/pronunciationvideo.component';
9
  import { PronunciationRaggComponent } from '../pronunciationragg/pronunciationragg.component';
 
10
 
11
  @Component({
12
  selector: 'app-home',
@@ -190,4 +191,13 @@ export class HomeComponent implements AfterViewInit, OnInit, OnDestroy {
190
  disableClose: true
191
  });
192
  }
 
 
 
 
 
 
 
 
 
193
  }
 
7
  import { PronunciationComponent } from '../pronunciation/pronunciation.component';
8
  import { PronunciationVideoComponent } from '../pronunciationvideo/pronunciationvideo.component';
9
  import { PronunciationRaggComponent } from '../pronunciationragg/pronunciationragg.component';
10
+ import { PronunciationRagUpgradeComponent } from '../pronunciationragupgrade/pronunciationragupgrade.component';
11
 
12
  @Component({
13
  selector: 'app-home',
 
191
  disableClose: true
192
  });
193
  }
194
+
195
+ openPronunciationRagUpgrade(): void {
196
+ const dialogRef = this.dialog.open(PronunciationRagUpgradeComponent, {
197
+ width: '90vw',
198
+ maxWidth: '95vw',
199
+ height: '85vh',
200
+ disableClose: true
201
+ });
202
+ }
203
  }
src/app/pronunciationragupgrade/pronunciationragupgrade.component.css ADDED
@@ -0,0 +1,920 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block;
3
+ /*font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;*/
4
+ font-family: Raleway, Roboto, "Helvetica Neue", sans-serif;
5
+ }
6
+
7
+ /* Page background */
8
+ .pp-page {
9
+ height: 85vh;
10
+ /* background: #e9f7f6;*/
11
+ padding: 28px 24px 18px;
12
+ box-sizing: border-box;
13
+ border: 7px solid #3aaea8;
14
+ border-radius: 1vw;
15
+ }
16
+
17
+ /* Header */
18
+ .pp-header {
19
+ text-align: center;
20
+ margin-bottom: 18px;
21
+ }
22
+
23
+ .pp-header h1 {
24
+ margin: 0;
25
+ font-size: 42px;
26
+ font-weight: 800;
27
+ color: #3aaea8;
28
+ letter-spacing: 0.3px;
29
+ }
30
+
31
+ .pp-sub {
32
+ margin-top: 6px;
33
+ color: #6b7f7e;
34
+ font-size: 15px;
35
+ position: relative;
36
+ }
37
+
38
+ .pp-tooltip {
39
+ margin-left: 8px;
40
+ display: inline-block;
41
+ background: #ffffff;
42
+ border: 1px solid #d8eeee;
43
+ color: #2f6f6b;
44
+ padding: 6px 10px;
45
+ border-radius: 14px;
46
+ font-size: 12px;
47
+ box-shadow: 0 6px 14px rgba(0,0,0,0.06);
48
+ }
49
+
50
+ /* Main 3 columns */
51
+ .pp-main {
52
+ display: flex;
53
+ gap: 1vw;
54
+ align-items: start;
55
+ justify-content: space-around;
56
+ }
57
+
58
+ /* LEFT */
59
+ .pp-left {
60
+ display: flex;
61
+ justify-content: center;
62
+ }
63
+
64
+ .word-card {
65
+ width: 22vw;
66
+ height: 34vw;
67
+ background: #e9f7f6;
68
+ border-radius: 18px;
69
+ padding: 22px 18px 26px;
70
+ text-align: center;
71
+ box-shadow: 0 12px 26px rgba(0, 0, 0, 0.08);
72
+ border: 3px dashed #3aaea8;
73
+ gap: 0.5vw;
74
+ display: flex;
75
+ flex-direction: column;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ }
79
+
80
+ .word-img-wrap {
81
+ width: 20vw;
82
+ height: 20vw;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ }
87
+
88
+ .word-img-wrap img {
89
+ max-width: 100%;
90
+ max-height: 100%;
91
+ object-fit: contain;
92
+ border-radius: 1vw;
93
+ }
94
+
95
+ .word-text {
96
+ font-size: 3vw;
97
+ font-weight: 800;
98
+ color: #1f2b2a;
99
+ }
100
+
101
+ .phonetic-pill {
102
+ color: #3aaea8;
103
+ font-size: 1.5vw;
104
+ font-weight: 600;
105
+ }
106
+
107
+ .audio-img {
108
+ width: 4.8vw;
109
+ cursor: pointer;
110
+ }
111
+ /* CENTER */
112
+ .pp-center {
113
+ display: flex;
114
+ flex-direction: column;
115
+ align-items: center;
116
+ gap: 14px;
117
+ position: relative;
118
+ }
119
+
120
+ .teacher-frame {
121
+ /* width: 240px;
122
+ height: 210px;
123
+ border-radius: 18px;
124
+ padding: 12px;
125
+ background: #e7f4f3;
126
+ border: 2px dashed #b9dedb;
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;*/
130
+ width: 22vw;
131
+ height: 33vw;
132
+ background: #e9f7f6;
133
+ border-radius: 18px;
134
+ padding: 22px 18px 26px;
135
+ text-align: center;
136
+ box-shadow: 0 12px 26px rgba(0, 0, 0, 0.08);
137
+ border: 3px dashed #3aaea8;
138
+ }
139
+
140
+ .teacher-frame img {
141
+ width: 100%;
142
+ height: 100%;
143
+ object-fit: cover;
144
+ border-radius: 12px;
145
+ background: #ffffff;
146
+ }
147
+
148
+ /* Listen button */
149
+ .listen-btn {
150
+ border: none;
151
+ background: #49b6ae;
152
+ color: #ffffff;
153
+ padding: 12px 18px;
154
+ border-radius: 12px;
155
+ font-weight: 700;
156
+ font-size: 16px;
157
+ display: inline-flex;
158
+ align-items: center;
159
+ gap: 10px;
160
+ cursor: pointer;
161
+ box-shadow: 0 8px 18px rgba(0,0,0,0.08);
162
+ }
163
+
164
+ .listen-btn:active {
165
+ transform: scale(0.99);
166
+ }
167
+
168
+ .listen-ico {
169
+ font-size: 18px;
170
+ }
171
+
172
+ /* Record circle */
173
+ .rec-circle {
174
+ width: 92px;
175
+ height: 92px;
176
+ border-radius: 50%;
177
+ border: none;
178
+ background: #f07b48;
179
+ color: #ffffff;
180
+ cursor: pointer;
181
+ /* box-shadow: 0 12px 22px rgba(0,0,0,0.12);*/
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ /* transition: transform 0.08s ease, filter 0.2s ease;*/
186
+ box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
187
+ transition: all 0.3s ease;
188
+ }
189
+
190
+ .rec-circle:active {
191
+ transform: scale(0.98);
192
+ }
193
+
194
+ .rec-circle.recording {
195
+ filter: brightness(0.95);
196
+ animation: recPulse 1s infinite;
197
+ }
198
+
199
+ @keyframes recPulse {
200
+ 0% {
201
+ box-shadow: 0 0 0 0 rgba(240,123,72,1);
202
+ }
203
+
204
+ 70% {
205
+ box-shadow: 0 0 0 18px rgba(240,123,72,0);
206
+ }
207
+
208
+ 100% {
209
+ box-shadow: 0 0 0 0 rgba(240,123,72,0);
210
+ }
211
+ }
212
+
213
+ .rec-inner {
214
+ text-align: center;
215
+ line-height: 1.1;
216
+ }
217
+
218
+ .mic {
219
+ font-size: 22px;
220
+ margin-bottom: 4px;
221
+ }
222
+
223
+ .rec-text {
224
+ font-size: 12px;
225
+ font-weight: 800;
226
+ letter-spacing: 0.6px;
227
+ }
228
+
229
+ /* RIGHT */
230
+ .pp-right {
231
+ display: flex;
232
+ justify-content: flex-start;
233
+ align-items: center;
234
+ flex-direction: column;
235
+ gap: 18vw;
236
+ }
237
+
238
+ /* little connector dots */
239
+ .connector {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 2vw;
243
+ }
244
+
245
+ .connector span {
246
+ width: 34px;
247
+ height: 10px;
248
+ border-radius: 999px;
249
+ background: #98d8d4;
250
+ opacity: 0.6;
251
+ }
252
+
253
+ /* feedback card */
254
+ .feedback-card {
255
+ width: 15vw;
256
+ background: #9edfd9;
257
+ border-radius: 16px;
258
+ padding: 22px 22px 24px;
259
+ box-shadow: 0 12px 22px rgba(0,0,0,0.08);
260
+ height: 15vw;
261
+ }
262
+
263
+ .feedback-title {
264
+ font-size: 18px;
265
+ font-weight: 800;
266
+ color: #205f5a;
267
+ letter-spacing: 0.6px;
268
+ margin-bottom: 8px;
269
+ }
270
+
271
+ .feedback-body {
272
+ background: transparent;
273
+ }
274
+
275
+ .feedback-muted {
276
+ color: #2c6d68;
277
+ opacity: 0.8;
278
+ font-style: italic;
279
+ font-size: 14px;
280
+ margin-top: 8px;
281
+ }
282
+
283
+ /* Result UI inside feedback */
284
+ .feedback-result {
285
+ margin-top: 8px;
286
+ }
287
+
288
+ .score-row {
289
+ display: flex;
290
+ align-items: baseline;
291
+ gap: 10px;
292
+ }
293
+
294
+ .score-label {
295
+ font-size: 12px;
296
+ color: #1d514d;
297
+ font-weight: 700;
298
+ }
299
+
300
+ .score-value {
301
+ font-size: 28px;
302
+ font-weight: 900;
303
+ color: #103c39;
304
+ }
305
+
306
+ /* Speakometer */
307
+ .meter {
308
+ margin-top: 10px;
309
+ }
310
+
311
+ .meter-track {
312
+ height: 10px;
313
+ background: rgba(255,255,255,0.45);
314
+ border-radius: 999px;
315
+ overflow: hidden;
316
+ }
317
+
318
+ .meter-fill {
319
+ height: 100%;
320
+ width: 0%;
321
+ background: #2b8f88;
322
+ transition: width 0.5s ease;
323
+ }
324
+
325
+ /* Stars */
326
+ .stars {
327
+ margin-top: 10px;
328
+ font-size: 22px;
329
+ display: flex;
330
+ gap: 4px;
331
+ }
332
+
333
+ .stars span {
334
+ color: rgba(255,255,255,0.55);
335
+ }
336
+
337
+ .stars span.active {
338
+ color: #ffcc4d;
339
+ animation: starPop 0.3s ease;
340
+ }
341
+
342
+ @keyframes starPop {
343
+ 0% {
344
+ transform: scale(0.85);
345
+ }
346
+
347
+ 60% {
348
+ transform: scale(1.12);
349
+ }
350
+
351
+ 100% {
352
+ transform: scale(1);
353
+ }
354
+ }
355
+
356
+ /* 3 lines feedback */
357
+ .feedback-lines {
358
+ margin: 10px 0 0 18px;
359
+ color: #114744;
360
+ font-size: 13.5px;
361
+ font-weight: 600;
362
+ }
363
+
364
+ /* Bottom area */
365
+ .pp-bottom {
366
+ max-width: 1200px;
367
+ margin: 22px auto 0;
368
+ display: flex;
369
+ flex-direction: column;
370
+ align-items: center;
371
+ gap: 14px;
372
+ }
373
+
374
+ /* Prev/Next line */
375
+ /* Prev/Next line */
376
+ .nav-row {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 3vw;
380
+ }
381
+
382
+ /* Increased arrow size and perfectly center it inside the circular button */
383
+ .nav-btn {
384
+ width: 7vw;
385
+ height: 7vw;
386
+ border-radius: 50%;
387
+ border: none;
388
+ background: #dcefee;
389
+ color: #1b5551;
390
+ font-size: 7vw; /* larger arrow */
391
+ display: flex; /* center text horizontally & vertically */
392
+ align-items: center;
393
+ justify-content: center;
394
+ text-align: center;
395
+ line-height: 1; /* avoid font baseline shifts */
396
+ padding: 0; /* ensure perfect centering */
397
+ cursor: pointer;
398
+ font-weight: 700;
399
+ box-shadow: 0 8px 18px rgba(0,0,0,0.06);
400
+ transition: transform 0.08s ease;
401
+ }
402
+
403
+ .nav-btn:active {
404
+ transform: scale(0.98);
405
+ }
406
+
407
+ .nav-btn:disabled {
408
+ opacity: 0.5;
409
+ cursor: not-allowed;
410
+ }
411
+
412
+ .nav-center {
413
+ display: inline-flex;
414
+ align-items: baseline;
415
+ gap: 8px;
416
+ }
417
+
418
+ .nav-letter {
419
+ font-size: 5vw;
420
+ font-weight: 900;
421
+ color: #3aaea8;
422
+ }
423
+
424
+ .nav-count {
425
+ font-size: 14px;
426
+ color: #667b79;
427
+ font-weight: 600;
428
+ }
429
+
430
+ /* Responsive override so buttons don't overflow on smaller screens */
431
+ @media (max-width: 980px) {
432
+ .nav-btn {
433
+ width: 56px;
434
+ height: 56px;
435
+ font-size: 28px;
436
+ }
437
+ }
438
+
439
+ /* Alphabet pills */
440
+ .alpha-row {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ justify-content: center;
444
+ gap: 8px;
445
+ max-width: 900px;
446
+ }
447
+
448
+ .alpha-pill {
449
+ width: 34px;
450
+ height: 34px;
451
+ border-radius: 50%;
452
+ border: none;
453
+ background: #dfeeee;
454
+ color: #3a5a58;
455
+ font-weight: 700;
456
+ cursor: pointer;
457
+ font-size: 13px;
458
+ }
459
+
460
+ .alpha-pill.active {
461
+ background: #49b6ae;
462
+ color: #ffffff;
463
+ box-shadow: 0 6px 14px rgba(0,0,0,0.08);
464
+ }
465
+
466
+ /* Responsive */
467
+ @media (max-width: 1100px) {
468
+ .pp-main {
469
+ grid-template-columns: 260px 320px 1fr;
470
+ }
471
+ }
472
+
473
+ @media (max-width: 980px) {
474
+ .pp-main {
475
+ grid-template-columns: 1fr;
476
+ }
477
+
478
+ .pp-left, .pp-center, .pp-right {
479
+ justify-content: center;
480
+ }
481
+
482
+ .connector {
483
+ display: none;
484
+ }
485
+
486
+ .feedback-card {
487
+ width: 100%;
488
+ max-width: 520px;
489
+ }
490
+ }
491
+
492
+ .gauge-wrapper {
493
+ position: relative;
494
+ width: 20vw;
495
+ height: 10vw;
496
+ }
497
+
498
+ .gauge {
499
+ position: absolute;
500
+ left: 50%;
501
+ top: 0;
502
+ transform: translateX(-50%);
503
+ width: 100%;
504
+ height: 100%;
505
+ border-radius: 260px 260px 0 0;
506
+ overflow: hidden;
507
+ background: #f3f3f3;
508
+ box-shadow: 0 4px 10px rgba(0,0,0,0.25) inset;
509
+ }
510
+
511
+ .gauge-arc {
512
+ position: absolute;
513
+ inset: 0;
514
+ border-radius: 50%;
515
+ background: conic-gradient( from 270deg, #e53935 0deg 45deg, #fb8c00 45deg 90deg, #fbc02d 90deg 135deg, #43a047 135deg 180deg, transparent 180deg 360deg );
516
+ height: 20vw;
517
+ }
518
+
519
+ .needle {
520
+ position: absolute;
521
+ bottom: 0vw;
522
+ left: 50%;
523
+ width: 0.7vw;
524
+ height: 8vw;
525
+ background: #333;
526
+ transform: translateX(-50%) rotate(var(--angle, -90deg));
527
+ transform-origin: 50% 100%;
528
+ transition: transform 700ms cubic-bezier(.2,.9,.2,1);
529
+ border-radius: 10px;
530
+ box-shadow: 0 2px 6px rgba(0,0,0,0.5);
531
+ }
532
+
533
+ .mic-badge {
534
+ position: absolute;
535
+ bottom: -0.3vw;
536
+ left: 50%;
537
+ transform: translate(-50%, 35%);
538
+ width: 3vw;
539
+ height: 3vw;
540
+ border-radius: 50%;
541
+ background: #000;
542
+ box-shadow: 0 8px 18px rgba(0,0,0,0.4);
543
+ display: flex;
544
+ align-items: center;
545
+ justify-content: center;
546
+ }
547
+
548
+ .score-span {
549
+ color: white;
550
+ font-size: 1vw;
551
+ font-weight: bold;
552
+ }
553
+
554
+ .notepad {
555
+ display: flex;
556
+ align-items: center;
557
+ }
558
+
559
+ .user-guide-close-icon {
560
+ position: fixed;
561
+ top: 3vw;
562
+ right: 4vw;
563
+ background: #009688;
564
+ border: none;
565
+ width: 44px;
566
+ height: 44px;
567
+ border-radius: 50%;
568
+ display: flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ font-size: 2vw;
572
+ color: black;
573
+ cursor: pointer;
574
+ z-index: 2010;
575
+ box-shadow: 0 2px 8px rgba(93, 145, 195, 0.18);
576
+ transition: background 0.2s, color 0.2s;
577
+ }
578
+ /* Teacher media container — ensures image and video occupy exactly the same box */
579
+ .teacher-media {
580
+ width: 20vw; /* same as you used inline before */
581
+ max-width: 260px; /* optional limit for very wide screens */
582
+ min-width: 140px; /* optional floor */
583
+ aspect-ratio: 3 / 4; /* change to 16 / 9 if your videos are widescreen */
584
+ position: relative;
585
+ overflow: hidden;
586
+ display: block;
587
+ }
588
+
589
+ /* Shared styles for image and video so sizes match exactly */
590
+ .teacher-media__img,
591
+ .teacher-media__video {
592
+ width: 100%;
593
+ height: 100%;
594
+ display: block;
595
+ border-radius: 1vw;
596
+ border: 2px solid #ccc; /* keep previously visible border */
597
+ object-fit: cover; /* makes video/image fill box similarly */
598
+ }
599
+
600
+ /* A neutral background for video while it loads */
601
+ .teacher-media__video {
602
+ background-color: #000;
603
+ }
604
+ /* Make play/pause image match record button size and add shadow */
605
+ .listen-img {
606
+ width: 4.8vw; /* match .rec-circle size */
607
+ height: 4.8vw;
608
+ border-radius: 50%;
609
+ display: inline-block;
610
+ object-fit: contain; /* keep icon aspect */
611
+ cursor: pointer;
612
+ user-select: none;
613
+ margin-right: 1vw;
614
+ box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
615
+ transition: all 0.3s ease;
616
+ /*box-shadow: 0 12px 22px rgba(0,0,0,0.12);
617
+ transition: transform 0.08s ease, filter 0.15s ease, box-shadow 0.15s ease;
618
+
619
+ box-sizing: border-box;*/
620
+ border: none;
621
+ }
622
+
623
+ /* pressed / active */
624
+ .listen-img:active {
625
+ transform: scale(0.98);
626
+ }
627
+
628
+ /* playing state — subtle visual change */
629
+ .listen-img.playing,
630
+ .listen-img[aria-pressed="true"] {
631
+ filter: brightness(0.95);
632
+ box-shadow: 0 18px 30px rgba(0,0,0,0.16);
633
+ }
634
+
635
+ /* keyboard focus for accessibility */
636
+ .listen-img:focus {
637
+ outline: 3px solid rgba(58,174,168,0.18);
638
+ outline-offset: 3px;
639
+ }
640
+
641
+ /* Small-screen fallback (keep sizes proportional) */
642
+ @media (max-width: 980px) {
643
+ .listen-img {
644
+ width: 72px;
645
+ height: 72px;
646
+ padding: 14px;
647
+ }
648
+ }
649
+ /* add oscillation keyframes and state */
650
+ @keyframes needleOscillate {
651
+ 0% {
652
+ transform: translateX(-50%) rotate(-70deg);
653
+ }
654
+
655
+ 25% {
656
+ transform: translateX(-50%) rotate(-20deg);
657
+ }
658
+
659
+ 50% {
660
+ transform: translateX(-50%) rotate(60deg);
661
+ }
662
+
663
+ 75% {
664
+ transform: translateX(-50%) rotate(-10deg);
665
+ }
666
+
667
+ 100% {
668
+ transform: translateX(-50%) rotate(-70deg);
669
+ }
670
+ }
671
+
672
+ /* existing needle default uses CSS variable for final angle */
673
+ .needle {
674
+ position: absolute;
675
+ bottom: 0vw;
676
+ left: 50%;
677
+ width: 0.7vw;
678
+ height: 8vw;
679
+ background: #333;
680
+ transform: translateX(-50%) rotate(var(--angle, -90deg));
681
+ transform-origin: 50% 100%;
682
+ transition: transform 700ms cubic-bezier(.2,.9,.2,1);
683
+ border-radius: 10px;
684
+ box-shadow: 0 2px 6px rgba(0,0,0,0.5);
685
+ }
686
+
687
+ /* while recording / waiting for score, run oscillation animation */
688
+ .needle.oscillate {
689
+ /* override transform with animation while oscillating */
690
+ animation: needleOscillate 1.2s ease-in-out infinite;
691
+ /* disable the smooth transition while animation runs to prevent conflicts */
692
+ transition: none;
693
+ }
694
+
695
+ /* when oscillation ends (isRecording/isScoring false), the animation class is removed
696
+ and the element will smoothly transition to the value provided by --angle */
697
+ .container {
698
+ display: flex;
699
+ justify-content: center;
700
+ align-items: center;
701
+ gap: 40px; /* Increase the space between elements */
702
+ }
703
+
704
+ .arrow {
705
+ font-size: 4rem; /* Increased font size for bigger buttons */
706
+ background-color: #e0f7fa;
707
+ border: none;
708
+ width: 92px; /* Set width */
709
+ height: 92px; /* Set height */
710
+ border-radius: 50%; /* Make the button circular */
711
+ display: flex;
712
+ justify-content: center;
713
+ align-items: center;
714
+ cursor: pointer;
715
+ box-shadow: 4px 4px 15px rgba(0, 0, 0, 0.2); /* Box shadow for the button */
716
+ color: #00796b; /* Set color of the arrows */
717
+ transition: background-color 0.3s, color 0.3s; /* Smooth transition for background and text color */
718
+ }
719
+
720
+ /* Disabled state styles */
721
+ .arrow:disabled {
722
+ background-color: #cfd8dc; /* Light gray background when disabled */
723
+ color: #90a4ae; /* Gray color for the arrow */
724
+ cursor: not-allowed; /* Change cursor to indicate the button is disabled */
725
+ box-shadow: none; /* Remove box shadow for disabled button */
726
+ }
727
+
728
+ .center-text {
729
+ font-size: 5rem; /* Increased font size for the 'Y' text */
730
+ font-weight: bold;
731
+ color: #00796b;
732
+ width: 4vw;
733
+ text-align: center;
734
+ }
735
+
736
+
737
+ /* Styling the container */
738
+ .image-container {
739
+ display: flex;
740
+ justify-content: center;
741
+ align-items: center;
742
+ height: 100vh;
743
+ }
744
+
745
+ /* Styling the round image with shadow */
746
+ .round-image {
747
+ width: 4.8vw; /* Adjust the size as needed */
748
+ height: 4.8vw;
749
+ border-radius: 50%; /* Makes the image round */
750
+ box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
751
+ transition: all 0.3s ease; /* Smooth transition for animation */
752
+ cursor: pointer;
753
+ }
754
+
755
+ /* Scaling effect on click */
756
+ .round-image:active {
757
+ transform: scale(1.1); /* Scale up by 10% when clicked */
758
+ }
759
+
760
+ /* Subtle zoom in/out */
761
+ .apple-anim {
762
+ transform-origin: 50% 50%;
763
+ animation: appleZoom 2.8s ease-in-out infinite alternate;
764
+ will-change: transform;
765
+ }
766
+
767
+ @keyframes appleZoom {
768
+ from {
769
+ transform: scale(0.8);
770
+ }
771
+
772
+ to {
773
+ transform: scale(1.06);
774
+ }
775
+ /* slight zoom */
776
+ }
777
+
778
+ /* Respect reduced motion preference */
779
+ @media (prefers-reduced-motion: reduce) {
780
+ .apple-anim {
781
+ animation: none;
782
+ transform: none;
783
+ }
784
+ }
785
+
786
+
787
+
788
+
789
+
790
+ /* Progress / record button (circular) */
791
+ .progress-btn {
792
+ width: 4.8vw;
793
+ height: 4.8vw;
794
+ border-radius: 50%;
795
+ display: inline-flex;
796
+ align-items: center;
797
+ justify-content: center;
798
+ padding: 0;
799
+ border: none;
800
+ background: #f07b48;
801
+ position: relative;
802
+ cursor: pointer;
803
+ box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
804
+ transition: transform .12s ease, box-shadow .12s ease;
805
+ }
806
+
807
+ .progress-btn:active {
808
+ transform: scale(.98);
809
+ }
810
+
811
+ .progress-btn.recording {
812
+ /* box-shadow: 0 10px 26px rgba(63,81,181,0.18);*/
813
+ filter: brightness(0.95);
814
+ animation: recPulse 1s infinite;
815
+ }
816
+
817
+ /* SVG ring occupies full button */
818
+ .progress-ring {
819
+ width: 100%;
820
+ height: 100%;
821
+ transform: rotate(-90deg); /* rotate so progress starts at top */
822
+ display: block;
823
+ }
824
+
825
+ /* style the static background ring and active bar */
826
+ .progress-ring__background {
827
+ stroke: #eee;
828
+ opacity: 1;
829
+ }
830
+
831
+ .progress-ring__bar {
832
+ stroke: #3aaea8;
833
+ transition: stroke-dashoffset 0.12s linear;
834
+ }
835
+
836
+ /* central label stacked over the SVG */
837
+ .label {
838
+ position: absolute;
839
+ left: 0;
840
+ right: 0;
841
+ top: 0;
842
+ bottom: 0;
843
+ display: flex;
844
+ align-items: center;
845
+ justify-content: center;
846
+ pointer-events: none;
847
+ font-weight: 700;
848
+ color: #222;
849
+ font-size: 2.5rem;
850
+ }
851
+
852
+ /* seconds shown while counting down */
853
+ .seconds {
854
+ color: #fff;
855
+ /* background: rgba(0,0,0,0.5);*/
856
+ padding: 4px 8px;
857
+ border-radius: 6px;
858
+ }
859
+
860
+ /* dot shown when recording/waiting for result */
861
+ .finished-dot {
862
+ color: #d32f2f;
863
+ font-size: 1.4rem;
864
+ }
865
+
866
+
867
+ /*.fb-board {
868
+ position: absolute;
869
+ top: 15.5vw;
870
+ right: 3.8vw;
871
+ }*/
872
+
873
+
874
+ .fb-board {
875
+ position: absolute;
876
+ top: 15.3vw;
877
+ right: 3.6vw;
878
+ }
879
+
880
+ .fb-board img {
881
+ width: 100%;
882
+ display: block;
883
+ }
884
+
885
+ .center-text1 {
886
+ position: absolute;
887
+ top: 50%;
888
+ left: 50%;
889
+ transform: translate(-50%, -50%);
890
+ color: white;
891
+ font-size: 20px;
892
+ font-weight: bold;
893
+ text-align: center;
894
+ width: 14vw;
895
+ }
896
+
897
+
898
+
899
+ /* Responsive video container */
900
+ .video-frame {
901
+ width: 100%;
902
+ aspect-ratio: 16 / 9; /* default; adjust per need */
903
+ border-radius: 1vw;
904
+ overflow: hidden;
905
+ background: #000; /* avoids blank edges before first frame */
906
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18), 0 12px 24px rgba(0, 0, 0, 0.16), 0 24px 48px rgba(0, 0, 0, 0.12);
907
+ }
908
+
909
+ /* For square videos (like the apple video) */
910
+ .video-frame.square {
911
+ aspect-ratio: 1 / 1;
912
+ }
913
+
914
+ /* Make the video fill the frame */
915
+ .video-frame > video {
916
+ width: 100%;
917
+ height: 100%;
918
+ display: block;
919
+ object-fit: cover; /* use 'contain' if you do not want cropping */
920
+ }
src/app/pronunciationragupgrade/pronunciationragupgrade.component.html ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="pp-page">
2
+
3
+ <!-- Header -->
4
+ <div class="pp-header">
5
+ <h1>Pronunciation Practice</h1>
6
+ </div>
7
+
8
+ <!-- Main area -->
9
+ <div class="pp-main">
10
+
11
+ <!-- LEFT: Word card -->
12
+ <div class="pp-left">
13
+ <div class="word-card">
14
+ <div class="word-img-wrap">
15
+ <div class="video-frame square">
16
+ <video [src]="current.imgSrc"
17
+ autoplay
18
+ loop
19
+ muted
20
+ playsinline>
21
+ Your browser does not support the video tag.
22
+ </video>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="word-text">{{ current.word }}</div>
27
+
28
+ <div class="phonetic-pill">
29
+ {{ current.phonetics }}
30
+ </div>
31
+
32
+
33
+ <div class="image-container">
34
+ <img src="assets/pronvideo/audio.png" alt="Round Image" class="round-image" (click)="playWordAudio()">
35
+ </div>
36
+
37
+ </div>
38
+ </div>
39
+
40
+ <!-- CENTER: Teacher area -->
41
+ <div class="pp-center">
42
+ <div class="word-card" style="width:30vw!important">
43
+ <div style="width:28vw;height:25vw">
44
+
45
+
46
+ <video *ngIf="!showVideo"
47
+ src="assets/pronvideo/listening.mp4"
48
+ style="border-radius:1vw; object-fit:cover;"
49
+ autoplay
50
+ loop
51
+ muted
52
+ (ended)="onVideoEnded()"
53
+ height="469"
54
+ width="521">
55
+ Your browser does not support the video tag.
56
+ </video>
57
+
58
+ <!-- single video element used for both teacher and feedback videos -->
59
+ <video *ngIf="showVideo"
60
+ #videoEl
61
+ [src]="videoSrc"
62
+ style="border-radius:1vw; object-fit:cover;"
63
+ (play)="onVideoPlay()"
64
+ (pause)="onVideoPause()"
65
+ autoplay
66
+ (ended)="onVideoEnded()"
67
+ height="469"
68
+ width="521">
69
+ Your browser does not support the video tag.
70
+ </video>
71
+ </div>
72
+
73
+ <!-- Toggle listen button: image acts as the button and switches between play/pause -->
74
+ <!-- Replace the button with a single image that acts as a toggle control -->
75
+
76
+ <div style="display:flex;margin-top:1.8vw;gap:2vw;">
77
+ <img class="listen-img"
78
+ [src]="isPlayingVideo ? pauseIconDataUrl : playIconDataUrl"
79
+ [attr.alt]="isPlayingVideo ? 'Pause pronunciation' : 'Play pronunciation'"
80
+ [attr.aria-label]="isPlayingVideo ? 'Pause pronunciation' : 'Play pronunciation'"
81
+ [attr.aria-pressed]="isPlayingVideo"
82
+ role="button"
83
+ tabindex="0"
84
+ (click)="toggleVideoPlay()"
85
+ (keydown.enter)="toggleVideoPlay()"
86
+ (keydown.space)="toggleVideoPlay(); $event.preventDefault()" />
87
+
88
+
89
+ <button class="progress-btn"
90
+ [class.recording]="isRecording"
91
+ [attr.aria-pressed]="isRecording"
92
+ (click)="toggleRecording()"
93
+ type="button">
94
+ <svg class="progress-ring" width="85" height="85" viewBox="0 0 90 90" aria-hidden="true">
95
+ <circle class="progress-ring__background"
96
+ stroke="#3aaea8"
97
+ stroke-width="6"
98
+ fill="transparent"
99
+ r="38"
100
+ cx="45"
101
+ cy="45" />
102
+ <circle class="progress-ring__bar"
103
+ stroke="#3aaea8"
104
+ stroke-linecap="round"
105
+ stroke-width="6"
106
+ fill="transparent"
107
+ r="38"
108
+ cx="45"
109
+ cy="45"
110
+ [attr.stroke-dasharray]="circumference"
111
+ [attr.stroke-dashoffset]="strokeDashoffset" />
112
+ </svg>
113
+
114
+ <div class="label" aria-hidden="true">
115
+ <!-- show a live mic while recording -->
116
+ <span class="action-text" *ngIf="isRecording">🎙️</span>
117
+
118
+ <!-- default mic before countdown starts -->
119
+ <span class="action-text" *ngIf="!isCountingDown && !isRecording">🎤</span>
120
+
121
+ <!-- countdown number -->
122
+ <span class="seconds" *ngIf="isCountingDown">{{ timeLeft | number:'1.0-0' }}</span>
123
+ </div>
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- RIGHT: Feedback panel -->
130
+ <div class="pp-right">
131
+
132
+ <!-- needle binding changed to use isOscillating -->
133
+ <div class="gauge-wrapper">
134
+ <div class="gauge">
135
+ <div class="gauge-arc"></div>
136
+ <div class="needle" [class.oscillate]="isOscillating" [style.--angle]="needleAngle + 'deg'"></div>
137
+ </div>
138
+
139
+ <div class="mic-badge">
140
+ <span class="score-span">{{score}}%</span>
141
+ </div>
142
+ </div>
143
+
144
+
145
+
146
+ <div class="fb-board">
147
+ <img src="assets/pronvideo/slate.png" alt="Sample Image">
148
+ <div class="center-text1">
149
+ {{ shortfeedback ? shortfeedback : 'Speak to get feedback' }}
150
+ </div>
151
+ </div>
152
+
153
+
154
+ <div class="container">
155
+ <button class="arrow left" (click)="prev()" [disabled]="index === 0">&#8249;</button>
156
+ <span class="center-text">{{ current.letter }}</span>
157
+ <button class="arrow right" (click)="next()" [disabled]="index === items.length - 1">&#8250;</button>
158
+ </div>
159
+
160
+ </div>
161
+
162
+ </div>
163
+
164
+ </div>
165
+ <button aria-label="Close" class="user-guide-close-icon" (click)="closePopup()">×</button>
src/app/pronunciationragupgrade/pronunciationragupgrade.component.ts ADDED
@@ -0,0 +1,679 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ChangeDetectorRef
3
+ } from '@angular/core';
4
+ import { HttpClient } from '@angular/common/http';
5
+ import { finalize, takeUntil } from 'rxjs/operators';
6
+ import { Subject } from 'rxjs';
7
+ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
8
+
9
+ interface PracticeItem {
10
+ letter: string;
11
+ word: string;
12
+ phonetics: string;
13
+ imgSrc: string;
14
+ audioSrc: string;
15
+ }
16
+
17
+ @Component({
18
+ selector: 'app-pronunciationragupgrade',
19
+ templateUrl: './pronunciationragupgrade.component.html',
20
+ styleUrls: ['./pronunciationragupgrade.component.css']
21
+ })
22
+ export class PronunciationRagUpgradeComponent implements OnInit, OnDestroy {
23
+
24
+ @ViewChild('videoEl') videoElRef?: ElementRef<HTMLVideoElement>;
25
+
26
+ // ---------------- CONFIG ----------------
27
+ private API_BASE = location.hostname.endsWith('hf.space')
28
+ ? 'https://pykara-py-learn-backend.hf.space'
29
+ : 'http://localhost:5000';
30
+
31
+ private readonly SCORE_ENDPOINT = `${this.API_BASE}/pronragupgrade/score`;
32
+
33
+ private readonly preferredMimeTypes = [
34
+ 'audio/webm;codecs=opus',
35
+ 'audio/webm',
36
+ 'audio/ogg;codecs=opus',
37
+ 'audio/ogg'
38
+ ];
39
+
40
+ // ---------------- UI STATE ----------------
41
+ showVideo = false;
42
+ videoSrc = '';
43
+ isPlayingVideo = false;
44
+ playIconDataUrl = 'assets/pronvideo/play.png';
45
+ pauseIconDataUrl = 'assets/pronvideo/pause.png';
46
+
47
+ // ---------------- DATA ----------------
48
+ items: PracticeItem[] = [
49
+ { letter: 'A', word: 'Apple', phonetics: '/ˈæpəl/', imgSrc: 'assets/pronvideo/animvideo/apple.mp4', audioSrc: 'assets/pronvideo/audio/apple.mp3' },
50
+ { letter: 'B', word: 'Ball', phonetics: '/bɔːl/', imgSrc: 'assets/pronvideo/animvideo/ball.mp4', audioSrc: 'assets/pronvideo/audio/ball.mp3' },
51
+ { letter: 'C', word: 'Cat', phonetics: '/kæt/', imgSrc: 'assets/pronvideo/animvideo/cat.mp4', audioSrc: 'assets/pronvideo/audio/cat.mp3' },
52
+ { letter: 'D', word: 'Dog', phonetics: '/dɒɡ/', imgSrc: 'assets/pronvideo/animvideo/dog.mp4', audioSrc: 'assets/pronvideo/audio/dog.mp3' },
53
+ { letter: 'E', word: 'Egg', phonetics: '/eɡ/', imgSrc: 'assets/pronvideo/animvideo/egg.mp4', audioSrc: 'assets/pronvideo/audio/egg.mp3' },
54
+ { letter: 'F', word: 'Fish', phonetics: '/fɪʃ/', imgSrc: 'assets/pronvideo/animvideo/fish.mp4', audioSrc: 'assets/pronvideo/audio/fish.mp3' },
55
+ { letter: 'G', word: 'Grapes', phonetics: '/ɡreɪps/', imgSrc: 'assets/pronvideo/animvideo/grapes.mp4', audioSrc: 'assets/pronvideo/audio/grapes.mp3' },
56
+ { letter: 'H', word: 'Hat', phonetics: '/hæt/', imgSrc: 'assets/pronvideo/animvideo/hat.mp4', audioSrc: 'assets/pronvideo/audio/hat.mp3' },
57
+ { letter: 'I', word: 'Ice cream', phonetics: '/ˈaɪs ˌkriːm/', imgSrc: 'assets/pronvideo/animvideo/icecream.mp4', audioSrc: 'assets/pronvideo/audio/icecream.mp3' },
58
+ { letter: 'J', word: 'Jar', phonetics: '/dʒɑːr/', imgSrc: 'assets/pronvideo/animvideo/jar.mp4', audioSrc: 'assets/pronvideo/audio/jar.mp3' },
59
+ { letter: 'K', word: 'Kite', phonetics: '/kaɪt/', imgSrc: 'assets/pronvideo/animvideo/kite.mp4', audioSrc: 'assets/pronvideo/audio/kite.mp3' },
60
+ { letter: 'L', word: 'Lion', phonetics: '/ˈlaɪən/', imgSrc: 'assets/pronvideo/animvideo/lion.mp4', audioSrc: 'assets/pronvideo/audio/lion.mp3' },
61
+ { letter: 'M', word: 'Moon', phonetics: '/muːn/', imgSrc: 'assets/pronvideo/animvideo/moon.mp4', audioSrc: 'assets/pronvideo/audio/moon.mp3' },
62
+ { letter: 'N', word: 'Nest', phonetics: '/nest/', imgSrc: 'assets/pronvideo/animvideo/nest.mp4', audioSrc: 'assets/pronvideo/audio/nest.mp3' },
63
+ { letter: 'O', word: 'Orange', phonetics: '/ˈɒrɪndʒ/', imgSrc: 'assets/pronvideo/animvideo/orange.mp4', audioSrc: 'assets/pronvideo/audio/orange.mp3' },
64
+ { letter: 'P', word: 'Pig', phonetics: '/pɪɡ/', imgSrc: 'assets/pronvideo/animvideo/pig.mp4', audioSrc: 'assets/pronvideo/audio/pig.mp3' },
65
+ { letter: 'Q', word: 'Queen', phonetics: '/kwiːn/', imgSrc: 'assets/pronvideo/animvideo/queen.mp4', audioSrc: 'assets/pronvideo/audio/queen.mp3' },
66
+ { letter: 'R', word: 'Rabbit', phonetics: '/ˈræbɪt/', imgSrc: 'assets/pronvideo/animvideo/rabbit.mp4', audioSrc: 'assets/pronvideo/audio/rabbit.mp3' },
67
+ { letter: 'S', word: 'Sun', phonetics: '/sʌn/', imgSrc: 'assets/pronvideo/animvideo/sun.mp4', audioSrc: 'assets/pronvideo/audio/sun.mp3' },
68
+ { letter: 'T', word: 'Tree', phonetics: '/triː/', imgSrc: 'assets/pronvideo/animvideo/tree.mp4', audioSrc: 'assets/pronvideo/audio/tree.mp3' },
69
+ { letter: 'U', word: 'Umbrella', phonetics: '/ʌmˈbrelə/', imgSrc: 'assets/pronvideo/animvideo/umbrella.mp4', audioSrc: 'assets/pronvideo/audio/umbrella.mp3' },
70
+ { letter: 'V', word: 'Van', phonetics: '/væn/', imgSrc: 'assets/pronvideo/animvideo/van.mp4', audioSrc: 'assets/pronvideo/audio/van.mp3' },
71
+ { letter: 'W', word: 'Watch', phonetics: '/wɒtʃ/', imgSrc: 'assets/pronvideo/animvideo/watch.mp4', audioSrc: 'assets/pronvideo/audio/watch.mp3' },
72
+ { letter: 'X', word: 'Xylophone', phonetics: '/ˈzaɪləfəʊn/', imgSrc: 'assets/pronvideo/animvideo/xylophone.mp4', audioSrc: 'assets/pronvideo/audio/xylophone.mp3' },
73
+ { letter: 'Y', word: 'Yarn', phonetics: '/jɑːn/', imgSrc: 'assets/pronvideo/animvideo/yarn.mp4', audioSrc: 'assets/pronvideo/audio/yarn.mp3' },
74
+ { letter: 'Z', word: 'Zebra', phonetics: '/ˈzebrə/', imgSrc: 'assets/pronvideo/animvideo/zebra.mp4', audioSrc: 'assets/pronvideo/audio/zebra.mp3' }
75
+ ];
76
+
77
+ index = 0;
78
+ get current(): PracticeItem { return this.items[this.index]; }
79
+
80
+ // ---------------- RECORDING ----------------
81
+ isRecording = false;
82
+ isScoring = false;
83
+ isOscillating = false;
84
+
85
+ private mediaStream?: MediaStream;
86
+ private mediaRecorder?: MediaRecorder;
87
+ private chunks: BlobPart[] = [];
88
+ private currentMimeType = 'audio/webm';
89
+
90
+ recordedAudioUrl: string | null = null;
91
+ lastRecordedBlob: Blob | null = null;
92
+
93
+ // ---------------- SILENCE DETECTION ----------------
94
+ private audioCtx?: AudioContext;
95
+ private analyser?: AnalyserNode;
96
+ private micSource?: MediaStreamAudioSourceNode;
97
+ private silenceCheckId?: number;
98
+
99
+ private lastSpeechAt = 0;
100
+ private recordingStartedAt = 0;
101
+ private hasSpoken = false;
102
+
103
+ private readonly silenceMs = 5000;
104
+ private readonly startSilenceMs = 5000;
105
+ private readonly silenceThreshold = 0.01;
106
+
107
+ // ---------------- COUNTDOWN ----------------
108
+ duration = 3;
109
+ isCountingDown = false;
110
+ timeLeft = this.duration;
111
+ private preRecordIntervalId?: number;
112
+
113
+ readonly radius = 38;
114
+ readonly circumference = 2 * Math.PI * this.radius;
115
+ strokeDashoffset = this.circumference;
116
+
117
+ // ---------------- RESULT ----------------
118
+ showResult = false;
119
+ score = 0;
120
+ stars = 0;
121
+ feedbackLines: string[] = [];
122
+ videoUrl = '';
123
+ private lastVideoBlobUrl: string | null = null;
124
+ shortfeedback: string = '';
125
+
126
+ // ---------------- CANCEL / RESET CONTROL ----------------
127
+ private cancelScoring$ = new Subject<void>(); // cancels API call
128
+ private recordRunId = 0; // blocks onstop->API after navigation
129
+
130
+ // ---------------- LIFECYCLE ----------------
131
+ constructor(
132
+ private http: HttpClient,
133
+ public dialogRef: MatDialogRef<PronunciationRagUpgradeComponent>,
134
+ @Inject(MAT_DIALOG_DATA) public data: any,
135
+ private cdr: ChangeDetectorRef
136
+ ) { }
137
+
138
+ ngOnInit(): void {
139
+ this.setupBestMimeType();
140
+ this.resetResult();
141
+ }
142
+
143
+ ngOnDestroy(): void {
144
+ // cancel any pending api
145
+ this.cancelScoring$.next();
146
+ this.cancelScoring$.complete();
147
+
148
+ this.stopTracks();
149
+ this.safeStopRecorder();
150
+ this.teardownAudioGraph();
151
+
152
+ if (this.lastVideoBlobUrl) {
153
+ try { URL.revokeObjectURL(this.lastVideoBlobUrl); } catch { }
154
+ this.lastVideoBlobUrl = null;
155
+ }
156
+ }
157
+
158
+ // ---------------- RECORD CONTROL ----------------
159
+ async toggleRecording(): Promise<void> {
160
+ if (this.isRecording) {
161
+ this.stopRecording(false);
162
+ return;
163
+ }
164
+ this.startPreRecordCountdown();
165
+ }
166
+
167
+ private startPreRecordCountdown(): void {
168
+ this.score = 0;
169
+ this.shortfeedback = '';
170
+
171
+ if (this.isCountingDown || this.isRecording) return;
172
+
173
+ // stop any previous run/api
174
+ this.cancelScoring$.next();
175
+ this.isScoring = false;
176
+ this.isOscillating = false;
177
+
178
+ this.isCountingDown = true;
179
+ this.timeLeft = this.duration;
180
+
181
+ const totalMs = this.duration * 1000;
182
+ const start = performance.now();
183
+
184
+ this.strokeDashoffset = this.circumference;
185
+
186
+ this.preRecordIntervalId = window.setInterval(() => {
187
+ const elapsed = performance.now() - start;
188
+ const progress = Math.min(1, elapsed / totalMs);
189
+
190
+ this.strokeDashoffset = this.circumference * (1 - progress);
191
+ this.timeLeft = Math.ceil((totalMs - elapsed) / 1000);
192
+
193
+ if (elapsed >= totalMs) {
194
+ if (this.preRecordIntervalId) {
195
+ try { clearInterval(this.preRecordIntervalId); } catch { }
196
+ this.preRecordIntervalId = undefined;
197
+ }
198
+ this.startRecordingInternal();
199
+ }
200
+
201
+ try { this.cdr.detectChanges(); } catch { }
202
+ }, 100);
203
+ }
204
+
205
+ private async startRecordingInternal(): Promise<void> {
206
+ this.isCountingDown = false;
207
+
208
+ // new run id (used to ignore late onstop)
209
+ const myRunId = ++this.recordRunId;
210
+
211
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
212
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, { mimeType: this.currentMimeType });
213
+ this.chunks = [];
214
+
215
+ this.mediaRecorder.ondataavailable = e => e.data.size && this.chunks.push(e.data);
216
+
217
+ this.mediaRecorder.onstop = () => {
218
+ // If user navigated/reset, ignore this stop event
219
+ if (myRunId !== this.recordRunId) return;
220
+ this.onRecordingStopped(myRunId);
221
+ };
222
+
223
+ this.isRecording = true;
224
+
225
+ this.setupSilenceDetection(this.mediaStream);
226
+ this.mediaRecorder.start();
227
+
228
+ try { this.cdr.detectChanges(); } catch { }
229
+ }
230
+
231
+ stopRecording(_isAutoStop: boolean = false): void {
232
+ if (!this.isRecording) return;
233
+
234
+ // stop countdown interval (if any)
235
+ if (this.preRecordIntervalId) {
236
+ try { clearInterval(this.preRecordIntervalId); } catch { }
237
+ this.preRecordIntervalId = undefined;
238
+ }
239
+ this.isCountingDown = false;
240
+
241
+ this.isRecording = false;
242
+
243
+ this.safeStopRecorder();
244
+ this.stopTracks();
245
+ this.teardownAudioGraph();
246
+
247
+ try { this.cdr.detectChanges(); } catch { }
248
+ }
249
+
250
+ private safeStopRecorder(): void {
251
+ try {
252
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
253
+ this.mediaRecorder.stop();
254
+ }
255
+ } catch { }
256
+ }
257
+
258
+ private onRecordingStopped(runId: number): void {
259
+ // if reset happened, do nothing
260
+ if (runId !== this.recordRunId) return;
261
+
262
+ const blob = new Blob(this.chunks, { type: this.currentMimeType });
263
+ this.chunks = [];
264
+
265
+ // If audio is too small, do not call API
266
+ if (!blob || blob.size < 2000) {
267
+ this.isOscillating = false;
268
+ this.isScoring = false;
269
+ this.shortfeedback = 'No voice detected. Please try again.';
270
+ this.showResult = true;
271
+ try { this.cdr.detectChanges(); } catch { }
272
+ return;
273
+ }
274
+
275
+ this.lastRecordedBlob = blob;
276
+
277
+ // start gauge oscillation while scoring
278
+ this.isOscillating = true;
279
+ try { this.cdr.detectChanges(); } catch { }
280
+
281
+ this.sendForScoring(blob, this.current.word, runId);
282
+ }
283
+
284
+ // ---------------- SILENCE DETECTION (AUTO STOP) ----------------
285
+ private setupSilenceDetection(stream: MediaStream): void {
286
+ this.teardownAudioGraph();
287
+
288
+ this.audioCtx = new AudioContext();
289
+ this.analyser = this.audioCtx.createAnalyser();
290
+ this.analyser.fftSize = 2048;
291
+
292
+ this.micSource = this.audioCtx.createMediaStreamSource(stream);
293
+ this.micSource.connect(this.analyser);
294
+
295
+ this.recordingStartedAt = performance.now();
296
+ this.lastSpeechAt = this.recordingStartedAt;
297
+ this.hasSpoken = false;
298
+
299
+ const loop = () => {
300
+ if (!this.analyser) return;
301
+ if (!this.isRecording) return;
302
+
303
+ const data = new Float32Array(this.analyser.fftSize);
304
+ this.analyser.getFloatTimeDomainData(data);
305
+
306
+ let sumSq = 0;
307
+ for (let i = 0; i < data.length; i++) sumSq += data[i] * data[i];
308
+ const rms = Math.sqrt(sumSq / data.length);
309
+
310
+ const now = performance.now();
311
+
312
+ if (rms > this.silenceThreshold) {
313
+ this.lastSpeechAt = now;
314
+ this.hasSpoken = true;
315
+ }
316
+
317
+ // A) never spoke -> stop
318
+ if (!this.hasSpoken && (now - this.recordingStartedAt) > this.startSilenceMs) {
319
+ this.stopRecording(true);
320
+ return;
321
+ }
322
+
323
+ // B) spoke then silent -> stop
324
+ if (this.hasSpoken && (now - this.lastSpeechAt) > this.silenceMs) {
325
+ this.stopRecording(true);
326
+ return;
327
+ }
328
+
329
+ this.silenceCheckId = window.setTimeout(loop, 100);
330
+ };
331
+
332
+ loop();
333
+ }
334
+
335
+ private teardownAudioGraph(): void {
336
+ if (this.silenceCheckId) {
337
+ try { clearTimeout(this.silenceCheckId); } catch { }
338
+ this.silenceCheckId = undefined;
339
+ }
340
+ try { this.micSource?.disconnect(); } catch { }
341
+ try { this.analyser?.disconnect(); } catch { }
342
+ try { this.audioCtx?.close(); } catch { }
343
+
344
+ this.micSource = undefined;
345
+ this.analyser = undefined;
346
+ this.audioCtx = undefined;
347
+ }
348
+
349
+ private stopTracks(): void {
350
+ this.mediaStream?.getTracks().forEach(t => t.stop());
351
+ this.mediaStream = undefined;
352
+ }
353
+
354
+ // ---------------- BACKEND ----------------
355
+ private sendForScoring(blob: Blob, word: string, runId: number): void {
356
+ // if reset happened, do nothing
357
+ if (runId !== this.recordRunId) return;
358
+
359
+ const fd = new FormData();
360
+ fd.append('audio', blob, 'student.webm');
361
+ fd.append('word', word.toLowerCase());
362
+
363
+ this.isScoring = true;
364
+
365
+ this.http.post<any>(this.SCORE_ENDPOINT, fd)
366
+ .pipe(
367
+ takeUntil(this.cancelScoring$), // ✅ cancel API on navigation
368
+ finalize(() => {
369
+ this.isScoring = false;
370
+ this.isOscillating = false;
371
+ try { this.cdr.detectChanges(); } catch { }
372
+ })
373
+ )
374
+ .subscribe(res => {
375
+ // If user navigated/reset while API was in progress, ignore response
376
+ if (runId !== this.recordRunId) return;
377
+
378
+ this.score = this.normalizeScore(res.score);
379
+ this.stars = this.mapStars(this.score);
380
+ this.shortfeedback = res.feedback;
381
+ this.feedbackLines = this.mapFeedbackFromStatus(res.status);
382
+ this.showResult = true;
383
+
384
+ if (res.videoBlobBase64) {
385
+ const bytes = Uint8Array.from(atob(res.videoBlobBase64), c => c.charCodeAt(0));
386
+ const videoBlob = new Blob([bytes], { type: 'video/mp4' });
387
+
388
+ // revoke previous
389
+ if (this.lastVideoBlobUrl) {
390
+ try { URL.revokeObjectURL(this.lastVideoBlobUrl); } catch { }
391
+ }
392
+
393
+ this.videoUrl = URL.createObjectURL(videoBlob);
394
+ this.lastVideoBlobUrl = this.videoUrl;
395
+ this.tryPlayFeedbackVideo(this.videoUrl);
396
+ }
397
+
398
+ try { this.cdr.detectChanges(); } catch { }
399
+ }, _err => {
400
+ if (runId !== this.recordRunId) return;
401
+ this.shortfeedback = 'Error while scoring. Please try again.';
402
+ this.showResult = true;
403
+ try { this.cdr.detectChanges(); } catch { }
404
+ });
405
+ }
406
+
407
+ // ---------------- HARD RESET (for navigation) ----------------
408
+ private cancelAllRunningProcesses(): void {
409
+ // 1) block any pending onstop -> API
410
+ this.recordRunId++;
411
+
412
+ // 2) cancel API call if running
413
+ this.cancelScoring$.next();
414
+
415
+ // 3) stop oscillation/scoring UI immediately
416
+ this.isScoring = false;
417
+ this.isOscillating = false;
418
+
419
+ // 4) stop countdown
420
+ if (this.preRecordIntervalId) {
421
+ try { clearInterval(this.preRecordIntervalId); } catch { }
422
+ this.preRecordIntervalId = undefined;
423
+ }
424
+ this.isCountingDown = false;
425
+ this.timeLeft = this.duration;
426
+ this.strokeDashoffset = this.circumference;
427
+
428
+ // 5) stop recording + audio graph + tracks
429
+ this.isRecording = false;
430
+ this.safeStopRecorder();
431
+ this.stopTracks();
432
+ this.teardownAudioGraph();
433
+ this.chunks = [];
434
+
435
+ // 6) reset speaker/video (pause button -> play)
436
+ this.resetVideoPlayerState();
437
+
438
+ try { this.cdr.detectChanges(); } catch { }
439
+ }
440
+
441
+ private resetVideoPlayerState(): void {
442
+ try {
443
+ const v = this.videoElRef?.nativeElement;
444
+ if (v) {
445
+ v.pause();
446
+ v.currentTime = 0;
447
+ v.removeAttribute('src');
448
+ v.load();
449
+ }
450
+ } catch { }
451
+
452
+ this.showVideo = false; // teacher looping video will show
453
+ this.videoSrc = '';
454
+ this.isPlayingVideo = false;
455
+ }
456
+
457
+ // ---------------- FEEDBACK ----------------
458
+ private mapFeedbackFromStatus(status: string): string[] {
459
+ switch (status) {
460
+ case 'success': return ['Excellent pronunciation.', 'Very clear sound.'];
461
+ case 'vowel_error': return ['Check your vowel sound.', 'Open your mouth clearly.'];
462
+ case 'consonant_error': return ['Focus on consonant sound.', 'Try again slowly.'];
463
+ case 'ending_error': return ['Ending sound missing.', 'Finish the word properly.'];
464
+ case 'stress_error': return ['Stress needs correction.', 'Listen and repeat.'];
465
+ case 'wrong_word': return ['Wrong word spoken.', 'Say only the target word.'];
466
+ case 'silence': return ['No speech detected.', 'Please try again.'];
467
+ default: return ['Good attempt.', 'Try once more.'];
468
+ }
469
+ }
470
+
471
+ private normalizeScore(n: any): number {
472
+ n = Number(n);
473
+ return isNaN(n) ? 0 : Math.min(100, Math.max(0, Math.round(n)));
474
+ }
475
+
476
+ private mapStars(s: number): number {
477
+ if (s >= 90) return 5;
478
+ if (s >= 80) return 4;
479
+ if (s >= 70) return 3;
480
+ if (s >= 60) return 2;
481
+ return 1;
482
+ }
483
+
484
+ // ---------------- VIDEO ----------------
485
+ private tryPlayFeedbackVideo(url: string): void {
486
+ this.showVideo = true;
487
+ this.videoSrc = url;
488
+
489
+ setTimeout(() => {
490
+ const v = this.videoElRef?.nativeElement;
491
+ if (!v) return;
492
+
493
+ v.src = url;
494
+ v.load();
495
+
496
+ v.play()
497
+ .then(() => {
498
+ this.isPlayingVideo = true; // ✅ icon becomes pause
499
+ try { this.cdr.detectChanges(); } catch { }
500
+ })
501
+ .catch(() => {
502
+ this.isPlayingVideo = false;
503
+ try { this.cdr.detectChanges(); } catch { }
504
+ });
505
+ }, 0);
506
+ }
507
+
508
+ // ---------------- UTILS ----------------
509
+ private setupBestMimeType(): void {
510
+ for (const t of this.preferredMimeTypes) {
511
+ if ((window as any).MediaRecorder?.isTypeSupported(t)) {
512
+ this.currentMimeType = t;
513
+ return;
514
+ }
515
+ }
516
+ }
517
+
518
+ private resetResult(): void {
519
+ this.showResult = false;
520
+ this.score = 0;
521
+ this.stars = 0;
522
+ this.feedbackLines = [];
523
+ this.showVideo = false;
524
+ this.videoSrc = '';
525
+ this.shortfeedback = '';
526
+ }
527
+
528
+ // ---------------------------
529
+ // PLAY MODEL PRONUNCIATION AUDIO
530
+ // ---------------------------
531
+ playWordAudio(): void {
532
+ const src = this.current?.audioSrc || this.getAudioSrcFromWord(this.current.word);
533
+ if (!src) return;
534
+
535
+ try {
536
+ const audio = new Audio(src);
537
+ audio.currentTime = 0;
538
+ audio.play().catch(() => { });
539
+ } catch { }
540
+ }
541
+
542
+ private getAudioSrcFromWord(word: string): string {
543
+ if (!word) return '';
544
+ const fileName = word.trim().toLowerCase().replace(/\s+/g, '-');
545
+ return `assets/pronvideo/audio/${fileName}.mp3`;
546
+ }
547
+
548
+ // ---------------------------
549
+ // VIDEO END HANDLER
550
+ // ---------------------------
551
+ onVideoEnded(): void {
552
+ this.resetVideoPlayerState();
553
+ this.isOscillating = false;
554
+ try { this.cdr.detectChanges(); } catch { }
555
+ }
556
+
557
+ onVideoPlay(): void {
558
+ this.isPlayingVideo = true;
559
+ try { this.cdr.detectChanges(); } catch { }
560
+ }
561
+
562
+ onVideoPause(): void {
563
+ this.isPlayingVideo = false;
564
+ try { this.cdr.detectChanges(); } catch { }
565
+ }
566
+
567
+ // ---------------------------
568
+ // TOGGLE PLAY / PAUSE FOR VIDEO
569
+ // ---------------------------
570
+ toggleVideoPlay(): void {
571
+ try {
572
+ const v = this.videoElRef?.nativeElement;
573
+
574
+ if (!this.showVideo) {
575
+ this.videoSrc = this.getVideoSrcFromWord(this.current.word);
576
+ this.showVideo = true;
577
+
578
+ setTimeout(() => {
579
+ const video = this.videoElRef?.nativeElement;
580
+ if (!video) return;
581
+
582
+ video.src = this.videoSrc;
583
+ video.load();
584
+ video.play()
585
+ .then(() => {
586
+ this.isPlayingVideo = true;
587
+ try { this.cdr.detectChanges(); } catch { }
588
+ })
589
+ .catch(() => {
590
+ this.isPlayingVideo = false;
591
+ try { this.cdr.detectChanges(); } catch { }
592
+ });
593
+ }, 0);
594
+
595
+ return;
596
+ }
597
+
598
+ if (!v) return;
599
+
600
+ if (v.paused) {
601
+ v.play()
602
+ .then(() => {
603
+ this.isPlayingVideo = true;
604
+ try { this.cdr.detectChanges(); } catch { }
605
+ })
606
+ .catch(() => {
607
+ this.isPlayingVideo = false;
608
+ try { this.cdr.detectChanges(); } catch { }
609
+ });
610
+ } else {
611
+ v.pause();
612
+ this.isPlayingVideo = false;
613
+ try { this.cdr.detectChanges(); } catch { }
614
+ }
615
+ } catch { }
616
+ }
617
+
618
+ private getVideoSrcFromWord(word: string): string {
619
+ if (!word) return '';
620
+ const fileName = word.trim().toLowerCase().replace(/\s+/g, '-');
621
+ return `assets/pronvideo/videos/${fileName}.mp4`;
622
+ }
623
+
624
+ // ---------------------------
625
+ // GAUGE NEEDLE ANGLE (0–100 → -90° to +90°)
626
+ // ---------------------------
627
+ get needleAngle(): number {
628
+ const value = Math.max(0, Math.min(100, Number(this.score || 0)));
629
+ return -90 + (value * 1.8);
630
+ }
631
+
632
+ // ---------------------------
633
+ // NAVIGATION : PREVIOUS / NEXT WORD
634
+ // ---------------------------
635
+ prev(): void {
636
+ if (this.index <= 0) return;
637
+
638
+ // ✅ stop api + reset oscillation + reset speaker + stop countdown/record
639
+ this.cancelAllRunningProcesses();
640
+
641
+ this.index--;
642
+ this.resetAfterNavigation();
643
+ }
644
+
645
+ next(): void {
646
+ if (this.index >= this.items.length - 1) return;
647
+
648
+ // ✅ stop api + reset oscillation + reset speaker + stop countdown/record
649
+ this.cancelAllRunningProcesses();
650
+
651
+ this.index++;
652
+ this.resetAfterNavigation();
653
+ }
654
+
655
+ private resetAfterNavigation(): void {
656
+ // ensure everything is stopped
657
+ this.cancelAllRunningProcesses();
658
+
659
+ this.score = 0;
660
+ this.stars = 0;
661
+ this.feedbackLines = [];
662
+ this.showResult = false;
663
+ this.shortfeedback = '';
664
+
665
+ this.lastRecordedBlob = null;
666
+ if (this.recordedAudioUrl) {
667
+ try { URL.revokeObjectURL(this.recordedAudioUrl); } catch { }
668
+ this.recordedAudioUrl = null;
669
+ }
670
+
671
+ try { this.cdr.detectChanges(); } catch { }
672
+ }
673
+
674
+ closePopup(): void {
675
+ // stop everything before closing
676
+ this.cancelAllRunningProcesses();
677
+ this.dialogRef.close();
678
+ }
679
+ }
src/assets/pronvideo/animvideo/apple.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:158222ad1b07188440d77474f9cdc6002973e5ed9213a234e603ff66adaa37ca
3
+ size 5790714
src/assets/pronvideo/animvideo/ball.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bbf6c21e57ebb0f34acf11dfc57eca9a6ceb944708b1dd573e0bb4f082efe7d7
3
+ size 5634357
src/assets/pronvideo/animvideo/cat.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a4e400917eb73e7b2481b5e89799f4c877f860b96fef49e61f1a6fee2db718d
3
+ size 5360080
src/assets/pronvideo/animvideo/dog.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aced7cd298f418f1d7ac6952c99df14d6030bd56a22ac17d08352bf283b1e0de
3
+ size 5342277
src/assets/pronvideo/animvideo/egg.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea0b1500966539eee652962355c20c380a903ee79bd79ef57e3c730302525382
3
+ size 4935460
src/assets/pronvideo/animvideo/fish.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c724abc31b32d058b69c2142093c24504fa863dc5f7e7fb78310e6c78295013
3
+ size 4907809
src/assets/pronvideo/animvideo/grapes.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8e26b650686a72c010852f336982b188f779af5938d554bf86f755a5fcdf9b6
3
+ size 9053666
src/assets/pronvideo/animvideo/hat.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a435204cb5e26fb7dda794b0b1ae482445841bd8643a3bd8eb4dbca2fa69921
3
+ size 7203469
src/assets/pronvideo/animvideo/icecream.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:89530e185f406945a71b19300c5a00711ada88641af6dcb6c59ab5dc5c5835c4
3
+ size 6187864
src/assets/pronvideo/animvideo/jar.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:85e954d6f782f5d17da42cf60c2a061d114068396285fd5d14602ca88f975d5d
3
+ size 9213510
src/assets/pronvideo/animvideo/kite.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba84336efc7cb4e687eee64ed861cd753fe99781cc6c5bebe3cefe1b4b5ca4e7
3
+ size 1843752
src/assets/pronvideo/animvideo/lion.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eef94bc7cd6806e735254fdd077c3c211ca6cb1b5eefff27e125a7723ca3b305
3
+ size 6097806
src/assets/pronvideo/animvideo/moon.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:147b9e565116439983091812d786a55752e4e44511d7bf61fa218ac9a0805464
3
+ size 4901274
src/assets/pronvideo/animvideo/nest.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:820d37c3b5b153753a58532c914e011d5cdbc90df30d1bd3c5788834bfbe6d84
3
+ size 7650498
src/assets/pronvideo/animvideo/orange.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e9e3c0daecbcf4c8c26c2ec43a0bc68531922bd621117849624a88f01e1eb5b4
3
+ size 6440257
src/assets/pronvideo/animvideo/pig.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b6db18186b1ac3e735eda91d9d73522f900a7414b83985fc063aa7b537be133e
3
+ size 6421810
src/assets/pronvideo/animvideo/queen.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b2e153fd06d061dc8f44595c3b5cccf1c11a2b4a3676c0d411f3586a4e965bd3
3
+ size 2724148
src/assets/pronvideo/animvideo/rabbit.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bfb9ec3714cc6dcd7cae3be5594e53a2267a26da0eb82770a60ea5c91bba6b8
3
+ size 7266712
src/assets/pronvideo/animvideo/sun.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:85cb830b40c979014ed19ace95229fd8d806db3be52a87a3a387a20a017a6110
3
+ size 1269242
src/assets/pronvideo/animvideo/tree.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e6a80995e8b2ab65495d769ca18250c2a979d0b0a2865f8ea6bfcb6e70106a70
3
+ size 12132430
src/assets/pronvideo/animvideo/umbrella.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6df6d94e25ac51f98d33f3a452a937bb81c8d6000172fd51564bc8b43f925678
3
+ size 2489346
src/assets/pronvideo/animvideo/van.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86c3bdd636d07bb59384b530c66cc6518ac251a5cdae3c02ca70818c8c69ae04
3
+ size 10097663
src/assets/pronvideo/animvideo/watch.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ed42674319ae37c38f5d1e1b27c7deb7ade849e91a53e7e105c0fbed36f9701
3
+ size 8078945
src/assets/pronvideo/animvideo/xylophone.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fa2d40bc1f9f0bc96f147b0696000b33607ad9662ca5dd319b2f429d6bcfaa6
3
+ size 2050298
src/assets/pronvideo/animvideo/yarn.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:742ba761ac8f917629fde9c560598fb2258131bb625d2d5c55f77c8440cf9e08
3
+ size 3490049
src/assets/pronvideo/animvideo/zebra.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1144f6b4892b7ed3b31d6895dd692b40428b8e985e41c0f7306087ad4a5a37fc
3
+ size 7161675
src/assets/pronvideo/listening.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26c028bad47e42b3b89e05b2e603d90309b82d53193d7e2be0c88686c5dc57ea
3
+ size 3569866
src/assets/pronvideo/slate.png ADDED

Git LFS Details

  • SHA256: c43b888d1dd64f1d0c93917dbe7813353d454b271eb0ae3315d70a3d24a52b76
  • Pointer size: 130 Bytes
  • Size of remote file: 21.4 kB
src/assets/pronvideo/videos/lion-old.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca2d4e211b05380e1152ede6052e0751d29e46da96c78431d7bd22a83f301e94
3
+ size 5431178
src/assets/pronvideo/videos/lion.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ca2d4e211b05380e1152ede6052e0751d29e46da96c78431d7bd22a83f301e94
3
- size 5431178
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:952c1a94530ce8e620851326e7d6d368be370b42709a3677aace687e925303f8
3
+ size 5127880
src/assets/pronvideo/videos/rabbit-old.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c3b59991ee348ba0a9117ad5648fbf0ea03fc69298b4d3f8863d45dd94194ee
3
+ size 4932831
src/assets/pronvideo/videos/rabbit.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9c3b59991ee348ba0a9117ad5648fbf0ea03fc69298b4d3f8863d45dd94194ee
3
- size 4932831
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cebe3f87a003315b40ef9ceaa62ac7ed47f05b5e37858d891c413b5dd01626a2
3
+ size 4795049
src/assets/pronvideo/videos/van-old.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4724e2cab20be89b9148c699a33c2a433e239611a7787c45487942ba7ef9d981
3
+ size 4462232
src/assets/pronvideo/videos/van.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:4724e2cab20be89b9148c699a33c2a433e239611a7787c45487942ba7ef9d981
3
- size 4462232
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19ef3b0027192a8d08ebd3069d0bbc09a3dbfdf2c10262ba0c756db54517d57b
3
+ size 4232804
src/assets/pronvideo/videos/xylophone-old.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:93fc9aef9f9419f344a0ba97ff3b5d1c0743ad121956f462e504e31388893164
3
+ size 5199424
src/assets/pronvideo/videos/xylophone.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:93fc9aef9f9419f344a0ba97ff3b5d1c0743ad121956f462e504e31388893164
3
- size 5199424
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfc61e0844b3df40b4ec74694fd048c3811ba2dd5fd16b48ac48a2619aa4d3a6
3
+ size 18925813
src/assets/pronvideo/videos/yarn-old.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fc19acd88c47a3f7c33d750ef0f77c2dce6272d51e9e532576c2d00bda52413
3
+ size 5095081
src/assets/pronvideo/videos/yarn.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3fc19acd88c47a3f7c33d750ef0f77c2dce6272d51e9e532576c2d00bda52413
3
- size 5095081
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:640cb94a1c14d8b733556a81f765319b3eea544b26022f5eece33530523c2b95
3
+ size 15318745
src/assets/pronvideo/videos/{zebra.mp4 → zebra-old.mp4} RENAMED
File without changes