voice-commands-ai
This package maps TEXT to actions and does not recognise speech itself: give it the transcript from any speech-to-text engine and it turns "um could you set the tempreture to twenty one degrees please" into thermostat(value=21). The matching is a heuristic (edit distance, filler-word lists, a spoken-number parser and a weighted alignment score), not a language model, and it needs no model, no network and no dependencies beyond the standard library.
Install
pip install voice-commands-ai
Quickstart
from voice_commands_ai import Commands
cmds = Commands()
cmds.add("set temperature to {value:number} degrees", lambda value: f"Heating to {value} C", name="thermostat")
cmds.add("turn {state:on|off} the {device:text}", name="switch")
print(cmds.run("um could you set the tempreture to twenty one degrees please"))
print(cmds.match("turn the kitchen lights off").summary())
Heating to 21 C
Matched command 'switch' with confidence 0.93 (floor 0.60)
Heard: "turn the kitchen lights off"
Pattern: turn {state:on|off} the {device:text}
Slots:
state = 'off'
device = 'kitchen lights'
Scores: 95% of the pattern found, 100% of the words explained
Out of order: {state}
Text that matches nothing returns None instead of the closest poor guess:
cmds.match("what time is it") is None, because the best candidate scored far
below the confidence floor (0.6 by default).
Plugging in a transcriber
listen takes any function that turns your audio into text, calls it, and
matches the result. The transcriber is yours: a cloud speech API, a local
engine, or a test stub like the one below.
def transcribe(audio):
return "please turn on the porch light" # replace with your speech-to-text call
match = cmds.listen(transcribe, audio=b"...raw audio bytes...")
if match:
print(match.name, match.slots) # switch {'state': 'on', 'device': 'porch light'}
cmds.run(match) # calls the switch handler, if it has one
If the transcriber returns several guesses (a list of strings, best first), the one that matches a command best is used.
What it does
- Patterns with typed slots.
{name}or{name:word}is one word,{name:number}a number,{name:on|off}one of the listed choices (a choice can be several words:{room:living room|kitchen}),{name:text}free text up to the next fixed word or to the end, and[words]are optional words. - Filler words cost nothing. Hesitations ("um", "uh", "erm") are dropped;
politeness and framing ("please", "could you", "i'd like to", "go ahead and",
"for me", "just", "okay") can be skipped for free, and "please" is trimmed off
the end of a free-text slot. Add your own with
Commands(fillers=[...]). - Spoken numbers. "twenty one" is 21, "a hundred and five" 105, "two point
five" 2.5, "minus three" -3, plus "twelve hundred", "1.5 million", "two and a
half", "three quarters", "twenty first", "nineteen eighty four" and digits read
one by one ("four five six" is 456). Whole numbers come back as
int. - Small transcription errors. A heard word can stand in for a pattern word
by edit distance (one typo in 4-5 letters, two in 6-8, three in longer
words, none in words of three letters or fewer), as a plural, with a doubled
letter dropped ("of" for "off"), as a rough English sound-alike ("whether"
for "weather"), or split in two or run together. Every such substitution is
listed in
Match.corrections, and a word read fuzzily earns less confidence than one heard exactly. - Word order within reason. A fixed word, number or choice said somewhere
else ("turn the lights off" for "turn {state:on|off} the {device}") is still
found, at a small cost, and listed in
Match.reordered. Free-text and one-word slots must stay in order. - A confidence floor. Confidence is
coverage^1.5 * precision^0.5: coverage is the weighted share of the pattern that was found (fuzzy words earn their similarity squared, and a word contradicted by a different word in its place loses extra), precision the weighted share of the words the pattern explains, with chatter before or after the command counting half. A pattern none of whose fixed words were heard exactly is scaled by 0.85. Belowmin_confidence(default 0.6)matchreturnsNone. - The more specific pattern wins. When two commands both clear the floor
within 0.05 of each other, the one with more fixed words and fewer open slots
wins ("play {song:text} by {artist:text}" beats "play {song:text}" for "play
hello by adele"), and the other is reported in
Match.alternatives. - Handlers fail loudly. If a handler raises,
runraisesCommandErrornaming the command, with the original exception as its cause.
Limits worth knowing: a {name:text} slot accepts anything, so "call
{contact:text}" will happily match "call of duty is a great game"; put fixed
words around text slots where you can. Sound-alike matching, fillers and spoken
numbers are English. Words are split on spaces and punctuation, so for languages
written without spaces (Chinese, Japanese, Thai) the transcriber has to separate
the words. The filler lists and weights are defaults chosen on hand-written test
transcripts, not learned from data; check Match.summary() or Commands.rank()
when a result surprises you.
API
Commands(*, min_confidence=0.6, fillers=()) holds the commands.
.add(pattern, handler=None, *, name=None, examples=None)registers a command and returns theCommands, so calls chain.namedefaults to the handler's function name, else the pattern. A handler that cannot accept the pattern's slots as keyword arguments is refused here, not at run time. Each ofexamplesmust match its own pattern, which catches pattern typos early..match(text) -> Match | Nonegives the best command, orNonefor empty text, text of only filler, or text that matches nothing above the floor..run(text, *, default=None)matches and calls the handler with the slots as keyword arguments, returning its result. It also accepts aMatch(fromlisten). A command registered without a handler returns itsMatch; nothing matching returnsdefault. A handler that raises becomes aCommandErrorwith.name,.matchand.original..listen(transcribe, audio) -> Match | Nonecallstranscribe(audio), which must return a string, a list of alternative strings, orNonefor silence, and matches the text. Errors raised by the transcriber propagate unchanged..rank(text) -> list[Match]scores every command, best first, including those below the floor. Use it to see why something did or did not match.len(cmds),"name" in cmds, and iterating gives the registeredCommands.
Match fields: name, slots (typed values), confidence (0-1), pattern,
alternatives (other commands above the floor), and the evidence: text,
slot_words (the words each slot came from), corrections, ignored,
extra, missing, reordered, replaced, coverage, precision,
specificity, threshold and chosen_because. .summary() explains it in
plain text; .to_dict() gives the same as JSON-safe data.
parse_number(text) -> float | None reads one spoken English number, or returns
None when the whole text is not a number ("twenty one degrees" is not).
Zero is 0.0, so compare the result with is None.
CLI
voice-commands-ai -c "set temperature to {value:number} degrees" "um set the temperature to twenty one degrees please"
voice-commands-ai -c "thermostat = set temperature to {value:number} degrees" \
-c "switch = turn {state:on|off} the {device:text}" "turn the kitchen light off"
voice-commands-ai -f commands.txt transcripts.txt --json > matches.json
some-transcriber | voice-commands-ai -f commands.json --fail-on-no-match
voice-commands-ai --number "a hundred and five" "two point five" "minus three"
Utterances come from the arguments, from .txt files named as arguments (one
per line) or from stdin. A commands file is JSON ({"name": "pattern"}, a list
of patterns, or a list of {"pattern", "name", "examples"} objects) or text with
one name = pattern per line. --all shows how every command scored,
--min-confidence moves the floor, --output PATH writes JSON. Exit status is
0 on success, 1 with --fail-on-no-match when an utterance matched nothing,
and 2 when the commands or input could not be read.
License
MIT
Metadata
Release files for voice-commands-ai 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| voice_commands_ai-0.1.0.tar.gz | 41.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| voice_commands_ai-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.4 kB
Release files / voice_commands_ai-0.1.0.tar.gz
| Download URL | voice_commands_ai-0.1.0.tar.gz |
|---|---|
| Size | 41.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e08414d56a5d9001716597dc0390dce513009a09cd1daadd776c51d6d6ffb248
|
|
BLAKE2b-256 checksum How to use checksums |
5c5c1615dbbb2b48ba16be87f28e73b6c12cb46878a95ce29f28cf54ef6596da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / voice_commands_ai-0.1.0-py3-none-any.whl
| Download URL | voice_commands_ai-0.1.0-py3-none-any.whl |
|---|---|
| Size | 34.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b45c345199804aeb7920acfec515730cd53718ed067a589ac0e8eaa2d84bd497
|
|
BLAKE2b-256 checksum How to use checksums |
1fd7eb3827742876f3b6ec3add69d0bdf54703b0da88975c942e2757083225b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|