Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
0f2d3d7
1
Parent(s): b7a1798
CLine 界面初版
Browse files- .gitignore +1 -0
- README.md +3 -0
- __pycache__/config.cpython-311.pyc +0 -0
- app.py +25 -0
- config.py +84 -0
- core/__init__.py +8 -0
- core/conversation_manager.py +47 -0
- core/model_loader.py +26 -0
- core/vision_processor.py +22 -0
- core/voice_processor.py +27 -0
- data/nutrition/raw/中国孕期妇女膳食指南2022图片转md版.md +0 -0
- data/nutrition_db.json +122 -0
- docs/开发日志.md +63 -0
- docs/项目理解_技术架构.md +300 -0
- modules/__init__.py +5 -0
- modules/__pycache__/__init__.cpython-311.pyc +0 -0
- modules/__pycache__/diet_logger.cpython-311.pyc +0 -0
- modules/__pycache__/meal_recommender.cpython-311.pyc +0 -0
- modules/__pycache__/nutrition_analyzer.cpython-311.pyc +0 -0
- modules/__pycache__/voiceprint.cpython-311.pyc +0 -0
- modules/diet_logger.py +165 -0
- modules/meal_recommender.py +89 -0
- modules/nutrition_analyzer.py +305 -0
- modules/voiceprint.py +158 -0
- requirements.txt +9 -0
- ui/__init__.py +5 -0
- ui/__pycache__/__init__.cpython-311.pyc +0 -0
- ui/__pycache__/app_builder.cpython-311.pyc +0 -0
- ui/app_builder.py +274 -0
- utils.py +28 -0
.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
|