Skip to content

Instantly share code, notes, and snippets.

@rickmed
Created December 16, 2025 15:56
Show Gist options
  • Select an option

  • Save rickmed/de19c4fa2ce3ad8dd1f345aa963b09bf to your computer and use it in GitHub Desktop.

Select an option

Save rickmed/de19c4fa2ce3ad8dd1f345aa963b09bf to your computer and use it in GitHub Desktop.
<!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