优化语音输入/普通文本输入
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)
|
||||
|
||||
Reference in New Issue
Block a user