diff --git a/demo.png b/demo.png index e15035a..bf25527 100644 Binary files a/demo.png and b/demo.png differ diff --git a/whisperlivekit/web/live_transcription.css b/whisperlivekit/web/live_transcription.css index 6d15f6e..61c4f13 100644 --- a/whisperlivekit/web/live_transcription.css +++ b/whisperlivekit/web/live_transcription.css @@ -184,7 +184,7 @@ body { .settings { display: flex; - flex-direction: column; + flex-wrap: wrap; align-items: flex-start; gap: 12px; } @@ -198,23 +198,27 @@ body { #chunkSelector, #websocketInput, -#themeSelector { +#themeSelector, +#microphoneSelect { font-size: 16px; padding: 5px 8px; border-radius: 8px; border: 1px solid var(--border); background-color: var(--button-bg); color: var(--text); - max-height: 34px; + max-height: 30px; } -#websocketInput { - width: 220px; +#microphoneSelect { + width: 100%; + max-width: 190px; + min-width: 120px; } #chunkSelector:focus, #websocketInput:focus, -#themeSelector:focus { +#themeSelector:focus, +#microphoneSelect:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); @@ -247,9 +251,9 @@ label { } .theme-selector-container { - position: absolute; - top: 20px; - right: 20px; + display: flex; + align-items: center; + margin-top: 17px; } .segmented label { @@ -400,3 +404,57 @@ label { font-size: 14px; margin-bottom: 0px; } + +/* for smaller screens */ +@media (max-width: 768px) { + .settings-container { + flex-direction: column; + gap: 10px; + } + + .settings { + justify-content: center; + gap: 8px; + } + + .field { + align-items: center; + } + + #websocketInput, + #microphoneSelect { + min-width: 100px; + max-width: 160px; + } + + .theme-selector-container { + margin-top: 10px; + } +} + +@media (max-width: 480px) { + body { + margin: 10px; + } + + .settings { + flex-direction: column; + align-items: center; + gap: 6px; + } + + #websocketInput, + #microphoneSelect { + max-width: 140px; + } + + .segmented label { + padding: 4px 8px; + font-size: 12px; + } + + .segmented img { + width: 14px; + height: 14px; + } +} diff --git a/whisperlivekit/web/live_transcription.html b/whisperlivekit/web/live_transcription.html index da0a187..90c54cb 100644 --- a/whisperlivekit/web/live_transcription.html +++ b/whisperlivekit/web/live_transcription.html @@ -1,61 +1,73 @@ + - - - WhisperLiveKit - + + + WhisperLiveKit + + -
- + +
+
+ + +
+ +
+ + +
+ +
+
+ + + + + + + + +
+
+
-
00:00
-
- - -
-
- - -
- -
- - -
-
- - - - - - - -
-
-

-
- +

+ +
+ + - + + \ No newline at end of file diff --git a/whisperlivekit/web/live_transcription.js b/whisperlivekit/web/live_transcription.js index 21ebba7..9fe4550 100644 --- a/whisperlivekit/web/live_transcription.js +++ b/whisperlivekit/web/live_transcription.js @@ -18,6 +18,8 @@ let animationFrame = null; let waitingForStop = false; let lastReceivedData = null; let lastSignature = null; +let availableMicrophones = []; +let selectedMicrophoneId = null; waveCanvas.width = 60 * (window.devicePixelRatio || 1); waveCanvas.height = 30 * (window.devicePixelRatio || 1); @@ -31,6 +33,7 @@ const websocketDefaultSpan = document.getElementById("wsDefaultUrl"); const linesTranscriptDiv = document.getElementById("linesTranscript"); const timerElement = document.querySelector(".timer"); const themeRadios = document.querySelectorAll('input[name="theme"]'); +const microphoneSelect = document.getElementById("microphoneSelect"); function getWaveStroke() { const styles = getComputedStyle(document.documentElement); @@ -82,6 +85,61 @@ if (darkMq && darkMq.addEventListener) { darkMq.addListener(handleOsThemeChange); } +async function enumerateMicrophones() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + stream.getTracks().forEach(track => track.stop()); + + const devices = await navigator.mediaDevices.enumerateDevices(); + availableMicrophones = devices.filter(device => device.kind === 'audioinput'); + + populateMicrophoneSelect(); + console.log(`Found ${availableMicrophones.length} microphone(s)`); + } catch (error) { + console.error('Error enumerating microphones:', error); + statusText.textContent = "Error accessing microphones. Please grant permission."; + } +} + +function populateMicrophoneSelect() { + if (!microphoneSelect) return; + + microphoneSelect.innerHTML = ''; + + availableMicrophones.forEach((device, index) => { + const option = document.createElement('option'); + option.value = device.deviceId; + option.textContent = device.label || `Microphone ${index + 1}`; + microphoneSelect.appendChild(option); + }); + + const savedMicId = localStorage.getItem('selectedMicrophone'); + if (savedMicId && availableMicrophones.some(mic => mic.deviceId === savedMicId)) { + microphoneSelect.value = savedMicId; + selectedMicrophoneId = savedMicId; + } +} + +function handleMicrophoneChange() { + selectedMicrophoneId = microphoneSelect.value || null; + localStorage.setItem('selectedMicrophone', selectedMicrophoneId || ''); + + const selectedDevice = availableMicrophones.find(mic => mic.deviceId === selectedMicrophoneId); + const deviceName = selectedDevice ? selectedDevice.label : 'Default Microphone'; + + console.log(`Selected microphone: ${deviceName}`); + statusText.textContent = `Microphone changed to: ${deviceName}`; + + if (isRecording) { + statusText.textContent = "Switching microphone... Please wait."; + stopRecording().then(() => { + setTimeout(() => { + toggleRecording(); + }, 1000); + }); + } +} + // Helpers function fmt1(x) { const n = Number(x); @@ -377,7 +435,11 @@ async function startRecording() { console.log("Error acquiring wake lock."); } - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const audioConstraints = selectedMicrophoneId + ? { audio: { deviceId: { exact: selectedMicrophoneId } } } + : { audio: true }; + + const stream = await navigator.mediaDevices.getUserMedia(audioConstraints); audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); @@ -516,3 +578,22 @@ function updateUI() { } recordButton.addEventListener("click", toggleRecording); + +if (microphoneSelect) { + microphoneSelect.addEventListener("change", handleMicrophoneChange); +} +document.addEventListener('DOMContentLoaded', async () => { + try { + await enumerateMicrophones(); + } catch (error) { + console.log("Could not enumerate microphones on load:", error); + } +}); +navigator.mediaDevices.addEventListener('devicechange', async () => { + console.log('Device change detected, re-enumerating microphones'); + try { + await enumerateMicrophones(); + } catch (error) { + console.log("Error re-enumerating microphones:", error); + } +});