LESSON 72 · ADVANCED4 min read

Project: A voice-command assistant

Build a text-first command router, wrap speech behind adapters, allowlist actions, and test the assistant without a microphone.

Python 3.12+Free to readModule 16 of 19
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Parse spoken commands into a small set of intents.
  • Hide speech recognition and speech synthesis behind replaceable adapters.
  • Allowlist actions and require confirmation for anything risky.
  • Test the assistant with recorded text instead of live audio.

01Start with a text-first command router

A voice assistant is a command parser with a microphone attached. Begin with text: receive a sentence, normalize it, and return an intent plus its arguments. Keeping this layer pure means you can test hundreds of phrasings in milliseconds and change the speech provider later without rewriting the rules.

Support a small vocabulary on purpose. A router that recognizes 'open', 'time', 'weather', and 'help' is easier to secure and explain than one that forwards arbitrary text to a shell. Unknown commands should return a clear fallback, not an exception that ends the session. A pure router also gives you one place to log intent and confidence without mixing presentation into parsing.

command_router.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02Wrap speech behind adapters

Define the smallest interface your assistant needs: listen() returns text, and speak(text) produces audio. A real adapter can use speech_recognition and pyttsx3, while a test adapter returns scripted strings and records what was spoken. The assistant depends on the interface, not on a microphone.

Speech libraries are platform-dependent and may fail to install. Make the dependency optional at the program boundary: if the adapter cannot start, fall back to typed input and explain why. A missing microphone should not prevent the command router from running.

speech_adapters.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Act safely

An assistant that opens websites needs an allowlist. Map friendly names to known URLs instead of passing user text to a browser or shell. Never build a shell command from recognized speech; use webbrowser.open with a validated URL, and refuse schemes such as file: or javascript:.

Require confirmation for actions that change state: sending a message, deleting a file, making a purchase, or installing software. The confirmation should name the exact action and target. Log the decision and the result, but do not log secrets or full transcripts by default.

safe_actions.pyPython
import webbrowserSITES = {    "youtube": "https://www.youtube.com",    "python": "https://www.python.org",}def open_site(name):    url = SITES.get(name)    if url is None:        return f"Unknown site: {name}"    webbrowser.open(url)    return f"Opened {url}"print(open_site("docs"))print(open_site("python"))
Outputstdout
Unknown site: docs
Opened https://www.python.org

04Test with transcripts

Record a small set of spoken-style phrases and the expected intents. Include punctuation, extra whitespace, synonyms, and an unknown command. These transcript tests are your regression suite when you change the parser or swap speech providers.

Test the failure paths explicitly: microphone unavailable, empty recognition result, network timeout, and an action that the user declines. A voice interface is pleasant only when it fails gracefully and tells the user what it heard.

test_transcripts.pyPython
import unittestdef parse_command(text):    cleaned = " ".join(text.lower().strip().split())    if cleaned in {"time", "what time is it"}:        return ("time", None)    if cleaned.startswith("open "):        return ("open", cleaned.removeprefix("open ").strip())    return ("unknown", cleaned)class TranscriptTests(unittest.TestCase):    def test_time_variants(self):        self.assertEqual(parse_command("  What   time is it "), ("time", None))        self.assertEqual(parse_command("TIME"), ("time", None))    def test_unknown_is_safe(self):        self.assertEqual(parse_command("delete everything"), ("unknown", "delete everything"))unittest.main(argv=[""], exit=False)
LET IT CLICK

The ideas worth keeping.

  • Parse commands as text before adding speech.
  • Adapters isolate platform-specific audio code and make testing possible.
  • Allowlist actions and confirm anything that changes state.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Add a weather intent

Extend parse_command to recognize 'weather' and 'weather in <city>'. Return ('weather', city_or_None) and keep unknown commands unchanged.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
def parse_command(text):    cleaned = " ".join(text.lower().strip().split())    if cleaned == "weather":        return ("weather", None)    if cleaned.startswith("weather in "):        return ("weather", cleaned.removeprefix("weather in ").strip())    if cleaned == "time":        return ("time", None)    return ("unknown", cleaned)print(parse_command("Weather in Pune"))print(parse_command("weather"))

The longer prefix is checked before the bare command, otherwise 'weather in pune' would be mistaken for the no-city form. Order matters when one pattern is a prefix of another.

02

Prove the speaker adapter works

Use ScriptedListener and RecordingSpeaker to run two commands through a small assistant loop. Assert that the speaker recorded the expected responses and that 'quit' ends the loop.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
class ScriptedListener:    def __init__(self, lines):        self._lines = iter(lines)    def listen(self):        return next(self._lines, "quit")class RecordingSpeaker:    def __init__(self):        self.spoken = []    def speak(self, text):        self.spoken.append(text)listener = ScriptedListener(["time", "help", "quit"])speaker = RecordingSpeaker()while True:    command = listener.listen()    if command == "quit":        break    speaker.speak(f"handled: {command}")print(speaker.spoken)

The recording speaker becomes the observable output of the assistant. No microphone or audio device is involved, so the test runs anywhere.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why start with a text-first command router?
02How should an assistant open a website?
03When is confirmation required?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Add a weather intent

Extend parse_command to recognize 'weather' and 'weather in <city>'. Return ('weather', city_or_None) and keep unknown commands unchanged.

EXPECTED OUTPUT
parse_command("Weather in Pune")
parse_command("weather")
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.