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 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:31:27.292878"
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:31:27.292878",
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:24:37 | 测试框架: 内置 TestRunner</p>
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:24:37.617014",
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 create_app(loop=None):
32
- """创建主应用"""
 
33
 
34
- with gr.Blocks(title="PregoPal - 孕期陪护AI助手") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- gr.Markdown(
37
- """
38
- # 🌸 PregoPal - 孕期陪护AI助手
39
- ### 一个温馨的家庭式孕期AI伴侣
40
- """
41
- )
 
 
 
42
 
43
- briefing = loop.get_briefing() if loop else {}
44
 
45
- # ==================== 页面 1: 语音交互 ====================
46
- with gr.Tab("🎙️ 语音交互"):
47
  with gr.Row():
48
- with gr.Column(scale=3):
49
- chatbot = gr.Chatbot(label="💬 对话", height=400)
50
- with gr.Row():
51
- msg = gr.Textbox(
52
- label="输入消息",
53
- placeholder="例如:我今天中午吃了番茄牛腩",
54
- scale=4
55
- )
56
- send_btn = gr.Button("发送", variant="primary", scale=1)
57
- with gr.Row():
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
- # ==================== 页面 2: 家庭饮食习惯 ====================
166
- with gr.Tab("👨‍👩‍👧‍👦 家庭饮食习惯"):
167
- with gr.Tabs():
168
- with gr.Tab("🥗 饮食偏好"):
169
- with gr.Row():
170
- with gr.Column():
171
- pref_name = gr.Textbox(label="姓名", placeholder="小明")
172
- pref_role = gr.Dropdown(
173
- label="身份",
174
- choices=["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"],
175
- value="孕妇"
176
- )
177
- pref_likes = gr.Textbox(label="偏好", placeholder="喜欢吃什么?", lines=1)
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(scale=1):
241
- trimester_select = gr.Radio(
242
- label="孕期阶段",
243
- choices=["孕早期", "孕中期", "孕晚期"],
244
- value="孕中期"
245
  )
246
- run_3day_btn = gr.Button("📊 运行分析", variant="primary")
247
-
248
- with gr.Column(scale=2):
249
- three_day_result = gr.Textbox(label="📋 分析结果", interactive=False, lines=15)
250
- menu_suggestion = gr.Textbox(label="💡 菜单改进建议", interactive=False, lines=4)
251
-
252
- def run_three_day_summary(trimester):
253
- from plugins.three_day_summary import ThreeDaySummaryPlugin
254
- from plugins.base import LoopContext
255
- import asyncio
256
-
257
- ctx = LoopContext()
258
- ctx.briefing["trimester"] = trimester
259
- asyncio.run(ThreeDaySummaryPlugin().run(ctx))
260
-
261
- report = ctx.briefing.get("three_day_summary", {}).get("report", "暂无数据")
262
- deficits = ctx.briefing.get("three_day_summary", {}).get("persistent_deficits", [])
263
-
264
- suggestions = []
265
- if deficits:
266
- suggestions.append("📋 建议增加以下食材到家庭菜单:")
267
- for n in deficits[:3]:
268
- foods = DRIsParser.RICH_FOODS.get(n, [])
269
- if foods:
270
- suggestions.append(f" • {n} → {'、'.join(foods[:4])}")
271
- else:
272
- suggestions.append("✅ 近三天营养状况良好!")
273
-
274
- return report, "\n".join(suggestions)
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
- # ==================== 页面 4: 营养报告 ====================
283
- with gr.Tab("📈 营养报告"):
284
- with gr.Row():
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
- with gr.Column(scale=2):
290
- report_text = gr.Textbox(label="📋 文本报告", interactive=False, lines=12)
 
 
291
 
292
- with gr.Row():
293
- report_plot = gr.Plot(label="📊 可视化图表")
 
294
 
295
- with gr.Row():
296
- export_md_btn = gr.Button("📥 导出 Markdown", size="sm")
297
- export_result = gr.Textbox(label="", interactive=False, lines=1)
298
-
299
- def generate_report(days):
300
- records = diet_logger.get_recent_records(days=int(days))
301
- analysis = nutrition_analyzer.analyze_diet(records)
302
- text_report = nutrition_analyzer.generate_report_text(analysis)
303
- chart = nutrition_analyzer.generate_report_chart(analysis)
304
- return text_report, chart
305
-
306
- def export_report(days):
307
- records = diet_logger.get_recent_records(days=int(days))
308
- analysis = nutrition_analyzer.analyze_diet(records)
309
- md_path = nutrition_analyzer.export_report_markdown(analysis)
310
- return f"✅ 已导出: {md_path.name}"
311
-
312
- generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=[report_text, report_plot])
313
- export_md_btn.click(fn=export_report, inputs=[analysis_days], outputs=export_result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- '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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)