Created
December 16, 2025 15:56
-
-
Save rickmed/de19c4fa2ce3ad8dd1f345aa963b09bf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chrome Speech-to-Text Quality Test</title> | |
| <!-- Mobile debugging console --> | |
| <script src="https://cdn.jsdelivr.net/npm/eruda"></script> | |
| <script>eruda.init();</script> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| max-width: 800px; | |
| margin: 50px auto; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| } | |
| .container { | |
| background: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| h1 { | |
| color: #333; | |
| margin-top: 0; | |
| } | |
| .controls { | |
| margin: 20px 0; | |
| } | |
| button { | |
| padding: 12px 24px; | |
| font-size: 16px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| margin-right: 10px; | |
| transition: all 0.3s; | |
| } | |
| #startBtn { | |
| background: #4CAF50; | |
| color: white; | |
| } | |
| #startBtn:hover { | |
| background: #45a049; | |
| } | |
| #startBtn:disabled { | |
| background: #cccccc; | |
| cursor: not-allowed; | |
| } | |
| #stopBtn { | |
| background: #f44336; | |
| color: white; | |
| } | |
| #stopBtn:hover { | |
| background: #da190b; | |
| } | |
| #stopBtn:disabled { | |
| background: #cccccc; | |
| cursor: not-allowed; | |
| } | |
| .status { | |
| padding: 10px; | |
| margin: 15px 0; | |
| border-radius: 5px; | |
| font-weight: bold; | |
| } | |
| .status.listening { | |
| background: #e8f5e9; | |
| color: #2e7d32; | |
| } | |
| .status.idle { | |
| background: #e3f2fd; | |
| color: #1565c0; | |
| } | |
| .status.error { | |
| background: #ffebee; | |
| color: #c62828; | |
| } | |
| .transcript-box { | |
| margin: 20px 0; | |
| } | |
| .transcript-box h3 { | |
| color: #555; | |
| margin-bottom: 10px; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 150px; | |
| padding: 15px; | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| font-size: 16px; | |
| font-family: inherit; | |
| resize: vertical; | |
| box-sizing: border-box; | |
| } | |
| .interim { | |
| color: #999; | |
| font-style: italic; | |
| } | |
| .settings { | |
| margin: 20px 0; | |
| padding: 15px; | |
| background: #f9f9f9; | |
| border-radius: 5px; | |
| } | |
| .settings label { | |
| display: block; | |
| margin: 10px 0; | |
| color: #555; | |
| } | |
| .settings select { | |
| padding: 5px; | |
| margin-left: 10px; | |
| border-radius: 3px; | |
| border: 1px solid #ddd; | |
| } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .metric { | |
| padding: 15px; | |
| background: #f0f0f0; | |
| border-radius: 5px; | |
| } | |
| .metric-label { | |
| font-size: 12px; | |
| color: #666; | |
| text-transform: uppercase; | |
| } | |
| .metric-value { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #333; | |
| margin-top: 5px; | |
| } | |
| .info { | |
| background: #fff3cd; | |
| border: 1px solid #ffc107; | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin: 20px 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎤 Chrome Speech-to-Text Quality Test</h1> | |
| <div class="info"> | |
| <strong>Note:</strong> This uses Chrome's Web Speech API. Make sure to allow microphone access when prompted. | |
| </div> | |
| <div class="settings"> | |
| <h3>Settings</h3> | |
| <label> | |
| Language: | |
| <select id="languageSelect"> | |
| <option value="en-US">English (US)</option> | |
| <option value="en-GB">English (UK)</option> | |
| <option value="es-ES">Spanish (Spain)</option> | |
| <option value="es-VE">Spanish (Venezuela)</option> | |
| <option value="es-MX">Spanish (Mexico)</option> | |
| <option value="fr-FR">French</option> | |
| <option value="de-DE">German</option> | |
| <option value="it-IT">Italian</option> | |
| <option value="pt-BR">Portuguese (Brazil)</option> | |
| <option value="ja-JP">Japanese</option> | |
| <option value="zh-CN">Chinese (Mandarin)</option> | |
| </select> | |
| </label> | |
| <label> | |
| <input type="checkbox" id="continuousMode" checked> Continuous mode | |
| </label> | |
| <label> | |
| <input type="checkbox" id="interimResults" checked> Show interim results | |
| </label> | |
| </div> | |
| <div class="controls"> | |
| <button id="startBtn">Start Recording</button> | |
| <button id="stopBtn" disabled>Stop Recording</button> | |
| <button id="clearBtn">Clear Transcript</button> | |
| </div> | |
| <div id="status" class="status idle">Ready to start</div> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <div class="metric-label">Words Detected</div> | |
| <div class="metric-value" id="wordCount">0</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-label">Recording Time</div> | |
| <div class="metric-value" id="recordingTime">0s</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-label">Confidence Avg</div> | |
| <div class="metric-value" id="confidenceAvg">0%</div> | |
| </div> | |
| </div> | |
| <div class="transcript-box"> | |
| <h3>Final Transcript</h3> | |
| <textarea id="finalTranscript" placeholder="Your speech will appear here..."></textarea> | |
| </div> | |
| <div class="transcript-box"> | |
| <h3>Interim Results (what's being processed)</h3> | |
| <div id="interimTranscript" class="interim" style="padding: 15px; background: #f9f9f9; border-radius: 5px; min-height: 50px;"> | |
| <em>Interim results will appear here...</em> | |
| </div> | |
| </div> | |
| <div class="transcript-box"> | |
| <h3>Detailed Log</h3> | |
| <textarea id="detailedLog" placeholder="Detailed recognition events will be logged here..." style="min-height: 200px; font-family: monospace; font-size: 12px;"></textarea> | |
| </div> | |
| <div class="transcript-box"> | |
| <h3>Debug Console (Live)</h3> | |
| <div id="debugConsole" style="background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 5px; min-height: 200px; max-height: 300px; overflow-y: auto; font-family: monospace; font-size: 11px; white-space: pre-wrap;"> | |
| Console output will appear here... | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Intercept console.log to show on screen | |
| const debugConsole = document.getElementById('debugConsole'); | |
| const originalLog = console.log; | |
| console.log = function(...args) { | |
| originalLog.apply(console, args); | |
| const message = args.map(arg => | |
| typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) | |
| ).join(' '); | |
| const timestamp = new Date().toLocaleTimeString(); | |
| debugConsole.textContent += `[${timestamp}] ${message}\n`; | |
| debugConsole.scrollTop = debugConsole.scrollHeight; | |
| }; | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const statusDiv = document.getElementById('status'); | |
| const finalTranscriptArea = document.getElementById('finalTranscript'); | |
| const interimTranscriptDiv = document.getElementById('interimTranscript'); | |
| const detailedLogArea = document.getElementById('detailedLog'); | |
| const languageSelect = document.getElementById('languageSelect'); | |
| const continuousModeCheckbox = document.getElementById('continuousMode'); | |
| const interimResultsCheckbox = document.getElementById('interimResults'); | |
| const wordCountDiv = document.getElementById('wordCount'); | |
| const recordingTimeDiv = document.getElementById('recordingTime'); | |
| const confidenceAvgDiv = document.getElementById('confidenceAvg'); | |
| let recognition; | |
| let finalTranscript = ''; | |
| let startTime; | |
| let timerInterval; | |
| let confidenceScores = []; | |
| // Check for browser support | |
| if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { | |
| statusDiv.textContent = 'Speech recognition not supported in this browser. Please use Chrome.'; | |
| statusDiv.className = 'status error'; | |
| startBtn.disabled = true; | |
| } else { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| recognition = new SpeechRecognition(); | |
| function initRecognition() { | |
| recognition.continuous = continuousModeCheckbox.checked; | |
| recognition.interimResults = interimResultsCheckbox.checked; | |
| recognition.lang = languageSelect.value; | |
| recognition.maxAlternatives = 3; | |
| } | |
| initRecognition(); | |
| recognition.onstart = () => { | |
| statusDiv.textContent = '🎤 Listening...'; | |
| statusDiv.className = 'status listening'; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| startTime = Date.now(); | |
| timerInterval = setInterval(updateTimer, 100); | |
| log('Recognition started'); | |
| }; | |
| recognition.onresult = (event) => { | |
| console.log('=== onresult fired ==='); | |
| console.log('resultIndex:', event.resultIndex); | |
| console.log('results.length:', event.results.length); | |
| let interimTranscript = ''; | |
| // Build final transcript from scratch each time to avoid duplication | |
| let tempFinalTranscript = ''; | |
| for (let i = 0; i < event.results.length; i++) { | |
| const result = event.results[i]; | |
| const transcript = result[0].transcript; | |
| const confidence = result[0].confidence; | |
| console.log(`Result [${i}]: isFinal=${result.isFinal}, transcript="${transcript}", confidence=${confidence}`); | |
| if (result.isFinal) { | |
| console.log(` → Adding to FINAL transcript`); | |
| tempFinalTranscript += transcript + ' '; | |
| // Only log and track confidence for newly finalized results | |
| if (i >= event.resultIndex) { | |
| if (confidence) { | |
| confidenceScores.push(confidence); | |
| updateConfidence(); | |
| } | |
| // Log all alternatives | |
| log(`Final: "${transcript}" (confidence: ${confidence ? (confidence * 100).toFixed(1) : 'N/A'}%)`); | |
| if (result.length > 1) { | |
| for (let j = 1; j < Math.min(result.length, 3); j++) { | |
| const altConf = result[j].confidence; | |
| log(` Alt ${j}: "${result[j].transcript}" (confidence: ${altConf ? (altConf * 100).toFixed(1) : 'N/A'}%)`); | |
| } | |
| } | |
| } | |
| } else { | |
| console.log(` → Adding to INTERIM transcript`); | |
| interimTranscript += transcript; | |
| } | |
| } | |
| console.log('Final transcript now:', tempFinalTranscript); | |
| console.log('Interim transcript:', interimTranscript); | |
| // Update final transcript | |
| finalTranscript = tempFinalTranscript; | |
| finalTranscriptArea.value = finalTranscript; | |
| // Update interim display | |
| if (interimTranscript) { | |
| interimTranscriptDiv.innerHTML = `<em style="color: #999;">${interimTranscript}</em>`; | |
| } else { | |
| interimTranscriptDiv.innerHTML = '<em>Interim results will appear here...</em>'; | |
| } | |
| updateWordCount(); | |
| }; | |
| recognition.onerror = (event) => { | |
| statusDiv.textContent = `Error: ${event.error}`; | |
| statusDiv.className = 'status error'; | |
| log(`Error: ${event.error} - ${event.message || 'No additional details'}`); | |
| stopRecording(); | |
| }; | |
| recognition.onend = () => { | |
| statusDiv.textContent = 'Recording stopped'; | |
| statusDiv.className = 'status idle'; | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| clearInterval(timerInterval); | |
| log('Recognition ended'); | |
| }; | |
| startBtn.onclick = () => { | |
| initRecognition(); | |
| recognition.start(); | |
| }; | |
| stopBtn.onclick = () => { | |
| stopRecording(); | |
| }; | |
| clearBtn.onclick = () => { | |
| finalTranscript = ''; | |
| finalTranscriptArea.value = ''; | |
| interimTranscriptDiv.innerHTML = '<em>Interim results will appear here...</em>'; | |
| detailedLogArea.value = ''; | |
| confidenceScores = []; | |
| updateWordCount(); | |
| updateConfidence(); | |
| log('Transcript cleared'); | |
| }; | |
| languageSelect.onchange = () => { | |
| log(`Language changed to: ${languageSelect.value}`); | |
| }; | |
| } | |
| function stopRecording() { | |
| if (recognition) { | |
| recognition.stop(); | |
| } | |
| } | |
| function updateTimer() { | |
| const elapsed = Math.floor((Date.now() - startTime) / 1000); | |
| recordingTimeDiv.textContent = `${elapsed}s`; | |
| } | |
| function updateWordCount() { | |
| const words = finalTranscript.trim().split(/\s+/).filter(w => w.length > 0); | |
| wordCountDiv.textContent = words.length; | |
| } | |
| function updateConfidence() { | |
| if (confidenceScores.length === 0) { | |
| confidenceAvgDiv.textContent = '0%'; | |
| return; | |
| } | |
| const avg = confidenceScores.reduce((a, b) => a + b, 0) / confidenceScores.length; | |
| confidenceAvgDiv.textContent = `${(avg * 100).toFixed(1)}%`; | |
| } | |
| function log(message) { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| detailedLogArea.value += `[${timestamp}] ${message}\n`; | |
| detailedLogArea.scrollTop = detailedLogArea.scrollHeight; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment