优化语音输入/普通文本输入

This commit is contained in:
minghanchen
2026-09-03 13:36:40 +08:00
parent 009d8ca73e
commit 32d179143f
26 changed files with 2318 additions and 53 deletions
@@ -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)