soiz1 commited on
Commit
64c2103
·
verified ·
1 Parent(s): 39e67ca

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +451 -19
index.html CHANGED
@@ -1,19 +1,451 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>歌唱類似度評価システム</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/essentia.js.umd.min.js"></script>
8
+ <style>
9
+ body {
10
+ font-family: Arial, sans-serif;
11
+ max-width: 800px;
12
+ margin: 0 auto;
13
+ padding: 20px;
14
+ line-height: 1.6;
15
+ }
16
+ .container {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 20px;
20
+ }
21
+ .control-panel {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: 15px;
25
+ padding: 20px;
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
28
+ background-color: #f9f9f9;
29
+ }
30
+ button {
31
+ padding: 10px 15px;
32
+ background-color: #4CAF50;
33
+ color: white;
34
+ border: none;
35
+ border-radius: 4px;
36
+ cursor: pointer;
37
+ font-size: 16px;
38
+ }
39
+ button:disabled {
40
+ background-color: #cccccc;
41
+ cursor: not-allowed;
42
+ }
43
+ .mode-selector {
44
+ display: flex;
45
+ gap: 15px;
46
+ margin-bottom: 10px;
47
+ }
48
+ .mode-option {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 5px;
52
+ }
53
+ .result {
54
+ margin-top: 20px;
55
+ padding: 15px;
56
+ border: 1px solid #ddd;
57
+ border-radius: 8px;
58
+ background-color: #f0f8ff;
59
+ display: none;
60
+ }
61
+ .progress-bar {
62
+ width: 100%;
63
+ background-color: #e0e0e0;
64
+ border-radius: 4px;
65
+ margin: 10px 0;
66
+ }
67
+ .progress {
68
+ height: 20px;
69
+ border-radius: 4px;
70
+ background-color: #4CAF50;
71
+ width: 0%;
72
+ transition: width 0.3s;
73
+ }
74
+ .audio-container {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ gap: 20px;
78
+ margin-top: 20px;
79
+ }
80
+ .audio-box {
81
+ flex: 1;
82
+ padding: 10px;
83
+ border: 1px solid #ddd;
84
+ border-radius: 8px;
85
+ }
86
+ h2 {
87
+ margin-top: 0;
88
+ color: #333;
89
+ }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <div class="container">
94
+ <h1>歌唱類似度評価システム</h1>
95
+
96
+ <div class="control-panel">
97
+ <h2>手本音声選択</h2>
98
+ <select id="sampleSelect">
99
+ <option value="sample1.mp3">サンプル曲1</option>
100
+ <option value="sample2.mp3">サンプル曲2</option>
101
+ <option value="sample3.mp3">サンプル曲3</option>
102
+ </select>
103
+
104
+ <h2>録音モード選択</h2>
105
+ <div class="mode-selector">
106
+ <div class="mode-option">
107
+ <input type="radio" id="micMode" name="recordingMode" value="mic" checked>
108
+ <label for="micMode">マイクで録音</label>
109
+ </div>
110
+ <div class="mode-option">
111
+ <input type="radio" id="uploadMode" name="recordingMode" value="upload">
112
+ <label for="uploadMode">ファイルをアップロード</label>
113
+ </div>
114
+ </div>
115
+
116
+ <div id="uploadContainer" style="display: none;">
117
+ <input type="file" id="audioUpload" accept="audio/*">
118
+ </div>
119
+
120
+ <button id="startButton">スタート</button>
121
+ <button id="stopButton" disabled>ストップ</button>
122
+
123
+ <div class="progress-bar">
124
+ <div class="progress" id="progressBar"></div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="result" id="resultContainer">
129
+ <h2>結果</h2>
130
+ <p>類似度スコア: <span id="score">0</span>%</p>
131
+
132
+ <div class="audio-container">
133
+ <div class="audio-box">
134
+ <h3>手本音声</h3>
135
+ <audio controls id="referenceAudio"></audio>
136
+ </div>
137
+ <div class="audio-box">
138
+ <h3>あなたの歌唱</h3>
139
+ <audio controls id="userAudio"></audio>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <script>
146
+ // グローバル変数
147
+ let audioContext;
148
+ let mediaRecorder;
149
+ let recordedChunks = [];
150
+ let referenceAudio = document.getElementById('referenceAudio');
151
+ let startButton = document.getElementById('startButton');
152
+ let stopButton = document.getElementById('stopButton');
153
+ let progressBar = document.getElementById('progressBar');
154
+ let resultContainer = document.getElementById('resultContainer');
155
+ let scoreElement = document.getElementById('score');
156
+ let userAudio = document.getElementById('userAudio');
157
+ let sampleSelect = document.getElementById('sampleSelect');
158
+ let micModeRadio = document.getElementById('micMode');
159
+ let uploadModeRadio = document.getElementById('uploadMode');
160
+ let uploadContainer = document.getElementById('uploadContainer');
161
+ let audioUpload = document.getElementById('audioUpload');
162
+
163
+ // モード選択の切り替え
164
+ micModeRadio.addEventListener('change', () => {
165
+ uploadContainer.style.display = 'none';
166
+ });
167
+
168
+ uploadModeRadio.addEventListener('change', () => {
169
+ uploadContainer.style.display = 'block';
170
+ });
171
+
172
+ // スタートボタンの処理
173
+ startButton.addEventListener('click', async () => {
174
+ try {
175
+ startButton.disabled = true;
176
+ stopButton.disabled = false;
177
+ resultContainer.style.display = 'none';
178
+ progressBar.style.width = '0%';
179
+
180
+ // 手本音声の設定
181
+ const sampleFile = sampleSelect.value;
182
+ referenceAudio.src = sampleFile;
183
+
184
+ if (micModeRadio.checked) {
185
+ // マイク録音モード
186
+ await startMicrophoneRecording();
187
+ } else {
188
+ // ファイルアップロードモード
189
+ if (!audioUpload.files[0]) {
190
+ alert('音声ファイルを選択してください');
191
+ startButton.disabled = false;
192
+ return;
193
+ }
194
+ }
195
+
196
+ // 手本音声再生開始
197
+ referenceAudio.play();
198
+
199
+ // 再生進捗をプログレスバーで表示
200
+ referenceAudio.addEventListener('timeupdate', updateProgressBar);
201
+
202
+ // 手本音声終了時の処理
203
+ referenceAudio.addEventListener('ended', () => {
204
+ stopRecording();
205
+ });
206
+
207
+ } catch (error) {
208
+ console.error('Error:', error);
209
+ alert('エラーが発生しました: ' + error.message);
210
+ startButton.disabled = false;
211
+ stopButton.disabled = true;
212
+ }
213
+ });
214
+
215
+ // ストップボタンの処理
216
+ stopButton.addEventListener('click', () => {
217
+ stopRecording();
218
+ });
219
+
220
+ // マイク録音開始
221
+ async function startMicrophoneRecording() {
222
+ recordedChunks = [];
223
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
224
+
225
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
226
+ mediaRecorder = new MediaRecorder(stream);
227
+
228
+ mediaRecorder.ondataavailable = (event) => {
229
+ if (event.data.size > 0) {
230
+ recordedChunks.push(event.data);
231
+ }
232
+ };
233
+
234
+ mediaRecorder.start();
235
+ }
236
+
237
+ // 録音停止
238
+ function stopRecording() {
239
+ // プログレスバーの更新を停止
240
+ referenceAudio.removeEventListener('timeupdate', updateProgressBar);
241
+
242
+ if (micModeRadio.checked && mediaRecorder && mediaRecorder.state !== 'inactive') {
243
+ mediaRecorder.stop();
244
+ mediaRecorder.onstop = processRecordedAudio;
245
+ } else if (uploadModeRadio.checked) {
246
+ processUploadedAudio();
247
+ } else {
248
+ startButton.disabled = false;
249
+ stopButton.disabled = true;
250
+ }
251
+ }
252
+
253
+ // 録音データの処理
254
+ function processRecordedAudio() {
255
+ const audioBlob = new Blob(recordedChunks, { type: 'audio/wav' });
256
+ const audioUrl = URL.createObjectURL(audioBlob);
257
+ userAudio.src = audioUrl;
258
+
259
+ // 音声解析を実行
260
+ analyzeAudio(referenceAudio.src, audioBlob);
261
+ }
262
+
263
+ // アップロードファイルの処理
264
+ function processUploadedAudio() {
265
+ const file = audioUpload.files[0];
266
+ const audioUrl = URL.createObjectURL(file);
267
+ userAudio.src = audioUrl;
268
+
269
+ // 音声解析を実行
270
+ analyzeAudio(referenceAudio.src, file);
271
+ }
272
+
273
+ // プログレスバー更新
274
+ function updateProgressBar() {
275
+ const progress = (referenceAudio.currentTime / referenceAudio.duration) * 100;
276
+ progressBar.style.width = `${progress}%`;
277
+ }
278
+
279
+ async function analyzeAudio(referenceSrc, userAudioData) {
280
+ try {
281
+ // Essentia.jsの初期化
282
+ const Essentia = await essentiajs.Essentia();
283
+ const essentia = new Essentia(Essentia.FFTW); // FFTバックエンドを使用
284
+
285
+ // 参照音声とユーザー音声をデコード
286
+ const [referenceArrayBuffer, userArrayBuffer] = await Promise.all([
287
+ fetch(referenceSrc).then(res => res.arrayBuffer()),
288
+ readAudioData(userAudioData)
289
+ ]);
290
+
291
+ const referenceAudioBuffer = await decodeAudioData(audioContext, referenceArrayBuffer);
292
+ const userAudioBuffer = await decodeAudioData(audioContext, userArrayBuffer);
293
+
294
+ // モノラル化とサンプルレートの統一
295
+ const referenceMono = convertToMono(referenceAudioBuffer);
296
+ const userMono = convertToMono(userAudioBuffer);
297
+
298
+ // 特徴量抽出
299
+ const referenceFeatures = extractFeatures(essentia, referenceMono);
300
+ const userFeatures = extractFeatures(essentia, userMono);
301
+
302
+ // 特徴量の正規化
303
+ normalizeFeatures(referenceFeatures);
304
+ normalizeFeatures(userFeatures);
305
+
306
+ // 類似度計算
307
+ const similarityScore = calculateSimilarity(
308
+ referenceFeatures.mfcc.flat(),
309
+ userFeatures.mfcc.flat()
310
+ );
311
+
312
+ // スコア表示 (0-100に変換)
313
+ const displayScore = Math.min(Math.max(Math.round(similarityScore * 100), 0), 100);
314
+ scoreElement.textContent = displayScore;
315
+
316
+ // 結果表示
317
+ resultContainer.style.display = 'block';
318
+ startButton.disabled = false;
319
+ stopButton.disabled = true;
320
+
321
+ // Essentiaインスタンスのクリーンアップ
322
+ essentia.delete();
323
+
324
+ } catch (error) {
325
+ console.error('分析エラー:', error);
326
+ alert('音声分析中にエラーが発生しました: ' + error.message);
327
+ startButton.disabled = false;
328
+ stopButton.disabled = true;
329
+ }
330
+ }
331
+
332
+ // 補助関数群
333
+ async function readAudioData(audioData) {
334
+ if (audioData instanceof Blob) {
335
+ return new Response(audioData).arrayBuffer();
336
+ }
337
+ return audioData;
338
+ }
339
+
340
+ async function decodeAudioData(context, arrayBuffer) {
341
+ return new Promise((resolve, reject) => {
342
+ context.decodeAudioData(arrayBuffer, resolve, reject);
343
+ });
344
+ }
345
+
346
+ function convertToMono(audioBuffer) {
347
+ if (audioBuffer.numberOfChannels === 1) return audioBuffer.getChannelData(0);
348
+
349
+ const left = audioBuffer.getChannelData(0);
350
+ const right = audioBuffer.numberOfChannels > 1 ?
351
+ audioBuffer.getChannelData(1) : left;
352
+ const mono = new Float32Array(left.length);
353
+
354
+ for (let i = 0; i < left.length; i++) {
355
+ mono[i] = (left[i] + right[i]) / 2;
356
+ }
357
+ return mono;
358
+ }
359
+
360
+ function extractFeatures(essentia, audioData) {
361
+ // パラメータ設定
362
+ const frameSize = 2048;
363
+ const hopSize = 1024;
364
+ const sampleRate = 44100;
365
+
366
+ // フレームごとに特徴量を抽出
367
+ const frames = essentia.FrameGenerator(audioData, frameSize, hopSize);
368
+
369
+ const features = {
370
+ mfcc: [],
371
+ spectral: {
372
+ centroid: [],
373
+ rolloff: [],
374
+ flux: []
375
+ }
376
+ };
377
+
378
+ for (let i = 0; i < frames.size(); i++) {
379
+ const frame = frames.get(i);
380
+
381
+ // MFCC (メル周波数ケプストラム係数)
382
+ const mfcc = essentia.MFCC(
383
+ essentia.Spectrum(essentia.Windowing(frame, 'hann', frameSize, false)).bands;
384
+ features.mfcc.push(mfcc);
385
+
386
+ // スペクトル特徴量
387
+ const spectrum = essentia.Spectrum(essentia.Windowing(frame, 'hann', frameSize, false));
388
+ features.spectral.centroid.push(
389
+ essentia.SpectralCentroid(spectrum).centroid
390
+ );
391
+ features.spectral.rolloff.push(
392
+ essentia.SpectralRollOff(spectrum, 0.85).rollOff
393
+ );
394
+ features.spectral.flux.push(
395
+ essentia.Flux(spectrum).flux
396
+ );
397
+
398
+ frame.delete();
399
+ }
400
+
401
+ frames.delete();
402
+ return features;
403
+ }
404
+
405
+ function normalizeFeatures(features) {
406
+ // MFCCの正規化 (0-1範囲に)
407
+ const mfccFlat = features.mfcc.flat();
408
+ const min = Math.min(...mfccFlat);
409
+ const max = Math.max(...mfccFlat);
410
+
411
+ for (let i = 0; i < features.mfcc.length; i++) {
412
+ for (let j = 0; j < features.mfcc[i].length; j++) {
413
+ features.mfcc[i][j] = (features.mfcc[i][j] - min) / (max - min);
414
+ }
415
+ }
416
+
417
+ // スペクトル特徴量の正規化
418
+ ['centroid', 'rolloff', 'flux'].forEach(key => {
419
+ const arr = features.spectral[key];
420
+ const min = Math.min(...arr);
421
+ const max = Math.max(...arr);
422
+
423
+ for (let i = 0; i < arr.length; i++) {
424
+ arr[i] = (arr[i] - min) / (max - min);
425
+ }
426
+ });
427
+ }
428
+
429
+ function calculateSimilarity(vec1, vec2) {
430
+ // コサイン類似度計算
431
+ if (vec1.length !== vec2.length || vec1.length === 0) return 0;
432
+
433
+ let dotProduct = 0;
434
+ let norm1 = 0;
435
+ let norm2 = 0;
436
+
437
+ for (let i = 0; i < vec1.length; i++) {
438
+ dotProduct += vec1[i] * vec2[i];
439
+ norm1 += vec1[i] * vec1[i];
440
+ norm2 += vec2[i] * vec2[i];
441
+ }
442
+
443
+ norm1 = Math.sqrt(norm1);
444
+ norm2 = Math.sqrt(norm2);
445
+
446
+ if (norm1 === 0 || norm2 === 0) return 0;
447
+ return dotProduct / (norm1 * norm2);
448
+ }
449
+ </script>
450
+ </body>
451
+ </html>