oscarzhang commited on
Commit
23bb099
·
verified ·
1 Parent(s): 1a77d3b

Upload folder using huggingface_hub

Browse files
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+
8
+ # Environment
9
+ .env
10
+ .venv
11
+ env/
12
+ venv/
13
+
14
+ # IDE
15
+ .vscode/
16
+ .idea/
17
+ *.swp
18
+ *.swo
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # Gradio
25
+ flagged/
26
+ gradio_cached_examples/
27
+
28
+ # Large files (if using Git LFS)
29
+ *.pt
30
+ *.pth
31
+ *.ckpt
32
+
33
+ # Logs
34
+ *.log
35
+
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 📟
4
  colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: "4.12.0"
8
  app_file: gradio_app.py
9
  pinned: false
10
  ---
 
4
  colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: "4.44.0"
8
  app_file: gradio_app.py
9
  pinned: false
10
  ---
configs/case_builder_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "main_model_confirmation": {
3
+ "enabled": true,
4
+ "mode": "auto",
5
+ "model_dir": "checkpoints/phase2/exp_factor_balanced",
6
+ "device": "cpu",
7
+ "threshold": null,
8
+ "min_duration_days": 3
9
+ }
10
+ }
11
+
configs/case_builder_config_README.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CaseBuilder 配置说明
2
+
3
+ ## 配置文件位置
4
+
5
+ `configs/case_builder_config.json`
6
+
7
+ ## 配置项说明
8
+
9
+ ```json
10
+ {
11
+ "main_model_confirmation": {
12
+ "enabled": false, // 是否启用主时序模型确认
13
+ "mode": "auto", // 模式:auto(自动)或 manual(手工)
14
+ "model_dir": "checkpoints/phase2/exp_factor_balanced", // 主时序模型路径
15
+ "device": "cpu", // 设备:cpu 或 cuda
16
+ "threshold": null, // 异常阈值(null 表示使用模型默认值)
17
+ "min_duration_days": 3 // 最小持续天数(用于 detect_pattern)
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## 使用场景
23
+
24
+ ### 场景1:自动模式(推荐)
25
+
26
+ **配置:**
27
+ ```json
28
+ {
29
+ "main_model_confirmation": {
30
+ "enabled": true,
31
+ "mode": "auto",
32
+ "model_dir": "checkpoints/phase2/exp_factor_balanced"
33
+ }
34
+ }
35
+ ```
36
+
37
+ **代码:**
38
+ ```python
39
+ from utils.case_builder import CaseBuilder
40
+
41
+ builder = CaseBuilder() # 自动从配置文件加载
42
+ result = builder.build_case(payload)
43
+
44
+ # 检查主时序模型确认结果
45
+ if result.get("risk_confirmed") is False:
46
+ print("主时序模型确认无风险,终止流程")
47
+ return
48
+
49
+ # 继续生成 LLM 输入
50
+ llm_input = result["llm_input"]
51
+ ```
52
+
53
+ **行为:**
54
+ - CaseBuilder 内部自动调用主时序模型确认风险
55
+ - 返回 `risk_confirmed: true/false`
56
+ - 外部可以根据结果决定是否继续
57
+
58
+ ### 场景2:手工模式
59
+
60
+ **配置:**
61
+ ```json
62
+ {
63
+ "main_model_confirmation": {
64
+ "enabled": false
65
+ }
66
+ }
67
+ ```
68
+
69
+ **代码:**
70
+ ```python
71
+ from utils.case_builder import CaseBuilder
72
+ from wearable_anomaly_detector import WearableAnomalyDetector
73
+
74
+ builder = CaseBuilder() # 只用 PatchAD,不调用主时序模型
75
+ result = builder.build_case(payload)
76
+
77
+ # 外部自己调用主时序模型确认
78
+ detector = WearableAnomalyDetector(model_dir="...")
79
+ all_data_points = []
80
+ for item in payload["history_windows"]:
81
+ all_data_points.extend(item["window"])
82
+
83
+ pattern_result = detector.detect_pattern(all_data_points, days=len(payload["history_windows"]))
84
+ risk_confirmed = pattern_result.get('anomaly_pattern', {}).get('has_pattern', False)
85
+
86
+ if not risk_confirmed:
87
+ print("主时序模型确认无风险,终止流程")
88
+ return
89
+ ```
90
+
91
+ **行为:**
92
+ - CaseBuilder 只用 PatchAD 分数,不调用主时序模型
93
+ - 外部可以自己调用 `WearableAnomalyDetector.detect_pattern()` 来确认风险
94
+
95
+ ### 场景3:外部传入检测器(复用已加载的模型)
96
+
97
+ **代码:**
98
+ ```python
99
+ from utils.case_builder import CaseBuilder
100
+ from wearable_anomaly_detector import WearableAnomalyDetector
101
+
102
+ # 外部已加载的检测器
103
+ detector = WearableAnomalyDetector(model_dir="...")
104
+
105
+ # 传入检测器实例,避免重复加载
106
+ builder = CaseBuilder(detector=detector)
107
+ result = builder.build_case(payload)
108
+
109
+ # 检查确认结果
110
+ if result.get("risk_confirmed") is False:
111
+ print("主时序模型确认无风险,终止流程")
112
+ return
113
+ ```
114
+
115
+ **行为:**
116
+ - CaseBuilder 使用外部传入的检测器
117
+ - 如果配置 `enabled=true`,会自动调用确认
118
+ - 避免重复加载模型,提高性能
119
+
120
+ ## 返回值说明
121
+
122
+ `build_case()` 返回的字典包含以下字段:
123
+
124
+ ```python
125
+ {
126
+ "case": {...}, # 标准 case JSON
127
+ "llm_input": "...", # LLM 输入 Markdown
128
+ "validation": {...}, # 验证结果
129
+ "risk_confirmed": True/False/None, # 主时序模型确认结果(如果启用)
130
+ "main_model_result": {...} # 主时序模型完整结果(如果启用)
131
+ }
132
+ ```
133
+
134
+ **`risk_confirmed` 说明:**
135
+ - `True`: 主时序模型确认有风险
136
+ - `False`: 主时序模型确认无风险
137
+ - `None`: 未启用主时序模型确认,或确认失败
138
+
139
+ ## 向后兼容
140
+
141
+ - 如果配置文件不存在,默认 `enabled=false`(只用 PatchAD)
142
+ - 如果 `enabled=false`,行为与现有代码完全一致
143
+ - 现有代码无需修改即可正常工作
144
+
145
+ ## 注意事项
146
+
147
+ 1. **性能**:自动模式需要加载模型,可能增加初始化时间(约 1-2 秒)
148
+ 2. **内存**:自动模式会占用更多内存(模型加载)
149
+ 3. **错误处理**:如果模型加载失败,会自动降级为只用 PatchAD,不会抛出异常
150
+ 4. **路径**:`model_dir` 可以是相对路径(相对于 `hf_release` 目录)或绝对路径
151
+
data_storage/users/demo_anomaly.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
data_storage/users/demo_pattern.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
data_storage/users/sample_user.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
demo_patchad_cases/demo_anomaly_official.json CHANGED
@@ -2,18 +2,18 @@
2
  "mode": "official",
3
  "sample": "demo_anomaly",
4
  "precheck": {
5
- "event_id": "demo_user-cf1207f8",
6
- "user_id": "demo_user",
7
  "status": "abnormal",
8
  "score": 0.3484,
9
  "threshold": 0.18,
10
- "created_at": "2025-11-27T08:33:52.547782+00:00"
11
  },
12
  "case_bundle": {
13
  "case": {
14
- "case_id": "demo_user-cf1207f8",
15
- "user_id": "demo_user",
16
- "generated_at": "2025-11-27T08:33:52.548064+00:00",
17
  "anomaly_pattern": {
18
  "type": "continuous_anomaly",
19
  "duration_days": 3,
@@ -87,6 +87,41 @@
87
  "passed": true,
88
  "errors": [],
89
  "warnings": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
  }
92
  }
 
2
  "mode": "official",
3
  "sample": "demo_anomaly",
4
  "precheck": {
5
+ "event_id": "demo_anomaly-4ba26c5e",
6
+ "user_id": "demo_anomaly",
7
  "status": "abnormal",
8
  "score": 0.3484,
9
  "threshold": 0.18,
10
+ "created_at": "2025-11-28T02:50:34.582609+00:00"
11
  },
12
  "case_bundle": {
13
  "case": {
14
+ "case_id": "demo_anomaly-4ba26c5e",
15
+ "user_id": "demo_anomaly",
16
+ "generated_at": "2025-11-28T02:50:34.603862+00:00",
17
  "anomaly_pattern": {
18
  "type": "continuous_anomaly",
19
  "duration_days": 3,
 
87
  "passed": true,
88
  "errors": [],
89
  "warnings": []
90
+ },
91
+ "risk_confirmed": false,
92
+ "main_model_result": {
93
+ "anomaly_pattern": {
94
+ "has_pattern": false,
95
+ "duration_days": 0,
96
+ "anomaly_dates": [],
97
+ "anomaly_scores": [],
98
+ "pattern_description": "异常仅持续0天,未达到最小持续天数3天"
99
+ },
100
+ "baseline_info": null,
101
+ "related_indicators": null,
102
+ "daily_results": [
103
+ {
104
+ "date": "2025-11-27T13:47:23.434200",
105
+ "anomaly_score": 0.5196528434753418,
106
+ "is_anomaly": false,
107
+ "hrv_rmssd": 68.38103114993835,
108
+ "hr": 73.06798211937824
109
+ },
110
+ {
111
+ "date": "2025-11-27T13:47:23.434200",
112
+ "anomaly_score": 0.5196528434753418,
113
+ "is_anomaly": false,
114
+ "hrv_rmssd": 68.38103114993835,
115
+ "hr": 73.06798211937824
116
+ },
117
+ {
118
+ "date": "2025-11-27T13:47:23.434200",
119
+ "anomaly_score": 0.5196528434753418,
120
+ "is_anomaly": false,
121
+ "hrv_rmssd": 68.38103114993835,
122
+ "hr": 73.06798211937824
123
+ }
124
+ ]
125
  }
126
  }
127
  }
demo_patchad_cases/demo_anomaly_platform.json CHANGED
@@ -10,7 +10,7 @@
10
  "case": {
11
  "case_id": "platform-demo-evt",
12
  "user_id": "demo_anomaly",
13
- "generated_at": "2025-11-27T08:33:52.547100+00:00",
14
  "anomaly_pattern": {
15
  "type": "continuous_anomaly",
16
  "duration_days": 3,
@@ -83,6 +83,41 @@
83
  "passed": true,
84
  "errors": [],
85
  "warnings": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
  }
88
  }
 
10
  "case": {
11
  "case_id": "platform-demo-evt",
12
  "user_id": "demo_anomaly",
13
+ "generated_at": "2025-11-28T02:50:34.467699+00:00",
14
  "anomaly_pattern": {
15
  "type": "continuous_anomaly",
16
  "duration_days": 3,
 
83
  "passed": true,
84
  "errors": [],
85
  "warnings": []
86
+ },
87
+ "risk_confirmed": false,
88
+ "main_model_result": {
89
+ "anomaly_pattern": {
90
+ "has_pattern": false,
91
+ "duration_days": 0,
92
+ "anomaly_dates": [],
93
+ "anomaly_scores": [],
94
+ "pattern_description": "异常仅持续0天,未达到最小持续天数3天"
95
+ },
96
+ "baseline_info": null,
97
+ "related_indicators": null,
98
+ "daily_results": [
99
+ {
100
+ "date": "2025-11-27T13:47:23.434200",
101
+ "anomaly_score": 0.5196528434753418,
102
+ "is_anomaly": false,
103
+ "hrv_rmssd": 68.38103114993835,
104
+ "hr": 73.06798211937824
105
+ },
106
+ {
107
+ "date": "2025-11-27T13:47:23.434200",
108
+ "anomaly_score": 0.5196528434753418,
109
+ "is_anomaly": false,
110
+ "hrv_rmssd": 68.38103114993835,
111
+ "hr": 73.06798211937824
112
+ },
113
+ {
114
+ "date": "2025-11-27T13:47:23.434200",
115
+ "anomaly_score": 0.5196528434753418,
116
+ "is_anomaly": false,
117
+ "hrv_rmssd": 68.38103114993835,
118
+ "hr": 73.06798211937824
119
+ }
120
+ ]
121
  }
122
  }
123
  }
demo_patchad_cases/demo_pattern_official.json CHANGED
@@ -2,18 +2,18 @@
2
  "mode": "official",
3
  "sample": "demo_pattern",
4
  "precheck": {
5
- "event_id": "demo_user-7f34fd17",
6
- "user_id": "demo_user",
7
  "status": "abnormal",
8
  "score": 0.1969,
9
  "threshold": 0.18,
10
- "created_at": "2025-11-27T08:32:52.564723+00:00"
11
  },
12
  "case_bundle": {
13
  "case": {
14
- "case_id": "demo_user-7f34fd17",
15
- "user_id": "demo_user",
16
- "generated_at": "2025-11-27T08:32:52.565035+00:00",
17
  "anomaly_pattern": {
18
  "type": "continuous_anomaly",
19
  "duration_days": 3,
@@ -87,6 +87,41 @@
87
  "passed": true,
88
  "errors": [],
89
  "warnings": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
  }
92
  }
 
2
  "mode": "official",
3
  "sample": "demo_pattern",
4
  "precheck": {
5
+ "event_id": "demo_pattern-e89db848",
6
+ "user_id": "demo_pattern",
7
  "status": "abnormal",
8
  "score": 0.1969,
9
  "threshold": 0.18,
10
+ "created_at": "2025-11-28T02:50:31.275982+00:00"
11
  },
12
  "case_bundle": {
13
  "case": {
14
+ "case_id": "demo_pattern-e89db848",
15
+ "user_id": "demo_pattern",
16
+ "generated_at": "2025-11-28T02:50:31.298789+00:00",
17
  "anomaly_pattern": {
18
  "type": "continuous_anomaly",
19
  "duration_days": 3,
 
87
  "passed": true,
88
  "errors": [],
89
  "warnings": []
90
+ },
91
+ "risk_confirmed": false,
92
+ "main_model_result": {
93
+ "anomaly_pattern": {
94
+ "has_pattern": false,
95
+ "duration_days": 0,
96
+ "anomaly_dates": [],
97
+ "anomaly_scores": [],
98
+ "pattern_description": "异常仅持续0天,未达到最小持续天数3天"
99
+ },
100
+ "baseline_info": null,
101
+ "related_indicators": null,
102
+ "daily_results": [
103
+ {
104
+ "date": "2025-11-20T08:00:00",
105
+ "anomaly_score": 0.5221930146217346,
106
+ "is_anomaly": false,
107
+ "hrv_rmssd": 76.75195878346763,
108
+ "hr": 69.55658937643992
109
+ },
110
+ {
111
+ "date": "2025-11-21T08:00:00",
112
+ "anomaly_score": 0.522602379322052,
113
+ "is_anomaly": false,
114
+ "hrv_rmssd": 74.44976175238928,
115
+ "hr": 69.4180874776572
116
+ },
117
+ {
118
+ "date": "2025-11-22T08:00:00",
119
+ "anomaly_score": 0.5226739645004272,
120
+ "is_anomaly": false,
121
+ "hrv_rmssd": 75.61461435432079,
122
+ "hr": 69.15957575082764
123
+ }
124
+ ]
125
  }
126
  }
127
  }
demo_patchad_cases/demo_pattern_platform.json CHANGED
@@ -10,7 +10,7 @@
10
  "case": {
11
  "case_id": "platform-demo-evt",
12
  "user_id": "demo_pattern",
13
- "generated_at": "2025-11-27T08:32:52.563962+00:00",
14
  "anomaly_pattern": {
15
  "type": "continuous_anomaly",
16
  "duration_days": 3,
@@ -83,6 +83,41 @@
83
  "passed": true,
84
  "errors": [],
85
  "warnings": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
  }
88
  }
 
10
  "case": {
11
  "case_id": "platform-demo-evt",
12
  "user_id": "demo_pattern",
13
+ "generated_at": "2025-11-28T02:50:31.157741+00:00",
14
  "anomaly_pattern": {
15
  "type": "continuous_anomaly",
16
  "duration_days": 3,
 
83
  "passed": true,
84
  "errors": [],
85
  "warnings": []
86
+ },
87
+ "risk_confirmed": false,
88
+ "main_model_result": {
89
+ "anomaly_pattern": {
90
+ "has_pattern": false,
91
+ "duration_days": 0,
92
+ "anomaly_dates": [],
93
+ "anomaly_scores": [],
94
+ "pattern_description": "异常仅持续0天,未达到最小持续天数3天"
95
+ },
96
+ "baseline_info": null,
97
+ "related_indicators": null,
98
+ "daily_results": [
99
+ {
100
+ "date": "2025-11-20T08:00:00",
101
+ "anomaly_score": 0.5221930146217346,
102
+ "is_anomaly": false,
103
+ "hrv_rmssd": 76.75195878346763,
104
+ "hr": 69.55658937643992
105
+ },
106
+ {
107
+ "date": "2025-11-21T08:00:00",
108
+ "anomaly_score": 0.522602379322052,
109
+ "is_anomaly": false,
110
+ "hrv_rmssd": 74.44976175238928,
111
+ "hr": 69.4180874776572
112
+ },
113
+ {
114
+ "date": "2025-11-22T08:00:00",
115
+ "anomaly_score": 0.5226739645004272,
116
+ "is_anomaly": false,
117
+ "hrv_rmssd": 75.61461435432079,
118
+ "hr": 69.15957575082764
119
+ }
120
+ ]
121
  }
122
  }
123
  }
demo_patchad_cases/demo_pattern_test_official.json ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "official",
3
+ "sample": "demo_pattern_test",
4
+ "precheck": {
5
+ "event_id": "demo_pattern-f92fc28c",
6
+ "user_id": "demo_pattern",
7
+ "status": "abnormal",
8
+ "score": 0.1969,
9
+ "threshold": 0.18,
10
+ "created_at": "2025-11-28T02:45:33.134958+00:00"
11
+ },
12
+ "case_bundle": {
13
+ "case": {
14
+ "case_id": "demo_pattern-f92fc28c",
15
+ "user_id": "demo_pattern",
16
+ "generated_at": "2025-11-28T02:45:33.158854+00:00",
17
+ "anomaly_pattern": {
18
+ "type": "continuous_anomaly",
19
+ "duration_days": 3,
20
+ "trend": "improving",
21
+ "min_score": 0.1969,
22
+ "max_score": 0.2275,
23
+ "avg_score": 0.2153,
24
+ "threshold": 0.18
25
+ },
26
+ "baseline_info": {
27
+ "personal_mean": 75.0,
28
+ "personal_std": 0.0,
29
+ "group_mean": 67.5,
30
+ "record_count": 36,
31
+ "baseline_type": "personal",
32
+ "baseline_reliability": "high"
33
+ },
34
+ "related_indicators": {
35
+ "activity_level": {
36
+ "level": "低",
37
+ "avg_steps": 0.0,
38
+ "trend": "stable"
39
+ },
40
+ "sleep_quality": {
41
+ "available": false,
42
+ "quality": "数据不可用"
43
+ },
44
+ "stress_indicators": {
45
+ "level": "unknown"
46
+ }
47
+ },
48
+ "daily_results": [
49
+ {
50
+ "date": "2025-11-20",
51
+ "hrv_rmssd": 76.75,
52
+ "hr": 69.56,
53
+ "anomaly_score": 0.2275
54
+ },
55
+ {
56
+ "date": "2025-11-21",
57
+ "hrv_rmssd": 74.45,
58
+ "hr": 69.42,
59
+ "anomaly_score": 0.2215
60
+ },
61
+ {
62
+ "date": "2025-11-22",
63
+ "hrv_rmssd": 75.61,
64
+ "hr": 69.16,
65
+ "anomaly_score": 0.1969
66
+ }
67
+ ],
68
+ "user_profile": {
69
+ "age_group": "30-35岁",
70
+ "estimated_age": 33,
71
+ "sex": "男性",
72
+ "exercise": "每周5次以上",
73
+ "coffee": "偶尔",
74
+ "smoking": "不吸烟",
75
+ "drinking": "经常饮酒",
76
+ "MEQ": 62.0,
77
+ "MEQ_type": "晨型"
78
+ },
79
+ "metadata": {
80
+ "detector": "official_patchad",
81
+ "patchad_score": 0.1969,
82
+ "threshold": 0.18
83
+ }
84
+ },
85
+ "llm_input": "# 健康异常检测分析报告\n\n## 异常概览\n**异常类型**:continuous_anomaly \n**持续时长**:3天 \n**异常趋势**:improving \n\n## 异常评分分析\n- **异常分数范围**:0.1969 - 0.2275\n- **平均异常分数**:0.2153\n- **检测阈值**:0.1800\n\n## 当前生理状态评估\n| 指标 | 当前值 | 基线值 | 偏离基线 |\n|------|--------|--------|----------|\n| HRV RMSSD | 75.61 ms | 75.00 ms | +0.8% |\n\n## 历史趋势分析\n| 日期 | HRV (ms) | 心率 (bpm) | 异常分数 |\n|------|----------|------------|----------|\n| 2025-11-20 | 76.75 | 69.56 | 0.2275 |\n| 2025-11-21 | 74.45 | 69.42 | 0.2215 |\n| 2025-11-22 | 75.61 | 69.16 | 0.1969 |\n\n## 相关健康指标分析\n- 活动水平:低(平均步数=0.0,趋势=stable)\n- 睡眠质量:数据不可用\n- 压力指标:暂无显著异常\n\n## 用户背景信息\n- 年龄:33岁(30-35岁)\n- 性别:男性\n- 运动习惯:每周5次以上\n- 咖啡:偶尔 / 饮酒:经常饮酒\n- 生物节律:MEQ=62.0 (晨型)",
86
+ "validation": {
87
+ "passed": true,
88
+ "errors": [],
89
+ "warnings": []
90
+ },
91
+ "risk_confirmed": false,
92
+ "main_model_result": {
93
+ "anomaly_pattern": {
94
+ "has_pattern": false,
95
+ "duration_days": 0,
96
+ "anomaly_dates": [],
97
+ "anomaly_scores": [],
98
+ "pattern_description": "异常仅持续0天,未达到最小持续天数3天"
99
+ },
100
+ "baseline_info": null,
101
+ "related_indicators": null,
102
+ "daily_results": [
103
+ {
104
+ "date": "2025-11-20T08:00:00",
105
+ "anomaly_score": 0.5221930146217346,
106
+ "is_anomaly": false,
107
+ "hrv_rmssd": 76.75195878346763,
108
+ "hr": 69.55658937643992
109
+ },
110
+ {
111
+ "date": "2025-11-21T08:00:00",
112
+ "anomaly_score": 0.522602379322052,
113
+ "is_anomaly": false,
114
+ "hrv_rmssd": 74.44976175238928,
115
+ "hr": 69.4180874776572
116
+ },
117
+ {
118
+ "date": "2025-11-22T08:00:00",
119
+ "anomaly_score": 0.5226739645004272,
120
+ "is_anomaly": false,
121
+ "hrv_rmssd": 75.61461435432079,
122
+ "hr": 69.15957575082764
123
+ }
124
+ ]
125
+ }
126
+ }
127
+ }
demo_patchad_cases/manifest.json CHANGED
@@ -1,26 +1,16 @@
1
  [
2
  {
3
  "sample": "demo_pattern",
4
- "mode": "platform",
5
- "title": "demo_pattern · 模式A:平台自带",
6
- "file": "demo_pattern_platform.json"
7
- },
8
- {
9
- "sample": "demo_pattern",
10
- "mode": "official",
11
- "title": "demo_pattern · 模式B:官方预筛",
12
- "file": "demo_pattern_official.json"
13
- },
14
- {
15
- "sample": "demo_anomaly",
16
- "mode": "platform",
17
- "title": "demo_anomaly · 模式A:平台自带",
18
- "file": "demo_anomaly_platform.json"
19
  },
20
  {
21
  "sample": "demo_anomaly",
22
- "mode": "official",
23
- "title": "demo_anomaly · 模式B:官方预筛",
24
- "file": "demo_anomaly_official.json"
 
25
  }
26
- ]
 
1
  [
2
  {
3
  "sample": "demo_pattern",
4
+ "title": "正常模式案例",
5
+ "file_platform": "demo_pattern_platform.json",
6
+ "file_official": "demo_pattern_official.json",
7
+ "description": "正常心率变异性模式,展示正常情况下的处理流程"
 
 
 
 
 
 
 
 
 
 
 
8
  },
9
  {
10
  "sample": "demo_anomaly",
11
+ "title": "异常模式案例",
12
+ "file_platform": "demo_anomaly_platform.json",
13
+ "file_official": "demo_anomaly_official.json",
14
+ "description": "异常心率变异性模式,展示异常情况下的处理流程"
15
  }
16
+ ]
gradio_app.py CHANGED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,3 +1,4 @@
 
1
  torch>=2.1.0
2
  numpy>=1.24
3
  pandas>=2.0
 
1
+ gradio>=4.44.0
2
  torch>=2.1.0
3
  numpy>=1.24
4
  pandas>=2.0
simulate_patchad_case_pipeline.py CHANGED
@@ -139,33 +139,38 @@ def run_mode_platform(records: List[Dict[str, Any]], sample_name: str, save_dir:
139
 
140
  def run_mode_official(records: List[Dict[str, Any]], sample_name: str, save_dir: Path | None) -> Dict[str, Any] | None:
141
  print("\n=== 模式B:官方 precheck + build_case ===")
142
- windows = split_windows(records, days=3)
143
- if len(windows) < 2:
144
- raise RuntimeError("样例数据不足以生成历史窗口")
145
- current_window = windows[-1]["window"]
146
 
147
  server = PrecheckServer(score_threshold=0.18)
148
- pre_result = server.precheck("demo_user", current_window)
149
  print(f"预筛接口返回: {pre_result}")
150
  if pre_result["status"] != "abnormal":
151
  print("预筛未触发异常,示例终止。")
152
  return None
153
 
154
- pending = server.pop_event(pre_result["event_id"])
 
 
 
 
 
 
 
155
  payload = {
156
- "event_id": pre_result["event_id"],
157
- "user_id": pending["user_id"],
158
- "window_data": pending["window_data"],
159
  "user_profile": sample_profile(),
160
- "history_windows": windows,
161
  "metadata": {
162
  "detector": "official_patchad",
163
- "patchad_score": pending["metadata"]["patchad_score"],
164
- "threshold": pending["metadata"]["threshold"],
165
  },
166
  }
167
 
168
- builder = CaseBuilder()
169
  result = builder.build_case(payload)
170
  print(f"验证结果: {result['validation']}")
171
  print(f"Case ID: {result['case']['case_id']}")
 
139
 
140
  def run_mode_official(records: List[Dict[str, Any]], sample_name: str, save_dir: Path | None) -> Dict[str, Any] | None:
141
  print("\n=== 模式B:官方 precheck + build_case ===")
142
+ print("步骤1:平台传入当前窗口到 precheck 接口")
143
+ current_window = records[-12:] if len(records) >= 12 else records
144
+ user_id = current_window[0].get("deviceId", "demo_user") if current_window else "demo_user"
 
145
 
146
  server = PrecheckServer(score_threshold=0.18)
147
+ pre_result = server.precheck(user_id, current_window)
148
  print(f"预筛接口返回: {pre_result}")
149
  if pre_result["status"] != "abnormal":
150
  print("预筛未触发异常,示例终止。")
151
  return None
152
 
153
+ print(f"\n步骤2:平台收到 event_id={pre_result['event_id']},平台内部合成历史窗口数据")
154
+ # 模拟平台自己合成历史窗口(从历史数据平台获取)
155
+ windows = split_windows(records, days=3)
156
+ if len(windows) < 2:
157
+ raise RuntimeError("样例数据不足以生成历史窗口")
158
+ print(f"平台合成历史窗口: {len(windows)} 天")
159
+
160
+ print(f"\n步骤3:平台调用 build_case,传入 event_id 和合成的历史窗口(不传 window_data,系统从缓存获取)")
161
  payload = {
162
+ "event_id": pre_result["event_id"], # 关键:只传 event_id,不传 window_data
163
+ "user_id": user_id,
 
164
  "user_profile": sample_profile(),
165
+ "history_windows": windows, # 平台自己合成的历史窗口
166
  "metadata": {
167
  "detector": "official_patchad",
168
+ "patchad_score": pre_result["score"],
169
+ "threshold": pre_result["threshold"],
170
  },
171
  }
172
 
173
+ builder = CaseBuilder(precheck_server=server) # 传入 PrecheckServer 实例
174
  result = builder.build_case(payload)
175
  print(f"验证结果: {result['validation']}")
176
  print(f"Case ID: {result['case']['case_id']}")
utils/case_builder.py CHANGED
@@ -5,11 +5,21 @@ import statistics
5
  from dataclasses import dataclass
6
  from datetime import datetime, timezone
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Tuple
9
  from uuid import uuid4
10
 
11
  from utils.patchad_filter import PatchADFilter
12
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  @dataclass
15
  class ValidationResult:
@@ -28,10 +38,80 @@ class ValidationResult:
28
  class CaseBuilder:
29
  """
30
  根据规范化 payload 生成 case JSON + Markdown 输入描述。
 
 
 
 
 
 
 
 
31
  """
32
 
33
- def __init__(self) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  self.patchad = PatchADFilter()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def validate_payload(self, payload: Dict[str, Any]) -> ValidationResult:
37
  errors: List[str] = []
@@ -40,9 +120,31 @@ class CaseBuilder:
40
  if not payload.get("user_id"):
41
  errors.append("缺少 user_id")
42
 
43
- window = payload.get("window_data", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  if not window or len(window) < 12:
45
- errors.append("window_data 不足 12 ")
46
 
47
  profile = payload.get("user_profile")
48
  if not profile:
@@ -50,7 +152,7 @@ class CaseBuilder:
50
 
51
  history = payload.get("history_windows", [])
52
  if not history:
53
- errors.append("缺少 history_windows")
54
 
55
  metadata = payload.get("metadata", {})
56
  if not metadata.get("detector"):
@@ -242,10 +344,40 @@ class CaseBuilder:
242
 
243
  llm_input = self._format_llm_input(case)
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  return {
246
  "case": case,
247
  "llm_input": llm_input,
248
  "validation": validation.to_dict(),
 
 
249
  }
250
 
251
 
 
5
  from dataclasses import dataclass
6
  from datetime import datetime, timezone
7
  from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple
9
  from uuid import uuid4
10
 
11
  from utils.patchad_filter import PatchADFilter
12
 
13
+ # 可选导入主时序模型(如果不存在则使用 None)
14
+ try:
15
+ import sys
16
+ base_dir = Path(__file__).parent.parent
17
+ if str(base_dir) not in sys.path:
18
+ sys.path.insert(0, str(base_dir))
19
+ from wearable_anomaly_detector import WearableAnomalyDetector
20
+ except ImportError:
21
+ WearableAnomalyDetector = None
22
+
23
 
24
  @dataclass
25
  class ValidationResult:
 
38
  class CaseBuilder:
39
  """
40
  根据规范化 payload 生成 case JSON + Markdown 输入描述。
41
+
42
+ 支持配置主时序模型确认(自动/手工模式):
43
+ - enabled=true, mode=auto: 自动调用主时序模型确认风险
44
+ - enabled=false: 只用 PatchAD 分数,不调用主时序模型
45
+
46
+ 支持两种模式:
47
+ - 模式A:平台有PatchAD,直接传入所有数据(window_data + history_windows)
48
+ - 模式B:平台没有PatchAD,通过 event_id 从 PrecheckServer 缓存获取 window_data
49
  """
50
 
51
+ def __init__(
52
+ self,
53
+ config_path: Optional[Path] = None,
54
+ detector: Optional[Any] = None,
55
+ precheck_server: Optional[Any] = None
56
+ ) -> None:
57
+ """
58
+ 初始化 CaseBuilder
59
+
60
+ 参数:
61
+ config_path: 配置文件路径,如果为None则使用默认配置
62
+ detector: 可选的主时序模型检测器实例(如果外部已加载)
63
+ precheck_server: 可选的 PrecheckServer 实例,用于模式B从缓存获取 window_data
64
+ """
65
  self.patchad = PatchADFilter()
66
+ self.detector = detector # 外部传入的检测器(可选)
67
+ self.precheck_server = precheck_server # PrecheckServer 实例(用于模式B)
68
+ self.config = self._load_config(config_path)
69
+
70
+ # 如果配置为自动且未传入 detector,则自动加载
71
+ if self.config.get("main_model_confirmation", {}).get("enabled", False):
72
+ if self.detector is None and WearableAnomalyDetector is not None:
73
+ model_dir = self.config["main_model_confirmation"].get("model_dir")
74
+ if model_dir:
75
+ # 转换为绝对路径(相对于 hf_release 目录)
76
+ base_dir = Path(__file__).parent.parent
77
+ model_path = base_dir / model_dir if not Path(model_dir).is_absolute() else Path(model_dir)
78
+ device = self.config["main_model_confirmation"].get("device", "cpu")
79
+ threshold = self.config["main_model_confirmation"].get("threshold")
80
+
81
+ try:
82
+ self.detector = WearableAnomalyDetector(
83
+ model_dir=model_path,
84
+ device=device,
85
+ threshold=threshold
86
+ )
87
+ print(f"✅ 已自动加载主时序模型: {model_path}")
88
+ except Exception as e:
89
+ print(f"⚠️ 自动加载主时序模型失败: {e},将只使用 PatchAD 分数")
90
+ self.detector = None
91
+
92
+ def _load_config(self, config_path: Optional[Path]) -> Dict[str, Any]:
93
+ """加载配置文件"""
94
+ if config_path is None:
95
+ config_path = Path(__file__).parent.parent / "configs" / "case_builder_config.json"
96
+
97
+ if config_path.exists():
98
+ try:
99
+ with open(config_path, 'r', encoding='utf-8') as f:
100
+ return json.load(f)
101
+ except Exception as e:
102
+ print(f"⚠️ 加载 CaseBuilder 配置失败: {e},使用默认配置")
103
+
104
+ # 返回默认配置(向后兼容)
105
+ return {
106
+ "main_model_confirmation": {
107
+ "enabled": False,
108
+ "mode": "auto",
109
+ "model_dir": "checkpoints/phase2/exp_factor_balanced",
110
+ "device": "cpu",
111
+ "threshold": None,
112
+ "min_duration_days": 3
113
+ }
114
+ }
115
 
116
  def validate_payload(self, payload: Dict[str, Any]) -> ValidationResult:
117
  errors: List[str] = []
 
120
  if not payload.get("user_id"):
121
  errors.append("缺少 user_id")
122
 
123
+ # 模式B:通过 event_id 从缓存获取 window_data
124
+ event_id = payload.get("event_id")
125
+ window = payload.get("window_data")
126
+
127
+ if event_id and self.precheck_server and not window:
128
+ # 模式B:从 PrecheckServer 缓存获取 window_data(不删除,因为可能还需要)
129
+ if self.precheck_server.has_pending(event_id):
130
+ # 使用 pop_event 获取并删除(因为已经使用过了)
131
+ pending = self.precheck_server.pop_event(event_id)
132
+ window = pending.get("window_data")
133
+ payload["window_data"] = window
134
+ if pending.get("user_id") and not payload.get("user_id"):
135
+ payload["user_id"] = pending.get("user_id")
136
+ # 补充 metadata 中的 patchad 信息
137
+ if not payload.get("metadata", {}).get("patchad_score"):
138
+ metadata = payload.get("metadata", {})
139
+ if "patchad_score" not in metadata:
140
+ metadata["patchad_score"] = pending.get("metadata", {}).get("patchad_score", 0.0)
141
+ if "threshold" not in metadata:
142
+ metadata["threshold"] = pending.get("metadata", {}).get("threshold", 0.0)
143
+ payload["metadata"] = metadata
144
+
145
+ # 模式A:直接传入 window_data
146
  if not window or len(window) < 12:
147
+ errors.append("window_data 不足 12 条(模式A需直接传入,模式B需通过 event_id 从缓存获取)")
148
 
149
  profile = payload.get("user_profile")
150
  if not profile:
 
152
 
153
  history = payload.get("history_windows", [])
154
  if not history:
155
+ errors.append("缺少 history_windows(平台需自己合成历史窗口数据)")
156
 
157
  metadata = payload.get("metadata", {})
158
  if not metadata.get("detector"):
 
344
 
345
  llm_input = self._format_llm_input(case)
346
 
347
+ # 【新增】主时序模型确认(如果配置启用)
348
+ risk_confirmed = None
349
+ main_model_result = None
350
+
351
+ if self.config.get("main_model_confirmation", {}).get("enabled", False):
352
+ if self.detector:
353
+ try:
354
+ # 使用主时序模型对历史窗口进行确认
355
+ # 将数据组织成按天的格式:[[day1_data], [day2_data], ...]
356
+ daily_data_points = []
357
+ for item in normalized_history:
358
+ daily_data_points.append(item["window"])
359
+
360
+ if daily_data_points:
361
+ pattern_result = self.detector.detect_pattern(
362
+ daily_data_points,
363
+ days=len(normalized_history),
364
+ min_duration_days=self.config["main_model_confirmation"].get("min_duration_days", 3)
365
+ )
366
+
367
+ # 判断是否确认有风险
368
+ risk_confirmed = pattern_result.get('anomaly_pattern', {}).get('has_pattern', False)
369
+ main_model_result = pattern_result
370
+ except Exception as e:
371
+ print(f"⚠️ 主时序模型确认失败: {e},将只使用 PatchAD 分数")
372
+ risk_confirmed = None
373
+ main_model_result = None
374
+
375
  return {
376
  "case": case,
377
  "llm_input": llm_input,
378
  "validation": validation.to_dict(),
379
+ "risk_confirmed": risk_confirmed, # 【新增】主时序模型确认结果
380
+ "main_model_result": main_model_result, # 【新增】主时序模型完整结果
381
  }
382
 
383
 
流程说明_修正版.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CaseBuilder 流程说明(修正版)
2
+
3
+ ## 两种流程模式
4
+
5
+ ### 模式A:平台有 PatchAD
6
+
7
+ **流程**:
8
+ 1. 平台自己有 PatchAD,自己检测到异常
9
+ 2. 平台自己合成数据:
10
+ - `window_data`:当前窗口(12条)
11
+ - `history_windows`:历史窗口(3-7天,每天12条)
12
+ - `user_profile`:用户档案
13
+ 3. 平台直接调用 `build_case`,传入所有数据
14
+
15
+ **代码示例**:
16
+ ```python
17
+ payload = {
18
+ "event_id": "platform-evt-123",
19
+ "user_id": "user_001",
20
+ "window_data": current_window, # 平台自己传入
21
+ "user_profile": user_profile, # 平台自己传入
22
+ "history_windows": history_windows, # 平台自己合成的
23
+ "metadata": {"detector": "platform_patchad", "score": 0.82},
24
+ }
25
+ builder = CaseBuilder()
26
+ result = builder.build_case(payload)
27
+ ```
28
+
29
+ ---
30
+
31
+ ### 模式B:平台没有 PatchAD
32
+
33
+ **流程**:
34
+ 1. **步骤1**:平台传入当前窗口(12条)到系统的 `/precheck` 接口
35
+ - 系统返回 `event_id` 和 `status`
36
+ - 系统内部缓存 `window_data`
37
+
38
+ 2. **步骤2**:平台收到 `event_id` 后,**在平台内部合成历史窗口数据**
39
+ - 平台从历史数据平台获取最近几天的数据
40
+ - 平台自己按日期分割,生成 `history_windows`
41
+ - 平台自己获取 `user_profile`
42
+
43
+ 3. **步骤3**:平台调用 `build_case`,传入:
44
+ - `event_id`:系统根据此从缓存获取 `window_data`
45
+ - `history_windows`:平台自己合成的
46
+ - `user_profile`:平台自己获取的
47
+ - **不传 `window_data`**(系统从缓存获取)
48
+
49
+ **代码示例**:
50
+ ```python
51
+ # 步骤1:precheck
52
+ from utils.patchad_filter import PrecheckServer
53
+ server = PrecheckServer()
54
+ pre_result = server.precheck(user_id, current_window)
55
+ # 返回: {"event_id": "evt-123", "status": "abnormal", ...}
56
+
57
+ # 步骤2:平台自己合成历史窗口(平台内部操作)
58
+ history_windows = platform_synthesize_history_windows(user_id, days=3)
59
+ user_profile = platform_get_user_profile(user_id)
60
+
61
+ # 步骤3:build_case(只传 event_id,不传 window_data)
62
+ from utils.case_builder import CaseBuilder
63
+ builder = CaseBuilder(precheck_server=server) # 传入 PrecheckServer 实例
64
+ payload = {
65
+ "event_id": pre_result["event_id"], # 关键:只传 event_id
66
+ "user_id": user_id,
67
+ "user_profile": user_profile, # 平台自己获取
68
+ "history_windows": history_windows, # 平台自己合成的
69
+ "metadata": {"detector": "official_patchad"},
70
+ }
71
+ result = builder.build_case(payload) # 系统从缓存获取 window_data
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 关键区别
77
+
78
+ | 项目 | 模式A | 模式B |
79
+ |------|-------|-------|
80
+ | **预筛位置** | 平台内部 | 系统 `/precheck` 接口 |
81
+ | **window_data 来源** | 平台直接传入 | 系统从缓存获取(通过 event_id) |
82
+ | **history_windows 来源** | 平台自己合成 | 平台自己合成 |
83
+ | **user_profile 来源** | 平台自己获取 | 平台自己获取 |
84
+ | **交互次数** | 1次(build_case) | 2次(precheck + build_case) |
85
+
86
+ ---
87
+
88
+ ## 重要说明
89
+
90
+ 1. **模式B中,平台需要自己合成 `history_windows`**
91
+ - 这不是系统的工作
92
+ - 平台从历史数据平台获取数据,按日期分割
93
+ - 系统只负责根据 `event_id` 从缓存获取 `window_data`
94
+
95
+ 2. **模式B中,`build_case` 不需要传入 `window_data`**
96
+ - 系统会根据 `event_id` 从 `PrecheckServer` 的缓存中获取
97
+ - 如果传入了 `window_data`,则优先使用传入的(兼容模式A)
98
+
99
+ 3. **CaseBuilder 需要传入 `precheck_server` 参数(模式B)**
100
+ - 这样系统才能从缓存中获取 `window_data`
101
+ - 模式A不需要传入
102
+