Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能
This commit is contained in:
153
.gitignore
vendored
Normal file
153
.gitignore
vendored
Normal 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
BIN
CN107633875A.pdf
Normal file
Binary file not shown.
237
OCR_USAGE_GUIDE.md
Normal file
237
OCR_USAGE_GUIDE.md
Normal 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
193
PROJECT_SUMMARY.md
Normal 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
324
README.md
Normal 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为您的饮食健康保驾护航!**
|
||||
213
UI_BEAUTIFICATION_SUMMARY.md
Normal file
213
UI_BEAUTIFICATION_SUMMARY.md
Normal 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
425
config/settings.py
Normal 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
620
core/base.py
Normal 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
928
core/base_engine.py
Normal 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
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
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
625
gui/new_main_window.py
Normal 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
637
gui/ocr_calorie_gui.py
Normal 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
526
gui/quick_user_input.py
Normal 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
579
gui/smart_meal_record.py
Normal 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
326
gui/styles.py
Normal 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()
|
||||
599
llm_integration/qwen_client.py
Normal file
599
llm_integration/qwen_client.py
Normal 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
249
main.py
Normal 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
778
modules/ai_analysis.py
Normal 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
367
modules/data_collection.py
Normal 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("数据采集模块测试完成!")
|
||||
634
modules/efficient_data_processing.py
Normal file
634
modules/efficient_data_processing.py
Normal 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("测试完成!")
|
||||
786
modules/ocr_calorie_recognition.py
Normal file
786
modules/ocr_calorie_recognition.py
Normal 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模块初始化失败")
|
||||
1016
modules/recommendation_engine.py
Normal file
1016
modules/recommendation_engine.py
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
30
requirements.txt
Normal 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
|
||||
728
smart_food/smart_database.py
Normal file
728
smart_food/smart_database.py
Normal 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
145
start.py
Normal 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)
|
||||
Reference in New Issue
Block a user