优化语音输入/普通文本输入
This commit is contained in:
@@ -15,6 +15,12 @@ _FRACTION = re.compile(
|
||||
r"([零〇一二两三四五六七八九十百点\d.]+)"
|
||||
)
|
||||
_SLASH_FRACTION = re.compile(r"(?<!\d)(\d+)\s*/\s*(\d+)(?!\d)")
|
||||
_CHINESE_UNIT_VALUE = re.compile(
|
||||
r"(?P<value>[零〇一二两三四五六七八九十百]+"
|
||||
r"(?:点[零〇一二两三四五六七八九]+)?)"
|
||||
r"(?P<spacing>\s*)"
|
||||
r"(?P<unit>摄氏度|℃|度|档|级)"
|
||||
)
|
||||
_DIGITS = {
|
||||
"零": 0,
|
||||
"〇": 0,
|
||||
@@ -76,6 +82,24 @@ def _number(token: str) -> int | float | None:
|
||||
return int(value) if value.is_integer() else value
|
||||
|
||||
|
||||
def normalize_chinese_unit_values(text: str) -> str:
|
||||
"""Convert explicitly unit-qualified Chinese numbers to Arabic digits.
|
||||
|
||||
Keeping the unit in the match is intentional: discourse words such as
|
||||
``一下`` or ``一点`` must not accidentally become control values. Both
|
||||
typed text and ASR text can call this helper before their domain grammar,
|
||||
so value recognition does not depend on which input path was used.
|
||||
"""
|
||||
|
||||
def replace(match: re.Match[str]) -> str:
|
||||
value = _number(match.group("value"))
|
||||
if value is None:
|
||||
return match.group(0)
|
||||
return f"{value}{match.group('spacing')}{match.group('unit')}"
|
||||
|
||||
return _CHINESE_UNIT_VALUE.sub(replace, text)
|
||||
|
||||
|
||||
def parse_percentage_value(text: str) -> int | float | None:
|
||||
"""Return an explicitly spoken percentage/ratio/extreme position.
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from cognitive_agent.application.chinese_values import parse_percentage_value
|
||||
from cognitive_agent.application.chinese_values import (
|
||||
normalize_chinese_unit_values,
|
||||
parse_percentage_value,
|
||||
)
|
||||
|
||||
WINDOW_TARGETS = {
|
||||
"全部": "all",
|
||||
@@ -177,7 +180,8 @@ _EXPLICIT_TARGET_PATTERNS = {
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"seat": re.compile(
|
||||
r"主驾|驾驶位|驾驶席|司机位|左前(?:座|座椅|座位)?|"
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|驾驶座(?:椅)?|"
|
||||
r"司机位|司机座(?:椅)?|左前(?:座|座椅|座位)?|"
|
||||
r"副驾(?:驶|位)?|乘客(?:位|座|座椅|座位)?|右前(?:座|座椅|座位)?|"
|
||||
r"前排|后排|左后|右后|所有座椅|全部座椅|全车座椅|"
|
||||
r"seat\s+(?:driver|front-left|passenger|front-right)",
|
||||
@@ -561,7 +565,11 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
clause = f"{clause}{continuation}"
|
||||
continuation_index += 1
|
||||
index = continuation_index - 1
|
||||
if re.search(r"主驾|驾驶位|司机位|左前座椅", clause):
|
||||
if re.search(
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|"
|
||||
r"驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前座椅",
|
||||
clause,
|
||||
):
|
||||
inherited_driver_scope = True
|
||||
elif re.search(r"副驾|副驾驶|乘客|右前", clause):
|
||||
inherited_driver_scope = False
|
||||
@@ -592,7 +600,13 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
re.IGNORECASE,
|
||||
):
|
||||
return []
|
||||
explicit_driver = bool(re.search(r"主驾|驾驶位|司机位|左前座椅", compact))
|
||||
explicit_driver = bool(
|
||||
re.search(
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|"
|
||||
r"驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前座椅",
|
||||
compact,
|
||||
)
|
||||
)
|
||||
if not explicit_driver and not _looks_like_seat_request(compact):
|
||||
return []
|
||||
default_reason = "用户未指定座椅目标,按驾驶台 CH1 安全默认选择主驾座椅。"
|
||||
@@ -950,7 +964,7 @@ def _parse_chinese_window_commands(compact: str) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
compact = re.sub(r"\s+", "", text)
|
||||
compact = re.sub(r"\s+", "", normalize_chinese_unit_values(text))
|
||||
seat_text = _scoped_text(
|
||||
compact,
|
||||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
|
||||
@@ -981,7 +995,7 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
|
||||
temperature = re.search(
|
||||
r"(?:空调(?:温度)?|温度)(?:都)?"
|
||||
r"(?:调整到|调整为|调到|调为|设为|设置为|调至|到|为)?"
|
||||
r"(?:调整到|调整为|调到|调成|调为|设到|设为|设置为|调至|到|为)?"
|
||||
r"(1[89]|2\d|3[0-2])(?:摄氏)?(?:℃|度)?(?![\d.])",
|
||||
climate_text,
|
||||
)
|
||||
@@ -1340,6 +1354,7 @@ def _drop_implicit_passenger_ch0_candidates(
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "seat.target_scope",
|
||||
"reason": "implicit_driver_default_unresolved",
|
||||
"question": (
|
||||
"未指定座椅目标时默认主驾 CH1;当前请求无法安全映射为已验证的主驾能力,"
|
||||
"且不会转入副驾 CH0。"
|
||||
|
||||
@@ -133,7 +133,9 @@ CONVERSATION_ID_PATTERN = re.compile(r"^conv-[a-z0-9-]{6,80}$")
|
||||
MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
|
||||
SEAT_SCOPE_PATTERN = re.compile(r"\bseats?\b|座椅|坐椅", re.IGNORECASE)
|
||||
DRIVER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|主驾|驾驶位|司机位|左前",
|
||||
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|"
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|驾驶座(?:椅)?|"
|
||||
r"司机位|司机座(?:椅)?|左前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
@@ -2257,13 +2259,9 @@ class SeatDemoService:
|
||||
local_preview=local_preview,
|
||||
model_parse_source=model_parse_source,
|
||||
)
|
||||
if self._is_dynamic_mode_stop_request(utterance):
|
||||
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
||||
if dynamic_mode_stop:
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
else:
|
||||
parsed = self._apply_mode_profiles(
|
||||
parsed, matched_modes, context, vehicle_profile
|
||||
)
|
||||
parsed = self._mark_control_parse_source(parsed, "local_rule")
|
||||
relative_resolution = (
|
||||
self.native_can_relative_resolver.resolve(utterance, context)
|
||||
if self.native_can_relative_resolver.matches(utterance)
|
||||
@@ -2282,6 +2280,19 @@ class SeatDemoService:
|
||||
relative_resolution.claimed_controls if relative_resolution else None
|
||||
),
|
||||
)
|
||||
if not dynamic_mode_stop:
|
||||
# Apply repository-backed modes after the final native-CAN
|
||||
# merge. Otherwise that merge can discard a valid passenger
|
||||
# mode operation while enforcing its generic unscoped-seat
|
||||
# CH1 default.
|
||||
parsed = self._apply_mode_profiles(
|
||||
parsed,
|
||||
matched_modes,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=utterance,
|
||||
)
|
||||
parsed = self._mark_control_parse_source(parsed, "local_rule")
|
||||
if direct_mode_profile is not None:
|
||||
parsed = self._mark_control_parse_source(
|
||||
parsed,
|
||||
@@ -3327,6 +3338,8 @@ class SeatDemoService:
|
||||
matched_modes: list[ModeProfile],
|
||||
context: dict[str, Any],
|
||||
vehicle_profile: VehicleProfile,
|
||||
*,
|
||||
utterance: str,
|
||||
) -> dict[str, Any]:
|
||||
if not matched_modes:
|
||||
return parsed
|
||||
@@ -3354,6 +3367,12 @@ class SeatDemoService:
|
||||
]
|
||||
return rejected
|
||||
|
||||
parsed = self._apply_implicit_mode_target_policy(
|
||||
parsed,
|
||||
matched_modes,
|
||||
utterance,
|
||||
)
|
||||
|
||||
safety_ambiguities = [
|
||||
item
|
||||
for item in parsed.get("ambiguities", [])
|
||||
@@ -3430,6 +3449,38 @@ class SeatDemoService:
|
||||
)
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _apply_implicit_mode_target_policy(
|
||||
parsed: dict[str, Any],
|
||||
matched_modes: list[ModeProfile],
|
||||
utterance: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Let one matched mode own only an omitted seat target.
|
||||
|
||||
The native CH1 parser can add an implicit-driver ambiguity before or
|
||||
after mode injection. A unique repository-backed mode already carries
|
||||
authoritative target scopes on every operation, so that one tagged
|
||||
default is redundant. Explicit scopes and all other safety questions
|
||||
remain untouched.
|
||||
"""
|
||||
|
||||
if len(matched_modes) != 1 or (
|
||||
DRIVER_SEAT_SCOPE_PATTERN.search(utterance)
|
||||
or PASSENGER_SEAT_SCOPE_PATTERN.search(utterance)
|
||||
):
|
||||
return parsed
|
||||
ambiguities = list(parsed.get("ambiguities", []))
|
||||
filtered = [
|
||||
item
|
||||
for item in ambiguities
|
||||
if item.get("reason") != "implicit_driver_default_unresolved"
|
||||
]
|
||||
if len(filtered) == len(ambiguities):
|
||||
return parsed
|
||||
resolved = deepcopy(parsed)
|
||||
resolved["ambiguities"] = filtered
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _is_dynamic_mode_stop_request(utterance: str) -> bool:
|
||||
compact = re.sub(r"\s+", "", utterance)
|
||||
|
||||
@@ -165,6 +165,7 @@ class CapabilityResolver:
|
||||
*,
|
||||
target_scope: str | None,
|
||||
target_explicit: bool,
|
||||
action: str = "apply",
|
||||
) -> ModeResolution:
|
||||
matches = self.mode_profiles.match_text(text, vehicle_id="denza_n7")
|
||||
if not matches:
|
||||
@@ -172,7 +173,24 @@ class CapabilityResolver:
|
||||
if len(matches) > 1:
|
||||
return ModeResolution("ambiguous", None, ("multiple_mode_profiles_matched",))
|
||||
profile = matches[0]
|
||||
operation_scopes = {operation.target_scope for operation in profile.operations}
|
||||
selected_operations = profile.operations
|
||||
if action == "stop":
|
||||
dynamic_operations = tuple(
|
||||
operation
|
||||
for operation in profile.operations
|
||||
if operation.capability == "seat.start_dynamic_mode"
|
||||
)
|
||||
if not dynamic_operations:
|
||||
return ModeResolution(
|
||||
"unsupported",
|
||||
profile,
|
||||
(f"mode_profile.{profile.id}不支持关闭指令",),
|
||||
)
|
||||
selected_operations = dynamic_operations
|
||||
|
||||
operation_scopes = {
|
||||
operation.target_scope for operation in selected_operations
|
||||
}
|
||||
if target_explicit and target_scope not in operation_scopes:
|
||||
scopes = ",".join(sorted(operation_scopes))
|
||||
return ModeResolution(
|
||||
@@ -183,22 +201,28 @@ class CapabilityResolver:
|
||||
|
||||
payloads: list[dict[str, Any]] = []
|
||||
issues: list[str] = []
|
||||
for operation in profile.operations:
|
||||
for operation in selected_operations:
|
||||
capability = (
|
||||
"seat.stop_dynamic_mode"
|
||||
if action == "stop"
|
||||
else operation.capability
|
||||
)
|
||||
parameters = {} if action == "stop" else operation.parameters
|
||||
resolution = self.resolve(
|
||||
domain=operation.domain,
|
||||
semantic_property="",
|
||||
target_scope=operation.target_scope,
|
||||
target_device=None,
|
||||
operation=operation.operation,
|
||||
parameters=operation.parameters,
|
||||
operation="set" if action == "stop" else operation.operation,
|
||||
parameters=parameters,
|
||||
source_text=text,
|
||||
preferred_capability=operation.capability,
|
||||
preferred_capability=capability,
|
||||
)
|
||||
if resolution.status != "executable" or resolution.executor_payload is None:
|
||||
issues.extend(resolution.issues or ("mode_operation_not_executable",))
|
||||
continue
|
||||
payloads.append(resolution.executor_payload)
|
||||
if issues or len(payloads) != len(profile.operations):
|
||||
if issues or len(payloads) != len(selected_operations):
|
||||
return ModeResolution("unsupported", profile, tuple(dict.fromkeys(issues)))
|
||||
return ModeResolution("executable", profile, executor_payloads=tuple(payloads))
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from cognitive_agent.application.chinese_values import parse_percentage_value
|
||||
from cognitive_agent.application.chinese_values import (
|
||||
normalize_chinese_unit_values,
|
||||
parse_percentage_value,
|
||||
)
|
||||
from cognitive_agent.application.native_can_intent import merge_native_can_intent
|
||||
from cognitive_agent.infrastructure.speech.capability import CapabilityResolver
|
||||
from cognitive_agent.infrastructure.speech.contracts import (
|
||||
@@ -858,11 +861,13 @@ class UnderstandingTranscriptProcessor:
|
||||
|
||||
mode_profile = self.resolver.match_mode(text)
|
||||
if mode_profile is not None:
|
||||
mode_operation = "stop" if action == "off" else "apply"
|
||||
mode_started = time.perf_counter()
|
||||
mode_resolution = self.resolver.resolve_mode(
|
||||
text,
|
||||
target_scope=scope,
|
||||
target_explicit=explicit_scope,
|
||||
action=mode_operation,
|
||||
)
|
||||
resolution_ms += (time.perf_counter() - mode_started) * 1000
|
||||
mode_issues = tuple(mode_resolution.issues)
|
||||
@@ -873,7 +878,7 @@ class UnderstandingTranscriptProcessor:
|
||||
target_scope=command_scope,
|
||||
target_device="seat",
|
||||
property_name="seat.mode",
|
||||
operation="apply",
|
||||
operation=mode_operation,
|
||||
parameters={"mode_profile_id": mode_profile.id},
|
||||
value=mode_profile.name,
|
||||
unit=None,
|
||||
@@ -954,6 +959,14 @@ class UnderstandingTranscriptProcessor:
|
||||
parameters["state"] = action if action in {"on", "off"} else "on"
|
||||
if value is not None:
|
||||
parameters["level"] = int(value)
|
||||
elif action == "on":
|
||||
# The passenger-seat text parser executes a bare ON at
|
||||
# its lowest supported level; verified CH1 driver control
|
||||
# uses level 2 for the same language. Preserve the
|
||||
# target-specific executable default in speech semantics,
|
||||
# but keep clean_text faithful to what the user said.
|
||||
parameters["level"] = 2 if scope == "driver" else 1
|
||||
qualifiers["implicit_parameters"] = ["level"]
|
||||
else:
|
||||
local = ("seat_thermal_action_incomplete",)
|
||||
commands.append(
|
||||
@@ -1317,7 +1330,7 @@ class UnderstandingTranscriptProcessor:
|
||||
|
||||
@staticmethod
|
||||
def _numeric_value(text: str) -> int | float | None:
|
||||
text = _NON_NUMERIC_ONE.sub("", text)
|
||||
text = normalize_chinese_unit_values(_NON_NUMERIC_ONE.sub("", text))
|
||||
match = _NUMBER.search(text)
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
@@ -1415,6 +1428,8 @@ class UnderstandingTranscriptProcessor:
|
||||
return f"将{self._scoped_target(command, feature)}设置为{level}"
|
||||
return f"{'打开' if command.operation == 'on' else '关闭'}{self._scoped_target(command, feature)}"
|
||||
if prop == "seat.mode":
|
||||
if command.operation == "stop":
|
||||
return f"关闭{command.value}"
|
||||
return f"将{self._scoped_target(command, '座椅')}调整为{command.value}"
|
||||
if prop == "seat.backrest_recline":
|
||||
direction = (
|
||||
|
||||
@@ -212,6 +212,9 @@ ACTION_ALIASES: dict[str, tuple[str, ...]] = {
|
||||
"调成",
|
||||
"调为",
|
||||
"变成",
|
||||
"进入",
|
||||
"切换到",
|
||||
"切换为",
|
||||
"设为",
|
||||
"设成",
|
||||
"设到",
|
||||
|
||||
@@ -532,6 +532,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
speech_utterance_id = str(
|
||||
payload.get("speech_utterance_id", "")
|
||||
).strip()
|
||||
speech_understanding: dict[str, object] | None = None
|
||||
speech_peripheral_route = False
|
||||
if speech_utterance_id:
|
||||
if path != "/api/requests":
|
||||
raise ValueError(
|
||||
@@ -540,13 +542,28 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
speech_understanding = self.speech.cached_result(
|
||||
speech_utterance_id
|
||||
)
|
||||
if str(speech_understanding.get("clean_text", "")).strip() != utterance:
|
||||
clean_text = str(
|
||||
speech_understanding.get("clean_text", "")
|
||||
).strip()
|
||||
raw_text = str(
|
||||
speech_understanding.get("raw_text", "")
|
||||
).strip()
|
||||
peripheral_routing = self._speech_peripheral_routing(raw_text)
|
||||
peripheral_transcript = bool(
|
||||
peripheral_routing.get("execution_eligible") and raw_text
|
||||
)
|
||||
expected_text = raw_text if peripheral_transcript else clean_text
|
||||
if utterance != expected_text:
|
||||
raise SpeechRecognitionError(
|
||||
"speech_text_changed",
|
||||
"语音文本已被修改,请确认修改后的内容再执行。",
|
||||
status=409,
|
||||
)
|
||||
if speech_understanding.get("execution_eligible") is not True:
|
||||
speech_peripheral_route = peripheral_transcript
|
||||
if (
|
||||
speech_understanding.get("execution_eligible") is not True
|
||||
and not speech_peripheral_route
|
||||
):
|
||||
raise SpeechRecognitionError(
|
||||
"speech_not_executable",
|
||||
"语音意图尚未通过能力或参数校验,已拒绝执行。",
|
||||
@@ -554,7 +571,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
provider = "local"
|
||||
common_arguments["provider"] = provider
|
||||
common_arguments["_speech_understanding"] = speech_understanding
|
||||
if not speech_peripheral_route:
|
||||
common_arguments["_speech_understanding"] = speech_understanding
|
||||
if (
|
||||
path != "/api/can/commands"
|
||||
and "application/x-ndjson" in self.headers.get("Accept", "")
|
||||
@@ -620,6 +638,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
else:
|
||||
result = self.service.process(**common_arguments)
|
||||
if speech_peripheral_route and speech_understanding is not None:
|
||||
result["speech_understanding"] = {
|
||||
"utterance_id": speech_utterance_id,
|
||||
"raw_text": speech_understanding.get("raw_text"),
|
||||
"clean_text": speech_understanding.get("clean_text"),
|
||||
"semantic_status": speech_understanding.get("semantic_status"),
|
||||
"routing": "validated_peripheral_text",
|
||||
}
|
||||
utterance = str(result.get("utterance") or utterance)
|
||||
self.vehicle_display_sync.update_task(
|
||||
status=str(result.get("status", "succeeded")),
|
||||
@@ -815,6 +841,54 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
return result
|
||||
|
||||
def _speech_peripheral_routing(self, utterance: str) -> dict[str, object]:
|
||||
"""Validate whether an ASR transcript may enter peripheral routing.
|
||||
|
||||
Vehicle speech keeps using its cached structured semantic payload. A
|
||||
learned peripheral cannot be represented by the static vehicle
|
||||
capability catalog, so it may instead reuse the exact server-owned ASR
|
||||
transcript only after the live NFC binding and learned action resolve.
|
||||
"""
|
||||
|
||||
text = str(utterance or "").strip()
|
||||
if not text:
|
||||
return {"execution_eligible": False, "action_count": 0}
|
||||
try:
|
||||
catalog = self.extension.list_peripheral_capabilities()
|
||||
nfc_snapshot = self.nfc.snapshot()
|
||||
partition = partition_active_peripheral_intent(
|
||||
text,
|
||||
catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
entries = list(partition.get("peripheral_actions") or [])
|
||||
unresolved = list(
|
||||
partition.get("unresolved_peripheral_clauses") or []
|
||||
)
|
||||
denied = list(partition.get("denied_peripheral_actions") or [])
|
||||
plan_catalog = active_peripheral_plan_catalog(
|
||||
text,
|
||||
catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
direct = None
|
||||
if not entries and not plan_catalog and not unresolved and not denied:
|
||||
direct = resolve_active_peripheral_command(
|
||||
text,
|
||||
catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
except (ExtensionIntegrationError, GatewayError, ValueError):
|
||||
return {"execution_eligible": False, "action_count": 0}
|
||||
action_count = len(entries) or (1 if direct is not None else 0)
|
||||
if plan_catalog:
|
||||
action_count = max(action_count, 1)
|
||||
return {
|
||||
"execution_eligible": bool(action_count and not unresolved and not denied),
|
||||
"action_count": action_count,
|
||||
"has_ordered_plan": bool(plan_catalog),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _residual_has_vehicle_language(utterance: str) -> bool:
|
||||
"""Recognize only obvious vehicle-domain text in a claimed mixed request."""
|
||||
@@ -2206,6 +2280,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
self.rfile.read(content_length),
|
||||
utterance_id=self.headers.get("X-Speech-Utterance-Id"),
|
||||
)
|
||||
raw_text = str(result.get("raw_text") or "").strip()
|
||||
peripheral_routing = self._speech_peripheral_routing(raw_text)
|
||||
if peripheral_routing.get("execution_eligible"):
|
||||
result = {
|
||||
**result,
|
||||
"routing_text": raw_text,
|
||||
"peripheral_routing": peripheral_routing,
|
||||
}
|
||||
self._json(result)
|
||||
except SpeechRecognitionError as exc:
|
||||
self._json(
|
||||
|
||||
Reference in New Issue
Block a user