优化了副驾座椅控制
This commit is contained in:
@@ -0,0 +1,847 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the expanded natural-language to CAN command test catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from collections import Counter
|
||||
from itertools import combinations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JSON_PATH = ROOT / "tests/fixtures/natural_can_command_combination_suite.json"
|
||||
CSV_PATH = ROOT / "docs/testing/自然语言转CAN组合测试用例.csv"
|
||||
DOC_PATH = ROOT / "docs/testing/自然语言转CAN组合测试说明.md"
|
||||
|
||||
|
||||
def can0_operation(
|
||||
capability: str,
|
||||
parameters: dict[str, Any],
|
||||
target: str = "passenger",
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"target": target,
|
||||
"capability": capability,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
|
||||
def build_cases() -> list[dict[str, Any]]:
|
||||
cases: list[dict[str, Any]] = []
|
||||
|
||||
def add(
|
||||
case_id: str,
|
||||
utterance: str,
|
||||
*,
|
||||
domains: tuple[str, ...],
|
||||
channels: tuple[str, ...] = ("can1",),
|
||||
commands: tuple[str, ...] = (),
|
||||
can0_operations: tuple[dict[str, Any], ...] = (),
|
||||
kind: str = "atomic",
|
||||
status: str = "succeeded",
|
||||
sources: tuple[str, ...] = (),
|
||||
) -> dict[str, Any]:
|
||||
case = {
|
||||
"id": case_id,
|
||||
"kind": kind,
|
||||
"channels": list(channels),
|
||||
"domains": list(domains),
|
||||
"utterance": utterance,
|
||||
"expected_status": status,
|
||||
"expected_commands": list(commands),
|
||||
"expected_can0_operations": list(can0_operations),
|
||||
}
|
||||
if sources:
|
||||
case["source_case_ids"] = list(sources)
|
||||
cases.append(case)
|
||||
return case
|
||||
|
||||
add(
|
||||
"CAN1-DOOR-REAR-LOCK",
|
||||
"后排车门上锁",
|
||||
domains=("door",),
|
||||
commands=("door rear lock",),
|
||||
)
|
||||
add(
|
||||
"CAN1-DOOR-REAR-UNLOCK",
|
||||
"后排门锁解锁",
|
||||
domains=("door",),
|
||||
commands=("door rear unlock",),
|
||||
)
|
||||
|
||||
window_targets = {
|
||||
"ALL": ("全部车窗", "all"),
|
||||
"FRONT": ("前排车窗", "front"),
|
||||
"REAR": ("后排车窗", "rear"),
|
||||
"FRONT-LEFT": ("左前车窗", "front-left"),
|
||||
"FRONT-RIGHT": ("右前车窗", "front-right"),
|
||||
"REAR-LEFT": ("左后车窗", "rear-left"),
|
||||
"REAR-RIGHT": ("右后车窗", "rear-right"),
|
||||
}
|
||||
window_motions = {
|
||||
"OPEN": ("打开", "open"),
|
||||
"CLOSE": ("关闭", "close"),
|
||||
"STOP": ("停止", "stop"),
|
||||
}
|
||||
for target_id, (target_text, target_command) in window_targets.items():
|
||||
for motion_id, (verb, motion) in window_motions.items():
|
||||
add(
|
||||
f"CAN1-WINDOW-{target_id}-{motion_id}",
|
||||
f"{verb}{target_text}",
|
||||
domains=("window",),
|
||||
commands=(f"window {target_command} {motion}",),
|
||||
)
|
||||
for target_id, (target_text, target_command) in tuple(window_targets.items())[3:]:
|
||||
for position in (0, 25, 50, 100):
|
||||
add(
|
||||
f"CAN1-WINDOW-{target_id}-POSITION-{position}",
|
||||
f"把{target_text}开到{position}%",
|
||||
domains=("window",),
|
||||
commands=(f"window {target_command} position {position}",),
|
||||
)
|
||||
for alias_id, alias, target in (
|
||||
("DRIVER", "主驾", "front-left"),
|
||||
("PASSENGER", "副驾", "front-right"),
|
||||
):
|
||||
for motion_id, (verb, motion) in window_motions.items():
|
||||
add(
|
||||
f"CAN1-WINDOW-{alias_id}-ALIAS-{motion_id}",
|
||||
f"{verb}{alias}车窗",
|
||||
domains=("window",),
|
||||
commands=(f"window {target} {motion}",),
|
||||
)
|
||||
|
||||
add("CAN1-AC-ON", "打开空调", domains=("climate",), commands=("ac on",))
|
||||
add("CAN1-AC-OFF", "关闭空调", domains=("climate",), commands=("ac off",))
|
||||
for level in range(1, 8):
|
||||
add(
|
||||
f"CAN1-AC-FAN-{level}",
|
||||
f"空调风量调到{level}档",
|
||||
domains=("climate",),
|
||||
commands=(f"ac fan {level}",),
|
||||
)
|
||||
for alias_id, alias, level in (
|
||||
("MIN", "最小", 1),
|
||||
("LOWEST", "最低", 1),
|
||||
("LOW", "低档", 1),
|
||||
("MIDDLE", "中等", 4),
|
||||
("MEDIUM", "中档", 4),
|
||||
("HIGH", "高档", 7),
|
||||
("HIGHEST", "最高", 7),
|
||||
("MAX", "最大", 7),
|
||||
):
|
||||
add(
|
||||
f"CAN1-AC-FAN-ALIAS-{alias_id}",
|
||||
f"空调风量设置为{alias}",
|
||||
domains=("climate",),
|
||||
commands=(f"ac fan {level}",),
|
||||
)
|
||||
|
||||
outlet_modes = (
|
||||
("FACE", "吹脸", "face"),
|
||||
("FACE-FEET", "吹面和吹脚", "face-feet"),
|
||||
("FEET", "吹脚", "feet"),
|
||||
("FEET-DEFROST", "吹脚除霜", "feet-defrost"),
|
||||
("DEFROST", "除霜", "defrost"),
|
||||
("FACE-FEET-DEFROST", "吹脸、吹脚和除霜", "face-feet-defrost"),
|
||||
("FACE-DEFROST", "吹脸和除霜", "face-defrost"),
|
||||
)
|
||||
for mode_id, mode_text, mode in outlet_modes:
|
||||
add(
|
||||
f"CAN1-AC-MODE-{mode_id}",
|
||||
f"空调出风模式设置为{mode_text}",
|
||||
domains=("climate",),
|
||||
commands=(f"ac mode {mode}",),
|
||||
)
|
||||
for target_id, target_text, target in (
|
||||
("DRIVER", "主驾", "driver"),
|
||||
("PASSENGER", "副驾", "passenger"),
|
||||
("REAR", "后排", "rear"),
|
||||
):
|
||||
for temperature in (18, 24, 32):
|
||||
add(
|
||||
f"CAN1-AC-TEMP-{target_id}-{temperature}",
|
||||
f"{target_text}空调温度设置为{temperature}度",
|
||||
domains=("climate",),
|
||||
commands=(f"ac temp {target} {temperature}",),
|
||||
)
|
||||
for mode_id, mode_text, mode in (
|
||||
("MANUAL", "手动控制", "manual"),
|
||||
("AUTO", "自动控制", "auto"),
|
||||
):
|
||||
add(
|
||||
f"CAN1-AC-CONTROL-{mode_id}",
|
||||
f"空调设置为{mode_text}",
|
||||
domains=("climate",),
|
||||
commands=(f"ac control {mode}",),
|
||||
)
|
||||
for cycle_id, cycle_text, cycle in (
|
||||
("INTERNAL", "内循环", "internal"),
|
||||
("EXTERNAL", "外循环", "external"),
|
||||
):
|
||||
add(
|
||||
f"CAN1-AC-CYCLE-{cycle_id}",
|
||||
f"空调设置为{cycle_text}",
|
||||
domains=("climate",),
|
||||
commands=(f"ac cycle {cycle}",),
|
||||
)
|
||||
|
||||
outlet_targets = (
|
||||
("DRIVER-LEFT", "主驾左", "driver-left", (-25, -40)),
|
||||
("DRIVER-RIGHT", "主驾右", "driver-right", (30, 30)),
|
||||
("PASSENGER-LEFT", "副驾左", "passenger-left", (-10, -40)),
|
||||
("PASSENGER-RIGHT", "副驾右", "passenger-right", (5, 30)),
|
||||
)
|
||||
for target_id, target_text, target, calibrated in outlet_targets:
|
||||
for horizontal, vertical in ((0, 0), (25, 75), (100, 100)):
|
||||
add(
|
||||
f"CAN1-OUTLET-{target_id}-PERCENT-{horizontal}-{vertical}",
|
||||
f"{target_text}风口水平{horizontal}%上下{vertical}%",
|
||||
domains=("outlet",),
|
||||
commands=(
|
||||
f"outlet {target} percent {horizontal} {vertical}",
|
||||
),
|
||||
)
|
||||
add(
|
||||
f"CAN1-OUTLET-{target_id}-CALIBRATED",
|
||||
f"{target_text}风口标定坐标左右{calibrated[0]}上下{calibrated[1]}",
|
||||
domains=("outlet",),
|
||||
commands=(
|
||||
f"outlet {target} position {calibrated[0]} {calibrated[1]}",
|
||||
),
|
||||
)
|
||||
|
||||
for feature_id, feature_text, feature in (
|
||||
("VENT", "通风", "vent"),
|
||||
("HEAT", "加热", "heat"),
|
||||
):
|
||||
for level_id, level_text, level in (
|
||||
("OFF", "关闭", "off"),
|
||||
("LOW", "低档", "low"),
|
||||
("MIN", "最低", "low"),
|
||||
("HIGH", "高档", "high"),
|
||||
("MAX", "最大", "high"),
|
||||
):
|
||||
utterance = (
|
||||
f"关闭主驾座椅{feature_text}"
|
||||
if level == "off"
|
||||
else f"主驾座椅{feature_text}设置为{level_text}"
|
||||
)
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-{feature_id}-{level_id}",
|
||||
utterance,
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver {feature} {level}",),
|
||||
)
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-{feature_id}-OPEN-DEFAULT",
|
||||
f"打开主驾座椅{feature_text}",
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver {feature} low",),
|
||||
)
|
||||
|
||||
add(
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-OPEN",
|
||||
"打开主驾座椅按摩",
|
||||
domains=("seat",),
|
||||
commands=("seat driver massage mode relax",),
|
||||
)
|
||||
add(
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-OFF",
|
||||
"关闭主驾座椅按摩",
|
||||
domains=("seat",),
|
||||
commands=("seat driver massage off",),
|
||||
)
|
||||
for strength_id, strength_text, strength in (
|
||||
("WEAK", "最小", "weak"),
|
||||
("MEDIUM", "中等", "medium"),
|
||||
("STRONG", "最大", "strong"),
|
||||
("LOWEST", "最低", "weak"),
|
||||
("HIGHEST", "最高", "strong"),
|
||||
):
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-{strength_id}",
|
||||
f"主驾座椅按摩强度设置为{strength_text}",
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver massage strength {strength}",),
|
||||
)
|
||||
for mode_id, mode_text, mode in (
|
||||
("WAVE", "波浪", "wave"),
|
||||
("PULSE", "脉冲", "pulse"),
|
||||
("STRETCH", "伸展", "stretch"),
|
||||
("SLACK", "松弛", "slack"),
|
||||
("RELAX", "放松", "relax"),
|
||||
):
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-MASSAGE-MODE-{mode_id}",
|
||||
f"主驾座椅按摩模式设置为{mode_text}",
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver massage mode {mode}",),
|
||||
)
|
||||
|
||||
driver_motion_cases = (
|
||||
("HORIZONTAL-FORWARD", "主驾座椅向前移动500毫秒", "horizontal forward 500"),
|
||||
("HORIZONTAL-BACKWARD", "主驾座椅后退两秒", "horizontal backward 2000"),
|
||||
("HORIZONTAL-STOP", "停止主驾座椅前后移动", "horizontal stop"),
|
||||
("HEIGHT-UP", "主驾座椅往上移动300毫秒", "height up 300"),
|
||||
("HEIGHT-DOWN", "主驾座椅向下移动一秒", "height down 1000"),
|
||||
("HEIGHT-STOP", "停止主驾座椅高度调节", "height stop"),
|
||||
("BACKREST-FORWARD", "主驾座椅靠背调直500毫秒", "backrest forward 500"),
|
||||
("BACKREST-BACKWARD", "主驾座椅椅背放倒800毫秒", "backrest backward 800"),
|
||||
("BACKREST-STOP", "停止主驾座椅靠背动作", "backrest stop"),
|
||||
("BASIN-UP", "主驾座椅坐盆抬高300毫秒", "basin up 300"),
|
||||
("BASIN-DOWN", "主驾座椅座垫放低一秒", "basin down 1000"),
|
||||
("BASIN-STOP", "停止主驾座椅坐盆动作", "basin stop"),
|
||||
)
|
||||
for case_id, utterance, suffix in driver_motion_cases:
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-{case_id}",
|
||||
utterance,
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver {suffix}",),
|
||||
)
|
||||
add(
|
||||
"CAN1-SEAT-DRIVER-STOP-ALL",
|
||||
"停止主驾座椅移动",
|
||||
domains=("seat",),
|
||||
commands=(
|
||||
"seat driver horizontal stop",
|
||||
"seat driver height stop",
|
||||
"seat driver backrest stop",
|
||||
"seat driver basin stop",
|
||||
),
|
||||
)
|
||||
for slot in (1, 2, 3):
|
||||
add(
|
||||
f"CAN1-SEAT-DRIVER-MEMORY-{slot}",
|
||||
f"调用主驾座椅记忆位置{slot}",
|
||||
domains=("seat",),
|
||||
commands=(f"seat driver position {slot}",),
|
||||
)
|
||||
|
||||
for state_id, state_text, state in (
|
||||
("ON", "打开", "on"),
|
||||
("OFF", "关闭", "off"),
|
||||
):
|
||||
add(
|
||||
f"CAN1-LIGHT-INDOOR-{state_id}",
|
||||
f"{state_text}室内灯",
|
||||
domains=("interior_light",),
|
||||
commands=(f"light indoor {state}",),
|
||||
)
|
||||
add(
|
||||
f"CAN1-LIGHT-READING-ALL-{state_id}",
|
||||
f"{state_text}全部阅读灯",
|
||||
domains=("interior_light",),
|
||||
commands=(f"light reading all {state}",),
|
||||
)
|
||||
add(
|
||||
f"CAN1-LIGHT-AMBIENT-{state_id}",
|
||||
f"{state_text}氛围灯",
|
||||
domains=("ambient_light",),
|
||||
commands=(f"light ambient {state}",),
|
||||
)
|
||||
for color in (1, 32, 70, 127):
|
||||
add(
|
||||
f"CAN1-LIGHT-AMBIENT-COLOR-{color}",
|
||||
f"氛围灯颜色编号设置为{color}",
|
||||
domains=("ambient_light",),
|
||||
commands=(f"light ambient color {color}",),
|
||||
)
|
||||
for brightness in range(1, 6):
|
||||
add(
|
||||
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-{brightness}",
|
||||
f"氛围灯亮度设置为{brightness}档",
|
||||
domains=("ambient_light",),
|
||||
commands=(f"light ambient brightness {brightness}",),
|
||||
)
|
||||
for alias_id, alias, brightness in (
|
||||
("MIN", "最低", 1),
|
||||
("MIDDLE", "中等", 3),
|
||||
("MAX", "最高", 5),
|
||||
):
|
||||
add(
|
||||
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-ALIAS-{alias_id}",
|
||||
f"氛围灯亮度设置为{alias}",
|
||||
domains=("ambient_light",),
|
||||
commands=(f"light ambient brightness {brightness}",),
|
||||
)
|
||||
|
||||
for motion_id, motion_text, motion in (
|
||||
("OPEN", "打开", "open"),
|
||||
("CLOSE", "关闭", "close"),
|
||||
("HALF", "半开", "half"),
|
||||
("STOP", "停止", "stop"),
|
||||
):
|
||||
utterance = f"遮阳帘{motion_text}" if motion == "half" else f"{motion_text}遮阳帘"
|
||||
add(
|
||||
f"CAN1-SHADE-{motion_id}",
|
||||
utterance,
|
||||
domains=("roof",),
|
||||
commands=(f"shade {motion}",),
|
||||
)
|
||||
for position in (0, 20, 30, 50, 100):
|
||||
add(
|
||||
f"CAN1-SHADE-POSITION-{position}",
|
||||
f"遮阳帘打开到{position}%",
|
||||
domains=("roof",),
|
||||
commands=(f"shade position {position}",),
|
||||
)
|
||||
|
||||
can0_atoms = (
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HEAT-1",
|
||||
"副驾驶座椅加热开到1档",
|
||||
can0_operation("seat.set_heating", {"state": "on", "level": 1}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HEAT-2",
|
||||
"副驾驶座椅加热开到2档",
|
||||
can0_operation("seat.set_heating", {"state": "on", "level": 2}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HEAT-3",
|
||||
"副驾驶座椅加热开到3档",
|
||||
can0_operation("seat.set_heating", {"state": "on", "level": 3}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HEAT-OFF",
|
||||
"关闭副驾驶座椅加热",
|
||||
can0_operation("seat.set_heating", {"state": "off"}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-VENT-1",
|
||||
"副驾驶座椅通风开到1档",
|
||||
can0_operation("seat.set_ventilation", {"state": "on", "level": 1}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-VENT-2",
|
||||
"副驾驶座椅通风开到2档",
|
||||
can0_operation("seat.set_ventilation", {"state": "on", "level": 2}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-VENT-3",
|
||||
"副驾驶座椅通风开到3档",
|
||||
can0_operation("seat.set_ventilation", {"state": "on", "level": 3}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-VENT-OFF",
|
||||
"关闭副驾驶座椅通风",
|
||||
can0_operation("seat.set_ventilation", {"state": "off"}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
|
||||
"把副驾驶座椅水平位置调到30%",
|
||||
can0_operation("seat.set_longitudinal_position", {"position_pct": 30}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-BASIN-40",
|
||||
"把副驾驶坐盆倾角调到40%",
|
||||
can0_operation("seat.set_cushion_tilt", {"position_pct": 40}),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-BACKREST-120",
|
||||
"把副驾驶靠背调到120度",
|
||||
can0_operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 37, "angle_deg": 120},
|
||||
),
|
||||
),
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-LEGREST-125",
|
||||
"把副驾驶腿托抬到125度",
|
||||
can0_operation(
|
||||
"seat.set_legrest_height",
|
||||
{"position_pct": 42, "angle_deg": 125},
|
||||
),
|
||||
),
|
||||
)
|
||||
for case_id, utterance, operation in can0_atoms:
|
||||
add(
|
||||
case_id,
|
||||
utterance,
|
||||
domains=("seat",),
|
||||
channels=("can0",),
|
||||
can0_operations=(operation,),
|
||||
)
|
||||
|
||||
atom_by_id = {case["id"]: case for case in cases}
|
||||
|
||||
def combine(
|
||||
case_id: str,
|
||||
source_ids: tuple[str, ...],
|
||||
*,
|
||||
separator: str = ",",
|
||||
kind: str,
|
||||
) -> None:
|
||||
source_cases = [atom_by_id[source_id] for source_id in source_ids]
|
||||
channels = tuple(
|
||||
channel
|
||||
for channel in ("can0", "can1")
|
||||
if any(channel in item["channels"] for item in source_cases)
|
||||
)
|
||||
domains = tuple(
|
||||
dict.fromkeys(
|
||||
domain for item in source_cases for domain in item["domains"]
|
||||
)
|
||||
)
|
||||
add(
|
||||
case_id,
|
||||
separator.join(item["utterance"] for item in source_cases),
|
||||
domains=domains,
|
||||
channels=channels,
|
||||
commands=tuple(
|
||||
command
|
||||
for item in source_cases
|
||||
for command in item["expected_commands"]
|
||||
),
|
||||
can0_operations=tuple(
|
||||
operation
|
||||
for item in source_cases
|
||||
for operation in item["expected_can0_operations"]
|
||||
),
|
||||
kind=kind,
|
||||
sources=source_ids,
|
||||
)
|
||||
|
||||
pair_pool = (
|
||||
"CAN1-DOOR-REAR-LOCK",
|
||||
"CAN1-WINDOW-FRONT-LEFT-OPEN",
|
||||
"CAN1-WINDOW-REAR-RIGHT-POSITION-25",
|
||||
"CAN1-AC-ON",
|
||||
"CAN1-AC-FAN-2",
|
||||
"CAN1-AC-MODE-FACE-FEET",
|
||||
"CAN1-AC-TEMP-DRIVER-24",
|
||||
"CAN1-AC-TEMP-PASSENGER-24",
|
||||
"CAN1-AC-TEMP-REAR-24",
|
||||
"CAN1-AC-CONTROL-AUTO",
|
||||
"CAN1-AC-CYCLE-INTERNAL",
|
||||
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
|
||||
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
|
||||
"CAN1-SEAT-DRIVER-VENT-HIGH",
|
||||
"CAN1-SEAT-DRIVER-HEAT-LOW",
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
|
||||
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
|
||||
"CAN1-SEAT-DRIVER-HEIGHT-UP",
|
||||
"CAN1-LIGHT-INDOOR-ON",
|
||||
"CAN1-LIGHT-READING-ALL-ON",
|
||||
"CAN1-LIGHT-AMBIENT-ON",
|
||||
"CAN1-LIGHT-AMBIENT-COLOR-70",
|
||||
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
|
||||
"CAN1-SHADE-POSITION-30",
|
||||
)
|
||||
for index, source_pair in enumerate(combinations(pair_pool, 2), start=1):
|
||||
combine(
|
||||
f"COMBO-PAIR-COMMA-{index:03d}",
|
||||
source_pair,
|
||||
kind="pairwise",
|
||||
)
|
||||
combine(
|
||||
f"COMBO-PAIR-SEMICOLON-{index:03d}",
|
||||
source_pair,
|
||||
separator=";",
|
||||
kind="pairwise_separator_variant",
|
||||
)
|
||||
|
||||
multi_anchors = (
|
||||
"CAN1-DOOR-REAR-LOCK",
|
||||
"CAN1-WINDOW-FRONT-LEFT-OPEN",
|
||||
"CAN1-AC-FAN-2",
|
||||
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
|
||||
"CAN1-SEAT-DRIVER-VENT-HIGH",
|
||||
"CAN1-LIGHT-INDOOR-ON",
|
||||
"CAN1-LIGHT-AMBIENT-COLOR-70",
|
||||
"CAN1-SHADE-POSITION-30",
|
||||
"CAN0-SEAT-PASSENGER-HEAT-2",
|
||||
)
|
||||
for index, source_ids in enumerate(combinations(multi_anchors, 3), start=1):
|
||||
combine(
|
||||
f"COMBO-TRIPLE-{index:03d}",
|
||||
source_ids,
|
||||
kind="three_command",
|
||||
)
|
||||
for index, source_ids in enumerate(combinations(multi_anchors, 4), start=1):
|
||||
combine(
|
||||
f"COMBO-QUADRUPLE-{index:03d}",
|
||||
source_ids,
|
||||
kind="four_command",
|
||||
)
|
||||
|
||||
cross_can1 = pair_pool[:15]
|
||||
cross_can0 = tuple(item[0] for item in can0_atoms[0:8])
|
||||
for index, (can1_id, can0_id) in enumerate(
|
||||
((left, right) for left in cross_can1 for right in cross_can0),
|
||||
start=1,
|
||||
):
|
||||
combine(
|
||||
f"COMBO-CROSS-CHANNEL-{index:03d}",
|
||||
(can1_id, can0_id),
|
||||
kind="cross_can0_can1",
|
||||
)
|
||||
|
||||
scenes = (
|
||||
(
|
||||
"MOVIE",
|
||||
(
|
||||
"CAN1-AC-ON",
|
||||
"CAN1-AC-TEMP-DRIVER-24",
|
||||
"CAN1-AC-FAN-2",
|
||||
"CAN1-SHADE-POSITION-100",
|
||||
"CAN1-LIGHT-AMBIENT-COLOR-70",
|
||||
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
|
||||
),
|
||||
),
|
||||
(
|
||||
"DRIVER-COMFORT",
|
||||
(
|
||||
"CAN1-SEAT-DRIVER-VENT-LOW",
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
|
||||
"CAN1-AC-TEMP-DRIVER-24",
|
||||
"CAN1-AC-FAN-2",
|
||||
),
|
||||
),
|
||||
(
|
||||
"PASSENGER-COMFORT",
|
||||
(
|
||||
"CAN0-SEAT-PASSENGER-HEAT-2",
|
||||
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
|
||||
"CAN0-SEAT-PASSENGER-BASIN-40",
|
||||
"CAN0-SEAT-PASSENGER-BACKREST-120",
|
||||
"CAN0-SEAT-PASSENGER-LEGREST-125",
|
||||
),
|
||||
),
|
||||
(
|
||||
"CABIN-BRIGHT",
|
||||
(
|
||||
"CAN1-LIGHT-INDOOR-ON",
|
||||
"CAN1-LIGHT-READING-ALL-ON",
|
||||
"CAN1-LIGHT-AMBIENT-ON",
|
||||
"CAN1-LIGHT-AMBIENT-COLOR-70",
|
||||
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-5",
|
||||
),
|
||||
),
|
||||
(
|
||||
"VENTILATION",
|
||||
(
|
||||
"CAN1-AC-ON",
|
||||
"CAN1-AC-FAN-7",
|
||||
"CAN1-AC-MODE-FACE-FEET",
|
||||
"CAN1-AC-CYCLE-EXTERNAL",
|
||||
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
|
||||
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
|
||||
),
|
||||
),
|
||||
(
|
||||
"PARK-CLOSE",
|
||||
(
|
||||
"CAN1-WINDOW-ALL-CLOSE",
|
||||
"CAN1-SHADE-CLOSE",
|
||||
"CAN1-DOOR-REAR-LOCK",
|
||||
"CAN1-LIGHT-INDOOR-OFF",
|
||||
"CAN1-LIGHT-AMBIENT-OFF",
|
||||
"CAN1-AC-OFF",
|
||||
),
|
||||
),
|
||||
(
|
||||
"DUAL-SEAT",
|
||||
(
|
||||
"CAN1-SEAT-DRIVER-VENT-HIGH",
|
||||
"CAN1-SEAT-DRIVER-MASSAGE-MODE-WAVE",
|
||||
"CAN0-SEAT-PASSENGER-VENT-2",
|
||||
"CAN0-SEAT-PASSENGER-BACKREST-120",
|
||||
"CAN1-AC-TEMP-DRIVER-24",
|
||||
"CAN1-AC-TEMP-PASSENGER-24",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ALL-ZONES-CLIMATE",
|
||||
(
|
||||
"CAN1-AC-ON",
|
||||
"CAN1-AC-TEMP-DRIVER-24",
|
||||
"CAN1-AC-TEMP-PASSENGER-24",
|
||||
"CAN1-AC-TEMP-REAR-24",
|
||||
"CAN1-AC-FAN-4",
|
||||
"CAN1-AC-CONTROL-AUTO",
|
||||
"CAN1-AC-CYCLE-INTERNAL",
|
||||
),
|
||||
),
|
||||
(
|
||||
"WINDOWS-SHADE",
|
||||
(
|
||||
"CAN1-WINDOW-FRONT-LEFT-POSITION-25",
|
||||
"CAN1-WINDOW-FRONT-RIGHT-POSITION-50",
|
||||
"CAN1-WINDOW-REAR-LEFT-POSITION-100",
|
||||
"CAN1-WINDOW-REAR-RIGHT-POSITION-0",
|
||||
"CAN1-SHADE-POSITION-30",
|
||||
),
|
||||
),
|
||||
(
|
||||
"DRIVER-POSTURE",
|
||||
(
|
||||
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
|
||||
"CAN1-SEAT-DRIVER-HEIGHT-UP",
|
||||
"CAN1-SEAT-DRIVER-BACKREST-BACKWARD",
|
||||
"CAN1-SEAT-DRIVER-BASIN-DOWN",
|
||||
),
|
||||
),
|
||||
)
|
||||
for scene_id, source_ids in scenes:
|
||||
combine(
|
||||
f"COMBO-SCENE-{scene_id}",
|
||||
source_ids,
|
||||
kind="multi_command_scene",
|
||||
)
|
||||
|
||||
safety_cases = (
|
||||
("DRIVER-VENT-LEVEL-4", "主驾座椅通风4档"),
|
||||
("DRIVER-VENT-MIDDLE", "主驾座椅通风中档"),
|
||||
("DRIVER-ABSOLUTE-BACKREST", "把驾驶位靠背调到50%"),
|
||||
("DRIVER-UNVERIFIED-MODE", "主驾进入零重力模式"),
|
||||
("FRONT-ROW-SEAT", "打开前排座椅加热"),
|
||||
("PASSENGER-HEAT-LEVEL-4", "副驾座椅加热开到4档"),
|
||||
("PASSENGER-VENT-MISSING-STATE", "副驾座椅通风"),
|
||||
("PASSENGER-CONTRADICTORY", "打开副驾加热然后关闭副驾加热"),
|
||||
("PASSENGER-MASSAGE", "把副驾座椅按摩打开"),
|
||||
("MULTIPLE-SEAT-MODES", "进入办公模式和零重力模式"),
|
||||
("PASSENGER-LEGREST-EXTEND", "把副驾腿托伸长到50%"),
|
||||
("WRONG-AXIS-UNIT", "坐垫向前移动10度"),
|
||||
("MISSING-SEAT-AXIS", "向前移动10度"),
|
||||
("REAR-SEAT", "打开后排座椅加热"),
|
||||
("ALL-SEATS", "打开所有座椅加热"),
|
||||
("OUTLET-DISABLE-WITHOUT-MODE", "关闭吹面出风口"),
|
||||
)
|
||||
for case_id, utterance in safety_cases:
|
||||
add(
|
||||
f"SAFETY-{case_id}",
|
||||
utterance,
|
||||
domains=("safety",),
|
||||
channels=(),
|
||||
kind="safety_rejection",
|
||||
status="needs_confirmation",
|
||||
)
|
||||
return cases
|
||||
|
||||
|
||||
def write_outputs(cases: list[dict[str, Any]]) -> None:
|
||||
kind_counts = Counter(case["kind"] for case in cases)
|
||||
channel_counts = Counter(
|
||||
"+".join(case["channels"]) or "none" for case in cases
|
||||
)
|
||||
payload = {
|
||||
"schema": "natural-can-command-combination-suite/v1",
|
||||
"generated_on": "2026-08-24",
|
||||
"description": "自然语言控制指令到CAN0/CAN1语义控制指令的原子、组合和安全测试目录。",
|
||||
"summary": {
|
||||
"total_cases": len(cases),
|
||||
"by_kind": dict(sorted(kind_counts.items())),
|
||||
"by_channel": dict(sorted(channel_counts.items())),
|
||||
},
|
||||
"cases": cases,
|
||||
}
|
||||
JSON_PATH.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with CSV_PATH.open("w", encoding="utf-8-sig", newline="") as handle:
|
||||
writer = csv.DictWriter(
|
||||
handle,
|
||||
fieldnames=(
|
||||
"id",
|
||||
"kind",
|
||||
"channels",
|
||||
"domains",
|
||||
"utterance",
|
||||
"expected_status",
|
||||
"expected_commands",
|
||||
"expected_can0_operations",
|
||||
"source_case_ids",
|
||||
),
|
||||
)
|
||||
writer.writeheader()
|
||||
for case in cases:
|
||||
writer.writerow(
|
||||
{
|
||||
**case,
|
||||
"channels": "/".join(case["channels"]),
|
||||
"domains": "/".join(case["domains"]),
|
||||
"expected_commands": " | ".join(case["expected_commands"]),
|
||||
"expected_can0_operations": json.dumps(
|
||||
case["expected_can0_operations"], ensure_ascii=False
|
||||
),
|
||||
"source_case_ids": " | ".join(
|
||||
case.get("source_case_ids", [])
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
kind_lines = "\n".join(
|
||||
f"| `{kind}` | {count} |" for kind, count in sorted(kind_counts.items())
|
||||
)
|
||||
channel_lines = "\n".join(
|
||||
f"| `{channel}` | {count} |"
|
||||
for channel, count in sorted(channel_counts.items())
|
||||
)
|
||||
DOC_PATH.write_text(
|
||||
f"""# 自然语言转 CAN 控制指令组合测试说明
|
||||
|
||||
- 生成日期:2026-08-24
|
||||
- 总用例数:**{len(cases)}**
|
||||
- 执行方式:本地规则、`dry_run`,不发送真实 CAN 帧
|
||||
- 完整逐条清单:`docs/testing/自然语言转CAN组合测试用例.csv`
|
||||
- 机器可执行数据:`tests/fixtures/natural_can_command_combination_suite.json`
|
||||
|
||||
## 覆盖数量
|
||||
|
||||
| 用例类型 | 数量 |
|
||||
| --- | ---: |
|
||||
{kind_lines}
|
||||
|
||||
| 通道 | 数量 |
|
||||
| --- | ---: |
|
||||
{channel_lines}
|
||||
|
||||
## 组合策略
|
||||
|
||||
1. 原子用例覆盖 CAN1 门锁、车窗、空调、风口、主驾座椅、灯光、遮阳帘,以及 CAN0 副驾座椅。
|
||||
2. 两两组合从 25 个互不冲突的控制槽中取组合,同时覆盖中文逗号和分号分隔形式。
|
||||
3. 三指令和四指令组合覆盖 CAN0/CAN1、座椅、空调、灯光、车窗、门锁、风口与遮阳帘的交叉。
|
||||
4. 跨通道组合明确检查副驾座椅留在 CAN0、主驾和车身控制留在 CAN1。
|
||||
5. 多指令场景覆盖观影、双座椅、全温区空调、泊车关闭、车窗与遮阳帘等常用场景。
|
||||
6. 安全用例要求白名单外目标、档位、能力和矛盾指令不生成 CAN 控制流。
|
||||
|
||||
## 执行
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src:domains/seat/src .venv/bin/python -m pytest -q tests/integration/test_natural_can_command_combinations.py
|
||||
```
|
||||
|
||||
重新生成清单:
|
||||
|
||||
```bash
|
||||
.venv/bin/python tools/generate_natural_can_command_suite.py
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cases = build_cases()
|
||||
ids = [case["id"] for case in cases]
|
||||
if len(ids) != len(set(ids)):
|
||||
duplicates = [item for item, count in Counter(ids).items() if count > 1]
|
||||
raise ValueError(f"duplicate case ids: {duplicates}")
|
||||
write_outputs(cases)
|
||||
print(f"generated {len(cases)} cases")
|
||||
print(JSON_PATH)
|
||||
print(CSV_PATH)
|
||||
print(DOC_PATH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user