feat: harden seat intent and expand HIL coverage
This commit is contained in:
@@ -22,6 +22,17 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
@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("打开副驾座椅通风")
|
||||
|
||||
@@ -32,7 +43,7 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": None,
|
||||
"requested_level": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -82,6 +93,229 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
[("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_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),
|
||||
"腿托升到最高": ("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_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_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("把主驾座椅四轴归零")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user