Skip to content

Commit 51f4846

Browse files
committed
Add voice-command router (injectable speech-to-text)
1 parent 2b7bd07 commit 51f4846

15 files changed

Lines changed: 452 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-20) — Voice-Command Router](#whats-new-2026-06-20--voice-command-router)
1617
- [What's new (2026-06-20) — Locale-Aware Number, Currency & Date Parsing](#whats-new-2026-06-20--locale-aware-number-currency--date-parsing)
1718
- [What's new (2026-06-20) — Perceptual-Hash Image Dedupe](#whats-new-2026-06-20--perceptual-hash-image-dedupe)
1819
- [What's new (2026-06-20) — S3-Compatible Artifact Store](#whats-new-2026-06-20--s3-compatible-artifact-store)
@@ -96,6 +97,12 @@
9697

9798
---
9899

100+
## What's new (2026-06-20) — Voice-Command Router
101+
102+
Trigger flows hands-free from recognized speech. Full reference: [`docs/source/Eng/doc/new_features/v44_features_doc.rst`](docs/source/Eng/doc/new_features/v44_features_doc.rst).
103+
104+
- **`VoiceRouter`** (`AC_voice_register` / `AC_voice_dispatch` / `AC_voice_list` / `AC_voice_clear`, `ac_*`): map spoken trigger phrases to `AC_*` action lists; feed it recognized text and it runs the closest registered command (phrase matching reuses the fuzzy matcher, so "save the file" fires "save file"). **Speech-to-text is out of scope and injectable** — the router takes text and a `recognizer`/`runner` callable, so routing is fully unit-tested without audio or any speech dependency (a real Vosk/mic recogniser plugs into `listen_once`).
105+
99106
## What's new (2026-06-20) — Locale-Aware Number, Currency & Date Parsing
100107

101108
Parse localized numbers/currency/dates. Full reference: [`docs/source/Eng/doc/new_features/v43_features_doc.rst`](docs/source/Eng/doc/new_features/v43_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-20) — 语音指令路由器](#本次更新-2026-06-20--语音指令路由器)
1516
- [本次更新 (2026-06-20) — 区域设置感知的数字、货币与日期解析](#本次更新-2026-06-20--区域设置感知的数字货币与日期解析)
1617
- [本次更新 (2026-06-20) — 感知哈希图像去重](#本次更新-2026-06-20--感知哈希图像去重)
1718
- [本次更新 (2026-06-20) — S3 兼容成品存储](#本次更新-2026-06-20--s3-兼容成品存储)
@@ -95,6 +96,12 @@
9596

9697
---
9798

99+
## 本次更新 (2026-06-20) — 语音指令路由器
100+
101+
以已识别语音免手动触发流程。完整参考:[`docs/source/Zh/doc/new_features/v44_features_doc.rst`](../docs/source/Zh/doc/new_features/v44_features_doc.rst)
102+
103+
- **`VoiceRouter`**(`AC_voice_register` / `AC_voice_dispatch` / `AC_voice_list` / `AC_voice_clear``ac_*`):将语音触发短语映射到 `AC_*` 动作列表;喂入已识别文本即执行最接近的已注册指令(短语匹配重用模糊匹配器,因此「save the file」会触发「save file」)。**语音转文本不在范围内且可注入** —— 路由器接受文本与 `recognizer`/`runner` 可调用对象,因此路由在无音频、无任何语音依赖下完整单元测试(真实 Vosk/麦克风识别器接入 `listen_once`)。
104+
98105
## 本次更新 (2026-06-20) — 区域设置感知的数字、货币与日期解析
99106

100107
解析本地化的数字/货币/日期。完整参考:[`docs/source/Zh/doc/new_features/v43_features_doc.rst`](../docs/source/Zh/doc/new_features/v43_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-20) — 語音指令路由器](#本次更新-2026-06-20--語音指令路由器)
1516
- [本次更新 (2026-06-20) — 區域設定感知的數字、貨幣與日期解析](#本次更新-2026-06-20--區域設定感知的數字貨幣與日期解析)
1617
- [本次更新 (2026-06-20) — 感知雜湊影像去重](#本次更新-2026-06-20--感知雜湊影像去重)
1718
- [本次更新 (2026-06-20) — S3 相容成品儲存](#本次更新-2026-06-20--s3-相容成品儲存)
@@ -95,6 +96,12 @@
9596

9697
---
9798

99+
## 本次更新 (2026-06-20) — 語音指令路由器
100+
101+
以已辨識語音免手動觸發流程。完整參考:[`docs/source/Zh/doc/new_features/v44_features_doc.rst`](../docs/source/Zh/doc/new_features/v44_features_doc.rst)
102+
103+
- **`VoiceRouter`**(`AC_voice_register` / `AC_voice_dispatch` / `AC_voice_list` / `AC_voice_clear``ac_*`):將語音觸發片語對應到 `AC_*` 動作清單;餵入已辨識文字即執行最接近的已註冊指令(片語比對重用模糊比對器,因此「save the file」會觸發「save file」)。**語音轉文字不在範圍內且可注入** —— 路由器接受文字與 `recognizer`/`runner` 可呼叫物件,因此路由在無音訊、無任何語音相依下完整單元測試(真實 Vosk/麥克風辨識器接入 `listen_once`)。
104+
98105
## 本次更新 (2026-06-20) — 區域設定感知的數字、貨幣與日期解析
99106

100107
解析在地化的數字/貨幣/日期。完整參考:[`docs/source/Zh/doc/new_features/v43_features_doc.rst`](../docs/source/Zh/doc/new_features/v43_features_doc.rst)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Voice-Command Router
2+
====================
3+
4+
``VoiceRouter`` maps spoken trigger *phrases* to ``AC_*`` action lists: feed it
5+
the text of a recognized utterance and it runs the closest registered command —
6+
hands-free triggering of automation flows. Phrase matching reuses the project's
7+
fuzzy matcher, so "save the file" still fires a ``"save file"`` command despite
8+
recogniser noise.
9+
10+
Speech-to-text is intentionally **out of scope and injectable**: the router takes
11+
already-recognised *text*. A real microphone/Vosk recogniser is supplied as a
12+
``recognizer`` callable to :meth:`VoiceRouter.listen_once`, which keeps the
13+
routing logic fully unit-testable without audio or any speech dependency. Imports
14+
no ``PySide6``.
15+
16+
Headless API
17+
------------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import VoiceRouter
22+
23+
router = VoiceRouter(threshold=0.7)
24+
router.register("save file", [["AC_hotkey", {"keys": ["ctrl", "s"]}]])
25+
router.register("close window", [["AC_close_window", {}]])
26+
27+
router.dispatch("save the file") # fuzzy-matches -> runs the save actions
28+
29+
# with a real recogniser (any callable returning text):
30+
def vosk_listen() -> str:
31+
... # capture audio, return transcript
32+
router.listen_once(vosk_listen)
33+
34+
``dispatch`` (and ``listen_once``) accept a ``runner`` to execute the action list
35+
— it defaults to the executor; inject a fake to test routing without running real
36+
automation. ``match`` returns the best ``VoiceCommand`` at or above ``threshold``
37+
(or ``None``); ``register`` replaces an existing phrase; ``phrases`` / ``clear``
38+
inspect and reset.
39+
40+
Executor commands
41+
-----------------
42+
43+
A module-level default router backs the executor/MCP surfaces:
44+
45+
================================ ===================================================
46+
Command Effect
47+
================================ ===================================================
48+
``AC_voice_register`` Map a ``phrase`` to an ``actions`` list.
49+
``AC_voice_dispatch`` Run the command best matching recognized ``text``.
50+
``AC_voice_list`` List registered phrases.
51+
``AC_voice_clear`` Remove all registered commands.
52+
================================ ===================================================
53+
54+
``actions`` accepts a list or a JSON-string list (so the visual builder works).
55+
The same operations are exposed as MCP tools (``ac_voice_register`` /
56+
``ac_voice_dispatch`` / ``ac_voice_list`` / ``ac_voice_clear``) and as Script
57+
Builder commands under **Agent**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Comprehensive guides for all AutoControl features.
6666
doc/new_features/v41_features_doc
6767
doc/new_features/v42_features_doc
6868
doc/new_features/v43_features_doc
69+
doc/new_features/v44_features_doc
6970
doc/ocr_backends/ocr_backends_doc
7071
doc/observability/observability_doc
7172
doc/operations_layer/operations_layer_doc
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
語音指令路由器
2+
==============
3+
4+
``VoiceRouter`` 將語音觸發*片語*對應到 ``AC_*`` 動作清單:餵入一段已辨識語句的文字,它
5+
就會執行最接近的已註冊指令 —— 免手動觸發自動化流程。片語比對重用本專案的模糊比對器,因
6+
此即使辨識有雜訊,「save the file」仍會觸發 ``"save file"`` 指令。
7+
8+
語音轉文字刻意**不在範圍內且可注入**:路由器接受的是已辨識的*文字*。真實的麥克風/Vosk
9+
辨識器以 ``recognizer`` 可呼叫物件傳入 :meth:`VoiceRouter.listen_once`,如此路由邏輯可
10+
在無音訊、無任何語音相依的情況下完整單元測試。不匯入 ``PySide6``。
11+
12+
無頭 API
13+
--------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import VoiceRouter
18+
19+
router = VoiceRouter(threshold=0.7)
20+
router.register("save file", [["AC_hotkey", {"keys": ["ctrl", "s"]}]])
21+
router.register("close window", [["AC_close_window", {}]])
22+
23+
router.dispatch("save the file") # 模糊比對 -> 執行儲存動作
24+
25+
# 搭配真實辨識器(任何回傳文字的可呼叫物件):
26+
def vosk_listen() -> str:
27+
... # 擷取音訊、回傳逐字稿
28+
router.listen_once(vosk_listen)
29+
30+
``dispatch``(與 ``listen_once``)接受 ``runner`` 來執行動作清單 —— 預設為執行器;注入假
31+
物件即可在不執行真實自動化下測試路由。``match`` 回傳達到或高於 ``threshold`` 的最佳
32+
``VoiceCommand``(否則 ``None``);``register`` 會取代既有片語;``phrases`` / ``clear``
33+
則用於檢視與重置。
34+
35+
執行器指令
36+
----------
37+
38+
模組層級的預設路由器支撐 executor/MCP 介面:
39+
40+
================================ ===================================================
41+
指令 效果
42+
================================ ===================================================
43+
``AC_voice_register`` 將 ``phrase`` 對應到 ``actions`` 清單。
44+
``AC_voice_dispatch`` 執行最符合已辨識 ``text`` 的指令。
45+
``AC_voice_list`` 列出已註冊片語。
46+
``AC_voice_clear`` 移除所有已註冊指令。
47+
================================ ===================================================
48+
49+
``actions`` 接受清單或 JSON 字串清單(因此視覺化建構器可用)。相同操作亦提供為 MCP 工具
50+
(``ac_voice_register`` / ``ac_voice_dispatch`` / ``ac_voice_list`` /
51+
``ac_voice_clear``),以及 Script Builder 中 **Agent** 分類下的指令。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ AutoControl 所有功能的完整使用指南。
6666
doc/new_features/v41_features_doc
6767
doc/new_features/v42_features_doc
6868
doc/new_features/v43_features_doc
69+
doc/new_features/v44_features_doc
6970
doc/ocr_backends/ocr_backends_doc
7071
doc/observability/observability_doc
7172
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@
247247
from je_auto_control.utils.locale_parse import (
248248
format_currency, format_date, format_decimal, parse_decimal, parse_number,
249249
)
250+
# Voice-command router (injectable speech-to-text)
251+
from je_auto_control.utils.voice import (
252+
VoiceCommand, VoiceRouter, default_voice_router,
253+
)
250254
# Background popup/interrupt watchdog (unattended automation)
251255
from je_auto_control.utils.watchdog import (
252256
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -700,6 +704,7 @@ def start_autocontrol_gui(*args, **kwargs):
700704
"images_similar",
701705
"format_currency", "format_date", "format_decimal", "parse_decimal",
702706
"parse_number",
707+
"VoiceCommand", "VoiceRouter", "default_voice_router",
703708
# MCP server
704709
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
705710
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,31 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
994994
),
995995
description="Format an ISO date string for a locale.",
996996
))
997+
specs.append(CommandSpec(
998+
"AC_voice_register", "Agent", "Voice: Register Command",
999+
fields=(
1000+
FieldSpec("phrase", FieldType.STRING, placeholder="save file"),
1001+
FieldSpec("actions", FieldType.STRING,
1002+
placeholder='[["AC_hotkey", {"keys": ["ctrl", "s"]}]]'),
1003+
),
1004+
description="Map a spoken phrase to an action list (JSON).",
1005+
))
1006+
specs.append(CommandSpec(
1007+
"AC_voice_dispatch", "Agent", "Voice: Dispatch Text",
1008+
fields=(FieldSpec("text", FieldType.STRING,
1009+
placeholder="save the file"),),
1010+
description="Run the command best matching recognized text.",
1011+
))
1012+
specs.append(CommandSpec(
1013+
"AC_voice_list", "Agent", "Voice: List Commands",
1014+
fields=(),
1015+
description="List registered voice-command phrases.",
1016+
))
1017+
specs.append(CommandSpec(
1018+
"AC_voice_clear", "Agent", "Voice: Clear Commands",
1019+
fields=(),
1020+
description="Remove all registered voice commands.",
1021+
))
9971022
specs.append(CommandSpec(
9981023
"AC_generate_sop", "Report", "Generate SOP Document",
9991024
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,6 +3168,33 @@ def _format_date(value: str, locale: str = "en_US",
31683168
return {"text": format_date(value, locale, fmt)}
31693169

31703170

3171+
def _voice_register(phrase: str, actions: Any) -> Dict[str, Any]:
3172+
"""Adapter: register a voice command on the default router."""
3173+
from je_auto_control.utils.voice import default_voice_router
3174+
default_voice_router.register(phrase, _coerce_list(actions))
3175+
return {"phrases": default_voice_router.phrases()}
3176+
3177+
3178+
def _voice_dispatch(text: str) -> Dict[str, Any]:
3179+
"""Adapter: run the command best matching recognized ``text``."""
3180+
from je_auto_control.utils.voice import default_voice_router
3181+
outcome = default_voice_router.dispatch(text)
3182+
return {"matched": outcome["matched"], "phrase": outcome["phrase"]}
3183+
3184+
3185+
def _voice_list() -> Dict[str, Any]:
3186+
"""Adapter: list registered voice-command phrases."""
3187+
from je_auto_control.utils.voice import default_voice_router
3188+
return {"phrases": default_voice_router.phrases()}
3189+
3190+
3191+
def _voice_clear() -> Dict[str, Any]:
3192+
"""Adapter: clear all registered voice commands."""
3193+
from je_auto_control.utils.voice import default_voice_router
3194+
default_voice_router.clear()
3195+
return {"cleared": True}
3196+
3197+
31713198
class Executor:
31723199
"""
31733200
Executor
@@ -3434,6 +3461,10 @@ def __init__(self):
34343461
"AC_format_decimal": _format_decimal,
34353462
"AC_format_currency": _format_currency,
34363463
"AC_format_date": _format_date,
3464+
"AC_voice_register": _voice_register,
3465+
"AC_voice_dispatch": _voice_dispatch,
3466+
"AC_voice_list": _voice_list,
3467+
"AC_voice_clear": _voice_clear,
34373468
"AC_a11y_record_start": _a11y_record_start,
34383469
"AC_a11y_record_stop": _a11y_record_stop,
34393470
"AC_a11y_record_events": _a11y_record_events,

0 commit comments

Comments
 (0)