J.B-Lin commited on
Commit
0f2d3d7
·
1 Parent(s): b7a1798

CLine 界面初版

Browse files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .vscode/
README.md CHANGED
@@ -13,3 +13,6 @@ short_description: Voice-based Pregnant Meal & Nutrition Tracker
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
16
+
17
+
18
+ Model to be Used: MiniCPM-o 4.5
__pycache__/config.cpython-311.pyc ADDED
Binary file (4.32 kB). View file
 
app.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 孕期陪护AI助手
3
+ =========================
4
+ 主入口:启动 Gradio 应用。
5
+
6
+ 架构:
7
+ app.py ← 薄入口(仅启动)
8
+ config.py ← 全局配置
9
+ core/ ← AI 核心层(等待 MiniCPM-o 部署)
10
+ modules/ ← 业务逻辑层
11
+ ui/ ← 表现层(Gradio 界面)
12
+ data/ ← 数据存储
13
+ """
14
+
15
+ import gradio as gr
16
+ from ui.app_builder import create_app
17
+
18
+ if __name__ == "__main__":
19
+ demo = create_app()
20
+ demo.launch(
21
+ theme=gr.themes.Soft(
22
+ primary_hue="pink",
23
+ secondary_hue="green",
24
+ )
25
+ )
config.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 全局配置
3
+ ====================
4
+ 所有路径、常量、营养数据库、食谱模板集中管理。
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ # ============================================================
10
+ # 目录路径
11
+ # ============================================================
12
+ DATA_DIR = Path("data")
13
+ VOICE_DIR = DATA_DIR / "voices"
14
+ LOGS_DIR = DATA_DIR / "logs"
15
+ REPORTS_DIR = DATA_DIR / "reports"
16
+ FAMILY_FILE = DATA_DIR / "family.json"
17
+ DIET_LOG_FILE = DATA_DIR / "diet_logs.json"
18
+ NUTRITION_DB_FILE = DATA_DIR / "nutrition_db.json"
19
+
20
+ # 确保目录存在
21
+ for d in [DATA_DIR, VOICE_DIR, LOGS_DIR, REPORTS_DIR]:
22
+ d.mkdir(parents=True, exist_ok=True)
23
+
24
+ # ============================================================
25
+ # 家庭成员身份枚举
26
+ # ============================================================
27
+ FAMILY_ROLES = ["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"]
28
+
29
+ # ============================================================
30
+ # 孕期阶段
31
+ # ============================================================
32
+ TRIMESTERS = ["孕早期", "孕中期", "孕晚期"]
33
+
34
+ # ============================================================
35
+ # 营养数据库(内置推荐)
36
+ # 注意:此为基于通用知识的估算,后续将替换为中国官方标准
37
+ # ============================================================
38
+ DEFAULT_NUTRITION_DB = {
39
+ "叶酸": {"category": "维生素", "daily_recommend_mg": 0.4, "foods": ["菠菜", "西兰花", "芦笋", "牛肝", "豆类"], "benefit": "预防胎儿神经管畸形"},
40
+ "铁": {"category": "矿物质", "daily_recommend_mg": 27, "foods": ["红肉", "动物肝脏", "菠菜", "黑木耳", "红枣"], "benefit": "预防孕期贫血"},
41
+ "钙": {"category": "矿物质", "daily_recommend_mg": 1000, "foods": ["牛奶", "酸奶", "豆腐", "芝麻", "小鱼干"], "benefit": "促进胎儿骨骼发育"},
42
+ "DHA": {"category": "脂肪酸", "daily_recommend_mg": 200, "foods": ["三文鱼", "鳕鱼", "核桃", "亚麻籽", "藻油"], "benefit": "促进胎儿大脑发育"},
43
+ "蛋白质": {"category": "宏量营养素", "daily_recommend_g": 70, "foods": ["鸡蛋", "鸡肉", "鱼肉", "豆腐", "牛奶"], "benefit": "胎儿生长发育的基础"},
44
+ "维生素C": {"category": "维生素", "daily_recommend_mg": 85, "foods": ["橙子", "猕猴桃", "草莓", "番茄", "青椒"], "benefit": "增强免疫力,促进铁吸收"},
45
+ "维生素D": {"category": "维生素", "daily_recommend_mcg": 10, "foods": ["蛋黄", "肝脏", "三文鱼", "蘑菇", "晒太阳"], "benefit": "促进钙吸收"},
46
+ "膳食纤维": {"category": "宏量营养素", "daily_recommend_g": 25, "foods": ["燕麦", "全麦面包", "红薯", "蔬菜", "水果"], "benefit": "预防孕期便秘"},
47
+ "锌": {"category": "矿物质", "daily_recommend_mg": 11, "foods": ["牡蛎", "瘦肉", "坚果", "全谷物", "豆类"], "benefit": "促进胎儿生长发育"},
48
+ "碘": {"category": "矿物质", "daily_recommend_mcg": 220, "foods": ["海带", "紫菜", "碘盐", "海鱼", "贝类"], "benefit": "促进胎儿智力发育"},
49
+ }
50
+
51
+ # ============================================================
52
+ # 孕期推荐食谱模板
53
+ # ============================================================
54
+ MEAL_TEMPLATES = {
55
+ "早餐": ["全麦面包+鸡蛋+牛奶", "燕麦粥+坚果+水果", "小米粥+包子+煮鸡蛋", "豆浆+全麦馒头+蔬菜沙拉"],
56
+ "午餐": ["清蒸鱼+糙米饭+炒时蔬", "番茄牛腩+杂粮饭+凉拌黄瓜", "鸡肉沙拉+藜麦饭+紫菜蛋花汤", "豆腐煲+红薯饭+清炒西兰花"],
57
+ "晚餐": ["鲫鱼豆腐汤+小馒头+清炒菠菜", "蒸蛋羹+小米粥+炒青菜", "虾仁西兰花+糙米饭+番茄汤", "瘦肉粥+蒸南瓜+凉拌木耳"],
58
+ "加餐": ["酸奶+坚果", "水果拼盘", "红枣枸杞茶+全麦饼干", "牛奶+燕麦能量棒"],
59
+ }
60
+
61
+ # ============================================================
62
+ # 孕期阶段调整建议
63
+ # ============================================================
64
+ TRIMESTER_ADJUSTMENTS = {
65
+ "孕早期": {"focus": "补充叶酸,缓解孕吐", "avoid": ["油腻", "辛辣"]},
66
+ "孕中期": {"focus": "补充蛋白质、钙、铁", "avoid": []},
67
+ "孕晚期": {"focus": "控制体重,补充DHA", "avoid": ["高糖", "高盐"]},
68
+ }
69
+
70
+ TRIMESTER_TIPS = {
71
+ "孕早期": "🌱 孕早期建议:多补充叶酸,少食多餐,避免空腹。可以吃一些苏打饼干缓解孕吐。",
72
+ "孕中期": "🌿 孕中期建议:胎儿快速发育期,注意补充优质蛋白和钙质。建议每天喝一杯牛奶。",
73
+ "孕晚期": "🌾 孕晚期建议:控制体重增长,减少高糖高盐食物。多补充DHA促进胎儿大脑发育。",
74
+ }
75
+
76
+ # ============================================================
77
+ # 数据存储 Schema 版本
78
+ # ============================================================
79
+ DIET_LOG_SCHEMA_VERSION = "1.0"
80
+
81
+ # ============================================================
82
+ # 声纹识别阈值
83
+ # ============================================================
84
+ VOICEPRINT_SIMILARITY_THRESHOLD = 0.7
core/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - AI 核心层
3
+ ======================
4
+ MiniCPM-o 4.5 多模态模型加载与推理。
5
+
6
+ 当前:空接口(等待 MiniCPM-o 部署)
7
+ 后续:填充模型加载、语音处理、视觉处理、对话管理
8
+ """
core/conversation_manager.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 对话管理器
3
+ ======================
4
+ 对话管理 + 系统提示词构建 + [DIET_RECORD] 标记解析。
5
+
6
+ 当前:空接口(等待 MiniCPM-o 部署)
7
+ 后续:管理任务模式/闲聊模式切换,构建系统提示词
8
+ """
9
+
10
+
11
+ class ConversationManager:
12
+ """对话管理:模式切换、提示词构建、输出解析"""
13
+
14
+ MODE_TASK = "task"
15
+ MODE_CHAT = "chat"
16
+
17
+ def __init__(self):
18
+ self.current_mode = self.MODE_CHAT
19
+ self.current_speaker = None
20
+ self.conversation_history = []
21
+
22
+ def build_system_prompt(self) -> str:
23
+ """
24
+ 构建系统提示词(待实现)
25
+ 包含:角色定义、当前模式、说话人身份、输出格式规则
26
+ """
27
+ raise NotImplementedError("等待 MiniCPM-o 部署后实现")
28
+
29
+ def switch_mode(self, mode: str):
30
+ """切换对话模式"""
31
+ self.current_mode = mode
32
+
33
+ def set_speaker(self, speaker_info: dict):
34
+ """设置当前说话人"""
35
+ self.current_speaker = speaker_info
36
+
37
+ def parse_response(self, response: str) -> dict:
38
+ """
39
+ 解析模型回复
40
+ 检测 [DIET_RECORD] 标记等结构化输出
41
+ """
42
+ from modules.diet_logger import DietLogger
43
+ diet_record = DietLogger.parse_diet_record(response)
44
+ return {
45
+ "text": response,
46
+ "diet_record": diet_record
47
+ }
core/model_loader.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 模型加载器
3
+ ======================
4
+ 加载 MiniCPM-o 4.5 多模态模型。
5
+
6
+ 当前:空接口(等待 MiniCPM-o 部署)
7
+ 后续:使用 OpenBMB 官方推理代码加载模型
8
+ """
9
+
10
+
11
+ class ModelLoader:
12
+ """MiniCPM-o 4.5 模型加载器"""
13
+
14
+ def __init__(self, model_path: str = None):
15
+ self.model_path = model_path
16
+ self.model = None
17
+ self.processor = None
18
+
19
+ def load(self):
20
+ """加载模型(待实现)"""
21
+ raise NotImplementedError("等待 MiniCPM-o 4.5 部署后实现")
22
+
23
+ def unload(self):
24
+ """卸载模型释放显存"""
25
+ self.model = None
26
+ self.processor = None
core/vision_processor.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 视觉处理器
3
+ ======================
4
+ SigLip2 视觉理解(食物/环境/表情识别)。
5
+
6
+ 当前:空接口(等待 MiniCPM-o 部署)
7
+ 后续:使用 MiniCPM-o 内置的 SigLip2 处理摄像头画面
8
+ """
9
+
10
+
11
+ class VisionProcessor:
12
+ """视觉处理:理解摄像头画面"""
13
+
14
+ def __init__(self):
15
+ self.vision_model = None
16
+
17
+ def analyze_frame(self, image_path: str) -> dict:
18
+ """
19
+ 分析单帧图像(待实现)
20
+ 返回:画面中的食物、环境、人物表情等信息
21
+ """
22
+ raise NotImplementedError("等待 MiniCPM-o 部署后实现")
core/voice_processor.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 语音处理器
3
+ ======================
4
+ Whisper-medium ASR + 声纹 embedding 提取。
5
+
6
+ 当前:空接口(等待 MiniCPM-o 部署)
7
+ 后续:提取 Whisper encoder hidden states 做 speaker embedding
8
+ """
9
+
10
+
11
+ class VoiceProcessor:
12
+ """语音处理:ASR + 声纹特征提取"""
13
+
14
+ def __init__(self):
15
+ self.asr_model = None
16
+ self.sample_rate = 16000
17
+
18
+ def transcribe(self, audio_path: str) -> str:
19
+ """语音转文字(待实现)"""
20
+ raise NotImplementedError("等待 MiniCPM-o 部署后实现")
21
+
22
+ def extract_speaker_embedding(self, audio_path: str) -> list:
23
+ """
24
+ 提取说话人声纹 embedding(待实现)
25
+ 方案:从 Whisper-medium encoder 的 hidden states 中提取
26
+ """
27
+ raise NotImplementedError("等待 MiniCPM-o 部署后实现")
data/nutrition/raw/中国孕期妇女膳食指南2022图片转md版.md ADDED
File without changes
data/nutrition_db.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "叶酸": {
3
+ "category": "维生素",
4
+ "daily_recommend_mg": 0.4,
5
+ "foods": [
6
+ "菠菜",
7
+ "西兰花",
8
+ "芦笋",
9
+ "牛肝",
10
+ "豆类"
11
+ ],
12
+ "benefit": "预防胎儿神经管畸形"
13
+ },
14
+ "铁": {
15
+ "category": "矿物质",
16
+ "daily_recommend_mg": 27,
17
+ "foods": [
18
+ "红肉",
19
+ "动物肝脏",
20
+ "菠菜",
21
+ "黑木耳",
22
+ "红枣"
23
+ ],
24
+ "benefit": "预防孕期贫血"
25
+ },
26
+ "钙": {
27
+ "category": "矿物质",
28
+ "daily_recommend_mg": 1000,
29
+ "foods": [
30
+ "牛奶",
31
+ "酸奶",
32
+ "豆腐",
33
+ "芝麻",
34
+ "小鱼干"
35
+ ],
36
+ "benefit": "促进胎儿骨骼发育"
37
+ },
38
+ "DHA": {
39
+ "category": "脂肪酸",
40
+ "daily_recommend_mg": 200,
41
+ "foods": [
42
+ "三文鱼",
43
+ "鳕鱼",
44
+ "核桃",
45
+ "亚麻籽",
46
+ "藻油"
47
+ ],
48
+ "benefit": "促进胎儿大脑发育"
49
+ },
50
+ "蛋白质": {
51
+ "category": "宏量营养素",
52
+ "daily_recommend_g": 70,
53
+ "foods": [
54
+ "鸡蛋",
55
+ "鸡肉",
56
+ "鱼肉",
57
+ "豆腐",
58
+ "牛奶"
59
+ ],
60
+ "benefit": "胎儿生长发育的基础"
61
+ },
62
+ "维生素C": {
63
+ "category": "维生素",
64
+ "daily_recommend_mg": 85,
65
+ "foods": [
66
+ "橙子",
67
+ "猕猴桃",
68
+ "草莓",
69
+ "番茄",
70
+ "青椒"
71
+ ],
72
+ "benefit": "增强免疫力,促进铁吸收"
73
+ },
74
+ "维生素D": {
75
+ "category": "维生素",
76
+ "daily_recommend_mcg": 10,
77
+ "foods": [
78
+ "蛋黄",
79
+ "肝脏",
80
+ "三文鱼",
81
+ "蘑菇",
82
+ "晒太阳"
83
+ ],
84
+ "benefit": "促进钙吸收"
85
+ },
86
+ "膳食纤维": {
87
+ "category": "宏量营养素",
88
+ "daily_recommend_g": 25,
89
+ "foods": [
90
+ "燕麦",
91
+ "全麦面包",
92
+ "红薯",
93
+ "蔬菜",
94
+ "水果"
95
+ ],
96
+ "benefit": "预防孕期便秘"
97
+ },
98
+ "锌": {
99
+ "category": "矿物质",
100
+ "daily_recommend_mg": 11,
101
+ "foods": [
102
+ "牡蛎",
103
+ "瘦肉",
104
+ "坚果",
105
+ "全谷物",
106
+ "豆类"
107
+ ],
108
+ "benefit": "促进胎儿生长发育"
109
+ },
110
+ "碘": {
111
+ "category": "矿物质",
112
+ "daily_recommend_mcg": 220,
113
+ "foods": [
114
+ "海带",
115
+ "紫菜",
116
+ "碘盐",
117
+ "海鱼",
118
+ "贝类"
119
+ ],
120
+ "benefit": "促进胎儿智力发育"
121
+ }
122
+ }
docs/开发日志.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PregoPal 开发日志
2
+
3
+ ## 2026-06-08 重构:四层架构 + 模块化
4
+
5
+ ### 架构变更
6
+
7
+ ```
8
+ PregoPal/
9
+ ├── app.py # 薄入口(仅启动 Gradio)
10
+ ├── config.py # 全局配置(路径、模板、营养数据库)
11
+ ├── utils.py # 工具函数(中文字体设置)
12
+ ├── core/ # AI 核心层(等待 MiniCPM-o 部署)
13
+ │ ├── __init__.py
14
+ │ ├── model_loader.py # 模型加载器(空接口)
15
+ │ ├── voice_processor.py # 语音处理器(空接口)
16
+ │ ├── vision_processor.py # 视觉处理器(空接口)
17
+ │ └── conversation_manager.py # 对话管理器(空接口)
18
+ ├── modules/ # 业务逻辑层
19
+ │ ├── __init__.py
20
+ │ ├── voiceprint.py # 声纹识别
21
+ │ ├── meal_recommender.py # 菜品推荐
22
+ │ ├── diet_logger.py # 饮食记录
23
+ │ └── nutrition_analyzer.py # 营养分析
24
+ ├── ui/ # 表现层
25
+ │ ├── __init__.py
26
+ │ └── app_builder.py # Gradio 界面构建
27
+ ├── data/ # 数据存储
28
+ │ ├── voices/ # 声纹音频
29
+ │ ├── logs/ # 饮食日志(Markdown)
30
+ │ ├── reports/ # 营养报告(Markdown)
31
+ │ ├── family.json # 家庭成员数据
32
+ │ ├── diet_log.json # 饮食记录数据
33
+ │ └── nutrition_db.json # 营养数据库
34
+ ├── docs/ # 文档
35
+ │ ├── 项目理解_技术架构.md
36
+ │ └── 开发日志.md
37
+ ├── requirements.txt # 依赖清单
38
+ └── README.md
39
+ ```
40
+
41
+ ### 模块说明
42
+
43
+ | 模块 | 文件 | 功能 |
44
+ |------|------|------|
45
+ | 声纹识别 | `modules/voiceprint.py` | 家庭成员注册/识别/管理 |
46
+ | 菜品推荐 | `modules/meal_recommender.py` | 根据孕期阶段推荐食谱 |
47
+ | 饮食记录 | `modules/diet_logger.py` | 记录饮食+Markdown自动生成 |
48
+ | 营养分析 | `modules/nutrition_analyzer.py` | 分析+可视化图表+报告导出 |
49
+
50
+ ### 界面 Tab
51
+
52
+ 1. 🔊 声纹识别 - 注册/识别/列表
53
+ 2. 🍳 今日菜品推荐 - 孕期阶段+偏好
54
+ 3. 📝 饮食记录 - 表单+Markdown日志
55
+ 4. 📊 营养报告 - 分析+图表+导出
56
+ 5. 💝 关于 - 项目介绍
57
+
58
+ ### 运行状态
59
+
60
+ - Gradio 6.16.0 运行在 localhost:7860
61
+ - 所有 5 个 Tab 正常加载
62
+ - 声纹识别、菜品推荐、饮食记录、营养报告功能可用
63
+ - core/ 层预留接口等待 MiniCPM-o 部署
docs/项目理解_技术架构.md ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🌸 PregoPal 项目理解与技术架构(v3 - 根据批注修正)
2
+
3
+ > 本文档用于对齐对项目的整体认知。v3 已整合你所有批注。
4
+
5
+ ---
6
+
7
+ ## 一、一句话定位
8
+
9
+ **一个基于 MiniCPM-o 4.5 全双工多模态大模型的 AI 孕期陪护助手**:家人(丈夫/婆婆/妈妈/孕妇本人)打开摄像头和麦克风,与 AI 实时语音对话,AI 能「看见」和「听见」——通过视觉识别食物、通过声纹区分身份、通过对话理解需求,自动记录饮食、推荐食谱、分析营养并输出报告。
10
+
11
+ > **✅ 已确认。**
12
+
13
+ ---
14
+
15
+ ## 二、四大模块详解
16
+
17
+ ### 模块 1:🔊 声纹识别 + 多模态身份感知
18
+
19
+ **核心变化:从「单独录音注册」变为「对话中自然感知」**
20
+
21
+ **业务需求:**
22
+ - 用户在实时对话过程中,AI 自动识别说话人身份(丈夫/婆婆/妈妈/孕妇…)
23
+ - 无需单独「注册」步骤,在自然对话中完成声纹采集和识别
24
+ - 结合摄像头画面辅助确认(视觉+语音多模态)
25
+
26
+ **技术选型(最终方案):**
27
+ 1. **主要方案:Whisper-medium encoder embedding**
28
+ - MiniCPM-o 4.5 内置了 Whisper-medium 作为语音编码器
29
+ - 当音频经过 Whisper encoder 时,中间层隐藏状态包含了说话人的声学特征(音色/语速/口音)
30
+ - 做法:提取 encoder 输出的 hidden states,做时间维度的平均池化,得到固定维度的 **speaker embedding**
31
+ - 新音频与该 embedding 做余弦相似度比对,判断是否为同一人
32
+ - 这是业界成熟做法,比当前 6 维统计特征鲁棒许多
33
+
34
+ 2. **备选方案:保留当前频谱相似度方案**
35
+ - 作为 fallback 兜底
36
+ - 也可作为置信度参考,与 embedding 方案加权融合
37
+
38
+ **部署说明:**
39
+ - MiniCPM-o 4.5 是一个统一的多模态模型,不是多个独立组件的拼装
40
+ - 需要用 MiniCPM-o 官方推理框架(OpenBMB 提供)部署
41
+ - **llama.cpp 对 MiniCPM-o 的全双工多模态支持可能不完整**,后续会根据实际情况选择:A) OpenBMB 官方推理 B) Modal GPU 部署 C) llama.cpp(如果兼容)
42
+
43
+ > **✅ 已根据你的批注修正:优先开发 Whisper-medium embedding 方案,频谱相似度保留为备选。**
44
+
45
+ ---
46
+
47
+ ### 模块 2:🍳 智能菜品推荐(由 MiniCPM-o 4.5 驱动)
48
+
49
+ **核心变化:从「模板随机选」变为「AI 实时对话推荐」**
50
+
51
+ **业务需求:**
52
+ - 用户和 AI 对话:「今天想吃鱼」「有点没胃口」「嘴里发苦」…
53
+ - AI 理解上下文 → 生成个性化推荐
54
+ - 输出三餐+加餐方案 + 理由 + 注意事项
55
+
56
+ **技术选型:**
57
+ - **MiniCPM-o 4.5(统一模型)**:直接用对话能力做推荐
58
+ - **营养数据本地嵌入**:把中国孕期营养数据整理成 JSON,在系统提示词中提供给模型
59
+ - **不需要 RAG**(你说得对,过度设计了),只需要把营养数据放在上下文里
60
+ - **规则兜底**:`MEAL_TEMPLATES` 保留作为离线 fallback(无模型时可用)
61
+
62
+ **注意:** MiniCPM-o 4.5 本身是一个统一的端到端多模态模型,不是「全家桶」——SigLip2、Whisper、Qwen3、CosyVoice2 是其内部组件,不是独立安装的库。
63
+
64
+ > **✅ 已根据你的批注修正:去掉 RAG,营养数据本地嵌入,规则兜底保留。**
65
+
66
+ ---
67
+
68
+ ### 模块 3:📝 饮食记录 — 对话中自动记录,存 Markdown
69
+
70
+ **核心变化:从「手动填表单」变为「AI 自动从对话中提取」**
71
+
72
+ **业务需求:**
73
+ - AI 在对话中记录:「今天中午吃了番茄牛腩和米饭」
74
+ - 用户确认后自动存档
75
+ - 生成结构化数据(JSON)+ 可读文档(Markdown)
76
+ - 对历史记录支持查询
77
+
78
+ **技术选型(修正后):**
79
+
80
+ **1. 数据提取方式(关键设计)**
81
+ MiniCPM-o 4.5 **没有 toolcall 功能**,所以不能直接调用 Python 函数。
82
+ 改为 **「标记格式输出 + 正则解析」** 方案:
83
+ - 在系统提示词中告诉模型:当你需要记录饮食时,请输出以下格式——
84
+ ```
85
+ [DIET_RECORD]
86
+ 日期: 2026-06-07
87
+ 早餐: 全麦面包+鸡蛋+牛奶
88
+ 午餐: 清蒸鱼+糙米饭+炒时蔬
89
+ 晚餐:
90
+ 加餐: 酸奶+坚果
91
+ 心情: 挺好
92
+ 备注:
93
+ [/DIET_RECORD]
94
+ ```
95
+ - 后端 Python 用正则表达式解析 `[DIET_RECORD]...[/DIET_RECORD]` 之间的内容
96
+ - 解析后存入 JSON + 生成 Markdown
97
+
98
+ **2. Markdown 模板引擎是什么?**
99
+ 就是当前 `diet_logger.py` 中的 `_generate_markdown()` 方法——**用 Python f-string 拼接 Markdown 文本**。不是什么复杂东西,就是一个字符串模板函数。保留不变,只是数据来源从「用户填表单」变为「AI 对话中提取」。
100
+
101
+ **3. 为什么不需要向量检索?**
102
+ 你直觉正确——**确实冗余了**。之前我提出向量检索(ChromaDB)是为了支持语义搜索「上周三吃了什么?」,但实际上直接用 JSON 按日期过滤就能实现。去掉此方案,保持简单。
103
+
104
+ **数据流向:**
105
+ ```
106
+ MiniCPM-o 回复 (含 [DIET_RECORD] 标记)
107
+ ��� Python 正则解析
108
+ → 存储到 data/diet_logs.json (结构化)
109
+ → 同时生成 data/logs/饮食日志_YYYY-MM-DD.md (可读)
110
+ ```
111
+
112
+ > **✅ 已根据你的批注修正:去掉 toolcall 假设,改用标记格式解析;去掉向量检索;详细解释了 Markdown 模板引擎。**
113
+
114
+ ---
115
+
116
+ ### 模块 4:📊 营养报告(基于中国官方标准)
117
+
118
+ **核心变化:从「凭记忆编数据库」变为「查中国官方营养标准」**
119
+
120
+ **业务需求:**
121
+ - 基于中国官方营养标准做分析
122
+ - 输出可视化报告 + 改善建议
123
+ - 模板预先设计好,美观专业
124
+
125
+ **需要你下载的文件清单:**
126
+
127
+ 请搜索并下载以下文件,放到 `data/nutrition/raw/` 目录下:
128
+
129
+ | # | 文件名/关键词 | 发布机构 | 建议搜索方式 |
130
+ |---|-------------|---------|------------|
131
+ | 1 | **中国食物成分表(标准版)** 第6版 | 中国疾控中心营养与健康所 | 搜索 "中国食物成分表 标准版 第6版 Excel" 或 PDF |
132
+ | 2 | **中国居民膳食营养素参考摄入量(DRIs 2023)** 摘要 | 中国营养学会 | 搜索 "DRIs 2023 中国居民膳食营养素参考摄入量" |
133
+ | 3 | **中国孕期妇女膳食指南(2022)** | 中国营养学会 | 搜索 "孕期妇女膳食指南 2022 中国营养学会" |
134
+ | 4 | **妊娠期妇女体重增长推荐值(WS/T 801-2022)** | 国家卫健委 | 搜索 "WS/T 801-2022 妊娠期妇女体重增长推荐值" |
135
+
136
+ 如果你有些文件找不到,给我说,我协助搜索。
137
+
138
+ **如果没有下载到正式文件怎么办:**
139
+ 我已编写了一份基于 DRIs 公开数据的营养数据库作为 baseline(当前 `DEFAULT_NUTRITION_DB`),可以先用它做原型开发,等你找到正式文件后替换即可。
140
+
141
+ > **✅ 已根据你的批注修正:列出具体文件清单,你下载后我整理成 JSON。**
142
+
143
+ ---
144
+
145
+ ## 三、交互流程设计
146
+
147
+ ### 核心设计原则
148
+
149
+ 你批注得对:**不需要穷举所有情况,而是设计好 MiniCPM-o 的系统提示词 + 当前模式状态。**
150
+
151
+ ### 系统提示词架构
152
+
153
+ ```
154
+ 你叫 PregoPal,是一个孕期陪护 AI 助手。
155
+ 你的任务:
156
+ 1. 识别说话人身份(通过声音判断是谁在说话)
157
+ 2. 根据对话理解用户需求(记录饮食、推荐菜品、闲聊陪伴)
158
+ 3. 当用户有明确需求时,按指定格式输出记录
159
+
160
+ 当前模式:{模式名称} ← 动态注入(任务模式/闲聊模式)
161
+ 当前说话人:{身份} ← 声纹识别结果
162
+
163
+ 输出格式规则:
164
+ - 当用户描述了饮食内容,请在回复末尾附加 [DIET_RECORD]...[/DIET_RECORD]
165
+ - 当用户要求推荐菜品,直接推荐并给出理由
166
+ - 普通对话保持自然、温馨、关心的语气
167
+ ```
168
+
169
+ ### 交互流程
170
+
171
+ ```
172
+ 用户打开摄像头+麦克风
173
+
174
+
175
+ AI 根据系统提示词主动问候
176
+
177
+
178
+ 用户说话 → MiniCPM-o 全双工处理
179
+ ├→ 声纹识别 → 更新「当前说话人」
180
+ ├→ 理解语义 → 更新「当前模式」
181
+ └→ 生成回复 → 含 [DIET_RECORD] 标记(如需)
182
+
183
+
184
+ 后端解析回复:
185
+ ├→ 检测到 [DIET_RECORD] → 提取数据 → 存 JSON + Markdown
186
+ ├→ 检测到推荐请求 → 显示推荐结果到 UI
187
+ └→ 其他 → 正常对话
188
+ ```
189
+
190
+ ### 双模式设计
191
+
192
+ | 模式 | 判定方式 | 行为 |
193
+ |------|---------|------|
194
+ | 🎯 **任务模式** | 用户提到「记录」「推荐」「分析」「报告」等关键词 | 执行对应业务逻辑 + 标记输出 |
195
+ | 💬 **闲聊模式** | 用户开启日常对话 | 自然陪伴,不做结构化记录 |
196
+
197
+ > **✅ 已根据你的批注修正:聚焦系统提示词设计 + 模式管理,不穷举情况。**
198
+
199
+ ---
200
+
201
+ ## 四、技术栈一览
202
+
203
+ | 层级 | 技术 | 用途 |
204
+ |------|------|------|
205
+ | 核心模型 | **MiniCPM-o 4.5**(统一多模态模型) | 全双工推理:视觉+语音+文本+语音输出 |
206
+ | 前端框架 | **Gradio** | Web 界面,实时对话交互 |
207
+ | 声纹识别 | **Whisper-medium encoder embedding**(模型内置) | 说话人身份识别 |
208
+ | 频谱方案 | **NumPy + SoundFile**(备选 fallback) | 声纹识别兜底 |
209
+ | 数据存储 | **JSON + Markdown**(结构化可扩展) | 饮食记录 + 报告存档 |
210
+ | 营养标准 | **中国官方 DRIs / 食物成分表**(你下载后我整理) | 营养分析基准 |
211
+ | 部署平台 | **Modal**(你有黑客松额度,后续配置) | 云端 GPU 部署 |
212
+
213
+ ### 数据存储 Schema 设计(可扩展)
214
+
215
+ **JSON 格式(diet_logs.json):**
216
+ ```json
217
+ {
218
+ "schema_version": "1.0",
219
+ "records": [
220
+ {
221
+ "id": "a1b2c3d4",
222
+ "date": "2026-06-07",
223
+ "speaker": {"name": "小明", "relation": "丈夫"},
224
+ "meals": {"早餐": "...", "午餐": "...", "晚餐": "", "加餐": "..."},
225
+ "mood": "挺好",
226
+ "notes": "",
227
+ "extensions": {},
228
+ "created_at": "2026-06-07T10:30:00"
229
+ }
230
+ ]
231
+ }
232
+ ```
233
+
234
+ - `extensions: {}` 保留给未来扩展(如血糖、体重、胎动等)
235
+ - `schema_version` 支持后续数据结构升级
236
+
237
+ > **✅ 已根据你的批注修正:去掉「全家桶」表述,添加数据 schema 设计说明。**
238
+
239
+ ---
240
+
241
+ ## 五、模块拆分方案(最终版)
242
+
243
+ ```
244
+ PregoPal/
245
+ ├── app.py # 主入口:启动 Gradio + 加载模型
246
+ ├── config.py # 路径 / 枚举 / 模型参数 / 系统提示词模板
247
+ ├── core/ # AI 核心层
248
+ │ ├── __init__.py
249
+ │ ├── model_loader.py # 加载 MiniCPM-o 4.5 模型
250
+ │ ├── voice_processor.py # Whisper ASR + 声纹 embedding 提取
251
+ │ ├── vision_processor.py # SigLip2 视觉处理
252
+ │ └── conversation_manager.py # 对话管理 + 系统提示词构建 + [DIET_RECORD] 解析
253
+ ├── modules/ # 业务逻辑层
254
+ │ ├── __init__.py
255
+ │ ├── voiceprint.py # 声纹注册/识别(调用 core 或 fallback 频谱方案)
256
+ │ ├── meal_recommender.py # 菜品推荐(调用 MiniCPM-o 或兜底规则)
257
+ │ ├── diet_logger.py # 饮食记录 + Markdown 生成(JSON 双写)
258
+ │ └── nutrition_analyzer.py # 营养分析(基于官方标准)+ matplotlib 可视化
259
+ ├── ui/ # 表现层
260
+ │ ├── __init__.py
261
+ │ └── app_builder.py # Gradio 界面 + 事件绑定
262
+ ├── data/ # 数据存储
263
+ │ ├── nutrition/
264
+ │ │ └── raw/ # ← 你下载的营养标准原始文件放这里
265
+ │ ├── voices/ # 声纹数据
266
+ │ ├── logs/ # Markdown 饮食日志
267
+ │ └── reports/ # 导出报告
268
+ ├── docs/
269
+ │ ├── 项目理解_技术架构.md # 本文档
270
+ │ └── 开发日志.md # 记录错误、调试经验、模型调用技巧
271
+ ├── requirements.txt # 每次运行成功后根据环境版本锁定
272
+ └── README.md
273
+ ```
274
+
275
+ > **✅ 已确认。**
276
+
277
+ ---
278
+
279
+ ## 六、决策记录(你已回答)
280
+
281
+ | # | 问题 | 你的决策 |
282
+ |---|------|---------|
283
+ | 1 | 声纹识别方案? | **Whisper-medium embedding**(优先),频谱相似度(备选) |
284
+ | 2 | 部署方式? | **Modal**(你有黑客松额度),后续我帮你配置 |
285
+ | 3 | 营养标准谁来整理? | **你下载文件**到 `data/nutrition/raw/`,我整理成 JSON |
286
+ | 4 | 拆分时机? | **处理好上述问题后**立即拆分 |
287
+
288
+ ---
289
+
290
+ ## 七、后续行动计划(按顺序)
291
+
292
+ - [ ] **你做的事:** 下载营养标准文件到 `data/nutrition/raw/`
293
+ - [ ] **我做的事:** 更新开发日志 → 列出需要下载的营养文件清单 → 等文件就绪后执行文件拆分
294
+ - [ ] **然后:** 研究 Whisper-medium embedding 提取方案
295
+ - [ ] **然后:** 配置 Modal 部署
296
+ - [ ] **然后:** 逐步替换规则引擎为 AI 驱动
297
+
298
+ ---
299
+
300
+ *文档生成时间:2026-06-07 | v3 - 已整合所有批注*
modules/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 业务逻辑层
3
+ ======================
4
+ 包含所有业务模块:声纹识别、菜品推荐、饮食记录、营养分析。
5
+ """
modules/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (326 Bytes). View file
 
modules/__pycache__/diet_logger.cpython-311.pyc ADDED
Binary file (8.61 kB). View file
 
modules/__pycache__/meal_recommender.cpython-311.pyc ADDED
Binary file (5.01 kB). View file
 
modules/__pycache__/nutrition_analyzer.cpython-311.pyc ADDED
Binary file (20.2 kB). View file
 
modules/__pycache__/voiceprint.cpython-311.pyc ADDED
Binary file (9.86 kB). View file
 
modules/diet_logger.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 饮食记录模块
3
+ ========================
4
+ 记录孕妇的饮食习惯,同时保存为 JSON(结构化)+ Markdown(可读)。
5
+
6
+ 数据流向:
7
+ MiniCPM-o 回复 (含 [DIET_RECORD] 标记) → 正则解析 → JSON + Markdown
8
+
9
+ 当前:手动表单输入
10
+ 后续:AI 对话中自动提取 [DIET_RECORD] 标记
11
+ """
12
+
13
+ import json
14
+ import datetime
15
+ import hashlib
16
+ import re
17
+ from pathlib import Path
18
+ from config import DIET_LOG_FILE, LOGS_DIR, DIET_LOG_SCHEMA_VERSION
19
+
20
+
21
+ class DietLogger:
22
+ """记录孕妇的饮食习惯并保存为 Markdown"""
23
+
24
+ def __init__(self):
25
+ self.log_file = DIET_LOG_FILE
26
+ self.logs = self._load_logs()
27
+
28
+ def _load_logs(self):
29
+ """加载饮食记录"""
30
+ if self.log_file.exists():
31
+ with open(self.log_file, 'r', encoding='utf-8') as f:
32
+ return json.load(f)
33
+ return {"schema_version": DIET_LOG_SCHEMA_VERSION, "records": []}
34
+
35
+ def _save_logs(self):
36
+ """保存饮食记录"""
37
+ with open(self.log_file, 'w', encoding='utf-8') as f:
38
+ json.dump(self.logs, f, ensure_ascii=False, indent=2)
39
+
40
+ def add_record(self, member_name: str, member_relation: str, date: str,
41
+ meals: dict, mood: str = "", notes: str = ""):
42
+ """添加一条饮食记录"""
43
+ record = {
44
+ "id": hashlib.md5(f"{date}_{member_name}_{datetime.datetime.now()}".encode()).hexdigest()[:8],
45
+ "member_name": member_name,
46
+ "member_relation": member_relation,
47
+ "date": date,
48
+ "meals": meals,
49
+ "mood": mood,
50
+ "notes": notes,
51
+ "extensions": {}, # 预留扩展字段
52
+ "created_at": datetime.datetime.now().isoformat()
53
+ }
54
+ self.logs["records"].append(record)
55
+ self._save_logs()
56
+
57
+ # 同时生成 Markdown 文件
58
+ md_path = self._generate_markdown(record)
59
+ return record, md_path
60
+
61
+ def _generate_markdown(self, record: dict) -> Path:
62
+ """生成 Markdown 格式的饮食日志"""
63
+ date_str = record["date"]
64
+ md_filename = LOGS_DIR / f"饮食日志_{date_str}.md"
65
+
66
+ content = f"""# 🥗 孕期饮食日志
67
+
68
+ ## 📋 基本信息
69
+ - **日期**: {date_str}
70
+ - **记录人**: {record['member_relation']} - {record['member_name']}
71
+ - **记录时间**: {record['created_at'][:19]}
72
+
73
+ ## 🍽️ 今日饮食记录
74
+
75
+ """
76
+ for meal_time, food in record["meals"].items():
77
+ content += f"### {meal_time}\n- {food}\n\n"
78
+
79
+ if record["mood"]:
80
+ content += f"## 😊 今日心情\n{record['mood']}\n\n"
81
+
82
+ if record["notes"]:
83
+ content += f"## 📝 备注\n{record['notes']}\n\n"
84
+
85
+ content += """---
86
+ *由 PregoPal 自动生成*
87
+ """
88
+
89
+ with open(md_filename, 'w', encoding='utf-8') as f:
90
+ f.write(content)
91
+
92
+ return md_filename
93
+
94
+ def get_recent_records(self, days: int = 7) -> list:
95
+ """获取最近几天的记录"""
96
+ today = datetime.date.today()
97
+ cutoff = today - datetime.timedelta(days=days)
98
+ recent = []
99
+ for r in self.logs["records"]:
100
+ try:
101
+ record_date = datetime.date.fromisoformat(r["date"])
102
+ if record_date >= cutoff:
103
+ recent.append(r)
104
+ except ValueError:
105
+ continue
106
+ return sorted(recent, key=lambda x: x["date"], reverse=True)
107
+
108
+ def get_all_markdown_files(self) -> list:
109
+ """获取所有 Markdown 日志文件"""
110
+ return sorted(LOGS_DIR.glob("*.md"), reverse=True)
111
+
112
+ # ============================================================
113
+ # [DIET_RECORD] 标记解析(后续 AI 版本使用)
114
+ # ============================================================
115
+ @staticmethod
116
+ def parse_diet_record(text: str) -> dict | None:
117
+ """
118
+ 从 AI 回复中解析 [DIET_RECORD] 标记
119
+
120
+ Args:
121
+ text: AI 模型的回复文本
122
+
123
+ Returns:
124
+ 解析出的饮食记录字典,或 None(未找到标记)
125
+
126
+ 示例输入:
127
+ [DIET_RECORD]
128
+ 日期: 2026-06-07
129
+ 早餐: 全麦面包+鸡蛋+牛奶
130
+ 午餐: 清蒸鱼+糙米饭+炒时蔬
131
+ 晚餐:
132
+ 加餐: 酸奶+坚果
133
+ 心情: 挺好
134
+ 备注:
135
+ [/DIET_RECORD]
136
+ """
137
+ pattern = r'\[DIET_RECORD\](.*?)\[/DIET_RECORD\]'
138
+ match = re.search(pattern, text, re.DOTALL)
139
+ if not match:
140
+ return None
141
+
142
+ content = match.group(1).strip()
143
+ record = {"meals": {}, "mood": "", "notes": ""}
144
+
145
+ for line in content.split('\n'):
146
+ line = line.strip()
147
+ if not line:
148
+ continue
149
+
150
+ if ':' in line:
151
+ key, value = line.split(':', 1)
152
+ key = key.strip()
153
+ value = value.strip()
154
+
155
+ if key == '日期':
156
+ record['date'] = value
157
+ elif key == '心情':
158
+ record['mood'] = value
159
+ elif key == '备注':
160
+ record['notes'] = value
161
+ elif key in ['早餐', '午餐', '晚餐', '加餐']:
162
+ if value: # 只记录非空值
163
+ record['meals'][key] = value
164
+
165
+ return record if record['meals'] else None
modules/meal_recommender.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 菜品推荐模块
3
+ ========================
4
+ 根据孕妇需求推荐今日菜品。
5
+
6
+ 当前:规则模板随机推荐(轻量 baseline)
7
+ 后续:MiniCPM-o 4.5 AI 对话推荐
8
+ """
9
+
10
+ import datetime
11
+ import random
12
+ from config import MEAL_TEMPLATES, TRIMESTER_ADJUSTMENTS, TRIMESTER_TIPS
13
+
14
+
15
+ class MealRecommender:
16
+ """根据孕妇需求推荐今日菜品"""
17
+
18
+ def __init__(self):
19
+ self.templates = MEAL_TEMPLATES
20
+
21
+ def get_recommendation(self, preference: str = "", trimester: str = "孕中期", restrictions: str = ""):
22
+ """
23
+ 根据孕妇偏好和孕期阶段推荐菜品
24
+
25
+ Args:
26
+ preference: 孕妇的口味偏好或要求
27
+ trimester: 孕期阶段(孕早期/孕中期/孕晚期)
28
+ restrictions: 饮食禁忌
29
+ """
30
+ # 根据孕期阶段调整推荐
31
+ adj = TRIMESTER_ADJUSTMENTS.get(trimester, TRIMESTER_ADJUSTMENTS["孕中期"])
32
+
33
+ # 根据偏好选择菜品
34
+ breakfast = random.choice(self.templates["早餐"])
35
+ lunch = random.choice(self.templates["午餐"])
36
+ dinner = random.choice(self.templates["晚餐"])
37
+ snack = random.choice(self.templates["加餐"])
38
+
39
+ # 如果有偏好,尝试匹配
40
+ if preference:
41
+ all_foods = []
42
+ for meal_list in self.templates.values():
43
+ all_foods.extend(meal_list)
44
+ matched = [f for f in all_foods if any(kw in f for kw in preference.split())]
45
+ if matched:
46
+ pass # 简化处理,后续 AI 版本会真正理解偏好
47
+
48
+ result = {
49
+ "date": datetime.date.today().isoformat(),
50
+ "trimester": trimester,
51
+ "preference": preference,
52
+ "focus": adj["focus"],
53
+ "meals": {
54
+ "早餐": breakfast,
55
+ "午餐": lunch,
56
+ "晚餐": dinner,
57
+ "加餐": snack
58
+ },
59
+ "tips": self._generate_tips(trimester, preference)
60
+ }
61
+
62
+ return result
63
+
64
+ def _generate_tips(self, trimester, preference):
65
+ """生成饮食建议"""
66
+ tips = []
67
+ trimester_tips = TRIMESTER_TIPS
68
+ tips.append(trimester_tips.get(trimester, ""))
69
+ tips.append("💡 建议每天饮水1.5-2L,适量运动如散步30分钟。")
70
+ tips.append("💡 保持心情愉快,避免过度焦虑。")
71
+ return tips
72
+
73
+ def format_meal_plan(self, recommendation: dict) -> str:
74
+ """将推荐结果格式化为可读文本"""
75
+ lines = [
76
+ f"📅 {recommendation['date']} 孕期食谱推荐",
77
+ f"🤰 阶段: {recommendation['trimester']}",
78
+ f"🎯 重点: {recommendation['focus']}",
79
+ "",
80
+ "🍳 今日食谱:",
81
+ ]
82
+ for meal_time, food in recommendation["meals"].items():
83
+ lines.append(f" {meal_time}: {food}")
84
+
85
+ lines.append("")
86
+ for tip in recommendation["tips"]:
87
+ lines.append(tip)
88
+
89
+ return "\n".join(lines)
modules/nutrition_analyzer.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 营养分析与报告模块
3
+ ==============================
4
+ 分析饮食数据,基于中国官方营养标准输出可视化报告。
5
+
6
+ 当前:基于内置 DEFAULT_NUTRITION_DB(通用知识估算)
7
+ 后续:替换为中国官方标准(DRIs 2023 / 中国食物成分表)
8
+ """
9
+
10
+ import json
11
+ import datetime
12
+ import numpy as np
13
+ import matplotlib
14
+ matplotlib.use('Agg')
15
+ import matplotlib.pyplot as plt
16
+ from pathlib import Path
17
+ from config import NUTRITION_DB_FILE, DEFAULT_NUTRITION_DB, REPORTS_DIR
18
+
19
+
20
+ class NutritionAnalyzer:
21
+ """分析饮食数据,输出可视化营养报告"""
22
+
23
+ def __init__(self):
24
+ self.nutrition_db = self._load_nutrition_db()
25
+
26
+ def _load_nutrition_db(self):
27
+ """加载营养数据库"""
28
+ if NUTRITION_DB_FILE.exists():
29
+ with open(NUTRITION_DB_FILE, 'r', encoding='utf-8') as f:
30
+ return json.load(f)
31
+ # 使用默认数据库并保存
32
+ self._save_nutrition_db(DEFAULT_NUTRITION_DB)
33
+ return DEFAULT_NUTRITION_DB
34
+
35
+ def _save_nutrition_db(self, db):
36
+ with open(NUTRITION_DB_FILE, 'w', encoding='utf-8') as f:
37
+ json.dump(db, f, ensure_ascii=False, indent=2)
38
+
39
+ def analyze_diet(self, records: list) -> dict:
40
+ """
41
+ 分析一段时间内的饮食记录
42
+
43
+ Args:
44
+ records: 饮食记录列表
45
+ Returns:
46
+ 分析结果字典
47
+ """
48
+ if not records:
49
+ return {"error": "暂无饮食记录", "score": 0}
50
+
51
+ # 统计各餐次频率
52
+ meal_counts = {"早餐": 0, "午餐": 0, "晚餐": 0, "加餐": 0}
53
+ food_items = []
54
+ total_days = len(set(r["date"] for r in records))
55
+
56
+ for r in records:
57
+ for meal_time, food in r.get("meals", {}).items():
58
+ if meal_time in meal_counts:
59
+ meal_counts[meal_time] += 1
60
+ # 提取食物关键词
61
+ food_items.extend(self._extract_foods(food))
62
+
63
+ # 计算营养覆盖情况
64
+ nutrition_coverage = self._calculate_nutrition_coverage(food_items)
65
+
66
+ # 计算饮食多样性评分
67
+ diversity_score = self._calculate_diversity_score(meal_counts, total_days)
68
+
69
+ # 生成建议
70
+ suggestions = self._generate_suggestions(nutrition_coverage, diversity_score)
71
+
72
+ return {
73
+ "total_days": total_days,
74
+ "total_records": len(records),
75
+ "meal_counts": meal_counts,
76
+ "food_items": list(set(food_items)),
77
+ "nutrition_coverage": nutrition_coverage,
78
+ "diversity_score": diversity_score,
79
+ "suggestions": suggestions
80
+ }
81
+
82
+ def _extract_foods(self, food_str: str) -> list:
83
+ """从餐食描述中提取食物名称"""
84
+ # 简单分词提取
85
+ separators = ['+', '、', ',', ',', '/', ' ']
86
+ foods = [food_str]
87
+ for sep in separators:
88
+ expanded = []
89
+ for f in foods:
90
+ expanded.extend(f.split(sep))
91
+ foods = expanded
92
+ return [f.strip() for f in foods if f.strip()]
93
+
94
+ def _calculate_nutrition_coverage(self, food_items: list) -> dict:
95
+ """计算营养覆盖情况"""
96
+ coverage = {}
97
+ food_text = " ".join(food_items)
98
+
99
+ for nutrient, info in self.nutrition_db.items():
100
+ # 检查食物列表中是否包含推荐食物
101
+ matched_foods = [f for f in info["foods"] if f in food_text]
102
+ coverage[nutrient] = {
103
+ "matched_foods": matched_foods,
104
+ "covered": len(matched_foods) > 0,
105
+ "recommended_foods": info["foods"],
106
+ "benefit": info["benefit"],
107
+ "daily_recommend": info.get("daily_recommend_mg") or info.get("daily_recommend_g") or info.get("daily_recommend_mcg", ""),
108
+ "unit": "mg" if "daily_recommend_mg" in info else ("g" if "daily_recommend_g" in info else "mcg")
109
+ }
110
+
111
+ return coverage
112
+
113
+ def _calculate_diversity_score(self, meal_counts: dict, total_days: int) -> dict:
114
+ """计算饮食多样性评分"""
115
+ if total_days == 0:
116
+ return {"score": 0, "details": "暂无数据"}
117
+
118
+ max_possible = total_days * len(meal_counts)
119
+ actual = sum(meal_counts.values())
120
+ score = min(100, int((actual / max_possible) * 100))
121
+
122
+ details = []
123
+ for meal, count in meal_counts.items():
124
+ rate = count / total_days if total_days > 0 else 0
125
+ status = "✅" if rate >= 0.7 else ("⚠️" if rate >= 0.4 else "❌")
126
+ details.append(f"{status} {meal}: {count}/{total_days}天 ({rate:.0%})")
127
+
128
+ return {"score": score, "details": details}
129
+
130
+ def _generate_suggestions(self, nutrition_coverage: dict, diversity: dict) -> list:
131
+ """生成营养建议"""
132
+ suggestions = []
133
+
134
+ # 检查未覆盖的营养素
135
+ missing = [n for n, info in nutrition_coverage.items() if not info["covered"]]
136
+ if missing:
137
+ suggestions.append(f"⚠️ 以下营养素摄入不足: {', '.join(missing[:5])}")
138
+ for n in missing[:3]:
139
+ info = nutrition_coverage[n]
140
+ suggestions.append(f" 💡 建议补充 {n}({info['benefit']}):可多吃 {', '.join(info['recommended_foods'][:3])}")
141
+
142
+ if diversity["score"] < 60:
143
+ suggestions.append("⚠️ 饮食多样性不足,建议增加食物种类")
144
+ elif diversity["score"] >= 80:
145
+ suggestions.append("✅ 饮食多样性良好,继续保持!")
146
+
147
+ suggestions.append("💪 建议每天摄入12种以上食物,每周25种以上")
148
+ suggestions.append("🥤 保证每天1.5-2L饮水")
149
+
150
+ return suggestions
151
+
152
+ def generate_report_chart(self, analysis: dict) -> plt.Figure:
153
+ """生成营养报告图表"""
154
+ if "error" in analysis:
155
+ fig, ax = plt.subplots(figsize=(8, 4))
156
+ ax.text(0.5, 0.5, analysis["error"], ha='center', va='center', fontsize=14)
157
+ return fig
158
+
159
+ fig = plt.figure(figsize=(14, 10))
160
+
161
+ # 1. 营养覆盖雷达图
162
+ ax1 = fig.add_subplot(2, 2, 1, polar=True)
163
+ nutrients = list(analysis["nutrition_coverage"].keys())[:8]
164
+ coverage_values = [1 if analysis["nutrition_coverage"][n]["covered"] else 0 for n in nutrients]
165
+
166
+ angles = np.linspace(0, 2 * np.pi, len(nutrients), endpoint=False).tolist()
167
+ coverage_values += coverage_values[:1]
168
+ angles += angles[:1]
169
+
170
+ ax1.plot(angles, coverage_values, 'o-', linewidth=2, color='#FF6B9D')
171
+ ax1.fill(angles, coverage_values, alpha=0.25, color='#FF6B9D')
172
+ ax1.set_xticks(angles[:-1])
173
+ ax1.set_xticklabels(nutrients, fontsize=9)
174
+ ax1.set_ylim(0, 1.2)
175
+ ax1.set_title('🥗 营养覆盖雷达图', pad=20, fontsize=13, fontweight='bold')
176
+
177
+ # 2. 各餐次频率柱状图
178
+ ax2 = fig.add_subplot(2, 2, 2)
179
+ meals = list(analysis["meal_counts"].keys())
180
+ counts = list(analysis["meal_counts"].values())
181
+ colors = ['#FF9AA2', '#FFB7B2', '#FFDAC1', '#E2F0CB']
182
+ bars = ax2.bar(meals, counts, color=colors, edgecolor='white', linewidth=1.5)
183
+ ax2.set_title('🍽️ 各餐次记录频率', fontsize=13, fontweight='bold')
184
+ ax2.set_ylabel('记录次数')
185
+ for bar, count in zip(bars, counts):
186
+ ax2.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.1,
187
+ str(count), ha='center', va='bottom', fontsize=11)
188
+
189
+ # 3. 饮食多样性评分仪表盘
190
+ ax3 = fig.add_subplot(2, 2, 3)
191
+ score = analysis["diversity_score"]["score"]
192
+ ax3.pie([score, 100 - score], startangle=90,
193
+ colors=['#FF6B9D', '#F0F0F0'],
194
+ wedgeprops={'width': 0.3, 'edgecolor': 'white'})
195
+ ax3.text(0, 0, f'{score}', ha='center', va='center', fontsize=28, fontweight='bold')
196
+ ax3.text(0, -0.15, '多样性评分', ha='center', va='center', fontsize=10, color='gray')
197
+ ax3.set_title('📊 饮食多样性评分', fontsize=13, fontweight='bold')
198
+
199
+ # 4. 建议文本
200
+ ax4 = fig.add_subplot(2, 2, 4)
201
+ ax4.axis('off')
202
+ suggestions = analysis.get("suggestions", [])
203
+ if suggestions:
204
+ text = "📋 营养建议\n" + "\n".join(f"• {s}" for s in suggestions[:6])
205
+ else:
206
+ text = "✅ 营养状况良好!"
207
+ ax4.text(0.05, 0.95, text, transform=ax4.transAxes,
208
+ fontsize=10, verticalalignment='top',
209
+ fontfamily='sans-serif',
210
+ bbox=dict(boxstyle='round,pad=0.5', facecolor='#FFF5F5', edgecolor='#FF6B9D'))
211
+
212
+ plt.tight_layout()
213
+ return fig
214
+
215
+ def generate_report_text(self, analysis: dict) -> str:
216
+ """生成文本格式的营养报告"""
217
+ if "error" in analysis:
218
+ return f"⚠️ {analysis['error']}"
219
+
220
+ lines = [
221
+ "=" * 50,
222
+ "📋 孕期营养分析报告",
223
+ "=" * 50,
224
+ f"📅 分析周期: {analysis['total_days']} 天",
225
+ f"📝 记录总数: {analysis['total_records']} 条",
226
+ "",
227
+ "📊 饮食多样性评分: {}/100".format(analysis['diversity_score']['score']),
228
+ ]
229
+
230
+ lines.append("")
231
+ lines.append("📈 各餐次记录情况:")
232
+ for detail in analysis['diversity_score']['details']:
233
+ lines.append(f" {detail}")
234
+
235
+ lines.append("")
236
+ lines.append("🥗 营养覆盖情况:")
237
+ for nutrient, info in analysis['nutrition_coverage'].items():
238
+ status = "✅" if info['covered'] else "❌"
239
+ matched = ", ".join(info['matched_foods']) if info['matched_foods'] else "无"
240
+ lines.append(f" {status} {nutrient}: 匹配食物 [{matched}]")
241
+ lines.append(f" 💡 {info['benefit']}")
242
+
243
+ lines.append("")
244
+ lines.append("💡 改善建议:")
245
+ for s in analysis.get("suggestions", []):
246
+ lines.append(f" {s}")
247
+
248
+ lines.append("")
249
+ lines.append("=" * 50)
250
+ lines.append("由 PregoPal 自动生成")
251
+
252
+ return "\n".join(lines)
253
+
254
+ def export_report_markdown(self, analysis: dict, filename: str = None) -> Path:
255
+ """导出营养报告为 Markdown 文件"""
256
+ if filename is None:
257
+ filename = f"营养报告_{datetime.date.today().isoformat()}.md"
258
+
259
+ md_path = REPORTS_DIR / filename
260
+
261
+ content = f"""# 📋 孕期营养分析报告
262
+
263
+ ## 📅 基本信息
264
+ - **分析周期**: {analysis.get('total_days', 0)} 天
265
+ - **记录总数**: {analysis.get('total_records', 0)} 条
266
+ - **生成时间**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
267
+
268
+ ## 📊 饮食多样性评分
269
+ **评分: {analysis.get('diversity_score', {}).get('score', 0)}/100**
270
+
271
+ | 餐次 | 记录天数 | 覆盖率 |
272
+ |------|---------|--------|
273
+ """
274
+ for detail in analysis.get('diversity_score', {}).get('details', []):
275
+ parts = detail.split(': ', 1)
276
+ if len(parts) == 2:
277
+ content += f"| {parts[0]} | {parts[1]} |\n"
278
+
279
+ content += """
280
+ ## 🥗 营养覆盖分析
281
+
282
+ | 营养素 | 状态 | 匹配食物 | 功效 |
283
+ |--------|------|---------|------|
284
+ """
285
+ for nutrient, info in analysis.get('nutrition_coverage', {}).items():
286
+ status = "✅" if info['covered'] else "❌"
287
+ matched = ", ".join(info['matched_foods']) if info['matched_foods'] else "-"
288
+ content += f"| {nutrient} | {status} | {matched} | {info['benefit']} |\n"
289
+
290
+ content += """
291
+ ## 💡 改善建议
292
+
293
+ """
294
+ for s in analysis.get('suggestions', []):
295
+ content += f"- {s}\n"
296
+
297
+ content += """
298
+ ---
299
+ *由 PregoPal 自动生成 | 仅供参考,不构成医疗建议*
300
+ """
301
+
302
+ with open(md_path, 'w', encoding='utf-8') as f:
303
+ f.write(content)
304
+
305
+ return md_path
modules/voiceprint.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 声纹识别模块
3
+ ========================
4
+ 家庭成员声纹注册、识别、管理。
5
+
6
+ 当前方案:频谱特征相似度(轻量 baseline)
7
+ 后续升级:Whisper-medium encoder embedding(MiniCPM-o 内置)
8
+ """
9
+
10
+ import json
11
+ import numpy as np
12
+ import datetime
13
+ import hashlib
14
+ from pathlib import Path
15
+ from config import VOICE_DIR, FAMILY_FILE, VOICEPRINT_SIMILARITY_THRESHOLD
16
+
17
+
18
+ class VoiceprintManager:
19
+ """声纹识别与家庭成员管理"""
20
+
21
+ def __init__(self):
22
+ self.family_file = FAMILY_FILE
23
+ self.family_data = self._load_family()
24
+
25
+ def _load_family(self):
26
+ """加载家庭成员数据"""
27
+ if self.family_file.exists():
28
+ with open(self.family_file, 'r', encoding='utf-8') as f:
29
+ return json.load(f)
30
+ return {"members": [], "voiceprints": {}}
31
+
32
+ def _save_family(self):
33
+ """保存家庭成员数据"""
34
+ with open(self.family_file, 'w', encoding='utf-8') as f:
35
+ json.dump(self.family_data, f, ensure_ascii=False, indent=2)
36
+
37
+ def register_member(self, name: str, relation: str, audio_path_input):
38
+ """注册家庭成员声纹"""
39
+ if audio_path_input is None:
40
+ return None, "请先录制或上传语音"
41
+
42
+ # 生成声纹ID
43
+ member_id = hashlib.md5(f"{name}_{datetime.datetime.now()}".encode()).hexdigest()[:8]
44
+
45
+ # 保存音频文件到数据目录
46
+ audio_path = VOICE_DIR / f"{member_id}.wav"
47
+ try:
48
+ import shutil
49
+ shutil.copy2(audio_path_input, audio_path)
50
+ except Exception as e:
51
+ return None, f"保存音频失败: {str(e)}"
52
+
53
+ # 提取简单的声纹特征(使用音频的频谱特征作为简化方案)
54
+ features = self._extract_features_from_file(audio_path)
55
+
56
+ # 保存成员信息
57
+ member_info = {
58
+ "id": member_id,
59
+ "name": name,
60
+ "relation": relation,
61
+ "registered_at": datetime.datetime.now().isoformat(),
62
+ "audio_path": str(audio_path),
63
+ "features": features
64
+ }
65
+
66
+ self.family_data["members"].append(member_info)
67
+ self.family_data["voiceprints"][member_id] = features
68
+ self._save_family()
69
+
70
+ return member_info, f"✅ 成功注册 {relation} - {name}!"
71
+
72
+ def _extract_features_from_file(self, audio_path):
73
+ """
74
+ 从音频文件提取声纹特征
75
+ 当前:频谱统计特征(轻量 baseline)
76
+ 后续:Whisper-medium encoder embedding(更鲁棒)
77
+ """
78
+ try:
79
+ import soundfile as sf
80
+ data, samplerate = sf.read(audio_path)
81
+ if len(data.shape) > 1:
82
+ data = data.mean(axis=1)
83
+
84
+ # 提取简单特征:MFCC-like 统计量
85
+ features = {
86
+ "mean": float(np.mean(data)),
87
+ "std": float(np.std(data)),
88
+ "max": float(np.max(data)),
89
+ "min": float(np.min(data)),
90
+ "zero_crossing_rate": float(np.sum(np.abs(np.diff(np.sign(data)))) / len(data)),
91
+ "energy": float(np.sum(data ** 2) / len(data)),
92
+ "duration": float(len(data) / samplerate)
93
+ }
94
+ return features
95
+ except Exception:
96
+ # 如果无法解析音频,返回占位特征
97
+ return {
98
+ "mean": 0.0, "std": 0.0, "max": 0.0, "min": 0.0,
99
+ "zero_crossing_rate": 0.0, "energy": 0.0, "duration": 0.0
100
+ }
101
+
102
+ def identify_speaker(self, audio_path_input):
103
+ """识别说话人身份"""
104
+ if audio_path_input is None:
105
+ return None, "请先录制或上传语音"
106
+
107
+ if not self.family_data["members"]:
108
+ return None, "⚠️ 尚未注册任何家庭成员,请先注册"
109
+
110
+ features = self._extract_features_from_file(audio_path_input)
111
+
112
+ # 计算与每个注册成员的相似度
113
+ best_match = None
114
+ best_score = -1
115
+
116
+ for member in self.family_data["members"]:
117
+ stored = member["features"]
118
+ score = self._compute_similarity(features, stored)
119
+ if score > best_score:
120
+ best_score = score
121
+ best_match = member
122
+
123
+ # 相似度阈值判断
124
+ threshold = VOICEPRINT_SIMILARITY_THRESHOLD
125
+ if best_score >= threshold:
126
+ return best_match, f"🔊 识别结果: {best_match['relation']} - {best_match['name']} (置信度: {best_score:.2f})"
127
+ else:
128
+ return None, f"🔊 未能识别说话人 (最高匹配: {best_score:.2f},需≥{threshold})"
129
+
130
+ def _compute_similarity(self, f1, f2):
131
+ """计算两个声纹特征的余弦相似度"""
132
+ keys = ["mean", "std", "max", "min", "zero_crossing_rate", "energy"]
133
+ v1 = np.array([f1.get(k, 0) for k in keys])
134
+ v2 = np.array([f2.get(k, 0) for k in keys])
135
+
136
+ # 归一化
137
+ norm1 = np.linalg.norm(v1)
138
+ norm2 = np.linalg.norm(v2)
139
+ if norm1 == 0 or norm2 == 0:
140
+ return 0
141
+
142
+ return float(np.dot(v1, v2) / (norm1 * norm2))
143
+
144
+ def get_members_list(self):
145
+ """获取家庭成员列表"""
146
+ if not self.family_data["members"]:
147
+ return "📋 暂无注册成员"
148
+ lines = ["📋 已注册家庭成员:"]
149
+ for m in self.family_data["members"]:
150
+ lines.append(f" • {m['relation']} - {m['name']} (注册于 {m['registered_at'][:10]})")
151
+ return "\n".join(lines)
152
+
153
+ def delete_member(self, member_id: str):
154
+ """删除家庭成员"""
155
+ self.family_data["members"] = [m for m in self.family_data["members"] if m["id"] != member_id]
156
+ self.family_data["voiceprints"].pop(member_id, None)
157
+ self._save_family()
158
+ return f"已删除成员 {member_id}"
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # PregoPal - 依赖清单
2
+ # 安装: pip install -r requirements.txt
3
+
4
+ gradio>=6.0.0
5
+ numpy>=1.24.0
6
+ matplotlib>=3.7.0
7
+ pandas>=2.0.0
8
+ soundfile>=0.12.0
9
+ pillow>=10.0.0
ui/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 表现层
3
+ ==================
4
+ Gradio 界面构建与事件绑定。
5
+ """
ui/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (261 Bytes). View file
 
ui/__pycache__/app_builder.cpython-311.pyc ADDED
Binary file (18.4 kB). View file
 
ui/app_builder.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - Gradio 界面构建
3
+ ============================
4
+ 所有 UI 组件和事件绑定集中在此文件。
5
+ """
6
+
7
+ import datetime
8
+ import gradio as gr
9
+
10
+ from modules.voiceprint import VoiceprintManager
11
+ from modules.meal_recommender import MealRecommender
12
+ from modules.diet_logger import DietLogger
13
+ from modules.nutrition_analyzer import NutritionAnalyzer
14
+
15
+ # ============================================================
16
+ # 全局实例(单例)
17
+ # ============================================================
18
+ voiceprint_mgr = VoiceprintManager()
19
+ meal_recommender = MealRecommender()
20
+ diet_logger = DietLogger()
21
+ nutrition_analyzer = NutritionAnalyzer()
22
+
23
+
24
+ def create_app():
25
+ """创建主应用"""
26
+
27
+ with gr.Blocks(title="PregoPal - 孕期陪护AI助手") as demo:
28
+
29
+ # ==================== 顶部导航 ====================
30
+ gr.Markdown(
31
+ """
32
+ # 🌸 PregoPal - 孕期陪护AI助手
33
+ ### 一个温馨的家庭式孕期AI伴侣
34
+ """
35
+ )
36
+
37
+ # ==================== Tab 1: 声纹识别 ====================
38
+ with gr.Tab("🔊 声纹识别"):
39
+ gr.Markdown("### 识别家庭成员,记录谁在说话")
40
+
41
+ with gr.Row():
42
+ with gr.Column(scale=1):
43
+ gr.Markdown("#### 🆕 注册新成员")
44
+ member_name = gr.Textbox(label="姓名", placeholder="例如:小明")
45
+ member_relation = gr.Dropdown(
46
+ label="身份",
47
+ choices=["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"],
48
+ value="孕妇"
49
+ )
50
+ audio_register = gr.Audio(label="录制语音", type="filepath")
51
+ register_btn = gr.Button("📝 注册声纹", variant="primary")
52
+ register_result = gr.Textbox(label="注册结果", interactive=False)
53
+
54
+ with gr.Column(scale=1):
55
+ gr.Markdown("#### 🔍 识别说话人")
56
+ audio_identify = gr.Audio(label="录制语音进行识别", type="filepath")
57
+ identify_btn = gr.Button("🔊 识别身份", variant="primary")
58
+ identify_result = gr.Textbox(label="识别结果", interactive=False)
59
+
60
+ with gr.Row():
61
+ with gr.Column():
62
+ gr.Markdown("#### 📋 家庭成员列表")
63
+ refresh_members_btn = gr.Button("🔄 刷新列表")
64
+ members_display = gr.Textbox(label="已注册成员", interactive=False)
65
+
66
+ # 事件绑定
67
+ register_btn.click(
68
+ fn=lambda name, rel, audio: voiceprint_mgr.register_member(name, rel, audio),
69
+ inputs=[member_name, member_relation, audio_register],
70
+ outputs=[gr.State(), register_result]
71
+ ).then(
72
+ fn=lambda: voiceprint_mgr.get_members_list(),
73
+ outputs=members_display
74
+ )
75
+
76
+ identify_btn.click(
77
+ fn=lambda audio: voiceprint_mgr.identify_speaker(audio),
78
+ inputs=[audio_identify],
79
+ outputs=[gr.State(), identify_result]
80
+ )
81
+
82
+ refresh_members_btn.click(
83
+ fn=lambda: voiceprint_mgr.get_members_list(),
84
+ outputs=members_display
85
+ )
86
+
87
+ # ==================== Tab 2: 今日菜品推荐 ====================
88
+ with gr.Tab("🍳 今日菜品推荐"):
89
+ gr.Markdown("### 根据孕妇的需求推荐今日菜品")
90
+
91
+ with gr.Row():
92
+ with gr.Column():
93
+ trimester = gr.Radio(
94
+ label="孕期阶段",
95
+ choices=["孕早期", "孕中期", "孕晚期"],
96
+ value="孕中期"
97
+ )
98
+ preference = gr.Textbox(
99
+ label="今天想吃什么?",
100
+ placeholder="例如:想吃清淡的、想吃鱼、不想吃油腻的...",
101
+ lines=2
102
+ )
103
+ restrictions = gr.Textbox(
104
+ label="饮食禁忌(可选)",
105
+ placeholder="例如:不能吃海鲜、不能吃辣...",
106
+ lines=1
107
+ )
108
+ recommend_btn = gr.Button("🎯 生成今日推荐", variant="primary", size="lg")
109
+
110
+ with gr.Column():
111
+ meal_result = gr.Textbox(label="📋 今日食谱推荐", lines=15)
112
+
113
+ # 事件绑定
114
+ recommend_btn.click(
115
+ fn=lambda tri, pref, rest: meal_recommender.format_meal_plan(
116
+ meal_recommender.get_recommendation(pref, tri, rest)
117
+ ),
118
+ inputs=[trimester, preference, restrictions],
119
+ outputs=meal_result
120
+ )
121
+
122
+ # ==================== Tab 3: 饮食记�� ====================
123
+ with gr.Tab("📝 饮食记录"):
124
+ gr.Markdown("### 记录孕妇的每日饮食,自动保存为Markdown日志")
125
+
126
+ with gr.Row():
127
+ with gr.Column():
128
+ gr.Markdown("#### 🍽️ 记录今日饮食")
129
+ log_date = gr.Textbox(label="日期", value=datetime.date.today().isoformat())
130
+ log_member_name = gr.Textbox(label="记录人姓名", placeholder="例如:小明")
131
+ log_member_relation = gr.Dropdown(
132
+ label="记录人身份",
133
+ choices=["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"],
134
+ value="孕妇"
135
+ )
136
+ log_breakfast = gr.Textbox(label="早餐", placeholder="吃了什么?")
137
+ log_lunch = gr.Textbox(label="午餐", placeholder="吃了什么?")
138
+ log_dinner = gr.Textbox(label="晚餐", placeholder="吃了什么?")
139
+ log_snack = gr.Textbox(label="加餐", placeholder="吃了什么?")
140
+ log_mood = gr.Textbox(label="今日心情", placeholder="今天感觉怎么样?")
141
+ log_notes = gr.Textbox(label="备注", placeholder="其他想记录的内容...", lines=2)
142
+ save_log_btn = gr.Button("💾 保存饮食记录", variant="primary", size="lg")
143
+
144
+ with gr.Column():
145
+ gr.Markdown("#### 📂 已保存的日志")
146
+ refresh_logs_btn = gr.Button("🔄 刷新日志列表")
147
+ log_result = gr.Textbox(label="保存结果", interactive=False)
148
+ log_files_display = gr.Textbox(label="Markdown日志文件", interactive=False, lines=10)
149
+
150
+ # 事件绑定
151
+ def save_diet_log(name, relation, date, breakfast, lunch, dinner, snack, mood, notes):
152
+ meals = {}
153
+ if breakfast: meals["早餐"] = breakfast
154
+ if lunch: meals["午餐"] = lunch
155
+ if dinner: meals["晚餐"] = dinner
156
+ if snack: meals["加餐"] = snack
157
+
158
+ if not meals:
159
+ return "⚠️ 请至少填写一餐的内容"
160
+
161
+ record, md_path = diet_logger.add_record(name, relation, date, meals, mood, notes)
162
+ return f"✅ 记录已保存!\n📄 Markdown文件: {md_path.name}"
163
+
164
+ def list_log_files():
165
+ files = diet_logger.get_all_markdown_files()
166
+ if not files:
167
+ return "暂无日志文件"
168
+ return "\n".join(f"📄 {f.name}" for f in files[:20])
169
+
170
+ save_log_btn.click(
171
+ fn=save_diet_log,
172
+ inputs=[log_member_name, log_member_relation, log_date,
173
+ log_breakfast, log_lunch, log_dinner, log_snack,
174
+ log_mood, log_notes],
175
+ outputs=log_result
176
+ ).then(
177
+ fn=list_log_files,
178
+ outputs=log_files_display
179
+ )
180
+
181
+ refresh_logs_btn.click(
182
+ fn=list_log_files,
183
+ outputs=log_files_display
184
+ )
185
+
186
+ # ==================== Tab 4: 营养报告 ====================
187
+ with gr.Tab("📊 营养报告"):
188
+ gr.Markdown("### 分析饮食习惯,生成可视化营养报告")
189
+
190
+ with gr.Row():
191
+ with gr.Column():
192
+ gr.Markdown("#### ⚙️ 分析设置")
193
+ analysis_days = gr.Slider(
194
+ label="分析天数",
195
+ minimum=1, maximum=30, value=7, step=1
196
+ )
197
+ generate_btn = gr.Button("📊 生成营养报告", variant="primary", size="lg")
198
+
199
+ with gr.Column():
200
+ gr.Markdown("#### 📋 报告预览")
201
+ report_text = gr.Textbox(label="文本报告", lines=15, interactive=False)
202
+
203
+ with gr.Row():
204
+ with gr.Column():
205
+ report_plot = gr.Plot(label="可视化报告图表")
206
+
207
+ with gr.Row():
208
+ with gr.Column():
209
+ export_md_btn = gr.Button("📥 导出Markdown报告")
210
+ export_result = gr.Textbox(label="导出结果", interactive=False)
211
+
212
+ # 事件绑定
213
+ def generate_report(days):
214
+ records = diet_logger.get_recent_records(days=int(days))
215
+ analysis = nutrition_analyzer.analyze_diet(records)
216
+ text_report = nutrition_analyzer.generate_report_text(analysis)
217
+ chart = nutrition_analyzer.generate_report_chart(analysis)
218
+ return text_report, chart
219
+
220
+ def export_report(days):
221
+ records = diet_logger.get_recent_records(days=int(days))
222
+ analysis = nutrition_analyzer.analyze_diet(records)
223
+ md_path = nutrition_analyzer.export_report_markdown(analysis)
224
+ return f"✅ 报告已导出: {md_path.name}"
225
+
226
+ generate_btn.click(
227
+ fn=generate_report,
228
+ inputs=[analysis_days],
229
+ outputs=[report_text, report_plot]
230
+ )
231
+
232
+ export_md_btn.click(
233
+ fn=export_report,
234
+ inputs=[analysis_days],
235
+ outputs=export_result
236
+ )
237
+
238
+ # ==================== Tab 5: 关于 ====================
239
+ with gr.Tab("💝 关于"):
240
+ gr.Markdown(
241
+ """
242
+ ## 🌸 PregoPal - 孕期陪护AI助手
243
+
244
+ ### 功能介绍
245
+
246
+ | 功能 | 说明 |
247
+ |------|------|
248
+ | 🔊 **声纹识别** | 识别家庭成员身份,自动区分谁在说话 |
249
+ | 🍳 **菜品推荐** | 根据孕期阶段和口味偏好推荐今日食谱 |
250
+ | 📝 **饮食记录** | 记录每日饮食,自动保存为Markdown日志 |
251
+ | 📊 **营养报告** | 分析饮食数据,生成可视化营养报告 |
252
+
253
+ ### 技术栈
254
+
255
+ - **前端框架**: Gradio
256
+ - **数据分析**: NumPy, Pandas, Matplotlib
257
+ - **音频处理**: SoundFile, Librosa
258
+ - **数据存储**: JSON + Markdown
259
+
260
+ ### 团队
261
+
262
+ - 项目参与: build-small-hackathon
263
+ - 开源协议: MIT
264
+
265
+ ### ⚠️ 免责声明
266
+
267
+ 本应用仅供参考,不构成医疗建议。如有特殊饮食需求或健康问题,请咨询专业医生或营养师。
268
+
269
+ ---
270
+ *用AI温暖每一个孕期家庭 ❤️*
271
+ """
272
+ )
273
+
274
+ return demo
utils.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - 工具函数
3
+ ====================
4
+ 通用工具函数,如字体设置等。
5
+ """
6
+
7
+ import matplotlib
8
+ matplotlib.use('Agg')
9
+ import matplotlib.pyplot as plt
10
+ import matplotlib.font_manager as fm
11
+
12
+
13
+ def setup_chinese_font():
14
+ """尝试设置中文字体,返回使用的字体名称"""
15
+ font_candidates = [
16
+ 'Microsoft YaHei', 'SimHei', 'WenQuanYi Micro Hei',
17
+ 'Noto Sans CJK SC', 'Noto Sans SC', 'Source Han Sans SC',
18
+ 'PingFang SC', 'Hiragino Sans GB', 'STHeiti'
19
+ ]
20
+ available = [f.name for f in fm.fontManager.ttflist]
21
+ for font in font_candidates:
22
+ if font in available:
23
+ plt.rcParams['font.sans-serif'] = [font]
24
+ plt.rcParams['axes.unicode_minus'] = False
25
+ return font
26
+ # fallback
27
+ plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
28
+ return None