A visitor who’d rather speak a question than type it isn’t asking for a WordPress
feature at all, they’re asking for something the browser itself already does. There is
no WordPress-specific voice input API, and no reason to invent one: the Web Speech API,
a standard, browser-native capability, converts speech to text entirely on the client
side, before any of it ever reaches your REST endpoint. By the time PHP sees anything,
it’s the exact same plain-text message field Course 11 always sent.
Course 11’s chat widget UI and sendMessage() function. No PHP AI Client SDK
prerequisite, this lesson is entirely client-side JavaScript.
Step 1: be clear about what this actually is
SpeechRecognition is a standard Web API, implemented by the browser itself, with no
relationship to wordpress/php-ai-client or any WordPress core capability. It converts
audio to text using whatever speech recognition service the browser vendor provides,
entirely outside your server. Your PHP code never sees audio, never sees a browser API
call, and never needs to know voice input exists, it only ever receives the resulting
text through the same message field Course 11 already built.
Step 2: add a microphone button that fills the text input
<!-- File: templates/chat-widget.php -->
<div class="site-chat-input-row">
<textarea id="site-chat-input" placeholder="Type your question..."></textarea>
<button type="button" id="site-chat-mic" aria-label="Speak your question">🎤</button>
<button type="button" id="site-chat-send">Send</button>
</div>
Step 3: wire up SpeechRecognition
// File: assets/js/chat-widget.js
function initVoiceInput() {
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
var micButton = document.getElementById( 'site-chat-mic' );
if ( ! SpeechRecognition ) {
micButton.style.display = 'none'; // No graceful degradation needed, just hide it.
return;
}
var recognition = new SpeechRecognition();
recognition.lang = document.documentElement.lang || 'en-US';
recognition.interimResults = false;
micButton.addEventListener( 'click', function () {
micButton.classList.add( 'site-chat-mic--listening' );
recognition.start();
} );
recognition.addEventListener( 'result', function ( event ) {
var transcript = event.results[ 0 ][ 0 ].transcript;
document.getElementById( 'site-chat-input' ).value = transcript;
} );
recognition.addEventListener( 'end', function () {
micButton.classList.remove( 'site-chat-mic--listening' );
} );
recognition.addEventListener( 'error', function () {
micButton.classList.remove( 'site-chat-mic--listening' );
appendMessage( 'assistant', 'Sorry, I couldn\'t hear that clearly. Try typing instead.' );
} );
}
recognition.lang, read from document.documentElement.lang, the same locale value
WordPress already writes onto the <html> tag, means a Spanish-language page gets
Spanish speech recognition without any extra configuration. This lines up directly with
Lesson 5’s multilingual handling: the transcript that lands in the text input goes
through the exact same language-detection and translation path any typed message would.
Step 4: let the visitor review before sending
Filling the text input rather than sending immediately on a recognized result matters: speech recognition is genuinely imperfect, and a visitor should get to glance at the transcript and correct an obvious misrecognition before it becomes a message sent to the model. Wire the existing send button, unchanged from Course 11, to fire normally once the visitor is satisfied with what’s in the box.
SpeechRecognition (unprefixed) and webkitSpeechRecognition (the older, prefixed form
Chrome and Chromium-based browsers still expose) cover the large majority of desktop and
Android traffic, but support is not universal, and it can vary by browser version and
platform. Checking window.SpeechRecognition || window.webkitSpeechRecognition before
doing anything else, exactly as Step 3 does, and hiding the microphone button entirely
when neither exists, is the whole mitigation. There is no polyfill worth reaching for
here, a visitor on an unsupported browser simply types, exactly as before this lesson
existed.
Step 5: confirm the backend truly needs nothing new
This is worth stating directly because it’s easy to assume otherwise: no REST route
change, no new parameter, no PHP file in this lesson at all. sendMessage(), Course 11’s
existing function, and my_plugin_handle_chat_request(), the existing callback, never
change. Voice input is entirely upstream of both, a different way of populating the same
text string sendMessage() already expected.
Test it: speak a question and confirm nothing downstream changed
In a supporting browser, click the microphone button, speak a short question, and
confirm the recognized text appears in the input box for review. Send it, and confirm
in the network tab that the request to /wp-json/my-plugin/v1/chat is byte-for-byte
identical in shape to one sent by typing, same message field, same session_id,
nothing marking it as voice-originated. Then test in a browser without support (or with
JavaScript’s SpeechRecognition temporarily deleted from window in devtools) and
confirm the microphone button is simply absent, with typing working exactly as before.
Recap
Voice input for this chatbot is entirely a front-end concern: the standard, browser-native
Web Speech API converts spoken audio to text client-side, fills the same text input
Course 11 already built, and lets the visitor review and edit before sending. Nothing
about the REST endpoint, the session handling, the RAG pipeline, or the human takeover
built in earlier lessons needed to change, because by the time any of that code runs,
voice input has already become an ordinary string in the message field it always
expected.