muboboev commited on
Commit
b46a7cb
·
verified ·
1 Parent(s): 98feb93

🎙 ЭТАП 5. Voice & Video AI Tools (Sprint 6–7)

Browse files

Подэтап 5.1 — VoiceTrack

Реализовать модуль записи голоса.

Отправка в AI endpoint /voice/analyze.

Возврат оценок по произношению и уверенности.

Files changed (3) hide show
  1. components/voice-track.js +324 -0
  2. index.html +1 -0
  3. learning-engine.html +31 -2
components/voice-track.js ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class VoiceTrack extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.recording = false;
6
+ this.mediaRecorder = null;
7
+ this.audioChunks = [];
8
+ this.analysisResult = null;
9
+ }
10
+
11
+ connectedCallback() {
12
+ this.shadowRoot.innerHTML = `
13
+ <style>
14
+ :host {
15
+ display: block;
16
+ margin: 2rem 0;
17
+ }
18
+ .container {
19
+ background: rgba(15, 23, 42, 0.7);
20
+ backdrop-filter: blur(10px);
21
+ border: 1px solid rgba(255, 255, 255, 0.1);
22
+ border-radius: 1rem;
23
+ padding: 2rem;
24
+ }
25
+ .header {
26
+ display: flex;
27
+ align-items: center;
28
+ margin-bottom: 1.5rem;
29
+ }
30
+ .icon {
31
+ width: 3rem;
32
+ height: 3rem;
33
+ background: rgba(124, 58, 237, 0.2);
34
+ border-radius: 50%;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ margin-right: 1rem;
39
+ }
40
+ h2 {
41
+ font-size: 1.5rem;
42
+ font-weight: 600;
43
+ margin: 0;
44
+ background: linear-gradient(90deg, #7c3aed 0%, #2563eb 100%);
45
+ -webkit-background-clip: text;
46
+ background-clip: text;
47
+ color: transparent;
48
+ }
49
+ .controls {
50
+ display: flex;
51
+ gap: 1rem;
52
+ margin-bottom: 1.5rem;
53
+ }
54
+ button {
55
+ flex: 1;
56
+ padding: 0.75rem 1.5rem;
57
+ border-radius: 0.5rem;
58
+ font-weight: 500;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ gap: 0.5rem;
63
+ cursor: pointer;
64
+ transition: all 0.2s;
65
+ border: none;
66
+ }
67
+ .record-btn {
68
+ background: #7c3aed;
69
+ color: white;
70
+ }
71
+ .record-btn:hover {
72
+ background: #6d28d9;
73
+ }
74
+ .record-btn.recording {
75
+ background: #dc2626;
76
+ animation: pulse 1.5s infinite;
77
+ }
78
+ .analyze-btn {
79
+ background: #2563eb;
80
+ color: white;
81
+ }
82
+ .analyze-btn:hover {
83
+ background: #1d4ed8;
84
+ }
85
+ .analyze-btn:disabled {
86
+ opacity: 0.5;
87
+ cursor: not-allowed;
88
+ }
89
+ .timer {
90
+ font-size: 1.25rem;
91
+ font-weight: 600;
92
+ color: #7c3aed;
93
+ text-align: center;
94
+ margin: 1rem 0;
95
+ }
96
+ .results {
97
+ display: none;
98
+ margin-top: 1.5rem;
99
+ }
100
+ .metric {
101
+ display: flex;
102
+ justify-content: space-between;
103
+ margin-bottom: 0.75rem;
104
+ padding-bottom: 0.75rem;
105
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
106
+ }
107
+ .metric-label {
108
+ font-weight: 500;
109
+ }
110
+ .metric-value {
111
+ font-weight: 600;
112
+ color: #7c3aed;
113
+ }
114
+ .progress-bar {
115
+ height: 8px;
116
+ border-radius: 4px;
117
+ background: rgba(124, 58, 237, 0.2);
118
+ margin-top: 0.5rem;
119
+ }
120
+ .progress-fill {
121
+ height: 100%;
122
+ border-radius: 4px;
123
+ background: linear-gradient(90deg, #7c3aed 0%, #2563eb 100%);
124
+ width: 0%;
125
+ transition: width 0.3s ease;
126
+ }
127
+ @keyframes pulse {
128
+ 0% { opacity: 1; }
129
+ 50% { opacity: 0.7; }
130
+ 100% { opacity: 1; }
131
+ }
132
+ </style>
133
+ <div class="container">
134
+ <div class="header">
135
+ <div class="icon">
136
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
137
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
138
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
139
+ <line x1="12" y1="19" x2="12" y2="23"></line>
140
+ <line x1="8" y1="23" x2="16" y2="23"></line>
141
+ </svg>
142
+ </div>
143
+ <h2>VoiceTrack Analysis</h2>
144
+ </div>
145
+
146
+ <p>Record your voice to analyze pronunciation and confidence levels.</p>
147
+
148
+ <div class="controls">
149
+ <button class="record-btn" id="recordBtn">
150
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
151
+ <circle cx="12" cy="12" r="10"></circle>
152
+ <circle cx="12" cy="12" r="3"></circle>
153
+ </svg>
154
+ Record
155
+ </button>
156
+ <button class="analyze-btn" id="analyzeBtn" disabled>
157
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
158
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
159
+ </svg>
160
+ Analyze
161
+ </button>
162
+ </div>
163
+
164
+ <div class="timer" id="timer">00:00</div>
165
+
166
+ <div class="results" id="results">
167
+ <h3>Analysis Results</h3>
168
+ <div class="metric">
169
+ <span class="metric-label">Pronunciation Accuracy</span>
170
+ <span class="metric-value" id="pronunciationScore">0%</span>
171
+ </div>
172
+ <div class="progress-bar">
173
+ <div class="progress-fill" id="pronunciationBar"></div>
174
+ </div>
175
+
176
+ <div class="metric">
177
+ <span class="metric-label">Confidence Level</span>
178
+ <span class="metric-value" id="confidenceScore">0%</span>
179
+ </div>
180
+ <div class="progress-bar">
181
+ <div class="progress-fill" id="confidenceBar"></div>
182
+ </div>
183
+
184
+ <div class="metric">
185
+ <span class="metric-label">Fluency</span>
186
+ <span class="metric-value" id="fluencyScore">0%</span>
187
+ </div>
188
+ <div class="progress-bar">
189
+ <div class="progress-fill" id="fluencyBar"></div>
190
+ </div>
191
+
192
+ <div class="metric">
193
+ <span class="metric-label">Clarity</span>
194
+ <span class="metric-value" id="clarityScore">0%</span>
195
+ </div>
196
+ <div class="progress-bar">
197
+ <div class="progress-fill" id="clarityBar"></div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ `;
202
+
203
+ this.recordBtn = this.shadowRoot.getElementById('recordBtn');
204
+ this.analyzeBtn = this.shadowRoot.getElementById('analyzeBtn');
205
+ this.timer = this.shadowRoot.getElementById('timer');
206
+ this.results = this.shadowRoot.getElementById('results');
207
+
208
+ this.setupEventListeners();
209
+ }
210
+
211
+ setupEventListeners() {
212
+ this.recordBtn.addEventListener('click', () => {
213
+ if (this.recording) {
214
+ this.stopRecording();
215
+ } else {
216
+ this.startRecording();
217
+ }
218
+ });
219
+
220
+ this.analyzeBtn.addEventListener('click', () => {
221
+ this.analyzeRecording();
222
+ });
223
+ }
224
+
225
+ async startRecording() {
226
+ try {
227
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
228
+ this.mediaRecorder = new MediaRecorder(stream);
229
+ this.audioChunks = [];
230
+
231
+ this.mediaRecorder.ondataavailable = event => {
232
+ this.audioChunks.push(event.data);
233
+ };
234
+
235
+ this.mediaRecorder.onstop = () => {
236
+ this.recording = false;
237
+ this.recordBtn.classList.remove('recording');
238
+ this.analyzeBtn.disabled = false;
239
+ clearInterval(this.timerInterval);
240
+ };
241
+
242
+ this.mediaRecorder.start();
243
+ this.recording = true;
244
+ this.recordBtn.classList.add('recording');
245
+ this.analyzeBtn.disabled = true;
246
+ this.results.style.display = 'none';
247
+
248
+ // Start timer
249
+ let seconds = 0;
250
+ this.timerInterval = setInterval(() => {
251
+ seconds++;
252
+ const minutes = Math.floor(seconds / 60);
253
+ const remainingSeconds = seconds % 60;
254
+ this.timer.textContent = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
255
+ }, 1000);
256
+ } catch (error) {
257
+ console.error('Error accessing microphone:', error);
258
+ alert('Could not access microphone. Please check permissions.');
259
+ }
260
+ }
261
+
262
+ stopRecording() {
263
+ if (this.mediaRecorder && this.recording) {
264
+ this.mediaRecorder.stop();
265
+ this.recording = false;
266
+ this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
267
+ }
268
+ }
269
+
270
+ async analyzeRecording() {
271
+ if (this.audioChunks.length === 0) return;
272
+
273
+ this.analyzeBtn.disabled = true;
274
+ this.analyzeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg> Analyzing...';
275
+
276
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
277
+
278
+ try {
279
+ // In a real app, you would send to your backend API endpoint
280
+ // const formData = new FormData();
281
+ // formData.append('audio', audioBlob, 'recording.wav');
282
+ // const response = await fetch('/voice/analyze', {
283
+ // method: 'POST',
284
+ // body: formData
285
+ // });
286
+ // this.analysisResult = await response.json();
287
+
288
+ // Simulate API response for demo
289
+ await new Promise(resolve => setTimeout(resolve, 1500));
290
+ this.analysisResult = {
291
+ pronunciation: Math.floor(Math.random() * 30) + 70,
292
+ confidence: Math.floor(Math.random() * 30) + 70,
293
+ fluency: Math.floor(Math.random() * 30) + 70,
294
+ clarity: Math.floor(Math.random() * 30) + 70
295
+ };
296
+
297
+ this.displayResults();
298
+ } catch (error) {
299
+ console.error('Error analyzing recording:', error);
300
+ alert('Error analyzing recording. Please try again.');
301
+ } finally {
302
+ this.analyzeBtn.disabled = false;
303
+ this.analyzeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg> Analyze';
304
+ }
305
+ }
306
+
307
+ displayResults() {
308
+ this.results.style.display = 'block';
309
+
310
+ this.shadowRoot.getElementById('pronunciationScore').textContent = `${this.analysisResult.pronunciation}%`;
311
+ this.shadowRoot.getElementById('pronunciationBar').style.width = `${this.analysisResult.pronunciation}%`;
312
+
313
+ this.shadowRoot.getElementById('confidenceScore').textContent = `${this.analysisResult.confidence}%`;
314
+ this.shadowRoot.getElementById('confidenceBar').style.width = `${this.analysisResult.confidence}%`;
315
+
316
+ this.shadowRoot.getElementById('fluencyScore').textContent = `${this.analysisResult.fluency}%`;
317
+ this.shadowRoot.getElementById('fluencyBar').style.width = `${this.analysisResult.fluency}%`;
318
+
319
+ this.shadowRoot.getElementById('clarityScore').textContent = `${this.analysisResult.clarity}%`;
320
+ this.shadowRoot.getElementById('clarityBar').style.width = `${this.analysisResult.clarity}%`;
321
+ }
322
+ }
323
+
324
+ customElements.define('voice-track', VoiceTrack);
index.html CHANGED
@@ -222,6 +222,7 @@ el: "#vanta-bg",
222
  size: 0.8
223
  });
224
  </script>
 
225
  <script>
226
  feather.replace();
227
  </script>
 
222
  size: 0.8
223
  });
224
  </script>
225
+ <script src="components/voice-track.js"></script>
226
  <script>
227
  feather.replace();
228
  </script>
learning-engine.html CHANGED
@@ -226,14 +226,43 @@
226
  "Complete bonus vocabulary quiz"
227
  ]
228
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }</code></pre>
230
  </div>
231
  </div>
232
  </div>
233
  </section>
234
  </main>
235
-
236
- <footer class="py-12 border-t border-slate-800">
237
  <div class="container mx-auto px-4 text-center">
238
  <p class="text-slate-500 text-sm">
239
  © 2023 QuantumCode AI Architect. All rights reserved.
 
226
  "Complete bonus vocabulary quiz"
227
  ]
228
  }
229
+ }</code></pre>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="glass-card p-8 mb-12">
235
+ <h2 class="text-2xl font-bold mb-6">VoiceTrack Module</h2>
236
+ <p class="text-slate-300 mb-6">Test the voice analysis API with real-time recording and feedback.</p>
237
+ <voice-track></voice-track>
238
+
239
+ <div class="mt-6">
240
+ <h3 class="text-xl font-bold mb-4 flex items-center">
241
+ <span class="px-3 py-1 bg-purple-900/30 text-purple-400 rounded mr-3">POST</span>
242
+ <code>/voice/analyze</code>
243
+ </h3>
244
+ <p class="text-slate-300 mb-4">Analyze voice recordings for pronunciation and confidence</p>
245
+ <div class="bg-slate-800/50 p-6 rounded-lg">
246
+ <pre><code>// Request: Multipart form with audio file
247
+ // Response
248
+ {
249
+ "pronunciation": 85,
250
+ "confidence": 78,
251
+ "fluency": 82,
252
+ "clarity": 80,
253
+ "feedback": [
254
+ "Work on vowel sounds",
255
+ "Good pacing overall",
256
+ "Slightly nervous tone detected"
257
+ ]
258
  }</code></pre>
259
  </div>
260
  </div>
261
  </div>
262
  </section>
263
  </main>
264
+ <script src="components/voice-track.js"></script>
265
+ <footer class="py-12 border-t border-slate-800">
266
  <div class="container mx-auto px-4 text-center">
267
  <p class="text-slate-500 text-sm">
268
  © 2023 QuantumCode AI Architect. All rights reserved.