

throw errors if websocket connection fails
@6d3d6ca132bfdfddf4910e1b6f950d01203a2165
--- src/live_transcription.html
+++ src/live_transcription.html
... | ... | @@ -1,178 +1,240 @@ |
1 | 1 |
<!DOCTYPE html> |
2 | 2 |
<html lang="en"> |
3 | 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 |
- text-align: center; |
|
11 |
- margin: 20px; |
|
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 |
- #transcriptions { |
|
32 |
- margin-top: 20px; |
|
33 |
- font-size: 18px; |
|
34 |
- text-align: left; |
|
35 |
- } |
|
36 |
- .transcription { |
|
37 |
- display: inline; |
|
38 |
- color: black; |
|
39 |
- } |
|
40 |
- .buffer { |
|
41 |
- display: inline; |
|
42 |
- color: rgb(197, 197, 197); |
|
43 |
- } |
|
44 |
- .settings-container { |
|
45 |
- display: flex; |
|
46 |
- justify-content: center; |
|
47 |
- align-items: center; |
|
48 |
- gap: 15px; |
|
49 |
- margin-top: 20px; |
|
50 |
- } |
|
51 |
- .settings { |
|
52 |
- display: flex; |
|
53 |
- flex-direction: column; |
|
54 |
- align-items: flex-start; |
|
55 |
- gap: 5px; |
|
56 |
- } |
|
57 |
- #chunkSelector, #websocketInput { |
|
58 |
- font-size: 16px; |
|
59 |
- padding: 5px; |
|
60 |
- border-radius: 5px; |
|
61 |
- border: 1px solid #ddd; |
|
62 |
- background-color: #f9f9f9; |
|
63 |
- } |
|
64 |
- #websocketInput { |
|
65 |
- width: 200px; |
|
66 |
- } |
|
67 |
- #chunkSelector:focus, #websocketInput:focus { |
|
68 |
- outline: none; |
|
69 |
- border-color: #007bff; |
|
70 |
- } |
|
71 |
- label { |
|
72 |
- font-size: 14px; |
|
73 |
- } |
|
74 |
- </style> |
|
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 |
+ text-align: center; |
|
11 |
+ margin: 20px; |
|
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 |
+ #transcriptions { |
|
32 |
+ margin-top: 20px; |
|
33 |
+ font-size: 18px; |
|
34 |
+ text-align: left; |
|
35 |
+ } |
|
36 |
+ .transcription { |
|
37 |
+ display: inline; |
|
38 |
+ color: black; |
|
39 |
+ } |
|
40 |
+ .buffer { |
|
41 |
+ display: inline; |
|
42 |
+ color: rgb(197, 197, 197); |
|
43 |
+ } |
|
44 |
+ .settings-container { |
|
45 |
+ display: flex; |
|
46 |
+ justify-content: center; |
|
47 |
+ align-items: center; |
|
48 |
+ gap: 15px; |
|
49 |
+ margin-top: 20px; |
|
50 |
+ } |
|
51 |
+ .settings { |
|
52 |
+ display: flex; |
|
53 |
+ flex-direction: column; |
|
54 |
+ align-items: flex-start; |
|
55 |
+ gap: 5px; |
|
56 |
+ } |
|
57 |
+ #chunkSelector, |
|
58 |
+ #websocketInput { |
|
59 |
+ font-size: 16px; |
|
60 |
+ padding: 5px; |
|
61 |
+ border-radius: 5px; |
|
62 |
+ border: 1px solid #ddd; |
|
63 |
+ background-color: #f9f9f9; |
|
64 |
+ } |
|
65 |
+ #websocketInput { |
|
66 |
+ width: 200px; |
|
67 |
+ } |
|
68 |
+ #chunkSelector:focus, |
|
69 |
+ #websocketInput:focus { |
|
70 |
+ outline: none; |
|
71 |
+ border-color: #007bff; |
|
72 |
+ } |
|
73 |
+ label { |
|
74 |
+ font-size: 14px; |
|
75 |
+ } |
|
76 |
+ </style> |
|
75 | 77 |
</head> |
76 | 78 |
<body> |
77 |
- <div class="settings-container"> |
|
78 |
- <button id="recordButton">🎙�</button> |
|
79 |
- <div class="settings"> |
|
80 |
- <div> |
|
81 |
- <label for="chunkSelector">Chunk size (ms):</label> |
|
82 |
- <select id="chunkSelector"> |
|
83 |
- <option value="500">500 ms</option> |
|
84 |
- <option value="1000" selected>1000 ms</option> |
|
85 |
- <option value="2000">2000 ms</option> |
|
86 |
- <option value="3000">3000 ms</option> |
|
87 |
- <option value="4000">4000 ms</option> |
|
88 |
- <option value="5000">5000 ms</option> |
|
89 |
- </select> |
|
90 |
- </div> |
|
91 |
- <div> |
|
92 |
- <label for="websocketInput">WebSocket URL:</label> |
|
93 |
- <input id="websocketInput" type="text" value="ws://localhost:8000/ws" /> |
|
94 |
- </div> |
|
95 |
- </div> |
|
79 |
+ <div class="settings-container"> |
|
80 |
+ <button id="recordButton">🎙�</button> |
|
81 |
+ <div class="settings"> |
|
82 |
+ <div> |
|
83 |
+ <label for="chunkSelector">Chunk size (ms):</label> |
|
84 |
+ <select id="chunkSelector"> |
|
85 |
+ <option value="500">500 ms</option> |
|
86 |
+ <option value="1000" selected>1000 ms</option> |
|
87 |
+ <option value="2000">2000 ms</option> |
|
88 |
+ <option value="3000">3000 ms</option> |
|
89 |
+ <option value="4000">4000 ms</option> |
|
90 |
+ <option value="5000">5000 ms</option> |
|
91 |
+ </select> |
|
92 |
+ </div> |
|
93 |
+ <div> |
|
94 |
+ <label for="websocketInput">WebSocket URL:</label> |
|
95 |
+ <input id="websocketInput" type="text" value="ws://localhost:8000/ws" /> |
|
96 |
+ </div> |
|
96 | 97 |
</div> |
97 |
- <p id="status"></p> |
|
98 |
+ </div> |
|
99 |
+ <p id="status"></p> |
|
98 | 100 |
|
99 |
- <div id="transcriptions"></div> |
|
101 |
+ <div id="transcriptions"></div> |
|
100 | 102 |
|
101 |
- <script> |
|
102 |
- let isRecording = false, websocket, recorder, chunkDuration = 1000, websocketUrl = "ws://localhost:8000/ws"; |
|
103 |
+ <script> |
|
104 |
+ let isRecording = false, |
|
105 |
+ websocket, |
|
106 |
+ recorder, |
|
107 |
+ chunkDuration = 1000, |
|
108 |
+ websocketUrl = "ws://localhost:8000/ws"; |
|
103 | 109 |
|
104 |
- const statusText = document.getElementById("status"); |
|
105 |
- const recordButton = document.getElementById("recordButton"); |
|
106 |
- const chunkSelector = document.getElementById("chunkSelector"); |
|
107 |
- const websocketInput = document.getElementById("websocketInput"); |
|
108 |
- const transcriptionsDiv = document.getElementById("transcriptions"); |
|
110 |
+ // Tracks whether the user voluntarily closed the WebSocket |
|
111 |
+ let userClosing = false; |
|
109 | 112 |
|
110 |
- let fullTranscription = ""; // Store confirmed transcription |
|
113 |
+ const statusText = document.getElementById("status"); |
|
114 |
+ const recordButton = document.getElementById("recordButton"); |
|
115 |
+ const chunkSelector = document.getElementById("chunkSelector"); |
|
116 |
+ const websocketInput = document.getElementById("websocketInput"); |
|
117 |
+ const transcriptionsDiv = document.getElementById("transcriptions"); |
|
111 | 118 |
|
112 |
- // Update chunk duration based on the selector |
|
113 |
- chunkSelector.addEventListener("change", () => { |
|
114 |
- chunkDuration = parseInt(chunkSelector.value); |
|
115 |
- }); |
|
119 |
+ let fullTranscription = ""; // Store confirmed transcription |
|
116 | 120 |
|
117 |
- // Update WebSocket URL dynamically |
|
118 |
- websocketInput.addEventListener("change", () => { |
|
119 |
- websocketUrl = websocketInput.value; |
|
120 |
- }); |
|
121 |
+ // Update chunk duration based on the selector |
|
122 |
+ chunkSelector.addEventListener("change", () => { |
|
123 |
+ chunkDuration = parseInt(chunkSelector.value); |
|
124 |
+ }); |
|
121 | 125 |
|
122 |
- function setupWebSocket() { |
|
123 |
- websocket = new WebSocket(websocketUrl); |
|
124 |
- websocket.onmessage = (event) => { |
|
125 |
- const data = JSON.parse(event.data); |
|
126 |
- const { transcription, buffer } = data; |
|
126 |
+ // Update WebSocket URL dynamically, with some basic checks |
|
127 |
+ websocketInput.addEventListener("change", () => { |
|
128 |
+ const urlValue = websocketInput.value.trim(); |
|
127 | 129 |
|
128 |
- // Update confirmed transcription |
|
129 |
- fullTranscription += transcription; |
|
130 |
+ // Quick check to see if it starts with ws:// or wss:// |
|
131 |
+ if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) { |
|
132 |
+ statusText.textContent = |
|
133 |
+ "Invalid WebSocket URL. It should start with ws:// or wss://"; |
|
134 |
+ return; |
|
135 |
+ } |
|
136 |
+ websocketUrl = urlValue; |
|
137 |
+ statusText.textContent = "WebSocket URL updated. Ready to connect."; |
|
138 |
+ }); |
|
130 | 139 |
|
131 |
- // Update the transcription display |
|
132 |
- transcriptionsDiv.innerHTML = ` |
|
133 |
- <span class="transcription">${fullTranscription}</span> |
|
134 |
- <span class="buffer">${buffer}</span> |
|
135 |
- `; |
|
136 |
- }; |
|
140 |
+ function setupWebSocket() { |
|
141 |
+ try { |
|
142 |
+ websocket = new WebSocket(websocketUrl); |
|
143 |
+ } catch (error) { |
|
144 |
+ statusText.textContent = |
|
145 |
+ "Invalid WebSocket URL. Please check the URL and try again."; |
|
146 |
+ throw error; |
|
147 |
+ } |
|
137 | 148 |
|
138 |
- websocket.onerror = () => { |
|
139 |
- statusText.textContent = "Error connecting to WebSocket"; |
|
140 |
- stopRecording(); // Stop recording if WebSocket fails |
|
141 |
- }; |
|
149 |
+ websocket.onopen = () => { |
|
150 |
+ statusText.textContent = "Connected to server"; |
|
151 |
+ }; |
|
152 |
+ |
|
153 |
+ websocket.onclose = (event) => { |
|
154 |
+ if (userClosing) { |
|
155 |
+ statusText.textContent = "WebSocket closed by user."; |
|
156 |
+ } else { |
|
157 |
+ statusText.textContent = "Disconnected from server (unexpected)."; |
|
158 |
+ } |
|
159 |
+ userClosing = false; |
|
160 |
+ }; |
|
161 |
+ |
|
162 |
+ websocket.onerror = () => { |
|
163 |
+ statusText.textContent = "Error connecting to WebSocket"; |
|
164 |
+ stopRecording(); |
|
165 |
+ }; |
|
166 |
+ |
|
167 |
+ websocket.onmessage = (event) => { |
|
168 |
+ const data = JSON.parse(event.data); |
|
169 |
+ const { transcription, buffer } = data; |
|
170 |
+ |
|
171 |
+ // Update confirmed transcription |
|
172 |
+ fullTranscription += transcription; |
|
173 |
+ |
|
174 |
+ // Update the transcription display |
|
175 |
+ transcriptionsDiv.innerHTML = ` |
|
176 |
+ <span class="transcription">${fullTranscription}</span> |
|
177 |
+ <span class="buffer">${buffer}</span> |
|
178 |
+ `; |
|
179 |
+ }; |
|
180 |
+ } |
|
181 |
+ |
|
182 |
+ async function startRecording() { |
|
183 |
+ try { |
|
184 |
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
185 |
+ recorder = new MediaRecorder(stream, { mimeType: "audio/webm" }); |
|
186 |
+ recorder.ondataavailable = (e) => { |
|
187 |
+ if (websocket && websocket.readyState === WebSocket.OPEN) { |
|
188 |
+ websocket.send(e.data); |
|
189 |
+ } |
|
190 |
+ }; |
|
191 |
+ recorder.start(chunkDuration); |
|
192 |
+ isRecording = true; |
|
193 |
+ updateUI(); |
|
194 |
+ } catch (err) { |
|
195 |
+ statusText.textContent = |
|
196 |
+ "Error accessing microphone. Please allow microphone access."; |
|
197 |
+ } |
|
198 |
+ } |
|
199 |
+ |
|
200 |
+ function stopRecording() { |
|
201 |
+ userClosing = true; |
|
202 |
+ |
|
203 |
+ recorder?.stop(); |
|
204 |
+ recorder = null; |
|
205 |
+ isRecording = false; |
|
206 |
+ |
|
207 |
+ websocket?.close(); |
|
208 |
+ websocket = null; |
|
209 |
+ |
|
210 |
+ updateUI(); |
|
211 |
+ } |
|
212 |
+ |
|
213 |
+ async function toggleRecording() { |
|
214 |
+ if (!isRecording) { |
|
215 |
+ fullTranscription = ""; |
|
216 |
+ transcriptionsDiv.innerHTML = ""; |
|
217 |
+ |
|
218 |
+ try { |
|
219 |
+ setupWebSocket(); |
|
220 |
+ } catch (err) { |
|
221 |
+ return; |
|
142 | 222 |
} |
143 | 223 |
|
144 |
- async function startRecording() { |
|
145 |
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
146 |
- recorder = new MediaRecorder(stream, { mimeType: "audio/webm" }); |
|
147 |
- recorder.ondataavailable = (e) => websocket?.send(e.data); |
|
148 |
- recorder.start(chunkDuration); // Use dynamic chunk duration |
|
149 |
- isRecording = true; |
|
150 |
- updateUI(); |
|
151 |
- } |
|
224 |
+ await startRecording(); |
|
225 |
+ } else { |
|
226 |
+ stopRecording(); |
|
227 |
+ } |
|
228 |
+ } |
|
152 | 229 |
|
153 |
- function stopRecording() { |
|
154 |
- recorder?.stop(); |
|
155 |
- recorder = null; |
|
156 |
- isRecording = false; |
|
157 |
- websocket?.close(); |
|
158 |
- websocket = null; |
|
159 |
- updateUI(); |
|
160 |
- } |
|
230 |
+ function updateUI() { |
|
231 |
+ recordButton.classList.toggle("recording", isRecording); |
|
232 |
+ statusText.textContent = isRecording |
|
233 |
+ ? "Recording..." |
|
234 |
+ : "Click to start transcription"; |
|
235 |
+ } |
|
161 | 236 |
|
162 |
- async function toggleRecording() { |
|
163 |
- if (isRecording) stopRecording(); |
|
164 |
- else { |
|
165 |
- setupWebSocket(); |
|
166 |
- await startRecording(); |
|
167 |
- } |
|
168 |
- } |
|
169 |
- |
|
170 |
- function updateUI() { |
|
171 |
- recordButton.classList.toggle("recording", isRecording); |
|
172 |
- statusText.textContent = isRecording ? "Recording..." : "Click to start transcription"; |
|
173 |
- } |
|
174 |
- |
|
175 |
- recordButton.addEventListener("click", toggleRecording); |
|
176 |
- </script> |
|
237 |
+ recordButton.addEventListener("click", toggleRecording); |
|
238 |
+ </script> |
|
177 | 239 |
</body> |
178 | 240 |
</html>(파일 끝에 줄바꿈 문자 없음) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?