File size: 27,777 Bytes
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
35f8467
50bb88b
 
 
35f8467
 
8193cd7
35f8467
 
 
 
 
 
 
50bb88b
35f8467
 
 
 
 
 
 
 
50bb88b
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
 
 
 
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
35f8467
 
50bb88b
35f8467
 
50bb88b
 
 
35f8467
 
 
 
 
 
 
 
 
33aa89f
 
35f8467
 
33aa89f
35f8467
 
 
33aa89f
35f8467
 
 
 
 
 
33aa89f
 
35f8467
33aa89f
 
35f8467
 
 
 
 
 
33aa89f
 
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bb88b
33aa89f
35f8467
 
 
 
50bb88b
 
 
 
 
 
35f8467
 
 
 
 
50bb88b
35f8467
50bb88b
 
 
 
35f8467
 
 
 
 
50bb88b
 
 
35f8467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33aa89f
35f8467
33aa89f
50bb88b
 
 
 
 
 
 
 
 
 
 
 
6cce707
50bb88b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35f8467
33aa89f
50bb88b
33aa89f
35f8467
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
---
title: PregoPal
emoji: 📈
colorFrom: pink
colorTo: green
sdk: gradio
sdk_version: 6.16.0
python_version: '3.13'
app_file: app.py
pinned: false
license: mit
short_description: Voice-based Pregnant Meal & Nutrition Tracker
---

# PregoPal — 孕期陪护 AI 助手

> 🔴 **【Cline 并行协作核心文档】**
>
> 本 README 是所有并行 Cline 同步接口规范、数据结构、协作约定的唯一全局文档。
> **任何 Cline 不得随意简化、删除或修改本 README 的结构性内容**(包括但不限于:模块接口签名、数据 Schema、协作流程、冲突策略)。
>
> 修改原则:
> 1. 改接口签名 → 必须同步更新对应章节
> 2. 新增模块 → 必须在架构图/模块表中添加条目
> 3. 新增配置 → 必须在 config 章节追加说明
> 4. 数据格式变更 → 更新 Schema 表
>
> 每次 commit 前请检查:本 README 是否仍反映项目最新状态。

---

## 1. 项目概述

PregoPal 是一款面向孕期家庭的 AI 陪护工具,支持:

| 功能 | 状态 | 负责模块 |
|------|------|----------|
| 🎤 声纹识别与家庭成员区分 | 基线可用 | `modules/voiceprint.py` |
| 🍽️ 今日菜品推荐(营养+家庭能力) | 基线可用 | `modules/meal_recommender.py` |
| 📝 饮食记录储存(JSON + Markdown) | 已实现 | `modules/diet_logger.py` + `modules/diet_extractor.py` |
| 📊 营养分析与可视化报告 | 基线可用 | `modules/nutrition_analyzer.py` + `modules/nutrition_standards.py` |
| 🤖 多模态大模型后端(MiniCPM-o 4.5) | ✅ 已部署 Modal | `modal_deploy/deploy.py` + `modal_deploy/client.py` |

**技术栈**: Python 3.13 · Gradio 6.16 · MiniCPM-o 4.5 · Modal · Matplotlib · Pandas

**大模型部署状态**:✅ 已完成,详见 [第11节](#11-多模态大模型部署-modal)




---

## 2. 架构总览

```
app.py                          ← Gradio 薄入口
├── ui/app_builder.py           ← 前端 Tab 布局(3 Tab: 首页/家庭/营养报告)
├── loop.py                     ← 每日自动分析循环(状态机)
│   ├── plugins/base.py         ← 插件基类(LoopPlugin, PluginRegistry, LoopContext)
│   ├── plugins/family_quiz.py  ← 家庭问卷插件(菜谱/体重检查)
│   ├── plugins/diet_summary.py ← 昨日饮食总结
│   ├── plugins/weight_check.py ← 体重检查插件
│   ├── plugins/family_memory.py← 家庭记忆处理
│   ├── plugins/dri_analysis.py ← DRIs 营养对比分析
│   ├── plugins/briefing_generator.py ← 今日简报生成
│   ├── plugins/three_day_summary.py  ← 三天深度缺失分析(数据由营养报告Tab调用)
│   └── plugins/preset_writer.py      ← 预设/缓存写入
├── modules/                    ← 核心业务逻辑(纯函数/无状态)
│   ├── voiceprint.py           ← 声纹识别
│   ├── meal_recommender.py     ← 菜品推荐
│   ├── diet_extractor.py       ← AI 回复数据提取
│   ├── diet_logger.py          ← 饮食记录存储
│   ├── family_manager.py       ← 家庭信息管理
│   ├── nutrition_standards.py  ← 中国官方营养标准
│   ├── nutrition_analyzer.py   ← 营养分析与可视化
│   └── core/                   ← 核心能力层
│       ├── model_loader.py     ← MiniCPM-o 模型加载
│       ├── voice_processor.py  ← 语音处理
│       ├── vision_processor.py ← 视觉处理
│       └── conversation_manager.py ← 对话管理
├── config.py                   ← 全局配置(路径/常量/数据模板)
├── utils.py                    ← 工具函数(CSS/翻译/HTML渲染/营养报告+三天缺失分析)
├── modal_deploy/               ← Modal 部署层
│   ├── deploy.py               ← Modal ASGI 部署入口(llama-server → FastAPI)
│   └── client.py               ← Python API 客户端(chat / chat_with_image / embed)
└── data/                       ← 持久化存储
    ├── diet_logs.json          ← 结构化饮食记录 JSON
    ├── nutrition_db.json       ← 营养数据库 JSON
    ├── family.json             ← 家庭成员声纹 JSON
    ├── family/                 ← 家庭信息 Markdown(recipes/preferences/memory)
    ├── logs/                   ← 每日饮食日志 Markdown
    ├── presets/                ← 预设/缓存 + .daily_status.json
    ├── reports/                ← 营养报告 Markdown
    ├── voices/                 ← 声纹音频文件
    └── nutrition/              ← 营养标准原始文档
```

---

## 3. 核心模块接口说明

> **约定**:模块间通过函数调用传递数据。所有返回 dict 的接口,其 key 约定在下方列出。
> **修改清单**:改任何模块的输入/输出签名时,必须同步更新本 README 对应条目。

### 3.1 声纹识别 — `modules/voiceprint.py`

类:`VoiceprintManager`

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `register_member(name, relation, audio_path)` | `name: str`, `relation: str`, `audio_path: str` (Path) | `(member_info: dict\|None, msg: str)` | 注册新成员声纹,返回 `{"id","name","relation","registered_at","audio_path","features"}` |
| `identify_speaker(audio_path)` | `audio_path: str` (Path) | `(member_info: dict\|None, msg: str)` | 识别说话人,需声纹库非空 |
| `get_members_list()` | — | `str` (格式化文本) | 获取已注册成员列表 |
| `delete_member(member_id)` | `member_id: str` | `str` (结果消息) | 删除指定成员 |

**依赖**:
- `VOICE_DIR``data/voices/`)从 `config.py` 导入
- `FAMILY_FILE``data/family.json`)— JSON 格式 `{"members": [...], "voiceprints": {...}}`
- `config.VOICEPRINT_SIMILARITY_THRESHOLD`(默认 0.7)

**内部数据结构**`data/family.json`):
```json
{
  "members": [
    {
      "id": "abc12345",
      "name": "小红",
      "relation": "孕妇",
      "registered_at": "2026-06-09T07:00:00",
      "audio_path": "data/voices/abc12345.wav",
      "features": {"mean": 0.01, "std": 0.05, "max": 0.5, "min": -0.5, "zero_crossing_rate": 0.3, "energy": 0.001, "duration": 3.5}
    }
  ],
  "voiceprints": {
    "abc12345": {"mean": 0.01, ...}
  }
}
```

> ⚠️ **风险等级:中** — 当前使用频谱统计特征(baseline),后续升级 Whisper encoder 需保持 `register_member`/`identify_speaker` 签名不变。

---

### 3.2 菜品推荐 — `modules/meal_recommender.py`

类:`MealRecommender`

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `get_recommendation(preference, trimester, restrictions)` | `preference: str=""`, `trimester: str="孕中期"`, `restrictions: str=""` | `dict` | 返回今日推荐食谱 |
| `format_meal_plan(recommendation)` | `recommendation: dict`(上一个方法的返回值) | `str` | 格式化为可读文本 |

**输出 dict schema**`get_recommendation` 返回):
```json
{
  "date": "2026-06-09",
  "trimester": "孕中期",
  "preference": "想吃清淡的",
  "focus": "补充蛋白质、钙、铁",
  "meals": {
    "早餐": "小米粥+包子+煮鸡蛋",
    "午餐": "番茄牛腩+杂粮饭+凉拌黄瓜",
    "晚餐": "蒸蛋羹+小米粥+炒青菜",
    "加餐": "酸奶+坚果"
  },
  "tips": ["🌿 孕中期建议:...", "💡 建议每天饮水..."]
}
```

**依赖**:
- `config.MEAL_TEMPLATES`(4 餐次 × 4 选项的食谱模板 dict)
- `config.TRIMESTER_ADJUSTMENTS`(各孕期阶段的 focus/avoid 建议)
- `config.TRIMESTER_TIPS`(各孕期阶段的饮食提示字符串)

> ⚠️ **风险等级:中** — 当前为随机模板推荐。后续对接 AI 对话推荐时需保持 `get_recommendation` 签名,内部逻辑可任意替换。

---

### 3.3 AI 对话数据提取 — `modules/diet_extractor.py`

类:`DietExtractor`(全静态方法)

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `extract_all(text)` | `text: str` (AI 回复全文) | `dict` | 正则提取所有结构化数据 |
| `fallback_extract_diet(text)` | `text: str` | `dict\|None` | 关键词 fallback 提取饮食 |
| `fallback_extract_thinking(text)` | `text: str` | `dict\|None` | 关键词 fallback 提取思考 |
| `robust_extract(text)` | `text: str` | `dict` | 正则优先 → fallback 兜底 |

**`extract_all` 返回 dict schema**:
```json
{
  "diets": [
    {
      "meals": {"早餐": "全麦面包+鸡蛋+牛奶", "午餐": "清蒸鱼", "晚餐": "", "加餐": ""},
      "日期": "2026-06-09",
      "记录人": "孕妇",
      "备注": "孕妇吃了"
    }
  ],
  "recipes": [{"菜名": "清蒸鱼", "制作人": "丈夫", "难度": "简单", "食材": "鱼、姜", "备注": ""}],
  "preferences": [{"人员": "孕妇", "类型": "偏好", "内容": "爱吃酸"}],
  "weights": [{"日期": "2026-06-09", "体重": "65", "记录人": "孕妇自己"}],
  "memories": [{"类型": "事件", "内容": "婆婆今天来家里"}],
  "thinking": {"当前步骤": "记录饮食", "下一步": "分析营养"}  // or None
}
```

**Markdown 提取标记格式**(AI 回复中需含):
```
[EXTRACT_DIET]...[/EXTRACT_DIET]
[EXTRACT_RECIPE]...[/EXTRACT_RECIPE]
[EXTRACT_PREFERENCE]...[/EXTRACT_PREFERENCE]
[EXTRACT_WEIGHT]...[/EXTRACT_WEIGHT]
[EXTRACT_MEMORY]...[/EXTRACT_MEMORY]
[THINKING]...[/THINKING]
```

**辅助函数**:
- `get_extract_prompt(date_str=None) -> str` — 返回含日期占位符的 System Prompt 模板

> ✅ **风险等级:低** — 接口稳定,已存在完整测试(`tests/test_diet_extractor.py`)。

---

### 3.4 饮食记录存储 — `modules/diet_logger.py`

类:`DietLogger`

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `add_record(member_name, member_relation, date, meals, mood, notes)` | `member_name: str`, `member_relation: str`, `date: str (ISO)`, `meals: dict`, `mood: str=""`, `notes: str=""` | `(record: dict, md_path: Path)` | 添加记录 → JSON + MD 双写 |
| `get_recent_records(days=7)` | `days: int` | `list[dict]` | 获取近 N 天记录 |
| `get_all_markdown_files()` | — | `list[Path]` | 所有 MD 日志文件 |
| `parse_diet_record(text) — static` | `text: str` | `dict\|None` | [DIET_RECORD] 标记解析(后续 AI 版本用) |

**`add_record` 返回的 record dict**:
```json
{
  "id": "a1b2c3d4",
  "member_name": "小红",
  "member_relation": "孕妇",
  "date": "2026-06-09",
  "meals": {"早餐": "燕麦粥+坚果", "午餐": "清蒸鱼+米饭"},
  "mood": "挺好",
  "notes": "今天胃口不错",
  "extensions": {},
  "created_at": "2026-06-09T12:00:00"
}
```

**依赖**:
- `config.DIET_LOG_FILE`(`data/diet_logs.json`)— JSON 格式
- `config.LOGS_DIR`(`data/logs/`)— Markdown 日志目录
- `config.DIET_LOG_SCHEMA_VERSION`(当前 "1.0")

**Markdown 日志文件格式**(`data/logs/饮食日志_YYYY-MM-DD.md`):
```markdown
# 🥗 孕期饮食日志

## 📋 基本信息
- **日期**: 2026-06-09
- **记录人**: 孕妇 - 小红
- **记录时间**: 2026-06-09T12:00:00

## 🍽️ 今日饮食记录

### 早餐
- 燕麦粥+坚果
... (output truncated) ...
```

---

### 3.5 家庭信息管理 — `modules/family_manager.py`

类:`RecipeManager`, `PreferenceManager`, `MemoryManager`

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `RecipeManager.add_recipe(name, maker, difficulty, ingredients, notes)` | 字段见函数签名 | `str` (结果消息) | 添加菜谱 |
| `RecipeManager.load_all()` | — | `list[dict]` | 加载所有菜谱 |
| `PreferenceManager.add_preference(member, ptype, content)` | `member/type/content: str` | `str` | 添加偏好/忌口 |
| `PreferenceManager.load_all()` | — | `list[dict]` | 加载所有偏好 |
| `MemoryManager.add_memory(mtype, content)` | `mtype/content: str` | `str` | 添加家庭记事 |
| `MemoryManager.load_all()` | — | `list[dict]` | 加载所有记忆 |

**依赖**:
- `config.FAMILY_DIR`(`data/family/`)— 目录包含 `recipes.md` / `preferences.md` / `memory.md`

---

### 3.6 营养标准 — `modules/nutrition_standards.py`

类:`DRIsParser`(全静态方法)

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `get_dri_for_trimester(trimester)` | `trimester: str`(孕早期/中期/晚期) | `dict` | 获取对应孕期 DRIs 推荐值 |
| `calculate_bmi(weight, height)` | `weight: float (kg)`, `height: float (cm)` | `float` | BMI 计算 |
| `get_weight_gain_goal(pre_bmi, trimester)` | `pre_bmi: float`, `trimester: str` | `dict` | 体重增长建议 |
| `get_dietary_guidelines(trimester)` | `trimester: str` | `dict` | 膳食指南建议 |
| `assess_nutrient_intake(actual, dri)` | `actual: dict (mg)`, `dri: dict (mg)` | `dict` | 实际 vs 推荐对比 |

**输出示例**:
```python
DRIsParser.get_dri_for_trimester("孕中期")
# 返回:
{
  "能量": {"推荐摄入量_kcal": 2100, ...},
  "蛋白质": {"推荐摄入量_g": 70, ...},
  "钙": {"推荐摄入量_mg": 1000, ...},
  "铁": {"推荐摄入量_mg": 24, ...},
  "叶酸": {"推荐摄入量_ug": 600, ...},
  "维生素D": {"推荐摄入量_ug": 10, ...},
  "DHA": {"适宜摄入量_mg": 200, ...}
}
```

> ✅ **风险等级:低** — 接口清晰。如需调整推荐值数值,修改 `DRIsParser` 内部常量即可。

---

### 3.7 营养分析与可视化 — `modules/nutrition_analyzer.py`

类:`NutritionAnalyzer`

| 方法 | 输入 | 输出 | 说明 |
|------|------|------|------|
| `analyze_diet(records)` | `records: list[dict]` (diet_logger 格式) | `dict` | 营养覆盖分析 |
| `generate_report_chart(analysis)` | `analysis: dict`(上一个方法返回) | `matplotlib.figure.Figure` | 4 面板可视化图表 |
| `generate_report_text(analysis)` | `analysis: dict` | `str` | 文本格式报告 |
| `export_report_markdown(analysis, filename)` | `analysis: dict`, `filename: str\|None` | `Path` | 导出 Markdown 报告 |

**`analyze_diet` 返回 dict schema**(最新版,含 `score` 综合评分):
```json
{
  "score": 72,
  "total_days": 7,
  "total_records": 21,
  "meal_counts": {"早餐": 5, "午餐": 6, "晚餐": 6, "加餐": 4},
  "food_items": ["全麦面包", "鸡蛋", "牛奶", "清蒸鱼", ...],
  "nutrition_coverage": {
    "叶酸": {
      "matched_foods": ["菠菜"],
      "covered": true,
      "recommended_foods": ["菠菜", "西兰花", "芦笋"],
      "benefit": "预防胎儿神经管畸形",
      "daily_recommend": "0.4",
      "unit": "mg"
    },
    ...
  },
  "diversity_score": 75,
  "diversity_details": ["[OK] 早餐: 6/7天 (86%)", ...],
  "suggestions": ["⚠️ 以下营养素摄入不足: 铁, 钙", ...]
}
```

**图表输出**`generate_report_chart` — 保留 matplotlib 版本):
1. 左上:营养覆盖雷达图(最多 8 种营养素)
2. 右上:各餐次频率柱状图
3. 左下:饮食多样性评分环形图
4. 右下:营养建议文本框

**依赖**:
- `config.NUTRITION_DB_FILE``data/nutrition_db.json`)— 可自定义
- `config.DEFAULT_NUTRITION_DB`(10 种营养素的内置推荐)
- `config.REPORTS_DIR``data/reports/`- `utils.setup_chinese_font()` — matplotlib 中文字体

> 🔴 **风险等级:高(待改造)** — 当前使用通用 `DEFAULT_NUTRITION_DB` 做营养覆盖分析,**尚未对接 `modules/nutrition_standards.py` 的 DRIs 数据**。改造时需:
> 1. 将 `_calculate_nutrition_coverage` 中的 `self.nutrition_db` 替换为 `DRIsParser.get_dri_for_trimester()`
> 2. 保持 `analyze_diet(records) -> dict` 签名不变
> 3. 保持 `nutrition_coverage` 的 dict key 不变(nutrient name 为 key)

---

### 3.8 插件基类 — `plugins/base.py`

核心类型(供所有插件和 loop 使用):

```python
# 阶段枚举
class LoopStage(Enum):
    FAMILY_QUIZ = "family_quiz"
    SUMMARIZE = "summarize"
    ANALYZE = "analyze"
    BRIEF = "brief"
    THREE_DAY = "three_day"
    CONSOLIDATE = "consolidate"

# 上下文容器(插件间共享)
@dataclass
class LoopContext:
    briefing: dict = {}           # 累积的简报数据
    weight_data: dict = {}
    diet_records: list = []
    family_recipes: list = []     # family_manager.RecipeManager.load_all() 格式
    family_memory: dict = {}      # family_manager.MemoryManager.load_all() 格式
    analysis_results: dict = {}
    errors: list = []

# 插件结果
@dataclass
class PluginResult:
    success: bool = True
    data: dict = {}
    message: str = ""

# 插件基类
class LoopPlugin(ABC):
    @abstractmethod
    def stage(self) -> LoopStage: ...
    @abstractmethod
    def name(self) -> str: ...
    @abstractmethod
    async def run(self, ctx: LoopContext) -> PluginResult: ...
```

> ✅ **风险等级:低** — 核心架构稳定。新增插件:实现 `LoopPlugin`,在 `loop.py` 的 `_register_default_plugins` 中注册即可。

---

## 4. 插件管线一览

| 插件名 | 阶段 | 文件 | 职责 |
|--------|------|------|------|
| `FamilyRecipeQuizPlugin` | `FAMILY_QUIZ` | `plugins/family_quiz.py` | 检查是否需要询问家庭菜谱 |
| `WeightQuizPlugin` | `FAMILY_QUIZ` | `plugins/family_quiz.py` | 检查是否需要询问体重 |
| `DietSummaryPlugin` | `SUMMARIZE` | `plugins/diet_summary.py` | 读取昨日饮食日志 |
| `WeightCheckPlugin` | `SUMMARIZE` | `plugins/weight_check.py` | 体重变化分析 |
| `FamilyMemoryPlugin` | `SUMMARIZE` | `plugins/family_memory.py` | 提取家庭记忆 |
| `DRIAnalysisPlugin` | `ANALYZE` | `plugins/dri_analysis.py` | DRIs 营养对比 |
| `BriefingGeneratorPlugin` | `BRIEF` | `plugins/briefing_generator.py` | 汇总生成今日简报 |
| `ThreeDaySummaryPlugin` | `THREE_DAY` | `plugins/three_day_summary.py` | 三天深度缺失分析(被营养报告Tab调用) |
| `PresetWriterPlugin` | `CONSOLIDATE` | `plugins/preset_writer.py` | 写入预设/缓存 |

**插件向 `ctx.briefing` 写入的 key**(`BriefingGeneratorPlugin` 最终消费):
```
ctx.briefing["trimester"]              # str
ctx.briefing["need_ask_weight"]        # bool
ctx.briefing["weight_quiz_message"]    # str
ctx.briefing["weight_evaluation"]      # dict
ctx.briefing["need_ask_recipe"]        # bool
ctx.briefing["recipe_quiz_message"]    # str
ctx.briefing["yesterday_diet"]         # dict {"status","summary","meal_count"}
ctx.briefing["dri_analysis"]           # dict {"focus_nutrients","summary"}
ctx.briefing["recommended_foods"]      # list[str]
ctx.briefing["family_memory"]          # dict
ctx.briefing["thinking_keywords"]      # str
ctx.briefing["three_day_summary"]      # dict(由报告Tab选择性地读取)
```

---

## 5. 配置模块 — `config.py`

每个 Cline 如需新增全局常量,**追加到同类型区域末尾**,并在 commit message 中注明。

| 配置区 | 主要内容 | 修改风险 |
|--------|---------|----------|
| 目录路径 | `DATA_DIR`, `VOICE_DIR`, `LOGS_DIR`, `REPORTS_DIR`, `FAMILY_FILE`, `DIET_LOG_FILE`, `NUTRITION_DB_FILE` | ⚠️ 中 |
| 常量枚举 | `FAMILY_ROLES`, `TRIMESTERS` | ✅ 低 |
| 数据模板 | `DEFAULT_NUTRITION_DB` (10种营养素), `MEAL_TEMPLATES` (4×4), `TRIMESTER_ADJUSTMENTS`, `TRIMESTER_TIPS` | ⚠️ 中 |
| Schema 版本 | `DIET_LOG_SCHEMA_VERSION = "1.0"` | ⚠️(改版本号前需评估向后兼容) |
| 声纹阈值 | `VOICEPRINT_SIMILARITY_THRESHOLD = 0.7` | ✅ 低 |

---

## 6. 数据文件格式规范

| 文件 | 格式 | Schema | 读模块 | 写模块 |
|------|------|--------|--------|--------|
| `data/diet_logs.json` | JSON | `{"schema_version": "1.0", "records": [...]}` | `diet_logger`, `nutrition_analyzer` | `diet_logger` |
| `data/nutrition_db.json` | JSON | `{"营养素名": {"category", "daily_recommend_mg/g/mcg", "foods": [...], "benefit"}}` | `nutrition_analyzer` | `nutrition_analyzer` (首次初始化) |
| `data/family.json` | JSON | `{"members": [...], "voiceprints": {...}}` | `voiceprint` | `voiceprint` |
| `data/family/recipes.md` | Markdown | `### 菜名\n- **制作人**: ...` | `family_manager.RecipeManager` | `family_manager.RecipeManager` |
| `data/family/preferences.md` | Markdown | `### 成员:姓名\n- **偏好**: ...` | `family_manager.PreferenceManager` | `family_manager.PreferenceManager` |
| `data/family/memory.md` | Markdown | `## 重要事件\n- **日期**: ...` | `family_manager.MemoryManager` | `family_manager.MemoryManager` |
| `data/logs/饮食日志_*.md` | Markdown | 见 3.4 节 | `plugins/diet_summary` | `diet_logger` |
| `data/reports/营养报告_*.md` | Markdown | 见 3.7 节 | 用户 | `nutrition_analyzer` |
| `data/presets/.daily_status.json` | JSON | `{"YYYY-MM-DD": {"summary_done": bool, "day_ended": bool}}` | `loop.DailyStatus` | `loop.DailyStatus` |

---

## 7. 并行 Cline 协作规范

### 7.1 分工建议

```
Cline A: Modules 强化(营养分析对接 DRIs、菜谱推荐 AI 化)
Cline B: UI 界面优化(Gradio 前端增强、报告模板美化)           ← 当前 Cline
Cline C: 声纹升级(Whisper encoder 替换频谱特征)
Cline D: 数据处理与插件增强(diet_extractor fallback、新插件)
Cline E: 大模型集成(对接 Modal API,实现全双工语音对话)        ← 新增(部署已完成)
```

> 注:MiniCPM-o 4.5 已在 Modal 完成部署,API 客户端 `modal_deploy/client.py` 已就绪。
> 接下来可并行推进:A) 前端接入大模型  B) 营养分析对接 DRIs  C) 声纹升级。

### 7.2 关键约定

1. **改接口前先 grep**:用 `search_files` 搜索方法名找到所有调用方
2. **config.py 修改需沟通**:新增常量追加到同类型区域,不删改已有常量名
3. **数据文件向后兼容**:新增字段优先用 `extensions: {}` 或新增可选 key,不删除已有 key
4. **测试保持通过**:`python tests/run_tests.py` 零失败
5. **README 同步更新**:接口签名变更 → 更新本 README 对应章节

### 7.3 Git 工作流(强制执行)

```bash
# 工作前
git pull --rebase origin main

# 开发中:每完成一个独立功能点就提交
git add -A
git commit -m "feat: 描述做了什么"

# 推送前再次 pull
git pull --rebase origin main

# 测试通过后推送
python tests/run_tests.py
git push origin main
```

### 7.4 Commit Message 规范

```
feat: 新功能/新模块/新接口
fix: 修复 bug
test: 添加或修改测试
refactor: 重构(不改功能)
docs: 文档更新(含本 README)
chore: 配置/依赖/路径调整
```

### 7.5 冲突处理策略

| 冲突类型 | 处理方式 |
|---------|---------|
| `config.py` 常量冲突 | 取并集,双方新增常量都保留 |
| 同函数不同实现 | 保留逻辑更完整的一方,另一方改动如果无冲突则合并到合适位置 |
| 数据文件(JSON/MD)冲突 | 保留两个版本共有的条目 + 各自独有条目(去重) |
| 测试文件冲突 | **取并集**:保留所有测试用例 |
| **本 README 冲突** | **取行数更长的一方**,手动整合 |
| `.gitignore` 冲突 | 取并集 |

---

## 8. UI 设计说明(当前 Cline B 负责)

### 8.1 页面结构

3 Tab 布局,使用 **唯一一个 `@gr.render`** 包裹全部 Tab(避免 Gradio 内部字典迭代崩溃):

| Tab | ID | 功能 |
|-----|----|------|
| 🏠 首页 | `tab_home` | 语音按钮 + AI 思考 + 信息卡片 + 最近记录 |
| 👨‍👩‍👧‍👦 家庭 | `tab_family` | 家庭菜谱/偏好/记事 HTML 卡片展示 |
| 📈 营养报告 | `tab_report` | 纯 HTML 营养仪表盘 + 三天缺失深度分析(已合并) |

> **架构变更说明**(v2.0):
> - 原「三天总结」Tab **已合并到营养报告 Tab** 中,作为一个统一下沉展示区块
> - 营养报告使用 Slider 防抖自动生成(0.8s),包含基础营养评分 + 三天缺失深度分析
> - 三天缺失分析页面使用与营养报告一致的设计语言(渐变 Header + 紧凑标签 + 结构化 Summary)

### 8.2 设计风格

- **Glassmorphism 毛玻璃**`backdrop-filter: blur(20px)` 半透明卡片
- **粉色暖调主题**:主色 `#E91E63`、渐变 `#FF6B9D → #FFD6E0`
- **纯 HTML 渲染**:家庭信息卡、营养报告、三天缺失分析均使用 HTML + CSS,无需生成图片
- **中英文切换**`lang_state` 驱动唯一 `@gr.render`
- **三天缺失分析**  - 持续不足营养素采用**紧凑标签式**显示(pill 标签,一行多个)
  - 食材补充建议区域采用 **1:2 扩大布局**(右侧占 2/3 宽度,每行 6 个食物标签)
  - 总结部分采用**结构化 UI 卡片**(红色/橙色/绿色 左侧边框,按严重程度分类显示)

### 8.3 关键文件

| 文件 | 职责 |
|------|------|
| `ui/app_builder.py` | Gradio 界面构建(3 Tab + 语言切换 + 报告自动生成) |
| `utils.py` | CSS 定义 + 翻译字典 + HTML 渲染函数(家庭卡片 + 营养仪表盘 + 三天缺失分析) |
| `plugins/three_day_summary.py` | 三天缺失分析插件(数据层,被报告 Tab 异步调用) |

---

## 9. 测试

```bash
# 运行全部测试
python tests/run_tests.py
```

当前测试覆盖:
- `test_diet_extractor.py` — ✅ 通过(正则解析 + fallback 提取)
- `test_family_manager.py` — ✅ 通过(菜谱/偏好/记忆 CRUD)
- `test_nutrition_standards.py` — ✅ 通过(BMI/膳食指南/DRIs 数据)
- `test_loop.py` — 循环执行测试
- `test_plugins.py` — 插件集成测试

---

## 10. Model: MiniCPM-o 4.5

```
Model to be Used: MiniCPM-o 4.5
```

核心能力层(`core/` 目录)提供模型加载、语音处理、视觉处理、对话管理的底层能力封装。详见各文件 docstring。

**部署状态**: ✅ 已完成 Modal 部署(见第11节)

---

## 11. 多模态大模型部署 (Modal)

### 11.1 部署状态

| 项目 | 状态 |
|------|------|
| API URL | `https://andrew-jiabin--prego-pal-minicpm-serve.modal.run` |
| Modal App | `prego-pal-minicpm` |
| GPU | T4 (16GB) |
| 模型 | MiniCPM-o 4.5 Q4_K_M (~12GB) + vision/audio/TTS 投影层 |
| 冷启动 | ~2-8 分钟(首次请求触发容器启动 + 模型加载) |
| 闲置缩容 | 300s 无请求自动缩容 |
| 并发 | `max_inputs=10` |

### 11.2 API 端点

| 端点 | 方法 | 功能 |
|------|------|------|
| `/v1/chat/completions` | POST | OpenAI 兼容聊天(文本) |
| `/v1/embeddings` | POST | 文本嵌入 |
| `/v1/models` | GET | 模型列表 |
| `/v1/multimodal/chat` | POST | 多模态(图片+文本) |
| `/health` | GET | 健康检查 |
| `/` | GET | 服务信息 |

### 11.3 Python 客户端

```python
from modal_deploy.client import MiniCPMClient

client = MiniCPMClient(base_url="https://andrew-jiabin--prego-pal-minicpm-serve.modal.run")

# 文本对话
response = client.ask("今天孕妇可以吃什么?")

# 多模态(图片理解)
response = client.describe_image(image_base64_str)

# 嵌入向量
embeddings = client.embed(["孕期饮食建议", "番茄牛腩"])
```

详见 `modal_deploy/client.py` 完整文档。

### 11.4 部署文件

| 文件 | 说明 |
|------|------|
| `modal_deploy/deploy.py` | Modal ASGI 部署入口(llama-server → FastAPI 包装) |
| `modal_deploy/client.py` | Python API 客户端(chat / chat_with_image / embed) |
| `docs/部署经验_Modal_MiniCPM-o.md` | 详细部署经验文档(踩坑记录、成本估算、后续优化) |

---

*最后更新: 2026-06-10 | 由 PregoPal Cline 团队维护*

> **⚠️ 重要提醒**:本 README 是所有并行 Cline 的协作基础,任何 Cline 在修改代码后,若涉及接口、数据格式、配置的变更,必须同步更新本文件。不得随意简化、删除结构性内容。