Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能

This commit is contained in:
赵杰
2025-09-25 14:20:11 +01:00
commit aea5f6bf74
27 changed files with 14015 additions and 0 deletions

153
.gitignore vendored Normal file
View File

@@ -0,0 +1,153 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Project specific
data/app.db
data/app.db-journal
data/app.db-wal
data/app.db-shm
logs/*.log
models/*.pkl
models/*.joblib
data/users/
data/training/
*.pkl
*.joblib
# OCR cache
data/ocr_cache/
data/user_corrections/
data/food_images/
# Test files
test_image.jpg
test_*.py
*_test.py
# Temporary files
*.tmp
*.temp
temp/
tmp/
# Configuration files with sensitive data
.env.local
.env.production
config/app_config.json
# Documentation build
docs/build/

BIN
CN107633875A.pdf Normal file

Binary file not shown.

237
OCR_USAGE_GUIDE.md Normal file
View File

@@ -0,0 +1,237 @@
# OCR热量识别功能使用指南
## 功能概述
OCR热量识别功能允许用户通过拍摄或上传包含食物信息的图片自动识别其中的热量信息大大简化了餐食记录的过程。
## 主要特性
### 1. 多OCR引擎支持
- **Tesseract OCR**: 开源OCR引擎支持中英文识别
- **PaddleOCR**: 百度开源OCR对中文识别效果优秀
- **EasyOCR**: 简单易用的OCR库支持多语言
### 2. 智能验证机制
- **多级验证**: 结合OCR结果、食物数据库和用户学习数据
- **置信度评估**: 为每个识别结果提供置信度评分
- **用户修正**: 支持用户手动修正识别结果
### 3. 学习优化系统
- **用户反馈学习**: 记录用户修正,提高后续识别准确性
- **数据库匹配**: 与内置食物数据库进行智能匹配
- **模式识别**: 识别多种热量表示格式
## 使用方法
### 1. 启动OCR功能
#### 在移动端界面:
1. 打开应用,进入"记录"页面
2. 在食物输入框右侧找到"📷"按钮
3. 点击按钮打开OCR识别界面
#### 在桌面端界面:
1. 在主界面选择"OCR热量识别"功能
2. 或通过菜单栏访问OCR功能
### 2. 上传图片
1. 点击"选择图片"按钮
2. 选择包含食物信息的图片文件
3. 支持的格式JPG、JPEG、PNG、BMP、GIF
4. 图片将显示在预览区域
### 3. 开始识别
1. 确认图片选择正确后,点击"开始识别"按钮
2. 系统将使用多个OCR引擎进行识别
3. 识别过程中会显示进度条和状态信息
4. 识别完成后显示结果
### 4. 查看和编辑结果
#### 识别结果表格:
- **食物名称**: 识别到的食物名称
- **热量**: 识别到的热量数值(卡路里)
- **置信度**: 识别结果的置信度0-1
- **来源**: 数据来源OCR、数据库、用户确认
#### 详细信息:
- OCR识别过程详情
- 各引擎的识别结果
- 处理时间和整体置信度
#### 建议:
- 系统提供的改进建议
- 识别准确性提示
- 手动输入建议
### 5. 编辑和确认结果
#### 编辑结果:
1. 双击表格中的任意行或选择后点击"编辑结果"
2. 在弹出的对话框中修改食物名称、热量和置信度
3. 点击"保存"确认修改
#### 确认结果:
1. 检查所有识别结果是否正确
2. 点击"确认结果"按钮
3. 系统将保存到餐食记录中
## 识别准确性优化
### 1. 图片质量要求
#### 推荐条件:
- **清晰度**: 图片清晰,文字可读
- **对比度**: 文字与背景对比明显
- **角度**: 文字水平,避免倾斜
- **光照**: 光线充足,避免阴影
#### 避免的情况:
- 模糊不清的图片
- 文字过小或过大的图片
- 严重倾斜的图片
- 光线过暗或过亮的图片
### 2. 文字格式支持
#### 支持的热量表示格式:
- `130卡路里`
- `155 kcal`
- `52千卡`
- `42大卡`
- `110 KJ` (千焦)
- `76卡`
#### 支持的食物名称:
- 中文食物名称:米饭、鸡蛋、苹果等
- 英文食物名称rice、egg、apple等
- 混合格式:米饭 130卡路里
### 3. 提高识别准确性的技巧
#### 图片预处理:
- 确保图片中的文字清晰可见
- 避免复杂的背景干扰
- 保持文字区域的完整性
#### 结果验证:
- 仔细检查识别结果
- 及时修正错误信息
- 利用数据库匹配功能
#### 学习优化:
- 经常使用修正功能
- 系统会学习您的修正习惯
- 提高后续识别的准确性
## 故障排除
### 1. 常见问题
#### 识别失败:
- **原因**: 图片质量差、OCR引擎不可用
- **解决**: 检查图片质量确保OCR依赖已安装
#### 识别结果不准确:
- **原因**: 图片模糊、文字格式特殊
- **解决**: 重新拍摄清晰图片,手动修正结果
#### 无法打开OCR界面
- **原因**: 依赖包未安装、模块初始化失败
- **解决**: 检查requirements.txt中的依赖是否已安装
### 2. 依赖安装
确保已安装以下依赖包:
```bash
pip install pytesseract>=0.3.10
pip install opencv-python>=4.8.0
pip install paddleocr>=2.7.0
pip install easyocr>=1.7.0
```
#### Tesseract安装
- **Windows**: 下载Tesseract安装包并添加到PATH
- **macOS**: `brew install tesseract`
- **Linux**: `sudo apt-get install tesseract-ocr`
### 3. 性能优化
#### 提高识别速度:
- 使用较小的图片文件
- 选择清晰的图片
- 避免过于复杂的图片
#### 提高识别准确性:
- 使用标准格式的食物标签
- 保持文字清晰可读
- 及时修正错误结果
## 技术架构
### 1. 模块结构
```
modules/ocr_calorie_recognition.py # OCR识别模块
gui/ocr_calorie_gui.py # OCR GUI界面
test_ocr_system.py # 测试脚本
```
### 2. 核心组件
#### OCRCalorieRecognitionModule:
- 多OCR引擎集成
- 图片预处理
- 热量信息提取
- 数据库匹配
- 用户学习系统
#### OCRCalorieGUI:
- 图片上传界面
- 识别结果展示
- 结果编辑功能
- 用户交互处理
### 3. 数据流程
1. **图片上传** → 图片预处理
2. **OCR识别** → 多引擎并行识别
3. **文本提取** → 热量信息解析
4. **数据库匹配** → 食物信息验证
5. **用户确认** → 结果保存和学习
## 未来改进计划
### 1. 功能增强
- 支持更多图片格式
- 增加批量识别功能
- 支持手写文字识别
- 集成营养信息识别
### 2. 性能优化
- 优化识别算法
- 提高处理速度
- 减少内存占用
- 支持GPU加速
### 3. 用户体验
- 改进界面设计
- 增加语音输入
- 支持离线识别
- 提供更多个性化选项
## 联系支持
如果您在使用OCR功能时遇到问题
1. 查看本文档的故障排除部分
2. 运行测试脚本检查系统状态
3. 检查依赖包是否正确安装
4. 提供详细的错误信息和截图
---
*最后更新: 2024年12月*

193
PROJECT_SUMMARY.md Normal file
View File

@@ -0,0 +1,193 @@
# 个性化饮食推荐助手 - 项目完成总结
## 🎯 项目概述
基于您的需求,我已经完成了一个完整的个性化饮食推荐系统,具有以下核心特性:
### ✨ 核心功能
1. **5天数据采集** - 详细记录用户三餐数据
2. **智能问卷系统** - 收集用户偏好、生理信息、个性化因素
3. **大模型集成** - 深度理解用户需求,提供智能分析
4. **混合推荐系统** - 结合机器学习和AI的个性化推荐
5. **持续学习机制** - 根据用户反馈不断优化模型
6. **女性专属优化** - 考虑生理周期、排卵期等特殊因素
7. **现代化GUI** - 基于CustomTkinter的美观界面
## 🏗️ 基座架构设计
### 核心基座 (`core/base.py`)
- **BaseModule**: 所有功能模块的抽象基类
- **DataManager**: 统一的数据管理基座
- **EventBus**: 事件总线,支持模块间通信
- **ModuleManager**: 模块管理器,统一管理所有功能模块
- **AppCore**: 应用核心,协调所有模块
### 功能模块
1. **数据采集模块** (`modules/data_collection.py`)
- 问卷数据收集
- 餐食记录管理
- 用户反馈处理
2. **AI分析模块** (`modules/ai_analysis.py`)
- 用户意图分析
- 营养状况分析
- 生理状态分析
- 餐食建议生成
3. **推荐引擎模块** (`modules/recommendation_engine.py`)
- 基于历史数据的推荐
- 基于相似用户的推荐
- 基于内容相似性的推荐
- 基于生理状态的推荐
- 多维度融合推荐
4. **GUI界面模块** (`gui/main_window.py`)
- 现代化界面设计
- 数据采集界面
- AI分析界面
- 推荐系统界面
- 个人中心界面
## 🔧 技术特点
### 1. 基座架构优势
- **代码复用**: 所有模块基于统一基座,减少重复代码
- **模块化设计**: 每个功能独立,易于维护和扩展
- **统一接口**: 所有模块使用相同的接口规范
- **事件驱动**: 支持模块间松耦合通信
### 2. 大模型深度集成
- **用户意图理解**: 不仅理解表面需求,还分析深层意图
- **情绪状态分析**: 考虑用户当前情绪对饮食需求的影响
- **生理周期智能**: 专门针对女性的生理周期分析
- **个性化建议**: 结合星座、性格等多维度因素
### 3. 智能推荐系统
- **多维度融合**: 历史偏好 + 相似用户 + 内容相似性 + 生理状态
- **持续学习**: 根据用户反馈不断优化推荐算法
- **个性化过滤**: 考虑过敏、不喜欢等个人限制
- **置信度评估**: 为每个推荐提供置信度评分
### 4. 女性专属功能
- **生理周期跟踪**: 自动计算月经周期状态
- **营养需求调整**: 根据生理周期推荐不同营养素
- **情绪变化考虑**: 分析生理周期对情绪和食欲的影响
- **个性化建议**: 提供针对性的饮食建议
## 📁 项目结构
```
diet_recommendation_app/
├── core/ # 核心基座
│ └── base.py # 基础架构
├── modules/ # 功能模块
│ ├── data_collection.py # 数据采集
│ ├── ai_analysis.py # AI分析
│ └── recommendation_engine.py # 推荐引擎
├── gui/ # GUI界面
│ └── main_window.py # 主窗口
├── data/ # 数据存储
│ ├── users/ # 用户数据
│ └── training/ # 训练数据
├── models/ # 模型存储
├── logs/ # 日志文件
├── main.py # 主应用入口
├── start.py # 启动脚本
├── requirements.txt # 依赖包
├── .env.example # 配置示例
└── README.md # 项目说明
```
## 🚀 快速开始
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 配置环境
```bash
cp .env.example .env
# 编辑 .env 文件配置API密钥可选
```
### 3. 启动应用
```bash
python start.py
```
## 💡 使用流程
### 1. 用户注册/登录
- 输入用户ID和姓名
- 系统自动创建用户档案
### 2. 数据采集5天
- **基础信息问卷**: 年龄、性别、身高体重等
- **口味偏好问卷**: 甜、咸、辣、酸等偏好评分
- **生理信息问卷**: 月经周期、排卵期症状等
- **餐食记录**: 详细记录三餐内容和满意度
### 3. AI分析
- **用户意图分析**: 理解用户真实需求
- **营养分析**: 分析餐食营养状况
- **生理状态分析**: 分析当前生理周期状态
### 4. 个性化推荐
- **智能推荐**: 基于多维度因素生成推荐
- **用户反馈**: 收集用户对推荐的反馈
- **持续优化**: 根据反馈不断改进推荐算法
## 🔮 核心创新点
### 1. 基座架构设计
- 避免了"一个代码一个功能"的问题
- 统一的数据管理和事件处理
- 模块化设计,易于扩展和维护
### 2. 大模型深度集成
- 不仅用于营养分析,还用于用户需求理解
- 结合传统机器学习和大模型的优势
- 提供更智能、更个性化的服务
### 3. 女性专属优化
- 深度考虑生理周期对饮食需求的影响
- 结合星座、性格等个性化因素
- 提供更贴心的个性化服务
### 4. 持续学习机制
- 避免完全随机推荐的问题
- 根据用户反馈不断优化模型
- 提供越来越精准的推荐
## 🎉 项目完成度
**核心基座架构** - 完成
**数据采集系统** - 完成
**AI分析模块** - 完成
**推荐引擎** - 完成
**GUI界面** - 完成
**女性专属功能** - 完成
**持续学习机制** - 完成
**大模型集成** - 完成
## 🔧 后续扩展建议
1. **移动端适配**: 开发手机APP版本
2. **云端部署**: 支持多用户在线使用
3. **更多大模型**: 集成更多AI模型
4. **营养数据库**: 扩展更丰富的食物营养数据
5. **社交功能**: 添加用户交流和分享功能
---
**项目已完成,可以立即运行使用!** 🎊
所有功能都基于您提出的需求设计,特别是:
- ✅ 5天数据采集系统
- ✅ 大模型深度集成用于需求分析
- ✅ 女性生理周期智能优化
- ✅ 星座等个性化因素考虑
- ✅ 持续学习和模型矫正机制
- ✅ 基座架构避免代码重复
- ✅ 现代化桌面GUI界面

324
README.md Normal file
View File

@@ -0,0 +1,324 @@
# 🍽️ 个性化饮食推荐APP
一个基于机器学习和OCR技术的智能饮食推荐系统具有现代化的移动端界面和强大的AI分析能力。
## ✨ 最新特性
### 📷 OCR热量识别
- **多引擎OCR支持**Tesseract、PaddleOCR、EasyOCR
- **智能图片识别**:自动识别食物名称和热量信息
- **多级验证机制**OCR结果 + 数据库匹配 + 用户确认
- **学习优化系统**:从用户修正中持续学习改进
- **一键记录**:拍照即可完成餐食记录
### 🎨 现代化界面设计
- **圆角设计系统**:统一的圆角主题,减少方形元素
- **多色主题支持**:主色、次色、强调色等多种配色
- **移动端适配**:专为手机屏幕优化的界面布局
- **卡片式设计**:清晰的信息层次和视觉反馈
- **响应式交互**:悬停效果和状态变化
## 🎯 核心功能
### 1. 智能数据采集
- **OCR图片识别**:📷 拍照识别食物热量信息
- **5天三餐数据记录**:详细记录早中晚三餐的饮食内容
- **用户偏好问卷**:口味偏好、饮食习惯、过敏信息等
- **生理周期跟踪**:针对女性的生理期、排卵期等特殊时期
- **个性化因素**:星座、性格特征等参考因素
### 2. 机器学习推荐引擎
- **个人饮食模型训练**:基于用户历史数据训练个性化模型
- **持续学习机制**:根据用户反馈不断优化推荐
- **偏好矫正**:用户不喜欢或已食用食物的反馈学习
- **多因素融合**:结合生理周期、星座等多维度因素
### 3. AI大模型分析
- **千问大模型集成**:智能营养分析和建议生成
- **热量分析**:每日饮食热量计算和评估
- **营养建议**:基于大模型的智能营养建议
- **个性化指导**:结合用户特征的定制化建议
### 4. 现代化应用界面
- **移动端设计**:模拟小程序/安卓App的界面体验
- **圆角美化**:统一的圆角设计语言
- **多色主题**:丰富的颜色搭配和视觉层次
- **实时推荐**:动态推荐系统
- **数据可视化**:饮食趋势、营养分析图表
## 🏗️ 技术架构
### 核心基座
- **统一基座架构**:模块化设计,支持功能扩展
- **SQLite数据库**:轻量级本地数据存储
- **事件总线系统**:模块间通信和事件处理
- **数据管理器**:统一的数据访问接口
### OCR识别技术
- **Tesseract OCR**开源OCR引擎支持中英文
- **PaddleOCR**百度开源OCR中文识别优秀
- **EasyOCR**简单易用的多语言OCR库
- **OpenCV**:图像预处理和增强
- **PIL/Pillow**:图像处理和格式转换
### 机器学习
- **scikit-learn**:推荐算法实现
- **pandas/numpy**:数据处理和分析
- **joblib**:模型序列化和加载
- **TF-IDF向量化**:文本特征提取
### AI大模型集成
- **千问大模型**阿里云通义千问API
- **智能分析**:用户意图分析和营养建议
- **个性化推理**:基于用户特征的智能推荐
### 现代化界面
- **CustomTkinter**现代化Python GUI框架
- **移动端设计**375x812像素模拟手机界面
- **圆角美化系统**:统一的视觉设计语言
- **多色主题**:丰富的颜色配置系统
## 📁 项目结构
```
diet_recommendation_app/
├── core/ # 核心基座架构
│ ├── base.py # 基础类和配置
│ └── base_engine.py # 基础引擎抽象类
├── modules/ # 功能模块
│ ├── data_collection.py # 数据采集模块
│ ├── ai_analysis.py # AI分析模块
│ ├── recommendation_engine.py # 推荐引擎模块
│ └── ocr_calorie_recognition.py # OCR热量识别模块
├── gui/ # 现代化界面
│ ├── mobile_main_window.py # 移动端主界面
│ ├── ocr_calorie_gui.py # OCR识别界面
│ ├── styles.py # 样式配置系统
│ └── main_window.py # 桌面端界面
├── llm_integration/ # 大模型集成
│ └── qwen_client.py # 千问大模型客户端
├── smart_food/ # 智能食物数据库
│ └── smart_database.py # 食物数据库管理
├── data/ # 数据存储
│ ├── users/ # 用户数据
│ ├── training/ # 训练数据
│ └── app.db # SQLite数据库
├── models/ # 机器学习模型
├── logs/ # 日志文件
├── main.py # 应用入口
├── requirements.txt # 依赖包列表
├── OCR_USAGE_GUIDE.md # OCR使用指南
├── UI_BEAUTIFICATION_SUMMARY.md # 界面美化总结
└── README.md # 项目说明文档
```
## 🚀 快速开始
### 1. 环境要求
- Python 3.8+
- Windows/macOS/Linux
### 2. 安装依赖
```bash
# 克隆项目
git clone <repository-url>
cd diet_recommendation_app
# 安装基础依赖
pip install -r requirements.txt
# 安装OCR依赖可选
pip install pytesseract opencv-python paddleocr easyocr
```
### 3. OCR引擎配置可选
#### Tesseract安装
- **Windows**: 下载 [Tesseract安装包](https://github.com/UB-Mannheim/tesseract/wiki)
- **macOS**: `brew install tesseract`
- **Linux**: `sudo apt-get install tesseract-ocr`
#### 其他OCR引擎
```bash
# PaddleOCR推荐中文识别效果好
pip install paddleocr
# EasyOCR简单易用
pip install easyocr
```
### 4. 配置环境
```bash
# 创建环境配置文件
echo "QWEN_API_KEY=your_qwen_api_key_here" > .env
```
### 5. 运行应用
```bash
python main.py
```
### 6. 使用OCR功能
1. 打开应用,进入"记录"页面
2. 点击食物输入框右侧的"📷"按钮
3. 选择包含食物信息的图片
4. 点击"开始识别"进行OCR识别
5. 查看和编辑识别结果
6. 确认保存到餐食记录
## 🔄 工作流程
### 1. 智能数据采集5天
- **OCR图片识别**:📷 拍照识别食物热量信息
- **手动数据记录**:详细记录三餐饮食内容
- **用户偏好问卷**:口味偏好、饮食习惯、过敏信息
- **个性化画像**:生理周期、星座、性格特征
### 2. AI模型训练
- **个人推荐模型**:基于采集数据训练个性化模型
- **多因素融合**:结合生理期、星座等多维度因素
- **OCR学习优化**:从用户修正中持续改进识别准确性
### 3. 智能推荐与学习
- **个性化推荐**第6天开始提供智能推荐
- **用户反馈收集**:喜欢/不喜欢/已食用反馈
- **持续模型优化**:基于反馈数据不断改进
- **OCR准确性提升**:学习用户修正习惯
### 4. AI智能分析
- **千问大模型分析**:每日营养状况智能分析
- **个性化健康建议**:基于用户特征的定制建议
- **营养趋势分析**:长期饮食模式分析
## 🎨 特色功能
### 📷 OCR智能识别
- **多引擎支持**Tesseract、PaddleOCR、EasyOCR
- **智能验证**OCR + 数据库 + 用户确认三级验证
- **学习优化**:从用户修正中持续改进
- **一键记录**:拍照即可完成餐食记录
### 🎨 现代化界面
- **移动端设计**375x812像素模拟手机界面
- **圆角美化**:统一的圆角设计语言
- **多色主题**:丰富的颜色搭配和视觉层次
- **响应式交互**:悬停效果和状态变化
### 🤖 AI智能分析
- **千问大模型**阿里云通义千问API集成
- **智能推理**:用户意图分析和营养建议
- **个性化指导**:基于用户特征的定制建议
### 👩 女性专属优化
- **生理周期智能调整**:月经期、排卵期等特殊时期
- **营养需求分析**:不同生理阶段的营养需求
- **个性化建议**:结合生理周期的饮食建议
### ⭐ 个性化因素
- **星座参考**:个性化因素融合
- **性格特征**:基于性格的饮食偏好分析
- **持续学习**:避免随机推荐问题
## 📚 文档说明
- **[OCR使用指南](OCR_USAGE_GUIDE.md)**详细的OCR功能使用说明
- **[界面美化总结](UI_BEAUTIFICATION_SUMMARY.md)**:界面设计和技术实现
- **[项目总结](PROJECT_SUMMARY.md)**:项目整体架构和功能说明
## 🧪 测试验证
### 功能测试
```bash
# 测试OCR系统
python test_ocr_system.py
# 测试界面美化
python test_ui_beautification.py
# 测试核心功能
python test_core.py
```
### 应用启动测试
```bash
# 启动应用
python main.py
# 检查日志
tail -f logs/app.log
```
## 🚀 技术亮点
### 1. 统一基座架构
- **模块化设计**:所有功能模块基于统一基座构建
- **事件驱动**:模块间通过事件总线通信
- **数据统一**:统一的数据管理和访问接口
- **扩展性强**:支持新功能模块的快速集成
### 2. OCR多引擎融合
- **多引擎并行**同时使用多个OCR引擎提高准确性
- **智能合并**:基于置信度的结果合并策略
- **学习优化**:从用户修正中持续学习改进
- **数据库匹配**:结合食物数据库进行智能验证
### 3. 现代化界面设计
- **移动端优先**:专为手机屏幕优化的界面设计
- **圆角美化**:统一的圆角设计语言
- **多色主题**:丰富的颜色配置和视觉层次
- **响应式交互**:流畅的用户交互体验
### 4. AI智能分析
- **千问大模型**集成阿里云通义千问API
- **智能推理**:基于用户数据的智能分析
- **个性化建议**:结合多维度因素的定制建议
- **持续学习**:从用户反馈中不断优化
## 📈 项目状态
-**核心功能**数据采集、推荐引擎、AI分析
-**OCR识别**:多引擎支持、智能验证、学习优化
-**界面美化**:圆角设计、多色主题、移动端适配
-**文档完善**:使用指南、技术文档、测试脚本
- 🔄 **持续优化**:性能提升、功能扩展、用户体验改进
## 🤝 贡献指南
欢迎提交Issue和Pull Request来改进项目
### 开发环境设置
```bash
# 克隆项目
git clone <repository-url>
cd diet_recommendation_app
# 安装开发依赖
pip install -r requirements.txt
# 运行测试
python test_ocr_system.py
python test_ui_beautification.py
```
### 代码规范
- 遵循PEP 8代码风格
- 添加适当的注释和文档字符串
- 编写单元测试
- 更新相关文档
## 📄 许可证
本项目采用MIT许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
## 🙏 致谢
感谢以下开源项目的支持:
- [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) - 现代化Python GUI框架
- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) - 开源OCR引擎
- [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR) - 百度开源OCR
- [EasyOCR](https://github.com/JaidedAI/EasyOCR) - 简单易用的OCR库
- [scikit-learn](https://scikit-learn.org/) - 机器学习库
---
**🍽️ 让AI为您的饮食健康保驾护航**

View File

@@ -0,0 +1,213 @@
# 界面美化总结
## 美化成果
我已经成功为饮食推荐应用进行了全面的界面美化,主要改进包括:
### 1. 圆角设计系统
#### 圆角半径配置
- **小圆角**: 8px - 用于小按钮和输入框
- **中圆角**: 12px - 用于标准组件
- **大圆角**: 15px - 用于主要按钮和卡片
- **超大圆角**: 20px - 用于页面容器
- **极大圆角**: 25px - 用于主框架
#### 应用范围
- ✅ 主容器和页面容器
- ✅ 状态栏和导航栏
- ✅ 卡片式框架
- ✅ 按钮组件
- ✅ 输入框组件
- ✅ 标签组件
### 2. 颜色主题系统
#### 主要颜色
- **主色调**: #3498db (蓝色)
- **次要色**: #2ecc71 (绿色)
- **强调色**: #e74c3c (红色)
- **警告色**: #f39c12 (橙色)
- **信息色**: #9b59b6 (紫色)
#### 背景色系
- **浅色背景**: #ffffff (白色)
- **深色背景**: #2b2b2b (深灰)
- **卡片背景**: #f8f9fa (浅灰)
- **容器背景**: #f0f0f0 (更浅灰)
#### 文字颜色
- **主要文字**: #2c3e50 (深蓝灰)
- **次要文字**: #34495e (中蓝灰)
- **辅助文字**: #7f8c8d (浅灰)
### 3. 组件美化
#### 按钮美化
- **圆角按钮**: 15px圆角半径
- **悬停效果**: 颜色渐变
- **多色主题**: 支持主色、次色、强调色等
- **图标按钮**: 支持emoji图标
#### 输入框美化
- **圆角输入框**: 12px圆角半径
- **边框设计**: 1px细边框
- **背景色**: 浅色背景
- **占位符**: 友好的提示文字
#### 卡片设计
- **卡片式布局**: 20px圆角半径
- **阴影效果**: 边框阴影
- **内边距**: 统一的内边距设计
- **层次感**: 多层级卡片结构
### 4. 布局优化
#### 间距系统
- **小间距**: 5px
- **标准间距**: 10px
- **中等间距**: 15px
- **大间距**: 20px
- **超大间距**: 25px
#### 字体系统
- **标题字体**: Arial 22px 粗体
- **副标题**: Arial 18px 粗体
- **正文字体**: Arial 14px
- **小字体**: Arial 12px
- **微小字体**: Arial 10px
### 5. 页面美化详情
#### 移动端主界面
- **主容器**: 20px圆角浅色背景
- **页面容器**: 25px圆角白色背景边框阴影
- **状态栏**: 15px圆角透明背景
- **导航栏**: 20px圆角白色背景边框设计
#### 首页美化
- **欢迎区域**: 25px圆角浅色背景
- **用户卡片**: 20px圆角白色背景
- **快速操作**: 20px圆角浅色背景
- **按钮设计**: 15px圆角多色主题
#### 记录页面美化
- **餐次选择**: 15px圆角白色背景
- **食物输入**: 15px圆角白色背景
- **输入框**: 12px圆角浅色背景
- **功能按钮**: 12px圆角彩色主题
#### OCR界面美化
- **标题**: 18px字体深色文字
- **上传区域**: 实线边框,浅色背景
- **控制区域**: 实线边框,浅色背景
- **按钮**: 强调色主题
### 6. 技术实现
#### 样式配置系统
- **StyleConfig类**: 统一管理颜色、圆角、字体、间距
- **预设样式**: 预定义的样式组合
- **工具函数**: 快速创建美化组件的函数
#### 组件工厂
- **create_rounded_frame()**: 创建圆角框架
- **create_accent_button()**: 创建强调色按钮
- **create_rounded_entry()**: 创建圆角输入框
- **create_card_frame()**: 创建卡片式框架
#### 主题应用
- **apply_rounded_theme()**: 应用圆角主题
- **apply_preset_style()**: 应用预设样式
- **颜色管理**: 统一的颜色配置系统
### 7. 用户体验提升
#### 视觉改进
- ✅ 减少方形设计,增加圆角元素
- ✅ 统一的颜色主题
- ✅ 清晰的层次结构
- ✅ 友好的视觉反馈
#### 交互改进
- ✅ 悬停效果
- ✅ 按钮状态变化
- ✅ 平滑的视觉过渡
- ✅ 直观的图标使用
#### 移动端适配
- ✅ 适合手机屏幕的尺寸
- ✅ 触摸友好的按钮大小
- ✅ 清晰的文字显示
- ✅ 合理的间距布局
### 8. 测试验证
#### 功能测试
- ✅ 样式配置测试通过
- ✅ 预设样式测试通过
- ✅ 圆角主题测试通过
- ✅ 组件创建测试通过
#### 界面测试
- ✅ 移动端界面美化完成
- ✅ OCR界面美化完成
- ✅ 按钮和输入框美化完成
- ✅ 卡片式布局美化完成
## 文件结构
```
gui/
├── styles.py # 样式配置系统
├── mobile_main_window.py # 移动端主界面(已美化)
├── ocr_calorie_gui.py # OCR界面已美化
└── main_window.py # 桌面端界面
test_ui_beautification.py # 界面美化测试脚本
UI_BEAUTIFICATION_SUMMARY.md # 美化总结文档
```
## 使用说明
### 应用美化样式
```python
from gui.styles import apply_rounded_theme, create_card_frame, create_accent_button
# 应用圆角主题
apply_rounded_theme()
# 创建美化组件
card = create_card_frame(parent)
button = create_accent_button(parent, "按钮文字", color_type='primary')
```
### 自定义样式
```python
from gui.styles import StyleConfig
# 使用预定义颜色
color = StyleConfig.COLORS['primary']
# 使用预定义圆角
radius = StyleConfig.CORNER_RADIUS['large']
# 使用预定义字体
font = StyleConfig.FONTS['title']
```
## 总结
通过这次界面美化,我们实现了:
1. **统一的圆角设计系统** - 所有组件都采用圆角设计,减少方形元素
2. **完整的颜色主题** - 统一的颜色配置,支持多色主题
3. **优化的用户体验** - 更友好的视觉设计和交互体验
4. **可维护的代码结构** - 模块化的样式配置系统
5. **全面的测试验证** - 确保美化效果正常工作
界面现在具有现代化的外观,符合移动端应用的设计趋势,为用户提供更好的使用体验。
---
*美化完成时间: 2024年12月*

425
config/settings.py Normal file
View File

@@ -0,0 +1,425 @@
"""
统一配置管理 - 所有接口、配置、SQL入口的集中管理
"""
import os
import sqlite3
from pathlib import Path
from typing import Optional, Dict, Any
from dataclasses import dataclass
import logging
# 尝试加载环境变量
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
# 如果没有.env文件或加载失败使用默认配置
pass
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class DatabaseConfig:
"""数据库配置"""
# 数据库路径
database_path: str = "data/app.db"
database_url: str = "sqlite:///./data/app.db"
# 数据库连接参数
connection_timeout: int = 30
check_same_thread: bool = False
# 表配置
enable_foreign_keys: bool = True
enable_wal_mode: bool = True
@dataclass
class APIConfig:
"""API接口配置"""
# 千问大模型配置
qwen_api_key: Optional[str] = None
qwen_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1"
qwen_model: str = "qwen-plus-latest"
qwen_temperature: float = 0.7
qwen_max_tokens: int = 2000
# OpenAI配置备用
openai_api_key: Optional[str] = None
openai_model: str = "gpt-4"
# Anthropic配置备用
anthropic_api_key: Optional[str] = None
anthropic_model: str = "claude-3-sonnet-20240229"
# API超时配置
api_timeout: int = 30
api_retry_count: int = 3
@dataclass
class AppConfig:
"""应用配置"""
# 应用基本信息
app_name: str = "个性化饮食推荐助手"
version: str = "1.0.0"
debug: bool = True
# 路径配置
model_path: str = "models/"
log_path: str = "logs/"
data_path: str = "data/"
user_data_path: str = "data/users/"
training_data_path: str = "data/training/"
# 日志配置
log_level: str = "INFO"
log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
log_file: str = "logs/app.log"
@dataclass
class MLConfig:
"""机器学习配置"""
# 推荐系统配置
max_recommendations: int = 5
min_training_samples: int = 10
model_update_threshold: int = 50
# 模型配置
model_save_format: str = "joblib"
enable_model_caching: bool = True
model_cache_size: int = 100
# 特征工程配置
tfidf_max_features: int = 1000
tfidf_ngram_range: tuple = (1, 2)
similarity_threshold: float = 0.7
@dataclass
class OCRConfig:
"""OCR识别配置"""
# OCR引擎配置
enable_tesseract: bool = True
enable_paddleocr: bool = True
enable_easyocr: bool = True
# 识别参数
min_confidence: float = 0.6
max_processing_time: float = 30.0
# 图片处理配置
image_max_size: tuple = (1920, 1080)
image_quality: int = 95
enable_image_preprocessing: bool = True
@dataclass
class UIConfig:
"""界面配置"""
# 移动端界面配置
mobile_width: int = 375
mobile_height: int = 812
# 主题配置
theme_mode: str = "light" # light, dark, system
color_theme: str = "blue"
# 圆角配置
corner_radius_small: int = 8
corner_radius_medium: int = 12
corner_radius_large: int = 15
corner_radius_xlarge: int = 20
corner_radius_xxlarge: int = 25
class UnifiedConfig:
"""统一配置管理类"""
def __init__(self):
self.database = DatabaseConfig()
self.api = APIConfig()
self.app = AppConfig()
self.ml = MLConfig()
self.ocr = OCRConfig()
self.ui = UIConfig()
# 从环境变量加载配置
self._load_from_env()
# 创建必要目录
self._create_directories()
def _load_from_env(self):
"""从环境变量加载配置"""
# 数据库配置
if os.getenv('DATABASE_PATH'):
self.database.database_path = os.getenv('DATABASE_PATH')
# API配置
self.api.qwen_api_key = os.getenv('QWEN_API_KEY')
self.api.qwen_base_url = os.getenv('QWEN_BASE_URL', self.api.qwen_base_url)
self.api.qwen_model = os.getenv('QWEN_MODEL', self.api.qwen_model)
self.api.openai_api_key = os.getenv('OPENAI_API_KEY')
self.api.anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')
# 应用配置
if os.getenv('DEBUG'):
self.app.debug = os.getenv('DEBUG').lower() == 'true'
if os.getenv('LOG_LEVEL'):
self.app.log_level = os.getenv('LOG_LEVEL')
# ML配置
if os.getenv('MAX_RECOMMENDATIONS'):
self.ml.max_recommendations = int(os.getenv('MAX_RECOMMENDATIONS'))
if os.getenv('MIN_TRAINING_SAMPLES'):
self.ml.min_training_samples = int(os.getenv('MIN_TRAINING_SAMPLES'))
def _create_directories(self):
"""创建必要的目录"""
directories = [
self.app.data_path,
self.app.user_data_path,
self.app.training_data_path,
self.app.model_path,
self.app.log_path,
Path(self.database.database_path).parent
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
def get_database_connection(self) -> sqlite3.Connection:
"""获取数据库连接"""
try:
conn = sqlite3.connect(
self.database.database_path,
timeout=self.database.connection_timeout,
check_same_thread=self.database.check_same_thread
)
# 启用外键约束
if self.database.enable_foreign_keys:
conn.execute("PRAGMA foreign_keys = ON")
# 启用WAL模式
if self.database.enable_wal_mode:
conn.execute("PRAGMA journal_mode = WAL")
return conn
except Exception as e:
logger.error(f"数据库连接失败: {e}")
raise
def get_api_config(self, provider: str = "qwen") -> Dict[str, Any]:
"""获取API配置"""
if provider == "qwen":
return {
"api_key": self.api.qwen_api_key,
"base_url": self.api.qwen_base_url,
"model": self.api.qwen_model,
"temperature": self.api.qwen_temperature,
"max_tokens": self.api.qwen_max_tokens,
"timeout": self.api.api_timeout,
"retry_count": self.api.api_retry_count
}
elif provider == "openai":
return {
"api_key": self.api.openai_api_key,
"model": self.api.openai_model,
"timeout": self.api.api_timeout,
"retry_count": self.api.api_retry_count
}
elif provider == "anthropic":
return {
"api_key": self.api.anthropic_api_key,
"model": self.api.anthropic_model,
"timeout": self.api.api_timeout,
"retry_count": self.api.api_retry_count
}
else:
raise ValueError(f"不支持的API提供商: {provider}")
def is_api_available(self, provider: str = "qwen") -> bool:
"""检查API是否可用"""
if provider == "qwen":
return self.api.qwen_api_key is not None
elif provider == "openai":
return self.api.openai_api_key is not None
elif provider == "anthropic":
return self.api.anthropic_api_key is not None
else:
return False
def get_ocr_config(self) -> Dict[str, Any]:
"""获取OCR配置"""
return {
"enable_tesseract": self.ocr.enable_tesseract,
"enable_paddleocr": self.ocr.enable_paddleocr,
"enable_easyocr": self.ocr.enable_easyocr,
"min_confidence": self.ocr.min_confidence,
"max_processing_time": self.ocr.max_processing_time,
"image_max_size": self.ocr.image_max_size,
"image_quality": self.ocr.image_quality,
"enable_preprocessing": self.ocr.enable_image_preprocessing
}
def get_ui_config(self) -> Dict[str, Any]:
"""获取界面配置"""
return {
"mobile_width": self.ui.mobile_width,
"mobile_height": self.ui.mobile_height,
"theme_mode": self.ui.theme_mode,
"color_theme": self.ui.color_theme,
"corner_radius": {
"small": self.ui.corner_radius_small,
"medium": self.ui.corner_radius_medium,
"large": self.ui.corner_radius_large,
"xlarge": self.ui.corner_radius_xlarge,
"xxlarge": self.ui.corner_radius_xxlarge
}
}
def validate_config(self) -> bool:
"""验证配置有效性"""
try:
# 检查数据库路径
db_path = Path(self.database.database_path)
if not db_path.parent.exists():
db_path.parent.mkdir(parents=True, exist_ok=True)
# 检查API配置
if not self.is_api_available("qwen"):
logger.warning("千问API密钥未配置部分功能可能不可用")
# 检查目录权限
for directory in [self.app.data_path, self.app.model_path, self.app.log_path]:
if not Path(directory).exists():
Path(directory).mkdir(parents=True, exist_ok=True)
logger.info("配置验证通过")
return True
except Exception as e:
logger.error(f"配置验证失败: {e}")
return False
def save_config_to_file(self, file_path: str = "config/app_config.json"):
"""保存配置到文件"""
import json
config_dict = {
"database": self.database.__dict__,
"api": self.api.__dict__,
"app": self.app.__dict__,
"ml": self.ml.__dict__,
"ocr": self.ocr.__dict__,
"ui": self.ui.__dict__
}
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(config_dict, f, ensure_ascii=False, indent=2)
logger.info(f"配置已保存到: {file_path}")
def load_config_from_file(self, file_path: str = "config/app_config.json"):
"""从文件加载配置"""
import json
if not Path(file_path).exists():
logger.warning(f"配置文件不存在: {file_path}")
return False
try:
with open(file_path, 'r', encoding='utf-8') as f:
config_dict = json.load(f)
# 更新配置
if "database" in config_dict:
for key, value in config_dict["database"].items():
if hasattr(self.database, key):
setattr(self.database, key, value)
if "api" in config_dict:
for key, value in config_dict["api"].items():
if hasattr(self.api, key):
setattr(self.api, key, value)
# 其他配置类似处理...
logger.info(f"配置已从文件加载: {file_path}")
return True
except Exception as e:
logger.error(f"加载配置文件失败: {e}")
return False
# 全局配置实例
_config_instance: Optional[UnifiedConfig] = None
def get_config() -> UnifiedConfig:
"""获取全局配置实例"""
global _config_instance
if _config_instance is None:
_config_instance = UnifiedConfig()
_config_instance.validate_config()
return _config_instance
def reload_config():
"""重新加载配置"""
global _config_instance
_config_instance = None
return get_config()
# 便捷函数
def get_database_connection() -> sqlite3.Connection:
"""获取数据库连接"""
return get_config().get_database_connection()
def get_api_config(provider: str = "qwen") -> Dict[str, Any]:
"""获取API配置"""
return get_config().get_api_config(provider)
def is_api_available(provider: str = "qwen") -> bool:
"""检查API是否可用"""
return get_config().is_api_available(provider)
if __name__ == "__main__":
# 测试配置系统
print("=== 统一配置管理测试 ===")
config = get_config()
print(f"✅ 应用名称: {config.app.app_name}")
print(f"✅ 数据库路径: {config.database.database_path}")
print(f"✅ 千问API可用: {config.is_api_available('qwen')}")
print(f"✅ OpenAI API可用: {config.is_api_available('openai')}")
# 测试数据库连接
try:
conn = config.get_database_connection()
print("✅ 数据库连接成功")
conn.close()
except Exception as e:
print(f"❌ 数据库连接失败: {e}")
# 保存配置
config.save_config_to_file()
print("✅ 配置系统测试完成")

620
core/base.py Normal file
View File

@@ -0,0 +1,620 @@
"""
核心基座架构 - Diet Recommendation App
统一的基座设计,支持所有功能模块的扩展
"""
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any, Union, Callable
from dataclasses import dataclass, field
import json
import sqlite3
import logging
from datetime import datetime, date
from pathlib import Path
import asyncio
from enum import Enum
import threading
from queue import Queue
import os
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
# 如果没有.env文件或加载失败使用默认配置
pass
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ModuleType(Enum):
"""模块类型枚举"""
DATA_COLLECTION = "data_collection"
USER_ANALYSIS = "user_analysis"
RECOMMENDATION = "recommendation"
GUI_INTERFACE = "gui_interface"
NOTIFICATION = "notification"
@dataclass
class BaseConfig:
"""基础配置类"""
app_name: str = "个性化饮食推荐助手"
version: str = "1.0.0"
debug: bool = True
database_path: str = "data/app.db"
model_path: str = "models/"
log_level: str = "INFO"
# API配置
qwen_api_key: Optional[str] = None
qwen_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1"
qwen_model: str = "qwen-plus-latest"
# 用户配置
max_recommendations: int = 5
min_training_samples: int = 10
model_update_threshold: int = 50
@dataclass
class UserData:
"""统一用户数据结构"""
user_id: str
profile: Dict[str, Any] = field(default_factory=dict)
meals: List[Dict[str, Any]] = field(default_factory=list)
feedback: List[Dict[str, Any]] = field(default_factory=list)
preferences: Dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
@dataclass
class AnalysisResult:
"""统一分析结果结构"""
module_type: ModuleType
user_id: str
input_data: Any
result: Dict[str, Any]
confidence: float
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
metadata: Dict[str, Any] = field(default_factory=dict)
class BaseModule(ABC):
"""基础模块抽象类"""
def __init__(self, config: BaseConfig, module_type: ModuleType):
self.config = config
self.module_type = module_type
self.is_initialized = False
self.logger = logging.getLogger(f"{self.__class__.__name__}")
@abstractmethod
def initialize(self) -> bool:
"""初始化模块"""
pass
@abstractmethod
def process(self, input_data: Any, user_data: UserData) -> AnalysisResult:
"""处理数据"""
pass
@abstractmethod
def cleanup(self) -> bool:
"""清理资源"""
pass
def is_ready(self) -> bool:
"""检查模块是否就绪"""
return self.is_initialized
class DataManager:
"""数据管理基座"""
def __init__(self, config: BaseConfig):
self.config = config
self.db_path = Path(config.database_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init_database()
def _init_database(self):
"""初始化数据库"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY,
data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 分析结果表
cursor.execute('''
CREATE TABLE IF NOT EXISTS analysis_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
module_type TEXT,
input_data TEXT,
result TEXT,
confidence REAL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
metadata TEXT,
FOREIGN KEY (user_id) REFERENCES users (user_id)
)
''')
# 系统配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def save_user_data(self, user_data: UserData) -> bool:
"""保存用户数据"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO users (user_id, data, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
''', (user_data.user_id, json.dumps(user_data.__dict__)))
conn.commit()
conn.close()
return True
except Exception as e:
logger.error(f"保存用户数据失败: {e}")
return False
def get_user_data(self, user_id: str) -> Optional[UserData]:
"""获取用户数据"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 获取用户基本信息
cursor.execute('SELECT data FROM users WHERE user_id = ?', (user_id,))
result = cursor.fetchone()
if not result:
conn.close()
return None
# 解析用户基本信息
data_dict = json.loads(result[0])
# 获取餐食记录
cursor.execute('''
SELECT date, meal_type, foods, quantities, calories, satisfaction_score, food_items
FROM meal_records
WHERE user_id = ?
ORDER BY date DESC
''', (user_id,))
meal_rows = cursor.fetchall()
meals = []
for row in meal_rows:
meal = {
'date': row[0],
'meal_type': row[1],
'foods': json.loads(row[2]) if row[2] else [],
'quantities': json.loads(row[3]) if row[3] else [],
'calories': row[4],
'satisfaction_score': row[5],
'food_items': json.loads(row[6]) if row[6] else []
}
meals.append(meal)
# 获取反馈记录
cursor.execute('''
SELECT date, recommended_foods, user_choice, feedback_type
FROM feedback_records
WHERE user_id = ?
ORDER BY date DESC
''', (user_id,))
feedback_rows = cursor.fetchall()
feedback = []
for row in feedback_rows:
fb = {
'date': row[0],
'recommended_foods': json.loads(row[1]) if row[1] else [],
'user_choice': row[2],
'feedback_type': row[3]
}
feedback.append(fb)
# 获取问卷数据
cursor.execute('''
SELECT questionnaire_type, answers
FROM questionnaire_records
WHERE user_id = ?
''', (user_id,))
questionnaire_rows = cursor.fetchall()
preferences = {}
for row in questionnaire_rows:
preferences[row[0]] = json.loads(row[1]) if row[1] else {}
conn.close()
# 构建完整的用户数据
user_data = UserData(
user_id=data_dict['user_id'],
profile=data_dict.get('profile', {}),
meals=meals,
feedback=feedback,
preferences=preferences,
created_at=data_dict.get('created_at', ''),
updated_at=data_dict.get('updated_at', '')
)
return user_data
except Exception as e:
logger.error(f"获取用户数据失败: {e}")
return None
def save_analysis_result(self, result: AnalysisResult) -> bool:
"""保存分析结果"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO analysis_results
(user_id, module_type, input_data, result, confidence, metadata)
VALUES (?, ?, ?, ?, ?, ?)
''', (
result.user_id,
result.module_type.value,
json.dumps(result.input_data),
json.dumps(result.result),
result.confidence,
json.dumps(result.metadata)
))
conn.commit()
conn.close()
return True
except Exception as e:
logger.error(f"保存分析结果失败: {e}")
return False
def get_analysis_history(self, user_id: str, module_type: Optional[ModuleType] = None,
limit: int = 10) -> List[AnalysisResult]:
"""获取分析历史"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if module_type:
cursor.execute('''
SELECT module_type, input_data, result, confidence, timestamp, metadata
FROM analysis_results
WHERE user_id = ? AND module_type = ?
ORDER BY timestamp DESC
LIMIT ?
''', (user_id, module_type.value, limit))
else:
cursor.execute('''
SELECT module_type, input_data, result, confidence, timestamp, metadata
FROM analysis_results
WHERE user_id = ?
ORDER BY timestamp DESC
LIMIT ?
''', (user_id, limit))
results = cursor.fetchall()
conn.close()
analysis_results = []
for row in results:
result = AnalysisResult(
module_type=ModuleType(row[0]),
user_id=user_id,
input_data=json.loads(row[1]),
result=json.loads(row[2]),
confidence=row[3],
timestamp=row[4],
metadata=json.loads(row[5])
)
analysis_results.append(result)
return analysis_results
except Exception as e:
logger.error(f"获取分析历史失败: {e}")
return []
class EventBus:
"""事件总线基座"""
def __init__(self):
self.subscribers: Dict[str, List[Callable]] = {}
self.event_queue = Queue()
self.is_running = False
self.worker_thread = None
def subscribe(self, event_type: str, callback: Callable):
"""订阅事件"""
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(callback)
def unsubscribe(self, event_type: str, callback: Callable):
"""取消订阅"""
if event_type in self.subscribers:
self.subscribers[event_type].remove(callback)
def publish(self, event_type: str, data: Any):
"""发布事件"""
self.event_queue.put((event_type, data))
def start(self):
"""启动事件总线"""
self.is_running = True
self.worker_thread = threading.Thread(target=self._process_events)
self.worker_thread.start()
def stop(self):
"""停止事件总线"""
self.is_running = False
if self.worker_thread:
self.worker_thread.join()
def _process_events(self):
"""处理事件"""
while self.is_running:
try:
event_type, data = self.event_queue.get(timeout=1)
if event_type in self.subscribers:
for callback in self.subscribers[event_type]:
try:
callback(data)
except Exception as e:
logger.error(f"事件处理失败: {e}")
self.event_queue.task_done()
except:
continue
class ModuleManager:
"""模块管理器基座"""
def __init__(self, config: BaseConfig):
self.config = config
self.modules: Dict[ModuleType, BaseModule] = {}
self.data_manager = DataManager(config)
self.event_bus = EventBus()
self.is_initialized = False
def register_module(self, module: BaseModule) -> bool:
"""注册模块"""
try:
# 检查模块是否已经注册
if module.module_type in self.modules:
logger.warning(f"模块 {module.module_type.value} 已经注册,跳过重复注册")
return True
if module.initialize():
self.modules[module.module_type] = module
logger.info(f"模块 {module.module_type.value} 注册成功")
return True
else:
logger.error(f"模块 {module.module_type.value} 初始化失败")
return False
except Exception as e:
logger.error(f"注册模块失败: {e}")
return False
def process_request(self, module_type: ModuleType, input_data: Any,
user_id: str) -> Optional[AnalysisResult]:
"""处理请求"""
if module_type not in self.modules:
logger.error(f"模块 {module_type.value} 未注册")
return None
module = self.modules[module_type]
if not module.is_ready():
logger.error(f"模块 {module_type.value} 未就绪")
return None
# 获取用户数据
user_data = self.data_manager.get_user_data(user_id)
if not user_data:
logger.error(f"用户 {user_id} 数据不存在")
return None
try:
# 处理请求
result = module.process(input_data, user_data)
# 保存结果
self.data_manager.save_analysis_result(result)
# 发布事件
self.event_bus.publish(f"{module_type.value}_completed", result)
return result
except Exception as e:
logger.error(f"处理请求失败: {e}")
return None
def get_module_status(self) -> Dict[str, bool]:
"""获取模块状态"""
return {module_type.value: module.is_ready()
for module_type, module in self.modules.items()}
def initialize_all(self) -> bool:
"""初始化所有模块"""
try:
self.event_bus.start()
self.is_initialized = True
logger.info("模块管理器初始化完成")
return True
except Exception as e:
logger.error(f"初始化失败: {e}")
return False
def cleanup_all(self) -> bool:
"""清理所有模块"""
try:
self.event_bus.stop()
for module in self.modules.values():
module.cleanup()
self.is_initialized = False
logger.info("模块管理器清理完成")
return True
except Exception as e:
logger.error(f"清理失败: {e}")
return False
class AppCore:
"""应用核心基座"""
def __init__(self, config: BaseConfig):
self.config = config
self.module_manager = ModuleManager(config)
self.data_manager = DataManager(config)
self.is_running = False
def start(self) -> bool:
"""启动应用"""
try:
if self.module_manager.initialize_all():
self.is_running = True
logger.info("应用启动成功")
return True
else:
logger.error("应用启动失败")
return False
except Exception as e:
logger.error(f"启动应用失败: {e}")
return False
def stop(self) -> bool:
"""停止应用"""
try:
if self.module_manager.cleanup_all():
self.is_running = False
logger.info("应用停止成功")
return True
else:
logger.error("应用停止失败")
return False
except Exception as e:
logger.error(f"停止应用失败: {e}")
return False
def create_user(self, user_id: str, initial_data: Dict[str, Any]) -> bool:
"""创建用户"""
user_data = UserData(
user_id=user_id,
profile=initial_data.get('profile', {}),
preferences=initial_data.get('preferences', {})
)
return self.module_manager.data_manager.save_user_data(user_data)
def process_user_request(self, module_type: ModuleType, input_data: Any,
user_id: str) -> Optional[AnalysisResult]:
"""处理用户请求"""
return self.module_manager.process_request(module_type, input_data, user_id)
def get_user_data(self, user_id: str) -> Optional[UserData]:
"""获取用户数据"""
return self.module_manager.data_manager.get_user_data(user_id)
def get_analysis_history(self, user_id: str, module_type: Optional[ModuleType] = None) -> List[AnalysisResult]:
"""获取分析历史"""
return self.module_manager.data_manager.get_analysis_history(user_id, module_type)
# 全局应用实例
app_core: Optional[AppCore] = None
def get_app_core() -> AppCore:
"""获取应用核心实例"""
global app_core
if app_core is None:
config = BaseConfig()
app_core = AppCore(config)
return app_core
def initialize_app(config: Optional[BaseConfig] = None) -> bool:
"""初始化应用"""
global app_core
if config is None:
config = BaseConfig()
app_core = AppCore(config)
return app_core.start()
def cleanup_app() -> bool:
"""清理应用"""
global app_core
if app_core:
return app_core.stop()
return True
if __name__ == "__main__":
# 测试基座架构
print("测试核心基座架构...")
# 初始化应用
if initialize_app():
print("✅ 应用初始化成功")
# 创建测试用户
test_user_id = "test_user_001"
initial_data = {
"profile": {
"name": "测试用户",
"age": 25,
"gender": ""
},
"preferences": {
"taste": "sweet",
"diet": "balanced"
}
}
app = get_app_core()
if app.create_user(test_user_id, initial_data):
print("✅ 用户创建成功")
# 获取用户数据
user_data = app.get_user_data(test_user_id)
if user_data:
print(f"✅ 用户数据获取成功: {user_data.user_id}")
# 清理应用
cleanup_app()
print("✅ 应用清理完成")
else:
print("❌ 应用初始化失败")
print("基座架构测试完成!")

928
core/base_engine.py Normal file
View File

@@ -0,0 +1,928 @@
"""
核心基座架构 - 个性化饮食推荐系统
提供统一的基础设施和接口,所有功能模块都基于此基座构建
"""
import json
import sqlite3
import asyncio
import logging
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any, Union
from dataclasses import dataclass, asdict
from datetime import datetime, date
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class UserProfile:
"""用户画像 - 统一数据结构"""
user_id: str
name: str
age: int
gender: str
height: float
weight: float
activity_level: str
# 偏好和限制
taste_preferences: Dict[str, int] = None
dietary_preferences: List[str] = None
allergies: List[str] = None
dislikes: List[str] = None
# 生理信息
is_female: bool = False
menstrual_cycle_length: Optional[int] = None
last_period_date: Optional[str] = None
# 个性化因素
zodiac_sign: Optional[str] = None
personality_traits: List[str] = None
health_goals: List[str] = None
def __post_init__(self):
if self.taste_preferences is None:
self.taste_preferences = {}
if self.dietary_preferences is None:
self.dietary_preferences = []
if self.allergies is None:
self.allergies = []
if self.dislikes is None:
self.dislikes = []
if self.personality_traits is None:
self.personality_traits = []
if self.health_goals is None:
self.health_goals = []
@dataclass
class MealRecord:
"""餐食记录 - 统一数据结构"""
user_id: str
date: str
meal_type: str # breakfast, lunch, dinner
foods: List[str]
quantities: List[str]
calories: Optional[float] = None
satisfaction_score: Optional[int] = None
notes: Optional[str] = None
@dataclass
class RecommendationResult:
"""推荐结果 - 统一数据结构"""
user_id: str
date: str
meal_type: str
recommended_foods: List[str]
reasoning: str
confidence_score: float
special_considerations: List[str] = None
def __post_init__(self):
if self.special_considerations is None:
self.special_considerations = []
class BaseEngine(ABC):
"""基础引擎抽象类 - 所有功能模块的基座"""
def __init__(self, config: Dict[str, Any] = None):
self.config = config or {}
self.logger = logging.getLogger(self.__class__.__name__)
self._initialized = False
@abstractmethod
async def initialize(self) -> bool:
"""初始化引擎"""
pass
@abstractmethod
async def process(self, data: Any) -> Any:
"""处理数据"""
pass
@abstractmethod
async def cleanup(self) -> bool:
"""清理资源"""
pass
def is_initialized(self) -> bool:
"""检查是否已初始化"""
return self._initialized
def get_config(self, key: str, default: Any = None) -> Any:
"""获取配置"""
return self.config.get(key, default)
class DataManager(BaseEngine):
"""数据管理基座 - 统一的数据存储和访问接口"""
def __init__(self, db_path: str = "data/app.db"):
super().__init__()
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
async def initialize(self) -> bool:
"""初始化数据库"""
try:
await self._create_tables()
self._initialized = True
self.logger.info("数据管理器初始化成功")
return True
except Exception as e:
self.logger.error(f"数据管理器初始化失败: {e}")
return False
async def _create_tables(self):
"""创建数据库表"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY,
profile_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 餐食记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS meals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
date TEXT,
meal_type TEXT,
foods TEXT,
quantities TEXT,
calories REAL,
satisfaction_score INTEGER,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (user_id)
)
''')
# 推荐记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS recommendations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
date TEXT,
meal_type TEXT,
recommended_foods TEXT,
reasoning TEXT,
confidence_score REAL,
special_considerations TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (user_id)
)
''')
# 用户反馈表
cursor.execute('''
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
date TEXT,
recommendation_id INTEGER,
user_choice TEXT,
feedback_type TEXT,
satisfaction_score INTEGER,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (user_id),
FOREIGN KEY (recommendation_id) REFERENCES recommendations (id)
)
''')
conn.commit()
conn.close()
async def process(self, operation: str, data: Any) -> Any:
"""处理数据操作"""
if not self._initialized:
await self.initialize()
operations = {
'save_user': self._save_user_profile,
'get_user': self._get_user_profile,
'save_meal': self._save_meal_record,
'get_meals': self._get_meal_records,
'save_recommendation': self._save_recommendation,
'get_recommendations': self._get_recommendations,
'save_feedback': self._save_feedback,
'get_feedback': self._get_feedback
}
if operation in operations:
return await operations[operation](data)
else:
raise ValueError(f"不支持的操作: {operation}")
async def _save_user_profile(self, profile: UserProfile) -> bool:
"""保存用户画像"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO users (user_id, profile_data, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
''', (profile.user_id, json.dumps(asdict(profile))))
conn.commit()
conn.close()
return True
except Exception as e:
self.logger.error(f"保存用户画像失败: {e}")
return False
async def _get_user_profile(self, user_id: str) -> Optional[UserProfile]:
"""获取用户画像"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT profile_data FROM users WHERE user_id = ?', (user_id,))
result = cursor.fetchone()
conn.close()
if result:
profile_dict = json.loads(result[0])
return UserProfile(**profile_dict)
return None
except Exception as e:
self.logger.error(f"获取用户画像失败: {e}")
return None
async def _save_meal_record(self, meal: MealRecord) -> bool:
"""保存餐食记录"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO meals (user_id, date, meal_type, foods, quantities,
calories, satisfaction_score, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
meal.user_id, meal.date, meal.meal_type,
json.dumps(meal.foods), json.dumps(meal.quantities),
meal.calories, meal.satisfaction_score, meal.notes
))
conn.commit()
conn.close()
return True
except Exception as e:
self.logger.error(f"保存餐食记录失败: {e}")
return False
async def _get_meal_records(self, params: Dict[str, Any]) -> List[MealRecord]:
"""获取餐食记录"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
user_id = params.get('user_id')
days = params.get('days', 5)
cursor.execute('''
SELECT user_id, date, meal_type, foods, quantities, calories,
satisfaction_score, notes
FROM meals
WHERE user_id = ?
ORDER BY date DESC, meal_type
LIMIT ?
''', (user_id, days * 3))
results = cursor.fetchall()
conn.close()
meals = []
for row in results:
meal = MealRecord(
user_id=row[0],
date=row[1],
meal_type=row[2],
foods=json.loads(row[3]),
quantities=json.loads(row[4]),
calories=row[5],
satisfaction_score=row[6],
notes=row[7]
)
meals.append(meal)
return meals
except Exception as e:
self.logger.error(f"获取餐食记录失败: {e}")
return []
async def _save_recommendation(self, recommendation: RecommendationResult) -> int:
"""保存推荐结果"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO recommendations (user_id, date, meal_type, recommended_foods,
reasoning, confidence_score, special_considerations)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
recommendation.user_id, recommendation.date, recommendation.meal_type,
json.dumps(recommendation.recommended_foods), recommendation.reasoning,
recommendation.confidence_score, json.dumps(recommendation.special_considerations)
))
recommendation_id = cursor.lastrowid
conn.commit()
conn.close()
return recommendation_id
except Exception as e:
self.logger.error(f"保存推荐结果失败: {e}")
return -1
async def _get_recommendations(self, params: Dict[str, Any]) -> List[RecommendationResult]:
"""获取推荐记录"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
user_id = params.get('user_id')
days = params.get('days', 7)
cursor.execute('''
SELECT user_id, date, meal_type, recommended_foods, reasoning,
confidence_score, special_considerations
FROM recommendations
WHERE user_id = ?
ORDER BY date DESC
LIMIT ?
''', (user_id, days))
results = cursor.fetchall()
conn.close()
recommendations = []
for row in results:
rec = RecommendationResult(
user_id=row[0],
date=row[1],
meal_type=row[2],
recommended_foods=json.loads(row[3]),
reasoning=row[4],
confidence_score=row[5],
special_considerations=json.loads(row[6]) if row[6] else []
)
recommendations.append(rec)
return recommendations
except Exception as e:
self.logger.error(f"获取推荐记录失败: {e}")
return []
async def _save_feedback(self, feedback_data: Dict[str, Any]) -> bool:
"""保存用户反馈"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO feedback (user_id, date, recommendation_id, user_choice,
feedback_type, satisfaction_score, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
feedback_data['user_id'], feedback_data['date'],
feedback_data.get('recommendation_id'), feedback_data['user_choice'],
feedback_data['feedback_type'], feedback_data.get('satisfaction_score'),
feedback_data.get('notes')
))
conn.commit()
conn.close()
return True
except Exception as e:
self.logger.error(f"保存用户反馈失败: {e}")
return False
async def _get_feedback(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
"""获取用户反馈"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
user_id = params.get('user_id')
days = params.get('days', 7)
cursor.execute('''
SELECT user_id, date, recommendation_id, user_choice, feedback_type,
satisfaction_score, notes
FROM feedback
WHERE user_id = ?
ORDER BY date DESC
LIMIT ?
''', (user_id, days))
results = cursor.fetchall()
conn.close()
feedbacks = []
for row in results:
feedback = {
'user_id': row[0],
'date': row[1],
'recommendation_id': row[2],
'user_choice': row[3],
'feedback_type': row[4],
'satisfaction_score': row[5],
'notes': row[6]
}
feedbacks.append(feedback)
return feedbacks
except Exception as e:
self.logger.error(f"获取用户反馈失败: {e}")
return []
async def cleanup(self) -> bool:
"""清理资源"""
self._initialized = False
return True
class AIAnalyzer(BaseEngine):
"""AI分析基座 - 统一的大模型分析接口"""
def __init__(self, config: Dict[str, Any] = None):
super().__init__(config)
self.openai_client = None
self.anthropic_client = None
async def initialize(self) -> bool:
"""初始化AI客户端"""
try:
import openai
import anthropic
self.openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
self.anthropic_client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self._initialized = True
self.logger.info("AI分析器初始化成功")
return True
except Exception as e:
self.logger.error(f"AI分析器初始化失败: {e}")
return False
async def process(self, analysis_type: str, data: Any) -> Any:
"""处理AI分析请求"""
if not self._initialized:
await self.initialize()
analysis_types = {
'user_intent': self._analyze_user_intent,
'physiological_state': self._analyze_physiological_state,
'nutrition_analysis': self._analyze_nutrition,
'recommendation_reasoning': self._generate_reasoning
}
if analysis_type in analysis_types:
return await analysis_types[analysis_type](data)
else:
raise ValueError(f"不支持的分析类型: {analysis_type}")
async def _analyze_user_intent(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""分析用户意图"""
user_input = data.get('user_input', '')
context = data.get('context', {})
prompt = f"""
请分析用户的真实意图和需求:
用户输入: "{user_input}"
用户背景: {json.dumps(context, ensure_ascii=False)}
请分析:
1. 真实意图
2. 情绪状态
3. 营养需求
4. 推荐理由
返回JSON格式结果。
"""
try:
response = await self.openai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是专业的营养师和心理分析师。"},
{"role": "user", "content": prompt}
],
temperature=0.3,
max_tokens=500
)
result_text = response.choices[0].message.content
return self._parse_json_result(result_text)
except Exception as e:
self.logger.error(f"用户意图分析失败: {e}")
return {"intent": "需要饮食建议", "confidence": 0.3}
async def _analyze_physiological_state(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""分析生理状态"""
profile = data.get('profile', {})
current_date = data.get('current_date', '')
if not profile.get('is_female', False):
return {"state": "normal", "needs": []}
# 计算生理周期
cycle_info = self._calculate_cycle_state(profile, current_date)
prompt = f"""
作为女性健康专家,分析用户的生理状态:
用户信息: {json.dumps(profile, ensure_ascii=False)}
当前日期: {current_date}
生理周期: {json.dumps(cycle_info, ensure_ascii=False)}
请分析营养需求和饮食建议。
"""
try:
response = await self.openai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是专业的女性健康专家。"},
{"role": "user", "content": prompt}
],
temperature=0.2,
max_tokens=400
)
result_text = response.choices[0].message.content
result = self._parse_json_result(result_text)
result['cycle_info'] = cycle_info
return result
except Exception as e:
self.logger.error(f"生理状态分析失败: {e}")
return {"state": "normal", "needs": [], "cycle_info": cycle_info}
async def _analyze_nutrition(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""营养分析"""
foods = data.get('foods', [])
quantities = data.get('quantities', [])
prompt = f"""
请分析以下食物的营养成分:
食物: {', '.join(foods)}
数量: {', '.join(quantities)}
请分析:
1. 总热量
2. 主要营养素
3. 营养均衡性
4. 改进建议
返回JSON格式结果。
"""
try:
response = await self.openai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是专业的营养师。"},
{"role": "user", "content": prompt}
],
temperature=0.2,
max_tokens=400
)
result_text = response.choices[0].message.content
return self._parse_json_result(result_text)
except Exception as e:
self.logger.error(f"营养分析失败: {e}")
return {"calories": 0, "analysis": "分析失败"}
async def _generate_reasoning(self, data: Dict[str, Any]) -> str:
"""生成推荐理由"""
recommendations = data.get('recommendations', [])
user_profile = data.get('user_profile', {})
prompt = f"""
请为以下推荐生成个性化理由:
推荐食物: {', '.join(recommendations)}
用户画像: {json.dumps(user_profile, ensure_ascii=False)}
请生成简洁明了的推荐理由。
"""
try:
response = await self.openai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是专业的营养师。"},
{"role": "user", "content": prompt}
],
temperature=0.3,
max_tokens=200
)
return response.choices[0].message.content
except Exception as e:
self.logger.error(f"生成推荐理由失败: {e}")
return "基于您的个人偏好和营养需求推荐"
def _calculate_cycle_state(self, profile: Dict[str, Any], current_date: str) -> Dict[str, Any]:
"""计算生理周期状态"""
try:
last_period = datetime.strptime(profile.get('last_period_date', ''), '%Y-%m-%d')
current = datetime.strptime(current_date, '%Y-%m-%d')
cycle_length = profile.get('menstrual_cycle_length', 28)
days_since_period = (current - last_period).days
days_to_next_period = cycle_length - (days_since_period % cycle_length)
if days_since_period % cycle_length < 5:
phase = "月经期"
elif days_since_period % cycle_length < 14:
phase = "卵泡期"
elif days_since_period % cycle_length < 18:
phase = "排卵期"
else:
phase = "黄体期"
return {
"phase": phase,
"days_since_period": days_since_period % cycle_length,
"days_to_next_period": days_to_next_period,
"is_ovulation": phase == "排卵期"
}
except Exception:
return {
"phase": "未知",
"days_since_period": 0,
"days_to_next_period": 0,
"is_ovulation": False
}
def _parse_json_result(self, text: str) -> Dict[str, Any]:
"""解析JSON结果"""
try:
start_idx = text.find('{')
end_idx = text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = text[start_idx:end_idx]
return json.loads(json_str)
except Exception as e:
self.logger.error(f"解析JSON结果失败: {e}")
return {}
async def cleanup(self) -> bool:
"""清理资源"""
self._initialized = False
return True
class RecommendationEngine(BaseEngine):
"""推荐引擎基座 - 统一的推荐算法接口"""
def __init__(self, config: Dict[str, Any] = None):
super().__init__(config)
self.data_manager = None
self.ai_analyzer = None
self.food_database = {}
async def initialize(self) -> bool:
"""初始化推荐引擎"""
try:
# 初始化依赖组件
self.data_manager = DataManager()
await self.data_manager.initialize()
self.ai_analyzer = AIAnalyzer()
await self.ai_analyzer.initialize()
# 加载食物数据库
await self._load_food_database()
self._initialized = True
self.logger.info("推荐引擎初始化成功")
return True
except Exception as e:
self.logger.error(f"推荐引擎初始化失败: {e}")
return False
async def process(self, request_type: str, data: Any) -> Any:
"""处理推荐请求"""
if not self._initialized:
await self.initialize()
request_types = {
'generate_recommendation': self._generate_recommendation,
'update_model': self._update_model,
'get_food_info': self._get_food_info
}
if request_type in request_types:
return await request_types[request_type](data)
else:
raise ValueError(f"不支持的请求类型: {request_type}")
async def _generate_recommendation(self, data: Dict[str, Any]) -> RecommendationResult:
"""生成推荐"""
user_id = data.get('user_id')
user_input = data.get('user_input', '')
current_date = data.get('current_date', datetime.now().strftime('%Y-%m-%d'))
meal_type = data.get('meal_type', 'lunch')
# 获取用户数据
user_profile = await self.data_manager.process('get_user', user_id)
meal_history = await self.data_manager.process('get_meals', {'user_id': user_id, 'days': 5})
if not user_profile:
raise ValueError(f"用户 {user_id} 不存在")
# AI分析用户意图
intent_analysis = await self.ai_analyzer.process('user_intent', {
'user_input': user_input,
'context': asdict(user_profile)
})
# AI分析生理状态
physiological_analysis = await self.ai_analyzer.process('physiological_state', {
'profile': asdict(user_profile),
'current_date': current_date
})
# 生成推荐食物
recommended_foods = await self._select_foods(
user_profile, intent_analysis, physiological_analysis
)
# 生成推荐理由
reasoning = await self.ai_analyzer.process('recommendation_reasoning', {
'recommendations': recommended_foods,
'user_profile': asdict(user_profile)
})
# 创建推荐结果
recommendation = RecommendationResult(
user_id=user_id,
date=current_date,
meal_type=meal_type,
recommended_foods=recommended_foods,
reasoning=reasoning,
confidence_score=intent_analysis.get('confidence', 0.5),
special_considerations=physiological_analysis.get('considerations', [])
)
# 保存推荐结果
await self.data_manager.process('save_recommendation', recommendation)
return recommendation
async def _select_foods(self, user_profile: UserProfile,
intent_analysis: Dict[str, Any],
physiological_analysis: Dict[str, Any]) -> List[str]:
"""选择推荐食物"""
# 基础食物池
base_foods = [
"米饭", "面条", "馒头", "包子", "饺子",
"鸡蛋", "豆腐", "鱼肉", "鸡肉", "瘦肉",
"青菜", "西红柿", "胡萝卜", "土豆", "西兰花",
"苹果", "香蕉", "橙子", "葡萄", "草莓",
"牛奶", "酸奶", "豆浆", "坚果", "红枣"
]
# 根据用户偏好过滤
filtered_foods = []
for food in base_foods:
if not any(dislike in food for dislike in user_profile.dislikes):
if not any(allergy in food for allergy in user_profile.allergies):
filtered_foods.append(food)
# 根据生理需求调整
physiological_needs = physiological_analysis.get('needs', [])
priority_foods = []
for need in physiological_needs:
if need == "铁质":
priority_foods.extend(["菠菜", "瘦肉", "红枣"])
elif need == "蛋白质":
priority_foods.extend(["鸡蛋", "豆腐", "鱼肉"])
elif need == "维生素C":
priority_foods.extend(["橙子", "柠檬", "西红柿"])
# 合并推荐
recommended = list(set(priority_foods + filtered_foods))[:5]
return recommended
async def _update_model(self, data: Dict[str, Any]) -> bool:
"""更新模型"""
# 这里可以实现机器学习模型的更新逻辑
user_id = data.get('user_id')
feedback_data = await self.data_manager.process('get_feedback', {'user_id': user_id, 'days': 30})
# 基于反馈数据更新推荐策略
self.logger.info(f"更新用户 {user_id} 的推荐模型")
return True
async def _get_food_info(self, food_name: str) -> Dict[str, Any]:
"""获取食物信息"""
return self.food_database.get(food_name, {})
async def _load_food_database(self):
"""加载食物数据库"""
# 这里可以从文件或API加载食物营养信息
self.food_database = {
"米饭": {"calories": 130, "protein": 2.7, "carbs": 28},
"鸡蛋": {"calories": 155, "protein": 13, "fat": 11},
"豆腐": {"calories": 76, "protein": 8, "carbs": 2},
# 更多食物数据...
}
async def cleanup(self) -> bool:
"""清理资源"""
if self.data_manager:
await self.data_manager.cleanup()
if self.ai_analyzer:
await self.ai_analyzer.cleanup()
self._initialized = False
return True
if __name__ == "__main__":
# 测试基座架构
async def test_base_engine():
print("测试基座架构...")
# 测试数据管理器
data_manager = DataManager()
await data_manager.initialize()
# 创建测试用户
test_user = UserProfile(
user_id="test_001",
name="测试用户",
age=25,
gender="",
height=165.0,
weight=55.0,
activity_level="moderate",
taste_preferences={"sweet": 4, "salty": 3},
is_female=True,
zodiac_sign="天秤座"
)
# 保存用户
await data_manager.process('save_user', test_user)
print("用户保存成功")
# 获取用户
retrieved_user = await data_manager.process('get_user', "test_001")
if retrieved_user:
print(f"获取用户: {retrieved_user.name}")
# 测试推荐引擎
rec_engine = RecommendationEngine()
await rec_engine.initialize()
# 生成推荐
recommendation = await rec_engine.process('generate_recommendation', {
'user_id': 'test_001',
'user_input': '我今天想吃点清淡的',
'meal_type': 'lunch'
})
print(f"推荐结果: {recommendation.recommended_foods}")
print(f"推荐理由: {recommendation.reasoning}")
# 清理资源
await data_manager.cleanup()
await rec_engine.cleanup()
print("测试完成")
# 运行测试
asyncio.run(test_base_engine())

1504
gui/main_window.py Normal file

File diff suppressed because it is too large Load Diff

1359
gui/mobile_main_window.py Normal file

File diff suppressed because it is too large Load Diff

625
gui/new_main_window.py Normal file
View File

@@ -0,0 +1,625 @@
"""
新的界面设计 - 信息录入/修改 + 随机转盘/扭蛋机
基于用户需求重新设计的界面
"""
import tkinter as tk
from tkinter import messagebox
import customtkinter as ctk
from typing import Dict, List, Optional, Any
import random
import math
import threading
import time
from datetime import datetime
import json
# 设置CustomTkinter主题
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
class SpinWheel(ctk.CTkCanvas):
"""随机转盘/扭蛋机组件"""
def __init__(self, parent, width=300, height=300, **kwargs):
super().__init__(parent, width=width, height=height, **kwargs)
self.width = width
self.height = height
self.center_x = width // 2
self.center_y = height // 2
self.radius = min(width, height) // 2 - 20
# 转盘状态
self.is_spinning = False
self.current_angle = 0
self.spin_speed = 0
self.target_angle = 0
# 转盘选项
self.options = [
{"text": "早餐推荐", "color": "#FF6B6B", "value": "breakfast"},
{"text": "午餐推荐", "color": "#4ECDC4", "value": "lunch"},
{"text": "晚餐推荐", "color": "#45B7D1", "value": "dinner"},
{"text": "健康建议", "color": "#96CEB4", "value": "health"},
{"text": "营养分析", "color": "#FFEAA7", "value": "nutrition"},
{"text": "运动建议", "color": "#DDA0DD", "value": "exercise"}
]
# 绑定点击事件
self.bind("<Button-1>", self._on_click)
# 绘制转盘
self._draw_wheel()
def _draw_wheel(self):
"""绘制转盘"""
self.delete("all")
# 绘制转盘背景
self.create_oval(
self.center_x - self.radius,
self.center_y - self.radius,
self.center_x + self.radius,
self.center_y + self.radius,
fill="#2B2B2B",
outline="#FFFFFF",
width=3
)
# 绘制扇形区域
angle_per_section = 360 / len(self.options)
for i, option in enumerate(self.options):
start_angle = i * angle_per_section + self.current_angle
end_angle = (i + 1) * angle_per_section + self.current_angle
# 绘制扇形
self.create_arc(
self.center_x - self.radius + 10,
self.center_y - self.radius + 10,
self.center_x + self.radius - 10,
self.center_y + self.radius - 10,
start=start_angle,
extent=angle_per_section,
fill=option["color"],
outline="#FFFFFF",
width=2
)
# 绘制文字
text_angle = start_angle + angle_per_section / 2
text_radius = self.radius * 0.7
text_x = self.center_x + text_radius * math.cos(math.radians(text_angle))
text_y = self.center_y + text_radius * math.sin(math.radians(text_angle))
self.create_text(
text_x, text_y,
text=option["text"],
fill="#FFFFFF",
font=("Arial", 10, "bold"),
angle=text_angle
)
# 绘制中心圆
self.create_oval(
self.center_x - 20,
self.center_y - 20,
self.center_x + 20,
self.center_y + 20,
fill="#FF6B6B",
outline="#FFFFFF",
width=2
)
# 绘制指针
pointer_length = self.radius - 30
pointer_x = self.center_x + pointer_length * math.cos(math.radians(self.current_angle))
pointer_y = self.center_y + pointer_length * math.sin(math.radians(self.current_angle))
self.create_line(
self.center_x, self.center_y,
pointer_x, pointer_y,
fill="#FFFFFF",
width=4
)
def _on_click(self, event):
"""点击转盘开始旋转"""
if not self.is_spinning:
self.spin()
def spin(self):
"""开始旋转"""
if self.is_spinning:
return
self.is_spinning = True
self.spin_speed = random.uniform(15, 25) # 初始速度
self.target_angle = random.uniform(720, 1440) # 随机旋转角度
self._animate_spin()
def _animate_spin(self):
"""动画旋转"""
if not self.is_spinning:
return
# 更新角度
self.current_angle += self.spin_speed
self.current_angle %= 360
# 减速
self.spin_speed *= 0.95
# 重绘转盘
self._draw_wheel()
# 检查是否停止
if self.spin_speed < 0.1:
self.is_spinning = False
self._on_spin_complete()
else:
self.after(50, self._animate_spin)
def _on_spin_complete(self):
"""旋转完成回调"""
# 计算选中的选项
angle_per_section = 360 / len(self.options)
selected_index = int(self.current_angle // angle_per_section)
selected_option = self.options[selected_index]
# 触发回调
if hasattr(self, 'on_spin_complete'):
self.on_spin_complete(selected_option)
class UserInfoForm(ctk.CTkFrame):
"""用户信息录入/修改表单"""
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.user_data = {}
self._create_widgets()
def _create_widgets(self):
"""创建表单组件"""
# 标题
title_label = ctk.CTkLabel(
self,
text="📝 个人信息管理",
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(pady=20)
# 表单框架
form_frame = ctk.CTkScrollableFrame(self)
form_frame.pack(fill="both", expand=True, padx=20, pady=10)
# 基本信息
self._create_basic_info_section(form_frame)
# 健康信息
self._create_health_info_section(form_frame)
# 饮食偏好
self._create_diet_preferences_section(form_frame)
# 按钮区域
self._create_buttons(form_frame)
def _create_basic_info_section(self, parent):
"""创建基本信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="👤 基本信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 信息网格
info_frame = ctk.CTkFrame(section_frame)
info_frame.pack(fill="x", padx=20, pady=10)
# 姓名
name_label = ctk.CTkLabel(info_frame, text="姓名:")
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.name_var = tk.StringVar()
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 年龄
age_label = ctk.CTkLabel(info_frame, text="年龄:")
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.age_var = tk.StringVar(value="25")
age_entry = ctk.CTkEntry(info_frame, textvariable=self.age_var, width=100)
age_entry.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 性别
gender_label = ctk.CTkLabel(info_frame, text="性别:")
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.gender_var = tk.StringVar(value="")
gender_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.gender_var,
values=["", ""]
)
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 身高体重
height_label = ctk.CTkLabel(info_frame, text="身高(cm):")
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.height_var = tk.StringVar(value="165")
height_entry = ctk.CTkEntry(info_frame, textvariable=self.height_var, width=100)
height_entry.grid(row=1, column=3, sticky="w", padx=10, pady=5)
weight_label = ctk.CTkLabel(info_frame, text="体重(kg):")
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.weight_var = tk.StringVar(value="55")
weight_entry = ctk.CTkEntry(info_frame, textvariable=self.weight_var, width=100)
weight_entry.grid(row=2, column=1, sticky="w", padx=10, pady=5)
def _create_health_info_section(self, parent):
"""创建健康信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="🏥 健康信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 健康信息网格
health_frame = ctk.CTkFrame(section_frame)
health_frame.pack(fill="x", padx=20, pady=10)
# 活动水平
activity_label = ctk.CTkLabel(health_frame, text="活动水平:")
activity_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.activity_var = tk.StringVar(value="中等")
activity_menu = ctk.CTkOptionMenu(
health_frame,
variable=self.activity_var,
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
)
activity_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 健康目标
goal_label = ctk.CTkLabel(health_frame, text="健康目标:")
goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.goal_var = tk.StringVar(value="保持健康")
goal_menu = ctk.CTkOptionMenu(
health_frame,
variable=self.goal_var,
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力"]
)
goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 过敏信息
allergy_label = ctk.CTkLabel(health_frame, text="过敏食物:")
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.allergy_var = tk.StringVar(value="")
allergy_entry = ctk.CTkEntry(health_frame, textvariable=self.allergy_var, width=200)
allergy_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
def _create_diet_preferences_section(self, parent):
"""创建饮食偏好区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="🍽️ 饮食偏好",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 偏好网格
pref_frame = ctk.CTkFrame(section_frame)
pref_frame.pack(fill="x", padx=20, pady=10)
# 口味偏好
taste_label = ctk.CTkLabel(pref_frame, text="主要口味:")
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.taste_var = tk.StringVar(value="均衡")
taste_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.taste_var,
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
)
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 饮食类型
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.diet_var = tk.StringVar(value="普通饮食")
diet_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.diet_var,
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食"]
)
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 不喜欢食物
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
dislike_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.dislike_var = tk.StringVar(value="")
dislike_entry = ctk.CTkEntry(pref_frame, textvariable=self.dislike_var, width=200)
dislike_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
def _create_buttons(self, parent):
"""创建按钮区域"""
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="💾 保存信息",
command=self._save_data,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
save_button.pack(side="left", padx=20, pady=10)
# 加载按钮
load_button = ctk.CTkButton(
button_frame,
text="📂 加载信息",
command=self._load_data,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
load_button.pack(side="left", padx=20, pady=10)
# 重置按钮
reset_button = ctk.CTkButton(
button_frame,
text="🔄 重置表单",
command=self._reset_form,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
reset_button.pack(side="right", padx=20, pady=10)
def _save_data(self):
"""保存数据"""
try:
self.user_data = {
'basic_info': {
'name': self.name_var.get(),
'age': int(self.age_var.get()),
'gender': self.gender_var.get(),
'height': int(self.height_var.get()),
'weight': int(self.weight_var.get())
},
'health_info': {
'activity_level': self.activity_var.get(),
'health_goal': self.goal_var.get(),
'allergies': self.allergy_var.get()
},
'diet_preferences': {
'taste': self.taste_var.get(),
'diet_type': self.diet_var.get(),
'dislikes': self.dislike_var.get()
},
'saved_at': datetime.now().isoformat()
}
# 保存到文件
with open('data/user_info.json', 'w', encoding='utf-8') as f:
json.dump(self.user_data, f, ensure_ascii=False, indent=2)
messagebox.showinfo("成功", "信息保存成功!")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _load_data(self):
"""加载数据"""
try:
with open('data/user_info.json', 'r', encoding='utf-8') as f:
self.user_data = json.load(f)
# 填充表单
basic_info = self.user_data.get('basic_info', {})
self.name_var.set(basic_info.get('name', ''))
self.age_var.set(str(basic_info.get('age', 25)))
self.gender_var.set(basic_info.get('gender', ''))
self.height_var.set(str(basic_info.get('height', 165)))
self.weight_var.set(str(basic_info.get('weight', 55)))
health_info = self.user_data.get('health_info', {})
self.activity_var.set(health_info.get('activity_level', '中等'))
self.goal_var.set(health_info.get('health_goal', '保持健康'))
self.allergy_var.set(health_info.get('allergies', ''))
diet_prefs = self.user_data.get('diet_preferences', {})
self.taste_var.set(diet_prefs.get('taste', '均衡'))
self.diet_var.set(diet_prefs.get('diet_type', '普通饮食'))
self.dislike_var.set(diet_prefs.get('dislikes', ''))
messagebox.showinfo("成功", "信息加载成功!")
except FileNotFoundError:
messagebox.showwarning("警告", "未找到保存的信息文件")
except Exception as e:
messagebox.showerror("错误", f"加载失败: {str(e)}")
def _reset_form(self):
"""重置表单"""
self.name_var.set("")
self.age_var.set("25")
self.gender_var.set("")
self.height_var.set("165")
self.weight_var.set("55")
self.activity_var.set("中等")
self.goal_var.set("保持健康")
self.allergy_var.set("")
self.taste_var.set("均衡")
self.diet_var.set("普通饮食")
self.dislike_var.set("")
class NewMainWindow(ctk.CTk):
"""新的主窗口 - 信息录入/修改 + 随机转盘/扭蛋机"""
def __init__(self):
super().__init__()
# 设置窗口
self.title("🍎 智能饮食推荐助手")
self.geometry("1200x800")
self.minsize(1000, 700)
# 创建界面
self._create_widgets()
# 设置转盘回调
self.spin_wheel.on_spin_complete = self._on_spin_complete
def _create_widgets(self):
"""创建界面组件"""
# 主标题
title_label = ctk.CTkLabel(
self,
text="🎯 智能饮食推荐助手",
font=ctk.CTkFont(size=32, weight="bold")
)
title_label.pack(pady=20)
# 主内容区域
main_frame = ctk.CTkFrame(self)
main_frame.pack(fill="both", expand=True, padx=20, pady=10)
# 左侧 - 信息录入区域
left_frame = ctk.CTkFrame(main_frame)
left_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
self.user_info_form = UserInfoForm(left_frame)
self.user_info_form.pack(fill="both", expand=True)
# 右侧 - 转盘区域
right_frame = ctk.CTkFrame(main_frame)
right_frame.pack(side="right", fill="both", padx=(10, 0))
# 转盘标题
wheel_title = ctk.CTkLabel(
right_frame,
text="🎰 随机推荐转盘",
font=ctk.CTkFont(size=20, weight="bold")
)
wheel_title.pack(pady=20)
# 转盘说明
wheel_desc = ctk.CTkLabel(
right_frame,
text="点击转盘开始随机推荐!",
font=ctk.CTkFont(size=14)
)
wheel_desc.pack(pady=10)
# 转盘组件
self.spin_wheel = SpinWheel(right_frame, width=350, height=350)
self.spin_wheel.pack(pady=20)
# 结果显示区域
self.result_frame = ctk.CTkFrame(right_frame)
self.result_frame.pack(fill="x", padx=20, pady=20)
self.result_label = ctk.CTkLabel(
self.result_frame,
text="等待转盘结果...",
font=ctk.CTkFont(size=16),
wraplength=300
)
self.result_label.pack(pady=20)
# 底部状态栏
self.status_frame = ctk.CTkFrame(self)
self.status_frame.pack(fill="x", padx=20, pady=10)
self.status_label = ctk.CTkLabel(
self.status_frame,
text="就绪",
font=ctk.CTkFont(size=12)
)
self.status_label.pack(pady=5)
def _on_spin_complete(self, selected_option):
"""转盘完成回调"""
self._update_status(f"转盘选中: {selected_option['text']}")
# 显示结果
result_text = f"🎯 推荐结果: {selected_option['text']}\n\n"
# 根据选中的选项生成具体建议
if selected_option['value'] == 'breakfast':
result_text += "🌅 早餐建议:\n"
result_text += "• 燕麦粥 + 牛奶 + 香蕉\n"
result_text += "• 全麦面包 + 鸡蛋 + 蔬菜\n"
result_text += "• 小米粥 + 咸菜 + 煮蛋"
elif selected_option['value'] == 'lunch':
result_text += "🌞 午餐建议:\n"
result_text += "• 米饭 + 鸡肉 + 青菜\n"
result_text += "• 面条 + 牛肉 + 西红柿\n"
result_text += "• 饺子 + 汤"
elif selected_option['value'] == 'dinner':
result_text += "🌙 晚餐建议:\n"
result_text += "• 粥 + 咸菜 + 豆腐\n"
result_text += "• 蒸蛋 + 青菜 + 汤\n"
result_text += "• 面条 + 蔬菜"
elif selected_option['value'] == 'health':
result_text += "🏥 健康建议:\n"
result_text += "• 多喝水,保持水分平衡\n"
result_text += "• 适量运动,增强体质\n"
result_text += "• 规律作息,保证睡眠"
elif selected_option['value'] == 'nutrition':
result_text += "🥗 营养建议:\n"
result_text += "• 多吃蔬菜水果\n"
result_text += "• 适量蛋白质摄入\n"
result_text += "• 控制糖分和盐分"
elif selected_option['value'] == 'exercise':
result_text += "🏃 运动建议:\n"
result_text += "• 每天30分钟有氧运动\n"
result_text += "• 适量力量训练\n"
result_text += "• 注意运动前后拉伸"
self.result_label.configure(text=result_text)
def _update_status(self, message):
"""更新状态栏"""
self.status_label.configure(text=f"{datetime.now().strftime('%H:%M:%S')} - {message}")
def main():
"""主函数"""
app = NewMainWindow()
app.mainloop()
if __name__ == "__main__":
main()

637
gui/ocr_calorie_gui.py Normal file
View File

@@ -0,0 +1,637 @@
"""
OCR热量识别GUI界面
提供图片上传、OCR识别、结果验证和修正功能
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from PIL import Image, ImageTk
import threading
from typing import Dict, List, Optional, Any
from pathlib import Path
import json
from datetime import datetime
from core.base import UserData, CalorieInfo, FoodRecognitionResult
class OCRCalorieGUI:
"""OCR热量识别GUI界面"""
def __init__(self, parent_window, app_core):
self.parent_window = parent_window
self.app_core = app_core
self.current_image_path = None
self.current_recognition_result = None
self.user_corrections = {}
# 创建主框架
self.main_frame = ttk.Frame(parent_window)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建界面
self._create_widgets()
self._setup_layout()
self._bind_events()
def _create_widgets(self):
"""创建界面组件"""
# 标题
self.title_label = ttk.Label(
self.main_frame,
text="📷 图片OCR热量识别",
font=("Arial", 18, "bold"),
foreground="#2c3e50"
)
# 图片上传区域
self.image_frame = ttk.LabelFrame(
self.main_frame,
text="📸 图片上传",
padding=15,
relief="solid",
borderwidth=1
)
self.upload_button = ttk.Button(
self.image_frame,
text="📁 选择图片",
command=self._select_image,
style="Accent.TButton"
)
self.image_label = ttk.Label(
self.image_frame,
text="请选择包含食物信息的图片",
background="#f8f9fa",
relief="solid",
borderwidth=1,
width=50,
height=15,
anchor="center"
)
# 识别控制区域
self.control_frame = ttk.LabelFrame(
self.main_frame,
text="⚙️ 识别控制",
padding=15,
relief="solid",
borderwidth=1
)
self.recognize_button = ttk.Button(
self.control_frame,
text="🚀 开始识别",
command=self._start_recognition,
state=tk.DISABLED,
style="Accent.TButton"
)
self.progress_bar = ttk.Progressbar(
self.control_frame,
mode='indeterminate',
style="Accent.TProgressbar"
)
self.status_label = ttk.Label(
self.control_frame,
text="✅ 准备就绪",
foreground="#27ae60"
)
# 识别结果区域
self.result_frame = ttk.LabelFrame(self.main_frame, text="识别结果", padding=10)
# 创建结果表格
self.result_tree = ttk.Treeview(
self.result_frame,
columns=('food_name', 'calories', 'confidence', 'source'),
show='headings',
height=8
)
# 设置列标题
self.result_tree.heading('food_name', text='食物名称')
self.result_tree.heading('calories', text='热量(卡路里)')
self.result_tree.heading('confidence', text='置信度')
self.result_tree.heading('source', text='来源')
# 设置列宽
self.result_tree.column('food_name', width=150)
self.result_tree.column('calories', width=100)
self.result_tree.column('confidence', width=80)
self.result_tree.column('source', width=100)
# 结果操作按钮
self.result_button_frame = ttk.Frame(self.result_frame)
self.edit_button = ttk.Button(
self.result_button_frame,
text="编辑结果",
command=self._edit_result,
state=tk.DISABLED
)
self.confirm_button = ttk.Button(
self.result_button_frame,
text="确认结果",
command=self._confirm_result,
state=tk.DISABLED
)
self.clear_button = ttk.Button(
self.result_button_frame,
text="清空结果",
command=self._clear_results
)
# 详细信息区域
self.detail_frame = ttk.LabelFrame(self.main_frame, text="详细信息", padding=10)
self.detail_text = scrolledtext.ScrolledText(
self.detail_frame,
height=8,
width=60
)
# 建议区域
self.suggestion_frame = ttk.LabelFrame(self.main_frame, text="建议", padding=10)
self.suggestion_text = scrolledtext.ScrolledText(
self.suggestion_frame,
height=4,
width=60,
state=tk.DISABLED
)
def _setup_layout(self):
"""设置布局"""
# 标题
self.title_label.pack(pady=(0, 10))
# 图片上传区域
self.image_frame.pack(fill=tk.X, pady=(0, 10))
self.upload_button.pack(side=tk.LEFT, padx=(0, 10))
self.image_label.pack(fill=tk.BOTH, expand=True)
# 识别控制区域
self.control_frame.pack(fill=tk.X, pady=(0, 10))
self.recognize_button.pack(side=tk.LEFT, padx=(0, 10))
self.progress_bar.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
self.status_label.pack(side=tk.LEFT)
# 识别结果区域
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 结果表格
result_scrollbar = ttk.Scrollbar(self.result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
self.result_tree.configure(yscrollcommand=result_scrollbar.set)
self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
result_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 结果操作按钮
self.result_button_frame.pack(fill=tk.X, pady=(10, 0))
self.edit_button.pack(side=tk.LEFT, padx=(0, 10))
self.confirm_button.pack(side=tk.LEFT, padx=(0, 10))
self.clear_button.pack(side=tk.LEFT)
# 详细信息和建议区域
detail_suggestion_frame = ttk.Frame(self.main_frame)
detail_suggestion_frame.pack(fill=tk.BOTH, expand=True)
self.detail_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
self.detail_text.pack(fill=tk.BOTH, expand=True)
self.suggestion_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.suggestion_text.pack(fill=tk.BOTH, expand=True)
def _bind_events(self):
"""绑定事件"""
self.result_tree.bind('<<TreeviewSelect>>', self._on_result_select)
self.result_tree.bind('<Double-1>', self._on_result_double_click)
def _select_image(self):
"""选择图片文件"""
file_types = [
("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif"),
("JPEG文件", "*.jpg *.jpeg"),
("PNG文件", "*.png"),
("所有文件", "*.*")
]
file_path = filedialog.askopenfilename(
title="选择包含食物信息的图片",
filetypes=file_types
)
if file_path:
self.current_image_path = file_path
self._display_image(file_path)
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text=f"已选择图片: {Path(file_path).name}")
def _display_image(self, image_path: str):
"""显示图片"""
try:
# 加载图片
image = Image.open(image_path)
# 调整图片大小以适应显示区域
display_size = (400, 300)
image.thumbnail(display_size, Image.Resampling.LANCZOS)
# 转换为Tkinter可显示的格式
photo = ImageTk.PhotoImage(image)
# 更新标签
self.image_label.config(image=photo, text="")
self.image_label.image = photo # 保持引用
except Exception as e:
messagebox.showerror("错误", f"无法显示图片: {str(e)}")
self.image_label.config(image="", text="图片显示失败")
def _start_recognition(self):
"""开始OCR识别"""
if not self.current_image_path:
messagebox.showwarning("警告", "请先选择图片")
return
# 禁用按钮,显示进度条
self.recognize_button.config(state=tk.DISABLED)
self.progress_bar.start()
self.status_label.config(text="正在识别...")
# 在新线程中执行识别
thread = threading.Thread(target=self._perform_recognition)
thread.daemon = True
thread.start()
def _perform_recognition(self):
"""执行OCR识别"""
try:
# 准备请求数据
request_data = {
'type': 'recognize_image',
'image_path': self.current_image_path
}
# 获取当前用户数据(这里需要根据实际情况调整)
user_data = UserData(
user_id="current_user",
profile={},
meals=[],
feedback=[],
preferences={}
)
# 调用OCR模块
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
ocr_module = OCRCalorieRecognitionModule(self.app_core.config)
if not ocr_module.initialize():
raise Exception("OCR模块初始化失败")
result = ocr_module.process(request_data, user_data)
# 在主线程中更新UI
self.parent_window.after(0, self._on_recognition_complete, result)
except Exception as e:
self.parent_window.after(0, self._on_recognition_error, str(e))
def _on_recognition_complete(self, result):
"""识别完成回调"""
try:
# 停止进度条
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
if result.result.get('success', False):
self.current_recognition_result = result.result['result']
self._display_recognition_results()
self.status_label.config(text="识别完成")
else:
error_msg = result.result.get('error', '识别失败')
messagebox.showerror("识别失败", error_msg)
self.status_label.config(text="识别失败")
except Exception as e:
self._on_recognition_error(str(e))
def _on_recognition_error(self, error_msg: str):
"""识别错误回调"""
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text="识别失败")
messagebox.showerror("识别错误", f"OCR识别过程中出现错误: {error_msg}")
def _display_recognition_results(self):
"""显示识别结果"""
if not self.current_recognition_result:
return
try:
# 清空现有结果
self._clear_results()
# 显示热量信息
calorie_infos = self.current_recognition_result.calorie_infos
for info in calorie_infos:
self.result_tree.insert('', tk.END, values=(
info.food_name,
f"{info.calories:.1f}" if info.calories else "未知",
f"{info.confidence:.2f}",
info.source
))
# 显示详细信息
self._update_detail_text()
# 显示建议
self._update_suggestion_text()
# 启用操作按钮
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
except Exception as e:
messagebox.showerror("错误", f"显示识别结果失败: {str(e)}")
def _update_detail_text(self):
"""更新详细信息文本"""
if not self.current_recognition_result:
return
try:
detail_text = "=== OCR识别详细信息 ===\n\n"
# OCR结果
detail_text += "OCR识别结果:\n"
for ocr_result in self.current_recognition_result.ocr_results:
detail_text += f"- 方法: {ocr_result.method}\n"
detail_text += f" 置信度: {ocr_result.confidence:.2f}\n"
detail_text += f" 识别文本: {ocr_result.text[:100]}...\n\n"
# 处理时间
detail_text += f"处理时间: {self.current_recognition_result.processing_time:.2f}\n"
detail_text += f"整体置信度: {self.current_recognition_result.overall_confidence:.2f}\n"
# 热量信息
detail_text += "\n=== 热量信息 ===\n"
for info in self.current_recognition_result.calorie_infos:
detail_text += f"食物: {info.food_name}\n"
detail_text += f"热量: {info.calories} 卡路里\n"
detail_text += f"置信度: {info.confidence:.2f}\n"
detail_text += f"来源: {info.source}\n"
detail_text += f"原始文本: {info.raw_text}\n\n"
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, detail_text)
except Exception as e:
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, f"详细信息加载失败: {str(e)}")
def _update_suggestion_text(self):
"""更新建议文本"""
if not self.current_recognition_result:
return
try:
suggestions = self.current_recognition_result.suggestions
suggestion_text = "=== 建议 ===\n\n"
for i, suggestion in enumerate(suggestions, 1):
suggestion_text += f"{i}. {suggestion}\n"
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, suggestion_text)
self.suggestion_text.config(state=tk.DISABLED)
except Exception as e:
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, f"建议加载失败: {str(e)}")
self.suggestion_text.config(state=tk.DISABLED)
def _on_result_select(self, event):
"""结果选择事件"""
selection = self.result_tree.selection()
if selection:
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
else:
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
def _on_result_double_click(self, event):
"""结果双击事件"""
self._edit_result()
def _edit_result(self):
"""编辑识别结果"""
selection = self.result_tree.selection()
if not selection:
messagebox.showwarning("警告", "请先选择要编辑的结果")
return
try:
item = selection[0]
values = self.result_tree.item(item, 'values')
food_name = values[0]
calories = values[1]
# 创建编辑对话框
self._create_edit_dialog(food_name, calories, item)
except Exception as e:
messagebox.showerror("错误", f"编辑结果失败: {str(e)}")
def _create_edit_dialog(self, food_name: str, calories: str, item_id: str):
"""创建编辑对话框"""
dialog = tk.Toplevel(self.parent_window)
dialog.title("编辑识别结果")
dialog.geometry("400x300")
dialog.resizable(False, False)
# 居中显示
dialog.transient(self.parent_window)
dialog.grab_set()
# 创建表单
form_frame = ttk.Frame(dialog, padding=20)
form_frame.pack(fill=tk.BOTH, expand=True)
# 食物名称
ttk.Label(form_frame, text="食物名称:").pack(anchor=tk.W, pady=(0, 5))
food_name_var = tk.StringVar(value=food_name)
food_name_entry = ttk.Entry(form_frame, textvariable=food_name_var, width=40)
food_name_entry.pack(fill=tk.X, pady=(0, 15))
# 热量
ttk.Label(form_frame, text="热量(卡路里):").pack(anchor=tk.W, pady=(0, 5))
calories_var = tk.StringVar(value=calories)
calories_entry = ttk.Entry(form_frame, textvariable=calories_var, width=40)
calories_entry.pack(fill=tk.X, pady=(0, 15))
# 置信度
ttk.Label(form_frame, text="置信度:").pack(anchor=tk.W, pady=(0, 5))
confidence_var = tk.StringVar(value="0.95")
confidence_entry = ttk.Entry(form_frame, textvariable=confidence_var, width=40)
confidence_entry.pack(fill=tk.X, pady=(0, 20))
# 按钮
button_frame = ttk.Frame(form_frame)
button_frame.pack(fill=tk.X)
def save_changes():
try:
new_food_name = food_name_var.get().strip()
new_calories = calories_var.get().strip()
new_confidence = float(confidence_var.get())
if not new_food_name:
messagebox.showwarning("警告", "食物名称不能为空")
return
if new_calories and new_calories != "未知":
try:
float(new_calories)
except ValueError:
messagebox.showwarning("警告", "热量必须是数字")
return
# 更新表格
self.result_tree.item(item_id, values=(
new_food_name,
new_calories,
f"{new_confidence:.2f}",
"user_corrected"
))
# 保存用户修正
self.user_corrections[new_food_name] = {
'calories': float(new_calories) if new_calories and new_calories != "未知" else None,
'confidence': new_confidence,
'timestamp': datetime.now().isoformat()
}
dialog.destroy()
messagebox.showinfo("成功", "结果已更新")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def cancel_changes():
dialog.destroy()
ttk.Button(button_frame, text="保存", command=save_changes).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(button_frame, text="取消", command=cancel_changes).pack(side=tk.LEFT)
def _confirm_result(self):
"""确认识别结果"""
if not self.current_recognition_result:
messagebox.showwarning("警告", "没有可确认的结果")
return
try:
# 获取所有结果
results = []
for item in self.result_tree.get_children():
values = self.result_tree.item(item, 'values')
results.append({
'food_name': values[0],
'calories': float(values[1]) if values[1] != "未知" else None,
'confidence': float(values[2]),
'source': values[3]
})
if not results:
messagebox.showwarning("警告", "没有可确认的结果")
return
# 确认对话框
confirm_msg = "确认以下识别结果:\n\n"
for i, result in enumerate(results, 1):
confirm_msg += f"{i}. {result['food_name']}: {result['calories']} 卡路里\n"
confirm_msg += "\n是否确认这些结果?"
if messagebox.askyesno("确认结果", confirm_msg):
# 保存到餐食记录
self._save_to_meal_record(results)
messagebox.showinfo("成功", "结果已保存到餐食记录")
# 清空结果
self._clear_results()
except Exception as e:
messagebox.showerror("错误", f"确认结果失败: {str(e)}")
def _save_to_meal_record(self, results: List[Dict[str, Any]]):
"""保存到餐食记录"""
try:
# 这里需要调用应用核心的餐食记录功能
# 暂时保存到本地文件
meal_record = {
'timestamp': datetime.now().isoformat(),
'source': 'ocr_recognition',
'foods': results,
'total_calories': sum(r['calories'] for r in results if r['calories'])
}
# 保存到文件
record_file = Path("data/ocr_meal_records.json")
record_file.parent.mkdir(parents=True, exist_ok=True)
records = []
if record_file.exists():
with open(record_file, 'r', encoding='utf-8') as f:
records = json.load(f)
records.append(meal_record)
with open(record_file, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
except Exception as e:
self.logger.error(f"保存餐食记录失败: {e}")
raise
def _clear_results(self):
"""清空识别结果"""
self.result_tree.delete(*self.result_tree.get_children())
self.detail_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.DISABLED)
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
self.current_recognition_result = None
if __name__ == "__main__":
# 测试GUI
root = tk.Tk()
root.title("OCR热量识别测试")
root.geometry("800x600")
# 模拟应用核心
class MockAppCore:
def __init__(self):
self.config = type('Config', (), {})()
app_core = MockAppCore()
# 创建GUI
ocr_gui = OCRCalorieGUI(root, app_core)
root.mainloop()

526
gui/quick_user_input.py Normal file
View File

@@ -0,0 +1,526 @@
"""
快速用户需求录入界面
优化用户录入流程,提高效率
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from typing import Dict, List, Optional
from datetime import datetime
import json
class QuickUserInputDialog:
"""快速用户需求录入对话框"""
def __init__(self, parent, user_id: str):
self.parent = parent
self.user_id = user_id
self.input_data = {}
# 创建对话框
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title("快速需求录入")
self.dialog.geometry("800x600")
self.dialog.transient(parent)
self.dialog.grab_set()
# 居中显示
self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self._create_widgets()
def _create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ctk.CTkScrollableFrame(self.dialog)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
main_frame,
text="🚀 快速需求录入",
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(pady=20)
# 步骤1基础信息
self._create_basic_info_section(main_frame)
# 步骤2饮食偏好
self._create_preferences_section(main_frame)
# 步骤3健康目标
self._create_health_goals_section(main_frame)
# 步骤4快速餐食记录
self._create_quick_meal_section(main_frame)
# 按钮区域
self._create_buttons(main_frame)
def _create_basic_info_section(self, parent):
"""创建基础信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="1⃣ 基础信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 信息网格
info_frame = ctk.CTkFrame(section_frame)
info_frame.pack(fill="x", padx=20, pady=10)
# 姓名
name_label = ctk.CTkLabel(info_frame, text="姓名:")
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.name_var = tk.StringVar()
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 年龄范围
age_label = ctk.CTkLabel(info_frame, text="年龄范围:")
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.age_var = tk.StringVar(value="25-30岁")
age_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.age_var,
values=["18-24岁", "25-30岁", "31-35岁", "36-40岁", "41-45岁", "46-50岁", "51-55岁", "56-60岁", "60岁以上"]
)
age_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 性别
gender_label = ctk.CTkLabel(info_frame, text="性别:")
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.gender_var = tk.StringVar(value="")
gender_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.gender_var,
values=["", ""]
)
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 身高体重范围
height_label = ctk.CTkLabel(info_frame, text="身高范围:")
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.height_var = tk.StringVar(value="160-165cm")
height_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.height_var,
values=["150cm以下", "150-155cm", "155-160cm", "160-165cm", "165-170cm", "170-175cm", "175-180cm", "180cm以上"]
)
height_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
weight_label = ctk.CTkLabel(info_frame, text="体重范围:")
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.weight_var = tk.StringVar(value="50-55kg")
weight_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.weight_var,
values=["40kg以下", "40-45kg", "45-50kg", "50-55kg", "55-60kg", "60-65kg", "65-70kg", "70-75kg", "75-80kg", "80kg以上"]
)
weight_menu.grid(row=2, column=1, sticky="w", padx=10, pady=5)
# 活动水平
activity_label = ctk.CTkLabel(info_frame, text="活动水平:")
activity_label.grid(row=2, column=2, sticky="w", padx=10, pady=5)
self.activity_var = tk.StringVar(value="中等")
activity_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.activity_var,
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
)
activity_menu.grid(row=2, column=3, sticky="w", padx=10, pady=5)
def _create_preferences_section(self, parent):
"""创建饮食偏好区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="2⃣ 饮食偏好",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 偏好网格
pref_frame = ctk.CTkFrame(section_frame)
pref_frame.pack(fill="x", padx=20, pady=10)
# 口味偏好
taste_label = ctk.CTkLabel(pref_frame, text="主要口味偏好:")
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.taste_var = tk.StringVar(value="均衡")
taste_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.taste_var,
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
)
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 饮食类型
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.diet_var = tk.StringVar(value="普通饮食")
diet_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.diet_var,
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食", "地中海饮食"]
)
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 过敏食物
allergy_label = ctk.CTkLabel(pref_frame, text="过敏食物:")
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.allergy_var = tk.StringVar(value="")
allergy_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.allergy_var,
values=["", "花生", "海鲜", "牛奶", "鸡蛋", "坚果", "大豆", "小麦", "其他"]
)
allergy_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 不喜欢食物
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
dislike_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.dislike_var = tk.StringVar(value="")
dislike_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.dislike_var,
values=["", "内脏", "辛辣", "油腻", "甜食", "酸味", "苦味", "其他"]
)
dislike_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
def _create_health_goals_section(self, parent):
"""创建健康目标区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="3⃣ 健康目标",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 目标选择
goals_frame = ctk.CTkFrame(section_frame)
goals_frame.pack(fill="x", padx=20, pady=10)
# 主要目标
main_goal_label = ctk.CTkLabel(goals_frame, text="主要目标:")
main_goal_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.main_goal_var = tk.StringVar(value="保持健康")
main_goal_menu = ctk.CTkOptionMenu(
goals_frame,
variable=self.main_goal_var,
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力", "控制血糖", "降低血压"]
)
main_goal_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 次要目标
sub_goal_label = ctk.CTkLabel(goals_frame, text="次要目标:")
sub_goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.sub_goal_var = tk.StringVar(value="")
sub_goal_menu = ctk.CTkOptionMenu(
goals_frame,
variable=self.sub_goal_var,
values=["", "改善睡眠", "提高精力", "美容养颜", "延缓衰老", "改善皮肤", "增强记忆", "缓解压力"]
)
sub_goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
def _create_quick_meal_section(self, parent):
"""创建快速餐食记录区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="4⃣ 快速餐食记录",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 餐食选择
meal_frame = ctk.CTkFrame(section_frame)
meal_frame.pack(fill="x", padx=20, pady=10)
# 餐次选择
meal_type_label = ctk.CTkLabel(meal_frame, text="餐次:")
meal_type_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.meal_type_var = tk.StringVar(value="午餐")
meal_type_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.meal_type_var,
values=["早餐", "午餐", "晚餐", "加餐"]
)
meal_type_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 快速食物选择
food_label = ctk.CTkLabel(meal_frame, text="快速选择食物:")
food_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.quick_food_var = tk.StringVar(value="米饭+鸡肉+蔬菜")
quick_food_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.quick_food_var,
values=[
"米饭+鸡肉+蔬菜",
"面条+鸡蛋+青菜",
"馒头+豆腐+白菜",
"粥+咸菜+鸡蛋",
"面包+牛奶+水果",
"饺子+汤",
"包子+豆浆",
"其他"
]
)
quick_food_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 分量选择
portion_label = ctk.CTkLabel(meal_frame, text="分量:")
portion_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.portion_var = tk.StringVar(value="正常")
portion_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.portion_var,
values=["少量", "正常", "较多", "很多"]
)
portion_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
# 满意度
satisfaction_label = ctk.CTkLabel(meal_frame, text="满意度:")
satisfaction_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.satisfaction_var = tk.IntVar(value=3)
satisfaction_slider = ctk.CTkSlider(
meal_frame,
from_=1,
to=5,
number_of_steps=4,
variable=self.satisfaction_var
)
satisfaction_slider.grid(row=2, column=1, columnspan=2, sticky="ew", padx=10, pady=5)
# 满意度标签
self.satisfaction_display = ctk.CTkLabel(meal_frame, text="3分 - 一般")
self.satisfaction_display.grid(row=2, column=3, sticky="w", padx=10, pady=5)
# 绑定滑块事件
satisfaction_slider.configure(command=self._on_satisfaction_changed)
def _create_buttons(self, parent):
"""创建按钮区域"""
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="💾 保存所有信息",
command=self._save_all_data,
width=200,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
save_button.pack(side="left", padx=20, pady=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="❌ 取消",
command=self._cancel,
width=150,
height=50
)
cancel_button.pack(side="right", padx=20, pady=10)
def _on_satisfaction_changed(self, value):
"""满意度改变事件"""
score = int(float(value))
score_texts = {
1: "1分 - 很不满意",
2: "2分 - 不满意",
3: "3分 - 一般",
4: "4分 - 满意",
5: "5分 - 很满意"
}
self.satisfaction_display.configure(text=score_texts.get(score, "3分 - 一般"))
def _save_all_data(self):
"""保存所有数据"""
try:
# 收集所有数据
self.input_data = {
'basic_info': {
'name': self.name_var.get(),
'age_range': self.age_var.get(),
'gender': self.gender_var.get(),
'height_range': self.height_var.get(),
'weight_range': self.weight_var.get(),
'activity_level': self.activity_var.get()
},
'preferences': {
'taste': self.taste_var.get(),
'diet_type': self.diet_var.get(),
'allergies': self.allergy_var.get(),
'dislikes': self.dislike_var.get()
},
'health_goals': {
'main_goal': self.main_goal_var.get(),
'sub_goal': self.sub_goal_var.get()
},
'quick_meal': {
'meal_type': self.meal_type_var.get(),
'food_combo': self.quick_food_var.get(),
'portion': self.portion_var.get(),
'satisfaction': self.satisfaction_var.get()
}
}
# 保存到数据库
if self._save_to_database():
messagebox.showinfo("成功", "所有信息保存成功!")
self.dialog.destroy()
else:
messagebox.showerror("错误", "保存失败,请重试")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _save_to_database(self) -> bool:
"""保存到数据库"""
try:
from modules.data_collection import collect_questionnaire_data, record_meal
# 保存基础信息
basic_data = self.input_data['basic_info']
age_mapping = {
"18-24岁": 21, "25-30岁": 27, "31-35岁": 33, "36-40岁": 38,
"41-45岁": 43, "46-50岁": 48, "51-55岁": 53, "56-60岁": 58, "60岁以上": 65
}
height_mapping = {
"150cm以下": 150, "150-155cm": 152, "155-160cm": 157, "160-165cm": 162,
"165-170cm": 167, "170-175cm": 172, "175-180cm": 177, "180cm以上": 180
}
weight_mapping = {
"40kg以下": 40, "40-45kg": 42, "45-50kg": 47, "50-55kg": 52,
"55-60kg": 57, "60-65kg": 62, "65-70kg": 67, "70-75kg": 72,
"75-80kg": 77, "80kg以上": 80
}
activity_mapping = {
"久坐": "sedentary", "轻度活动": "light", "中等": "moderate",
"高度活动": "high", "极度活动": "very_high"
}
basic_answers = {
'name': basic_data['name'],
'age': age_mapping.get(basic_data['age_range'], 25),
'gender': basic_data['gender'],
'height': height_mapping.get(basic_data['height_range'], 165),
'weight': weight_mapping.get(basic_data['weight_range'], 55),
'activity_level': activity_mapping.get(basic_data['activity_level'], 'moderate')
}
collect_questionnaire_data(self.user_id, 'basic', basic_answers)
# 保存口味偏好
preferences_data = self.input_data['preferences']
taste_answers = {
'taste_preference': preferences_data['taste'],
'diet_type': preferences_data['diet_type'],
'allergies': [preferences_data['allergies']] if preferences_data['allergies'] != "" else [],
'dislikes': [preferences_data['dislikes']] if preferences_data['dislikes'] != "" else []
}
collect_questionnaire_data(self.user_id, 'taste', taste_answers)
# 保存健康目标
health_data = self.input_data['health_goals']
health_answers = {
'main_goal': health_data['main_goal'],
'sub_goal': health_data['sub_goal']
}
collect_questionnaire_data(self.user_id, 'health', health_answers)
# 保存快速餐食记录
meal_data = self.input_data['quick_meal']
meal_type_mapping = {
"早餐": "breakfast", "午餐": "lunch", "晚餐": "dinner", "加餐": "snack"
}
# 解析食物组合
food_combo = meal_data['food_combo']
if "+" in food_combo:
foods = [food.strip() for food in food_combo.split("+")]
else:
foods = [food_combo]
# 估算分量
portion_mapping = {
"少量": "1小份", "正常": "1份", "较多": "1大份", "很多": "2份"
}
quantities = [portion_mapping.get(meal_data['portion'], "1份")] * len(foods)
meal_record = {
'date': datetime.now().strftime('%Y-%m-%d'),
'meal_type': meal_type_mapping.get(meal_data['meal_type'], 'lunch'),
'foods': foods,
'quantities': quantities,
'satisfaction_score': meal_data['satisfaction']
}
record_meal(self.user_id, meal_record)
return True
except Exception as e:
print(f"保存到数据库失败: {e}")
return False
def _cancel(self):
"""取消录入"""
self.dialog.destroy()
# 便捷函数
def show_quick_user_input_dialog(parent, user_id: str):
"""显示快速用户需求录入对话框"""
dialog = QuickUserInputDialog(parent, user_id)
parent.wait_window(dialog.dialog)
if __name__ == "__main__":
# 测试快速录入对话框
root = tk.Tk()
root.title("测试快速录入")
def test_dialog():
show_quick_user_input_dialog(root, "test_user")
test_button = tk.Button(root, text="测试快速录入", command=test_dialog)
test_button.pack(pady=20)
root.mainloop()

579
gui/smart_meal_record.py Normal file
View File

@@ -0,0 +1,579 @@
"""
简化的餐食记录界面
使用选择式输入,减少用户负担
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from typing import Dict, List, Optional
from smart_food.smart_database import (
search_foods, get_food_categories, get_foods_by_category,
get_portion_options, estimate_calories, record_meal_smart
)
class SmartMealRecordDialog:
"""智能餐食记录对话框"""
def __init__(self, parent, user_id: str, meal_type: str = "lunch"):
self.parent = parent
self.user_id = user_id
self.meal_type = meal_type
self.selected_foods = []
# 创建对话框
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title(f"记录{self._get_meal_name(meal_type)}")
self.dialog.geometry("600x700")
self.dialog.transient(parent)
self.dialog.grab_set()
# 居中显示
self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self._create_widgets()
def _get_meal_name(self, meal_type: str) -> str:
"""获取餐次中文名称"""
meal_names = {
"breakfast": "早餐",
"lunch": "午餐",
"dinner": "晚餐"
}
return meal_names.get(meal_type, "餐食")
def _create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ctk.CTkScrollableFrame(self.dialog)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
main_frame,
text=f"🍽️ 记录{self._get_meal_name(self.meal_type)}",
font=ctk.CTkFont(size=20, weight="bold")
)
title_label.pack(pady=10)
# 食物选择区域
self._create_food_selection(main_frame)
# 已选食物列表
self._create_selected_foods(main_frame)
# 满意度评分
self._create_satisfaction_rating(main_frame)
# 备注
self._create_notes_section(main_frame)
# 按钮区域
self._create_buttons(main_frame)
def _create_food_selection(self, parent):
"""创建食物选择区域"""
# 食物选择框架
food_frame = ctk.CTkFrame(parent)
food_frame.pack(fill="x", padx=10, pady=10)
# 标题
food_title = ctk.CTkLabel(
food_frame,
text="选择食物",
font=ctk.CTkFont(size=16, weight="bold")
)
food_title.pack(pady=10)
# 食物搜索
search_frame = ctk.CTkFrame(food_frame)
search_frame.pack(fill="x", padx=10, pady=5)
search_label = ctk.CTkLabel(search_frame, text="搜索食物:")
search_label.pack(anchor="w", padx=5, pady=2)
self.search_var = tk.StringVar()
self.search_entry = ctk.CTkEntry(search_frame, textvariable=self.search_var, placeholder_text="输入食物名称搜索...")
self.search_entry.pack(fill="x", padx=5, pady=2)
self.search_entry.bind("<KeyRelease>", self._on_search_changed)
# 搜索结果
self.search_results_frame = ctk.CTkFrame(food_frame)
self.search_results_frame.pack(fill="x", padx=10, pady=5)
self.search_results_label = ctk.CTkLabel(self.search_results_frame, text="搜索结果:")
self.search_results_label.pack(anchor="w", padx=5, pady=2)
self.search_results_menu = ctk.CTkOptionMenu(
self.search_results_frame,
values=[],
command=self._on_search_result_selected
)
self.search_results_menu.pack(fill="x", padx=5, pady=2)
# 分隔线
separator = ctk.CTkFrame(food_frame, height=2)
separator.pack(fill="x", padx=10, pady=5)
# 分类选择
category_frame = ctk.CTkFrame(food_frame)
category_frame.pack(fill="x", padx=10, pady=5)
category_label = ctk.CTkLabel(category_frame, text="食物分类:")
category_label.pack(side="left", padx=5)
self.category_var = tk.StringVar(value="主食")
self.category_menu = ctk.CTkOptionMenu(
category_frame,
variable=self.category_var,
values=get_food_categories(),
command=self._on_category_changed
)
self.category_menu.pack(side="left", padx=5)
# 食物选择
food_select_frame = ctk.CTkFrame(food_frame)
food_select_frame.pack(fill="x", padx=10, pady=5)
food_label = ctk.CTkLabel(food_select_frame, text="选择食物:")
food_label.pack(anchor="w", padx=5, pady=2)
self.food_var = tk.StringVar()
self.food_menu = ctk.CTkOptionMenu(
food_select_frame,
variable=self.food_var,
values=[]
)
self.food_menu.pack(fill="x", padx=5, pady=2)
# 分量选择
portion_frame = ctk.CTkFrame(food_frame)
portion_frame.pack(fill="x", padx=10, pady=5)
portion_label = ctk.CTkLabel(portion_frame, text="分量:")
portion_label.pack(anchor="w", padx=5, pady=2)
self.portion_var = tk.StringVar(value="适量")
self.portion_menu = ctk.CTkOptionMenu(
portion_frame,
variable=self.portion_var,
values=["适量"]
)
self.portion_menu.pack(fill="x", padx=5, pady=2)
# 添加按钮
add_button = ctk.CTkButton(
food_frame,
text="添加到餐食",
command=self._add_food,
width=150
)
add_button.pack(pady=10)
# AI分析按钮
ai_analyze_button = ctk.CTkButton(
food_frame,
text="AI分析食物",
command=self._analyze_food_with_ai,
width=150,
fg_color="green"
)
ai_analyze_button.pack(pady=5)
# 初始化食物列表
self._on_category_changed("主食")
def _create_selected_foods(self, parent):
"""创建已选食物列表"""
# 已选食物框架
selected_frame = ctk.CTkFrame(parent)
selected_frame.pack(fill="x", padx=10, pady=10)
# 标题
selected_title = ctk.CTkLabel(
selected_frame,
text="已选食物",
font=ctk.CTkFont(size=16, weight="bold")
)
selected_title.pack(pady=10)
# 食物列表
self.foods_listbox = tk.Listbox(selected_frame, height=6)
self.foods_listbox.pack(fill="x", padx=10, pady=5)
# 删除按钮
delete_button = ctk.CTkButton(
selected_frame,
text="删除选中",
command=self._remove_food,
width=150
)
delete_button.pack(pady=5)
def _create_satisfaction_rating(self, parent):
"""创建满意度评分"""
# 满意度框架
satisfaction_frame = ctk.CTkFrame(parent)
satisfaction_frame.pack(fill="x", padx=10, pady=10)
# 标题
satisfaction_title = ctk.CTkLabel(
satisfaction_frame,
text="满意度评分",
font=ctk.CTkFont(size=16, weight="bold")
)
satisfaction_title.pack(pady=10)
# 评分滑块
self.satisfaction_var = tk.IntVar(value=3)
satisfaction_slider = ctk.CTkSlider(
satisfaction_frame,
from_=1,
to=5,
number_of_steps=4,
variable=self.satisfaction_var
)
satisfaction_slider.pack(fill="x", padx=20, pady=10)
# 评分标签
self.satisfaction_label = ctk.CTkLabel(
satisfaction_frame,
text="3分 - 一般",
font=ctk.CTkFont(size=14)
)
self.satisfaction_label.pack(pady=5)
# 绑定滑块事件
satisfaction_slider.configure(command=self._on_satisfaction_changed)
def _create_notes_section(self, parent):
"""创建备注区域"""
# 备注框架
notes_frame = ctk.CTkFrame(parent)
notes_frame.pack(fill="x", padx=10, pady=10)
# 标题
notes_title = ctk.CTkLabel(
notes_frame,
text="备注 (可选)",
font=ctk.CTkFont(size=16, weight="bold")
)
notes_title.pack(pady=10)
# 备注输入
self.notes_text = ctk.CTkTextbox(notes_frame, height=60)
self.notes_text.pack(fill="x", padx=10, pady=5)
self.notes_text.insert("1.0", "可以记录一些感受或特殊说明...")
def _create_buttons(self, parent):
"""创建按钮区域"""
# 按钮框架
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="保存餐食记录",
command=self._save_meal,
width=150,
height=40
)
save_button.pack(side="left", padx=20, pady=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="取消",
command=self._cancel,
width=150,
height=40
)
cancel_button.pack(side="right", padx=20, pady=10)
def _on_search_changed(self, event=None):
"""搜索输入改变事件"""
query = self.search_var.get().strip()
if not query:
self.search_results_menu.configure(values=[])
return
try:
from smart_food.smart_database import search_foods
results = search_foods(query)
if results:
food_names = [result["name"] for result in results]
self.search_results_menu.configure(values=food_names)
self.search_results_label.configure(text=f"搜索结果 ({len(food_names)}个):")
else:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="未找到匹配的食物")
except Exception as e:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="搜索失败")
def _on_search_result_selected(self, food_name):
"""搜索结果选择事件"""
self.food_var.set(food_name)
self._on_food_changed(food_name)
# 自动选择对应的分类
try:
from smart_food.smart_database import get_food_categories, get_foods_by_category
categories = get_food_categories()
for category in categories:
foods_in_category = get_foods_by_category(category)
if food_name in foods_in_category:
self.category_var.set(category)
self._on_category_changed(category)
break
except Exception:
pass
def _analyze_food_with_ai(self):
"""使用AI分析食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
try:
from smart_food.smart_database import analyze_food_with_ai
# 显示分析进度
self._show_ai_analysis_dialog(food_name, portion)
except Exception as e:
messagebox.showerror("错误", f"AI分析失败: {str(e)}")
def _show_ai_analysis_dialog(self, food_name: str, portion: str):
"""显示AI分析对话框"""
# 创建分析对话框
analysis_dialog = ctk.CTkToplevel(self.dialog)
analysis_dialog.title(f"AI分析 - {food_name}")
analysis_dialog.geometry("500x600")
analysis_dialog.transient(self.dialog)
analysis_dialog.grab_set()
# 居中显示
analysis_dialog.geometry("+%d+%d" % (self.dialog.winfo_rootx() + 50, self.dialog.winfo_rooty() + 50))
# 创建滚动框架
scroll_frame = ctk.CTkScrollableFrame(analysis_dialog)
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
scroll_frame,
text=f"🤖 AI分析: {food_name}",
font=ctk.CTkFont(size=18, weight="bold")
)
title_label.pack(pady=10)
# 分析进度
progress_label = ctk.CTkLabel(scroll_frame, text="正在分析中...")
progress_label.pack(pady=10)
# 分析结果区域
result_text = ctk.CTkTextbox(scroll_frame, height=400, width=450)
result_text.pack(fill="both", expand=True, pady=10)
# 关闭按钮
close_button = ctk.CTkButton(
scroll_frame,
text="关闭",
command=analysis_dialog.destroy,
width=100
)
close_button.pack(pady=10)
# 在后台线程中执行AI分析
import threading
def analyze_thread():
try:
from smart_food.smart_database import analyze_food_with_ai
# 执行AI分析
result = analyze_food_with_ai(food_name, portion)
# 更新UI
analysis_dialog.after(0, lambda: self._update_ai_analysis_result(
analysis_dialog, result_text, progress_label, result
))
except Exception as e:
analysis_dialog.after(0, lambda: self._update_ai_analysis_error(
analysis_dialog, result_text, progress_label, str(e)
))
threading.Thread(target=analyze_thread, daemon=True).start()
def _update_ai_analysis_result(self, dialog, result_text, progress_label, result):
"""更新AI分析结果"""
progress_label.configure(text="分析完成")
if result.get('success'):
content = f"""
🍎 食物分析结果: {result.get('reasoning', 'AI分析')}
📊 营养成分:
- 热量: {result.get('calories', 0)} 卡路里
- 蛋白质: {result.get('protein', 0):.1f}
- 碳水化合物: {result.get('carbs', 0):.1f}
- 脂肪: {result.get('fat', 0):.1f}
- 纤维: {result.get('fiber', 0):.1f}
🏷️ 分类: {result.get('category', '其他')}
💡 健康建议:
"""
for tip in result.get('health_tips', []):
content += f"{tip}\n"
content += "\n👨‍🍳 制作建议:\n"
for suggestion in result.get('cooking_suggestions', []):
content += f"{suggestion}\n"
content += f"\n🎯 置信度: {result.get('confidence', 0.5):.1%}"
else:
content = "AI分析失败请稍后重试。"
result_text.delete("1.0", "end")
result_text.insert("1.0", content)
def _update_ai_analysis_error(self, dialog, result_text, progress_label, error_msg):
"""更新AI分析错误"""
progress_label.configure(text="分析失败")
result_text.delete("1.0", "end")
result_text.insert("1.0", f"AI分析失败: {error_msg}")
def _on_category_changed(self, category):
"""分类改变事件"""
foods = get_foods_by_category(category)
self.food_menu.configure(values=foods)
if foods:
self.food_var.set(foods[0])
self._on_food_changed(foods[0])
def _on_food_changed(self, food_name):
"""食物改变事件"""
portions = get_portion_options(food_name)
self.portion_menu.configure(values=portions)
if portions:
self.portion_var.set(portions[0])
def _add_food(self):
"""添加食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
# 估算热量
calories = estimate_calories(food_name, portion)
# 添加到列表
food_item = {
"name": food_name,
"portion": portion,
"calories": calories
}
self.selected_foods.append(food_item)
self._update_foods_list()
def _update_foods_list(self):
"""更新食物列表显示"""
self.foods_listbox.delete(0, tk.END)
total_calories = 0
for i, food_item in enumerate(self.selected_foods):
display_text = f"{food_item['name']} - {food_item['portion']} ({food_item['calories']}卡)"
self.foods_listbox.insert(tk.END, display_text)
total_calories += food_item['calories']
# 显示总热量
if self.selected_foods:
total_text = f"总热量: {total_calories}卡路里"
self.foods_listbox.insert(tk.END, "")
self.foods_listbox.insert(tk.END, total_text)
def _remove_food(self):
"""删除选中的食物"""
selection = self.foods_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请选择要删除的食物")
return
index = selection[0]
if index < len(self.selected_foods):
self.selected_foods.pop(index)
self._update_foods_list()
def _on_satisfaction_changed(self, value):
"""满意度改变事件"""
score = int(float(value))
score_texts = {
1: "1分 - 很不满意",
2: "2分 - 不满意",
3: "3分 - 一般",
4: "4分 - 满意",
5: "5分 - 很满意"
}
self.satisfaction_label.configure(text=score_texts.get(score, "3分 - 一般"))
def _save_meal(self):
"""保存餐食记录"""
if not self.selected_foods:
messagebox.showwarning("警告", "请至少添加一种食物")
return
try:
# 构建餐食数据
meal_data = {
"meal_type": self.meal_type,
"foods": self.selected_foods,
"satisfaction_score": self.satisfaction_var.get(),
"notes": self.notes_text.get("1.0", "end-1c").strip()
}
# 智能记录餐食
if record_meal_smart(self.user_id, meal_data):
messagebox.showinfo("成功", "餐食记录保存成功!")
self.dialog.destroy()
else:
messagebox.showerror("错误", "餐食记录保存失败")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _cancel(self):
"""取消记录"""
self.dialog.destroy()
# 便捷函数
def show_smart_meal_record_dialog(parent, user_id: str, meal_type: str = "lunch"):
"""显示智能餐食记录对话框"""
dialog = SmartMealRecordDialog(parent, user_id, meal_type)
parent.wait_window(dialog.dialog)
if __name__ == "__main__":
# 测试智能餐食记录对话框
root = tk.Tk()
root.title("测试智能餐食记录")
def test_dialog():
show_smart_meal_record_dialog(root, "test_user", "lunch")
test_button = tk.Button(root, text="测试餐食记录", command=test_dialog)
test_button.pack(pady=20)
root.mainloop()

326
gui/styles.py Normal file
View File

@@ -0,0 +1,326 @@
"""
界面美化样式配置
提供统一的圆角设计和颜色主题
"""
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
class StyleConfig:
"""样式配置类"""
# 颜色主题
COLORS = {
'primary': '#3498db',
'primary_hover': '#2980b9',
'secondary': '#2ecc71',
'secondary_hover': '#27ae60',
'accent': '#e74c3c',
'accent_hover': '#c0392b',
'warning': '#f39c12',
'warning_hover': '#e67e22',
'info': '#9b59b6',
'info_hover': '#8e44ad',
'success': '#27ae60',
'success_hover': '#229954',
'danger': '#e74c3c',
'danger_hover': '#c0392b',
# 背景色
'bg_light': '#ffffff',
'bg_dark': '#2b2b2b',
'bg_card': '#f8f9fa',
'bg_card_dark': '#3b3b3b',
'bg_container': '#f0f0f0',
'bg_container_dark': '#1e1e1e',
# 文字色
'text_primary': '#2c3e50',
'text_primary_dark': '#ecf0f1',
'text_secondary': '#34495e',
'text_secondary_dark': '#bdc3c7',
'text_muted': '#7f8c8d',
'text_muted_dark': '#95a5a6',
# 边框色
'border': '#e0e0e0',
'border_dark': '#404040',
'border_light': '#f0f0f0',
'border_light_dark': '#555555',
}
# 圆角半径
CORNER_RADIUS = {
'small': 8,
'medium': 12,
'large': 15,
'xlarge': 20,
'xxlarge': 25,
}
# 字体配置
FONTS = {
'title': ('Arial', 22, 'bold'),
'subtitle': ('Arial', 18, 'bold'),
'heading': ('Arial', 16, 'bold'),
'body': ('Arial', 14),
'small': ('Arial', 12),
'tiny': ('Arial', 10),
}
# 间距配置
SPACING = {
'xs': 5,
'sm': 10,
'md': 15,
'lg': 20,
'xl': 25,
'xxl': 30,
}
def apply_rounded_theme():
"""应用圆角主题到CustomTkinter"""
# 设置全局主题
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue")
# 注意CustomTkinter不支持类级别的configure方法
# 样式需要在创建组件时单独设置
print("✅ 圆角主题已应用")
def create_rounded_frame(parent, **kwargs):
"""创建圆角框架"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['bg_light'], colors['bg_dark']),
'border_width': 1,
'border_color': (colors['border'], colors['border_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkFrame(parent, **default_kwargs)
def create_rounded_button(parent, text, **kwargs):
"""创建圆角按钮"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['primary'], colors['primary_hover']),
'hover_color': (colors['primary_hover'], colors['primary']),
'text_color': ('#ffffff', '#ffffff'),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkButton(parent, text=text, **default_kwargs)
def create_rounded_entry(parent, **kwargs):
"""创建圆角输入框"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
'border_width': 1,
'border_color': (colors['border_light'], colors['border_light_dark']),
'font': StyleConfig.FONTS['body'],
'text_color': (colors['text_primary'], colors['text_primary_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkEntry(parent, **default_kwargs)
def create_rounded_label(parent, text, **kwargs):
"""创建圆角标签"""
colors = StyleConfig.COLORS
default_kwargs = {
'text_color': (colors['text_primary'], colors['text_primary_dark']),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkLabel(parent, text=text, **default_kwargs)
def create_card_frame(parent, **kwargs):
"""创建卡片式框架"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['large'],
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
'border_width': 1,
'border_color': (colors['border'], colors['border_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkFrame(parent, **default_kwargs)
def create_accent_button(parent, text, color_type='primary', **kwargs):
"""创建强调色按钮"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
color_map = {
'primary': (colors['primary'], colors['primary_hover']),
'secondary': (colors['secondary'], colors['secondary_hover']),
'accent': (colors['accent'], colors['accent_hover']),
'warning': (colors['warning'], colors['warning_hover']),
'info': (colors['info'], colors['info_hover']),
'success': (colors['success'], colors['success_hover']),
'danger': (colors['danger'], colors['danger_hover']),
}
fg_color, hover_color = color_map.get(color_type, color_map['primary'])
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': fg_color,
'hover_color': hover_color,
'text_color': ('#ffffff', '#ffffff'),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkButton(parent, text=text, **default_kwargs)
def apply_ttk_styles():
"""应用TTK样式"""
style = ttk.Style()
# 配置样式
style.configure('Rounded.TFrame',
relief='solid',
borderwidth=1,
background='#f8f9fa')
style.configure('Rounded.TLabelFrame',
relief='solid',
borderwidth=1,
background='#f8f9fa')
style.configure('Accent.TButton',
relief='flat',
borderwidth=0,
background='#3498db',
foreground='white',
font=('Arial', 12, 'bold'))
style.map('Accent.TButton',
background=[('active', '#2980b9'),
('pressed', '#1f618d')])
style.configure('Accent.TProgressbar',
background='#3498db',
troughcolor='#ecf0f1',
borderwidth=0,
lightcolor='#3498db',
darkcolor='#3498db')
def get_spacing(size='md'):
"""获取间距值"""
return StyleConfig.SPACING.get(size, StyleConfig.SPACING['md'])
def get_font(font_type='body'):
"""获取字体配置"""
return StyleConfig.FONTS.get(font_type, StyleConfig.FONTS['body'])
def get_color(color_name):
"""获取颜色值"""
return StyleConfig.COLORS.get(color_name, StyleConfig.COLORS['text_primary'])
def get_radius(size='medium'):
"""获取圆角半径"""
return StyleConfig.CORNER_RADIUS.get(size, StyleConfig.CORNER_RADIUS['medium'])
# 预定义的样式组合
STYLE_PRESETS = {
'card': {
'corner_radius': 20,
'fg_color': ('#ffffff', '#3b3b3b'),
'border_width': 1,
'border_color': ('#e0e0e0', '#404040'),
},
'button_primary': {
'corner_radius': 15,
'fg_color': ('#3498db', '#2980b9'),
'hover_color': ('#2980b9', '#1f618d'),
'text_color': ('#ffffff', '#ffffff'),
},
'button_secondary': {
'corner_radius': 15,
'fg_color': ('#2ecc71', '#27ae60'),
'hover_color': ('#27ae60', '#229954'),
'text_color': ('#ffffff', '#ffffff'),
},
'button_accent': {
'corner_radius': 15,
'fg_color': ('#e74c3c', '#c0392b'),
'hover_color': ('#c0392b', '#a93226'),
'text_color': ('#ffffff', '#ffffff'),
},
'input_field': {
'corner_radius': 12,
'fg_color': ('#f8f9fa', '#404040'),
'border_width': 1,
'border_color': ('#e0e0e0', '#555555'),
},
}
def apply_preset_style(widget, preset_name):
"""应用预设样式"""
if preset_name in STYLE_PRESETS:
style_config = STYLE_PRESETS[preset_name]
for key, value in style_config.items():
if hasattr(widget, key):
setattr(widget, key, value)
if __name__ == "__main__":
# 测试样式配置
root = tk.Tk()
root.title("样式测试")
root.geometry("400x300")
# 应用TTK样式
apply_ttk_styles()
# 创建测试框架
main_frame = ttk.Frame(root, style='Rounded.TFrame')
main_frame.pack(fill='both', expand=True, padx=20, pady=20)
# 创建测试按钮
test_button = ttk.Button(main_frame, text="测试按钮", style='Accent.TButton')
test_button.pack(pady=10)
# 创建测试标签框架
test_frame = ttk.LabelFrame(main_frame, text="测试框架", style='Rounded.TLabelFrame')
test_frame.pack(fill='both', expand=True, pady=10)
test_label = ttk.Label(test_frame, text="这是一个测试标签")
test_label.pack(pady=20)
root.mainloop()

View File

@@ -0,0 +1,599 @@
"""
千问大模型集成模块
支持千问API的智能分析功能
"""
import requests
import json
import os
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
@dataclass
class LLMConfig:
"""大模型配置"""
provider: str
api_key: str
base_url: str
model: str
temperature: float = 0.7
max_tokens: int = 2000
class QwenLLMClient:
"""千问大模型客户端"""
def __init__(self, config: LLMConfig):
self.config = config
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {config.api_key}',
'Content-Type': 'application/json'
})
def chat_completion(self, messages: List[Dict[str, str]], **kwargs) -> Optional[Dict]:
"""聊天完成"""
try:
# 构建请求数据
data = {
'model': self.config.model,
'messages': messages,
'temperature': kwargs.get('temperature', self.config.temperature),
'max_tokens': kwargs.get('max_tokens', self.config.max_tokens),
'stream': False
}
# 发送请求
response = self.session.post(
f"{self.config.base_url}/chat/completions",
json=data,
timeout=30
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"千问API请求失败: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"千问API调用失败: {e}")
return None
def analyze_user_intent(self, user_input: str, user_context: Dict) -> Dict[str, Any]:
"""分析用户意图"""
system_prompt = """
你是一个专业的营养师和心理学专家,擅长分析用户的饮食需求和心理状态。
你的任务是:
1. 深度理解用户的真实需求,不仅仅是表面的话语
2. 考虑用户的生理状态、情绪状态、历史偏好等多维度因素
3. 提供个性化的饮食建议
4. 特别关注女性用户的生理周期对饮食需求的影响
分析时要:
- 透过现象看本质,理解用户的真实意图
- 综合考虑生理、心理、社会等多重因素
- 提供科学、实用、个性化的建议
- 保持专业性和同理心
请以JSON格式返回分析结果包含以下字段
- user_intent: 用户真实意图
- emotional_state: 情绪状态
- nutritional_needs: 营养需求列表
- recommended_foods: 推荐食物列表
- reasoning: 推荐理由
- confidence: 置信度(0-1)
"""
user_prompt = f"""
请分析以下用户输入的真实需求和意图:
用户输入: "{user_input}"
用户背景信息:
- 姓名: {user_context.get('name', '未知')}
- 年龄: {user_context.get('age', '未知')}
- 性别: {user_context.get('gender', '未知')}
- 身高体重: {user_context.get('height', '未知')}cm, {user_context.get('weight', '未知')}kg
- 活动水平: {user_context.get('activity_level', '未知')}
口味偏好: {json.dumps(user_context.get('taste_preferences', {}), ensure_ascii=False, indent=2)}
饮食限制:
- 过敏: {', '.join(user_context.get('allergies', []))}
- 不喜欢: {', '.join(user_context.get('dislikes', []))}
- 饮食偏好: {', '.join(user_context.get('dietary_preferences', []))}
最近3天饮食记录: {self._format_meal_history(user_context.get('recent_meals', []))}
用户反馈历史: {self._format_feedback_history(user_context.get('feedback_history', []))}
当前情况:
- 日期: {datetime.now().strftime('%Y-%m-%d')}
- 生理状态: {json.dumps(user_context.get('physiological_state', {}), ensure_ascii=False)}
请从以下维度分析用户需求:
1. **真实意图分析**: 用户真正想要什么?是饿了、馋了、还是需要特定营养?
2. **情绪状态**: 用户当前的情绪如何?压力大、开心、疲惫、焦虑等?
3. **生理需求**: 基于用户的身体状况、生理周期等,需要什么营养?
4. **口味偏好**: 基于历史数据和当前状态,推测用户可能的口味偏好
5. **饮食限制**: 考虑过敏、不喜欢、饮食偏好等限制
6. **推荐食物**: 基于以上分析推荐3-5种合适的食物
7. **推荐理由**: 解释为什么推荐这些食物
8. **置信度**: 对分析的信心程度 (0-1)
请以JSON格式返回分析结果。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.chat_completion(messages, temperature=0.3)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
return self._parse_analysis_result(content)
else:
return self._get_fallback_analysis(user_input, user_context)
def analyze_nutrition(self, meal_data: Dict, user_context: Dict) -> Dict[str, Any]:
"""分析营养状况"""
system_prompt = """
你是一个专业的营养师,擅长分析餐食的营养价值和健康建议。
你的任务是:
1. 分析餐食的营养均衡性
2. 评估热量是否合适
3. 识别缺少的营养素
4. 提供改进建议
5. 考虑用户的个人情况
分析时要:
- 基于科学的营养学知识
- 考虑用户的年龄、性别、体重、活动水平等因素
- 提供具体可行的建议
- 保持客观和专业
请以JSON格式返回分析结果包含以下字段
- nutrition_balance: 营养均衡性评估
- calorie_assessment: 热量评估
- missing_nutrients: 缺少的营养素列表
- improvements: 改进建议列表
- recommendations: 个性化建议列表
- confidence: 置信度(0-1)
"""
user_prompt = f"""
请分析以下餐食的营养状况:
餐食信息:
- 食物: {', '.join(meal_data.get('foods', []))}
- 分量: {', '.join(meal_data.get('quantities', []))}
- 热量: {meal_data.get('calories', '未知')}卡路里
用户信息:
- 年龄: {user_context.get('age', '未知')}
- 性别: {user_context.get('gender', '未知')}
- 身高体重: {user_context.get('height', '未知')}cm, {user_context.get('weight', '未知')}kg
- 活动水平: {user_context.get('activity_level', '未知')}
- 健康目标: {', '.join(user_context.get('health_goals', []))}
请分析:
1. 营养均衡性
2. 热量是否合适
3. 缺少的营养素
4. 建议改进的地方
5. 个性化建议
请以JSON格式返回分析结果。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.chat_completion(messages, temperature=0.2)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
return self._parse_nutrition_analysis(content)
else:
return self._get_fallback_nutrition_analysis(meal_data, user_context)
def analyze_physiological_state(self, profile: Dict, cycle_info: Dict) -> Dict[str, Any]:
"""分析生理状态"""
system_prompt = """
你是专业的女性健康专家,了解生理周期对营养需求的影响。
你的任务是:
1. 分析女性用户的生理周期状态
2. 评估当前阶段的营养需求
3. 提供针对性的饮食建议
4. 考虑情绪和食欲的变化
5. 提供个性化建议
分析时要:
- 基于科学的生理学知识
- 考虑个体差异
- 提供温和、实用的建议
- 保持专业和同理心
请以JSON格式返回分析结果包含以下字段
- physiological_state: 生理状态描述
- nutritional_needs: 营养需求列表
- foods_to_avoid: 需要避免的食物列表
- emotional_changes: 情绪变化描述
- appetite_changes: 食欲变化描述
- recommendations: 个性化建议列表
- confidence: 置信度(0-1)
"""
user_prompt = f"""
作为专业的女性健康专家,请分析以下用户的生理状态和营养需求:
用户信息:
- 年龄: {profile.get('age', '未知')}
- 身高体重: {profile.get('height', '未知')}cm, {profile.get('weight', '未知')}kg
- 月经周期长度: {profile.get('menstrual_cycle_length', '未知')}
- 上次月经: {profile.get('last_period_date', '未知')}
当前生理周期状态:
- 周期阶段: {cycle_info.get('phase', '未知')}
- 距离下次月经: {cycle_info.get('days_to_next_period', '未知')}
请分析:
1. 当前生理状态对营养需求的影响
2. 建议补充的营养素
3. 需要避免的食物
4. 情绪和食欲的变化
5. 个性化建议
请以JSON格式返回分析结果。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.chat_completion(messages, temperature=0.2)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
return self._parse_physiological_analysis(content, cycle_info)
else:
return self._get_fallback_physiological_analysis(cycle_info)
def generate_meal_suggestion(self, meal_type: str, preferences: Dict, user_context: Dict) -> Dict[str, Any]:
"""生成餐食建议"""
system_prompt = """
你是一个专业的营养师和厨师,擅长根据用户需求推荐合适的餐食。
你的任务是:
1. 根据用户的口味偏好推荐食物
2. 考虑饮食限制和过敏情况
3. 提供营养均衡的建议
4. 考虑用户的健康目标
5. 提供实用的制作建议
推荐时要:
- 基于营养学原理
- 考虑用户的个人喜好
- 提供多样化的选择
- 保持实用性和可操作性
请以JSON格式返回建议包含以下字段
- recommended_foods: 推荐食物列表
- reasoning: 推荐理由
- nutrition_tips: 营养搭配建议列表
- cooking_suggestions: 制作建议列表
- confidence: 置信度(0-1)
"""
user_prompt = f"""
请为以下用户推荐{meal_type}
用户信息:
- 姓名: {user_context.get('name', '未知')}
- 年龄: {user_context.get('age', '未知')}
- 性别: {user_context.get('gender', '未知')}
- 身高体重: {user_context.get('height', '未知')}cm, {user_context.get('weight', '未知')}kg
口味偏好: {json.dumps(user_context.get('taste_preferences', {}), ensure_ascii=False)}
饮食限制: 过敏({', '.join(user_context.get('allergies', []))}), 不喜欢({', '.join(user_context.get('dislikes', []))})
健康目标: {', '.join(user_context.get('health_goals', []))}
特殊偏好: {json.dumps(preferences, ensure_ascii=False)}
请推荐:
1. 3-5种适合的食物
2. 推荐理由
3. 营养搭配建议
4. 制作建议
请以JSON格式返回建议。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.chat_completion(messages, temperature=0.4)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
return self._parse_meal_suggestion(content)
else:
return self._get_fallback_meal_suggestion(meal_type, user_context)
def _format_meal_history(self, meals: List[Dict]) -> str:
"""格式化餐食历史"""
if not meals:
return "暂无饮食记录"
formatted = []
for meal in meals:
foods = ', '.join(meal.get('foods', []))
satisfaction = meal.get('satisfaction_score', '未知')
formatted.append(f"- {meal.get('date', '')} {meal.get('meal_type', '')}: {foods} (满意度: {satisfaction})")
return '\n'.join(formatted)
def _format_feedback_history(self, feedbacks: List[Dict]) -> str:
"""格式化反馈历史"""
if not feedbacks:
return "暂无反馈记录"
formatted = []
for feedback in feedbacks:
recommended = ', '.join(feedback.get('recommended_foods', []))
choice = feedback.get('user_choice', '未知')
feedback_type = feedback.get('feedback_type', '未知')
formatted.append(f"- 推荐: {recommended} | 选择: {choice} | 反馈: {feedback_type}")
return '\n'.join(formatted)
def _parse_analysis_result(self, analysis_text: str) -> Dict[str, Any]:
"""解析分析结果"""
try:
# 尝试提取JSON部分
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'user_intent': result_dict.get('user_intent', ''),
'emotional_state': result_dict.get('emotional_state', ''),
'nutritional_needs': result_dict.get('nutritional_needs', []),
'recommended_foods': result_dict.get('recommended_foods', []),
'reasoning': result_dict.get('reasoning', ''),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
logger.error(f"解析分析结果失败: {e}")
return self._get_fallback_analysis("", {})
def _parse_nutrition_analysis(self, analysis_text: str) -> Dict[str, Any]:
"""解析营养分析结果"""
try:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'nutrition_balance': result_dict.get('nutrition_balance', ''),
'calorie_assessment': result_dict.get('calorie_assessment', ''),
'missing_nutrients': result_dict.get('missing_nutrients', []),
'improvements': result_dict.get('improvements', []),
'recommendations': result_dict.get('recommendations', []),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
logger.error(f"解析营养分析结果失败: {e}")
return self._get_fallback_nutrition_analysis({}, {})
def _parse_physiological_analysis(self, analysis_text: str, cycle_info: Dict) -> Dict[str, Any]:
"""解析生理状态分析结果"""
try:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'physiological_state': result_dict.get('physiological_state', cycle_info.get('phase', '')),
'nutritional_needs': result_dict.get('nutritional_needs', []),
'foods_to_avoid': result_dict.get('foods_to_avoid', []),
'emotional_changes': result_dict.get('emotional_changes', ''),
'appetite_changes': result_dict.get('appetite_changes', ''),
'recommendations': result_dict.get('recommendations', []),
'cycle_info': cycle_info,
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
logger.error(f"解析生理分析结果失败: {e}")
return self._get_fallback_physiological_analysis(cycle_info)
def _parse_meal_suggestion(self, suggestion_text: str) -> Dict[str, Any]:
"""解析餐食建议结果"""
try:
start_idx = suggestion_text.find('{')
end_idx = suggestion_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = suggestion_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'recommended_foods': result_dict.get('recommended_foods', []),
'reasoning': result_dict.get('reasoning', ''),
'nutrition_tips': result_dict.get('nutrition_tips', []),
'cooking_suggestions': result_dict.get('cooking_suggestions', []),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
logger.error(f"解析餐食建议结果失败: {e}")
return self._get_fallback_meal_suggestion("lunch", {})
def _get_fallback_analysis(self, user_input: str, user_context: Dict) -> Dict[str, Any]:
"""获取备用分析结果"""
return {
'success': True,
'user_intent': '需要饮食建议',
'emotional_state': '正常',
'nutritional_needs': ['均衡营养'],
'recommended_foods': ['米饭', '蔬菜', '蛋白质'],
'reasoning': '基于基础营养需求',
'confidence': 0.3
}
def _get_fallback_nutrition_analysis(self, meal_data: Dict, user_context: Dict) -> Dict[str, Any]:
"""获取备用营养分析结果"""
return {
'success': True,
'nutrition_balance': '基本均衡',
'calorie_assessment': '适中',
'missing_nutrients': [],
'improvements': ['增加蔬菜摄入'],
'recommendations': ['保持均衡饮食'],
'confidence': 0.3
}
def _get_fallback_physiological_analysis(self, cycle_info: Dict) -> Dict[str, Any]:
"""获取备用生理状态分析结果"""
return {
'success': True,
'physiological_state': cycle_info.get('phase', '正常'),
'nutritional_needs': ['均衡营养'],
'foods_to_avoid': [],
'emotional_changes': '正常',
'appetite_changes': '正常',
'recommendations': ['保持规律饮食'],
'cycle_info': cycle_info,
'confidence': 0.3
}
def _get_fallback_meal_suggestion(self, meal_type: str, user_context: Dict) -> Dict[str, Any]:
"""获取备用餐食建议结果"""
return {
'success': True,
'recommended_foods': ['米饭', '蔬菜', '蛋白质'],
'reasoning': '营养均衡的基础搭配',
'nutrition_tips': ['注意营养搭配'],
'cooking_suggestions': ['简单烹饪'],
'confidence': 0.3
}
# 千问配置 - 从环境变量获取
def get_qwen_config() -> LLMConfig:
"""获取千问配置"""
api_key = os.getenv('QWEN_API_KEY', 'sk-c0dbefa1718d46eaa897199135066f00')
base_url = os.getenv('QWEN_BASE_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
model = os.getenv('QWEN_MODEL', 'qwen-plus-latest')
return LLMConfig(
provider="qwen",
api_key=api_key,
base_url=base_url,
model=model,
temperature=0.7,
max_tokens=2000
)
# 全局千问客户端实例
qwen_client: Optional[QwenLLMClient] = None
def get_qwen_client() -> QwenLLMClient:
"""获取千问客户端实例"""
global qwen_client
if qwen_client is None:
config = get_qwen_config()
qwen_client = QwenLLMClient(config)
return qwen_client
# 便捷函数
def analyze_user_intent_with_qwen(user_input: str, user_context: Dict) -> Dict[str, Any]:
"""使用千问分析用户意图"""
client = get_qwen_client()
return client.analyze_user_intent(user_input, user_context)
def analyze_nutrition_with_qwen(meal_data: Dict, user_context: Dict) -> Dict[str, Any]:
"""使用千问分析营养状况"""
client = get_qwen_client()
return client.analyze_nutrition(meal_data, user_context)
def analyze_physiological_state_with_qwen(profile: Dict, cycle_info: Dict) -> Dict[str, Any]:
"""使用千问分析生理状态"""
client = get_qwen_client()
return client.analyze_physiological_state(profile, cycle_info)
def generate_meal_suggestion_with_qwen(meal_type: str, preferences: Dict, user_context: Dict) -> Dict[str, Any]:
"""使用千问生成餐食建议"""
client = get_qwen_client()
return client.generate_meal_suggestion(meal_type, preferences, user_context)
if __name__ == "__main__":
# 测试千问大模型集成
print("测试千问大模型集成...")
# 测试用户意图分析
user_input = "我今天有点累,想吃点甜的,但是又怕胖"
user_context = {
'name': '小美',
'age': 25,
'gender': '',
'height': 165,
'weight': 55,
'taste_preferences': {'sweet': 4, 'salty': 3, 'spicy': 2},
'allergies': ['花生'],
'dislikes': ['内脏'],
'recent_meals': [],
'feedback_history': []
}
result = analyze_user_intent_with_qwen(user_input, user_context)
print(f"用户意图分析结果: {result}")
print("千问大模型集成测试完成!")

249
main.py Normal file
View File

@@ -0,0 +1,249 @@
"""
主应用入口 - 个性化饮食推荐助手
基于基座架构的完整应用
"""
import sys
import os
from pathlib import Path
from typing import Optional
import logging
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from core.base import BaseConfig, AppCore, ModuleManager, ModuleType, initialize_app, cleanup_app
from modules.data_collection import DataCollectionModule
from modules.ai_analysis import AIAnalysisModule
from modules.recommendation_engine import RecommendationEngine
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
from gui.main_window import MainWindow
import tkinter as tk
from tkinter import messagebox
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/app.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class DietRecommendationApp:
"""饮食推荐应用主类"""
def __init__(self):
self.config = self._load_config()
self.app_core: Optional[AppCore] = None
self.main_window: Optional[MainWindow] = None
self.is_running = False
def _load_config(self) -> BaseConfig:
"""加载配置"""
config = BaseConfig()
# 从环境变量加载API密钥
config.qwen_api_key = os.getenv('QWEN_API_KEY')
# 从.env文件加载配置
env_file = Path('.env')
if env_file.exists():
try:
from dotenv import load_dotenv
load_dotenv()
config.qwen_api_key = os.getenv('QWEN_API_KEY')
except Exception:
# 如果.env文件有编码问题跳过加载
pass
return config
def initialize(self) -> bool:
"""初始化应用"""
try:
logger.info("正在初始化饮食推荐应用...")
# 创建必要的目录
self._create_directories()
# 初始化应用核心
if not initialize_app(self.config):
logger.error("应用核心初始化失败")
return False
self.app_core = AppCore(self.config)
# 注册所有模块
if not self._register_modules():
logger.error("模块注册失败")
return False
# 启动应用核心
if not self.app_core.start():
logger.error("应用核心启动失败")
return False
self.is_running = True
logger.info("饮食推荐应用初始化成功")
return True
except Exception as e:
logger.error(f"应用初始化失败: {e}")
return False
def _create_directories(self):
"""创建必要的目录"""
directories = [
'data',
'data/users',
'data/training',
'models',
'logs',
'gui'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
def _register_modules(self) -> bool:
"""注册所有模块"""
try:
module_manager = ModuleManager(self.config)
# 注册数据采集模块
data_collection_module = DataCollectionModule(self.config)
if not module_manager.register_module(data_collection_module):
logger.error("数据采集模块注册失败")
return False
# 注册AI分析模块
ai_analysis_module = AIAnalysisModule(self.config)
if not module_manager.register_module(ai_analysis_module):
logger.error("AI分析模块注册失败")
return False
# 注册推荐引擎模块
recommendation_module = RecommendationEngine(self.config)
if not module_manager.register_module(recommendation_module):
logger.error("推荐引擎模块注册失败")
return False
# 注册OCR热量识别模块
ocr_module = OCRCalorieRecognitionModule(self.config)
if not module_manager.register_module(ocr_module):
logger.error("OCR热量识别模块注册失败")
return False
# 初始化模块管理器
if not module_manager.initialize_all():
logger.error("模块管理器初始化失败")
return False
# 将模块管理器设置到应用核心
self.app_core.module_manager = module_manager
logger.info("所有模块注册成功")
return True
except Exception as e:
logger.error(f"模块注册失败: {e}")
return False
def run_gui(self):
"""运行GUI界面"""
try:
logger.info("启动移动端GUI界面...")
# 使用移动端界面,传递应用核心
from gui.mobile_main_window import MobileMainWindow
self.main_window = MobileMainWindow(self.app_core)
# 启动GUI主循环
self.main_window.run()
except Exception as e:
logger.error(f"GUI启动失败: {e}")
messagebox.showerror("错误", f"GUI启动失败: {str(e)}")
def _on_closing(self):
"""窗口关闭事件处理"""
try:
if messagebox.askokcancel("退出", "确定要退出应用吗?"):
self.shutdown()
except Exception as e:
logger.error(f"关闭应用失败: {e}")
sys.exit(1)
def shutdown(self):
"""关闭应用"""
try:
logger.info("正在关闭应用...")
# 关闭GUI
if self.main_window:
self.main_window.destroy()
# 关闭应用核心
if self.app_core:
self.app_core.stop()
# 清理资源
cleanup_app()
self.is_running = False
logger.info("应用关闭完成")
except Exception as e:
logger.error(f"应用关闭失败: {e}")
finally:
sys.exit(0)
def get_app_status(self) -> dict:
"""获取应用状态"""
if not self.app_core:
return {"status": "not_initialized"}
module_status = self.app_core.module_manager.get_module_status()
return {
"status": "running" if self.is_running else "stopped",
"modules": module_status,
"config": {
"app_name": self.config.app_name,
"version": self.config.version,
"debug": self.config.debug
}
}
def main():
"""主函数"""
try:
# 创建应用实例
app = DietRecommendationApp()
# 初始化应用
if not app.initialize():
logger.error("应用初始化失败,退出")
sys.exit(1)
# 运行GUI
app.run_gui()
except KeyboardInterrupt:
logger.info("用户中断,正在退出...")
sys.exit(0)
except Exception as e:
logger.error(f"应用运行失败: {e}")
import traceback
logger.error(f"详细错误信息: {traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
main()

778
modules/ai_analysis.py Normal file
View File

@@ -0,0 +1,778 @@
"""
AI分析模块 - 基于基座架构
集成大模型进行用户需求分析和营养建议
"""
from typing import Dict, List, Optional, Any
import json
import requests
from datetime import datetime, date
from core.base import BaseModule, ModuleType, UserData, AnalysisResult, BaseConfig
import os
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
# 如果没有.env文件或加载失败使用默认配置
pass
# 导入千问客户端
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent))
from llm_integration.qwen_client import get_qwen_client, analyze_user_intent_with_qwen, analyze_nutrition_with_qwen
class AIAnalysisModule(BaseModule):
"""AI分析模块"""
def __init__(self, config: BaseConfig):
super().__init__(config, ModuleType.USER_ANALYSIS)
self.qwen_client = None
self.analysis_templates = self._load_analysis_templates()
def initialize(self) -> bool:
"""初始化模块"""
try:
self.logger.info("AI分析模块初始化中...")
# 初始化千问客户端
self.qwen_client = get_qwen_client()
self.logger.info("千问客户端初始化成功")
self.is_initialized = True
self.logger.info("AI分析模块初始化完成")
return True
except Exception as e:
self.logger.error(f"AI分析模块初始化失败: {e}")
return False
def process(self, input_data: Any, user_data: UserData) -> AnalysisResult:
"""处理AI分析请求"""
try:
analysis_type = input_data.get('type', 'user_intent')
if analysis_type == 'user_intent':
result = self._analyze_user_intent(input_data, user_data)
elif analysis_type == 'nutrition_analysis':
result = self._analyze_nutrition(input_data, user_data)
elif analysis_type == 'calorie_estimation':
result = self._estimate_calories(input_data, user_data)
elif analysis_type == 'physiological_state':
result = self._analyze_physiological_state(input_data, user_data)
elif analysis_type == 'meal_suggestion':
result = self._generate_meal_suggestion(input_data, user_data)
else:
result = self._create_error_result("未知的分析类型")
return AnalysisResult(
module_type=self.module_type,
user_id=user_data.user_id,
input_data=input_data,
result=result,
confidence=result.get('confidence', 0.5)
)
except Exception as e:
self.logger.error(f"处理AI分析请求失败: {e}")
return self._create_error_result(str(e))
def _analyze_user_intent(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""分析用户意图"""
user_input = input_data.get('user_input', '')
if not self.qwen_client:
return self._get_fallback_intent_analysis(user_input, user_data)
try:
# 构建用户上下文
user_context = {
'name': user_data.profile.get('name', '未知'),
'age': user_data.profile.get('age', '未知'),
'gender': user_data.profile.get('gender', '未知'),
'height': user_data.profile.get('height', '未知'),
'weight': user_data.profile.get('weight', '未知'),
'activity_level': user_data.profile.get('activity_level', '未知'),
'taste_preferences': user_data.profile.get('taste_preferences', {}),
'allergies': user_data.profile.get('allergies', []),
'dislikes': user_data.profile.get('dislikes', []),
'dietary_preferences': user_data.profile.get('dietary_preferences', []),
'recent_meals': user_data.meals[-3:] if user_data.meals else [],
'feedback_history': user_data.feedback[-5:] if user_data.feedback else []
}
# 使用千问分析用户意图
result = analyze_user_intent_with_qwen(user_input, user_context)
return result
except Exception as e:
self.logger.error(f"用户意图分析失败: {e}")
return self._get_fallback_intent_analysis(user_input, user_data)
def _analyze_nutrition(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""分析营养状况"""
meal_data = input_data.get('meal_data', {})
if not self.qwen_client:
return self._get_fallback_nutrition_analysis(meal_data, user_data)
try:
# 构建用户上下文
user_context = {
'age': user_data.profile.get('age', '未知'),
'gender': user_data.profile.get('gender', '未知'),
'height': user_data.profile.get('height', '未知'),
'weight': user_data.profile.get('weight', '未知'),
'activity_level': user_data.profile.get('activity_level', '未知'),
'health_goals': user_data.profile.get('health_goals', [])
}
# 使用千问分析营养状况
result = analyze_nutrition_with_qwen(meal_data, user_context)
return result
except Exception as e:
self.logger.error(f"营养分析失败: {e}")
return self._get_fallback_nutrition_analysis(meal_data, user_data)
def _estimate_calories(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""估算食物热量"""
food_data = input_data.get('food_data', {})
food_name = food_data.get('food_name', '')
quantity = food_data.get('quantity', '')
if not food_name or not quantity:
return self._create_error_result("缺少食物名称或分量信息")
try:
# 基础热量数据库
calorie_db = {
"米饭": 130, "面条": 110, "包子": 200, "饺子": 250, "馒头": 220, "面包": 250,
"鸡蛋": 150, "牛奶": 60, "豆浆": 30, "酸奶": 80, "苹果": 50, "香蕉": 90,
"鸡肉": 165, "牛肉": 250, "猪肉": 300, "鱼肉": 120, "豆腐": 80, "青菜": 20,
"西红柿": 20, "黄瓜": 15, "胡萝卜": 40, "土豆": 80, "红薯": 100, "玉米": 90
}
# 获取基础热量
base_calories = calorie_db.get(food_name, 100) # 默认100卡路里
# 简单的分量解析
quantity_lower = quantity.lower()
if '' in quantity_lower:
multiplier = 1.0
elif 'g' in quantity_lower or '' in quantity_lower:
# 假设一碗米饭约200g
multiplier = 0.5
elif '' in quantity_lower:
multiplier = 1.0
else:
multiplier = 1.0
# 计算总热量
total_calories = base_calories * multiplier
return {
'success': True,
'calories': total_calories,
'food_name': food_name,
'quantity': quantity,
'base_calories': base_calories,
'multiplier': multiplier,
'confidence': 0.8
}
except Exception as e:
self.logger.error(f"热量估算失败: {e}")
return self._create_error_result(f"热量估算失败: {str(e)}")
def _analyze_physiological_state(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""分析生理状态"""
current_date = input_data.get('current_date', datetime.now().strftime('%Y-%m-%d'))
if not user_data.profile.get('is_female', False):
return {
'success': True,
'physiological_state': 'normal',
'needs': [],
'recommendations': [],
'confidence': 0.8
}
try:
cycle_info = self._calculate_menstrual_cycle(user_data.profile, current_date)
if not self.qwen_client:
return self._get_fallback_physiological_analysis(cycle_info)
prompt = self._build_physiological_analysis_prompt(user_data.profile, cycle_info)
messages = [
{"role": "system", "content": self._get_physiological_analysis_system_prompt()},
{"role": "user", "content": prompt}
]
response = self.qwen_client.chat_completion(messages, temperature=0.2, max_tokens=800)
if response and 'choices' in response:
analysis_text = response['choices'][0]['message']['content']
else:
return self._get_fallback_physiological_analysis(cycle_info)
return self._parse_physiological_analysis(analysis_text, cycle_info)
except Exception as e:
self.logger.error(f"生理状态分析失败: {e}")
return self._get_fallback_physiological_analysis({})
def _generate_meal_suggestion(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""生成餐食建议"""
meal_type = input_data.get('meal_type', 'lunch')
preferences = input_data.get('preferences', {})
if not self.qwen_client:
return self._get_fallback_meal_suggestion(meal_type, user_data)
try:
prompt = self._build_meal_suggestion_prompt(meal_type, preferences, user_data)
messages = [
{"role": "system", "content": self._get_meal_suggestion_system_prompt()},
{"role": "user", "content": prompt}
]
response = self.qwen_client.chat_completion(messages, temperature=0.4, max_tokens=1000)
if response and 'choices' in response:
suggestion_text = response['choices'][0]['message']['content']
else:
return self._get_fallback_meal_suggestion(meal_type, user_data)
return self._parse_meal_suggestion(suggestion_text)
except Exception as e:
self.logger.error(f"餐食建议生成失败: {e}")
return self._get_fallback_meal_suggestion(meal_type, user_data)
def _build_intent_analysis_prompt(self, user_input: str, user_data: UserData) -> str:
"""构建意图分析提示词"""
return f"""
请分析以下用户输入的真实意图和需求:
用户输入: "{user_input}"
用户背景:
- 姓名: {user_data.profile.get('name', '未知')}
- 年龄: {user_data.profile.get('age', '未知')}
- 性别: {user_data.profile.get('gender', '未知')}
- 身高体重: {user_data.profile.get('height', '未知')}cm, {user_data.profile.get('weight', '未知')}kg
口味偏好: {json.dumps(user_data.profile.get('taste_preferences', {}), ensure_ascii=False)}
饮食限制: {', '.join(user_data.profile.get('allergies', []) + user_data.profile.get('dislikes', []))}
最近饮食记录: {self._format_recent_meals(user_data.meals[-3:])}
请分析:
1. 用户的真实意图(饿了、馋了、需要特定营养等)
2. 情绪状态(压力、开心、疲惫等)
3. 营养需求
4. 推荐的食物类型
5. 推荐理由
请以JSON格式返回分析结果。
"""
def _build_nutrition_analysis_prompt(self, meal_data: Dict, user_data: UserData) -> str:
"""构建营养分析提示词"""
return f"""
请分析以下餐食的营养状况:
餐食信息:
- 食物: {', '.join(meal_data.get('foods', []))}
- 分量: {', '.join(meal_data.get('quantities', []))}
- 热量: {meal_data.get('calories', '未知')}卡路里
用户信息:
- 年龄: {user_data.profile.get('age', '未知')}
- 性别: {user_data.profile.get('gender', '未知')}
- 身高体重: {user_data.profile.get('height', '未知')}cm, {user_data.profile.get('weight', '未知')}kg
- 活动水平: {user_data.profile.get('activity_level', '未知')}
- 健康目标: {', '.join(user_data.profile.get('health_goals', []))}
请分析:
1. 营养均衡性
2. 热量是否合适
3. 缺少的营养素
4. 建议改进的地方
5. 个性化建议
请以JSON格式返回分析结果。
"""
def _build_physiological_analysis_prompt(self, profile: Dict, cycle_info: Dict) -> str:
"""构建生理状态分析提示词"""
return f"""
作为专业的女性健康专家,请分析以下用户的生理状态:
用户信息:
- 年龄: {profile.get('age', '未知')}
- 身高体重: {profile.get('height', '未知')}cm, {profile.get('weight', '未知')}kg
- 月经周期长度: {profile.get('menstrual_cycle_length', '未知')}
- 上次月经: {profile.get('last_period_date', '未知')}
当前生理周期状态:
- 周期阶段: {cycle_info.get('phase', '未知')}
- 距离下次月经: {cycle_info.get('days_to_next_period', '未知')}
请分析:
1. 当前生理状态对营养需求的影响
2. 建议补充的营养素
3. 需要避免的食物
4. 情绪和食欲的变化
5. 个性化建议
请以JSON格式返回分析结果。
"""
def _build_meal_suggestion_prompt(self, meal_type: str, preferences: Dict, user_data: UserData) -> str:
"""构建餐食建议提示词"""
return f"""
请为以下用户推荐{meal_type}
用户信息:
- 姓名: {user_data.profile.get('name', '未知')}
- 年龄: {user_data.profile.get('age', '未知')}
- 性别: {user_data.profile.get('gender', '未知')}
- 身高体重: {user_data.profile.get('height', '未知')}cm, {user_data.profile.get('weight', '未知')}kg
口味偏好: {json.dumps(user_data.profile.get('taste_preferences', {}), ensure_ascii=False)}
饮食限制: 过敏({', '.join(user_data.profile.get('allergies', []))}), 不喜欢({', '.join(user_data.profile.get('dislikes', []))})
健康目标: {', '.join(user_data.profile.get('health_goals', []))}
特殊偏好: {json.dumps(preferences, ensure_ascii=False)}
请推荐:
1. 3-5种适合的食物
2. 推荐理由
3. 营养搭配建议
4. 制作建议
请以JSON格式返回建议。
"""
def _get_intent_analysis_system_prompt(self) -> str:
"""获取意图分析系统提示词"""
return """
你是一个专业的营养师和心理学专家,擅长分析用户的饮食需求和心理状态。
你的任务是:
1. 深度理解用户的真实需求,不仅仅是表面的话语
2. 考虑用户的生理状态、情绪状态、历史偏好等多维度因素
3. 提供个性化的饮食建议
4. 特别关注女性用户的生理周期对饮食需求的影响
分析时要:
- 透过现象看本质,理解用户的真实意图
- 综合考虑生理、心理、社会等多重因素
- 提供科学、实用、个性化的建议
- 保持专业性和同理心
返回格式必须是有效的JSON包含所有必需字段。
"""
def _get_nutrition_analysis_system_prompt(self) -> str:
"""获取营养分析系统提示词"""
return """
你是一个专业的营养师,擅长分析餐食的营养价值和健康建议。
你的任务是:
1. 分析餐食的营养均衡性
2. 评估热量是否合适
3. 识别缺少的营养素
4. 提供改进建议
5. 考虑用户的个人情况
分析时要:
- 基于科学的营养学知识
- 考虑用户的年龄、性别、体重、活动水平等因素
- 提供具体可行的建议
- 保持客观和专业
返回格式必须是有效的JSON包含所有必需字段。
"""
def _get_physiological_analysis_system_prompt(self) -> str:
"""获取生理状态分析系统提示词"""
return """
你是专业的女性健康专家,了解生理周期对营养需求的影响。
你的任务是:
1. 分析女性用户的生理周期状态
2. 评估当前阶段的营养需求
3. 提供针对性的饮食建议
4. 考虑情绪和食欲的变化
5. 提供个性化建议
分析时要:
- 基于科学的生理学知识
- 考虑个体差异
- 提供温和、实用的建议
- 保持专业和同理心
返回格式必须是有效的JSON包含所有必需字段。
"""
def _get_meal_suggestion_system_prompt(self) -> str:
"""获取餐食建议系统提示词"""
return """
你是一个专业的营养师和厨师,擅长根据用户需求推荐合适的餐食。
你的任务是:
1. 根据用户的口味偏好推荐食物
2. 考虑饮食限制和过敏情况
3. 提供营养均衡的建议
4. 考虑用户的健康目标
5. 提供实用的制作建议
推荐时要:
- 基于营养学原理
- 考虑用户的个人喜好
- 提供多样化的选择
- 保持实用性和可操作性
返回格式必须是有效的JSON包含所有必需字段。
"""
def _calculate_menstrual_cycle(self, profile: Dict, current_date: str) -> Dict[str, Any]:
"""计算月经周期状态"""
try:
last_period = datetime.strptime(profile.get('last_period_date', ''), '%Y-%m-%d')
current = datetime.strptime(current_date, '%Y-%m-%d')
cycle_length = profile.get('menstrual_cycle_length', 28)
days_since_period = (current - last_period).days
days_to_next_period = cycle_length - (days_since_period % cycle_length)
# 判断周期阶段
if days_since_period % cycle_length < 5:
phase = "月经期"
elif days_since_period % cycle_length < 14:
phase = "卵泡期"
elif days_since_period % cycle_length < 18:
phase = "排卵期"
else:
phase = "黄体期"
return {
"phase": phase,
"days_since_period": days_since_period % cycle_length,
"days_to_next_period": days_to_next_period,
"is_ovulation": phase == "排卵期",
"cycle_length": cycle_length
}
except Exception:
return {
"phase": "未知",
"days_since_period": 0,
"days_to_next_period": 0,
"is_ovulation": False,
"cycle_length": 28
}
def _format_recent_meals(self, meals: List[Dict]) -> str:
"""格式化最近餐食"""
if not meals:
return "暂无饮食记录"
formatted = []
for meal in meals:
foods = ', '.join(meal.get('foods', []))
satisfaction = meal.get('satisfaction_score', '未知')
formatted.append(f"- {meal.get('date', '')} {meal.get('meal_type', '')}: {foods} (满意度: {satisfaction})")
return '\n'.join(formatted)
def _parse_intent_analysis(self, analysis_text: str) -> Dict[str, Any]:
"""解析意图分析结果"""
try:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'user_intent': result_dict.get('user_intent', ''),
'emotional_state': result_dict.get('emotional_state', ''),
'nutritional_needs': result_dict.get('nutritional_needs', []),
'recommended_foods': result_dict.get('recommended_foods', []),
'reasoning': result_dict.get('reasoning', ''),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
self.logger.error(f"解析意图分析结果失败: {e}")
return self._get_fallback_intent_analysis("", None)
def _parse_nutrition_analysis(self, analysis_text: str) -> Dict[str, Any]:
"""解析营养分析结果"""
try:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'nutrition_balance': result_dict.get('nutrition_balance', ''),
'calorie_assessment': result_dict.get('calorie_assessment', ''),
'missing_nutrients': result_dict.get('missing_nutrients', []),
'improvements': result_dict.get('improvements', []),
'recommendations': result_dict.get('recommendations', []),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
self.logger.error(f"解析营养分析结果失败: {e}")
return self._get_fallback_nutrition_analysis({}, None)
def _parse_physiological_analysis(self, analysis_text: str, cycle_info: Dict) -> Dict[str, Any]:
"""解析生理状态分析结果"""
try:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = analysis_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'physiological_state': result_dict.get('physiological_state', cycle_info.get('phase', '')),
'nutritional_needs': result_dict.get('nutritional_needs', []),
'foods_to_avoid': result_dict.get('foods_to_avoid', []),
'emotional_changes': result_dict.get('emotional_changes', ''),
'appetite_changes': result_dict.get('appetite_changes', ''),
'recommendations': result_dict.get('recommendations', []),
'cycle_info': cycle_info,
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
self.logger.error(f"解析生理分析结果失败: {e}")
return self._get_fallback_physiological_analysis(cycle_info)
def _parse_meal_suggestion(self, suggestion_text: str) -> Dict[str, Any]:
"""解析餐食建议结果"""
try:
start_idx = suggestion_text.find('{')
end_idx = suggestion_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = suggestion_text[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'recommended_foods': result_dict.get('recommended_foods', []),
'reasoning': result_dict.get('reasoning', ''),
'nutrition_tips': result_dict.get('nutrition_tips', []),
'cooking_suggestions': result_dict.get('cooking_suggestions', []),
'confidence': result_dict.get('confidence', 0.5)
}
except Exception as e:
self.logger.error(f"解析餐食建议结果失败: {e}")
return self._get_fallback_meal_suggestion("lunch", None)
def _get_fallback_intent_analysis(self, user_input: str, user_data: UserData) -> Dict[str, Any]:
"""获取备用意图分析结果"""
return {
'success': True,
'user_intent': '需要饮食建议',
'emotional_state': '正常',
'nutritional_needs': ['均衡营养'],
'recommended_foods': ['米饭', '蔬菜', '蛋白质'],
'reasoning': '基于基础营养需求',
'confidence': 0.3
}
def _get_fallback_nutrition_analysis(self, meal_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""获取备用营养分析结果"""
return {
'success': True,
'nutrition_balance': '基本均衡',
'calorie_assessment': '适中',
'missing_nutrients': [],
'improvements': ['增加蔬菜摄入'],
'recommendations': ['保持均衡饮食'],
'confidence': 0.3
}
def _get_fallback_physiological_analysis(self, cycle_info: Dict) -> Dict[str, Any]:
"""获取备用生理状态分析结果"""
return {
'success': True,
'physiological_state': cycle_info.get('phase', '正常'),
'nutritional_needs': ['均衡营养'],
'foods_to_avoid': [],
'emotional_changes': '正常',
'appetite_changes': '正常',
'recommendations': ['保持规律饮食'],
'cycle_info': cycle_info,
'confidence': 0.3
}
def _get_fallback_meal_suggestion(self, meal_type: str, user_data: UserData) -> Dict[str, Any]:
"""获取备用餐食建议结果"""
return {
'success': True,
'recommended_foods': ['米饭', '蔬菜', '蛋白质'],
'reasoning': '营养均衡的基础搭配',
'nutrition_tips': ['注意营养搭配'],
'cooking_suggestions': ['简单烹饪'],
'confidence': 0.3
}
def _load_analysis_templates(self) -> Dict[str, Dict]:
"""加载分析模板"""
return {
'intent_analysis': {
'description': '用户意图分析',
'required_fields': ['user_input']
},
'nutrition_analysis': {
'description': '营养状况分析',
'required_fields': ['meal_data']
},
'physiological_state': {
'description': '生理状态分析',
'required_fields': ['current_date']
},
'meal_suggestion': {
'description': '餐食建议生成',
'required_fields': ['meal_type']
}
}
def _create_error_result(self, error_message: str) -> Dict[str, Any]:
"""创建错误结果"""
return {
'success': False,
'error': error_message,
'message': f'AI分析失败: {error_message}',
'confidence': 0.0
}
def cleanup(self) -> bool:
"""清理资源"""
try:
self.logger.info("AI分析模块清理完成")
return True
except Exception as e:
self.logger.error(f"AI分析模块清理失败: {e}")
return False
# 便捷函数
def analyze_user_intent(user_id: str, user_input: str) -> Optional[Dict]:
"""分析用户意图"""
from core.base import get_app_core
app = get_app_core()
input_data = {
'type': 'user_intent',
'user_input': user_input
}
result = app.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id)
return result.result if result else None
def analyze_nutrition(user_id: str, meal_data: Dict) -> Optional[Dict]:
"""分析营养状况"""
from core.base import get_app_core
app = get_app_core()
input_data = {
'type': 'nutrition_analysis',
'meal_data': meal_data
}
result = app.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id)
return result.result if result else None
def analyze_physiological_state(user_id: str, current_date: str = None) -> Optional[Dict]:
"""分析生理状态"""
from core.base import get_app_core
from datetime import datetime
if current_date is None:
current_date = datetime.now().strftime('%Y-%m-%d')
app = get_app_core()
input_data = {
'type': 'physiological_state',
'current_date': current_date
}
result = app.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id)
return result.result if result else None
def generate_meal_suggestion(user_id: str, meal_type: str, preferences: Dict = None) -> Optional[Dict]:
"""生成餐食建议"""
from core.base import get_app_core
if preferences is None:
preferences = {}
app = get_app_core()
input_data = {
'type': 'meal_suggestion',
'meal_type': meal_type,
'preferences': preferences
}
result = app.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id)
return result.result if result else None
if __name__ == "__main__":
# 测试AI分析模块
from core.base import BaseConfig, initialize_app, cleanup_app
print("测试AI分析模块...")
# 初始化应用
config = BaseConfig()
if initialize_app(config):
print("✅ 应用初始化成功")
# 测试用户意图分析
test_user_id = "test_user_001"
user_input = "我今天有点累,想吃点甜的,但是又怕胖"
result = analyze_user_intent(test_user_id, user_input)
if result:
print(f"✅ 用户意图分析成功: {result.get('user_intent', '')}")
# 测试营养分析
meal_data = {
'foods': ['燕麦粥', '香蕉', '牛奶'],
'quantities': ['1碗', '1根', '200ml'],
'calories': 350.0
}
result = analyze_nutrition(test_user_id, meal_data)
if result:
print(f"✅ 营养分析成功: {result.get('nutrition_balance', '')}")
# 清理应用
cleanup_app()
print("✅ 应用清理完成")
else:
print("❌ 应用初始化失败")
print("AI分析模块测试完成")

367
modules/data_collection.py Normal file
View File

@@ -0,0 +1,367 @@
"""
数据采集模块 - 基于基座架构
负责收集用户数据、问卷和餐食记录
"""
from typing import Dict, List, Optional, Any
import json
from datetime import datetime, date
from core.base import BaseModule, ModuleType, UserData, AnalysisResult, BaseConfig
class DataCollectionModule(BaseModule):
"""数据采集模块"""
def __init__(self, config: BaseConfig):
super().__init__(config, ModuleType.DATA_COLLECTION)
self.questionnaire_templates = self._load_questionnaire_templates()
def initialize(self) -> bool:
"""初始化模块"""
try:
self.logger.info("数据采集模块初始化中...")
self.is_initialized = True
self.logger.info("数据采集模块初始化完成")
return True
except Exception as e:
self.logger.error(f"数据采集模块初始化失败: {e}")
return False
def process(self, input_data: Any, user_data: UserData) -> AnalysisResult:
"""处理数据采集请求"""
try:
request_type = input_data.get('type', 'unknown')
if request_type == 'questionnaire':
result = self._process_questionnaire(input_data, user_data)
elif request_type == 'meal_record':
result = self._process_meal_record(input_data, user_data)
elif request_type == 'feedback':
result = self._process_feedback(input_data, user_data)
else:
result = self._create_error_result("未知的请求类型")
return AnalysisResult(
module_type=self.module_type,
user_id=user_data.user_id,
input_data=input_data,
result=result,
confidence=0.9 if result.get('success', False) else 0.1
)
except Exception as e:
self.logger.error(f"处理数据采集请求失败: {e}")
return self._create_error_result(str(e))
def _process_questionnaire(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""处理问卷数据"""
questionnaire_type = input_data.get('questionnaire_type', 'basic')
answers = input_data.get('answers', {})
# 根据问卷类型处理答案
if questionnaire_type == 'basic':
processed_data = self._process_basic_questionnaire(answers)
elif questionnaire_type == 'taste':
processed_data = self._process_taste_questionnaire(answers)
elif questionnaire_type == 'physiological':
processed_data = self._process_physiological_questionnaire(answers)
else:
processed_data = answers
# 更新用户数据
user_data.profile.update(processed_data)
user_data.updated_at = datetime.now().isoformat()
return {
'success': True,
'processed_data': processed_data,
'message': f'{questionnaire_type}问卷处理完成'
}
def _process_meal_record(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""处理餐食记录"""
meal_data = {
'date': input_data.get('date', datetime.now().strftime('%Y-%m-%d')),
'meal_type': input_data.get('meal_type', 'unknown'),
'foods': input_data.get('foods', []),
'quantities': input_data.get('quantities', []),
'calories': input_data.get('calories'),
'satisfaction_score': input_data.get('satisfaction_score'),
'notes': input_data.get('notes', ''),
'timestamp': datetime.now().isoformat()
}
# 添加到用户餐食记录
user_data.meals.append(meal_data)
user_data.updated_at = datetime.now().isoformat()
return {
'success': True,
'meal_data': meal_data,
'message': '餐食记录保存成功'
}
def _process_feedback(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]:
"""处理用户反馈"""
feedback_data = {
'date': input_data.get('date', datetime.now().strftime('%Y-%m-%d')),
'recommended_foods': input_data.get('recommended_foods', []),
'user_choice': input_data.get('user_choice', ''),
'feedback_type': input_data.get('feedback_type', 'unknown'),
'satisfaction_score': input_data.get('satisfaction_score'),
'notes': input_data.get('notes', ''),
'timestamp': datetime.now().isoformat()
}
# 添加到用户反馈记录
user_data.feedback.append(feedback_data)
user_data.updated_at = datetime.now().isoformat()
return {
'success': True,
'feedback_data': feedback_data,
'message': '反馈记录保存成功'
}
def _process_basic_questionnaire(self, answers: Dict) -> Dict[str, Any]:
"""处理基础信息问卷"""
return {
'name': answers.get('name', ''),
'age': int(answers.get('age', 0)),
'gender': answers.get('gender', ''),
'height': float(answers.get('height', 0)),
'weight': float(answers.get('weight', 0)),
'activity_level': answers.get('activity_level', ''),
'health_goals': answers.get('health_goals', [])
}
def _process_taste_questionnaire(self, answers: Dict) -> Dict[str, Any]:
"""处理口味偏好问卷"""
return {
'taste_preferences': {
'sweet': int(answers.get('sweet', 3)),
'salty': int(answers.get('salty', 3)),
'spicy': int(answers.get('spicy', 3)),
'sour': int(answers.get('sour', 3)),
'bitter': int(answers.get('bitter', 3)),
'umami': int(answers.get('umami', 3))
},
'dietary_preferences': answers.get('dietary_preferences', []),
'allergies': answers.get('allergies', []),
'dislikes': answers.get('dislikes', [])
}
def _process_physiological_questionnaire(self, answers: Dict) -> Dict[str, Any]:
"""处理生理信息问卷"""
return {
'is_female': answers.get('gender') == '',
'menstrual_cycle_length': int(answers.get('menstrual_cycle_length', 28)),
'last_period_date': answers.get('last_period_date', ''),
'ovulation_symptoms': answers.get('ovulation_symptoms', []),
'zodiac_sign': answers.get('zodiac_sign', ''),
'personality_traits': answers.get('personality_traits', [])
}
def _load_questionnaire_templates(self) -> Dict[str, Dict]:
"""加载问卷模板"""
return {
'basic': {
'title': '基本信息问卷',
'questions': {
'name': {'question': '您的姓名', 'type': 'text'},
'age': {'question': '您的年龄', 'type': 'number', 'min': 1, 'max': 120},
'gender': {'question': '性别', 'type': 'select', 'options': ['', '']},
'height': {'question': '身高 (cm)', 'type': 'number', 'min': 100, 'max': 250},
'weight': {'question': '体重 (kg)', 'type': 'number', 'min': 30, 'max': 200},
'activity_level': {
'question': '日常活动水平',
'type': 'select',
'options': ['久坐', '轻度活动', '中度活动', '高度活动', '极高活动']
},
'health_goals': {
'question': '健康目标 (可多选)',
'type': 'checkbox',
'options': ['减肥', '增肌', '维持体重', '提高免疫力', '改善消化']
}
}
},
'taste': {
'title': '口味偏好问卷',
'questions': {
'sweet': {'question': '甜味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'salty': {'question': '咸味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'spicy': {'question': '辣味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'sour': {'question': '酸味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'bitter': {'question': '苦味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'umami': {'question': '鲜味偏好 (1-5分)', 'type': 'scale', 'min': 1, 'max': 5},
'dietary_preferences': {
'question': '饮食限制 (可多选)',
'type': 'checkbox',
'options': ['素食', '纯素食', '无麸质', '无乳制品', '无坚果', '低钠', '低碳水', '无糖']
},
'allergies': {
'question': '过敏食物 (可多选)',
'type': 'checkbox',
'options': ['花生', '坚果', '海鲜', '鸡蛋', '牛奶', '大豆', '小麦', '无过敏']
},
'dislikes': {
'question': '不喜欢的食物类型',
'type': 'checkbox',
'options': ['内脏', '海鲜', '蘑菇', '香菜', '洋葱', '大蒜', '辛辣食物', '甜食']
}
}
},
'physiological': {
'title': '生理信息问卷',
'questions': {
'menstrual_cycle_length': {
'question': '月经周期长度 (天)',
'type': 'number',
'min': 20,
'max': 40,
'optional': True
},
'last_period_date': {
'question': '上次月经日期',
'type': 'date',
'optional': True
},
'ovulation_symptoms': {
'question': '排卵期症状 (可多选)',
'type': 'checkbox',
'options': ['乳房胀痛', '情绪波动', '食欲变化', '疲劳', '无特殊症状'],
'optional': True
},
'zodiac_sign': {
'question': '星座',
'type': 'select',
'options': ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座',
'天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座'],
'optional': True
},
'personality_traits': {
'question': '性格特征 (可多选)',
'type': 'checkbox',
'options': ['外向', '内向', '理性', '感性', '冒险', '保守', '创新', '传统'],
'optional': True
}
}
}
}
def get_questionnaire_template(self, questionnaire_type: str) -> Optional[Dict]:
"""获取问卷模板"""
return self.questionnaire_templates.get(questionnaire_type)
def get_all_questionnaire_types(self) -> List[str]:
"""获取所有问卷类型"""
return list(self.questionnaire_templates.keys())
def _create_error_result(self, error_message: str) -> Dict[str, Any]:
"""创建错误结果"""
return {
'success': False,
'error': error_message,
'message': f'数据采集失败: {error_message}'
}
def cleanup(self) -> bool:
"""清理资源"""
try:
self.logger.info("数据采集模块清理完成")
return True
except Exception as e:
self.logger.error(f"数据采集模块清理失败: {e}")
return False
# 便捷函数
def collect_questionnaire_data(user_id: str, questionnaire_type: str, answers: Dict) -> bool:
"""收集问卷数据"""
from core.base import get_app_core
app = get_app_core()
input_data = {
'type': 'questionnaire',
'questionnaire_type': questionnaire_type,
'answers': answers
}
result = app.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id)
return result and result.result.get('success', False)
def record_meal(user_id: str, meal_data: Dict) -> bool:
"""记录餐食"""
from core.base import get_app_core
app = get_app_core()
input_data = {
'type': 'meal_record',
**meal_data
}
result = app.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id)
return result and result.result.get('success', False)
def record_feedback(user_id: str, feedback_data: Dict) -> bool:
"""记录反馈"""
from core.base import get_app_core
app = get_app_core()
input_data = {
'type': 'feedback',
**feedback_data
}
result = app.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id)
return result and result.result.get('success', False)
if __name__ == "__main__":
# 测试数据采集模块
from core.base import BaseConfig, initialize_app, cleanup_app
print("测试数据采集模块...")
# 初始化应用
config = BaseConfig()
if initialize_app(config):
print("✅ 应用初始化成功")
# 测试问卷数据收集
test_user_id = "test_user_001"
questionnaire_answers = {
'name': '小美',
'age': 25,
'gender': '',
'height': 165,
'weight': 55,
'activity_level': '中度活动',
'health_goals': ['维持体重', '提高免疫力']
}
if collect_questionnaire_data(test_user_id, 'basic', questionnaire_answers):
print("✅ 基础问卷数据收集成功")
# 测试餐食记录
meal_data = {
'date': '2024-01-15',
'meal_type': 'breakfast',
'foods': ['燕麦粥', '香蕉', '牛奶'],
'quantities': ['1碗', '1根', '200ml'],
'calories': 350.0,
'satisfaction_score': 4,
'notes': '很满意,营养均衡'
}
if record_meal(test_user_id, meal_data):
print("✅ 餐食记录成功")
# 清理应用
cleanup_app()
print("✅ 应用清理完成")
else:
print("❌ 应用初始化失败")
print("数据采集模块测试完成!")

View File

@@ -0,0 +1,634 @@
"""
高效数据处理和训练模块
优化数据处理流程,提高训练效率
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple, Any
import json
import pickle
from pathlib import Path
from datetime import datetime, timedelta
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
from collections import defaultdict, Counter
logger = logging.getLogger(__name__)
class EfficientDataProcessor:
"""高效数据处理器"""
def __init__(self, data_dir: str = "data"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(exist_ok=True)
# 数据缓存
self.user_data_cache = {}
self.meal_data_cache = {}
self.feedback_data_cache = {}
# 预计算数据
self.food_frequency = {}
self.user_preferences = {}
self.nutrition_patterns = {}
# 线程锁
self.cache_lock = threading.Lock()
# 加载预计算数据
self._load_precomputed_data()
def _load_precomputed_data(self):
"""加载预计算数据"""
try:
# 加载食物频率
freq_file = self.data_dir / "food_frequency.pkl"
if freq_file.exists():
with open(freq_file, 'rb') as f:
self.food_frequency = pickle.load(f)
# 加载用户偏好
pref_file = self.data_dir / "user_preferences.pkl"
if pref_file.exists():
with open(pref_file, 'rb') as f:
self.user_preferences = pickle.load(f)
# 加载营养模式
pattern_file = self.data_dir / "nutrition_patterns.pkl"
if pattern_file.exists():
with open(pattern_file, 'rb') as f:
self.nutrition_patterns = pickle.load(f)
except Exception as e:
logger.warning(f"加载预计算数据失败: {e}")
def _save_precomputed_data(self):
"""保存预计算数据"""
try:
# 保存食物频率
freq_file = self.data_dir / "food_frequency.pkl"
with open(freq_file, 'wb') as f:
pickle.dump(self.food_frequency, f)
# 保存用户偏好
pref_file = self.data_dir / "user_preferences.pkl"
with open(pref_file, 'wb') as f:
pickle.dump(self.user_preferences, f)
# 保存营养模式
pattern_file = self.data_dir / "nutrition_patterns.pkl"
with open(pattern_file, 'wb') as f:
pickle.dump(self.nutrition_patterns, f)
except Exception as e:
logger.error(f"保存预计算数据失败: {e}")
def batch_process_user_data(self, user_ids: List[str]) -> Dict[str, Any]:
"""批量处理用户数据"""
logger.info(f"开始批量处理 {len(user_ids)} 个用户的数据")
results = {}
# 使用线程池并行处理
with ThreadPoolExecutor(max_workers=4) as executor:
# 提交任务
future_to_user = {
executor.submit(self._process_single_user, user_id): user_id
for user_id in user_ids
}
# 收集结果
for future in as_completed(future_to_user):
user_id = future_to_user[future]
try:
result = future.result()
results[user_id] = result
except Exception as e:
logger.error(f"处理用户 {user_id} 数据失败: {e}")
results[user_id] = {'error': str(e)}
# 更新预计算数据
self._update_precomputed_data(results)
logger.info(f"批量处理完成,成功处理 {len(results)} 个用户")
return results
def _process_single_user(self, user_id: str) -> Dict[str, Any]:
"""处理单个用户数据"""
try:
from core.base import AppCore
app_core = AppCore()
user_data = app_core.get_user_data(user_id)
if not user_data:
return {'error': '用户数据不存在'}
# 处理餐食数据
meal_analysis = self._analyze_meal_patterns(user_data.meals)
# 处理反馈数据
feedback_analysis = self._analyze_feedback_patterns(user_data.feedback)
# 处理用户偏好
preference_analysis = self._analyze_user_preferences(user_data)
# 生成个性化建议
recommendations = self._generate_personalized_recommendations(
user_data, meal_analysis, feedback_analysis, preference_analysis
)
return {
'user_id': user_id,
'meal_analysis': meal_analysis,
'feedback_analysis': feedback_analysis,
'preference_analysis': preference_analysis,
'recommendations': recommendations,
'processed_at': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"处理用户 {user_id} 数据失败: {e}")
return {'error': str(e)}
def _analyze_meal_patterns(self, meals: List[Dict]) -> Dict[str, Any]:
"""分析餐食模式"""
if not meals:
return {'total_meals': 0, 'patterns': {}}
# 统计食物频率
food_counter = Counter()
meal_type_counter = Counter()
satisfaction_scores = []
calorie_totals = []
for meal in meals:
# 统计食物
for food in meal.get('foods', []):
food_counter[food] += 1
# 统计餐次
meal_type_counter[meal.get('meal_type', 'unknown')] += 1
# 收集满意度
if 'satisfaction_score' in meal:
satisfaction_scores.append(meal['satisfaction_score'])
# 收集热量
if 'calories' in meal and meal['calories']:
calorie_totals.append(meal['calories'])
# 计算统计信息
avg_satisfaction = np.mean(satisfaction_scores) if satisfaction_scores else 0
avg_calories = np.mean(calorie_totals) if calorie_totals else 0
# 识别模式
patterns = {
'favorite_foods': [food for food, count in food_counter.most_common(5)],
'meal_type_preference': dict(meal_type_counter.most_common()),
'avg_satisfaction': round(avg_satisfaction, 2),
'avg_calories': round(avg_calories, 2),
'total_meals': len(meals),
'food_diversity': len(food_counter)
}
return patterns
def _analyze_feedback_patterns(self, feedbacks: List[Dict]) -> Dict[str, Any]:
"""分析反馈模式"""
if not feedbacks:
return {'total_feedback': 0, 'patterns': {}}
feedback_types = Counter()
user_choices = []
for feedback in feedbacks:
feedback_types[feedback.get('feedback_type', 'unknown')] += 1
if 'user_choice' in feedback:
user_choices.append(feedback['user_choice'])
patterns = {
'feedback_distribution': dict(feedback_types.most_common()),
'total_feedback': len(feedbacks),
'common_choices': Counter(user_choices).most_common(5)
}
return patterns
def _analyze_user_preferences(self, user_data) -> Dict[str, Any]:
"""分析用户偏好"""
profile = user_data.profile
preferences = {
'basic_info': {
'age': profile.get('age', 'unknown'),
'gender': profile.get('gender', 'unknown'),
'activity_level': profile.get('activity_level', 'unknown')
},
'taste_preferences': profile.get('taste_preferences', {}),
'dietary_restrictions': {
'allergies': profile.get('allergies', []),
'dislikes': profile.get('dislikes', []),
'dietary_preferences': profile.get('dietary_preferences', [])
},
'health_goals': profile.get('health_goals', [])
}
return preferences
def _generate_personalized_recommendations(self, user_data, meal_analysis,
feedback_analysis, preference_analysis) -> Dict[str, Any]:
"""生成个性化建议"""
recommendations = {
'food_recommendations': [],
'meal_suggestions': [],
'health_tips': [],
'improvement_suggestions': []
}
# 基于食物频率推荐
favorite_foods = meal_analysis.get('favorite_foods', [])
if favorite_foods:
recommendations['food_recommendations'].extend(favorite_foods[:3])
# 基于反馈推荐
feedback_patterns = feedback_analysis.get('feedback_distribution', {})
if feedback_patterns.get('like', 0) > feedback_patterns.get('dislike', 0):
recommendations['meal_suggestions'].append("继续选择您喜欢的食物")
# 基于健康目标推荐
health_goals = preference_analysis.get('health_goals', [])
if '减重' in health_goals:
recommendations['health_tips'].append("建议增加蔬菜摄入,减少高热量食物")
elif '增重' in health_goals:
recommendations['health_tips'].append("建议增加蛋白质和健康脂肪摄入")
# 基于满意度推荐
avg_satisfaction = meal_analysis.get('avg_satisfaction', 0)
if avg_satisfaction < 3:
recommendations['improvement_suggestions'].append("尝试新的食物组合以提高满意度")
return recommendations
def _update_precomputed_data(self, results: Dict[str, Any]):
"""更新预计算数据"""
with self.cache_lock:
# 更新食物频率
for user_id, result in results.items():
if 'error' in result:
continue
meal_analysis = result.get('meal_analysis', {})
favorite_foods = meal_analysis.get('favorite_foods', [])
for food in favorite_foods:
self.food_frequency[food] = self.food_frequency.get(food, 0) + 1
# 更新用户偏好
for user_id, result in results.items():
if 'error' in result:
continue
preference_analysis = result.get('preference_analysis', {})
self.user_preferences[user_id] = preference_analysis
# 更新营养模式
for user_id, result in results.items():
if 'error' in result:
continue
meal_analysis = result.get('meal_analysis', {})
self.nutrition_patterns[user_id] = {
'avg_calories': meal_analysis.get('avg_calories', 0),
'avg_satisfaction': meal_analysis.get('avg_satisfaction', 0),
'food_diversity': meal_analysis.get('food_diversity', 0)
}
# 保存预计算数据
self._save_precomputed_data()
def get_popular_foods(self, limit: int = 10) -> List[Tuple[str, int]]:
"""获取热门食物"""
return sorted(self.food_frequency.items(), key=lambda x: x[1], reverse=True)[:limit]
def get_user_similarity(self, user_id: str) -> List[Tuple[str, float]]:
"""获取相似用户"""
if user_id not in self.user_preferences:
return []
target_prefs = self.user_preferences[user_id]
similarities = []
for other_user_id, other_prefs in self.user_preferences.items():
if other_user_id == user_id:
continue
# 计算相似度(简化版本)
similarity = self._calculate_preference_similarity(target_prefs, other_prefs)
similarities.append((other_user_id, similarity))
return sorted(similarities, key=lambda x: x[1], reverse=True)[:5]
def _calculate_preference_similarity(self, prefs1: Dict, prefs2: Dict) -> float:
"""计算偏好相似度"""
# 简化的相似度计算
score = 0.0
total = 0.0
# 比较基本特征
basic1 = prefs1.get('basic_info', {})
basic2 = prefs2.get('basic_info', {})
if basic1.get('gender') == basic2.get('gender'):
score += 0.3
total += 0.3
if basic1.get('activity_level') == basic2.get('activity_level'):
score += 0.2
total += 0.2
# 比较口味偏好
taste1 = prefs1.get('taste_preferences', {})
taste2 = prefs2.get('taste_preferences', {})
for key in taste1:
if key in taste2 and taste1[key] == taste2[key]:
score += 0.1
total += 0.1
return score / total if total > 0 else 0.0
def export_analysis_report(self, user_id: str, output_file: str = None) -> str:
"""导出分析报告"""
if not output_file:
output_file = self.data_dir / f"analysis_report_{user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
try:
# 获取用户数据
from core.base import AppCore
app_core = AppCore()
user_data = app_core.get_user_data(user_id)
if not user_data:
raise ValueError("用户数据不存在")
# 生成分析报告
report = {
'user_id': user_id,
'generated_at': datetime.now().isoformat(),
'user_profile': user_data.profile,
'meal_statistics': self._analyze_meal_patterns(user_data.meals),
'feedback_statistics': self._analyze_feedback_patterns(user_data.feedback),
'recommendations': self._generate_personalized_recommendations(
user_data,
self._analyze_meal_patterns(user_data.meals),
self._analyze_feedback_patterns(user_data.feedback),
self._analyze_user_preferences(user_data)
),
'similar_users': self.get_user_similarity(user_id),
'popular_foods': self.get_popular_foods(10)
}
# 保存报告
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
logger.info(f"分析报告已导出到: {output_file}")
return str(output_file)
except Exception as e:
logger.error(f"导出分析报告失败: {e}")
raise
class FastTrainingPipeline:
"""快速训练管道"""
def __init__(self, data_processor: EfficientDataProcessor):
self.data_processor = data_processor
self.models = {}
self.training_cache = {}
self._background_thread: threading.Thread | None = None
self._stop_event = threading.Event()
def train_recommendation_model(self, user_ids: List[str]) -> Dict[str, Any]:
"""训练推荐模型"""
logger.info(f"开始训练推荐模型,用户数量: {len(user_ids)}")
# 批量处理用户数据
processed_data = self.data_processor.batch_process_user_data(user_ids)
# 提取特征
features = self._extract_features(processed_data)
# 训练模型(简化版本)
model_results = self._train_simple_recommendation_model(features)
# 缓存模型
self.models['recommendation'] = model_results
logger.info("推荐模型训练完成")
return model_results
def start_background_training(self, user_ids_provider=None, interval_minutes: int = 60) -> None:
"""后台周期训练。
user_ids_provider: 可选的函数返回需要训练的user_id列表若为空则从预计算偏好中取键。
"""
if self._background_thread and self._background_thread.is_alive():
return
self._stop_event.clear()
def _loop():
while not self._stop_event.is_set():
try:
if user_ids_provider is not None:
user_ids = list(user_ids_provider()) or list(self.data_processor.user_preferences.keys())
else:
user_ids = list(self.data_processor.user_preferences.keys())
if user_ids:
self.train_recommendation_model(user_ids)
except Exception as e:
logger.warning(f"后台训练失败: {e}")
finally:
self._stop_event.wait(interval_minutes * 60)
self._background_thread = threading.Thread(target=_loop, daemon=True)
self._background_thread.start()
def stop_background_training(self) -> None:
"""停止后台训练"""
self._stop_event.set()
if self._background_thread and self._background_thread.is_alive():
self._background_thread.join(timeout=1.0)
def _extract_features(self, processed_data: Dict[str, Any]) -> Dict[str, Any]:
"""提取特征"""
features = {
'user_features': {},
'food_features': {},
'interaction_features': {}
}
for user_id, data in processed_data.items():
if 'error' in data:
continue
# 用户特征
preference_analysis = data.get('preference_analysis', {})
features['user_features'][user_id] = {
'age': preference_analysis.get('basic_info', {}).get('age', 25),
'gender': preference_analysis.get('basic_info', {}).get('gender', 'unknown'),
'activity_level': preference_analysis.get('basic_info', {}).get('activity_level', 'moderate')
}
# 食物特征
meal_analysis = data.get('meal_analysis', {})
favorite_foods = meal_analysis.get('favorite_foods', [])
for food in favorite_foods:
if food not in features['food_features']:
features['food_features'][food] = 0
features['food_features'][food] += 1
# 交互特征
features['interaction_features'][user_id] = {
'avg_satisfaction': meal_analysis.get('avg_satisfaction', 0),
'avg_calories': meal_analysis.get('avg_calories', 0),
'food_diversity': meal_analysis.get('food_diversity', 0)
}
return features
def _train_simple_recommendation_model(self, features: Dict[str, Any]) -> Dict[str, Any]:
"""训练简单推荐模型"""
# 这里是一个简化的推荐模型
# 在实际应用中,可以使用更复杂的机器学习算法
model_results = {
'model_type': 'simple_collaborative_filtering',
'trained_at': datetime.now().isoformat(),
'user_count': len(features['user_features']),
'food_count': len(features['food_features']),
'recommendation_rules': self._generate_recommendation_rules(features),
'performance_metrics': {
'accuracy': 0.75, # 模拟指标
'precision': 0.72,
'recall': 0.68,
'f1_score': 0.70
}
}
return model_results
def _generate_recommendation_rules(self, features: Dict[str, Any]) -> List[Dict[str, Any]]:
"""生成推荐规则"""
rules = []
# 基于食物频率的规则
food_features = features['food_features']
popular_foods = sorted(food_features.items(), key=lambda x: x[1], reverse=True)[:10]
for food, count in popular_foods:
rules.append({
'type': 'popular_food',
'condition': f"food_popularity >= {count}",
'recommendation': f"推荐 {food}",
'confidence': min(count / 10.0, 1.0)
})
# 基于用户特征的规则
user_features = features['user_features']
for user_id, user_feat in user_features.items():
if user_feat['gender'] == '':
rules.append({
'type': 'gender_based',
'condition': f"gender == ''",
'recommendation': "推荐富含铁质的食物",
'confidence': 0.8
})
return rules
def predict_recommendations(self, user_id: str, meal_type: str = "lunch") -> List[Dict[str, Any]]:
"""预测推荐"""
if 'recommendation' not in self.models:
return []
# 获取用户数据
from core.base import AppCore
app_core = AppCore()
user_data = app_core.get_user_data(user_id)
if not user_data:
return []
# 基于规则生成推荐
recommendations = []
rules = self.models['recommendation'].get('recommendation_rules', [])
for rule in rules:
if self._evaluate_rule(rule, user_data):
recommendations.append({
'food': rule['recommendation'],
'confidence': rule['confidence'],
'reason': rule['type']
})
return recommendations[:5] # 返回前5个推荐
def _evaluate_rule(self, rule: Dict[str, Any], user_data) -> bool:
"""评估规则"""
# 简化的规则评估
rule_type = rule.get('type', '')
if rule_type == 'popular_food':
return True # 总是推荐热门食物
elif rule_type == 'gender_based':
return user_data.profile.get('gender') == ''
return False
# 全局实例
data_processor = EfficientDataProcessor()
training_pipeline = FastTrainingPipeline(data_processor)
# 便捷函数
def batch_process_users(user_ids: List[str]) -> Dict[str, Any]:
"""批量处理用户数据"""
return data_processor.batch_process_user_data(user_ids)
def train_recommendation_model(user_ids: List[str]) -> Dict[str, Any]:
"""训练推荐模型"""
return training_pipeline.train_recommendation_model(user_ids)
def get_user_recommendations(user_id: str, meal_type: str = "lunch") -> List[Dict[str, Any]]:
"""获取用户推荐"""
return training_pipeline.predict_recommendations(user_id, meal_type)
def export_user_report(user_id: str, output_file: str = None) -> str:
"""导出用户报告"""
return data_processor.export_analysis_report(user_id, output_file)
if __name__ == "__main__":
# 测试数据处理
print("测试高效数据处理...")
# 测试批量处理
test_users = ["user1", "user2", "user3"]
results = batch_process_users(test_users)
print(f"批量处理结果: {len(results)} 个用户")
# 测试训练
model_results = train_recommendation_model(test_users)
print(f"模型训练完成: {model_results['model_type']}")
print("测试完成!")

View File

@@ -0,0 +1,786 @@
"""
图片OCR热量识别模块 - 基于基座架构
支持多种OCR技术识别食物热量信息包含智能验证和修正机制
"""
import cv2
import numpy as np
import re
import json
import logging
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
import requests
import base64
from PIL import Image, ImageEnhance, ImageFilter
import pytesseract
from core.base import BaseModule, ModuleType, UserData, AnalysisResult, BaseConfig
logger = logging.getLogger(__name__)
@dataclass
class OCRResult:
"""OCR识别结果"""
text: str
confidence: float
bounding_boxes: List[Dict[str, Any]]
processing_time: float
method: str
@dataclass
class CalorieInfo:
"""热量信息"""
food_name: str
calories: Optional[float]
serving_size: Optional[str]
confidence: float
source: str # 'ocr', 'database', 'user_confirmed'
raw_text: str
validation_status: str # 'pending', 'validated', 'corrected'
@dataclass
class FoodRecognitionResult:
"""食物识别结果"""
image_path: str
ocr_results: List[OCRResult]
calorie_infos: List[CalorieInfo]
overall_confidence: float
processing_time: float
suggestions: List[str]
class OCRCalorieRecognitionModule(BaseModule):
"""OCR热量识别模块"""
def __init__(self, config: BaseConfig):
super().__init__(config, ModuleType.DATA_COLLECTION)
# OCR配置
self.ocr_methods = ['tesseract', 'paddleocr', 'easyocr']
self.min_confidence = 0.6
self.max_processing_time = 30.0
# 热量识别模式
self.calorie_patterns = [
r'(\d+(?:\.\d+)?)\s*[kK]?[cC][aA][lL](?:ories?)?',
r'(\d+(?:\.\d+)?)\s*[kK][cC][aA][lL]',
r'(\d+(?:\.\d+)?)\s*卡路里',
r'(\d+(?:\.\d+)?)\s*千卡',
r'(\d+(?:\.\d+)?)\s*大卡',
r'(\d+(?:\.\d+)?)\s*[kK][jJ]', # 千焦
]
# 食物名称模式
self.food_patterns = [
r'([a-zA-Z\u4e00-\u9fff]+)\s*(?:\d+(?:\.\d+)?)',
r'(\d+(?:\.\d+)?)\s*([a-zA-Z\u4e00-\u9fff]+)',
]
# 食物数据库
self.food_database = self._load_food_database()
# 用户学习数据
self.user_corrections = {}
# 初始化OCR引擎
self.ocr_engines = {}
self._initialize_ocr_engines()
def initialize(self) -> bool:
"""初始化模块"""
try:
self.logger.info("OCR热量识别模块初始化中...")
# 创建必要的目录
self._create_directories()
# 加载用户学习数据
self._load_user_corrections()
self.is_initialized = True
self.logger.info("OCR热量识别模块初始化完成")
return True
except Exception as e:
self.logger.error(f"OCR热量识别模块初始化失败: {e}")
return False
def process(self, input_data: Any, user_data: UserData) -> AnalysisResult:
"""处理OCR识别请求"""
try:
request_type = input_data.get('type', 'unknown')
if request_type == 'recognize_image':
result = self._recognize_image_calories(input_data, user_data)
elif request_type == 'validate_result':
result = self._validate_recognition_result(input_data, user_data)
elif request_type == 'learn_correction':
result = self._learn_from_correction(input_data, user_data)
else:
result = self._create_error_result("未知的请求类型")
return AnalysisResult(
module_type=self.module_type,
user_id=user_data.user_id,
input_data=input_data,
result=result,
confidence=result.get('confidence', 0.5)
)
except Exception as e:
self.logger.error(f"OCR识别处理失败: {e}")
return self._create_error_result(f"处理失败: {str(e)}")
def _recognize_image_calories(self, input_data: Dict[str, Any], user_data: UserData) -> Dict[str, Any]:
"""识别图片中的热量信息"""
start_time = datetime.now()
try:
image_path = input_data.get('image_path')
if not image_path or not Path(image_path).exists():
return self._create_error_result("图片文件不存在")
# 预处理图片
processed_image = self._preprocess_image(image_path)
# 多OCR引擎识别
ocr_results = []
for method in self.ocr_methods:
try:
result = self._ocr_recognize(processed_image, method)
if result:
ocr_results.append(result)
except Exception as e:
self.logger.warning(f"OCR方法 {method} 失败: {e}")
# 合并和去重OCR结果
merged_text = self._merge_ocr_results(ocr_results)
# 提取热量信息
calorie_infos = self._extract_calorie_info(merged_text, user_data)
# 数据库匹配和验证
validated_infos = self._validate_with_database(calorie_infos, user_data)
# 生成建议
suggestions = self._generate_suggestions(validated_infos, user_data)
processing_time = (datetime.now() - start_time).total_seconds()
result = FoodRecognitionResult(
image_path=image_path,
ocr_results=ocr_results,
calorie_infos=validated_infos,
overall_confidence=self._calculate_overall_confidence(ocr_results, validated_infos),
processing_time=processing_time,
suggestions=suggestions
)
return {
'success': True,
'result': result,
'confidence': result.overall_confidence,
'message': f"识别完成,处理时间: {processing_time:.2f}"
}
except Exception as e:
self.logger.error(f"图片热量识别失败: {e}")
return self._create_error_result(f"识别失败: {str(e)}")
def _preprocess_image(self, image_path: str) -> np.ndarray:
"""预处理图片以提高OCR准确性"""
try:
# 读取图片
image = cv2.imread(image_path)
if image is None:
raise ValueError("无法读取图片")
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 降噪
denoised = cv2.medianBlur(gray, 3)
# 增强对比度
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(denoised)
# 二值化
_, binary = cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 形态学操作
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
return cleaned
except Exception as e:
self.logger.error(f"图片预处理失败: {e}")
return cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
def _ocr_recognize(self, image: np.ndarray, method: str) -> Optional[OCRResult]:
"""使用指定方法进行OCR识别"""
start_time = datetime.now()
try:
if method == 'tesseract':
return self._tesseract_ocr(image)
elif method == 'paddleocr':
return self._paddleocr_recognize(image)
elif method == 'easyocr':
return self._easyocr_recognize(image)
else:
return None
except Exception as e:
self.logger.error(f"OCR方法 {method} 失败: {e}")
return None
def _tesseract_ocr(self, image: np.ndarray) -> OCRResult:
"""使用Tesseract进行OCR识别"""
try:
# 配置Tesseract
config = '--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\u4e00-\u9fff'
# OCR识别
text = pytesseract.image_to_string(image, config=config, lang='chi_sim+eng')
# 获取置信度
data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT, config=config, lang='chi_sim+eng')
confidences = [int(conf) for conf in data['conf'] if int(conf) > 0]
avg_confidence = sum(confidences) / len(confidences) / 100.0 if confidences else 0.0
# 获取边界框
bounding_boxes = []
for i in range(len(data['text'])):
if int(data['conf'][i]) > 0:
bounding_boxes.append({
'text': data['text'][i],
'confidence': int(data['conf'][i]) / 100.0,
'bbox': [data['left'][i], data['top'][i], data['width'][i], data['height'][i]]
})
processing_time = (datetime.now() - datetime.now()).total_seconds()
return OCRResult(
text=text.strip(),
confidence=avg_confidence,
bounding_boxes=bounding_boxes,
processing_time=processing_time,
method='tesseract'
)
except Exception as e:
self.logger.error(f"Tesseract OCR失败: {e}")
return None
def _paddleocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
"""使用PaddleOCR进行识别"""
try:
# 这里需要安装paddleocr: pip install paddleocr
from paddleocr import PaddleOCR
if 'paddleocr' not in self.ocr_engines:
self.ocr_engines['paddleocr'] = PaddleOCR(use_angle_cls=True, lang='ch')
ocr = self.ocr_engines['paddleocr']
result = ocr.ocr(image, cls=True)
if not result or not result[0]:
return None
# 提取文本和置信度
texts = []
confidences = []
bounding_boxes = []
for line in result[0]:
if line:
bbox, (text, confidence) = line
texts.append(text)
confidences.append(confidence)
bounding_boxes.append({
'text': text,
'confidence': confidence,
'bbox': bbox
})
merged_text = ' '.join(texts)
avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
return OCRResult(
text=merged_text,
confidence=avg_confidence,
bounding_boxes=bounding_boxes,
processing_time=0.0,
method='paddleocr'
)
except ImportError:
self.logger.warning("PaddleOCR未安装跳过此方法")
return None
except Exception as e:
self.logger.error(f"PaddleOCR识别失败: {e}")
return None
def _easyocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
"""使用EasyOCR进行识别"""
try:
# 这里需要安装easyocr: pip install easyocr
import easyocr
if 'easyocr' not in self.ocr_engines:
self.ocr_engines['easyocr'] = easyocr.Reader(['ch_sim', 'en'])
reader = self.ocr_engines['easyocr']
result = reader.readtext(image)
if not result:
return None
# 提取文本和置信度
texts = []
confidences = []
bounding_boxes = []
for bbox, text, confidence in result:
texts.append(text)
confidences.append(confidence)
bounding_boxes.append({
'text': text,
'confidence': confidence,
'bbox': bbox
})
merged_text = ' '.join(texts)
avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
return OCRResult(
text=merged_text,
confidence=avg_confidence,
bounding_boxes=bounding_boxes,
processing_time=0.0,
method='easyocr'
)
except ImportError:
self.logger.warning("EasyOCR未安装跳过此方法")
return None
except Exception as e:
self.logger.error(f"EasyOCR识别失败: {e}")
return None
def _merge_ocr_results(self, ocr_results: List[OCRResult]) -> str:
"""合并多个OCR结果"""
if not ocr_results:
return ""
# 按置信度排序
sorted_results = sorted(ocr_results, key=lambda x: x.confidence, reverse=True)
# 使用最高置信度的结果作为主要结果
primary_result = sorted_results[0]
# 如果有多个高置信度结果,尝试合并
if len(sorted_results) > 1 and sorted_results[1].confidence > 0.7:
# 简单的文本合并策略
merged_text = self._smart_text_merge([r.text for r in sorted_results[:3]])
return merged_text
return primary_result.text
def _smart_text_merge(self, texts: List[str]) -> str:
"""智能文本合并"""
if not texts:
return ""
if len(texts) == 1:
return texts[0]
# 简单的合并策略:选择最长的文本
return max(texts, key=len)
def _extract_calorie_info(self, text: str, user_data: UserData) -> List[CalorieInfo]:
"""从文本中提取热量信息"""
calorie_infos = []
try:
# 查找热量数值
for pattern in self.calorie_patterns:
matches = re.finditer(pattern, text, re.IGNORECASE)
for match in matches:
calories = float(match.group(1))
# 查找对应的食物名称
food_name = self._extract_food_name(text, match.start())
calorie_info = CalorieInfo(
food_name=food_name,
calories=calories,
serving_size=None,
confidence=0.8, # OCR基础置信度
source='ocr',
raw_text=match.group(0),
validation_status='pending'
)
calorie_infos.append(calorie_info)
# 如果没有找到热量信息,尝试查找食物名称
if not calorie_infos:
food_names = self._extract_all_food_names(text)
for food_name in food_names:
calorie_info = CalorieInfo(
food_name=food_name,
calories=None,
serving_size=None,
confidence=0.6,
source='ocr',
raw_text=food_name,
validation_status='pending'
)
calorie_infos.append(calorie_info)
return calorie_infos
except Exception as e:
self.logger.error(f"热量信息提取失败: {e}")
return []
def _extract_food_name(self, text: str, calorie_position: int) -> str:
"""提取食物名称"""
try:
# 在热量数值前后查找食物名称
context_start = max(0, calorie_position - 50)
context_end = min(len(text), calorie_position + 50)
context = text[context_start:context_end]
# 查找中文和英文食物名称
food_pattern = r'([a-zA-Z\u4e00-\u9fff]{2,20})'
matches = re.findall(food_pattern, context)
if matches:
# 选择最可能的食物名称
return matches[0]
return "未知食物"
except Exception as e:
self.logger.error(f"食物名称提取失败: {e}")
return "未知食物"
def _extract_all_food_names(self, text: str) -> List[str]:
"""提取所有可能的食物名称"""
try:
food_pattern = r'([a-zA-Z\u4e00-\u9fff]{2,20})'
matches = re.findall(food_pattern, text)
# 去重并过滤
unique_foods = list(set(matches))
return unique_foods[:5] # 最多返回5个
except Exception as e:
self.logger.error(f"食物名称提取失败: {e}")
return []
def _validate_with_database(self, calorie_infos: List[CalorieInfo], user_data: UserData) -> List[CalorieInfo]:
"""使用数据库验证热量信息"""
validated_infos = []
for info in calorie_infos:
try:
# 在食物数据库中查找匹配
db_match = self._find_database_match(info.food_name)
if db_match:
# 使用数据库信息更新
info.calories = db_match.get('calories', info.calories)
info.serving_size = db_match.get('serving_size', info.serving_size)
info.confidence = max(info.confidence, 0.9)
info.source = 'database'
# 应用用户学习数据
user_correction = self._get_user_correction(user_data.user_id, info.food_name)
if user_correction:
info.calories = user_correction.get('calories', info.calories)
info.confidence = max(info.confidence, 0.95)
info.source = 'user_confirmed'
validated_infos.append(info)
except Exception as e:
self.logger.error(f"数据库验证失败: {e}")
validated_infos.append(info)
return validated_infos
def _find_database_match(self, food_name: str) -> Optional[Dict[str, Any]]:
"""在数据库中查找食物匹配"""
try:
# 精确匹配
if food_name in self.food_database:
return self.food_database[food_name]
# 模糊匹配
for db_food, info in self.food_database.items():
if food_name in db_food or db_food in food_name:
return info
return None
except Exception as e:
self.logger.error(f"数据库匹配失败: {e}")
return None
def _get_user_correction(self, user_id: str, food_name: str) -> Optional[Dict[str, Any]]:
"""获取用户修正数据"""
try:
user_data = self.user_corrections.get(user_id, {})
return user_data.get(food_name)
except Exception as e:
self.logger.error(f"获取用户修正数据失败: {e}")
return None
def _generate_suggestions(self, calorie_infos: List[CalorieInfo], user_data: UserData) -> List[str]:
"""生成建议"""
suggestions = []
try:
for info in calorie_infos:
if info.calories is None:
suggestions.append(f"未识别到 {info.food_name} 的热量信息,请手动输入")
elif info.confidence < 0.8:
suggestions.append(f"{info.food_name} 的热量 {info.calories} 可能不准确,请确认")
else:
suggestions.append(f"{info.food_name}: {info.calories} 卡路里")
if not calorie_infos:
suggestions.append("未识别到任何食物信息,请检查图片质量或手动输入")
return suggestions
except Exception as e:
self.logger.error(f"生成建议失败: {e}")
return ["识别过程中出现错误"]
def _calculate_overall_confidence(self, ocr_results: List[OCRResult], calorie_infos: List[CalorieInfo]) -> float:
"""计算整体置信度"""
try:
if not ocr_results and not calorie_infos:
return 0.0
# OCR置信度
ocr_confidence = sum(r.confidence for r in ocr_results) / len(ocr_results) if ocr_results else 0.0
# 热量信息置信度
calorie_confidence = sum(info.confidence for info in calorie_infos) / len(calorie_infos) if calorie_infos else 0.0
# 综合置信度
overall_confidence = (ocr_confidence * 0.4 + calorie_confidence * 0.6)
return min(overall_confidence, 1.0)
except Exception as e:
self.logger.error(f"计算置信度失败: {e}")
return 0.0
def _validate_recognition_result(self, input_data: Dict[str, Any], user_data: UserData) -> Dict[str, Any]:
"""验证识别结果"""
try:
food_name = input_data.get('food_name')
calories = input_data.get('calories')
is_correct = input_data.get('is_correct', True)
if not is_correct:
# 用户修正
corrected_calories = input_data.get('corrected_calories')
self._save_user_correction(user_data.user_id, food_name, corrected_calories)
return {
'success': True,
'message': '验证结果已保存',
'confidence': 1.0
}
except Exception as e:
self.logger.error(f"验证识别结果失败: {e}")
return self._create_error_result(f"验证失败: {str(e)}")
def _learn_from_correction(self, input_data: Dict[str, Any], user_data: UserData) -> Dict[str, Any]:
"""从用户修正中学习"""
try:
food_name = input_data.get('food_name')
corrected_calories = input_data.get('corrected_calories')
self._save_user_correction(user_data.user_id, food_name, corrected_calories)
return {
'success': True,
'message': '学习数据已保存',
'confidence': 1.0
}
except Exception as e:
self.logger.error(f"学习修正数据失败: {e}")
return self._create_error_result(f"学习失败: {str(e)}")
def _save_user_correction(self, user_id: str, food_name: str, calories: float):
"""保存用户修正数据"""
try:
if user_id not in self.user_corrections:
self.user_corrections[user_id] = {}
self.user_corrections[user_id][food_name] = {
'calories': calories,
'timestamp': datetime.now().isoformat(),
'correction_count': self.user_corrections[user_id].get(food_name, {}).get('correction_count', 0) + 1
}
# 保存到文件
self._save_user_corrections_to_file()
except Exception as e:
self.logger.error(f"保存用户修正数据失败: {e}")
def _load_food_database(self) -> Dict[str, Dict[str, Any]]:
"""加载食物数据库"""
try:
# 基础食物数据库
food_db = {
"米饭": {"calories": 130, "serving_size": "100g"},
"面条": {"calories": 110, "serving_size": "100g"},
"馒头": {"calories": 221, "serving_size": "100g"},
"包子": {"calories": 250, "serving_size": "100g"},
"饺子": {"calories": 250, "serving_size": "100g"},
"鸡蛋": {"calories": 155, "serving_size": "100g"},
"豆腐": {"calories": 76, "serving_size": "100g"},
"鱼肉": {"calories": 206, "serving_size": "100g"},
"鸡肉": {"calories": 165, "serving_size": "100g"},
"瘦肉": {"calories": 250, "serving_size": "100g"},
"青菜": {"calories": 25, "serving_size": "100g"},
"西红柿": {"calories": 18, "serving_size": "100g"},
"胡萝卜": {"calories": 41, "serving_size": "100g"},
"土豆": {"calories": 77, "serving_size": "100g"},
"西兰花": {"calories": 34, "serving_size": "100g"},
"苹果": {"calories": 52, "serving_size": "100g"},
"香蕉": {"calories": 89, "serving_size": "100g"},
"橙子": {"calories": 47, "serving_size": "100g"},
"葡萄": {"calories": 67, "serving_size": "100g"},
"草莓": {"calories": 32, "serving_size": "100g"},
"牛奶": {"calories": 42, "serving_size": "100ml"},
"酸奶": {"calories": 59, "serving_size": "100g"},
"豆浆": {"calories": 31, "serving_size": "100ml"},
"坚果": {"calories": 607, "serving_size": "100g"},
"红枣": {"calories": 264, "serving_size": "100g"},
}
# 尝试从文件加载扩展数据库
db_file = Path("data/food_database.json")
if db_file.exists():
with open(db_file, 'r', encoding='utf-8') as f:
extended_db = json.load(f)
food_db.update(extended_db)
return food_db
except Exception as e:
self.logger.error(f"加载食物数据库失败: {e}")
return {}
def _create_directories(self):
"""创建必要的目录"""
directories = [
'data/ocr_cache',
'data/user_corrections',
'data/food_images'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
def _load_user_corrections(self):
"""加载用户修正数据"""
try:
corrections_file = Path("data/user_corrections.json")
if corrections_file.exists():
with open(corrections_file, 'r', encoding='utf-8') as f:
self.user_corrections = json.load(f)
else:
self.user_corrections = {}
except Exception as e:
self.logger.error(f"加载用户修正数据失败: {e}")
self.user_corrections = {}
def _save_user_corrections_to_file(self):
"""保存用户修正数据到文件"""
try:
corrections_file = Path("data/user_corrections.json")
with open(corrections_file, 'w', encoding='utf-8') as f:
json.dump(self.user_corrections, f, ensure_ascii=False, indent=2)
except Exception as e:
self.logger.error(f"保存用户修正数据失败: {e}")
def _initialize_ocr_engines(self):
"""初始化OCR引擎"""
try:
# 检查Tesseract是否可用
try:
pytesseract.get_tesseract_version()
self.logger.info("Tesseract OCR引擎可用")
except Exception:
self.logger.warning("Tesseract OCR引擎不可用")
# 其他OCR引擎将在需要时初始化
self.logger.info("OCR引擎初始化完成")
except Exception as e:
self.logger.error(f"OCR引擎初始化失败: {e}")
def _create_error_result(self, message: str) -> Dict[str, Any]:
"""创建错误结果"""
return {
'success': False,
'error': message,
'confidence': 0.0
}
def cleanup(self) -> bool:
"""清理资源"""
try:
# 保存用户修正数据
self._save_user_corrections_to_file()
# 清理OCR引擎
self.ocr_engines.clear()
self.is_initialized = False
self.logger.info("OCR热量识别模块清理完成")
return True
except Exception as e:
self.logger.error(f"OCR热量识别模块清理失败: {e}")
return False
if __name__ == "__main__":
# 测试OCR模块
from core.base import BaseConfig
config = BaseConfig()
ocr_module = OCRCalorieRecognitionModule(config)
if ocr_module.initialize():
print("OCR模块初始化成功")
# 测试图片识别
test_data = {
'type': 'recognize_image',
'image_path': 'test_image.jpg' # 需要提供测试图片
}
# 这里需要用户数据,暂时跳过实际测试
print("OCR模块测试完成")
else:
print("OCR模块初始化失败")

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "diet-recommendation-app",
"version": "1.0.0",
"description": "智能饮食推荐应用",
"main": "index.js",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"axios": "1.6.2",
"tailwindcss": "3.3.6",
"autoprefixer": "10.4.16",
"postcss": "8.4.32",
"@headlessui/react": "1.7.17",
"@heroicons/react": "2.0.18",
"recharts": "2.8.0",
"react-hook-form": "7.48.2",
"zustand": "4.4.7"
},
"devDependencies": {
"@types/node": "20.10.0",
"@types/react": "18.2.38",
"@types/react-dom": "18.2.17",
"eslint": "8.54.0",
"eslint-config-next": "14.0.3",
"typescript": "5.3.2"
}
}

30
requirements.txt Normal file
View File

@@ -0,0 +1,30 @@
# 个性化饮食推荐APP - 依赖包
# 核心依赖
customtkinter>=5.2.0
scikit-learn>=1.3.0
pandas>=2.0.0
numpy>=1.24.0
joblib>=1.3.0
# 大模型集成 (千问API)
requests>=2.31.0
# 配置管理
python-dotenv>=1.0.0
# 数据处理
python-dateutil>=2.8.0
# 图像处理 (GUI需要)
Pillow>=10.0.0
# OCR识别依赖
pytesseract>=0.3.10
opencv-python>=4.8.0
paddleocr>=2.7.0
easyocr>=1.7.0
# 移动端支持 (可选)
kivy>=2.1.0
kivymd>=1.1.1

View File

@@ -0,0 +1,728 @@
"""
智能食物数据库和热量估算模块
简化用户数据录入过程
"""
from typing import Dict, List, Optional, Tuple
import json
from pathlib import Path
class SmartFoodDatabase:
"""智能食物数据库"""
def __init__(self):
self.food_database = self._load_food_database()
self.portion_sizes = self._load_portion_sizes()
self.calorie_estimates = self._load_calorie_estimates()
# 添加缓存
self.ai_cache = {} # AI分析结果缓存
self.calorie_cache = {} # 热量估算缓存
self.search_cache = {} # 搜索结果缓存
# 预计算常用食物
self._precompute_common_foods()
def _load_food_database(self) -> Dict[str, Dict]:
"""加载食物数据库"""
return {
# 主食类
"米饭": {"category": "主食", "calories_per_100g": 130, "protein": 2.7, "carbs": 28, "fat": 0.3},
"面条": {"category": "主食", "calories_per_100g": 131, "protein": 5, "carbs": 25, "fat": 1.1},
"馒头": {"category": "主食", "calories_per_100g": 221, "protein": 7, "carbs": 47, "fat": 1.1},
"包子": {"category": "主食", "calories_per_100g": 227, "protein": 7.3, "carbs": 45, "fat": 2.6},
"饺子": {"category": "主食", "calories_per_100g": 250, "protein": 11, "carbs": 35, "fat": 8},
"": {"category": "主食", "calories_per_100g": 50, "protein": 1.1, "carbs": 10, "fat": 0.3},
"燕麦": {"category": "主食", "calories_per_100g": 389, "protein": 17, "carbs": 66, "fat": 7},
"面包": {"category": "主食", "calories_per_100g": 265, "protein": 9, "carbs": 49, "fat": 3.2},
"饼干": {"category": "主食", "calories_per_100g": 433, "protein": 9, "carbs": 71, "fat": 12},
"薯条": {"category": "主食", "calories_per_100g": 319, "protein": 4, "carbs": 41, "fat": 15},
"玉米": {"category": "主食", "calories_per_100g": 86, "protein": 3.4, "carbs": 19, "fat": 1.2},
"红薯": {"category": "主食", "calories_per_100g": 86, "protein": 1.6, "carbs": 20, "fat": 0.1},
# 蛋白质类
"鸡蛋": {"category": "蛋白质", "calories_per_100g": 155, "protein": 13, "carbs": 1.1, "fat": 11},
"鸡肉": {"category": "蛋白质", "calories_per_100g": 165, "protein": 31, "carbs": 0, "fat": 3.6},
"猪肉": {"category": "蛋白质", "calories_per_100g": 143, "protein": 20, "carbs": 0, "fat": 6.2},
"牛肉": {"category": "蛋白质", "calories_per_100g": 250, "protein": 26, "carbs": 0, "fat": 15},
"鱼肉": {"category": "蛋白质", "calories_per_100g": 206, "protein": 22, "carbs": 0, "fat": 12},
"豆腐": {"category": "蛋白质", "calories_per_100g": 76, "protein": 8, "carbs": 2, "fat": 4.8},
"牛奶": {"category": "蛋白质", "calories_per_100g": 42, "protein": 3.4, "carbs": 5, "fat": 1},
"酸奶": {"category": "蛋白质", "calories_per_100g": 59, "protein": 3.3, "carbs": 4.7, "fat": 3.2},
"": {"category": "蛋白质", "calories_per_100g": 99, "protein": 24, "carbs": 0, "fat": 0.2},
"": {"category": "蛋白质", "calories_per_100g": 97, "protein": 20, "carbs": 0, "fat": 1.5},
"鸭肉": {"category": "蛋白质", "calories_per_100g": 183, "protein": 25, "carbs": 0, "fat": 9},
"羊肉": {"category": "蛋白质", "calories_per_100g": 203, "protein": 25, "carbs": 0, "fat": 11},
"火腿": {"category": "蛋白质", "calories_per_100g": 145, "protein": 18, "carbs": 1.5, "fat": 7.5},
"香肠": {"category": "蛋白质", "calories_per_100g": 301, "protein": 13, "carbs": 2, "fat": 25},
# 蔬菜类
"白菜": {"category": "蔬菜", "calories_per_100g": 17, "protein": 1.5, "carbs": 3.2, "fat": 0.1},
"菠菜": {"category": "蔬菜", "calories_per_100g": 23, "protein": 2.9, "carbs": 3.6, "fat": 0.4},
"西兰花": {"category": "蔬菜", "calories_per_100g": 34, "protein": 2.8, "carbs": 7, "fat": 0.4},
"胡萝卜": {"category": "蔬菜", "calories_per_100g": 41, "protein": 0.9, "carbs": 10, "fat": 0.2},
"土豆": {"category": "蔬菜", "calories_per_100g": 77, "protein": 2, "carbs": 17, "fat": 0.1},
"西红柿": {"category": "蔬菜", "calories_per_100g": 18, "protein": 0.9, "carbs": 3.9, "fat": 0.2},
"黄瓜": {"category": "蔬菜", "calories_per_100g": 16, "protein": 0.7, "carbs": 4, "fat": 0.1},
"茄子": {"category": "蔬菜", "calories_per_100g": 25, "protein": 1.1, "carbs": 6, "fat": 0.2},
"豆角": {"category": "蔬菜", "calories_per_100g": 31, "protein": 2.1, "carbs": 7, "fat": 0.2},
"韭菜": {"category": "蔬菜", "calories_per_100g": 25, "protein": 2.4, "carbs": 4, "fat": 0.4},
"芹菜": {"category": "蔬菜", "calories_per_100g": 16, "protein": 0.7, "carbs": 4, "fat": 0.1},
"洋葱": {"category": "蔬菜", "calories_per_100g": 40, "protein": 1.1, "carbs": 9, "fat": 0.1},
"大蒜": {"category": "蔬菜", "calories_per_100g": 149, "protein": 6.4, "carbs": 33, "fat": 0.5},
"生姜": {"category": "蔬菜", "calories_per_100g": 80, "protein": 1.8, "carbs": 18, "fat": 0.8},
# 水果类
"苹果": {"category": "水果", "calories_per_100g": 52, "protein": 0.3, "carbs": 14, "fat": 0.2},
"香蕉": {"category": "水果", "calories_per_100g": 89, "protein": 1.1, "carbs": 23, "fat": 0.3},
"橙子": {"category": "水果", "calories_per_100g": 47, "protein": 0.9, "carbs": 12, "fat": 0.1},
"葡萄": {"category": "水果", "calories_per_100g": 44, "protein": 0.2, "carbs": 11, "fat": 0.2},
"草莓": {"category": "水果", "calories_per_100g": 32, "protein": 0.7, "carbs": 8, "fat": 0.3},
"西瓜": {"category": "水果", "calories_per_100g": 30, "protein": 0.6, "carbs": 8, "fat": 0.1},
"": {"category": "水果", "calories_per_100g": 57, "protein": 0.4, "carbs": 15, "fat": 0.1},
"桃子": {"category": "水果", "calories_per_100g": 39, "protein": 0.9, "carbs": 10, "fat": 0.3},
"樱桃": {"category": "水果", "calories_per_100g": 63, "protein": 1.1, "carbs": 16, "fat": 0.2},
"柠檬": {"category": "水果", "calories_per_100g": 29, "protein": 1.1, "carbs": 9, "fat": 0.3},
"芒果": {"category": "水果", "calories_per_100g": 60, "protein": 0.8, "carbs": 15, "fat": 0.4},
"菠萝": {"category": "水果", "calories_per_100g": 50, "protein": 0.5, "carbs": 13, "fat": 0.1},
"猕猴桃": {"category": "水果", "calories_per_100g": 61, "protein": 1.1, "carbs": 15, "fat": 0.5},
# 坚果类
"花生": {"category": "坚果", "calories_per_100g": 567, "protein": 25, "carbs": 16, "fat": 49},
"核桃": {"category": "坚果", "calories_per_100g": 654, "protein": 15, "carbs": 14, "fat": 65},
"杏仁": {"category": "坚果", "calories_per_100g": 579, "protein": 21, "carbs": 22, "fat": 50},
"腰果": {"category": "坚果", "calories_per_100g": 553, "protein": 18, "carbs": 30, "fat": 44},
"开心果": {"category": "坚果", "calories_per_100g": 560, "protein": 20, "carbs": 28, "fat": 45},
"瓜子": {"category": "坚果", "calories_per_100g": 606, "protein": 19, "carbs": 20, "fat": 53},
# 饮料类
"": {"category": "饮料", "calories_per_100g": 0, "protein": 0, "carbs": 0, "fat": 0},
"": {"category": "饮料", "calories_per_100g": 1, "protein": 0.1, "carbs": 0.3, "fat": 0},
"咖啡": {"category": "饮料", "calories_per_100g": 2, "protein": 0.1, "carbs": 0.3, "fat": 0},
"果汁": {"category": "饮料", "calories_per_100g": 45, "protein": 0.3, "carbs": 11, "fat": 0.1},
"可乐": {"category": "饮料", "calories_per_100g": 42, "protein": 0, "carbs": 10.6, "fat": 0},
"雪碧": {"category": "饮料", "calories_per_100g": 40, "protein": 0, "carbs": 10, "fat": 0},
"啤酒": {"category": "饮料", "calories_per_100g": 43, "protein": 0.5, "carbs": 3.6, "fat": 0},
"红酒": {"category": "饮料", "calories_per_100g": 83, "protein": 0.1, "carbs": 2.6, "fat": 0},
"白酒": {"category": "饮料", "calories_per_100g": 298, "protein": 0, "carbs": 0, "fat": 0},
# 调料类
"": {"category": "调料", "calories_per_100g": 0, "protein": 0, "carbs": 0, "fat": 0},
"": {"category": "调料", "calories_per_100g": 387, "protein": 0, "carbs": 100, "fat": 0},
"酱油": {"category": "调料", "calories_per_100g": 63, "protein": 7, "carbs": 7, "fat": 0},
"": {"category": "调料", "calories_per_100g": 31, "protein": 0.1, "carbs": 7, "fat": 0},
"": {"category": "调料", "calories_per_100g": 884, "protein": 0, "carbs": 0, "fat": 100},
"辣椒": {"category": "调料", "calories_per_100g": 40, "protein": 1.9, "carbs": 9, "fat": 0.4},
"胡椒": {"category": "调料", "calories_per_100g": 251, "protein": 10, "carbs": 64, "fat": 3.3},
"花椒": {"category": "调料", "calories_per_100g": 258, "protein": 6, "carbs": 37, "fat": 8.9},
}
def _load_portion_sizes(self) -> Dict[str, List[str]]:
"""加载分量选项"""
return {
"主食": ["1小碗", "1中碗", "1大碗", "1个", "2个", "3个", "半份", "1份", "2份"],
"蛋白质": ["1个", "2个", "3个", "1小块", "2小块", "1片", "2片", "1杯", "2杯", "适量"],
"蔬菜": ["1小份", "1中份", "1大份", "1把", "2把", "1根", "2根", "适量", "很多"],
"水果": ["1个", "2个", "3个", "1小个", "1大个", "1片", "2片", "适量"],
"坚果": ["1小把", "1把", "2把", "1颗", "2颗", "3颗", "适量"],
"饮料": ["1杯", "2杯", "3杯", "1小杯", "1大杯", "1瓶", "2瓶", "适量"],
"调料": ["1小勺", "1勺", "2勺", "1小匙", "1匙", "2匙", "适量", "少许"]
}
def _load_calorie_estimates(self) -> Dict[str, Dict]:
"""加载热量估算"""
return {
"1小碗": {"米饭": 130, "面条": 131, "": 50},
"1中碗": {"米饭": 195, "面条": 196, "": 75},
"1大碗": {"米饭": 260, "面条": 262, "": 100},
"1个": {"鸡蛋": 77, "苹果": 52, "香蕉": 89, "馒头": 221, "包子": 227},
"2个": {"鸡蛋": 154, "苹果": 104, "香蕉": 178, "馒头": 442, "包子": 454},
"1小块": {"鸡肉": 50, "猪肉": 50, "牛肉": 50, "豆腐": 50},
"2小块": {"鸡肉": 100, "猪肉": 100, "牛肉": 100, "豆腐": 100},
"1杯": {"牛奶": 150, "酸奶": 150, "": 0, "": 0, "咖啡": 0},
"2杯": {"牛奶": 300, "酸奶": 300, "": 0, "": 0, "咖啡": 0},
"1小份": {"白菜": 50, "菠菜": 50, "西兰花": 50, "胡萝卜": 50},
"1中份": {"白菜": 100, "菠菜": 100, "西兰花": 100, "胡萝卜": 100},
"1大份": {"白菜": 150, "菠菜": 150, "西兰花": 150, "胡萝卜": 150},
"1小把": {"花生": 30, "核桃": 30, "杏仁": 30},
"1把": {"花生": 60, "核桃": 60, "杏仁": 60},
"适量": {"default": 50}, # 默认适量为50卡路里
"很多": {"default": 150}, # 默认很多为150卡路里
"1小勺": {"": 0, "": 16, "酱油": 3, "": 2, "": 44, "辣椒": 2, "胡椒": 13, "花椒": 13},
"1勺": {"": 0, "": 32, "酱油": 6, "": 4, "": 88, "辣椒": 4, "胡椒": 25, "花椒": 26},
"2勺": {"": 0, "": 64, "酱油": 12, "": 8, "": 176, "辣椒": 8, "胡椒": 50, "花椒": 52},
"1小匙": {"": 0, "": 8, "酱油": 1.5, "": 1, "": 22, "辣椒": 1, "胡椒": 6, "花椒": 6},
"1匙": {"": 0, "": 16, "酱油": 3, "": 2, "": 44, "辣椒": 2, "胡椒": 13, "花椒": 13},
"2匙": {"": 0, "": 32, "酱油": 6, "": 4, "": 88, "辣椒": 4, "胡椒": 25, "花椒": 26},
"少许": {"default": 5}, # 默认少许为5卡路里
}
def search_foods(self, query: str) -> List[Dict]:
"""搜索食物(优化版本)"""
query = query.lower().strip()
# 检查缓存
if query in self.search_cache:
return self.search_cache[query]
results = []
# 精确匹配优先
for food_name, food_info in self.food_database.items():
if query == food_name.lower():
results.insert(0, {
"name": food_name,
"category": food_info["category"],
"calories_per_100g": food_info["calories_per_100g"],
"match_type": "exact"
})
# 包含匹配
for food_name, food_info in self.food_database.items():
if query in food_name.lower() and query != food_name.lower():
results.append({
"name": food_name,
"category": food_info["category"],
"calories_per_100g": food_info["calories_per_100g"],
"match_type": "contains"
})
# 关键词匹配
if len(results) < 5:
keywords = query.split()
for food_name, food_info in self.food_database.items():
if any(keyword in food_name.lower() for keyword in keywords):
if not any(r["name"] == food_name for r in results):
results.append({
"name": food_name,
"category": food_info["category"],
"calories_per_100g": food_info["calories_per_100g"],
"match_type": "keyword"
})
# 限制结果数量并缓存
results = results[:10]
self.search_cache[query] = results
return results
def get_food_info(self, food_name: str) -> Optional[Dict]:
"""获取食物信息"""
return self.food_database.get(food_name)
def get_portion_options(self, food_name: str) -> List[str]:
"""获取分量选项"""
food_info = self.get_food_info(food_name)
if not food_info:
return ["适量"]
category = food_info["category"]
return self.portion_sizes.get(category, ["适量"])
def estimate_calories(self, food_name: str, portion: str) -> int:
"""估算热量(优化版本)"""
# 检查缓存
cache_key = f"{food_name}_{portion}"
if cache_key in self.calorie_cache:
return self.calorie_cache[cache_key]
# 首先尝试精确匹配
if portion in self.calorie_estimates:
portion_data = self.calorie_estimates[portion]
if food_name in portion_data:
calories = portion_data[food_name]
self.calorie_cache[cache_key] = calories
return calories
elif "default" in portion_data:
calories = portion_data["default"]
self.calorie_cache[cache_key] = calories
return calories
# 使用快速估算
calories = self._calculate_calories_fast(food_name, portion)
self.calorie_cache[cache_key] = calories
return calories
def _estimate_weight(self, portion: str, category: str) -> int:
"""估算重量(克)"""
weight_estimates = {
"1小碗": 100, "1中碗": 150, "1大碗": 200,
"1个": 50, "2个": 100, "3个": 150,
"1小块": 30, "2小块": 60,
"1片": 20, "2片": 40,
"1杯": 150, "2杯": 300,
"1小份": 50, "1中份": 100, "1大份": 150,
"1把": 30, "2把": 60,
"1根": 100, "2根": 200,
"1小把": 15, "1把": 30, "2把": 60,
"1颗": 10, "2颗": 20, "3颗": 30,
"1小勺": 5, "1勺": 10, "2勺": 20,
"1小匙": 3, "1匙": 5, "2匙": 10,
"适量": 50, "很多": 150, "少许": 2
}
return weight_estimates.get(portion, 50)
def _precompute_common_foods(self):
"""预计算常用食物的热量"""
common_foods = [
"米饭", "面条", "馒头", "包子", "饺子", "", "面包",
"鸡蛋", "鸡肉", "猪肉", "牛肉", "鱼肉", "豆腐", "牛奶", "酸奶",
"白菜", "菠菜", "西兰花", "胡萝卜", "土豆", "西红柿", "黄瓜",
"苹果", "香蕉", "橙子", "葡萄", "草莓", "西瓜"
]
common_portions = ["1小碗", "1中碗", "1大碗", "1个", "2个", "1小块", "2小块", "1杯", "2杯"]
for food in common_foods:
for portion in common_portions:
cache_key = f"{food}_{portion}"
if cache_key not in self.calorie_cache:
calories = self._calculate_calories_fast(food, portion)
self.calorie_cache[cache_key] = calories
def _calculate_calories_fast(self, food_name: str, portion: str) -> int:
"""快速计算热量不使用AI"""
# 首先尝试精确匹配
if portion in self.calorie_estimates:
portion_data = self.calorie_estimates[portion]
if food_name in portion_data:
return portion_data[food_name]
elif "default" in portion_data:
return portion_data["default"]
# 使用食物数据库估算
food_info = self.get_food_info(food_name)
if food_info:
weight_estimate = self._estimate_weight(portion, food_info["category"])
calories = int(food_info["calories_per_100g"] * weight_estimate / 100)
return max(calories, 10)
# 基于食物名称的快速估算
return self._quick_estimate_by_name(food_name, portion)
def _quick_estimate_by_name(self, food_name: str, portion: str) -> int:
"""基于食物名称的快速估算"""
# 食物类型快速估算
if any(keyword in food_name for keyword in ["米饭", "面条", "馒头", "包子", "饺子", "", "面包"]):
base_calories = 200 # 主食基础热量
elif any(keyword in food_name for keyword in ["鸡蛋", "鸡肉", "猪肉", "牛肉", "鱼肉", "豆腐", "牛奶", "酸奶"]):
base_calories = 150 # 蛋白质基础热量
elif any(keyword in food_name for keyword in ["白菜", "菠菜", "西兰花", "胡萝卜", "土豆", "西红柿", "黄瓜"]):
base_calories = 50 # 蔬菜基础热量
elif any(keyword in food_name for keyword in ["苹果", "香蕉", "橙子", "葡萄", "草莓", "西瓜"]):
base_calories = 80 # 水果基础热量
else:
base_calories = 100 # 默认基础热量
# 根据分量调整
portion_multiplier = {
"1小碗": 0.8, "1中碗": 1.0, "1大碗": 1.5,
"1个": 0.6, "2个": 1.2, "3个": 1.8,
"1小块": 0.4, "2小块": 0.8,
"1杯": 1.0, "2杯": 2.0,
"适量": 0.8, "很多": 1.5
}
multiplier = portion_multiplier.get(portion, 1.0)
return int(base_calories * multiplier)
def _estimate_calories_with_ai(self, food_name: str, portion: str) -> int:
"""使用AI估算食物热量"""
try:
from llm_integration.qwen_client import get_qwen_client
client = get_qwen_client()
# 构建AI提示词
system_prompt = """
你是一个专业的营养师,擅长估算食物的热量和营养成分。
你的任务是:
1. 根据食物名称和分量估算热量
2. 提供准确的营养信息
3. 考虑食物的常见制作方式
请以JSON格式返回结果包含以下字段
- calories: 估算的热量值(整数)
- category: 食物分类
- confidence: 置信度(0-1)
- reasoning: 估算理由
注意:
- 热量值应该是整数
- 考虑食物的常见分量
- 基于科学的营养学知识
- 如果不确定,给出保守估算
"""
user_prompt = f"""
请估算以下食物的热量:
食物名称: {food_name}
分量: {portion}
请提供:
1. 估算的热量值(卡路里)
2. 食物分类
3. 估算理由
4. 置信度
请以JSON格式返回结果。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = client.chat_completion(messages, temperature=0.2, max_tokens=500)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
result = self._parse_ai_calorie_result(content)
if result.get('success'):
calories = result.get('calories', 50)
# 将AI估算的食物添加到缓存
self._cache_ai_food_info(food_name, result)
return max(calories, 10)
else:
print(f"AI解析失败: {result}")
return 50
except Exception as e:
print(f"AI热量估算失败: {e}")
# 如果AI估算失败返回默认值
return 50
def _parse_ai_calorie_result(self, content: str) -> Dict:
"""解析AI热量估算结果"""
try:
import json
# 尝试提取JSON部分
start_idx = content.find('{')
end_idx = content.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = content[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'calories': int(result_dict.get('calories', 50)),
'category': result_dict.get('category', '其他'),
'confidence': float(result_dict.get('confidence', 0.5)),
'reasoning': result_dict.get('reasoning', 'AI估算')
}
except Exception as e:
print(f"解析AI结果失败: {e}")
return {'success': False, 'calories': 50}
def _cache_ai_food_info(self, food_name: str, ai_result: Dict):
"""缓存AI估算的食物信息"""
try:
# 将AI估算的食物添加到内存数据库
self.food_database[food_name] = {
"category": ai_result.get('category', '其他'),
"calories_per_100g": ai_result.get('calories', 50),
"protein": 0, # AI暂时不提供详细营养成分
"carbs": 0,
"fat": 0,
"ai_estimated": True, # 标记为AI估算
"confidence": ai_result.get('confidence', 0.5)
}
except Exception as e:
print(f"缓存AI食物信息失败: {e}")
def get_food_categories(self) -> List[str]:
"""获取食物分类"""
return ["主食", "蛋白质", "蔬菜", "水果", "坚果", "饮料", "调料"]
def get_foods_by_category(self, category: str) -> List[str]:
"""根据分类获取食物列表"""
foods = []
for food_name, food_info in self.food_database.items():
if food_info["category"] == category:
foods.append(food_name)
return foods
def analyze_food_with_ai(self, food_name: str, portion: str) -> Dict:
"""使用AI分析食物详细信息"""
try:
from llm_integration.qwen_client import get_qwen_client
client = get_qwen_client()
# 构建AI提示词
system_prompt = """
你是一个专业的营养师和食物分析专家,擅长分析食物的营养成分和健康价值。
你的任务是:
1. 分析食物的详细营养成分
2. 估算热量和主要营养素含量
3. 提供健康建议
4. 考虑食物的制作方式
请以JSON格式返回结果包含以下字段
- calories: 热量值(整数)
- protein: 蛋白质含量(克)
- carbs: 碳水化合物含量(克)
- fat: 脂肪含量(克)
- fiber: 纤维含量(克)
- category: 食物分类
- health_tips: 健康建议列表
- cooking_suggestions: 制作建议列表
- confidence: 置信度(0-1)
- reasoning: 分析理由
注意:
- 所有数值应该是数字
- 基于科学的营养学知识
- 考虑食物的常见制作方式
- 提供实用的健康建议
"""
user_prompt = f"""
请详细分析以下食物:
食物名称: {food_name}
分量: {portion}
请提供:
1. 详细的营养成分分析
2. 热量和主要营养素含量
3. 食物分类
4. 健康建议
5. 制作建议
6. 分析理由
请以JSON格式返回结果。
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = client.chat_completion(messages, temperature=0.2, max_tokens=800)
if response and 'choices' in response:
content = response['choices'][0]['message']['content']
result = self._parse_ai_food_analysis(content)
if result.get('success'):
# 缓存AI分析结果
self._cache_ai_food_analysis(food_name, result)
return result
else:
print(f"AI食物分析解析失败: {result}")
return self._get_fallback_food_analysis(food_name, portion)
except Exception as e:
print(f"AI食物分析失败: {e}")
# 如果AI分析失败返回基础估算
return self._get_fallback_food_analysis(food_name, portion)
def _get_fallback_food_analysis(self, food_name: str, portion: str) -> Dict:
"""获取备用食物分析结果"""
return {
'success': False,
'calories': self.estimate_calories(food_name, portion),
'protein': 0,
'carbs': 0,
'fat': 0,
'fiber': 0,
'category': '其他',
'health_tips': ['保持均衡饮食'],
'cooking_suggestions': ['简单烹饪'],
'confidence': 0.3,
'reasoning': '基础估算'
}
def _parse_ai_food_analysis(self, content: str) -> Dict:
"""解析AI食物分析结果"""
try:
import json
# 尝试提取JSON部分
start_idx = content.find('{')
end_idx = content.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
json_str = content[start_idx:end_idx]
result_dict = json.loads(json_str)
return {
'success': True,
'calories': int(result_dict.get('calories', 50)),
'protein': float(result_dict.get('protein', 0)),
'carbs': float(result_dict.get('carbs', 0)),
'fat': float(result_dict.get('fat', 0)),
'fiber': float(result_dict.get('fiber', 0)),
'category': result_dict.get('category', '其他'),
'health_tips': result_dict.get('health_tips', ['保持均衡饮食']),
'cooking_suggestions': result_dict.get('cooking_suggestions', ['简单烹饪']),
'confidence': float(result_dict.get('confidence', 0.5)),
'reasoning': result_dict.get('reasoning', 'AI分析')
}
except Exception as e:
print(f"解析AI食物分析结果失败: {e}")
return {'success': False}
def _cache_ai_food_analysis(self, food_name: str, analysis_result: Dict):
"""缓存AI食物分析结果"""
try:
# 将AI分析的食物添加到内存数据库
self.food_database[food_name] = {
"category": analysis_result.get('category', '其他'),
"calories_per_100g": analysis_result.get('calories', 50),
"protein": analysis_result.get('protein', 0),
"carbs": analysis_result.get('carbs', 0),
"fat": analysis_result.get('fat', 0),
"fiber": analysis_result.get('fiber', 0),
"ai_estimated": True,
"confidence": analysis_result.get('confidence', 0.5),
"health_tips": analysis_result.get('health_tips', []),
"cooking_suggestions": analysis_result.get('cooking_suggestions', [])
}
except Exception as e:
print(f"缓存AI食物分析结果失败: {e}")
class SmartMealRecorder:
"""智能餐食记录器"""
def __init__(self):
self.food_db = SmartFoodDatabase()
def record_meal_smart(self, user_id: str, meal_data: Dict) -> bool:
"""智能记录餐食"""
try:
# 自动估算热量
total_calories = 0
for food_item in meal_data.get("foods", []):
food_name = food_item.get("name", "")
portion = food_item.get("portion", "适量")
calories = self.food_db.estimate_calories(food_name, portion)
total_calories += calories
food_item["estimated_calories"] = calories
# 更新总热量
meal_data["total_calories"] = total_calories
# 保存到数据库
from modules.data_collection import record_meal
return record_meal(user_id, meal_data)
except Exception as e:
print(f"智能记录餐食失败: {e}")
return False
def suggest_foods(self, category: str = None) -> List[str]:
"""建议食物"""
if category:
return self.food_db.get_foods_by_category(category)
else:
# 返回所有食物
return list(self.food_db.food_database.keys())
def search_foods(self, query: str) -> List[Dict]:
"""搜索食物"""
return self.food_db.search_foods(query)
# 全局实例
smart_meal_recorder = SmartMealRecorder()
# 便捷函数
def search_foods(query: str) -> List[Dict]:
"""搜索食物"""
return smart_meal_recorder.search_foods(query)
def get_food_categories() -> List[str]:
"""获取食物分类"""
return smart_meal_recorder.food_db.get_food_categories()
def get_foods_by_category(category: str) -> List[str]:
"""根据分类获取食物"""
return smart_meal_recorder.food_db.get_foods_by_category(category)
def get_portion_options(food_name: str) -> List[str]:
"""获取分量选项"""
return smart_meal_recorder.food_db.get_portion_options(food_name)
def estimate_calories(food_name: str, portion: str) -> int:
"""估算热量"""
return smart_meal_recorder.food_db.estimate_calories(food_name, portion)
def record_meal_smart(user_id: str, meal_data: Dict) -> bool:
"""智能记录餐食"""
return smart_meal_recorder.record_meal_smart(user_id, meal_data)
def analyze_food_with_ai(food_name: str, portion: str) -> Dict:
"""使用AI分析食物详细信息"""
return smart_meal_recorder.food_db.analyze_food_with_ai(food_name, portion)
def get_food_ai_suggestions(food_name: str) -> Dict:
"""获取食物的AI建议"""
try:
food_info = smart_meal_recorder.food_db.get_food_info(food_name)
if food_info and food_info.get('ai_estimated'):
return {
'health_tips': food_info.get('health_tips', ['保持均衡饮食']),
'cooking_suggestions': food_info.get('cooking_suggestions', ['简单烹饪']),
'confidence': food_info.get('confidence', 0.5)
}
else:
# 如果数据库中没有AI分析结果进行AI分析
analysis = analyze_food_with_ai(food_name, "适量")
return {
'health_tips': analysis.get('health_tips', ['保持均衡饮食']),
'cooking_suggestions': analysis.get('cooking_suggestions', ['简单烹饪']),
'confidence': analysis.get('confidence', 0.5)
}
except Exception as e:
print(f"获取AI建议失败: {e}")
return {
'health_tips': ['保持均衡饮食'],
'cooking_suggestions': ['简单烹饪'],
'confidence': 0.3
}
if __name__ == "__main__":
# 测试智能食物数据库
print("测试智能食物数据库...")
# 测试搜索
results = search_foods("")
print(f"搜索''的结果: {results}")
# 测试分类
categories = get_food_categories()
print(f"食物分类: {categories}")
# 测试分量选项
portions = get_portion_options("鸡肉")
print(f"鸡肉的分量选项: {portions}")
# 测试热量估算
calories = estimate_calories("鸡肉", "1小块")
print(f"1小块鸡肉的热量: {calories}卡路里")
print("智能食物数据库测试完成!")

145
start.py Normal file
View File

@@ -0,0 +1,145 @@
"""
启动脚本 - 个性化饮食推荐助手
"""
import sys
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
def check_dependencies():
"""检查依赖包"""
required_packages = [
('customtkinter', 'customtkinter'),
('openai', 'openai'),
('anthropic', 'anthropic'),
('sklearn', 'scikit-learn'),
('pandas', 'pandas'),
('numpy', 'numpy'),
('dotenv', 'python-dotenv')
]
missing_packages = []
for import_name, package_name in required_packages:
try:
__import__(import_name)
except ImportError:
missing_packages.append(package_name)
if missing_packages:
print("❌ 缺少以下依赖包:")
for package in missing_packages:
print(f" - {package}")
print("\n请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
print("✅ 所有依赖包已安装")
return True
def check_config():
"""检查配置文件"""
env_file = Path('.env')
if not env_file.exists():
print("⚠️ 配置文件 .env 不存在")
print("正在创建示例配置文件...")
env_content = """# 个性化饮食推荐助手配置文件
# 数据库配置
DATABASE_URL=sqlite:///./data/app.db
# 大模型API配置 (可选,不配置将使用备用方案)
OPENAI_API_KEY=your-openai-api-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here
# 模型配置
MODEL_SAVE_PATH=./models/
TRAINING_DATA_PATH=./data/training/
USER_DATA_PATH=./data/users/
# 推荐系统配置
RECOMMENDATION_TOP_K=5
MIN_TRAINING_SAMPLES=10
MODEL_RETRAIN_THRESHOLD=50
# 用户画像配置
ENABLE_PHYSIOLOGICAL_TRACKING=true
ENABLE_ASTROLOGY_FACTORS=true
ENABLE_TASTE_PREFERENCES=true
# GUI配置
APP_TITLE=个性化饮食推荐助手
WINDOW_SIZE=1200x800
THEME=dark
# 开发配置
DEBUG=true
LOG_LEVEL=INFO
"""
with open('.env', 'w', encoding='utf-8') as f:
f.write(env_content)
print("✅ 示例配置文件已创建: .env")
print("💡 提示: 如需使用大模型功能,请在 .env 文件中配置API密钥")
else:
print("✅ 配置文件存在")
return True
def create_directories():
"""创建必要的目录"""
directories = [
'data',
'data/users',
'data/training',
'models',
'logs',
'gui'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
print("✅ 目录结构创建完成")
def main():
"""主函数"""
print("🍎 个性化饮食推荐助手 - 启动检查")
print("=" * 50)
# 检查依赖
if not check_dependencies():
return False
# 检查配置
if not check_config():
return False
# 创建目录
create_directories()
print("\n🚀 启动应用...")
print("=" * 50)
try:
# 导入并运行主应用
from main import main as run_app
run_app()
except KeyboardInterrupt:
print("\n👋 用户中断,应用退出")
except Exception as e:
print(f"\n❌ 应用启动失败: {e}")
return False
return True
if __name__ == "__main__":
success = main()
if not success:
sys.exit(1)