A voice widget that speaks fluent, confident, wrong answers is worse than no voice widget at all, since a spoken answer carries an authority a visitor is less likely to double-check than text they can reread. This lesson connects the transcript coming out of Lessons 2 and 3 to Course 10’s actual retrieval pipeline, so a spoken question gets answered from your real site content, with the same “I don’t have information on that” honesty Course 10 built in, not a model’s best guess dressed up in a synthesized voice.
Course 10’s Grounding an AI Assistant’s Answers in Retrieved Content
for my_plugin_answer_from_knowledge_base(), and this course’s Lesson 4 for
my_plugin_speak_text().
Step 1: Why this is a separate endpoint from the chat widget
Lesson 5’s chat widget already calls a REST endpoint that talks to
wp_ai_client_prompt() directly. Voice search is a narrower job: one question, one
grounded answer, sourced explicitly from your content, no multi-turn conversation
state to track. Keeping it as its own endpoint keeps that distinction clear in the
code, rather than overloading the general chat endpoint with a “but sometimes only
answer from retrieved content” branch.
// File: inc/rest-voice-search-endpoint.php
add_action( 'rest_api_init', function () {
register_rest_route( 'my-plugin/v1', '/voice-search', array(
'methods' => 'POST',
'callback' => 'my_plugin_handle_voice_search',
'permission_callback' => '__return_true',
) );
} );
Step 2: Feed the transcript straight into Course 10’s pipeline
// File: inc/rest-voice-search-endpoint.php (continued)
function my_plugin_handle_voice_search( WP_REST_Request $request ) {
$question = sanitize_textarea_field( $request->get_param( 'question' ) );
if ( ! $question ) {
return new WP_Error( 'missing_question', 'No question provided.', array( 'status' => 400 ) );
}
$result = my_plugin_answer_from_knowledge_base( $question ); // Course 10, Lesson 7
$audio_url = my_plugin_speak_text( $result['answer'] ); // this course, Lesson 4
return array(
'answer' => $result['answer'],
'sources' => array_map( 'get_the_title', $result['sources'] ),
'audio_url' => is_wp_error( $audio_url ) ? null : $audio_url,
);
}
Nothing about my_plugin_answer_from_knowledge_base() changes. It still embeds the
question, runs cosine similarity against stored chunk vectors, assembles the same
three-part grounded prompt, and calls generate_text(). The only thing new here is
the input arriving as a voice transcript instead of typed text, and the output getting
spoken instead of only displayed.
Step 3: The front-end voice search box
// File: assets/js/voice-search.js
function runVoiceSearch( transcript ) {
document.getElementById( 'voice-search-status' ).textContent = 'Searching...';
fetch( window.siteVoiceSearch.restUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.siteVoiceSearch.nonce,
},
body: JSON.stringify( { question: transcript } ),
} )
.then( function ( response ) {
return response.json();
} )
.then( function ( data ) {
var status = document.getElementById( 'voice-search-status' );
status.textContent = data.answer;
if ( data.sources && data.sources.length ) {
status.textContent += ' (Sources: ' + data.sources.join( ', ' ) + ')';
}
if ( data.audio_url ) {
new Audio( data.audio_url ).play();
}
} );
}
This reuses Lesson 2’s SpeechRecognition setup exactly, just pointed at
runVoiceSearch() instead of Course 11’s chat form, one recognition result handler,
two different destinations depending on which widget is listening.
Step 4: Why the refusal path is not optional here
Course 10’s grounded prompt tells the model explicitly to say it doesn’t have information on a topic rather than guess, when the retrieved chunks don’t actually cover the question. That instruction matters everywhere RAG is used, and it matters more for voice specifically:
Text a visitor can reread, question, or fact-check against the source article next to it. A spoken answer, especially through a natural-sounding voice like Piper’s, tends to be taken at face value in the moment. That makes Course 10’s refusal instruction, “say plainly that you don’t have information on that topic,” not a nice-to-have here, it’s the difference between an honest voice search feature and one that confidently tells people wrong things out loud.
Test it: a question your content covers, and one it doesn’t
wp eval '
$r1 = my_plugin_answer_from_knowledge_base( "How do I request a refund?" );
echo "Q1: " . $r1["answer"] . "\n\n";
$r2 = my_plugin_answer_from_knowledge_base( "What year did the moon landing happen?" );
echo "Q2: " . $r2["answer"] . "\n";
'
The first should reflect your real, indexed content, with real source titles behind it. The second, assuming your knowledge base has nothing about space history, should be a plain statement that the assistant doesn’t have information on that topic. Then run both through the actual voice widget with the mic, confirming the spoken audio matches that same honest behavior, not a version that quietly reverts to a confident guess once speech is involved.
Recap
Voice search here is not a new answering pipeline, it’s Course 10’s existing, grounded RAG pipeline with a transcript from Lessons 2 or 3 as its input and Lesson 4’s local TTS on its output. Keeping it a dedicated endpoint rather than folding it into the general chat widget keeps its one job, a single grounded question and answer, clear in the code. And because a spoken wrong answer is genuinely more convincing than a written one, Course 10’s refusal instruction is the one piece of this lesson worth testing the hardest before shipping it.