feat(v1): edit detection sensitivity from the settings tab

Adds write_local_event_tuning and a sensitivity group (confirm window,
horizontal angle, require-rapid-drop, require-lower-body) that persists to
the untracked local config and applies next start. 79 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-22 00:00:51 +08:00
co-authored by Claude Opus 4.8
parent 922609f236
commit d3cd8ecb55
7 changed files with 123 additions and 4 deletions
+8 -1
View File
@@ -99,8 +99,15 @@ class ApplicationController:
self._draft = _draft_from_config(config)
env_ready = bool(config.source_url)
model_summary = "{0} · {1}…".format(config.model_path.name, config.model_sha256[:12])
event_tuning = {
"require_rapid_drop": config.event.require_rapid_drop,
"require_lower_body": config.event.require_lower_body,
"horizontal_angle_threshold_degrees": config.event.horizontal_angle_threshold_degrees,
"confirm_window_seconds": config.event.confirm_window_seconds,
}
self.window = MainWindow(
self._draft, env_ready, model_summary, config.source_id, config_path, camera
self._draft, env_ready, model_summary, config.source_id,
config_path, camera, event_tuning,
)
self.window.monitor.start_requested.connect(self.start)
self.window.monitor.stop_requested.connect(self.stop)
+25
View File
@@ -196,6 +196,31 @@ def write_local_camera_source(
return path
def write_local_event_tuning(
config_path: Path,
require_rapid_drop: bool,
require_lower_body: bool,
horizontal_angle_threshold_degrees: float,
confirm_window_seconds: float,
) -> Path:
"""Persist detection-sensitivity tuning into the local config's event block.
Only the tuning keys are updated; other event fields are preserved. Applies
at the next start (the running config is a start-time snapshot).
"""
path = Path(config_path)
data = json.loads(path.read_text(encoding="utf-8"))
event = dict(data.get("event", {}))
event["require_rapid_drop"] = bool(require_rapid_drop)
event["require_lower_body"] = bool(require_lower_body)
event["horizontal_angle_threshold_degrees"] = float(horizontal_angle_threshold_degrees)
event["confirm_window_seconds"] = float(confirm_window_seconds)
data["event"] = event
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return path
def load_config(path: Path) -> AppConfig:
"""Load one public/local configuration pair without persisting credentials.
+53 -1
View File
@@ -254,6 +254,7 @@ class SettingsTab(QtWidgets.QWidget):
self, draft: SettingsDraft, env_ready: bool, model_summary: str,
config_path: Optional[str] = None,
camera: Optional[Dict] = None,
event_tuning: Optional[Dict] = None,
parent: Optional[QtWidgets.QWidget] = None,
) -> None:
super().__init__(parent)
@@ -269,6 +270,7 @@ class SettingsTab(QtWidgets.QWidget):
source_form.addRow("模型", QtWidgets.QLabel(model_summary))
layout.addWidget(source_box)
layout.addWidget(self._build_camera_box(camera or {}))
layout.addWidget(self._build_sensitivity_box(event_tuning or {}))
params_box = QtWidgets.QGroupBox("事件参数草稿")
form = QtWidgets.QFormLayout(params_box)
@@ -402,6 +404,53 @@ class SettingsTab(QtWidgets.QWidget):
self._probe_worker = None
self.test_button.setEnabled(True)
def _build_sensitivity_box(self, tuning: Dict) -> QtWidgets.QGroupBox:
box = QtWidgets.QGroupBox("检测灵敏度(保存至本地配置,下次启动生效)")
form = QtWidgets.QFormLayout(box)
self.confirm_spin = QtWidgets.QDoubleSpinBox()
self.confirm_spin.setRange(1.0, 3.0)
self.confirm_spin.setSingleStep(0.1)
self.confirm_spin.setDecimals(1)
self.confirm_spin.setValue(float(tuning.get("confirm_window_seconds", 1.8)))
self.angle_spin = QtWidgets.QDoubleSpinBox()
self.angle_spin.setRange(0.0, 90.0)
self.angle_spin.setSingleStep(5.0)
self.angle_spin.setDecimals(0)
self.angle_spin.setValue(float(tuning.get("horizontal_angle_threshold_degrees", 45.0)))
self.rapid_check = QtWidgets.QCheckBox("要求先快速下移(更严,误报更少)")
self.rapid_check.setChecked(bool(tuning.get("require_rapid_drop", False)))
self.lower_body_check = QtWidgets.QCheckBox("要求膝踝清晰(更严)")
self.lower_body_check.setChecked(bool(tuning.get("require_lower_body", False)))
form.addRow("确认窗口(秒,1–3,主防误报)", self.confirm_spin)
form.addRow("水平角度阈值(°,越大越易判水平)", self.angle_spin)
form.addRow("", self.rapid_check)
form.addRow("", self.lower_body_check)
self.sensitivity_status = QtWidgets.QLabel("")
self.save_sensitivity_button = QtWidgets.QPushButton("保存灵敏度到本地配置")
self.save_sensitivity_button.clicked.connect(self._on_save_sensitivity)
form.addRow(self.save_sensitivity_button)
form.addRow(self.sensitivity_status)
return box
def _on_save_sensitivity(self) -> None:
if not self._config_path:
self.sensitivity_status.setText("未指定本地配置路径,无法保存")
return
from v1.config import write_local_event_tuning
try:
write_local_event_tuning(
self._config_path,
require_rapid_drop=self.rapid_check.isChecked(),
require_lower_body=self.lower_body_check.isChecked(),
horizontal_angle_threshold_degrees=self.angle_spin.value(),
confirm_window_seconds=self.confirm_spin.value(),
)
except Exception as exc: # noqa: BLE001 - surface any write error to the UI
self.sensitivity_status.setText("保存失败:{0}".format(exc))
return
self.sensitivity_status.setText("已保存,将在下次开始监控时生效")
def _on_save_camera(self) -> None:
if not self._config_path:
self.camera_status.setText("未指定本地配置路径,无法保存")
@@ -433,13 +482,16 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(
self, draft: SettingsDraft, env_ready: bool, model_summary: str, source_name: str,
config_path: Optional[str] = None, camera: Optional[Dict] = None,
event_tuning: Optional[Dict] = None,
) -> None:
super().__init__()
self.setWindowTitle("Silver Pose · {0}".format(source_name))
self.resize(1280, 800)
self.setStyleSheet(_QSS)
self.monitor = MonitorTab()
self.settings = SettingsTab(draft, env_ready, model_summary, config_path, camera)
self.settings = SettingsTab(
draft, env_ready, model_summary, config_path, camera, event_tuning
)
tabs = QtWidgets.QTabWidget()
tabs.setTabPosition(QtWidgets.QTabWidget.North)
tabs.addTab(self.monitor, "实时监控")
+26
View File
@@ -129,6 +129,32 @@ def test_write_local_camera_source_round_trips_through_load_config(tmp_path):
assert config.source_id == "hik"
def test_write_local_event_tuning_round_trips_and_preserves_other_fields(tmp_path):
from v1.config import write_local_event_tuning
config_file = tmp_path / "config.local.json"
_write_config(
config_file,
{"id": "seed", "host": "192.0.2.10", "username": "u", "password": "p"},
)
write_local_event_tuning(
config_file,
require_rapid_drop=True,
require_lower_body=True,
horizontal_angle_threshold_degrees=35.0,
confirm_window_seconds=2.5,
)
config = load_config(config_file)
assert config.event.require_rapid_drop is True
assert config.event.require_lower_body is True
assert config.event.horizontal_angle_threshold_degrees == 35.0
assert config.event.confirm_window_seconds == 2.5
# unrelated event fields are preserved from the seed
assert config.event.cooldown_seconds == 10.0
def test_public_example_config_has_no_embedded_credentials():
from pathlib import Path