Skip to content

Instantly share code, notes, and snippets.

@vielhuber
Last active December 16, 2025 11:33
Show Gist options
  • Select an option

  • Save vielhuber/03cd3995cba42b6e43bf533277b9998d to your computer and use it in GitHub Desktop.

Select an option

Save vielhuber/03cd3995cba42b6e43bf533277b9998d to your computer and use it in GitHub Desktop.
stt speech-to-text native browser web speech api #js
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, minimum-scale=1" />
<title>Web Speech API</title>
<script>
class STT {
static instances = [];
labels = {
RECORDING_STARTED: '🎤 Aufnahme läuft...',
RECORDING_STOPPED: '⏸ Aufnahme gestoppt',
TALK_NOW: 'Sprechen Sie jetzt',
BUTTON_START: '▶️ Start',
BUTTON_STOP: '⏸ Stop',
ERROR: '❌ Fehler',
MESSAGE_SENT: '✅ Nachricht gesendet.',
NOT_SUPPORTED: '⛔ Browser unterstützt keine Spracheingabe!'
};
init($el) {
STT.instances.push(this);
// outer wrap textarea
this.$wrapper = new DOMParser().parseFromString(
'<div class="stt-wrapper"></div>',
'text/html'
).body.childNodes[0];
$el.parentNode.insertBefore(this.$wrapper, $el.nextSibling);
this.$wrapper.appendChild($el);
this.autostart = $el.hasAttribute('data-autostart');
this.setupSpeech();
this.bindEvents();
if (this.autostart) {
this.startListening();
}
}
setupSpeech() {
this.$wrapper.insertAdjacentHTML(
'beforeend',
`
<button class="toggle">${this.autostart ? this.labels.BUTTON_STOP : this.labels.BUTTON_START}</button>
<div class="status">...</div>
`
);
this.$textarea = this.$wrapper.querySelector('textarea');
this.$status = this.$wrapper.querySelector('.status');
this.$toggleButton = this.$wrapper.querySelector('.toggle');
this.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.isListening = false;
this.shouldRestart = false;
if (!this.SpeechRecognition) {
this.$status.textContent = this.labels.NOT_SUPPORTED;
this.$toggleButton.disabled = true;
return;
}
this.recognition = new this.SpeechRecognition();
this.recognition.lang = 'de-DE';
this.recognition.continuous = true;
this.recognition.interimResults = true;
}
bindEvents() {
this.recognition.onresult = event => {
let interimTranscript = '';
let hasFinal = false;
for (let i = event.resultIndex; i < event.results.length; i++) {
let transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
hasFinal = true;
} else {
interimTranscript += transcript;
}
}
if (interimTranscript) {
let text = interimTranscript.trim();
this.$textarea.value = text;
}
if (hasFinal) {
this.$status.textContent = this.labels.MESSAGE_SENT;
setTimeout(() => {
this.$textarea.value = '';
if (this.isListening) {
this.$status.textContent =
this.labels.RECORDING_STARTED + ' ' + this.labels.TALK_NOW;
} else {
this.$status.textContent = this.labels.RECORDING_STOPPED;
}
}, 1000);
if (this.$wrapper.closest('form')) {
this.$wrapper.closest('form').dispatchEvent(new Event('submit', { bubbles: true }));
}
}
if (!hasFinal && interimTranscript) {
this.$status.textContent = this.labels.RECORDING_STARTED;
}
};
this.recognition.onerror = event => {
this.$status.textContent = this.labels.ERROR + ': ' + event.error;
};
this.recognition.onend = () => {
if (this.shouldRestart && this.isListening) {
this.recognition.start();
}
};
this.$toggleButton.addEventListener('click', e => {
e.preventDefault();
if (this.isListening) {
this.stopListening();
} else {
this.startListening();
}
});
}
startListening() {
STT.instances.forEach(instances__value => {
if (instances__value !== this && instances__value.isListening) {
instances__value.stopListening();
}
});
this.shouldRestart = true;
this.isListening = true;
this.recognition.start();
this.$toggleButton.textContent = this.labels.BUTTON_STOP;
this.$status.textContent = this.labels.RECORDING_STARTED + ' ' + this.labels.TALK_NOW;
}
stopListening() {
this.shouldRestart = false;
this.isListening = false;
this.recognition.stop();
this.$toggleButton.textContent = this.labels.BUTTON_START;
this.$status.textContent = this.labels.RECORDING_STOPPED;
}
}
window.addEventListener('load', e => {
if (document.querySelector('.stt') !== null) {
document.querySelectorAll('.stt').forEach($el => {
let stt = new STT();
stt.init($el);
});
}
});
</script>
<style>
textarea {
width: 100%;
height: 300px;
display: block;
}
</style>
</head>
<body>
<form>
<textarea class="stt" data-autostart></textarea>
</form>
<form>
<textarea class="stt"></textarea>
</form>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment