Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
9046d8e
1
Parent(s): ec90eae
feat: add i18n (zh/en) language switcher + fix matplotlib Chinese font rendering
Browse files- .gitignore +2 -1
- app.py +4 -0
- data/presets/.daily_status.json +1 -1
- data/presets/今日简报_2026-06-10.json +3 -5
- modules/__pycache__/nutrition_analyzer.cpython-311.pyc +0 -0
- modules/nutrition_analyzer.py +4 -0
- test_report.html +1 -1
- test_report.json +1 -1
- ui/__pycache__/app_builder.cpython-311.pyc +0 -0
- ui/app_builder.py +352 -271
- utils.py +196 -6
.gitignore
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
.vscode/
|
|
|
|
|
|
| 1 |
+
.vscode/
|
| 2 |
+
*.pyc
|
app.py
CHANGED
|
@@ -16,6 +16,10 @@ PregoPal - 孕期陪护AI助手
|
|
| 16 |
import gradio as gr
|
| 17 |
from ui.app_builder import create_app
|
| 18 |
from loop import check_and_run_loop
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
if __name__ == "__main__":
|
| 21 |
# 启动时检查并运行每日 Loop
|
|
|
|
| 16 |
import gradio as gr
|
| 17 |
from ui.app_builder import create_app
|
| 18 |
from loop import check_and_run_loop
|
| 19 |
+
from utils import setup_chinese_font
|
| 20 |
+
|
| 21 |
+
# 启动时设置中文字体(解决 matplotlib 中文乱码)
|
| 22 |
+
_CHINESE_FONT = setup_chinese_font()
|
| 23 |
|
| 24 |
if __name__ == "__main__":
|
| 25 |
# 启动时检查并运行每日 Loop
|
data/presets/.daily_status.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"2026-06-09": {
|
| 3 |
"summary_done": true,
|
| 4 |
-
"completed_at": "2026-06-09T00:
|
| 5 |
}
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"2026-06-09": {
|
| 3 |
"summary_done": true,
|
| 4 |
+
"completed_at": "2026-06-09T00:40:53.606526"
|
| 5 |
}
|
| 6 |
}
|
data/presets/今日简报_2026-06-10.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
| 1 |
{
|
| 2 |
"date": "2026-06-10",
|
| 3 |
"generated_from": "2026-06-09",
|
| 4 |
-
"generated_at": "2026-06-09T00:
|
| 5 |
"trimester": "孕中期",
|
| 6 |
"nutrient_focus": [],
|
| 7 |
"recommended_foods": [],
|
| 8 |
"need_ask_weight": false,
|
| 9 |
"need_ask_recipe": false,
|
| 10 |
-
"conversation_topics": [
|
| 11 |
-
|
| 12 |
-
],
|
| 13 |
-
"family_memory_summary": "{'relationships': ['- **小红**: 孕妇,孕中期', '- **小明**: 丈夫,负责做饭', '- **张阿姨**: 婆婆,帮忙照顾'], 'recent_events': ['- **2026-06-07**: 丈夫做了番茄牛腩,孕妇说很好吃', '- **2026-06-08**: 婆婆煲了鲫鱼豆腐汤,孕妇喝了两碗', '- **2026-06-09**: 孕妇说想吃清蒸鲈鱼'], 'recent_daily': ['- **2026-06-07**: 孕妇今天心情不错,吃了很多水果', '- **2026-06-08**: 孕妇有点孕吐,但坚持吃了晚饭', '- **2026-06-09**: 孕妇说最近想吃酸的']}",
|
| 14 |
"three_day_summary": {}
|
| 15 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"date": "2026-06-10",
|
| 3 |
"generated_from": "2026-06-09",
|
| 4 |
+
"generated_at": "2026-06-09T00:40:53.606526",
|
| 5 |
"trimester": "孕中期",
|
| 6 |
"nutrient_focus": [],
|
| 7 |
"recommended_foods": [],
|
| 8 |
"need_ask_weight": false,
|
| 9 |
"need_ask_recipe": false,
|
| 10 |
+
"conversation_topics": [],
|
| 11 |
+
"family_memory_summary": "{}",
|
|
|
|
|
|
|
| 12 |
"three_day_summary": {}
|
| 13 |
}
|
modules/__pycache__/nutrition_analyzer.cpython-311.pyc
CHANGED
|
Binary files a/modules/__pycache__/nutrition_analyzer.cpython-311.pyc and b/modules/__pycache__/nutrition_analyzer.cpython-311.pyc differ
|
|
|
modules/nutrition_analyzer.py
CHANGED
|
@@ -15,6 +15,10 @@ 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:
|
|
|
|
| 15 |
import matplotlib.pyplot as plt
|
| 16 |
from pathlib import Path
|
| 17 |
from config import NUTRITION_DB_FILE, DEFAULT_NUTRITION_DB, REPORTS_DIR
|
| 18 |
+
from utils import setup_chinese_font
|
| 19 |
+
|
| 20 |
+
# 启动时设置中文字体
|
| 21 |
+
_CHINESE_FONT = setup_chinese_font()
|
| 22 |
|
| 23 |
|
| 24 |
class NutritionAnalyzer:
|
test_report.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
| 30 |
<div class="container">
|
| 31 |
<div class="header">
|
| 32 |
<h1>PregoPal 测试报告</h1>
|
| 33 |
-
<p>生成时间: 2026-06-09 00:
|
| 34 |
</div>
|
| 35 |
|
| 36 |
<div class="summary">
|
|
|
|
| 30 |
<div class="container">
|
| 31 |
<div class="header">
|
| 32 |
<h1>PregoPal 测试报告</h1>
|
| 33 |
+
<p>生成时间: 2026-06-09 00:40:53 | 测试框架: 内置 TestRunner</p>
|
| 34 |
</div>
|
| 35 |
|
| 36 |
<div class="summary">
|
test_report.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
"failed": 0,
|
| 5 |
"pass_rate": 100.0,
|
| 6 |
"duration": 0.01,
|
| 7 |
-
"timestamp": "2026-06-09T00:
|
| 8 |
"results": [
|
| 9 |
{
|
| 10 |
"class": "TestDRIsParser",
|
|
|
|
| 4 |
"failed": 0,
|
| 5 |
"pass_rate": 100.0,
|
| 6 |
"duration": 0.01,
|
| 7 |
+
"timestamp": "2026-06-09T00:40:53.609608",
|
| 8 |
"results": [
|
| 9 |
{
|
| 10 |
"class": "TestDRIsParser",
|
ui/__pycache__/app_builder.cpython-311.pyc
CHANGED
|
Binary files a/ui/__pycache__/app_builder.cpython-311.pyc and b/ui/__pycache__/app_builder.cpython-311.pyc differ
|
|
|
ui/app_builder.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建
|
| 3 |
===========================
|
| 4 |
-
4 个子页面:
|
| 5 |
1. 🎙️ 语音交互(主界面)
|
| 6 |
2. 👨👩👧👦 家庭饮食习惯
|
| 7 |
3. 📊 三天总结
|
|
@@ -18,6 +18,7 @@ from modules.nutrition_analyzer import NutritionAnalyzer
|
|
| 18 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 19 |
from modules.diet_extractor import DietExtractor
|
| 20 |
from modules.nutrition_standards import DRIsParser
|
|
|
|
| 21 |
|
| 22 |
# ============================================================
|
| 23 |
# 全局实例(单例)
|
|
@@ -28,288 +29,368 @@ diet_logger = DietLogger()
|
|
| 28 |
nutrition_analyzer = NutritionAnalyzer()
|
| 29 |
|
| 30 |
|
| 31 |
-
def
|
| 32 |
-
"""
|
|
|
|
| 33 |
|
| 34 |
-
with gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
-
|
| 46 |
-
with gr.Tab("🎙️ 语音交互"):
|
| 47 |
with gr.Row():
|
| 48 |
-
with gr.Column(
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
audio_input = gr.Audio(label="🎤 语音输入", type="filepath")
|
| 59 |
-
clear_btn = gr.Button("🗑️ 清空对话", size="sm")
|
| 60 |
-
|
| 61 |
-
with gr.Column(scale=1):
|
| 62 |
-
gr.Markdown("#### 🔊 声纹识别")
|
| 63 |
-
with gr.Group():
|
| 64 |
-
audio_register = gr.Audio(label="注册", type="filepath")
|
| 65 |
-
member_name = gr.Textbox(label="姓名", placeholder="小明")
|
| 66 |
-
member_relation = gr.Dropdown(
|
| 67 |
-
label="身份",
|
| 68 |
-
choices=["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"],
|
| 69 |
-
value="孕妇"
|
| 70 |
-
)
|
| 71 |
-
register_btn = gr.Button("📝 注册", variant="primary", size="sm")
|
| 72 |
-
register_result = gr.Textbox(label="", interactive=False, lines=1)
|
| 73 |
-
|
| 74 |
-
with gr.Group():
|
| 75 |
-
audio_identify = gr.Audio(label="识别", type="filepath")
|
| 76 |
-
identify_btn = gr.Button("🔊 识别", variant="primary", size="sm")
|
| 77 |
-
identify_result = gr.Textbox(label="", interactive=False, lines=1)
|
| 78 |
-
|
| 79 |
-
gr.Markdown("#### 📋 今日简报")
|
| 80 |
-
briefing_display = gr.JSON(value=briefing, label="")
|
| 81 |
-
|
| 82 |
-
# 对话事件
|
| 83 |
-
def respond(message, chat_history):
|
| 84 |
-
if not message:
|
| 85 |
-
return "", chat_history
|
| 86 |
-
|
| 87 |
-
extracted = DietExtractor.robust_extract(message)
|
| 88 |
-
response_parts = ["👋 收到!"]
|
| 89 |
-
|
| 90 |
-
if extracted["diets"]:
|
| 91 |
-
for diet in extracted["diets"]:
|
| 92 |
-
meals_str = "、".join(
|
| 93 |
-
f"{k}: {v}" for k, v in diet.get("meals", {}).items()
|
| 94 |
-
)
|
| 95 |
-
response_parts.append(f"✅ 已记录饮食:{meals_str}")
|
| 96 |
-
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 97 |
-
diet_logger.add_record(
|
| 98 |
-
member_name=diet.get("记录人", "用户"),
|
| 99 |
-
member_relation="家人",
|
| 100 |
-
date=date_str,
|
| 101 |
-
meals=diet.get("meals", {}),
|
| 102 |
-
notes=diet.get("备注", "")
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
if extracted["recipes"]:
|
| 106 |
-
for recipe in extracted["recipes"]:
|
| 107 |
-
name = recipe.get("菜名", "")
|
| 108 |
-
if name:
|
| 109 |
-
result = RecipeManager.add_recipe(
|
| 110 |
-
name=name,
|
| 111 |
-
cook=recipe.get("制作人", ""),
|
| 112 |
-
difficulty=recipe.get("难度", ""),
|
| 113 |
-
ingredients=recipe.get("食材", ""),
|
| 114 |
-
notes=recipe.get("备注", "")
|
| 115 |
-
)
|
| 116 |
-
response_parts.append(f"✅ {result}")
|
| 117 |
-
|
| 118 |
-
if extracted["preferences"]:
|
| 119 |
-
for pref in extracted["preferences"]:
|
| 120 |
-
PreferenceManager.add_member(
|
| 121 |
-
name=pref.get("人员", ""),
|
| 122 |
-
role="家人",
|
| 123 |
-
preferences=pref.get("内容", "") if pref.get("类型") == "偏好" else "",
|
| 124 |
-
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 125 |
-
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 126 |
-
)
|
| 127 |
-
response_parts.append("✅ 已记录饮食偏好")
|
| 128 |
-
|
| 129 |
-
if extracted["weights"]:
|
| 130 |
-
for w in extracted["weights"]:
|
| 131 |
-
response_parts.append(f"✅ 已记录体重:{w.get('体重', '')}kg")
|
| 132 |
-
|
| 133 |
-
if extracted["thinking"]:
|
| 134 |
-
step = extracted["thinking"].get("当前步骤", "")
|
| 135 |
-
next_step = extracted["thinking"].get("下一步", "")
|
| 136 |
-
response_parts.append(f"🤔 当前:{step} → 下一步:{next_step}")
|
| 137 |
-
|
| 138 |
-
if len(response_parts) == 1:
|
| 139 |
-
if any(kw in message for kw in ["吃了", "想吃", "午餐", "晚餐", "早餐"]):
|
| 140 |
-
response_parts.append("我来帮你记录!下次可以直接说「我吃了...」我会自动记录。")
|
| 141 |
-
elif any(kw in message for kw in ["菜谱", "会做", "做饭"]):
|
| 142 |
-
response_parts.append("好的,我来记录家庭菜谱!")
|
| 143 |
-
else:
|
| 144 |
-
response_parts.append("有什么我可以帮你的吗?比如记录饮食、推荐食谱、或者聊聊今天的感受。")
|
| 145 |
-
|
| 146 |
-
response = "\n\n".join(response_parts)
|
| 147 |
-
chat_history.append((message, response))
|
| 148 |
-
return "", chat_history
|
| 149 |
-
|
| 150 |
-
send_btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 151 |
-
msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 152 |
-
clear_btn.click(lambda: None, outputs=chatbot)
|
| 153 |
-
|
| 154 |
-
register_btn.click(
|
| 155 |
-
fn=lambda name, rel, audio: voiceprint_mgr.register_member(name, rel, audio),
|
| 156 |
-
inputs=[member_name, member_relation, audio_register],
|
| 157 |
-
outputs=[gr.State(), register_result]
|
| 158 |
-
)
|
| 159 |
-
identify_btn.click(
|
| 160 |
-
fn=lambda audio: voiceprint_mgr.identify_speaker(audio),
|
| 161 |
-
inputs=[audio_identify],
|
| 162 |
-
outputs=[gr.State(), identify_result]
|
| 163 |
-
)
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
pref_avoid = gr.Textbox(label="忌口", placeholder="不吃什么?", lines=1)
|
| 179 |
-
pref_allergy = gr.Textbox(label="过敏", placeholder="对什么过敏?", lines=1)
|
| 180 |
-
save_pref_btn = gr.Button("💾 保存", variant="primary", size="sm")
|
| 181 |
-
|
| 182 |
-
with gr.Column():
|
| 183 |
-
refresh_pref_btn = gr.Button("🔄 刷新", size="sm")
|
| 184 |
-
pref_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 185 |
-
|
| 186 |
-
save_pref_btn.click(
|
| 187 |
-
fn=lambda n, r, p, a, al: PreferenceManager.add_member(n, r, p, a, al, ""),
|
| 188 |
-
inputs=[pref_name, pref_role, pref_likes, pref_avoid, pref_allergy],
|
| 189 |
-
outputs=[gr.Textbox()]
|
| 190 |
-
).then(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
|
| 191 |
-
|
| 192 |
-
refresh_pref_btn.click(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
|
| 193 |
-
|
| 194 |
-
with gr.Tab("🍳 家庭菜谱"):
|
| 195 |
-
with gr.Row():
|
| 196 |
-
with gr.Column():
|
| 197 |
-
recipe_name = gr.Textbox(label="菜名", placeholder="番茄牛腩")
|
| 198 |
-
recipe_cook = gr.Textbox(label="制作人", placeholder="谁会做?")
|
| 199 |
-
recipe_diff = gr.Dropdown(label="难度", choices=["简单", "中等", "困难"], value="中等")
|
| 200 |
-
recipe_ingredients = gr.Textbox(label="食材", placeholder="主要食材", lines=1)
|
| 201 |
-
save_recipe_btn = gr.Button("💾 添加", variant="primary", size="sm")
|
| 202 |
-
|
| 203 |
-
with gr.Column():
|
| 204 |
-
refresh_recipe_btn = gr.Button("🔄 刷新", size="sm")
|
| 205 |
-
recipe_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 206 |
-
|
| 207 |
-
save_recipe_btn.click(
|
| 208 |
-
fn=lambda n, c, d, i: RecipeManager.add_recipe(n, c, d, i, ""),
|
| 209 |
-
inputs=[recipe_name, recipe_cook, recipe_diff, recipe_ingredients],
|
| 210 |
-
outputs=[gr.Textbox()]
|
| 211 |
-
).then(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
|
| 212 |
-
|
| 213 |
-
refresh_recipe_btn.click(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
|
| 214 |
-
|
| 215 |
-
with gr.Tab("📖 家庭记忆"):
|
| 216 |
-
with gr.Row():
|
| 217 |
-
with gr.Column():
|
| 218 |
-
memory_type = gr.Dropdown(label="类型", choices=["关系", "事件", "日常"], value="日常")
|
| 219 |
-
memory_content = gr.Textbox(label="内容", placeholder="记录家庭关系、重要事件或日常点滴", lines=2)
|
| 220 |
-
save_memory_btn = gr.Button("💾 保存", variant="primary", size="sm")
|
| 221 |
-
|
| 222 |
-
with gr.Column():
|
| 223 |
-
refresh_memory_btn = gr.Button("🔄 刷新", size="sm")
|
| 224 |
-
memory_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 225 |
-
|
| 226 |
-
def save_memory(m_type, content):
|
| 227 |
-
if m_type == "事件":
|
| 228 |
-
return MemoryManager.add_event(content)
|
| 229 |
-
return MemoryManager.add_daily(f"[{m_type}] {content}")
|
| 230 |
-
|
| 231 |
-
save_memory_btn.click(
|
| 232 |
-
fn=save_memory, inputs=[memory_type, memory_content], outputs=[gr.Textbox()]
|
| 233 |
-
).then(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
|
| 234 |
-
|
| 235 |
-
refresh_memory_btn.click(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
|
| 236 |
-
|
| 237 |
-
# ==================== 页面 3: 三天总结 ====================
|
| 238 |
-
with gr.Tab("📊 三天总结"):
|
| 239 |
with gr.Row():
|
| 240 |
-
with gr.Column(
|
| 241 |
-
|
| 242 |
-
label="
|
| 243 |
-
choices=["
|
| 244 |
-
value="
|
| 245 |
)
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
run_3day_btn.click(
|
| 277 |
-
fn=run_three_day_summary,
|
| 278 |
-
inputs=[trimester_select],
|
| 279 |
-
outputs=[three_day_result, menu_suggestion]
|
| 280 |
)
|
|
|
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
with gr.Column(scale=1):
|
| 286 |
-
analysis_days = gr.Slider(label="分析天数", minimum=1, maximum=30, value=7, step=1)
|
| 287 |
-
generate_btn = gr.Button("📊 生成报告", variant="primary")
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
|
|
|
| 294 |
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
return demo
|
|
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建
|
| 3 |
===========================
|
| 4 |
+
4 个子页面 + 中英文切换:
|
| 5 |
1. 🎙️ 语音交互(主界面)
|
| 6 |
2. 👨👩👧👦 家庭饮食习惯
|
| 7 |
3. 📊 三天总结
|
|
|
|
| 18 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 19 |
from modules.diet_extractor import DietExtractor
|
| 20 |
from modules.nutrition_standards import DRIsParser
|
| 21 |
+
from utils import t, ZH, EN
|
| 22 |
|
| 23 |
# ============================================================
|
| 24 |
# 全局实例(单例)
|
|
|
|
| 29 |
nutrition_analyzer = NutritionAnalyzer()
|
| 30 |
|
| 31 |
|
| 32 |
+
def _build_voice_tab(lang_state):
|
| 33 |
+
"""构建语音交互页面(动态语言)"""
|
| 34 |
+
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 35 |
|
| 36 |
+
with gr.Row():
|
| 37 |
+
with gr.Column(scale=3):
|
| 38 |
+
chatbot = gr.Chatbot(label=t("chat_label", lang), height=400)
|
| 39 |
+
with gr.Row():
|
| 40 |
+
msg = gr.Textbox(
|
| 41 |
+
label=t("chat_label", lang),
|
| 42 |
+
placeholder=t("msg_placeholder", lang),
|
| 43 |
+
scale=4
|
| 44 |
+
)
|
| 45 |
+
send_btn = gr.Button(t("btn_send", lang), variant="primary", scale=1)
|
| 46 |
+
with gr.Row():
|
| 47 |
+
audio_input = gr.Audio(label="🎤 " + t("voice_register", lang), type="filepath")
|
| 48 |
+
clear_btn = gr.Button(t("btn_clear", lang), size="sm")
|
| 49 |
+
|
| 50 |
+
with gr.Column(scale=1):
|
| 51 |
+
gr.Markdown(t("voice_title", lang))
|
| 52 |
+
with gr.Group():
|
| 53 |
+
audio_register = gr.Audio(label=t("voice_register", lang), type="filepath")
|
| 54 |
+
member_name = gr.Textbox(label=t("member_name", lang), placeholder="小明")
|
| 55 |
+
member_relation = gr.Dropdown(
|
| 56 |
+
label=t("member_relation", lang),
|
| 57 |
+
choices=ZH["relation_choices"] if lang == "zh" else EN["relation_choices"],
|
| 58 |
+
value=ZH["relation_choices"][0] if lang == "zh" else EN["relation_choices"][0]
|
| 59 |
+
)
|
| 60 |
+
register_btn = gr.Button(t("btn_register", lang), variant="primary", size="sm")
|
| 61 |
+
register_result = gr.Textbox(label="", interactive=False, lines=1)
|
| 62 |
+
|
| 63 |
+
with gr.Group():
|
| 64 |
+
audio_identify = gr.Audio(label=t("voice_identify", lang), type="filepath")
|
| 65 |
+
identify_btn = gr.Button(t("btn_identify", lang), variant="primary", size="sm")
|
| 66 |
+
identify_result = gr.Textbox(label="", interactive=False, lines=1)
|
| 67 |
+
|
| 68 |
+
gr.Markdown(t("briefing_title", lang))
|
| 69 |
+
briefing_display = gr.JSON(value={}, label="")
|
| 70 |
+
|
| 71 |
+
# 对话事件
|
| 72 |
+
def respond(message, chat_history):
|
| 73 |
+
if not message:
|
| 74 |
+
return "", chat_history
|
| 75 |
+
|
| 76 |
+
extracted = DietExtractor.robust_extract(message)
|
| 77 |
+
response_parts = ["👋 " + (t("btn_send", lang) if lang == "zh" else "Got it!")]
|
| 78 |
+
|
| 79 |
+
if extracted["diets"]:
|
| 80 |
+
for diet in extracted["diets"]:
|
| 81 |
+
meals_str = "、".join(
|
| 82 |
+
f"{k}: {v}" for k, v in diet.get("meals", {}).items()
|
| 83 |
+
)
|
| 84 |
+
response_parts.append(f"✅ {t('btn_send', lang) if lang == 'zh' else 'Logged'}: {meals_str}")
|
| 85 |
+
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 86 |
+
diet_logger.add_record(
|
| 87 |
+
member_name=diet.get("记录人", "用户"),
|
| 88 |
+
member_relation="家人",
|
| 89 |
+
date=date_str,
|
| 90 |
+
meals=diet.get("meals", {}),
|
| 91 |
+
notes=diet.get("备注", "")
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if extracted["recipes"]:
|
| 95 |
+
for recipe in extracted["recipes"]:
|
| 96 |
+
name = recipe.get("菜名", "")
|
| 97 |
+
if name:
|
| 98 |
+
result = RecipeManager.add_recipe(
|
| 99 |
+
name=name,
|
| 100 |
+
cook=recipe.get("制作人", ""),
|
| 101 |
+
difficulty=recipe.get("难度", ""),
|
| 102 |
+
ingredients=recipe.get("食材", ""),
|
| 103 |
+
notes=recipe.get("备注", "")
|
| 104 |
+
)
|
| 105 |
+
response_parts.append(f"✅ {result}")
|
| 106 |
+
|
| 107 |
+
if extracted["preferences"]:
|
| 108 |
+
for pref in extracted["preferences"]:
|
| 109 |
+
PreferenceManager.add_member(
|
| 110 |
+
name=pref.get("人员", ""),
|
| 111 |
+
role="家人",
|
| 112 |
+
preferences=pref.get("内容", "") if pref.get("类型") == "偏好" else "",
|
| 113 |
+
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 114 |
+
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 115 |
+
)
|
| 116 |
+
response_parts.append("✅ " + (t("btn_save", lang) if lang == "zh" else "Saved preferences"))
|
| 117 |
+
|
| 118 |
+
if extracted["weights"]:
|
| 119 |
+
for w in extracted["weights"]:
|
| 120 |
+
response_parts.append(f"✅ {t('btn_save', lang) if lang == 'zh' else 'Logged weight'}: {w.get('体重', '')}kg")
|
| 121 |
+
|
| 122 |
+
if extracted["thinking"]:
|
| 123 |
+
step = extracted["thinking"].get("当前步骤", "")
|
| 124 |
+
next_step = extracted["thinking"].get("下一步", "")
|
| 125 |
+
response_parts.append(f"🤔 {t('btn_analyze', lang) if lang == 'zh' else 'Thinking'}: {step} → {next_step}")
|
| 126 |
+
|
| 127 |
+
if len(response_parts) == 1:
|
| 128 |
+
if any(kw in message for kw in ["吃了", "想吃", "午餐", "晚餐", "早餐"]):
|
| 129 |
+
response_parts.append("我来帮你记录!下次可以直接说「我吃了...」我会自动记录。")
|
| 130 |
+
elif any(kw in message for kw in ["菜谱", "会做", "做饭"]):
|
| 131 |
+
response_parts.append("好的,我来记录家庭菜谱!")
|
| 132 |
+
else:
|
| 133 |
+
response_parts.append("有什么我可以帮你的吗?比如记录饮食、推荐食谱、或者聊聊今天的感受。")
|
| 134 |
+
|
| 135 |
+
response = "\n\n".join(response_parts)
|
| 136 |
+
chat_history.append((message, response))
|
| 137 |
+
return "", chat_history
|
| 138 |
+
|
| 139 |
+
send_btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 140 |
+
msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 141 |
+
clear_btn.click(lambda: None, outputs=chatbot)
|
| 142 |
+
|
| 143 |
+
register_btn.click(
|
| 144 |
+
fn=lambda name, rel, audio: voiceprint_mgr.register_member(name, rel, audio),
|
| 145 |
+
inputs=[member_name, member_relation, audio_register],
|
| 146 |
+
outputs=[gr.State(), register_result]
|
| 147 |
+
)
|
| 148 |
+
identify_btn.click(
|
| 149 |
+
fn=lambda audio: voiceprint_mgr.identify_speaker(audio),
|
| 150 |
+
inputs=[audio_identify],
|
| 151 |
+
outputs=[gr.State(), identify_result]
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return chatbot, msg, send_btn, clear_btn, briefing_display
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _build_family_tab(lang_state):
|
| 158 |
+
"""构建家庭饮食习惯页面(动态语言)"""
|
| 159 |
+
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 160 |
+
|
| 161 |
+
with gr.Tabs():
|
| 162 |
+
with gr.Tab(t("tab_preferences", lang)):
|
| 163 |
+
with gr.Row():
|
| 164 |
+
with gr.Column():
|
| 165 |
+
pref_name = gr.Textbox(label=t("pref_name", lang), placeholder="小明")
|
| 166 |
+
pref_role = gr.Dropdown(
|
| 167 |
+
label=t("pref_role", lang),
|
| 168 |
+
choices=ZH["relation_choices"] if lang == "zh" else EN["relation_choices"],
|
| 169 |
+
value=ZH["relation_choices"][0] if lang == "zh" else EN["relation_choices"][0]
|
| 170 |
+
)
|
| 171 |
+
pref_likes = gr.Textbox(label=t("pref_likes", lang), placeholder=t("pref_likes_placeholder", lang), lines=1)
|
| 172 |
+
pref_avoid = gr.Textbox(label=t("pref_avoid", lang), placeholder=t("pref_avoid_placeholder", lang), lines=1)
|
| 173 |
+
pref_allergy = gr.Textbox(label=t("pref_allergy", lang), placeholder=t("pref_allergy_placeholder", lang), lines=1)
|
| 174 |
+
save_pref_btn = gr.Button(t("btn_save", lang), variant="primary", size="sm")
|
| 175 |
|
| 176 |
+
with gr.Column():
|
| 177 |
+
refresh_pref_btn = gr.Button(t("btn_refresh", lang), size="sm")
|
| 178 |
+
pref_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 179 |
+
|
| 180 |
+
save_pref_btn.click(
|
| 181 |
+
fn=lambda n, r, p, a, al: PreferenceManager.add_member(n, r, p, a, al, ""),
|
| 182 |
+
inputs=[pref_name, pref_role, pref_likes, pref_avoid, pref_allergy],
|
| 183 |
+
outputs=[gr.Textbox()]
|
| 184 |
+
).then(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
|
| 185 |
|
| 186 |
+
refresh_pref_btn.click(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
|
| 187 |
|
| 188 |
+
with gr.Tab(t("tab_recipes", lang)):
|
|
|
|
| 189 |
with gr.Row():
|
| 190 |
+
with gr.Column():
|
| 191 |
+
recipe_name = gr.Textbox(label=t("recipe_name", lang), placeholder=t("recipe_name_placeholder", lang))
|
| 192 |
+
recipe_cook = gr.Textbox(label=t("recipe_cook", lang), placeholder=t("recipe_cook_placeholder", lang))
|
| 193 |
+
recipe_diff = gr.Dropdown(
|
| 194 |
+
label=t("recipe_diff", lang),
|
| 195 |
+
choices=ZH["recipe_diff_choices"] if lang == "zh" else EN["recipe_diff_choices"],
|
| 196 |
+
value=ZH["recipe_diff_choices"][1] if lang == "zh" else EN["recipe_diff_choices"][1]
|
| 197 |
+
)
|
| 198 |
+
recipe_ingredients = gr.Textbox(label=t("recipe_ingredients", lang), placeholder=t("recipe_ingredients_placeholder", lang), lines=1)
|
| 199 |
+
save_recipe_btn = gr.Button(t("btn_add", lang), variant="primary", size="sm")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
+
with gr.Column():
|
| 202 |
+
refresh_recipe_btn = gr.Button(t("btn_refresh", lang), size="sm")
|
| 203 |
+
recipe_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 204 |
+
|
| 205 |
+
save_recipe_btn.click(
|
| 206 |
+
fn=lambda n, c, d, i: RecipeManager.add_recipe(n, c, d, i, ""),
|
| 207 |
+
inputs=[recipe_name, recipe_cook, recipe_diff, recipe_ingredients],
|
| 208 |
+
outputs=[gr.Textbox()]
|
| 209 |
+
).then(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
|
| 210 |
+
|
| 211 |
+
refresh_recipe_btn.click(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
|
| 212 |
+
|
| 213 |
+
with gr.Tab(t("tab_memory", lang)):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
with gr.Row():
|
| 215 |
+
with gr.Column():
|
| 216 |
+
memory_type = gr.Dropdown(
|
| 217 |
+
label=t("memory_type", lang),
|
| 218 |
+
choices=ZH["memory_type_choices"] if lang == "zh" else EN["memory_type_choices"],
|
| 219 |
+
value=ZH["memory_type_choices"][0] if lang == "zh" else EN["memory_type_choices"][0]
|
| 220 |
)
|
| 221 |
+
memory_content = gr.Textbox(label=t("memory_content", lang), placeholder=t("memory_content_placeholder", lang), lines=2)
|
| 222 |
+
save_memory_btn = gr.Button(t("btn_save_memory", lang), variant="primary", size="sm")
|
| 223 |
+
|
| 224 |
+
with gr.Column():
|
| 225 |
+
refresh_memory_btn = gr.Button(t("btn_refresh", lang), size="sm")
|
| 226 |
+
memory_display = gr.Textbox(label="", interactive=False, lines=12)
|
| 227 |
+
|
| 228 |
+
def save_memory(m_type, content):
|
| 229 |
+
if m_type in ["事件", "Event"]:
|
| 230 |
+
return MemoryManager.add_event(content)
|
| 231 |
+
return MemoryManager.add_daily(f"[{m_type}] {content}")
|
| 232 |
+
|
| 233 |
+
save_memory_btn.click(
|
| 234 |
+
fn=save_memory, inputs=[memory_type, memory_content], outputs=[gr.Textbox()]
|
| 235 |
+
).then(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
|
| 236 |
+
|
| 237 |
+
refresh_memory_btn.click(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _build_summary_tab(lang_state):
|
| 241 |
+
"""构建三天总结页面(动态语言)"""
|
| 242 |
+
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 243 |
+
|
| 244 |
+
with gr.Row():
|
| 245 |
+
with gr.Column(scale=1):
|
| 246 |
+
trimester_select = gr.Radio(
|
| 247 |
+
label=t("trimester_label", lang),
|
| 248 |
+
choices=ZH["trimester_choices"] if lang == "zh" else EN["trimester_choices"],
|
| 249 |
+
value=ZH["trimester_choices"][1] if lang == "zh" else EN["trimester_choices"][1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
)
|
| 251 |
+
run_3day_btn = gr.Button(t("btn_analyze", lang), variant="primary")
|
| 252 |
|
| 253 |
+
with gr.Column(scale=2):
|
| 254 |
+
three_day_result = gr.Textbox(label=t("analysis_result", lang), interactive=False, lines=15)
|
| 255 |
+
menu_suggestion = gr.Textbox(label=t("menu_suggestion", lang), interactive=False, lines=4)
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
+
def run_three_day_summary(trimester):
|
| 258 |
+
from plugins.three_day_summary import ThreeDaySummaryPlugin
|
| 259 |
+
from plugins.base import LoopContext
|
| 260 |
+
import asyncio
|
| 261 |
|
| 262 |
+
ctx = LoopContext()
|
| 263 |
+
ctx.briefing["trimester"] = trimester
|
| 264 |
+
asyncio.run(ThreeDaySummaryPlugin().run(ctx))
|
| 265 |
|
| 266 |
+
report = ctx.briefing.get("three_day_summary", {}).get("report", "暂无数据")
|
| 267 |
+
deficits = ctx.briefing.get("three_day_summary", {}).get("persistent_deficits", [])
|
| 268 |
+
|
| 269 |
+
suggestions = []
|
| 270 |
+
if deficits:
|
| 271 |
+
suggestions.append("📋 建议增加以下食材到家庭菜单:")
|
| 272 |
+
for n in deficits[:3]:
|
| 273 |
+
foods = DRIsParser.RICH_FOODS.get(n, [])
|
| 274 |
+
if foods:
|
| 275 |
+
suggestions.append(f" • {n} → {'、'.join(foods[:4])}")
|
| 276 |
+
else:
|
| 277 |
+
suggestions.append("✅ 近三天营养状况良好!")
|
| 278 |
+
|
| 279 |
+
return report, "\n".join(suggestions)
|
| 280 |
+
|
| 281 |
+
run_3day_btn.click(
|
| 282 |
+
fn=run_three_day_summary,
|
| 283 |
+
inputs=[trimester_select],
|
| 284 |
+
outputs=[three_day_result, menu_suggestion]
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def _build_report_tab(lang_state):
|
| 289 |
+
"""构建营养报告页面(动态语言)"""
|
| 290 |
+
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 291 |
+
|
| 292 |
+
with gr.Row():
|
| 293 |
+
with gr.Column(scale=1):
|
| 294 |
+
analysis_days = gr.Slider(label=t("analysis_days", lang), minimum=1, maximum=30, value=7, step=1)
|
| 295 |
+
generate_btn = gr.Button(t("btn_generate", lang), variant="primary")
|
| 296 |
+
|
| 297 |
+
with gr.Column(scale=2):
|
| 298 |
+
report_text = gr.Textbox(label=t("report_text", lang), interactive=False, lines=12)
|
| 299 |
+
|
| 300 |
+
with gr.Row():
|
| 301 |
+
report_plot = gr.Plot(label=t("report_chart", lang))
|
| 302 |
+
|
| 303 |
+
with gr.Row():
|
| 304 |
+
export_md_btn = gr.Button(t("btn_export_md", lang), size="sm")
|
| 305 |
+
export_result = gr.Textbox(label="", interactive=False, lines=1)
|
| 306 |
+
|
| 307 |
+
def generate_report(days):
|
| 308 |
+
records = diet_logger.get_recent_records(days=int(days))
|
| 309 |
+
analysis = nutrition_analyzer.analyze_diet(records)
|
| 310 |
+
text_report = nutrition_analyzer.generate_report_text(analysis)
|
| 311 |
+
chart = nutrition_analyzer.generate_report_chart(analysis)
|
| 312 |
+
return text_report, chart
|
| 313 |
+
|
| 314 |
+
def export_report(days):
|
| 315 |
+
records = diet_logger.get_recent_records(days=int(days))
|
| 316 |
+
analysis = nutrition_analyzer.analyze_diet(records)
|
| 317 |
+
md_path = nutrition_analyzer.export_report_markdown(analysis)
|
| 318 |
+
return f"✅ {t('btn_export_md', lang) if lang == 'zh' else 'Exported'}: {md_path.name}"
|
| 319 |
+
|
| 320 |
+
generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=[report_text, report_plot])
|
| 321 |
+
export_md_btn.click(fn=export_report, inputs=[analysis_days], outputs=export_result)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def create_app(loop=None):
|
| 325 |
+
"""创建主应用(含中英��切换)"""
|
| 326 |
+
|
| 327 |
+
# 获取简报
|
| 328 |
+
briefing = loop.get_briefing() if loop else {}
|
| 329 |
+
|
| 330 |
+
with gr.Blocks(title="PregoPal - 孕期陪护AI助手", theme=gr.themes.Soft(
|
| 331 |
+
primary_hue="pink",
|
| 332 |
+
secondary_hue="green",
|
| 333 |
+
)) as demo:
|
| 334 |
+
|
| 335 |
+
# ============================================================
|
| 336 |
+
# 语言状态(存储在 gr.State 中)
|
| 337 |
+
# ============================================================
|
| 338 |
+
lang_state = gr.State(value="zh")
|
| 339 |
+
|
| 340 |
+
# ============================================================
|
| 341 |
+
# 顶部标题 + 语言切换
|
| 342 |
+
# ============================================================
|
| 343 |
+
with gr.Row():
|
| 344 |
+
with gr.Column(scale=4):
|
| 345 |
+
gr.Markdown(
|
| 346 |
+
"""
|
| 347 |
+
# 🌸 PregoPal - 孕期陪护AI助手
|
| 348 |
+
### 一个温馨的家庭式孕期AI伴侣
|
| 349 |
+
"""
|
| 350 |
+
)
|
| 351 |
+
with gr.Column(scale=1, min_width=120):
|
| 352 |
+
lang_selector = gr.Radio(
|
| 353 |
+
label="🌐 Language",
|
| 354 |
+
choices=["中文", "English"],
|
| 355 |
+
value="中文",
|
| 356 |
+
interactive=True
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# ============================================================
|
| 360 |
+
# 语言切换回调:更新所有组件的 label
|
| 361 |
+
# ============================================================
|
| 362 |
+
def switch_lang(choice):
|
| 363 |
+
new_lang = "zh" if choice == "中文" else "en"
|
| 364 |
+
return new_lang
|
| 365 |
+
|
| 366 |
+
lang_selector.change(
|
| 367 |
+
fn=switch_lang,
|
| 368 |
+
inputs=[lang_selector],
|
| 369 |
+
outputs=[lang_state]
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
# ============================================================
|
| 373 |
+
# 页面 1: 语音交互
|
| 374 |
+
# ============================================================
|
| 375 |
+
with gr.Tab(t("tab_voice", "zh"), id="tab_voice"):
|
| 376 |
+
_build_voice_tab(lang_state)
|
| 377 |
+
|
| 378 |
+
# ============================================================
|
| 379 |
+
# 页面 2: 家庭饮食习惯
|
| 380 |
+
# ============================================================
|
| 381 |
+
with gr.Tab(t("tab_family", "zh"), id="tab_family"):
|
| 382 |
+
_build_family_tab(lang_state)
|
| 383 |
+
|
| 384 |
+
# ============================================================
|
| 385 |
+
# 页面 3: 三天总结
|
| 386 |
+
# ============================================================
|
| 387 |
+
with gr.Tab(t("tab_summary", "zh"), id="tab_summary"):
|
| 388 |
+
_build_summary_tab(lang_state)
|
| 389 |
+
|
| 390 |
+
# ============================================================
|
| 391 |
+
# 页面 4: 营养报告
|
| 392 |
+
# ============================================================
|
| 393 |
+
with gr.Tab(t("tab_report", "zh"), id="tab_report"):
|
| 394 |
+
_build_report_tab(lang_state)
|
| 395 |
|
| 396 |
return demo
|
utils.py
CHANGED
|
@@ -1,28 +1,218 @@
|
|
| 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 |
-
'
|
| 17 |
-
'
|
| 18 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
|
|
|
|
| 28 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
from pathlib import Path
|
| 12 |
|
| 13 |
|
| 14 |
+
# ============================================================
|
| 15 |
+
# 中文字体设置
|
| 16 |
+
# ============================================================
|
| 17 |
+
|
| 18 |
def setup_chinese_font():
|
| 19 |
"""尝试设置中文字体,返回使用的字体名称"""
|
| 20 |
+
# 优先使用 Windows 系统字体
|
| 21 |
font_candidates = [
|
| 22 |
+
'SimHei', # 黑体(Windows 自带)
|
| 23 |
+
'Microsoft YaHei', # 微软雅黑
|
| 24 |
+
'SimSun', # 宋体
|
| 25 |
+
'KaiTi', # 楷体
|
| 26 |
+
'FangSong', # 仿宋
|
| 27 |
+
'WenQuanYi Micro Hei',
|
| 28 |
+
'Noto Sans CJK SC',
|
| 29 |
+
'Noto Sans SC',
|
| 30 |
+
'Source Han Sans SC',
|
| 31 |
+
'PingFang SC',
|
| 32 |
+
'Hiragino Sans GB',
|
| 33 |
+
'STHeiti',
|
| 34 |
]
|
| 35 |
available = [f.name for f in fm.fontManager.ttflist]
|
| 36 |
for font in font_candidates:
|
| 37 |
if font in available:
|
| 38 |
+
plt.rcParams['font.sans-serif'] = [font, 'DejaVu Sans']
|
| 39 |
plt.rcParams['axes.unicode_minus'] = False
|
| 40 |
return font
|
| 41 |
+
|
| 42 |
+
# fallback: 尝试直接加载系统字体文件
|
| 43 |
+
system_fonts = [
|
| 44 |
+
Path("C:/Windows/Fonts/simhei.ttf"),
|
| 45 |
+
Path("C:/Windows/Fonts/msyh.ttc"),
|
| 46 |
+
Path("C:/Windows/Fonts/simsun.ttc"),
|
| 47 |
+
]
|
| 48 |
+
for fp in system_fonts:
|
| 49 |
+
if fp.exists():
|
| 50 |
+
try:
|
| 51 |
+
fm.fontManager.addfont(str(fp))
|
| 52 |
+
font_name = fm.FontProperties(fname=str(fp)).get_name()
|
| 53 |
+
plt.rcParams['font.sans-serif'] = [font_name, 'DejaVu Sans']
|
| 54 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 55 |
+
return font_name
|
| 56 |
+
except Exception:
|
| 57 |
+
continue
|
| 58 |
+
|
| 59 |
+
# 最终 fallback
|
| 60 |
plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
|
| 61 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 62 |
return None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# ============================================================
|
| 66 |
+
# 国际化(i18n)
|
| 67 |
+
# ============================================================
|
| 68 |
+
|
| 69 |
+
# 中文字典
|
| 70 |
+
ZH = {
|
| 71 |
+
# 应用标题
|
| 72 |
+
"app_title": "🌸 PregoPal - 孕期陪护AI助手",
|
| 73 |
+
"app_subtitle": "一个温馨的家庭式孕期AI伴侣",
|
| 74 |
+
|
| 75 |
+
# Tab 名称
|
| 76 |
+
"tab_voice": "🎙️ 语音交互",
|
| 77 |
+
"tab_family": "👨👩👧👦 家庭饮食习惯",
|
| 78 |
+
"tab_summary": "📊 三天总结",
|
| 79 |
+
"tab_report": "📈 营养报告",
|
| 80 |
+
|
| 81 |
+
# 语音交互页
|
| 82 |
+
"chat_label": "💬 对话",
|
| 83 |
+
"msg_placeholder": "例如:我今天中午吃了番茄牛腩",
|
| 84 |
+
"btn_send": "发送",
|
| 85 |
+
"btn_clear": "🗑️ 清空对话",
|
| 86 |
+
"voice_title": "#### 🔊 声纹识别",
|
| 87 |
+
"voice_register": "注册",
|
| 88 |
+
"voice_identify": "识别",
|
| 89 |
+
"member_name": "姓名",
|
| 90 |
+
"member_relation": "身份",
|
| 91 |
+
"relation_choices": ["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"],
|
| 92 |
+
"btn_register": "📝 注册",
|
| 93 |
+
"btn_identify": "🔊 识别",
|
| 94 |
+
"briefing_title": "#### 📋 今日简报",
|
| 95 |
+
|
| 96 |
+
# 家庭饮食习惯页
|
| 97 |
+
"tab_preferences": "🥗 饮食偏好",
|
| 98 |
+
"tab_recipes": "🍳 家庭菜谱",
|
| 99 |
+
"tab_memory": "📖 家庭记忆",
|
| 100 |
+
"pref_name": "姓名",
|
| 101 |
+
"pref_role": "身份",
|
| 102 |
+
"pref_likes": "偏好",
|
| 103 |
+
"pref_likes_placeholder": "喜欢吃什么?",
|
| 104 |
+
"pref_avoid": "忌口",
|
| 105 |
+
"pref_avoid_placeholder": "不吃什么?",
|
| 106 |
+
"pref_allergy": "过敏",
|
| 107 |
+
"pref_allergy_placeholder": "对什么过敏?",
|
| 108 |
+
"btn_save": "💾 保存",
|
| 109 |
+
"btn_refresh": "🔄 刷新",
|
| 110 |
+
"recipe_name": "菜名",
|
| 111 |
+
"recipe_name_placeholder": "番茄牛腩",
|
| 112 |
+
"recipe_cook": "制作人",
|
| 113 |
+
"recipe_cook_placeholder": "谁会做?",
|
| 114 |
+
"recipe_diff": "难度",
|
| 115 |
+
"recipe_diff_choices": ["简单", "中等", "困难"],
|
| 116 |
+
"recipe_ingredients": "食材",
|
| 117 |
+
"recipe_ingredients_placeholder": "主要食材",
|
| 118 |
+
"btn_add": "💾 添加",
|
| 119 |
+
"memory_type": "类型",
|
| 120 |
+
"memory_type_choices": ["关系", "事件", "日常"],
|
| 121 |
+
"memory_content": "内容",
|
| 122 |
+
"memory_content_placeholder": "记录家庭关系、重要事件或日常点滴",
|
| 123 |
+
"btn_save_memory": "💾 保存",
|
| 124 |
+
|
| 125 |
+
# 三天总结页
|
| 126 |
+
"trimester_label": "孕期阶段",
|
| 127 |
+
"trimester_choices": ["孕早期", "孕中期", "孕晚期"],
|
| 128 |
+
"btn_analyze": "📊 运行分析",
|
| 129 |
+
"analysis_result": "📋 分析结果",
|
| 130 |
+
"menu_suggestion": "💡 菜单改进建议",
|
| 131 |
+
|
| 132 |
+
# 营养报告页
|
| 133 |
+
"analysis_days": "分析天数",
|
| 134 |
+
"btn_generate": "📊 生成报告",
|
| 135 |
+
"report_text": "📋 文本报告",
|
| 136 |
+
"report_chart": "📊 可视化图表",
|
| 137 |
+
"btn_export_md": "📥 导�� Markdown",
|
| 138 |
+
|
| 139 |
+
# 通用
|
| 140 |
+
"lang_label": "🌐 语言",
|
| 141 |
+
"lang_zh": "中文",
|
| 142 |
+
"lang_en": "English",
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# 英文字典
|
| 146 |
+
EN = {
|
| 147 |
+
"app_title": "🌸 PregoPal - Pregnancy Companion AI",
|
| 148 |
+
"app_subtitle": "A cozy home-oriented AI companion for expectant mothers",
|
| 149 |
+
|
| 150 |
+
"tab_voice": "🎙️ Voice Chat",
|
| 151 |
+
"tab_family": "👨👩👧👦 Family Diet",
|
| 152 |
+
"tab_summary": "📊 3-Day Summary",
|
| 153 |
+
"tab_report": "📈 Nutrition Report",
|
| 154 |
+
|
| 155 |
+
"chat_label": "💬 Chat",
|
| 156 |
+
"msg_placeholder": "e.g. I had tomato beef brisket for lunch today",
|
| 157 |
+
"btn_send": "Send",
|
| 158 |
+
"btn_clear": "🗑️ Clear",
|
| 159 |
+
"voice_title": "#### 🔊 Voiceprint",
|
| 160 |
+
"voice_register": "Register",
|
| 161 |
+
"voice_identify": "Identify",
|
| 162 |
+
"member_name": "Name",
|
| 163 |
+
"member_relation": "Role",
|
| 164 |
+
"relation_choices": ["Pregnant", "Husband", "Mother-in-law", "Mom", "Dad", "Other"],
|
| 165 |
+
"btn_register": "📝 Register",
|
| 166 |
+
"btn_identify": "🔊 Identify",
|
| 167 |
+
"briefing_title": "#### 📋 Today's Briefing",
|
| 168 |
+
|
| 169 |
+
"tab_preferences": "🥗 Preferences",
|
| 170 |
+
"tab_recipes": "🍳 Recipes",
|
| 171 |
+
"tab_memory": "📖 Family Memory",
|
| 172 |
+
"pref_name": "Name",
|
| 173 |
+
"pref_role": "Role",
|
| 174 |
+
"pref_likes": "Likes",
|
| 175 |
+
"pref_likes_placeholder": "What do they like?",
|
| 176 |
+
"pref_avoid": "Dislikes",
|
| 177 |
+
"pref_avoid_placeholder": "What don't they eat?",
|
| 178 |
+
"pref_allergy": "Allergies",
|
| 179 |
+
"pref_allergy_placeholder": "Any allergies?",
|
| 180 |
+
"btn_save": "💾 Save",
|
| 181 |
+
"btn_refresh": "🔄 Refresh",
|
| 182 |
+
"recipe_name": "Dish Name",
|
| 183 |
+
"recipe_name_placeholder": "Tomato Beef Brisket",
|
| 184 |
+
"recipe_cook": "Cook",
|
| 185 |
+
"recipe_cook_placeholder": "Who makes it?",
|
| 186 |
+
"recipe_diff": "Difficulty",
|
| 187 |
+
"recipe_diff_choices": ["Easy", "Medium", "Hard"],
|
| 188 |
+
"recipe_ingredients": "Ingredients",
|
| 189 |
+
"recipe_ingredients_placeholder": "Main ingredients",
|
| 190 |
+
"btn_add": "💾 Add",
|
| 191 |
+
"memory_type": "Type",
|
| 192 |
+
"memory_type_choices": ["Relation", "Event", "Daily"],
|
| 193 |
+
"memory_content": "Content",
|
| 194 |
+
"memory_content_placeholder": "Record family relations, events or daily moments",
|
| 195 |
+
"btn_save_memory": "💾 Save",
|
| 196 |
+
|
| 197 |
+
"trimester_label": "Trimester",
|
| 198 |
+
"trimester_choices": ["1st Trimester", "2nd Trimester", "3rd Trimester"],
|
| 199 |
+
"btn_analyze": "📊 Analyze",
|
| 200 |
+
"analysis_result": "📋 Analysis Result",
|
| 201 |
+
"menu_suggestion": "💡 Menu Suggestions",
|
| 202 |
+
|
| 203 |
+
"analysis_days": "Analysis Days",
|
| 204 |
+
"btn_generate": "📊 Generate Report",
|
| 205 |
+
"report_text": "📋 Text Report",
|
| 206 |
+
"report_chart": "📊 Chart",
|
| 207 |
+
"btn_export_md": "📥 Export Markdown",
|
| 208 |
+
|
| 209 |
+
"lang_label": "🌐 Language",
|
| 210 |
+
"lang_zh": "中文",
|
| 211 |
+
"lang_en": "English",
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def t(key: str, lang: str = "zh") -> str:
|
| 216 |
+
"""翻译函数"""
|
| 217 |
+
d = ZH if lang == "zh" else EN
|
| 218 |
+
return d.get(key, key)
|