matsuap commited on
Commit
0ebb77f
·
verified ·
1 Parent(s): 9689a37

Update src/web/live_transcription.html

Browse files
Files changed (1) hide show
  1. src/web/live_transcription.html +300 -300
src/web/live_transcription.html CHANGED
@@ -1,301 +1,301 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Audio Transcription</title>
7
- <style>
8
- body {
9
- font-family: 'Inter', sans-serif;
10
- margin: 20px;
11
- text-align: center;
12
- }
13
- #recordButton {
14
- width: 80px;
15
- height: 80px;
16
- font-size: 36px;
17
- border: none;
18
- border-radius: 50%;
19
- background-color: white;
20
- cursor: pointer;
21
- box-shadow: 0 0px 10px rgba(0, 0, 0, 0.2);
22
- transition: background-color 0.3s ease, transform 0.2s ease;
23
- }
24
- #recordButton.recording {
25
- background-color: #ff4d4d;
26
- color: white;
27
- }
28
- #recordButton:active {
29
- transform: scale(0.95);
30
- }
31
- #status {
32
- margin-top: 20px;
33
- font-size: 16px;
34
- color: #333;
35
- }
36
- .settings-container {
37
- display: flex;
38
- justify-content: center;
39
- align-items: center;
40
- gap: 15px;
41
- margin-top: 20px;
42
- }
43
- .settings {
44
- display: flex;
45
- flex-direction: column;
46
- align-items: flex-start;
47
- gap: 5px;
48
- }
49
- #chunkSelector,
50
- #websocketInput {
51
- font-size: 16px;
52
- padding: 5px;
53
- border-radius: 5px;
54
- border: 1px solid #ddd;
55
- background-color: #f9f9f9;
56
- }
57
- #websocketInput {
58
- width: 200px;
59
- }
60
- #chunkSelector:focus,
61
- #websocketInput:focus {
62
- outline: none;
63
- border-color: #007bff;
64
- }
65
- label {
66
- font-size: 14px;
67
- }
68
- /* Speaker-labeled transcript area */
69
- #linesTranscript {
70
- margin: 20px auto;
71
- max-width: 600px;
72
- text-align: left;
73
- font-size: 16px;
74
- }
75
- #linesTranscript p {
76
- margin: 5px 0;
77
- }
78
- #linesTranscript strong {
79
- color: #333;
80
- }
81
- /* Grey buffer styling */
82
- .buffer {
83
- color: rgb(180, 180, 180);
84
- font-style: italic;
85
- margin-left: 4px;
86
- }
87
- </style>
88
- </head>
89
- <body>
90
-
91
- <div class="settings-container">
92
- <button id="recordButton">🎙️</button>
93
- <div class="settings">
94
- <div>
95
- <label for="chunkSelector">Chunk size (ms):</label>
96
- <select id="chunkSelector">
97
- <option value="500" selected>500 ms</option>
98
- <option value="1000">1000 ms</option>
99
- <option value="2000">2000 ms</option>
100
- <option value="3000">3000 ms</option>
101
- <option value="4000">4000 ms</option>
102
- <option value="5000">5000 ms</option>
103
- </select>
104
- </div>
105
- <div>
106
- <label for="websocketInput">WebSocket URL:</label>
107
- <input id="websocketInput" type="text" value="ws://localhost:8000/asr" />
108
- </div>
109
- </div>
110
- </div>
111
-
112
- <p id="status"></p>
113
-
114
- <!-- Speaker-labeled transcript -->
115
- <div id="linesTranscript"></div>
116
-
117
- <script>
118
- let isRecording = false;
119
- let websocket = null;
120
- let recorder = null;
121
- let chunkDuration = 500;
122
- let websocketUrl = "ws://localhost:8000/asr";
123
- let userClosing = false;
124
- let lines = []; // フロント側でlinesを保持
125
- let audioQueue = []; // 再生待ちのaudio_urlを保持
126
- let isPlayingAudio = false; // オーディオ再生中かどうかを示すフラグ
127
- let currentAudio = null; // 現在再生中のオーディオを保持
128
-
129
- const statusText = document.getElementById("status");
130
- const recordButton = document.getElementById("recordButton");
131
- const chunkSelector = document.getElementById("chunkSelector");
132
- const websocketInput = document.getElementById("websocketInput");
133
- const linesTranscriptDiv = document.getElementById("linesTranscript");
134
-
135
- chunkSelector.addEventListener("change", () => {
136
- chunkDuration = parseInt(chunkSelector.value);
137
- });
138
-
139
- websocketInput.addEventListener("change", () => {
140
- const urlValue = websocketInput.value.trim();
141
- if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) {
142
- statusText.textContent = "Invalid WebSocket URL (must start with ws:// or wss://)";
143
- return;
144
- }
145
- websocketUrl = urlValue;
146
- statusText.textContent = "WebSocket URL updated. Ready to connect.";
147
- });
148
-
149
- function setupWebSocket() {
150
- return new Promise((resolve, reject) => {
151
- try {
152
- websocket = new WebSocket(websocketUrl);
153
- } catch (error) {
154
- statusText.textContent = "Invalid WebSocket URL. Please check and try again.";
155
- reject(error);
156
- return;
157
- }
158
-
159
- websocket.onopen = () => {
160
- statusText.textContent = "Connected to server.";
161
- resolve();
162
- };
163
-
164
- websocket.onclose = () => {
165
- if (userClosing) {
166
- statusText.textContent = "WebSocket closed by user.";
167
- } else {
168
- statusText.textContent =
169
- "Disconnected from the WebSocket server. (Check logs if model is loading.)";
170
- }
171
- userClosing = false;
172
- };
173
-
174
- websocket.onerror = () => {
175
- statusText.textContent = "Error connecting to WebSocket.";
176
- reject(new Error("Error connecting to WebSocket"));
177
- };
178
-
179
- // Handle messages from server
180
- websocket.onmessage = (event) => {
181
- const data = JSON.parse(event.data);
182
- /*
183
- The server might send:
184
- {
185
- "line": {"speaker": 0, "text": "Hello."},
186
- "buffer": "...",
187
- "audio_url": "https://example.com/audio.wav"
188
- }
189
- */
190
- const { line = "", buffer = "" } = data;
191
- if (line) {
192
- lines.push(line); // 新しいlineをスタック
193
- renderLinesWithBuffer(lines, buffer);
194
- }
195
- if (line.audio_url) {
196
- audioQueue.push(line.audio_url);
197
- if (!isPlayingAudio) {
198
- playNextAudio();
199
- }
200
- }
201
- };
202
- });
203
- }
204
-
205
- function renderLinesWithBuffer(lines, buffer) {
206
- // Build the HTML
207
- // The buffer is appended to the last line if it's non-empty
208
- const linesHtml = lines.map((item, idx) => {
209
- let textContent = item.text;
210
- if (idx === lines.length - 1 && buffer) {
211
- textContent += `<span class="buffer">${buffer}</span>`;
212
- }
213
- return `<p><strong>Speaker ${item.speaker}:</strong> ${textContent}</p>`;
214
- }).join("");
215
-
216
- linesTranscriptDiv.innerHTML = linesHtml;
217
- }
218
-
219
- function playNextAudio() {
220
- if (audioQueue.length === 0) {
221
- isPlayingAudio = false;
222
- return;
223
- }
224
- isPlayingAudio = true;
225
- const url = audioQueue.shift(); // ここでshiftして次のオーディオを取得
226
- currentAudio = new Audio(url);
227
- currentAudio.onended = () => {
228
- playNextAudio(); // 再生が終了したときに次のオーディオを再生
229
- };
230
- currentAudio.onerror = (err) => {
231
- console.error("Error playing audio:", err);
232
- playNextAudio(); // エラーが発生した場合も次のオーディオを再生
233
- };
234
- currentAudio.play().catch(err => {
235
- console.error("Error playing audio:", err);
236
- playNextAudio(); // 再生エラーが発生した場合も次のオーディオを再生
237
- });
238
- }
239
-
240
- async function startRecording() {
241
- try {
242
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
243
- recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
244
- recorder.ondataavailable = (e) => {
245
- if (websocket && websocket.readyState === WebSocket.OPEN) {
246
- websocket.send(e.data);
247
- }
248
- };
249
- recorder.start(chunkDuration);
250
- isRecording = true;
251
- updateUI();
252
- } catch (err) {
253
- statusText.textContent = "Error accessing microphone. Please allow microphone access.";
254
- }
255
- }
256
-
257
- function stopRecording() {
258
- userClosing = true;
259
- if (recorder) {
260
- recorder.stop();
261
- recorder = null;
262
- }
263
- isRecording = false;
264
-
265
- if (websocket) {
266
- websocket.close();
267
- websocket = null;
268
- }
269
-
270
- if (currentAudio) {
271
- currentAudio.pause();
272
- currentAudio = null;
273
- }
274
-
275
- updateUI();
276
- }
277
-
278
- async function toggleRecording() {
279
- if (!isRecording) {
280
- linesTranscriptDiv.innerHTML = "";
281
- lines = []; // 録音開始時にlinesをクリア
282
- try {
283
- await setupWebSocket();
284
- await startRecording();
285
- } catch (err) {
286
- statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
287
- }
288
- } else {
289
- stopRecording();
290
- }
291
- }
292
-
293
- function updateUI() {
294
- recordButton.classList.toggle("recording", isRecording);
295
- statusText.textContent = isRecording ? "Recording..." : "Click to start transcription";
296
- }
297
-
298
- recordButton.addEventListener("click", toggleRecording);
299
- </script>
300
- </body>
301
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Audio Transcription</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Inter', sans-serif;
10
+ margin: 20px;
11
+ text-align: center;
12
+ }
13
+ #recordButton {
14
+ width: 80px;
15
+ height: 80px;
16
+ font-size: 36px;
17
+ border: none;
18
+ border-radius: 50%;
19
+ background-color: white;
20
+ cursor: pointer;
21
+ box-shadow: 0 0px 10px rgba(0, 0, 0, 0.2);
22
+ transition: background-color 0.3s ease, transform 0.2s ease;
23
+ }
24
+ #recordButton.recording {
25
+ background-color: #ff4d4d;
26
+ color: white;
27
+ }
28
+ #recordButton:active {
29
+ transform: scale(0.95);
30
+ }
31
+ #status {
32
+ margin-top: 20px;
33
+ font-size: 16px;
34
+ color: #333;
35
+ }
36
+ .settings-container {
37
+ display: flex;
38
+ justify-content: center;
39
+ align-items: center;
40
+ gap: 15px;
41
+ margin-top: 20px;
42
+ }
43
+ .settings {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: flex-start;
47
+ gap: 5px;
48
+ }
49
+ #chunkSelector,
50
+ #websocketInput {
51
+ font-size: 16px;
52
+ padding: 5px;
53
+ border-radius: 5px;
54
+ border: 1px solid #ddd;
55
+ background-color: #f9f9f9;
56
+ }
57
+ #websocketInput {
58
+ width: 200px;
59
+ }
60
+ #chunkSelector:focus,
61
+ #websocketInput:focus {
62
+ outline: none;
63
+ border-color: #007bff;
64
+ }
65
+ label {
66
+ font-size: 14px;
67
+ }
68
+ /* Speaker-labeled transcript area */
69
+ #linesTranscript {
70
+ margin: 20px auto;
71
+ max-width: 600px;
72
+ text-align: left;
73
+ font-size: 16px;
74
+ }
75
+ #linesTranscript p {
76
+ margin: 5px 0;
77
+ }
78
+ #linesTranscript strong {
79
+ color: #333;
80
+ }
81
+ /* Grey buffer styling */
82
+ .buffer {
83
+ color: rgb(180, 180, 180);
84
+ font-style: italic;
85
+ margin-left: 4px;
86
+ }
87
+ </style>
88
+ </head>
89
+ <body>
90
+
91
+ <div class="settings-container">
92
+ <button id="recordButton">🎙️</button>
93
+ <div class="settings">
94
+ <div>
95
+ <label for="chunkSelector">Chunk size (ms):</label>
96
+ <select id="chunkSelector">
97
+ <option value="500" selected>500 ms</option>
98
+ <option value="1000">1000 ms</option>
99
+ <option value="2000">2000 ms</option>
100
+ <option value="3000">3000 ms</option>
101
+ <option value="4000">4000 ms</option>
102
+ <option value="5000">5000 ms</option>
103
+ </select>
104
+ </div>
105
+ <div>
106
+ <label for="websocketInput">WebSocket URL:</label>
107
+ <input id="websocketInput" type="text" value="wss://atpeak-realtime-stt-translation.hf.space/asr" />
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <p id="status"></p>
113
+
114
+ <!-- Speaker-labeled transcript -->
115
+ <div id="linesTranscript"></div>
116
+
117
+ <script>
118
+ let isRecording = false;
119
+ let websocket = null;
120
+ let recorder = null;
121
+ let chunkDuration = 500;
122
+ let websocketUrl = "ws://localhost:8000/asr";
123
+ let userClosing = false;
124
+ let lines = []; // フロント側でlinesを保持
125
+ let audioQueue = []; // 再生待ちのaudio_urlを保持
126
+ let isPlayingAudio = false; // オーディオ再生中かどうかを示すフラグ
127
+ let currentAudio = null; // 現在再生中のオーディオを保持
128
+
129
+ const statusText = document.getElementById("status");
130
+ const recordButton = document.getElementById("recordButton");
131
+ const chunkSelector = document.getElementById("chunkSelector");
132
+ const websocketInput = document.getElementById("websocketInput");
133
+ const linesTranscriptDiv = document.getElementById("linesTranscript");
134
+
135
+ chunkSelector.addEventListener("change", () => {
136
+ chunkDuration = parseInt(chunkSelector.value);
137
+ });
138
+
139
+ websocketInput.addEventListener("change", () => {
140
+ const urlValue = websocketInput.value.trim();
141
+ if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) {
142
+ statusText.textContent = "Invalid WebSocket URL (must start with ws:// or wss://)";
143
+ return;
144
+ }
145
+ websocketUrl = urlValue;
146
+ statusText.textContent = "WebSocket URL updated. Ready to connect.";
147
+ });
148
+
149
+ function setupWebSocket() {
150
+ return new Promise((resolve, reject) => {
151
+ try {
152
+ websocket = new WebSocket(websocketUrl);
153
+ } catch (error) {
154
+ statusText.textContent = "Invalid WebSocket URL. Please check and try again.";
155
+ reject(error);
156
+ return;
157
+ }
158
+
159
+ websocket.onopen = () => {
160
+ statusText.textContent = "Connected to server.";
161
+ resolve();
162
+ };
163
+
164
+ websocket.onclose = () => {
165
+ if (userClosing) {
166
+ statusText.textContent = "WebSocket closed by user.";
167
+ } else {
168
+ statusText.textContent =
169
+ "Disconnected from the WebSocket server. (Check logs if model is loading.)";
170
+ }
171
+ userClosing = false;
172
+ };
173
+
174
+ websocket.onerror = () => {
175
+ statusText.textContent = "Error connecting to WebSocket.";
176
+ reject(new Error("Error connecting to WebSocket"));
177
+ };
178
+
179
+ // Handle messages from server
180
+ websocket.onmessage = (event) => {
181
+ const data = JSON.parse(event.data);
182
+ /*
183
+ The server might send:
184
+ {
185
+ "line": {"speaker": 0, "text": "Hello."},
186
+ "buffer": "...",
187
+ "audio_url": "https://example.com/audio.wav"
188
+ }
189
+ */
190
+ const { line = "", buffer = "" } = data;
191
+ if (line) {
192
+ lines.push(line); // 新しいlineをスタック
193
+ renderLinesWithBuffer(lines, buffer);
194
+ }
195
+ if (line.audio_url) {
196
+ audioQueue.push(line.audio_url);
197
+ if (!isPlayingAudio) {
198
+ playNextAudio();
199
+ }
200
+ }
201
+ };
202
+ });
203
+ }
204
+
205
+ function renderLinesWithBuffer(lines, buffer) {
206
+ // Build the HTML
207
+ // The buffer is appended to the last line if it's non-empty
208
+ const linesHtml = lines.map((item, idx) => {
209
+ let textContent = item.text;
210
+ if (idx === lines.length - 1 && buffer) {
211
+ textContent += `<span class="buffer">${buffer}</span>`;
212
+ }
213
+ return `<p><strong>Speaker ${item.speaker}:</strong> ${textContent}</p>`;
214
+ }).join("");
215
+
216
+ linesTranscriptDiv.innerHTML = linesHtml;
217
+ }
218
+
219
+ function playNextAudio() {
220
+ if (audioQueue.length === 0) {
221
+ isPlayingAudio = false;
222
+ return;
223
+ }
224
+ isPlayingAudio = true;
225
+ const url = audioQueue.shift(); // ここでshiftして次のオーディオを取得
226
+ currentAudio = new Audio(url);
227
+ currentAudio.onended = () => {
228
+ playNextAudio(); // 再生が終了したときに次のオーディオを再生
229
+ };
230
+ currentAudio.onerror = (err) => {
231
+ console.error("Error playing audio:", err);
232
+ playNextAudio(); // エラーが発生した場合も次のオーディオを再生
233
+ };
234
+ currentAudio.play().catch(err => {
235
+ console.error("Error playing audio:", err);
236
+ playNextAudio(); // 再生エラーが発生した場合も次のオーディオを再生
237
+ });
238
+ }
239
+
240
+ async function startRecording() {
241
+ try {
242
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
243
+ recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
244
+ recorder.ondataavailable = (e) => {
245
+ if (websocket && websocket.readyState === WebSocket.OPEN) {
246
+ websocket.send(e.data);
247
+ }
248
+ };
249
+ recorder.start(chunkDuration);
250
+ isRecording = true;
251
+ updateUI();
252
+ } catch (err) {
253
+ statusText.textContent = "Error accessing microphone. Please allow microphone access.";
254
+ }
255
+ }
256
+
257
+ function stopRecording() {
258
+ userClosing = true;
259
+ if (recorder) {
260
+ recorder.stop();
261
+ recorder = null;
262
+ }
263
+ isRecording = false;
264
+
265
+ if (websocket) {
266
+ websocket.close();
267
+ websocket = null;
268
+ }
269
+
270
+ if (currentAudio) {
271
+ currentAudio.pause();
272
+ currentAudio = null;
273
+ }
274
+
275
+ updateUI();
276
+ }
277
+
278
+ async function toggleRecording() {
279
+ if (!isRecording) {
280
+ linesTranscriptDiv.innerHTML = "";
281
+ lines = []; // 録音開始時にlinesをクリア
282
+ try {
283
+ await setupWebSocket();
284
+ await startRecording();
285
+ } catch (err) {
286
+ statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
287
+ }
288
+ } else {
289
+ stopRecording();
290
+ }
291
+ }
292
+
293
+ function updateUI() {
294
+ recordButton.classList.toggle("recording", isRecording);
295
+ statusText.textContent = isRecording ? "Recording..." : "Click to start transcription";
296
+ }
297
+
298
+ recordButton.addEventListener("click", toggleRecording);
299
+ </script>
300
+ </body>
301
  </html>