673 lines
31 KiB
Python
Executable File
673 lines
31 KiB
Python
Executable File
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from seat_domain.intent import parse_local_intent
|
|
|
|
|
|
class LocalIntentParserTests(unittest.TestCase):
|
|
@staticmethod
|
|
def context(level: int = 0) -> dict:
|
|
return {
|
|
"last_target": "passenger" if level else None,
|
|
"devices": {
|
|
"driver": {
|
|
"ventilation": {"state": "off", "level": 0},
|
|
"heating": {"state": "off", "level": 0},
|
|
},
|
|
"passenger": {
|
|
"ventilation": {"state": "on" if level else "off", "level": level},
|
|
"heating": {"state": "off", "level": 0},
|
|
},
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def motion_context(cls) -> dict:
|
|
context = cls.context()
|
|
context["devices"]["passenger"]["position"] = {
|
|
"longitudinal": {"position_pct": 75},
|
|
"cushion_tilt": {"position_pct": 60},
|
|
"backrest_recline": {"position_pct": 51, "angle_deg": 130},
|
|
"legrest_height": {"position_pct": 69, "angle_deg": 145},
|
|
}
|
|
return context
|
|
|
|
def test_parses_passenger_ventilation_on(self) -> None:
|
|
result = parse_local_intent("打开副驾座椅通风")
|
|
|
|
self.assertEqual(
|
|
result["actions"],
|
|
[
|
|
{
|
|
"target": "passenger",
|
|
"function": "ventilation",
|
|
"state": "on",
|
|
"requested_level": 1,
|
|
}
|
|
],
|
|
)
|
|
self.assertEqual(result["ambiguities"], [])
|
|
|
|
def test_rejects_scope_that_includes_driver(self) -> None:
|
|
result = parse_local_intent("主副驾座椅通风都打开")
|
|
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
|
self.assertIn("仅控制副驾", result["ambiguities"][0]["question"])
|
|
|
|
def test_keeps_requested_level_for_safety_validation(self) -> None:
|
|
result = parse_local_intent("副驾座椅加热开到三档")
|
|
|
|
self.assertEqual(result["actions"][0]["requested_level"], 3)
|
|
|
|
def test_defaults_missing_target_to_passenger(self) -> None:
|
|
result = parse_local_intent("打开座椅通风")
|
|
|
|
self.assertEqual(result["actions"][0]["target"], "passenger")
|
|
self.assertEqual(result["ambiguities"], [])
|
|
|
|
def test_non_seat_spatial_qualifiers_do_not_enter_can0_seat_rules(self) -> None:
|
|
utterances = (
|
|
"打开主驾车窗",
|
|
"打开副驾车窗",
|
|
"把主驾和副驾温度都设为24度",
|
|
"风量调到2档",
|
|
"设置主驾出风模式为吹脸",
|
|
"把副驾车窗移动到30%",
|
|
"将主驾空调温度设置为24度",
|
|
"把遮阳帘转动到20%",
|
|
)
|
|
for utterance in utterances:
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance)
|
|
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"], [])
|
|
|
|
def test_parses_four_axis_zero_for_passenger(self) -> None:
|
|
result = parse_local_intent("把水平、坐盆、靠背、腿托四轴归零")
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(
|
|
[(action["axis"], action["position_pct"]) for action in result["actions"]],
|
|
[
|
|
("longitudinal", 0),
|
|
("cushion_tilt", 0),
|
|
("backrest_recline", 0),
|
|
("legrest_height", 0),
|
|
],
|
|
)
|
|
self.assertTrue(
|
|
all(action["target"] == "passenger" for action in result["actions"])
|
|
)
|
|
|
|
def test_parses_explicit_motion_percentages(self) -> None:
|
|
result = parse_local_intent("副驾水平移到63%,靠背调到54%")
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(
|
|
[(action["axis"], action["position_pct"]) for action in result["actions"]],
|
|
[("longitudinal", 63), ("backrest_recline", 54)],
|
|
)
|
|
|
|
def test_normalizes_target_level_and_fullwidth_percentage_aliases(self) -> None:
|
|
thermal = parse_local_intent("右前座加热设置为中档")
|
|
motion = parse_local_intent("副驾驶滑轨调到百分之五十")
|
|
|
|
self.assertEqual(thermal["actions"][0]["requested_level"], 2)
|
|
self.assertEqual(motion["actions"][0]["axis"], "longitudinal")
|
|
self.assertEqual(motion["actions"][0]["position_pct"], 50)
|
|
|
|
def test_converts_backrest_angle_to_calibrated_percentage(self) -> None:
|
|
result = parse_local_intent("把副驾驶椅背设为130°")
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(result["actions"][0]["axis"], "backrest_recline")
|
|
self.assertEqual(result["actions"][0]["position_pct"], 51)
|
|
|
|
def test_parses_relative_backrest_angle_from_current_state(self) -> None:
|
|
result = parse_local_intent("靠背向前移动10度", self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["axis"], "backrest_recline")
|
|
self.assertEqual(action["position_pct"], 36)
|
|
self.assertEqual(action["angle_deg"], 120)
|
|
self.assertEqual(
|
|
action["motion_intent"],
|
|
{
|
|
"mode": "relative",
|
|
"direction": "decrease",
|
|
"start_position_pct": 51,
|
|
"delta_position_pct": 15,
|
|
"start_angle_deg": 130.0,
|
|
"delta_angle_deg": 10.0,
|
|
},
|
|
)
|
|
|
|
def test_parses_relative_direction_synonyms_and_chinese_angle(self) -> None:
|
|
backward = parse_local_intent("椅背后仰十度", self.motion_context())
|
|
legrest = parse_local_intent("腿托向下调10度", self.motion_context())
|
|
|
|
self.assertEqual(backward["actions"][0]["position_pct"], 66)
|
|
self.assertEqual(backward["actions"][0]["angle_deg"], 140)
|
|
self.assertEqual(legrest["actions"][0]["position_pct"], 55)
|
|
self.assertEqual(legrest["actions"][0]["angle_deg"], 135)
|
|
|
|
def test_parses_relative_percentage_for_linear_axis(self) -> None:
|
|
result = parse_local_intent("副驾滑轨向前移动10%", self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(result["actions"][0]["position_pct"], 65)
|
|
self.assertEqual(result["actions"][0]["motion_intent"]["direction"], "decrease")
|
|
|
|
def test_parses_passenger_directional_relative_percentages(self) -> None:
|
|
context = self.motion_context()
|
|
for state in context["devices"]["passenger"]["position"].values():
|
|
state["position_pct"] = 50
|
|
cases = (
|
|
("副驾座椅往上移动10%", "cushion_tilt", "decrease", 10),
|
|
("副驾座椅往上移动20%", "cushion_tilt", "decrease", 20),
|
|
("副驾往下移动10%", "cushion_tilt", "increase", 10),
|
|
("副驾往下移动20%", "cushion_tilt", "increase", 20),
|
|
("副驾座椅往前移动10%", "longitudinal", "decrease", 10),
|
|
("副驾座椅往前移动20%", "longitudinal", "decrease", 20),
|
|
("副驾往后移动10%", "longitudinal", "increase", 10),
|
|
("副驾往后移动20%", "longitudinal", "increase", 20),
|
|
("副驾座椅靠背往前移动10%", "backrest_recline", "decrease", 10),
|
|
("副驾座椅靠背往前移动30%", "backrest_recline", "decrease", 30),
|
|
("副驾靠背往后移动10%", "backrest_recline", "increase", 10),
|
|
("副驾靠背往后移动30%", "backrest_recline", "increase", 30),
|
|
("副驾推托往上移动20%", "legrest_height", "increase", 20),
|
|
("副驾推托往上移动40%", "legrest_height", "increase", 40),
|
|
("副驾推托往下移动20%", "legrest_height", "decrease", 20),
|
|
("副驾推托往下移动40%", "legrest_height", "decrease", 40),
|
|
)
|
|
|
|
for utterance, axis, direction, delta in cases:
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, context)
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["axis"], axis)
|
|
self.assertEqual(action["motion_intent"]["mode"], "relative")
|
|
self.assertEqual(action["motion_intent"]["direction"], direction)
|
|
self.assertEqual(action["motion_intent"]["delta_position_pct"], delta)
|
|
|
|
def test_motion_verbs_are_weak_signals_for_relative_seat_controls(self) -> None:
|
|
context = self.motion_context()
|
|
for state in context["devices"]["passenger"]["position"].values():
|
|
state["position_pct"] = 50
|
|
cases = (
|
|
("副驾座椅往前移动10%", "longitudinal", "decrease", 10),
|
|
("副驾靠背往后转动20%", "backrest_recline", "increase", 20),
|
|
("副驾腿托往上调节30%", "legrest_height", "increase", 30),
|
|
("副驾座椅高度往下设置10%", "cushion_tilt", "increase", 10),
|
|
("副驾靠背转动往前20%", "backrest_recline", "decrease", 20),
|
|
("副驾腿托设置往下30%", "legrest_height", "decrease", 30),
|
|
("副驾靠背往后旋转10度", "backrest_recline", "increase", 10),
|
|
)
|
|
|
|
for utterance, axis, direction, amount in cases:
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, context)
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["axis"], axis)
|
|
self.assertEqual(action["motion_intent"]["mode"], "relative")
|
|
self.assertEqual(action["motion_intent"]["direction"], direction)
|
|
motion = action["motion_intent"]
|
|
requested = motion.get("requested_delta_angle_deg")
|
|
if requested is None:
|
|
requested = motion.get("requested_delta_position_pct")
|
|
if requested is None:
|
|
requested = motion.get(
|
|
"delta_angle_deg", motion.get("delta_position_pct")
|
|
)
|
|
self.assertEqual(requested, amount)
|
|
|
|
def test_directional_move_to_percentage_is_absolute_not_relative(self) -> None:
|
|
cases = (
|
|
("副驾座椅往上移动到10%", "cushion_tilt", 10),
|
|
("副驾座椅往上移动到20%", "cushion_tilt", 20),
|
|
("副驾往下移动到10%", "cushion_tilt", 10),
|
|
("副驾往下移动到20%", "cushion_tilt", 20),
|
|
("副驾座椅往前移动到10%", "longitudinal", 10),
|
|
("副驾座椅往前移动到20%", "longitudinal", 20),
|
|
("副驾往后移动到10%", "longitudinal", 10),
|
|
("副驾往后移动到20%", "longitudinal", 20),
|
|
("副驾座椅靠背往前移动到10%", "backrest_recline", 10),
|
|
("副驾座椅靠背往前移动到30%", "backrest_recline", 30),
|
|
("副驾靠背往后移动到10%", "backrest_recline", 10),
|
|
("副驾靠背往后移动到30%", "backrest_recline", 30),
|
|
("副驾推托往上移动到20%", "legrest_height", 20),
|
|
("副驾推托往上移动到40%", "legrest_height", 40),
|
|
("副驾推托往下移动到20%", "legrest_height", 20),
|
|
("副驾推托往下移动到40%", "legrest_height", 40),
|
|
)
|
|
|
|
for utterance, axis, position in cases:
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["axis"], axis)
|
|
self.assertEqual(action["position_pct"], position)
|
|
self.assertNotIn("motion_intent", action)
|
|
|
|
def test_motion_verbs_with_position_markers_are_absolute(self) -> None:
|
|
cases = (
|
|
("副驾滑轨移动至60%", "longitudinal", 60),
|
|
("副驾靠背转动到30%", "backrest_recline", 30),
|
|
("副驾腿托调节至40%", "legrest_height", 40),
|
|
("副驾座椅高度设置为20%", "cushion_tilt", 20),
|
|
("副驾靠背往后转动到35%", "backrest_recline", 35),
|
|
("副驾腿托往上设置为45%", "legrest_height", 45),
|
|
("副驾靠背旋转到130度", "backrest_recline", 51),
|
|
)
|
|
|
|
for utterance, axis, position in cases:
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["axis"], axis)
|
|
self.assertEqual(action["position_pct"], position)
|
|
self.assertNotIn("motion_intent", action)
|
|
|
|
def test_explicit_legrest_height_beats_generic_seat_height_alias(self) -> None:
|
|
for utterance in (
|
|
"副驾腿托高度调到50%",
|
|
"副驾小腿托高度设置为50%",
|
|
"脚托高度调至50%",
|
|
"腿部支撑高度设置到50%",
|
|
):
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
self.assertEqual(result["actions"][0]["axis"], "legrest_height")
|
|
self.assertEqual(result["actions"][0]["position_pct"], 50)
|
|
|
|
seat_height = parse_local_intent("副驾高度调到25%", self.motion_context())
|
|
self.assertEqual(len(seat_height["actions"]), 1)
|
|
self.assertEqual(seat_height["actions"][0]["axis"], "cushion_tilt")
|
|
self.assertEqual(seat_height["actions"][0]["position_pct"], 25)
|
|
|
|
def test_rejects_relative_motion_without_state_and_clamps_endpoint_overflow(self) -> None:
|
|
missing = parse_local_intent("靠背向前移动10度")
|
|
context = self.motion_context()
|
|
context["devices"]["passenger"]["position"]["backrest_recline"] = {
|
|
"position_pct": 0,
|
|
"angle_deg": 95,
|
|
}
|
|
overflow = parse_local_intent("靠背向前移动10度", context)
|
|
|
|
self.assertEqual(missing["actions"], [])
|
|
self.assertIn("缺少当前座椅位置", missing["ambiguities"][0]["question"])
|
|
self.assertEqual(overflow["ambiguities"], [])
|
|
self.assertEqual(overflow["actions"][0]["position_pct"], 0)
|
|
self.assertTrue(overflow["actions"][0]["motion_intent"]["range_clamped"])
|
|
self.assertIn("最前方", overflow["actions"][0]["user_notice"])
|
|
|
|
def test_clamps_oversized_relative_angle_to_backrest_endpoint(self) -> None:
|
|
context = self.motion_context()
|
|
context["devices"]["passenger"]["position"]["backrest_recline"] = {
|
|
"position_pct": 0,
|
|
"angle_deg": 95,
|
|
}
|
|
|
|
result = parse_local_intent("靠背向后移动70度", context)
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["position_pct"], 100)
|
|
self.assertEqual(action["angle_deg"], 163)
|
|
self.assertEqual(action["motion_intent"]["requested_delta_angle_deg"], 70)
|
|
self.assertEqual(action["motion_intent"]["delta_angle_deg"], 68)
|
|
self.assertIn("备用目标", action["user_notice"])
|
|
|
|
def test_parses_and_clamps_user_timed_motion(self) -> None:
|
|
result = parse_local_intent("靠背向后移动70秒", self.motion_context())
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
action = result["actions"][0]
|
|
self.assertEqual(action["position_pct"], 100)
|
|
self.assertEqual(
|
|
action["motion_intent"],
|
|
{
|
|
"mode": "timed",
|
|
"direction": "increase",
|
|
"start_position_pct": 51,
|
|
"delta_position_pct": 49,
|
|
"requested_duration_ms": 70_000,
|
|
"applied_duration_ms": 30_000,
|
|
"max_duration_ms": 30_000,
|
|
"min_duration_ms": 300,
|
|
"duration_clamped": True,
|
|
"endpoint_expected": "far",
|
|
},
|
|
)
|
|
self.assertIn("最大允许时长为30秒", action["user_notice"])
|
|
|
|
def test_supports_chinese_seconds_minutes_and_minimum_duration(self) -> None:
|
|
chinese = parse_local_intent("椅背后仰两秒", self.motion_context())
|
|
minute = parse_local_intent("座椅向前移动1分钟", self.motion_context())
|
|
minimum = parse_local_intent("坐垫向下移动0.1秒", self.motion_context())
|
|
|
|
self.assertEqual(chinese["actions"][0]["motion_intent"]["applied_duration_ms"], 2_000)
|
|
self.assertEqual(minute["actions"][0]["axis"], "longitudinal")
|
|
self.assertEqual(minute["actions"][0]["position_pct"], 0)
|
|
self.assertEqual(minute["actions"][0]["motion_intent"]["applied_duration_ms"], 30_000)
|
|
self.assertEqual(minimum["actions"][0]["motion_intent"]["applied_duration_ms"], 300)
|
|
|
|
def test_clamps_absolute_position_and_angle_to_endpoint(self) -> None:
|
|
percentage = parse_local_intent("副驾水平位置调到101%")
|
|
angle = parse_local_intent("副驾靠背调到200度")
|
|
|
|
self.assertEqual(percentage["ambiguities"], [])
|
|
self.assertEqual(percentage["actions"][0]["position_pct"], 100)
|
|
self.assertIn("备用目标", percentage["actions"][0]["user_notice"])
|
|
self.assertEqual(angle["ambiguities"], [])
|
|
self.assertEqual(angle["actions"][0]["position_pct"], 100)
|
|
self.assertEqual(angle["actions"][0]["angle_deg"], 163)
|
|
|
|
def test_applies_one_shared_percentage_to_all_named_axes(self) -> None:
|
|
result = parse_local_intent("副驾驶水平、坐垫、椅背、脚托都调到40%")
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(
|
|
{(action["axis"], action["position_pct"]) for action in result["actions"]},
|
|
{
|
|
("longitudinal", 40),
|
|
("cushion_tilt", 40),
|
|
("backrest_recline", 40),
|
|
("legrest_height", 40),
|
|
},
|
|
)
|
|
|
|
def test_normalizes_common_heating_and_ventilation_synonyms(self) -> None:
|
|
warming = parse_local_intent("副驾座椅取暖开到2档")
|
|
brief_warming = parse_local_intent("副驾座椅暖一下")
|
|
airflow = parse_local_intent("副驾座椅吹风开到2档")
|
|
|
|
self.assertEqual(
|
|
(warming["actions"][0]["function"], warming["actions"][0]["requested_level"]),
|
|
("heating", 2),
|
|
)
|
|
self.assertEqual(
|
|
(
|
|
brief_warming["actions"][0]["function"],
|
|
brief_warming["actions"][0]["requested_level"],
|
|
),
|
|
("heating", 1),
|
|
)
|
|
self.assertEqual(
|
|
(airflow["actions"][0]["function"], airflow["actions"][0]["requested_level"]),
|
|
("ventilation", 2),
|
|
)
|
|
|
|
def test_rejects_unsupported_seat_scopes_instead_of_defaulting_to_passenger(self) -> None:
|
|
for utterance in (
|
|
"打开后排座椅加热",
|
|
"打开左前座椅通风",
|
|
"打开所有座椅加热",
|
|
):
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance)
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
|
self.assertIn("不能把", result["ambiguities"][0]["question"])
|
|
|
|
def test_axis_specific_endpoint_phrases_do_not_move_the_longitudinal_axis(self) -> None:
|
|
cases = {
|
|
"坐盆抬到最上方": ("cushion_tilt", 0),
|
|
"坐盆降到最下方": ("cushion_tilt", 100),
|
|
"靠背放倒到最后": ("backrest_recline", 100),
|
|
"副驾座椅靠背向后移动到最大位置": ("backrest_recline", 100),
|
|
"靠背往后调整至极限位置": ("backrest_recline", 100),
|
|
"靠背调整到最大位置": ("backrest_recline", 100),
|
|
"副驾靠背往后移到底": ("backrest_recline", 100),
|
|
"副驾椅背后移到底": ("backrest_recline", 100),
|
|
"副驾靠背调到最后位置": ("backrest_recline", 100),
|
|
"靠背向前移动到最大位置": ("backrest_recline", 0),
|
|
"副驾靠背向前移到底": ("backrest_recline", 0),
|
|
"副驾椅背前倾到底": ("backrest_recline", 0),
|
|
"靠背设置为最小角度": ("backrest_recline", 0),
|
|
"腿托升到最高": ("legrest_height", 100),
|
|
"腿托降到最低": ("legrest_height", 0),
|
|
}
|
|
for utterance, expected in cases.items():
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance)
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(
|
|
[(item["axis"], item["position_pct"]) for item in result["actions"]],
|
|
[expected],
|
|
)
|
|
|
|
def test_compositional_directional_endpoints_cover_every_seat_axis(self) -> None:
|
|
cases = {
|
|
"滑轨向前调整到最大位置": ("longitudinal", 0),
|
|
"前后往后移动至极限位置": ("longitudinal", 100),
|
|
"纵向朝前调节到极限位置": ("longitudinal", 0),
|
|
"水平向后设置到最大行程": ("longitudinal", 100),
|
|
"座椅高度向上调整到最大位置": ("cushion_tilt", 0),
|
|
"坐垫往下移动至极限位置": ("cushion_tilt", 100),
|
|
"坐盆倾角上调到最大位置": ("cushion_tilt", 0),
|
|
"坐垫倾角向下设置到最大行程": ("cushion_tilt", 100),
|
|
"靠背向前调整到最大位置": ("backrest_recline", 0),
|
|
"椅背往后移动至极限位置": ("backrest_recline", 100),
|
|
"靠背前倾调到最大角度": ("backrest_recline", 0),
|
|
"椅背后倾设置到最大行程": ("backrest_recline", 100),
|
|
"腿托向上调整到最大位置": ("legrest_height", 100),
|
|
"脚托往下移动至极限位置": ("legrest_height", 0),
|
|
"小腿托上调到最大位置": ("legrest_height", 100),
|
|
"腿部支撑高度向下设置到最大行程": ("legrest_height", 0),
|
|
}
|
|
for utterance, expected in cases.items():
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance)
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(
|
|
[(item["axis"], item["position_pct"]) for item in result["actions"]],
|
|
[expected],
|
|
)
|
|
|
|
def test_endpoint_conflicts_and_directionless_extremes_fail_with_a_real_hint(
|
|
self,
|
|
) -> None:
|
|
for utterance in (
|
|
"滑轨向前移到最后方",
|
|
"座椅高度向上调到最低",
|
|
"靠背向后调到最前",
|
|
"腿托向上调到最低",
|
|
"靠背向上调整到最大位置",
|
|
"腿托调整到极限位置",
|
|
"滑轨调整到最大位置",
|
|
):
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance)
|
|
self.assertEqual(result["actions"], [])
|
|
questions = [
|
|
item["question"]
|
|
for item in result["ambiguities"]
|
|
if item["field"].startswith("passenger.position.")
|
|
]
|
|
self.assertEqual(len(questions), 1)
|
|
self.assertIn("请", questions[0])
|
|
|
|
def test_parses_normal_half_second_and_minimum_clamped_timed_motion(self) -> None:
|
|
normal = parse_local_intent("滑轨向后移动2秒", self.motion_context())
|
|
half_second = parse_local_intent("靠背向后移动半秒", self.motion_context())
|
|
minimum = parse_local_intent("腿托向上移动200毫秒", self.motion_context())
|
|
|
|
self.assertEqual(normal["actions"][0]["motion_intent"]["applied_duration_ms"], 2_000)
|
|
self.assertFalse(normal["actions"][0]["motion_intent"]["duration_clamped"])
|
|
self.assertEqual(
|
|
half_second["actions"][0]["motion_intent"]["requested_duration_ms"],
|
|
500,
|
|
)
|
|
self.assertEqual(minimum["actions"][0]["motion_intent"]["applied_duration_ms"], 300)
|
|
self.assertTrue(minimum["actions"][0]["motion_intent"]["duration_clamped"])
|
|
self.assertIn("最小允许时长为0.3秒", minimum["actions"][0]["user_notice"])
|
|
|
|
def test_timed_motion_accepts_continuation_and_compact_direction_phrases(self) -> None:
|
|
cases = {
|
|
"座椅水平再后移动3秒": ("longitudinal", "increase"),
|
|
"座椅水平往后移3秒": ("longitudinal", "increase"),
|
|
"座椅水平继续前移3秒": ("longitudinal", "decrease"),
|
|
"腿托向上调3秒": ("legrest_height", "increase"),
|
|
"腿托再向上调3秒": ("legrest_height", "increase"),
|
|
"腿托再下调3秒": ("legrest_height", "decrease"),
|
|
}
|
|
for utterance, (axis, direction) in cases.items():
|
|
with self.subTest(utterance=utterance):
|
|
result = parse_local_intent(utterance, self.motion_context())
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertEqual(len(result["actions"]), 1)
|
|
self.assertEqual(result["actions"][0]["axis"], axis)
|
|
motion = result["actions"][0]["motion_intent"]
|
|
self.assertEqual(motion["mode"], "timed")
|
|
self.assertEqual(motion["direction"], direction)
|
|
self.assertEqual(motion["requested_duration_ms"], 3_000)
|
|
|
|
def test_rounds_decimal_and_clamps_negative_absolute_positions(self) -> None:
|
|
decimal = parse_local_intent("副驾座椅水平位置调到50.5%")
|
|
negative = parse_local_intent("副驾座椅水平位置调到-10%")
|
|
|
|
self.assertEqual(decimal["ambiguities"], [])
|
|
self.assertEqual(decimal["actions"][0]["position_pct"], 51)
|
|
self.assertIn("取整为51%", decimal["actions"][0]["user_notice"])
|
|
self.assertEqual(negative["ambiguities"], [])
|
|
self.assertEqual(negative["actions"][0]["position_pct"], 0)
|
|
self.assertIn("0%的备用目标", negative["actions"][0]["user_notice"])
|
|
|
|
def test_zero_duration_uses_minimum_safe_motion_instead_of_rejecting(self) -> None:
|
|
result = parse_local_intent(
|
|
"副驾靠背向后移动0秒", self.motion_context()
|
|
)
|
|
|
|
self.assertEqual(result["ambiguities"], [])
|
|
motion = result["actions"][0]["motion_intent"]
|
|
self.assertEqual(motion["requested_duration_ms"], 0)
|
|
self.assertEqual(motion["applied_duration_ms"], 300)
|
|
self.assertTrue(motion["duration_clamped"])
|
|
self.assertIn("已按0.3秒", result["actions"][0]["user_notice"])
|
|
|
|
def test_rejects_unsupported_level_and_conflicting_state(self) -> None:
|
|
unsupported = parse_local_intent("副驾加热开到4档")
|
|
conflicting = parse_local_intent("打开副驾加热然后关闭副驾加热")
|
|
|
|
self.assertEqual(unsupported["actions"], [])
|
|
self.assertEqual(unsupported["ambiguities"][0]["field"], "passenger.heating.level")
|
|
self.assertEqual(conflicting["actions"], [])
|
|
self.assertEqual(conflicting["ambiguities"][0]["field"], "passenger.thermal.state")
|
|
|
|
def test_rejects_driver_target_even_when_only_a_mode_is_named(self) -> None:
|
|
result = parse_local_intent("打开主驾零重力模式")
|
|
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
|
|
|
def test_rejects_driver_four_axis_zero(self) -> None:
|
|
result = parse_local_intent("把主驾座椅四轴归零")
|
|
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
|
self.assertIn("仅控制副驾", result["ambiguities"][0]["question"])
|
|
|
|
def test_maps_mild_hot_and_stuffy_feedback_to_level_one(self) -> None:
|
|
result = parse_local_intent("副驾有点热,有点闷", self.context())
|
|
|
|
self.assertEqual(
|
|
result["actions"],
|
|
[
|
|
{
|
|
"target": "passenger",
|
|
"function": "ventilation",
|
|
"state": "on",
|
|
"requested_level": 1,
|
|
}
|
|
],
|
|
)
|
|
self.assertIn("热、闷", "".join(result["decision_summary"]))
|
|
|
|
def test_increments_active_level_for_continuing_discomfort(self) -> None:
|
|
result = parse_local_intent("还有点闷", self.context(level=1))
|
|
|
|
self.assertEqual(result["actions"][0]["requested_level"], 2)
|
|
self.assertEqual(result["actions"][0]["target"], "passenger")
|
|
self.assertIn("提升到2档", "".join(result["decision_summary"]))
|
|
|
|
def test_cold_feedback_reduces_active_ventilation_before_heating(self) -> None:
|
|
result = parse_local_intent("我有点冷", self.context(level=1))
|
|
|
|
self.assertEqual(
|
|
result["actions"],
|
|
[
|
|
{
|
|
"target": "passenger",
|
|
"function": "ventilation",
|
|
"state": "off",
|
|
"requested_level": None,
|
|
}
|
|
],
|
|
)
|
|
self.assertIn("不立即开启加热", "".join(result["decision_summary"]))
|
|
|
|
def test_airflow_cause_targets_ventilation_not_heating(self) -> None:
|
|
context = self.context()
|
|
context["devices"]["passenger"]["ventilation"] = {"state": "on", "level": 2}
|
|
result = parse_local_intent("副驾吹的我有点冷", context)
|
|
|
|
self.assertEqual(
|
|
result["actions"],
|
|
[
|
|
{
|
|
"target": "passenger",
|
|
"function": "ventilation",
|
|
"state": "on",
|
|
"requested_level": 1,
|
|
}
|
|
],
|
|
)
|
|
self.assertNotIn("heating", {action["function"] for action in result["actions"]})
|
|
self.assertIn("致冷来源", "".join(result["decision_summary"]))
|
|
|
|
def test_cold_and_stuffy_are_independent_comfort_dimensions(self) -> None:
|
|
result = parse_local_intent("副驾有点冷、有点闷", self.context())
|
|
|
|
self.assertEqual(
|
|
{
|
|
(action["function"], action["state"], action["requested_level"])
|
|
for action in result["actions"]
|
|
},
|
|
{("heating", "on", 1), ("ventilation", "on", 1)},
|
|
)
|
|
self.assertEqual(result["ambiguities"], [])
|
|
self.assertIn("不将其判定为语义冲突", "".join(result["decision_summary"]))
|
|
|
|
def test_vehicle_scope_does_not_inherit_a_seat_target(self) -> None:
|
|
result = parse_local_intent("车上太闷了", self.context(level=1))
|
|
|
|
self.assertEqual(result["actions"], [])
|
|
self.assertEqual(result["ambiguities"][0]["field"], "domain")
|
|
self.assertEqual(result["detected_domains"], ["climate"])
|
|
self.assertEqual(result["scope"], "vehicle")
|
|
self.assertIn("整车舱范围", "".join(result["decision_summary"]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|