commit aea5f6bf74fde5e31c53fe02f08bb4b7941ef72e Author: 赵杰 Date: Thu Sep 25 14:20:11 2025 +0100 Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78b3bed --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/CN107633875A.pdf b/CN107633875A.pdf new file mode 100644 index 0000000..dc1a9de Binary files /dev/null and b/CN107633875A.pdf differ diff --git a/OCR_USAGE_GUIDE.md b/OCR_USAGE_GUIDE.md new file mode 100644 index 0000000..38a2258 --- /dev/null +++ b/OCR_USAGE_GUIDE.md @@ -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月* diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md new file mode 100644 index 0000000..485f0b5 --- /dev/null +++ b/PROJECT_SUMMARY.md @@ -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界面 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2be2f4c --- /dev/null +++ b/README.md @@ -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 +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 +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为您的饮食健康保驾护航!** \ No newline at end of file diff --git a/UI_BEAUTIFICATION_SUMMARY.md b/UI_BEAUTIFICATION_SUMMARY.md new file mode 100644 index 0000000..a736a2c --- /dev/null +++ b/UI_BEAUTIFICATION_SUMMARY.md @@ -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月* diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..76b056f --- /dev/null +++ b/config/settings.py @@ -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("✅ 配置系统测试完成") diff --git a/core/base.py b/core/base.py new file mode 100644 index 0000000..bc8aa66 --- /dev/null +++ b/core/base.py @@ -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("基座架构测试完成!") diff --git a/core/base_engine.py b/core/base_engine.py new file mode 100644 index 0000000..04eb859 --- /dev/null +++ b/core/base_engine.py @@ -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()) diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 0000000..e65dd26 --- /dev/null +++ b/gui/main_window.py @@ -0,0 +1,1504 @@ +""" +主GUI界面 - 基于CustomTkinter的现代化界面 +""" + +import tkinter as tk +from tkinter import ttk, messagebox, scrolledtext +import customtkinter as ctk +from typing import Optional, Dict, Any, List +from datetime import datetime, date +import json +import threading +from core.base import AppCore, UserData, ModuleType +# 移除直接导入,改为通过应用核心调用 +# from modules.data_collection import collect_questionnaire_data, record_meal, record_feedback +# from modules.ai_analysis import analyze_user_intent, analyze_nutrition, analyze_physiological_state +# from modules.recommendation_engine import generate_meal_recommendations, find_similar_foods + +# 设置CustomTkinter主题 +ctk.set_appearance_mode("dark") +ctk.set_default_color_theme("blue") + + +class MainWindow: + """主窗口类""" + + def __init__(self, root: tk.Tk, app_core: AppCore): + self.root = root + self.app_core = app_core + self.current_user_id: Optional[str] = None + self.current_user_data: Optional[UserData] = None + + # 设置窗口属性 + self._setup_window() + + # 创建界面 + self._create_widgets() + + # 绑定事件 + self._bind_events() + + # 初始化界面状态 + self._initialize_ui_state() + + def _setup_window(self): + """设置窗口属性""" + self.root.title("个性化饮食推荐助手") + self.root.geometry("1200x800") + self.root.minsize(800, 600) + + # 设置窗口图标(如果有的话) + try: + # self.root.iconbitmap("assets/icon.ico") + pass + except: + pass + + def _create_widgets(self): + """创建界面组件""" + # 创建主框架 + self.main_frame = ctk.CTkFrame(self.root) + self.main_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 创建顶部导航栏 + self._create_navigation_bar() + + # 创建主内容区域 + self._create_main_content() + + # 创建状态栏 + self._create_status_bar() + + def _create_navigation_bar(self): + """创建导航栏""" + nav_frame = ctk.CTkFrame(self.main_frame) + nav_frame.pack(fill="x", padx=10, pady=(10, 5)) + + # 应用标题 + title_label = ctk.CTkLabel( + nav_frame, + text="🍎 个性化饮食推荐助手", + font=ctk.CTkFont(size=24, weight="bold") + ) + title_label.pack(side="left", padx=20, pady=10) + + # 用户信息区域 + self.user_info_frame = ctk.CTkFrame(nav_frame) + self.user_info_frame.pack(side="right", padx=20, pady=10) + + self.user_label = ctk.CTkLabel( + self.user_info_frame, + text="未登录", + font=ctk.CTkFont(size=14) + ) + self.user_label.pack(padx=10, pady=5) + + # 登录/注册按钮 + self.login_button = ctk.CTkButton( + self.user_info_frame, + text="登录/注册", + command=self._show_login_dialog, + width=100 + ) + self.login_button.pack(padx=10, pady=5) + + def _create_main_content(self): + """创建主内容区域""" + # 创建选项卡 + self.tabview = ctk.CTkTabview(self.main_frame) + self.tabview.pack(fill="both", expand=True, padx=10, pady=5) + + # 添加选项卡 + self.tabview.add("数据采集") + self.tabview.add("AI分析") + self.tabview.add("推荐系统") + self.tabview.add("历史推荐") + self.tabview.add("个人中心") + + # 设置选项卡名称 + self.tabview.set("数据采集") + + # 创建各个选项卡的内容 + self._create_data_collection_tab() + self._create_ai_analysis_tab() + self._create_recommendation_tab() + self._create_history_recommend_tab() + self._create_profile_tab() + + def _create_data_collection_tab(self): + """创建数据采集选项卡""" + tab = self.tabview.tab("数据采集") + + # 创建滚动框架 + scroll_frame = ctk.CTkScrollableFrame(tab) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 问卷部分 + questionnaire_frame = ctk.CTkFrame(scroll_frame) + questionnaire_frame.pack(fill="x", padx=10, pady=10) + + questionnaire_title = ctk.CTkLabel( + questionnaire_frame, + text="📋 用户问卷", + font=ctk.CTkFont(size=18, weight="bold") + ) + questionnaire_title.pack(pady=10) + + # 问卷类型选择 + self.questionnaire_type_var = tk.StringVar(value="basic") + questionnaire_type_label = ctk.CTkLabel(questionnaire_frame, text="问卷类型:") + questionnaire_type_label.pack(anchor="w", padx=20, pady=5) + + questionnaire_type_menu = ctk.CTkOptionMenu( + questionnaire_frame, + variable=self.questionnaire_type_var, + values=["basic", "taste", "physiological"], + command=self._on_questionnaire_type_changed + ) + questionnaire_type_menu.pack(anchor="w", padx=20, pady=5) + + # 问卷内容区域 + self.questionnaire_content_frame = ctk.CTkFrame(questionnaire_frame) + self.questionnaire_content_frame.pack(fill="x", padx=20, pady=10) + + # 餐食记录部分 + meal_frame = ctk.CTkFrame(scroll_frame) + meal_frame.pack(fill="x", padx=10, pady=10) + + meal_title = ctk.CTkLabel( + meal_frame, + text="🍽️ 餐食记录", + font=ctk.CTkFont(size=18, weight="bold") + ) + meal_title.pack(pady=10) + + # 餐食记录表单 + self._create_meal_record_form(meal_frame) + + # 反馈记录部分 + feedback_frame = ctk.CTkFrame(scroll_frame) + feedback_frame.pack(fill="x", padx=10, pady=10) + + feedback_title = ctk.CTkLabel( + feedback_frame, + text="💬 用户反馈", + font=ctk.CTkFont(size=18, weight="bold") + ) + feedback_title.pack(pady=10) + + # 反馈记录表单 + self._create_feedback_form(feedback_frame) + + def _create_meal_record_form(self, parent): + """创建餐食记录表单""" + form_frame = ctk.CTkFrame(parent) + form_frame.pack(fill="x", padx=20, pady=10) + + # 日期选择 + date_label = ctk.CTkLabel(form_frame, text="日期:") + date_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.meal_date_var = tk.StringVar(value=datetime.now().strftime('%Y-%m-%d')) + date_entry = ctk.CTkEntry(form_frame, textvariable=self.meal_date_var, width=150) + date_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 餐次选择 + meal_type_label = ctk.CTkLabel(form_frame, text="餐次:") + meal_type_label.grid(row=1, column=0, sticky="w", padx=10, pady=5) + + self.meal_type_var = tk.StringVar(value="breakfast") + meal_type_menu = ctk.CTkOptionMenu( + form_frame, + variable=self.meal_type_var, + values=["breakfast", "lunch", "dinner"] + ) + meal_type_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5) + + # 食物输入 + foods_label = ctk.CTkLabel(form_frame, text="食物:") + foods_label.grid(row=2, column=0, sticky="w", padx=10, pady=5) + + self.foods_text = ctk.CTkTextbox(form_frame, height=60, width=300) + self.foods_text.grid(row=2, column=1, sticky="w", padx=10, pady=5) + self.foods_text.insert("1.0", "请输入食物名称,每行一个") + self.foods_text.bind("", self._on_foods_changed) + + # 分量输入 + quantities_label = ctk.CTkLabel(form_frame, text="分量:") + quantities_label.grid(row=3, column=0, sticky="w", padx=10, pady=5) + + self.quantities_text = ctk.CTkTextbox(form_frame, height=60, width=300) + self.quantities_text.grid(row=3, column=1, sticky="w", padx=10, pady=5) + self.quantities_text.insert("1.0", "请输入对应分量,每行一个") + self.quantities_text.bind("", self._on_quantities_changed) + + # 热量显示(自动估算) + calories_label = ctk.CTkLabel(form_frame, text="预估热量:") + calories_label.grid(row=4, column=0, sticky="w", padx=10, pady=5) + + self.calories_display = ctk.CTkLabel(form_frame, text="系统将自动估算", width=150, anchor="w") + self.calories_display.grid(row=4, column=1, sticky="w", padx=10, pady=5) + + # 满意度评分 + satisfaction_label = ctk.CTkLabel(form_frame, text="满意度:") + satisfaction_label.grid(row=5, column=0, sticky="w", padx=10, pady=5) + + self.satisfaction_var = tk.IntVar(value=3) + satisfaction_slider = ctk.CTkSlider( + form_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.satisfaction_var + ) + satisfaction_slider.grid(row=5, column=1, sticky="w", padx=10, pady=5) + + # 快速录入按钮 + quick_input_button = ctk.CTkButton( + form_frame, + text="🚀 快速录入", + command=self._show_quick_input_dialog, + width=150, + fg_color="purple" + ) + quick_input_button.grid(row=6, column=0, sticky="w", padx=10, pady=10) + + # 智能记录按钮 + smart_record_button = ctk.CTkButton( + form_frame, + text="智能记录餐食", + command=self._show_smart_meal_record, + width=150, + fg_color="green" + ) + smart_record_button.grid(row=6, column=1, sticky="w", padx=10, pady=10) + + # 传统记录按钮 + save_meal_button = ctk.CTkButton( + form_frame, + text="手动记录餐食", + command=self._save_meal_record, + width=150 + ) + save_meal_button.grid(row=6, column=1, sticky="w", padx=10, pady=10) + + def _create_feedback_form(self, parent): + """创建反馈表单""" + form_frame = ctk.CTkFrame(parent) + form_frame.pack(fill="x", padx=20, pady=10) + + # 推荐食物 + recommended_label = ctk.CTkLabel(form_frame, text="推荐食物:") + recommended_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.recommended_foods_text = ctk.CTkTextbox(form_frame, height=60, width=300) + self.recommended_foods_text.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 用户选择 + user_choice_label = ctk.CTkLabel(form_frame, text="用户选择:") + user_choice_label.grid(row=1, column=0, sticky="w", padx=10, pady=5) + + self.user_choice_var = tk.StringVar() + user_choice_entry = ctk.CTkEntry(form_frame, textvariable=self.user_choice_var, width=300) + user_choice_entry.grid(row=1, column=1, sticky="w", padx=10, pady=5) + + # 反馈类型 + feedback_type_label = ctk.CTkLabel(form_frame, text="反馈类型:") + feedback_type_label.grid(row=2, column=0, sticky="w", padx=10, pady=5) + + self.feedback_type_var = tk.StringVar(value="like") + feedback_type_menu = ctk.CTkOptionMenu( + form_frame, + variable=self.feedback_type_var, + values=["like", "dislike", "ate"] + ) + feedback_type_menu.grid(row=2, column=1, sticky="w", padx=10, pady=5) + + # 保存按钮 + save_feedback_button = ctk.CTkButton( + form_frame, + text="保存反馈", + command=self._save_feedback, + width=150 + ) + save_feedback_button.grid(row=3, column=1, sticky="w", padx=10, pady=10) + + def _create_ai_analysis_tab(self): + """创建AI分析选项卡""" + tab = self.tabview.tab("AI分析") + + # 创建滚动框架 + scroll_frame = ctk.CTkScrollableFrame(tab) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 用户意图分析 + intent_frame = ctk.CTkFrame(scroll_frame) + intent_frame.pack(fill="x", padx=10, pady=10) + + intent_title = ctk.CTkLabel( + intent_frame, + text="🧠 用户意图分析", + font=ctk.CTkFont(size=18, weight="bold") + ) + intent_title.pack(pady=10) + + # 用户输入 + input_label = ctk.CTkLabel(intent_frame, text="用户输入:") + input_label.pack(anchor="w", padx=20, pady=5) + + self.user_input_text = ctk.CTkTextbox(intent_frame, height=80, width=600) + self.user_input_text.pack(fill="x", padx=20, pady=5) + self.user_input_text.insert("1.0", "请输入用户的饮食需求或问题...") + + # 分析按钮 + analyze_button = ctk.CTkButton( + intent_frame, + text="分析用户意图", + command=self._analyze_user_intent, + width=150 + ) + analyze_button.pack(padx=20, pady=10) + + # 分析结果显示 + self.intent_result_text = ctk.CTkTextbox(intent_frame, height=200, width=600) + self.intent_result_text.pack(fill="x", padx=20, pady=10) + + # 营养分析 + nutrition_frame = ctk.CTkFrame(scroll_frame) + nutrition_frame.pack(fill="x", padx=10, pady=10) + + nutrition_title = ctk.CTkLabel( + nutrition_frame, + text="🥗 营养分析", + font=ctk.CTkFont(size=18, weight="bold") + ) + nutrition_title.pack(pady=10) + + # 营养分析按钮 + nutrition_button = ctk.CTkButton( + nutrition_frame, + text="分析最近餐食营养", + command=self._analyze_nutrition, + width=150 + ) + nutrition_button.pack(padx=20, pady=10) + + # 营养分析结果显示 + self.nutrition_result_text = ctk.CTkTextbox(nutrition_frame, height=200, width=600) + self.nutrition_result_text.pack(fill="x", padx=20, pady=10) + + def _create_recommendation_tab(self): + """创建推荐系统选项卡""" + tab = self.tabview.tab("推荐系统") + + # 创建滚动框架 + scroll_frame = ctk.CTkScrollableFrame(tab) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 餐食推荐 + recommendation_frame = ctk.CTkFrame(scroll_frame) + recommendation_frame.pack(fill="x", padx=10, pady=10) + + recommendation_title = ctk.CTkLabel( + recommendation_frame, + text="🎯 个性化推荐", + font=ctk.CTkFont(size=18, weight="bold") + ) + recommendation_title.pack(pady=10) + + # 推荐参数 + params_frame = ctk.CTkFrame(recommendation_frame) + params_frame.pack(fill="x", padx=20, pady=10) + + # 餐次选择 + meal_type_label = ctk.CTkLabel(params_frame, text="餐次:") + meal_type_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.recommendation_meal_type_var = tk.StringVar(value="lunch") + meal_type_menu = ctk.CTkOptionMenu( + params_frame, + variable=self.recommendation_meal_type_var, + values=["breakfast", "lunch", "dinner"] + ) + meal_type_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 口味偏好 + taste_label = ctk.CTkLabel(params_frame, text="口味偏好:") + taste_label.grid(row=1, column=0, sticky="w", padx=10, pady=5) + + self.taste_preference_var = tk.StringVar(value="balanced") + taste_menu = ctk.CTkOptionMenu( + params_frame, + variable=self.taste_preference_var, + values=["balanced", "sweet", "salty", "spicy", "sour"] + ) + taste_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5) + + # 推荐按钮 + recommend_button = ctk.CTkButton( + params_frame, + text="生成推荐", + command=self._generate_recommendations, + width=150 + ) + recommend_button.grid(row=2, column=1, sticky="w", padx=10, pady=10) + + # 推荐结果显示 + self.recommendation_result_text = ctk.CTkTextbox(recommendation_frame, height=300, width=600) + self.recommendation_result_text.pack(fill="x", padx=20, pady=10) + + def _create_history_recommend_tab(self): + """创建历史数据驱动的推荐页签(前端仅展示推荐列表,训练在后台)""" + tab = self.tabview.tab("历史推荐") + + # 创建滚动框架 + scroll_frame = ctk.CTkScrollableFrame(tab) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 标题 + title = ctk.CTkLabel( + scroll_frame, + text="📊 基于历史数据的个性化推荐", + font=ctk.CTkFont(size=18, weight="bold") + ) + title.pack(anchor="w", padx=10, pady=10) + + # 说明 + info = ctk.CTkLabel( + scroll_frame, + text="训练在后台自动进行,页面展示最新推荐结果。", + font=ctk.CTkFont(size=12) + ) + info.pack(anchor="w", padx=10, pady=5) + + # 控制区域 + control_frame = ctk.CTkFrame(scroll_frame) + control_frame.pack(fill="x", padx=10, pady=10) + + # 餐次选择 + meal_type_label = ctk.CTkLabel(control_frame, text="餐次:") + meal_type_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.hist_meal_type_var = tk.StringVar(value="lunch") + meal_menu = ctk.CTkOptionMenu( + control_frame, + variable=self.hist_meal_type_var, + values=["breakfast", "lunch", "dinner", "snack"] + ) + meal_menu.grid(row=0, column=1, padx=5, pady=5, sticky="w") + + # 刷新按钮 + refresh_btn = ctk.CTkButton( + control_frame, + text="🔄 刷新推荐", + command=self._refresh_history_recommendations + ) + refresh_btn.grid(row=0, column=2, padx=5, pady=5, sticky="w") + + # 结果显示区域 + self.history_rec_text = ctk.CTkTextbox(scroll_frame, height=420) + self.history_rec_text.pack(fill="both", expand=True, padx=10, pady=10) + + # 页面打开时自动触发一次刷新 + self.root.after(300, self._refresh_history_recommendations) + + def _refresh_history_recommendations(self): + """刷新历史推荐""" + if not self.current_user_id: + self._update_status("请先登录") + return + + meal_type = self.hist_meal_type_var.get() + + def work(): + try: + # 启动后台训练(幂等) + from modules.efficient_data_processing import training_pipeline + training_pipeline.start_background_training() + + # 立即进行一次快速训练+推荐(内部做了缓存) + recs = training_pipeline.predict_recommendations(self.current_user_id, meal_type) + self.root.after(0, lambda: self._render_history_recs(recs)) + except Exception as e: + self.root.after(0, lambda: self._update_status(f"历史推荐失败: {e}")) + + threading.Thread(target=work, daemon=True).start() + + def _render_history_recs(self, recs: List[Dict[str, Any]]): + """渲染历史推荐结果""" + self.history_rec_text.delete("1.0", "end") + + if not recs: + self.history_rec_text.insert("1.0", "暂无推荐,请先记录一些餐食或稍后再试。") + return + + lines = [] + for i, r in enumerate(recs, 1): + food = r.get('food', '推荐项') + confidence = r.get('confidence', 0) + reason = r.get('reason', '-') + lines.append(f"{i}. {food} 可信度: {confidence:.2f} 原因: {reason}") + + self.history_rec_text.insert("1.0", "\n".join(lines)) + + def _create_profile_tab(self): + """创建个人中心选项卡""" + tab = self.tabview.tab("个人中心") + + # 创建滚动框架 + scroll_frame = ctk.CTkScrollableFrame(tab) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 用户信息 + profile_frame = ctk.CTkFrame(scroll_frame) + profile_frame.pack(fill="x", padx=10, pady=10) + + profile_title = ctk.CTkLabel( + profile_frame, + text="👤 个人信息", + font=ctk.CTkFont(size=18, weight="bold") + ) + profile_title.pack(pady=10) + + # 用户信息显示 + self.profile_info_text = ctk.CTkTextbox(profile_frame, height=200, width=600) + self.profile_info_text.pack(fill="x", padx=20, pady=10) + + # 刷新按钮 + refresh_button = ctk.CTkButton( + profile_frame, + text="刷新信息", + command=self._refresh_profile_info, + width=150 + ) + refresh_button.pack(padx=20, pady=10) + + # 数据统计 + stats_frame = ctk.CTkFrame(scroll_frame) + stats_frame.pack(fill="x", padx=10, pady=10) + + stats_title = ctk.CTkLabel( + stats_frame, + text="📊 数据统计", + font=ctk.CTkFont(size=18, weight="bold") + ) + stats_title.pack(pady=10) + + # 统计数据 + self.stats_text = ctk.CTkTextbox(stats_frame, height=200, width=600) + self.stats_text.pack(fill="x", padx=20, pady=10) + + def _create_status_bar(self): + """创建状态栏""" + self.status_frame = ctk.CTkFrame(self.main_frame) + self.status_frame.pack(fill="x", padx=10, pady=(5, 10)) + + self.status_label = ctk.CTkLabel( + self.status_frame, + text="就绪", + font=ctk.CTkFont(size=12) + ) + self.status_label.pack(side="left", padx=10, pady=5) + + # 模块状态 + self.module_status_label = ctk.CTkLabel( + self.status_frame, + text="模块状态: 正常", + font=ctk.CTkFont(size=12) + ) + self.module_status_label.pack(side="right", padx=10, pady=5) + + def _bind_events(self): + """绑定事件""" + pass + + def _initialize_ui_state(self): + """初始化界面状态""" + self._update_status("就绪") + self._load_questionnaire_content("basic") + + def _update_status(self, message: str): + """更新状态栏""" + self.status_label.configure(text=message) + self.root.update_idletasks() + + def _show_login_dialog(self): + """显示登录对话框""" + dialog = LoginDialog(self.root, self) + self.root.wait_window(dialog.dialog) + + def _on_questionnaire_type_changed(self, value): + """问卷类型改变事件""" + self._load_questionnaire_content(value) + + def _load_questionnaire_content(self, questionnaire_type: str): + """加载问卷内容""" + # 清空现有内容 + for widget in self.questionnaire_content_frame.winfo_children(): + widget.destroy() + + # 根据问卷类型创建内容 + if questionnaire_type == "basic": + self._create_basic_questionnaire() + elif questionnaire_type == "taste": + self._create_taste_questionnaire() + elif questionnaire_type == "physiological": + self._create_physiological_questionnaire() + + def _create_basic_questionnaire(self): + """创建基础问卷""" + # 姓名 + name_label = ctk.CTkLabel(self.questionnaire_content_frame, text="姓名:") + name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.name_var = tk.StringVar() + name_entry = ctk.CTkEntry(self.questionnaire_content_frame, textvariable=self.name_var, width=200) + name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 年龄范围选择 + age_label = ctk.CTkLabel(self.questionnaire_content_frame, text="年龄范围:") + age_label.grid(row=1, column=0, sticky="w", padx=10, pady=5) + + self.age_range_var = tk.StringVar(value="25-30岁") + age_menu = ctk.CTkOptionMenu( + self.questionnaire_content_frame, + variable=self.age_range_var, + values=["18-24岁", "25-30岁", "31-35岁", "36-40岁", "41-45岁", "46-50岁", "51-55岁", "56-60岁", "60岁以上"] + ) + age_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5) + + # 性别 + gender_label = ctk.CTkLabel(self.questionnaire_content_frame, text="性别:") + gender_label.grid(row=2, column=0, sticky="w", padx=10, pady=5) + + self.gender_var = tk.StringVar(value="女") + gender_menu = ctk.CTkOptionMenu( + self.questionnaire_content_frame, + variable=self.gender_var, + values=["男", "女"] + ) + gender_menu.grid(row=2, column=1, sticky="w", padx=10, pady=5) + + # 身高范围 + height_label = ctk.CTkLabel(self.questionnaire_content_frame, text="身高范围:") + height_label.grid(row=3, column=0, sticky="w", padx=10, pady=5) + + self.height_range_var = tk.StringVar(value="160-165cm") + height_menu = ctk.CTkOptionMenu( + self.questionnaire_content_frame, + variable=self.height_range_var, + values=["150cm以下", "150-155cm", "155-160cm", "160-165cm", "165-170cm", "170-175cm", "175-180cm", "180cm以上"] + ) + height_menu.grid(row=3, column=1, sticky="w", padx=10, pady=5) + + # 体重范围 + weight_label = ctk.CTkLabel(self.questionnaire_content_frame, text="体重范围:") + weight_label.grid(row=4, column=0, sticky="w", padx=10, pady=5) + + self.weight_range_var = tk.StringVar(value="50-55kg") + weight_menu = ctk.CTkOptionMenu( + self.questionnaire_content_frame, + variable=self.weight_range_var, + values=["40kg以下", "40-45kg", "45-50kg", "50-55kg", "55-60kg", "60-65kg", "65-70kg", "70-75kg", "75-80kg", "80kg以上"] + ) + weight_menu.grid(row=4, column=1, sticky="w", padx=10, pady=5) + + # 活动水平 + activity_label = ctk.CTkLabel(self.questionnaire_content_frame, text="活动水平:") + activity_label.grid(row=5, column=0, sticky="w", padx=10, pady=5) + + self.activity_var = tk.StringVar(value="中等") + activity_menu = ctk.CTkOptionMenu( + self.questionnaire_content_frame, + variable=self.activity_var, + values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"] + ) + activity_menu.grid(row=5, column=1, sticky="w", padx=10, pady=5) + + # 保存按钮 + save_button = ctk.CTkButton( + self.questionnaire_content_frame, + text="保存基础信息", + command=self._save_basic_questionnaire, + width=150 + ) + save_button.grid(row=6, column=1, sticky="w", padx=10, pady=10) + + def _create_taste_questionnaire(self): + """创建口味问卷""" + # 甜味偏好 + sweet_label = ctk.CTkLabel(self.questionnaire_content_frame, text="甜味偏好:") + sweet_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.sweet_var = tk.IntVar(value=3) + sweet_slider = ctk.CTkSlider( + self.questionnaire_content_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.sweet_var + ) + sweet_slider.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 咸味偏好 + salty_label = ctk.CTkLabel(self.questionnaire_content_frame, text="咸味偏好:") + salty_label.grid(row=1, column=0, sticky="w", padx=10, pady=5) + + self.salty_var = tk.IntVar(value=3) + salty_slider = ctk.CTkSlider( + self.questionnaire_content_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.salty_var + ) + salty_slider.grid(row=1, column=1, sticky="w", padx=10, pady=5) + + # 辣味偏好 + spicy_label = ctk.CTkLabel(self.questionnaire_content_frame, text="辣味偏好:") + spicy_label.grid(row=2, column=0, sticky="w", padx=10, pady=5) + + self.spicy_var = tk.IntVar(value=3) + spicy_slider = ctk.CTkSlider( + self.questionnaire_content_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.spicy_var + ) + spicy_slider.grid(row=2, column=1, sticky="w", padx=10, pady=5) + + # 酸味偏好 + sour_label = ctk.CTkLabel(self.questionnaire_content_frame, text="酸味偏好:") + sour_label.grid(row=3, column=0, sticky="w", padx=10, pady=5) + + self.sour_var = tk.IntVar(value=3) + sour_slider = ctk.CTkSlider( + self.questionnaire_content_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.sour_var + ) + sour_slider.grid(row=3, column=1, sticky="w", padx=10, pady=5) + + # 苦味偏好 + bitter_label = ctk.CTkLabel(self.questionnaire_content_frame, text="苦味偏好:") + bitter_label.grid(row=4, column=0, sticky="w", padx=10, pady=5) + + self.bitter_var = tk.IntVar(value=3) + bitter_slider = ctk.CTkSlider( + self.questionnaire_content_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.bitter_var + ) + bitter_slider.grid(row=4, column=1, sticky="w", padx=10, pady=5) + + # 保存按钮 + save_button = ctk.CTkButton( + self.questionnaire_content_frame, + text="保存口味偏好", + command=self._save_taste_questionnaire, + width=150 + ) + save_button.grid(row=5, column=1, sticky="w", padx=10, pady=10) + + def _create_physiological_questionnaire(self): + """创建生理问卷""" + # 月经周期长度 + cycle_label = ctk.CTkLabel(self.questionnaire_content_frame, text="月经周期长度:") + cycle_label.grid(row=0, column=0, sticky="w", padx=10, pady=5) + + self.cycle_length_var = tk.StringVar(value="28") + cycle_entry = ctk.CTkEntry(self.questionnaire_content_frame, textvariable=self.cycle_length_var, width=200) + cycle_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5) + + # 保存按钮 + save_button = ctk.CTkButton( + self.questionnaire_content_frame, + text="保存生理信息", + command=self._save_physiological_questionnaire, + width=150 + ) + save_button.grid(row=1, column=1, sticky="w", padx=10, pady=10) + + def _save_basic_questionnaire(self): + """保存基础问卷""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + # 将范围转换为具体数值 + age_range = self.age_range_var.get() + height_range = self.height_range_var.get() + weight_range = self.weight_range_var.get() + + # 年龄范围转换 + 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" + } + + answers = { + 'name': self.name_var.get(), + 'age': age_mapping.get(age_range, 25), + 'gender': self.gender_var.get(), + 'height': height_mapping.get(height_range, 165), + 'weight': weight_mapping.get(weight_range, 55), + 'activity_level': activity_mapping.get(self.activity_var.get(), 'moderate') + } + + try: + # 通过应用核心调用数据收集模块 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'questionnaire', 'questionnaire_type': 'basic', 'answers': answers}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "基础信息保存成功") + else: + messagebox.showerror("错误", "基础信息保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _save_taste_questionnaire(self): + """保存口味问卷""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + answers = { + 'sweet': self.sweet_var.get() + } + + try: + # 通过应用核心调用数据收集模块 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'questionnaire', 'questionnaire_type': 'taste', 'answers': answers}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "口味偏好保存成功") + else: + messagebox.showerror("错误", "口味偏好保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _save_physiological_questionnaire(self): + """保存生理问卷""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + answers = { + 'menstrual_cycle_length': int(self.cycle_length_var.get()) if self.cycle_length_var.get().isdigit() else 28 + } + + try: + # 通过应用核心调用数据收集模块 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'questionnaire', 'questionnaire_type': 'physiological', 'answers': answers}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "生理信息保存成功") + else: + messagebox.showerror("错误", "生理信息保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _on_foods_changed(self, event=None): + """食物输入改变事件""" + self._update_calories_estimate() + + def _on_quantities_changed(self, event=None): + """分量输入改变事件""" + self._update_calories_estimate() + + def _update_calories_estimate(self): + """更新热量估算""" + try: + foods_text = self.foods_text.get("1.0", "end-1c") + quantities_text = self.quantities_text.get("1.0", "end-1c") + + foods = [food.strip() for food in foods_text.split('\n') if food.strip()] + quantities = [qty.strip() for qty in quantities_text.split('\n') if qty.strip()] + + if not foods or not quantities or len(foods) != len(quantities): + self.calories_display.configure(text="系统将自动估算") + return + + # 估算热量 + from smart_food.smart_database import estimate_calories + total_calories = 0 + + for food, quantity in zip(foods, quantities): + calories = estimate_calories(food, quantity) + total_calories += calories + + self.calories_display.configure(text=f"约 {total_calories} 卡路里") + + except Exception: + self.calories_display.configure(text="系统将自动估算") + + def _save_meal_record(self): + """保存餐食记录""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + foods_text = self.foods_text.get("1.0", "end-1c") + quantities_text = self.quantities_text.get("1.0", "end-1c") + + foods = [food.strip() for food in foods_text.split('\n') if food.strip()] + quantities = [qty.strip() for qty in quantities_text.split('\n') if qty.strip()] + + if not foods: + messagebox.showwarning("警告", "请输入食物") + return + + if len(foods) != len(quantities): + messagebox.showwarning("警告", "食物和分量数量不匹配") + return + + # 自动估算热量 + try: + from smart_food.smart_database import estimate_calories + total_calories = 0 + food_items = [] + + for food, quantity in zip(foods, quantities): + calories = estimate_calories(food, quantity) + total_calories += calories + food_items.append({ + "name": food, + "portion": quantity, + "calories": calories + }) + + # 更新热量显示 + self.calories_display.configure(text=f"约 {total_calories} 卡路里") + + except Exception as e: + messagebox.showwarning("警告", f"热量估算失败: {str(e)}") + total_calories = None + food_items = [{"name": food, "portion": qty} for food, qty in zip(foods, quantities)] + + meal_data = { + 'date': self.meal_date_var.get(), + 'meal_type': self.meal_type_var.get(), + 'foods': foods, + 'quantities': quantities, + 'calories': total_calories, + 'satisfaction_score': self.satisfaction_var.get(), + 'food_items': food_items + } + + try: + # 通过应用核心调用数据收集模块 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'meal_record', 'meal_data': meal_data}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "餐食记录保存成功") + # 清空表单 + self.foods_text.delete("1.0", "end") + self.quantities_text.delete("1.0", "end") + self.calories_display.configure(text="系统将自动估算") + else: + messagebox.showerror("错误", "餐食记录保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _save_feedback(self): + """保存反馈""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + recommended_text = self.recommended_foods_text.get("1.0", "end-1c") + recommended_foods = [food.strip() for food in recommended_text.split('\n') if food.strip()] + + feedback_data = { + 'recommended_foods': recommended_foods, + 'user_choice': self.user_choice_var.get(), + 'feedback_type': self.feedback_type_var.get() + } + + try: + # 通过应用核心调用数据收集模块 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'feedback', 'feedback_data': feedback_data}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "反馈保存成功") + else: + messagebox.showerror("错误", "反馈保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _analyze_user_intent(self): + """分析用户意图""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + user_input = self.user_input_text.get("1.0", "end-1c").strip() + if not user_input: + messagebox.showwarning("警告", "请输入用户输入内容") + return + + self._update_status("正在分析用户意图...") + + def analyze_thread(): + try: + # 直接使用千问API + from llm_integration.qwen_client import analyze_user_intent_with_qwen + + # 获取用户数据 + user_data = self.app_core.get_user_data(self.current_user_id) + if not user_data: + self.root.after(0, lambda: self._update_status("用户数据不存在")) + return + + # 构建用户上下文 + 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) + if result: + self.root.after(0, lambda: self._display_intent_result(result)) + else: + self.root.after(0, lambda: self._update_status("分析失败")) + except Exception as e: + self.root.after(0, lambda: self._update_status(f"分析错误: {str(e)}")) + + threading.Thread(target=analyze_thread, daemon=True).start() + + def _display_intent_result(self, result: Dict): + """显示意图分析结果""" + self.intent_result_text.delete("1.0", "end") + + if result.get('success'): + content = f""" +用户意图: {result.get('user_intent', '未知')} +情绪状态: {result.get('emotional_state', '未知')} +营养需求: {', '.join(result.get('nutritional_needs', []))} +推荐食物: {', '.join(result.get('recommended_foods', []))} +推荐理由: {result.get('reasoning', '无')} +置信度: {result.get('confidence', 0):.2f} +""" + else: + content = f"分析失败: {result.get('error', '未知错误')}" + + self.intent_result_text.insert("1.0", content) + self._update_status("用户意图分析完成") + + def _analyze_nutrition(self): + """分析营养""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + # 获取最近的餐食数据 + user_data = self.app_core.get_user_data(self.current_user_id) + if not user_data or not user_data.meals: + messagebox.showwarning("警告", "没有餐食记录") + return + + latest_meal = user_data.meals[-1] + + self._update_status("正在分析营养...") + + def analyze_thread(): + try: + # 直接使用千问API + from llm_integration.qwen_client import analyze_nutrition_with_qwen + + # 获取用户数据 + user_data = self.app_core.get_user_data(self.current_user_id) + if not user_data: + self.root.after(0, lambda: self._update_status("用户数据不存在")) + return + + # 构建用户上下文 + 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(latest_meal, user_context) + if result: + self.root.after(0, lambda: self._display_nutrition_result(result)) + else: + self.root.after(0, lambda: self._update_status("营养分析失败")) + except Exception as e: + self.root.after(0, lambda: self._update_status(f"营养分析错误: {str(e)}")) + + threading.Thread(target=analyze_thread, daemon=True).start() + + def _display_nutrition_result(self, result: Dict): + """显示营养分析结果""" + self.nutrition_result_text.delete("1.0", "end") + + if result.get('success'): + content = f""" +营养均衡性: {result.get('nutrition_balance', '未知')} +热量评估: {result.get('calorie_assessment', '未知')} +缺少营养素: {', '.join(result.get('missing_nutrients', []))} +改进建议: {', '.join(result.get('improvements', []))} +个性化建议: {', '.join(result.get('recommendations', []))} +置信度: {result.get('confidence', 0):.2f} +""" + else: + content = f"分析失败: {result.get('error', '未知错误')}" + + self.nutrition_result_text.insert("1.0", content) + self._update_status("营养分析完成") + + def _generate_recommendations(self): + """生成推荐""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + meal_type = self.recommendation_meal_type_var.get() + preferences = {'taste': self.taste_preference_var.get()} + + self._update_status("正在生成推荐...") + + def recommend_thread(): + try: + # 通过应用核心调用推荐引擎 + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.RECOMMENDATION, + {'type': 'meal_recommendation', 'meal_type': meal_type, 'preferences': preferences}, + self.current_user_id + ) + + if result and result.result: + self.root.after(0, lambda: self._display_recommendation_result(result.result)) + else: + self.root.after(0, lambda: self._update_status("推荐生成失败")) + else: + self.root.after(0, lambda: self._update_status("应用核心未初始化")) + except Exception as e: + self.root.after(0, lambda: self._update_status(f"推荐生成错误: {str(e)}")) + + threading.Thread(target=recommend_thread, daemon=True).start() + + def _display_recommendation_result(self, result: Dict): + """显示推荐结果""" + self.recommendation_result_text.delete("1.0", "end") + + if result.get('success'): + recommendations = result.get('recommendations', []) + content = f"推荐理由: {result.get('reasoning', '无')}\n\n" + content += f"置信度: {result.get('confidence', 0):.2f}\n\n" + content += "推荐餐食搭配:\n\n" + + for i, combo in enumerate(recommendations, 1): + content += f"{i}. {combo.get('name', '搭配')}\n" + content += f" 描述: {combo.get('description', '')}\n" + content += f" 食物: {', '.join([f['name'] for f in combo.get('foods', [])])}\n" + content += f" 总热量: {combo.get('total_calories', 0):.0f}卡路里\n" + content += f" 个性化得分: {combo.get('personalization_score', 0):.2f}\n" + content += f" 营养得分: {combo.get('nutrition_score', 0):.2f}\n" + content += f" 来源: {combo.get('source', 'unknown')}\n\n" + else: + content = f"推荐失败: {result.get('error', '未知错误')}" + + self.recommendation_result_text.insert("1.0", content) + self._update_status("推荐生成完成") + + def _refresh_profile_info(self): + """刷新个人信息""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + user_data = self.app_core.get_user_data(self.current_user_id) + if user_data: + self._display_profile_info(user_data) + self._display_stats_info(user_data) + else: + messagebox.showerror("错误", "无法获取用户信息") + + def _display_profile_info(self, user_data: UserData): + """显示个人信息""" + self.profile_info_text.delete("1.0", "end") + + profile = user_data.profile + content = f""" +用户ID: {user_data.user_id} +姓名: {profile.get('name', '未设置')} +年龄: {profile.get('age', '未设置')} +性别: {profile.get('gender', '未设置')} +身高: {profile.get('height', '未设置')}cm +体重: {profile.get('weight', '未设置')}kg +活动水平: {profile.get('activity_level', '未设置')} +口味偏好: {json.dumps(profile.get('taste_preferences', {}), ensure_ascii=False)} +过敏食物: {', '.join(profile.get('allergies', []))} +不喜欢的食物: {', '.join(profile.get('dislikes', []))} +健康目标: {', '.join(profile.get('health_goals', []))} +创建时间: {user_data.created_at} +更新时间: {user_data.updated_at} +""" + + self.profile_info_text.insert("1.0", content) + + def _display_stats_info(self, user_data: UserData): + """显示统计信息""" + self.stats_text.delete("1.0", "end") + + meal_count = len(user_data.meals) + feedback_count = len(user_data.feedback) + + # 计算平均满意度 + satisfaction_scores = [meal.get('satisfaction_score', 0) for meal in user_data.meals if meal.get('satisfaction_score')] + avg_satisfaction = sum(satisfaction_scores) / len(satisfaction_scores) if satisfaction_scores else 0 + + content = f""" +数据统计: +- 餐食记录数: {meal_count} +- 反馈记录数: {feedback_count} +- 平均满意度: {avg_satisfaction:.2f} + +最近餐食: +""" + + for meal in user_data.meals[-5:]: # 显示最近5餐 + content += f"- {meal.get('date', '')} {meal.get('meal_type', '')}: {', '.join(meal.get('foods', []))}\n" + + self.stats_text.insert("1.0", content) + + def set_current_user(self, user_id: str, user_data: UserData): + """设置当前用户""" + self.current_user_id = user_id + self.current_user_data = user_data + + # 更新用户信息显示 + self.user_label.configure(text=f"用户: {user_data.profile.get('name', user_id)}") + self.login_button.configure(text="切换用户") + + # 刷新个人信息 + self._refresh_profile_info() + + def destroy(self): + """销毁窗口""" + if hasattr(self, 'root'): + self.root.destroy() + + def _show_quick_input_dialog(self): + """显示快速录入对话框""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + try: + from gui.quick_user_input import show_quick_user_input_dialog + show_quick_user_input_dialog(self.root, self.current_user_id) + except Exception as e: + messagebox.showerror("错误", f"打开快速录入失败: {str(e)}") + + def _show_smart_meal_record(self): + """显示智能餐食记录对话框""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + try: + from gui.smart_meal_record import show_smart_meal_record_dialog + show_smart_meal_record_dialog(self.root, self.current_user_id, self.meal_type_var.get()) + except Exception as e: + messagebox.showerror("错误", f"打开智能记录失败: {str(e)}") + + +class LoginDialog: + """登录对话框""" + + def __init__(self, parent, main_window): + self.main_window = main_window + + # 创建对话框 + self.dialog = ctk.CTkToplevel(parent) + self.dialog.title("用户登录/注册") + self.dialog.geometry("400x300") + 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): + """创建对话框组件""" + # 标题 + title_label = ctk.CTkLabel( + self.dialog, + text="用户登录/注册", + font=ctk.CTkFont(size=20, weight="bold") + ) + title_label.pack(pady=20) + + # 用户ID输入 + user_id_label = ctk.CTkLabel(self.dialog, text="用户ID:") + user_id_label.pack(pady=5) + + self.user_id_var = tk.StringVar() + user_id_entry = ctk.CTkEntry(self.dialog, textvariable=self.user_id_var, width=250) + user_id_entry.pack(pady=5) + + # 用户名输入 + name_label = ctk.CTkLabel(self.dialog, text="姓名:") + name_label.pack(pady=5) + + self.name_var = tk.StringVar() + name_entry = ctk.CTkEntry(self.dialog, textvariable=self.name_var, width=250) + name_entry.pack(pady=5) + + # 按钮框架 + button_frame = ctk.CTkFrame(self.dialog) + button_frame.pack(pady=20) + + # 登录按钮 + login_button = ctk.CTkButton( + button_frame, + text="登录/注册", + command=self._login, + width=100 + ) + login_button.pack(side="left", padx=10) + + # 取消按钮 + cancel_button = ctk.CTkButton( + button_frame, + text="取消", + command=self._cancel, + width=100 + ) + cancel_button.pack(side="left", padx=10) + + def _login(self): + """登录处理""" + user_id = self.user_id_var.get().strip() + name = self.name_var.get().strip() + + if not user_id: + messagebox.showwarning("警告", "请输入用户ID") + return + + if not name: + messagebox.showwarning("警告", "请输入姓名") + return + + try: + # 创建或获取用户数据 + user_data = self.main_window.app_core.get_user_data(user_id) + + if not user_data: + # 创建新用户 + initial_data = { + 'profile': { + 'name': name, + 'age': 25, + 'gender': '女', + 'height': 165, + 'weight': 55, + 'activity_level': 'moderate' + }, + 'preferences': {} + } + + if self.main_window.app_core.create_user(user_id, initial_data): + user_data = self.main_window.app_core.get_user_data(user_id) + messagebox.showinfo("成功", "新用户创建成功") + else: + messagebox.showerror("错误", "用户创建失败") + return + + # 设置当前用户 + self.main_window.set_current_user(user_id, user_data) + + # 关闭对话框 + self.dialog.destroy() + + except Exception as e: + messagebox.showerror("错误", f"登录失败: {str(e)}") + + def _cancel(self): + """取消登录""" + self.dialog.destroy() + + +if __name__ == "__main__": + # 测试GUI + root = tk.Tk() + app = MainWindow(root, None) + root.mainloop() diff --git a/gui/mobile_main_window.py b/gui/mobile_main_window.py new file mode 100644 index 0000000..6dddd5a --- /dev/null +++ b/gui/mobile_main_window.py @@ -0,0 +1,1359 @@ +""" +移动端界面设计 - 小程序/安卓App尺寸 +适配手机屏幕的饮食推荐应用界面 +""" + +import customtkinter as ctk +import tkinter as tk +from tkinter import messagebox +import json +import threading +from typing import Dict, List, Optional, Any +from core.base import AppCore, UserData, ModuleType +from gui.styles import StyleConfig, apply_rounded_theme, create_card_frame, create_accent_button, create_rounded_entry, create_rounded_label + +class MobileMainWindow: + """移动端主窗口 - 模拟小程序/安卓App界面""" + + def __init__(self, app_core=None): + # 移动端尺寸设置 + self.width = 375 # iPhone标准宽度 + self.height = 812 # iPhone标准高度 + + # 创建主窗口 + self.root = ctk.CTk() + self.root.title("饮食推荐助手") + self.root.geometry(f"{self.width}x{self.height}") + self.root.resizable(False, False) + + # 应用核心 + self.app_core = app_core + self.current_user_id = None + self.current_user_data = None + + # 当前页面 + self.current_page = "home" + + # 应用圆角主题 + apply_rounded_theme() + + # 创建界面 + self._create_mobile_ui() + + # 初始化应用 + self._initialize_app() + + def _create_mobile_ui(self): + """创建移动端界面""" + # 主容器 - 增加圆角和内边距 + self.main_container = ctk.CTkFrame( + self.root, + corner_radius=20, + fg_color=("#f8f9fa", "#1e1e1e") + ) + self.main_container.pack(fill="both", expand=True, padx=5, pady=5) + + # 状态栏(模拟手机状态栏) + self._create_status_bar() + + # 页面容器 - 增加圆角和阴影效果 + self.page_container = ctk.CTkFrame( + self.main_container, + corner_radius=25, + fg_color=("#ffffff", "#2b2b2b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + self.page_container.pack(fill="both", expand=True, padx=15, pady=15) + + # 底部导航栏 + self._create_bottom_navigation() + + # 创建各个页面 + self._create_home_page() + self._create_record_page() + self._create_recommend_page() + self._create_profile_page() + + # 默认显示首页 + self._show_page("home") + + def _create_status_bar(self): + """创建状态栏""" + status_frame = ctk.CTkFrame( + self.main_container, + height=35, + corner_radius=15, + fg_color=("transparent", "transparent") + ) + status_frame.pack(fill="x", padx=10, pady=(5, 0)) + status_frame.pack_propagate(False) + + # 时间显示 + self.time_label = ctk.CTkLabel( + status_frame, + text="12:34", + font=("Arial", 13, "bold"), + text_color=("#333333", "#ffffff") + ) + self.time_label.pack(side="left", padx=15, pady=8) + + # 信号和电池图标(模拟) + signal_label = ctk.CTkLabel( + status_frame, + text="📶", + font=("Arial", 12), + text_color=("#333333", "#ffffff") + ) + signal_label.pack(side="right", padx=8, pady=8) + + battery_label = ctk.CTkLabel( + status_frame, + text="🔋", + font=("Arial", 12), + text_color=("#333333", "#ffffff") + ) + battery_label.pack(side="right", padx=8, pady=8) + + def _create_bottom_navigation(self): + """创建底部导航栏""" + nav_frame = ctk.CTkFrame( + self.main_container, + height=70, + corner_radius=20, + fg_color=("#ffffff", "#2b2b2b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + nav_frame.pack(fill="x", side="bottom", padx=10, pady=(0, 10)) + nav_frame.pack_propagate(False) + + # 导航按钮 + nav_buttons = [ + ("🏠", "home", "首页"), + ("📝", "record", "记录"), + ("🎯", "recommend", "推荐"), + ("👤", "profile", "我的") + ] + + for icon, page, text in nav_buttons: + btn = ctk.CTkButton( + nav_frame, + text=f"{icon}\n{text}", + font=("Arial", 10), + height=55, + width=75, + corner_radius=15, + fg_color=("transparent", "transparent"), + hover_color=("#f0f0f0", "#404040"), + command=lambda p=page: self._show_page(p) + ) + btn.pack(side="left", padx=8, pady=8, expand=True, fill="x") + + def _create_home_page(self): + """创建首页""" + self.home_frame = ctk.CTkFrame( + self.page_container, + corner_radius=20, + fg_color=("transparent", "transparent") + ) + + # 欢迎区域 + welcome_frame = ctk.CTkFrame( + self.home_frame, + corner_radius=25, + fg_color=("#f8f9fa", "#2b2b2b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + welcome_frame.pack(fill="x", padx=20, pady=(20, 15)) + + welcome_label = ctk.CTkLabel( + welcome_frame, + text="🍽️ 饮食推荐助手", + font=("Arial", 22, "bold"), + text_color=("#2c3e50", "#ecf0f1") + ) + welcome_label.pack(pady=15) + + # 用户信息卡片 + self.user_card = ctk.CTkFrame( + self.home_frame, + corner_radius=20, + fg_color=("#ffffff", "#3b3b3b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + self.user_card.pack(fill="x", padx=20, pady=10) + + self.user_info_label = ctk.CTkLabel( + self.user_card, + text="请先登录", + font=("Arial", 16), + text_color=("#34495e", "#bdc3c7") + ) + self.user_info_label.pack(pady=15) + + # 快速操作按钮 + quick_actions_frame = ctk.CTkFrame( + self.home_frame, + corner_radius=20, + fg_color=("#f8f9fa", "#2b2b2b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + quick_actions_frame.pack(fill="x", padx=20, pady=15) + + # 记录餐食按钮 + record_btn = ctk.CTkButton( + quick_actions_frame, + text="📝 记录餐食", + font=("Arial", 15, "bold"), + height=55, + corner_radius=15, + fg_color=("#3498db", "#2980b9"), + hover_color=("#2980b9", "#1f618d"), + text_color=("#ffffff", "#ffffff"), + command=self._quick_record_meal + ) + record_btn.pack(fill="x", padx=15, pady=(15, 8)) + + # 获取推荐按钮 + recommend_btn = ctk.CTkButton( + quick_actions_frame, + text="🎯 获取推荐", + font=("Arial", 15, "bold"), + height=55, + corner_radius=15, + fg_color=("#e74c3c", "#c0392b"), + hover_color=("#c0392b", "#a93226"), + text_color=("#ffffff", "#ffffff"), + command=self._quick_get_recommendation + ) + recommend_btn.pack(fill="x", padx=15, pady=(8, 15)) + + # 今日统计 + stats_frame = ctk.CTkFrame( + self.home_frame, + corner_radius=20, + fg_color=("#ffffff", "#3b3b3b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + stats_frame.pack(fill="x", padx=20, pady=15) + + stats_label = ctk.CTkLabel( + stats_frame, + text="📊 今日统计", + font=("Arial", 16, "bold") + ) + stats_label.pack(pady=5) + + self.stats_text = ctk.CTkTextbox(stats_frame, height=100) + self.stats_text.pack(fill="x", padx=10, pady=5) + + def _create_record_page(self): + """创建记录页面""" + self.record_frame = ctk.CTkFrame( + self.page_container, + corner_radius=20, + fg_color=("transparent", "transparent") + ) + + # 页面标题 + title_label = ctk.CTkLabel( + self.record_frame, + text="📝 记录餐食", + font=("Arial", 20, "bold"), + text_color=("#2c3e50", "#ecf0f1") + ) + title_label.pack(pady=(20, 15)) + + # 餐次选择 + meal_type_frame = ctk.CTkFrame( + self.record_frame, + corner_radius=15, + fg_color=("#ffffff", "#3b3b3b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + meal_type_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel( + meal_type_frame, + text="餐次:", + font=("Arial", 15), + text_color=("#34495e", "#bdc3c7") + ).pack(side="left", padx=15, pady=12) + + self.meal_type_var = ctk.StringVar(value="breakfast") + meal_type_menu = ctk.CTkOptionMenu( + meal_type_frame, + variable=self.meal_type_var, + values=["breakfast", "lunch", "dinner", "snack"], + width=130, + corner_radius=10, + fg_color=("#3498db", "#2980b9"), + button_color=("#2980b9", "#1f618d"), + button_hover_color=("#1f618d", "#154360") + ) + meal_type_menu.pack(side="right", padx=15, pady=8) + + # 食物输入 + food_frame = ctk.CTkFrame( + self.record_frame, + corner_radius=15, + fg_color=("#ffffff", "#3b3b3b"), + border_width=1, + border_color=("#e0e0e0", "#404040") + ) + food_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel( + food_frame, + text="食物:", + font=("Arial", 15), + text_color=("#34495e", "#bdc3c7") + ).pack(anchor="w", padx=15, pady=(12, 5)) + + # 食物输入框和转盘按钮 + food_input_frame = ctk.CTkFrame( + food_frame, + corner_radius=10, + fg_color=("transparent", "transparent") + ) + food_input_frame.pack(fill="x", padx=15, pady=(5, 12)) + + self.food_entry = ctk.CTkEntry( + food_input_frame, + placeholder_text="输入食物名称", + corner_radius=12, + height=40, + font=("Arial", 14), + fg_color=("#f8f9fa", "#404040"), + border_width=1, + border_color=("#e0e0e0", "#555555") + ) + self.food_entry.pack(side="left", fill="x", expand=True, padx=(0, 8)) + + # 转盘按钮和OCR按钮 + button_frame = ctk.CTkFrame( + food_input_frame, + corner_radius=10, + fg_color=("transparent", "transparent") + ) + button_frame.pack(side="right") + + roulette_btn = ctk.CTkButton( + button_frame, + text="🎲", + width=40, + height=40, + corner_radius=12, + fg_color=("#f39c12", "#e67e22"), + hover_color=("#e67e22", "#d35400"), + command=self._show_food_roulette + ) + roulette_btn.pack(side="left", padx=(0, 4)) + + ocr_btn = ctk.CTkButton( + button_frame, + text="📷", + width=40, + height=40, + corner_radius=12, + fg_color=("#9b59b6", "#8e44ad"), + hover_color=("#8e44ad", "#7d3c98"), + command=self._show_ocr_recognition + ) + ocr_btn.pack(side="right", padx=(4, 0)) + + # 分量输入 + quantity_frame = ctk.CTkFrame(self.record_frame) + quantity_frame.pack(fill="x", padx=15, pady=10) + + ctk.CTkLabel(quantity_frame, text="分量:", font=("Arial", 14)).pack(anchor="w", padx=10, pady=5) + self.quantity_entry = ctk.CTkEntry(quantity_frame, placeholder_text="如:1碗、200g") + self.quantity_entry.pack(fill="x", padx=10, pady=5) + + # 热量显示 + calorie_frame = ctk.CTkFrame(self.record_frame) + calorie_frame.pack(fill="x", padx=15, pady=10) + + ctk.CTkLabel(calorie_frame, text="热量:", font=("Arial", 14)).pack(anchor="w", padx=10, pady=5) + self.calorie_display = ctk.CTkLabel(calorie_frame, text="0 卡路里", font=("Arial", 16, "bold")) + self.calorie_display.pack(anchor="w", padx=10, pady=5) + + # 满意度评分 + satisfaction_frame = ctk.CTkFrame(self.record_frame) + satisfaction_frame.pack(fill="x", padx=15, pady=10) + + ctk.CTkLabel(satisfaction_frame, text="满意度:", font=("Arial", 14)).pack(anchor="w", padx=10, pady=5) + + self.satisfaction_var = ctk.IntVar(value=4) + satisfaction_slider = ctk.CTkSlider( + satisfaction_frame, + from_=1, + to=5, + number_of_steps=4, + variable=self.satisfaction_var + ) + satisfaction_slider.pack(fill="x", padx=10, pady=5) + + satisfaction_label = ctk.CTkLabel(satisfaction_frame, text="4分") + satisfaction_label.pack() + + # 保存按钮 + save_btn = ctk.CTkButton( + self.record_frame, + text="💾 保存记录", + font=("Arial", 14, "bold"), + height=50, + command=self._save_meal_record + ) + save_btn.pack(fill="x", padx=15, pady=15) + + # 绑定食物输入变化事件 + self.food_entry.bind("", self._on_food_input_change) + self.quantity_entry.bind("", self._on_food_input_change) + + def _show_ocr_recognition(self): + """显示OCR识别界面""" + try: + # 创建OCR识别窗口 + ocr_window = ctk.CTkToplevel(self.root) + ocr_window.title("📷 OCR热量识别") + ocr_window.geometry("400x500") + ocr_window.resizable(False, False) + + # 居中显示 + ocr_window.transient(self.root) + ocr_window.grab_set() + + # 创建OCR界面 + from gui.ocr_calorie_gui import OCRCalorieGUI + ocr_gui = OCRCalorieGUI(ocr_window, self.app_core) + + except Exception as e: + messagebox.showerror("错误", f"打开OCR识别界面失败: {str(e)}") + + def _create_recommend_page(self): + """创建推荐页面""" + self.recommend_frame = ctk.CTkFrame(self.page_container) + + # 页面标题 + title_label = ctk.CTkLabel( + self.recommend_frame, + text="🎯 个性化推荐", + font=("Arial", 18, "bold") + ) + title_label.pack(pady=15) + + # 推荐设置 + settings_frame = ctk.CTkFrame(self.recommend_frame) + settings_frame.pack(fill="x", padx=15, pady=10) + + # 餐次选择 + meal_type_row = ctk.CTkFrame(settings_frame) + meal_type_row.pack(fill="x", padx=10, pady=5) + + ctk.CTkLabel(meal_type_row, text="餐次:", font=("Arial", 14)).pack(side="left") + + self.rec_meal_type_var = ctk.StringVar(value="lunch") + rec_meal_type_menu = ctk.CTkOptionMenu( + meal_type_row, + variable=self.rec_meal_type_var, + values=["breakfast", "lunch", "dinner", "snack"], + width=120 + ) + rec_meal_type_menu.pack(side="right") + + # 口味偏好 + taste_row = ctk.CTkFrame(settings_frame) + taste_row.pack(fill="x", padx=10, pady=5) + + ctk.CTkLabel(taste_row, text="口味:", font=("Arial", 14)).pack(side="left") + + self.taste_var = ctk.StringVar(value="balanced") + taste_menu = ctk.CTkOptionMenu( + taste_row, + variable=self.taste_var, + values=["balanced", "sweet", "salty", "spicy", "sour"], + width=120 + ) + taste_menu.pack(side="right") + + # 生成推荐按钮 + generate_btn = ctk.CTkButton( + self.recommend_frame, + text="🎲 生成推荐", + font=("Arial", 14, "bold"), + height=50, + command=self._generate_recommendations + ) + generate_btn.pack(fill="x", padx=15, pady=15) + + # 推荐结果 + result_frame = ctk.CTkFrame(self.recommend_frame) + result_frame.pack(fill="both", expand=True, padx=15, pady=10) + + self.recommendation_text = ctk.CTkTextbox(result_frame, height=300) + self.recommendation_text.pack(fill="both", expand=True, padx=10, pady=10) + + def _create_profile_page(self): + """创建个人中心页面""" + self.profile_frame = ctk.CTkFrame(self.page_container) + + # 页面标题 + title_label = ctk.CTkLabel( + self.profile_frame, + text="👤 个人中心", + font=("Arial", 18, "bold") + ) + title_label.pack(pady=15) + + # 用户信息卡片 + user_info_frame = ctk.CTkFrame(self.profile_frame) + user_info_frame.pack(fill="x", padx=15, pady=10) + + self.profile_user_label = ctk.CTkLabel( + user_info_frame, + text="请先登录", + font=("Arial", 16, "bold") + ) + self.profile_user_label.pack(pady=10) + + # 登录/注册按钮 + login_btn = ctk.CTkButton( + self.profile_frame, + text="🔑 登录/注册", + font=("Arial", 14, "bold"), + height=50, + command=self._show_login_dialog + ) + login_btn.pack(fill="x", padx=15, pady=10) + + # 功能菜单 + menu_frame = ctk.CTkFrame(self.profile_frame) + menu_frame.pack(fill="x", padx=15, pady=10) + + menu_items = [ + ("📊 数据统计", self._show_data_stats), + ("⚙️ 设置", self._show_settings), + ("❓ 帮助", self._show_help), + ("📞 联系我们", self._show_contact) + ] + + for text, command in menu_items: + btn = ctk.CTkButton( + menu_frame, + text=text, + font=("Arial", 14), + height=40, + command=command + ) + btn.pack(fill="x", pady=2) + + def _show_page(self, page_name: str): + """显示指定页面""" + # 隐藏所有页面 + for frame in [self.home_frame, self.record_frame, self.recommend_frame, self.profile_frame]: + frame.pack_forget() + + # 显示指定页面 + if page_name == "home": + self.home_frame.pack(fill="both", expand=True) + elif page_name == "record": + self.record_frame.pack(fill="both", expand=True) + elif page_name == "recommend": + self.recommend_frame.pack(fill="both", expand=True) + elif page_name == "profile": + self.profile_frame.pack(fill="both", expand=True) + + self.current_page = page_name + + def _initialize_app(self): + """初始化应用""" + if self.app_core: + self._update_status("应用核心已就绪") + else: + self._update_status("应用核心未初始化") + + def _update_status(self, message: str): + """更新状态信息""" + print(f"状态: {message}") + + def _quick_record_meal(self): + """快速记录餐食""" + self._show_page("record") + + def _quick_get_recommendation(self): + """快速获取推荐""" + self._show_page("recommend") + + def _save_meal_record(self): + """保存餐食记录""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + food = self.food_entry.get().strip() + quantity = self.quantity_entry.get().strip() + meal_type = self.meal_type_var.get() + satisfaction = self.satisfaction_var.get() + + if not food or not quantity: + messagebox.showwarning("警告", "请填写完整信息") + return + + try: + meal_data = { + 'meal_type': meal_type, + 'foods': [food], + 'quantities': [quantity], + 'satisfaction_score': satisfaction + } + + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'meal_record', 'meal_data': meal_data}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "餐食记录保存成功") + self.food_entry.delete(0, "end") + self.quantity_entry.delete(0, "end") + + # 同步更新用户数据 + self._refresh_user_data() + + # 如果当前在首页,更新统计信息 + if self.current_page == "home": + self._update_stats() + + else: + messagebox.showerror("错误", "餐食记录保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _generate_recommendations(self): + """生成推荐""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + self.recommendation_text.delete("1.0", "end") + self.recommendation_text.insert("1.0", "正在生成推荐...") + + def recommend_thread(): + try: + meal_type = self.rec_meal_type_var.get() + preferences = {'taste': self.taste_var.get()} + + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.RECOMMENDATION, + { + 'type': 'meal_recommendation', + 'meal_type': meal_type, + 'preferences': preferences, + 'context': {} + }, + self.current_user_id + ) + + if result and result.result.get('success'): + recommendations = result.result.get('recommendations', []) + reasoning = result.result.get('reasoning', '无') + confidence = result.result.get('confidence', 0) + + content = f"推荐理由: {reasoning}\n\n" + content += f"置信度: {confidence:.2f}\n\n" + content += "推荐餐食搭配:\n\n" + + for i, combo in enumerate(recommendations, 1): + content += f"{i}. {combo.get('name', '搭配')}\n" + foods = [f['name'] for f in combo.get('foods', [])] + content += f" 食物: {', '.join(foods)}\n" + content += f" 热量: {combo.get('total_calories', 0):.0f}卡路里\n\n" + + self.root.after(0, lambda: self.recommendation_text.delete("1.0", "end")) + self.root.after(0, lambda: self.recommendation_text.insert("1.0", content)) + else: + error_msg = result.result.get('error', '未知错误') if result else '无结果' + self.root.after(0, lambda: self.recommendation_text.delete("1.0", "end")) + self.root.after(0, lambda: self.recommendation_text.insert("1.0", f"推荐失败: {error_msg}")) + else: + self.root.after(0, lambda: self.recommendation_text.delete("1.0", "end")) + self.root.after(0, lambda: self.recommendation_text.insert("1.0", "应用核心未初始化")) + + except Exception as e: + self.root.after(0, lambda: self.recommendation_text.delete("1.0", "end")) + self.root.after(0, lambda: self.recommendation_text.insert("1.0", f"推荐生成错误: {str(e)}")) + + threading.Thread(target=recommend_thread, daemon=True).start() + + def _show_login_dialog(self): + """显示登录对话框""" + dialog = MobileLoginDialog(self.root, self) + dialog.show() + + def _show_data_stats(self): + """显示数据统计""" + if not self.current_user_data: + messagebox.showwarning("警告", "请先登录") + return + + # 创建统计窗口 + stats_window = ctk.CTkToplevel(self.root) + stats_window.title("数据统计") + stats_window.geometry("350x500") + stats_window.resizable(False, False) + + # 主容器 + main_frame = ctk.CTkScrollableFrame(stats_window, width=320, height=450) + main_frame.pack(padx=10, pady=10, fill="both", expand=True) + + # 标题 + title_label = ctk.CTkLabel(main_frame, text="📊 数据统计", font=ctk.CTkFont(size=20, weight="bold")) + title_label.pack(pady=(0, 20)) + + # 基础统计 + basic_frame = ctk.CTkFrame(main_frame) + basic_frame.pack(fill="x", pady=(0, 10)) + + basic_title = ctk.CTkLabel(basic_frame, text="基础统计", font=ctk.CTkFont(size=16, weight="bold")) + basic_title.pack(pady=10) + + # 餐食记录统计 + meal_count = len(self.current_user_data.meals) + meal_label = ctk.CTkLabel(basic_frame, text=f"餐食记录: {meal_count}条") + meal_label.pack(pady=2) + + # 反馈记录统计 + feedback_count = len(self.current_user_data.feedback) + feedback_label = ctk.CTkLabel(basic_frame, text=f"反馈记录: {feedback_count}条") + feedback_label.pack(pady=2) + + # 满意度统计 + if self.current_user_data.meals: + satisfaction_scores = [meal.get('satisfaction_score', 0) for meal in self.current_user_data.meals if meal.get('satisfaction_score')] + if satisfaction_scores: + avg_satisfaction = sum(satisfaction_scores) / len(satisfaction_scores) + satisfaction_label = ctk.CTkLabel(basic_frame, text=f"平均满意度: {avg_satisfaction:.1f}分") + satisfaction_label.pack(pady=2) + + # 餐次分布统计 + meal_dist_frame = ctk.CTkFrame(main_frame) + meal_dist_frame.pack(fill="x", pady=(0, 10)) + + meal_dist_title = ctk.CTkLabel(meal_dist_frame, text="餐次分布", font=ctk.CTkFont(size=16, weight="bold")) + meal_dist_title.pack(pady=10) + + meal_types = {} + for meal in self.current_user_data.meals: + meal_type = meal.get('meal_type', 'unknown') + meal_types[meal_type] = meal_types.get(meal_type, 0) + 1 + + for meal_type, count in meal_types.items(): + type_label = ctk.CTkLabel(meal_dist_frame, text=f"{meal_type}: {count}次") + type_label.pack(pady=2) + + # 最近餐食 + recent_frame = ctk.CTkFrame(main_frame) + recent_frame.pack(fill="x", pady=(0, 10)) + + recent_title = ctk.CTkLabel(recent_frame, text="最近餐食", font=ctk.CTkFont(size=16, weight="bold")) + recent_title.pack(pady=10) + + recent_meals = sorted(self.current_user_data.meals, key=lambda x: x.get('date', ''), reverse=True)[:5] + for meal in recent_meals: + meal_text = f"{meal.get('date', '未知日期')} - {meal.get('meal_type', '未知餐次')}" + if meal.get('foods'): + meal_text += f" ({', '.join(meal['foods'])})" + meal_label = ctk.CTkLabel(recent_frame, text=meal_text, wraplength=300) + meal_label.pack(pady=2) + + # 关闭按钮 + close_btn = ctk.CTkButton(main_frame, text="关闭", command=stats_window.destroy) + close_btn.pack(pady=20) + + def _show_settings(self): + """显示设置""" + if not self.current_user_data: + messagebox.showwarning("警告", "请先登录") + return + + # 创建设置窗口 + settings_window = ctk.CTkToplevel(self.root) + settings_window.title("设置") + settings_window.geometry("350x400") + settings_window.resizable(False, False) + + # 主容器 + main_frame = ctk.CTkScrollableFrame(settings_window, width=320, height=350) + main_frame.pack(padx=10, pady=10, fill="both", expand=True) + + # 标题 + title_label = ctk.CTkLabel(main_frame, text="⚙️ 设置", font=ctk.CTkFont(size=20, weight="bold")) + title_label.pack(pady=(0, 20)) + + # 用户偏好设置 + pref_frame = ctk.CTkFrame(main_frame) + pref_frame.pack(fill="x", pady=(0, 10)) + + pref_title = ctk.CTkLabel(pref_frame, text="用户偏好", font=ctk.CTkFont(size=16, weight="bold")) + pref_title.pack(pady=10) + + # 口味偏好 + taste_label = ctk.CTkLabel(pref_frame, text="口味偏好:") + taste_label.pack(pady=(10, 5)) + + taste_var = ctk.StringVar(value="balanced") + taste_options = ["清淡", "适中", "重口味", "甜食", "咸食", "辣食"] + taste_menu = ctk.CTkOptionMenu(pref_frame, variable=taste_var, values=taste_options) + taste_menu.pack(pady=5) + + # 饮食目标 + goal_label = ctk.CTkLabel(pref_frame, text="饮食目标:") + goal_label.pack(pady=(10, 5)) + + goal_var = ctk.StringVar(value="maintain") + goal_options = ["维持体重", "减重", "增重", "增肌", "健康饮食"] + goal_menu = ctk.CTkOptionMenu(pref_frame, variable=goal_var, values=goal_options) + goal_menu.pack(pady=5) + + # 过敏食物 + allergy_label = ctk.CTkLabel(pref_frame, text="过敏食物:") + allergy_label.pack(pady=(10, 5)) + + allergy_entry = ctk.CTkEntry(pref_frame, placeholder_text="请输入过敏食物,用逗号分隔") + allergy_entry.pack(pady=5, fill="x") + + # 保存设置按钮 + def save_settings(): + try: + preferences = { + 'taste_preference': taste_var.get(), + 'diet_goal': goal_var.get(), + 'allergies': allergy_entry.get().strip() + } + + # 保存到用户数据 + if self.app_core and self.app_core.data_manager: + # 更新用户偏好 + self.current_user_data.preferences.update(preferences) + + # 保存到数据库 + self.app_core.data_manager.save_user_data(self.current_user_data) + + messagebox.showinfo("成功", "设置保存成功") + settings_window.destroy() + else: + messagebox.showerror("错误", "应用核心未初始化") + + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + save_btn = ctk.CTkButton(pref_frame, text="保存设置", command=save_settings) + save_btn.pack(pady=20) + + # 关闭按钮 + close_btn = ctk.CTkButton(main_frame, text="关闭", command=settings_window.destroy) + close_btn.pack(pady=10) + + def _show_help(self): + """显示帮助""" + # 创建帮助窗口 + help_window = ctk.CTkToplevel(self.root) + help_window.title("帮助") + help_window.geometry("350x500") + help_window.resizable(False, False) + + # 主容器 + main_frame = ctk.CTkScrollableFrame(help_window, width=320, height=450) + main_frame.pack(padx=10, pady=10, fill="both", expand=True) + + # 标题 + title_label = ctk.CTkLabel(main_frame, text="❓ 使用帮助", font=ctk.CTkFont(size=20, weight="bold")) + title_label.pack(pady=(0, 20)) + + # 功能介绍 + features_frame = ctk.CTkFrame(main_frame) + features_frame.pack(fill="x", pady=(0, 10)) + + features_title = ctk.CTkLabel(features_frame, text="功能介绍", font=ctk.CTkFont(size=16, weight="bold")) + features_title.pack(pady=10) + + features_text = """ +🏠 首页 +• 查看今日统计信息 +• 快速记录餐食 +• 获取个性化推荐 + +📝 记录 +• 记录餐食信息 +• 设置满意度评分 +• 自动计算热量 + +🎯 推荐 +• 个性化餐食推荐 +• 基于历史数据 +• 营养搭配建议 + +👤 个人中心 +• 查看详细统计 +• 设置个人偏好 +• 管理账户信息 + """ + + features_label = ctk.CTkLabel(features_frame, text=features_text, justify="left") + features_label.pack(pady=10) + + # 使用说明 + usage_frame = ctk.CTkFrame(main_frame) + usage_frame.pack(fill="x", pady=(0, 10)) + + usage_title = ctk.CTkLabel(usage_frame, text="使用说明", font=ctk.CTkFont(size=16, weight="bold")) + usage_title.pack(pady=10) + + usage_text = """ +1. 首次使用请先登录 +2. 在记录页面输入餐食信息 +3. 在推荐页面获取建议 +4. 定期查看统计了解饮食情况 +5. 在设置中调整个人偏好 + """ + + usage_label = ctk.CTkLabel(usage_frame, text=usage_text, justify="left") + usage_label.pack(pady=10) + + # 关闭按钮 + close_btn = ctk.CTkButton(main_frame, text="关闭", command=help_window.destroy) + close_btn.pack(pady=20) + + def _show_contact(self): + """显示联系我们""" + # 创建联系窗口 + contact_window = ctk.CTkToplevel(self.root) + contact_window.title("联系我们") + contact_window.geometry("350x300") + contact_window.resizable(False, False) + + # 主容器 + main_frame = ctk.CTkFrame(contact_window) + main_frame.pack(padx=20, pady=20, fill="both", expand=True) + + # 标题 + title_label = ctk.CTkLabel(main_frame, text="📞 联系我们", font=ctk.CTkFont(size=20, weight="bold")) + title_label.pack(pady=(0, 30)) + + # 联系方式 + contact_info = """ +📧 邮箱支持 +support@dietapp.com + +📱 客服电话 +400-123-4567 +工作时间:9:00-18:00 + +💬 在线客服 +微信:DietApp_Support + +🌐 官方网站 +www.dietapp.com + +📝 意见反馈 +feedback@dietapp.com + """ + + contact_label = ctk.CTkLabel(main_frame, text=contact_info, justify="left") + contact_label.pack(pady=20) + + # 关闭按钮 + close_btn = ctk.CTkButton(main_frame, text="关闭", command=contact_window.destroy) + close_btn.pack(pady=20) + + def set_current_user(self, user_id: str, user_data: UserData): + """设置当前用户""" + self.current_user_id = user_id + self.current_user_data = user_data + + # 更新用户信息显示 + profile = user_data.profile + user_info = f"👤 {profile.get('name', '未知用户')}\n" + user_info += f"📊 餐食记录: {len(user_data.meals)}条\n" + user_info += f"💬 反馈记录: {len(user_data.feedback)}条" + + self.user_info_label.configure(text=user_info) + self.profile_user_label.configure(text=f"欢迎,{profile.get('name', '用户')}!") + + # 更新统计信息 + self._update_stats() + + def _refresh_user_data(self): + """刷新用户数据""" + if self.current_user_id and self.app_core: + try: + self.current_user_data = self.app_core.data_manager.get_user_data(self.current_user_id) + self._update_status("用户数据已刷新") + except Exception as e: + self._update_status(f"数据刷新失败: {e}") + + def _on_food_input_change(self, event=None): + """食物输入变化时更新热量""" + food = self.food_entry.get().strip() + quantity = self.quantity_entry.get().strip() + + if food and quantity and self.app_core: + try: + # 使用AI分析获取热量 + result = self.app_core.process_user_request( + ModuleType.USER_ANALYSIS, + {'type': 'calorie_estimation', 'food_data': {'food_name': food, 'quantity': quantity}}, + self.current_user_id or "test" + ) + + if result and result.result.get('success'): + calories = result.result.get('calories', 0) + self.calorie_display.configure(text=f"{calories:.0f} 卡路里") + else: + self.calorie_display.configure(text="热量计算中...") + + except Exception as e: + self.calorie_display.configure(text="热量计算失败") + + def _save_meal_record(self): + """保存餐食记录""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + food = self.food_entry.get().strip() + quantity = self.quantity_entry.get().strip() + meal_type = self.meal_type_var.get() + satisfaction = self.satisfaction_var.get() + + if not food or not quantity: + messagebox.showwarning("警告", "请填写完整信息") + return + + try: + meal_data = { + 'meal_type': meal_type, + 'foods': [food], + 'quantities': [quantity], + 'satisfaction_score': satisfaction + } + + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.DATA_COLLECTION, + {'type': 'meal_record', 'meal_data': meal_data}, + self.current_user_id + ) + + if result and result.result.get('success'): + messagebox.showinfo("成功", "餐食记录保存成功") + self.food_entry.delete(0, "end") + self.quantity_entry.delete(0, "end") + + # 同步更新用户数据 + self._refresh_user_data() + + # 如果当前在首页,更新统计信息 + if self.current_page == "home": + self._update_stats() + + else: + messagebox.showerror("错误", "餐食记录保存失败") + else: + messagebox.showerror("错误", "应用核心未初始化") + + except Exception as e: + messagebox.showerror("错误", f"保存失败: {str(e)}") + + def _show_food_roulette(self): + """显示食物转盘""" + # 创建转盘窗口 + roulette_window = ctk.CTkToplevel(self.root) + roulette_window.title("食物转盘") + roulette_window.geometry("300x400") + roulette_window.resizable(False, False) + + # 主容器 + main_frame = ctk.CTkFrame(roulette_window) + main_frame.pack(padx=20, pady=20, fill="both", expand=True) + + # 标题 + title_label = ctk.CTkLabel(main_frame, text="🎲 食物转盘", font=ctk.CTkFont(size=20, weight="bold")) + title_label.pack(pady=(0, 20)) + + # 转盘显示区域 + roulette_frame = ctk.CTkFrame(main_frame) + roulette_frame.pack(fill="x", pady=20) + + self.roulette_display = ctk.CTkLabel(roulette_frame, text="点击开始转盘", font=ctk.CTkFont(size=16)) + self.roulette_display.pack(pady=20) + + # 食物列表 + food_list = [ + "米饭", "面条", "包子", "饺子", "馒头", "面包", + "鸡蛋", "牛奶", "豆浆", "酸奶", "苹果", "香蕉", + "鸡肉", "牛肉", "猪肉", "鱼肉", "豆腐", "青菜", + "西红柿", "黄瓜", "胡萝卜", "土豆", "红薯", "玉米" + ] + + # 转盘按钮 + def spin_roulette(): + import random + import time + + self.roulette_display.configure(text="转盘中...") + roulette_window.update() + + # 模拟转盘效果 + for _ in range(10): + random_food = random.choice(food_list) + self.roulette_display.configure(text=f"🎯 {random_food}") + roulette_window.update() + time.sleep(0.1) + + # 最终结果 + final_food = random.choice(food_list) + self.roulette_display.configure(text=f"🎉 {final_food}") + + # 自动填入食物输入框 + self.food_entry.delete(0, "end") + self.food_entry.insert(0, final_food) + + # 触发热量计算 + self._on_food_input_change() + + spin_btn = ctk.CTkButton(main_frame, text="🎲 开始转盘", command=spin_roulette, height=40) + spin_btn.pack(pady=20) + + # 关闭按钮 + close_btn = ctk.CTkButton(main_frame, text="关闭", command=roulette_window.destroy) + close_btn.pack(pady=10) + + def _update_stats(self): + """更新统计信息""" + if not self.current_user_data: + return + + stats = f"📊 今日统计\n\n" + stats += f"餐食记录: {len(self.current_user_data.meals)}条\n" + stats += f"反馈记录: {len(self.current_user_data.feedback)}条\n" + + # 计算平均满意度 + if self.current_user_data.meals: + satisfaction_scores = [meal.get('satisfaction_score', 0) for meal in self.current_user_data.meals if meal.get('satisfaction_score')] + if satisfaction_scores: + avg_satisfaction = sum(satisfaction_scores) / len(satisfaction_scores) + stats += f"平均满意度: {avg_satisfaction:.1f}分\n" + + self.stats_text.delete("1.0", "end") + self.stats_text.insert("1.0", stats) + + def _generate_recommendations(self): + """生成推荐""" + if not self.current_user_id: + messagebox.showwarning("警告", "请先登录") + return + + try: + input_data = { + 'meal_type': self.rec_meal_type_var.get(), + 'preferences': { + 'taste': self.taste_var.get(), + 'health_goal': 'maintain' + }, + 'context': { + 'time': '12:00', + 'weather': 'sunny' + } + } + + if self.app_core and self.app_core.module_manager: + result = self.app_core.process_user_request( + ModuleType.RECOMMENDATION, + {'type': 'meal_recommendation', 'input_data': input_data}, + self.current_user_id + ) + + if result and result.result.get('success'): + recommendations = result.result.get('recommendations', []) + reasoning = result.result.get('reasoning', '无') + confidence = result.result.get('confidence', 0) + + # 显示推荐结果 + self.recommendation_text.delete("1.0", "end") + content = f"推荐理由: {reasoning}\n\n" + content += f"置信度: {confidence:.2f}\n\n" + content += "推荐餐食搭配:\n\n" + + for i, combo in enumerate(recommendations[:3], 1): + content += f"{i}. {combo.get('name', '搭配')}\n" + content += f" 食物: {', '.join([f['name'] for f in combo.get('foods', [])])}\n" + content += f" 总热量: {combo.get('total_calories', 0):.0f}卡路里\n" + content += f" 营养得分: {combo.get('nutrition_score', 0):.2f}\n\n" + + self.recommendation_text.insert("1.0", content) + else: + self.recommendation_text.delete("1.0", "end") + self.recommendation_text.insert("1.0", f"推荐失败: {result.result.get('error', '未知错误') if result else '无结果'}") + else: + messagebox.showerror("错误", "应用核心未初始化") + + except Exception as e: + messagebox.showerror("错误", f"推荐生成失败: {str(e)}") + + def _quick_record_meal(self): + """快速记录餐食""" + self._show_page("record") + + def _quick_get_recommendation(self): + """快速获取推荐""" + self._show_page("recommend") + + def run(self): + """运行应用""" + self.root.mainloop() + + +class MobileLoginDialog: + """移动端登录对话框""" + + def __init__(self, parent, main_window): + self.parent = parent + self.main_window = main_window + + # 创建对话框 + self.dialog = ctk.CTkToplevel(parent) + self.dialog.title("登录") + self.dialog.geometry("300x400") + self.dialog.resizable(False, False) + + # 居中显示 + self.dialog.transient(parent) + self.dialog.grab_set() + + self._create_login_ui() + + def _create_login_ui(self): + """创建登录界面""" + # 标题 + title_label = ctk.CTkLabel( + self.dialog, + text="🔑 用户登录", + font=("Arial", 18, "bold") + ) + title_label.pack(pady=20) + + # 用户ID输入 + user_id_frame = ctk.CTkFrame(self.dialog) + user_id_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(user_id_frame, text="用户ID:", font=("Arial", 14)).pack(anchor="w", padx=10, pady=5) + self.user_id_entry = ctk.CTkEntry(user_id_frame, placeholder_text="输入用户ID") + self.user_id_entry.pack(fill="x", padx=10, pady=5) + + # 姓名输入 + name_frame = ctk.CTkFrame(self.dialog) + name_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(name_frame, text="姓名:", font=("Arial", 14)).pack(anchor="w", padx=10, pady=5) + self.name_entry = ctk.CTkEntry(name_frame, placeholder_text="输入姓名") + self.name_entry.pack(fill="x", padx=10, pady=5) + + # 登录按钮 + login_btn = ctk.CTkButton( + self.dialog, + text="🚀 登录", + font=("Arial", 14, "bold"), + height=50, + command=self._login + ) + login_btn.pack(fill="x", padx=20, pady=20) + + # 测试用户按钮 + test_users_frame = ctk.CTkFrame(self.dialog) + test_users_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(test_users_frame, text="测试用户:", font=("Arial", 12)).pack(pady=5) + + test_users = ["user001", "user002", "user003"] + for user_id in test_users: + btn = ctk.CTkButton( + test_users_frame, + text=f"👤 {user_id}", + font=("Arial", 12), + height=30, + command=lambda u=user_id: self._quick_login(u) + ) + btn.pack(fill="x", pady=2) + + def _login(self): + """登录""" + user_id = self.user_id_entry.get().strip() + name = self.name_entry.get().strip() + + if not user_id or not name: + messagebox.showwarning("警告", "请填写完整信息") + return + + try: + # 获取或创建用户数据 + user_data = self.main_window.app_core.get_user_data(user_id) + if not user_data: + # 创建新用户 + from core.base import UserData + user_data = UserData( + user_id=user_id, + profile={'name': name, 'age': 25, 'gender': '女', 'height': 165, 'weight': 55, 'activity_level': 'moderate'}, + meals=[], + feedback=[], + preferences={} + ) + self.main_window.app_core.data_manager.save_user_data(user_data) + + # 设置当前用户 + self.main_window.set_current_user(user_id, user_data) + + # 关闭对话框 + self.dialog.destroy() + + messagebox.showinfo("成功", f"欢迎,{name}!") + + except Exception as e: + messagebox.showerror("错误", f"登录失败: {str(e)}") + + def _quick_login(self, user_id: str): + """快速登录测试用户""" + self.user_id_entry.delete(0, "end") + self.user_id_entry.insert(0, user_id) + + # 设置默认姓名 + names = {"user001": "张三", "user002": "李四", "user003": "王五"} + self.name_entry.delete(0, "end") + self.name_entry.insert(0, names.get(user_id, "测试用户")) + + def show(self): + """显示对话框""" + self.dialog.wait_window() + + +def main(): + """主函数""" + app = MobileMainWindow() + app.run() + + +if __name__ == "__main__": + main() diff --git a/gui/new_main_window.py b/gui/new_main_window.py new file mode 100644 index 0000000..1681932 --- /dev/null +++ b/gui/new_main_window.py @@ -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("", 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() diff --git a/gui/ocr_calorie_gui.py b/gui/ocr_calorie_gui.py new file mode 100644 index 0000000..efeed0b --- /dev/null +++ b/gui/ocr_calorie_gui.py @@ -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('<>', self._on_result_select) + self.result_tree.bind('', 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() diff --git a/gui/quick_user_input.py b/gui/quick_user_input.py new file mode 100644 index 0000000..323e257 --- /dev/null +++ b/gui/quick_user_input.py @@ -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() diff --git a/gui/smart_meal_record.py b/gui/smart_meal_record.py new file mode 100644 index 0000000..28bca52 --- /dev/null +++ b/gui/smart_meal_record.py @@ -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("", 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() diff --git a/gui/styles.py b/gui/styles.py new file mode 100644 index 0000000..29dfd3d --- /dev/null +++ b/gui/styles.py @@ -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() diff --git a/llm_integration/qwen_client.py b/llm_integration/qwen_client.py new file mode 100644 index 0000000..b3dce1e --- /dev/null +++ b/llm_integration/qwen_client.py @@ -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("千问大模型集成测试完成!") diff --git a/main.py b/main.py new file mode 100644 index 0000000..51485b2 --- /dev/null +++ b/main.py @@ -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() diff --git a/modules/ai_analysis.py b/modules/ai_analysis.py new file mode 100644 index 0000000..6111414 --- /dev/null +++ b/modules/ai_analysis.py @@ -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分析模块测试完成!") diff --git a/modules/data_collection.py b/modules/data_collection.py new file mode 100644 index 0000000..cede8de --- /dev/null +++ b/modules/data_collection.py @@ -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("数据采集模块测试完成!") diff --git a/modules/efficient_data_processing.py b/modules/efficient_data_processing.py new file mode 100644 index 0000000..67931e1 --- /dev/null +++ b/modules/efficient_data_processing.py @@ -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("测试完成!") diff --git a/modules/ocr_calorie_recognition.py b/modules/ocr_calorie_recognition.py new file mode 100644 index 0000000..55aeae0 --- /dev/null +++ b/modules/ocr_calorie_recognition.py @@ -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模块初始化失败") diff --git a/modules/recommendation_engine.py b/modules/recommendation_engine.py new file mode 100644 index 0000000..0a93151 --- /dev/null +++ b/modules/recommendation_engine.py @@ -0,0 +1,1016 @@ +""" +推荐引擎模块 - 基于基座架构 +结合机器学习和AI分析的混合推荐系统 +""" + +from typing import Dict, List, Optional, Any, Tuple +import json +import numpy as np +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity +from sklearn.cluster import KMeans +from sklearn.preprocessing import StandardScaler +import joblib +from datetime import datetime, timedelta +from core.base import BaseModule, ModuleType, UserData, AnalysisResult, BaseConfig +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +class RecommendationEngine(BaseModule): + """推荐引擎模块""" + + def __init__(self, config: BaseConfig): + super().__init__(config, ModuleType.RECOMMENDATION) + self.model_path = Path(config.model_path) + self.model_path.mkdir(parents=True, exist_ok=True) + + # 推荐模型组件 + self.tfidf_vectorizer = None + self.user_clustering_model = None + self.food_similarity_matrix = None + self.user_preference_model = None + + # 食物数据库 + self.food_database = self._load_food_database() + + # 餐食搭配模板 + self.meal_templates = self._load_meal_templates() + + # 推荐配置 + self.max_recommendations = config.max_recommendations + self.min_training_samples = config.min_training_samples + self.model_update_threshold = config.model_update_threshold + + def initialize(self) -> bool: + """初始化推荐引擎""" + try: + self.logger.info("推荐引擎初始化中...") + + # 加载或训练模型 + self._load_or_train_models() + + 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: + recommendation_type = input_data.get('type', 'meal_recommendation') + + if recommendation_type == 'meal_recommendation': + result = self._generate_meal_recommendations(input_data, user_data) + elif recommendation_type == 'food_similarity': + result = self._find_similar_foods(input_data, user_data) + elif recommendation_type == 'preference_update': + result = self._update_user_preferences(input_data, user_data) + elif recommendation_type == 'model_retrain': + result = self._retrain_models(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"处理推荐请求失败: {e}") + return self._create_error_result(str(e)) + + def _generate_meal_recommendations(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]: + """生成餐食推荐""" + meal_type = input_data.get('meal_type', 'lunch') + preferences = input_data.get('preferences', {}) + context = input_data.get('context', {}) + + try: + # 生成完整餐食搭配推荐 + meal_combinations = self._generate_meal_combinations(user_data, meal_type, preferences, context) + + return { + 'success': True, + 'recommendations': meal_combinations, + 'reasoning': self._generate_meal_reasoning(meal_combinations, user_data, meal_type), + 'confidence': self._calculate_recommendation_confidence(user_data), + 'metadata': { + 'meal_type': meal_type, + 'combination_count': len(meal_combinations) + } + } + + except Exception as e: + self.logger.error(f"生成餐食推荐失败: {e}") + return self._create_error_result(f"推荐生成失败: {str(e)}") + + def _generate_meal_combinations(self, user_data: UserData, meal_type: str, + preferences: Dict, context: Dict) -> List[Dict[str, Any]]: + """生成基于用户数据的动态餐食搭配组合""" + combinations = [] + + try: + # 1. 基于用户历史数据生成搭配 + historical_combinations = self._generate_historical_combinations(user_data, meal_type) + combinations.extend(historical_combinations) + + # 2. 基于用户偏好生成个性化搭配 + personalized_combinations = self._generate_personalized_combinations(user_data, meal_type, preferences) + combinations.extend(personalized_combinations) + + # 3. 基于相似用户生成搭配 + similar_user_combinations = self._generate_similar_user_combinations(user_data, meal_type) + combinations.extend(similar_user_combinations) + + # 4. 如果没有足够数据,使用模板生成 + if len(combinations) < 3: + template_combinations = self._generate_template_combinations(user_data, meal_type) + combinations.extend(template_combinations) + + # 5. 去重和排序 + combinations = self._deduplicate_and_rank_combinations(combinations, user_data) + + # 6. 确保至少有一些推荐 + if not combinations: + combinations = self._generate_fallback_combinations(meal_type) + + return combinations[:5] # 返回前5个最佳搭配 + + except Exception as e: + self.logger.error(f"生成餐食搭配失败: {e}") + # 返回基础推荐 + return self._generate_fallback_combinations(meal_type) + + def _generate_historical_combinations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于用户历史数据生成搭配""" + combinations = [] + + # 获取该餐次的所有历史记录 + meal_records = [meal for meal in user_data.meals if meal.get('meal_type') == meal_type] + + if not meal_records: + return combinations + + # 分析高频搭配 + food_combinations = {} + for meal in meal_records: + foods = meal.get('foods', []) + if len(foods) >= 2 and meal.get('satisfaction_score', 0) >= 4: + # 生成2-3食物组合 + for i in range(len(foods)): + for j in range(i+1, min(i+3, len(foods))): + combo = tuple(sorted(foods[i:j+1])) + food_combinations[combo] = food_combinations.get(combo, 0) + 1 + + # 选择出现频率最高的搭配 + sorted_combinations = sorted(food_combinations.items(), key=lambda x: x[1], reverse=True) + + for combo, count in sorted_combinations[:2]: # 取前2个高频搭配 + if count >= 2: # 至少出现2次 + combination = { + "name": f"历史搭配{len(combinations)+1}", + "description": f"基于您{meal_type}的历史偏好", + "foods": [], + "total_calories": 0, + "nutrition_score": 0, + "personalization_score": 1.0, + "categories": ["历史数据"], + "source": "historical" + } + + for food_name in combo: + food_info = self._get_food_info(food_name) + if food_info: + combination["foods"].append(food_info) + combination["total_calories"] += food_info.get("calories", 0) + + if len(combination["foods"]) >= 2: + combination["nutrition_score"] = self._calculate_nutrition_score(combination["foods"]) + combinations.append(combination) + + return combinations + + def _generate_personalized_combinations(self, user_data: UserData, meal_type: str, preferences: Dict) -> List[Dict[str, Any]]: + """基于用户偏好生成个性化搭配""" + combinations = [] + + # 获取用户喜爱的食物 + favorite_foods = self._get_user_favorite_foods(user_data, meal_type) + + if len(favorite_foods) >= 2: + combination = { + "name": "个性化搭配", + "description": f"基于您的{meal_type}偏好定制", + "foods": [], + "total_calories": 0, + "nutrition_score": 0, + "personalization_score": 0.9, + "categories": ["个性化"], + "source": "personalized" + } + + # 选择2-4种用户喜爱的食物 + selected_foods = favorite_foods[:min(4, len(favorite_foods))] + for food in selected_foods: + food_info = self._get_food_info(food) + if food_info: + combination["foods"].append(food_info) + combination["total_calories"] += food_info.get("calories", 0) + + if len(combination["foods"]) >= 2: + combination["nutrition_score"] = self._calculate_nutrition_score(combination["foods"]) + combinations.append(combination) + + return combinations + + def _generate_similar_user_combinations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于相似用户生成搭配""" + return [] # 暂时返回空列表 + + def _generate_template_combinations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于模板生成搭配(当历史数据不足时)""" + combinations = [] + + templates = self.meal_templates.get(meal_type, []) + + for template in templates[:2]: # 只使用前2个模板 + combination = { + "name": template["name"], + "description": f"营养均衡的{meal_type}搭配", + "foods": [], + "total_calories": 0, + "nutrition_score": 0, + "personalization_score": 0.3, + "categories": template["categories"], + "source": "template" + } + + # 为每个类别选择常见食物 + for category in template["categories"]: + food = self._select_common_food_for_category(category, meal_type) + if food: + combination["foods"].append(food) + combination["total_calories"] += food.get("calories", 0) + + if len(combination["foods"]) >= 2: + combination["nutrition_score"] = self._calculate_nutrition_score(combination["foods"]) + combinations.append(combination) + + return combinations + + def _select_common_food_for_category(self, category: str, meal_type: str) -> Dict[str, Any]: + """为特定类别选择常见食物""" + common_foods = { + "主食": ["米饭", "面条", "面包", "粥"], + "蛋白质": ["鸡蛋", "鸡肉", "牛肉", "豆腐"], + "蔬菜": ["青菜", "白菜", "西红柿"], + "饮品": ["牛奶", "酸奶", "汤"], + "水果": ["苹果", "香蕉"], + "坚果": ["坚果"], + "小食": ["饼干"] + } + + foods = common_foods.get(category, []) + if foods: + # 根据餐次选择合适的主食 + if category == "主食" and meal_type == "breakfast": + food_name = "面包" if "面包" in foods else foods[0] + elif category == "主食" and meal_type == "lunch": + food_name = "米饭" if "米饭" in foods else foods[0] + elif category == "主食" and meal_type == "dinner": + food_name = "粥" if "粥" in foods else foods[0] + else: + food_name = foods[0] + + return self._get_food_info(food_name) + + return None + + def _deduplicate_and_rank_combinations(self, combinations: List[Dict], user_data: UserData) -> List[Dict]: + """去重和排序搭配组合""" + # 去重:基于食物组合去重 + unique_combinations = [] + seen_combinations = set() + + for combo in combinations: + food_names = tuple(sorted([f["name"] for f in combo["foods"]])) + if food_names not in seen_combinations: + seen_combinations.add(food_names) + unique_combinations.append(combo) + + # 排序:综合得分 + def score_combination(combo): + personal_score = combo.get("personalization_score", 0) * 0.4 + nutrition_score = combo.get("nutrition_score", 0) * 0.3 + satisfaction_score = sum(f.get("satisfaction_score", 4) for f in combo["foods"]) / len(combo["foods"]) * 0.3 + return personal_score + nutrition_score + satisfaction_score + + return sorted(unique_combinations, key=score_combination, reverse=True) + + def _generate_meal_reasoning(self, combinations: List[Dict], user_data: UserData, meal_type: str) -> str: + """生成动态餐食推荐理由""" + if not combinations: + return "暂无推荐" + + top_combination = combinations[0] + foods = top_combination["foods"] + + reasoning_parts = [] + + # 基于数据来源生成理由 + source = top_combination.get("source", "unknown") + if source == "historical": + reasoning_parts.append("基于您的历史用餐偏好") + elif source == "personalized": + reasoning_parts.append("基于您的个人喜好") + elif source == "template": + reasoning_parts.append("营养均衡搭配") + + # 满意度理由 + avg_satisfaction = sum(f.get("satisfaction_score", 4) for f in foods) / len(foods) + if avg_satisfaction > 4.0: + reasoning_parts.append(f"历史满意度: {avg_satisfaction:.1f}分") + + # 食物搭配理由 + food_names = [f["name"] for f in foods] + reasoning_parts.append(f"推荐搭配: {', '.join(food_names)}") + + return ",".join(reasoning_parts) if reasoning_parts else "营养搭配推荐" + + def _get_user_favorite_foods(self, user_data: UserData, meal_type: str) -> List[str]: + """获取用户喜爱的食物""" + favorite_foods = [] + + # 从历史餐食记录中提取 + for meal in user_data.meals: + if meal.get('meal_type') == meal_type and meal.get('satisfaction_score', 0) >= 4: + favorite_foods.extend(meal.get('foods', [])) + + # 统计频率 + food_counts = {} + for food in favorite_foods: + food_counts[food] = food_counts.get(food, 0) + 1 + + # 按频率排序 + sorted_foods = sorted(food_counts.items(), key=lambda x: x[1], reverse=True) + return [food for food, count in sorted_foods[:10]] + + def _get_food_info(self, food_name: str) -> Dict[str, Any]: + """获取食物信息""" + # 从食物数据库获取信息 + food_info = self.food_database.get(food_name, {}) + return { + "name": food_name, + "calories": food_info.get("calories", 100), + "category": food_info.get("category", "其他"), + "nutrition": food_info.get("nutrition", {}), + "satisfaction_score": food_info.get("avg_satisfaction", 4.0) + } + + def _calculate_nutrition_score(self, foods: List[Dict]) -> float: + """计算营养得分""" + if not foods: + return 0.0 + + # 简单的营养评分逻辑 + score = 0.0 + categories = set() + + for food in foods: + category = food.get("category", "其他") + categories.add(category) + score += food.get("satisfaction_score", 4.0) + + # 多样性加分 + diversity_bonus = min(len(categories) * 0.1, 0.3) + + return (score / len(foods)) + diversity_bonus + + def _generate_fallback_combinations(self, meal_type: str) -> List[Dict[str, Any]]: + """生成备选推荐(当其他方法都失败时)""" + combinations = [] + + # 基础推荐搭配 + fallback_combinations = { + "breakfast": [ + {"name": "营养早餐", "foods": ["面包", "鸡蛋", "牛奶"]}, + {"name": "健康早餐", "foods": ["燕麦粥", "香蕉", "酸奶"]} + ], + "lunch": [ + {"name": "均衡午餐", "foods": ["米饭", "鸡肉", "青菜"]}, + {"name": "轻食午餐", "foods": ["面条", "牛肉", "西红柿"]} + ], + "dinner": [ + {"name": "清淡晚餐", "foods": ["粥", "豆腐", "白菜"]}, + {"name": "温馨晚餐", "foods": ["饺子", "汤"]} + ], + "snack": [ + {"name": "健康小食", "foods": ["苹果", "坚果"]}, + {"name": "休闲小食", "foods": ["酸奶", "饼干"]} + ] + } + + templates = fallback_combinations.get(meal_type, []) + + for template in templates: + combination = { + "name": template["name"], + "description": f"营养均衡的{meal_type}搭配", + "foods": [], + "total_calories": 0, + "nutrition_score": 4.0, + "personalization_score": 0.2, + "categories": ["基础推荐"], + "source": "fallback" + } + + for food_name in template["foods"]: + food_info = self._get_food_info(food_name) + if food_info: + combination["foods"].append(food_info) + combination["total_calories"] += food_info.get("calories", 0) + + if len(combination["foods"]) >= 2: + combinations.append(combination) + + return combinations + + def _get_historical_recommendations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于历史数据的推荐""" + recommendations = [] + + # 分析用户历史餐食 + historical_meals = [meal for meal in user_data.meals if meal.get('meal_type') == meal_type] + + if not historical_meals: + return recommendations + + # 统计用户喜欢的食物 + food_scores = {} + for meal in historical_meals: + satisfaction = meal.get('satisfaction_score', 3) # 默认3分 + for food in meal.get('foods', []): + if food not in food_scores: + food_scores[food] = [] + food_scores[food].append(satisfaction) + + # 计算平均满意度 + for food, scores in food_scores.items(): + avg_score = np.mean(scores) + if avg_score >= 3: # 只推荐满意度>=3的食物 + recommendations.append({ + 'food': food, + 'score': avg_score, + 'type': 'historical', + 'reason': f'历史满意度: {avg_score:.1f}分' + }) + + return sorted(recommendations, key=lambda x: x['score'], reverse=True) + + def _get_similar_user_recommendations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于相似用户的推荐""" + recommendations = [] + + if not self.user_clustering_model: + return recommendations + + try: + # 获取用户特征向量 + user_features = self._extract_user_features(user_data) + + # 找到相似用户 + similar_users = self._find_similar_users(user_features) + + # 基于相似用户的偏好推荐 + for similar_user_id in similar_users: + similar_user_data = self._get_user_data_by_id(similar_user_id) + if similar_user_data: + user_recommendations = self._get_historical_recommendations(similar_user_data, meal_type) + for rec in user_recommendations: + rec['type'] = 'similar_user' + rec['reason'] = f'相似用户推荐' + recommendations.append(rec) + + except Exception as e: + self.logger.error(f"相似用户推荐失败: {e}") + + return recommendations + + def _get_content_based_recommendations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]: + """基于内容相似性的推荐""" + recommendations = [] + + if not self.food_similarity_matrix: + return recommendations + + try: + # 获取用户喜欢的食物 + liked_foods = [] + for meal in user_data.meals: + if meal.get('satisfaction_score', 0) >= 4: # 满意度>=4的食物 + liked_foods.extend(meal.get('foods', [])) + + if not liked_foods: + return recommendations + + # 基于食物相似性推荐 + for liked_food in liked_foods: + if liked_food in self.food_similarity_matrix: + similar_foods = self.food_similarity_matrix[liked_food] + for food, similarity in similar_foods.items(): + if food not in liked_foods: # 避免重复推荐 + recommendations.append({ + 'food': food, + 'score': similarity, + 'type': 'content_based', + 'reason': f'与{liked_food}相似' + }) + + except Exception as e: + self.logger.error(f"内容推荐失败: {e}") + + return recommendations + + def _get_physiological_recommendations(self, user_data: UserData, context: Dict) -> List[Dict[str, Any]]: + """基于生理状态的推荐""" + recommendations = [] + + # 获取生理状态信息 + physiological_state = context.get('physiological_state', {}) + needs = physiological_state.get('needs', []) + + if not needs: + return recommendations + + # 根据营养需求推荐食物 + nutrition_food_mapping = { + '铁质': ['菠菜', '瘦肉', '红枣', '黑芝麻', '猪肝'], + '蛋白质': ['鸡蛋', '豆腐', '鱼肉', '鸡肉', '牛奶'], + '维生素C': ['橙子', '柠檬', '西红柿', '西兰花', '草莓'], + '叶酸': ['绿叶蔬菜', '豆类', '坚果', '菠菜', '芦笋'], + '维生素B': ['全谷物', '瘦肉', '蛋类', '香蕉', '土豆'], + '锌': ['牡蛎', '瘦肉', '坚果', '豆类', '南瓜子'], + '维生素E': ['坚果', '植物油', '鳄梨', '葵花籽', '杏仁'], + '镁': ['坚果', '绿叶蔬菜', '全谷物', '黑巧克力', '香蕉'], + '维生素B6': ['香蕉', '土豆', '鸡肉', '三文鱼', '鹰嘴豆'], + '钙质': ['牛奶', '豆腐', '绿叶蔬菜', '奶酪', '酸奶'] + } + + for need in needs: + foods = nutrition_food_mapping.get(need, []) + for food in foods: + recommendations.append({ + 'food': food, + 'score': 0.8, # 生理需求推荐分数较高 + 'type': 'physiological', + 'reason': f'补充{need}' + }) + + return recommendations + + def _fuse_recommendations(self, recommendation_lists: List[List[Dict]], user_data: UserData) -> List[Dict[str, Any]]: + """融合多种推荐结果""" + food_scores = {} + + # 权重配置 + weights = { + 'historical': 0.4, + 'similar_user': 0.2, + 'content_based': 0.2, + 'physiological': 0.2 + } + + for rec_list in recommendation_lists: + for rec in rec_list: + food = rec['food'] + score = rec['score'] + rec_type = rec['type'] + + if food not in food_scores: + food_scores[food] = { + 'total_score': 0, + 'count': 0, + 'reasons': [], + 'types': [] + } + + weight = weights.get(rec_type, 0.1) + food_scores[food]['total_score'] += score * weight + food_scores[food]['count'] += 1 + food_scores[food]['reasons'].append(rec['reason']) + food_scores[food]['types'].append(rec_type) + + # 转换为推荐列表 + recommendations = [] + for food, data in food_scores.items(): + recommendations.append({ + 'food': food, + 'score': data['total_score'], + 'count': data['count'], + 'reasons': data['reasons'], + 'types': data['types'] + }) + + return sorted(recommendations, key=lambda x: x['score'], reverse=True) + + def _filter_and_rank_recommendations(self, recommendations: List[Dict], + user_data: UserData, preferences: Dict) -> List[Dict[str, Any]]: + """过滤和排序推荐""" + filtered = [] + + # 获取用户不喜欢的食物 + dislikes = user_data.profile.get('dislikes', []) + allergies = user_data.profile.get('allergies', []) + + for rec in recommendations: + food = rec['food'] + + # 过滤不喜欢的食物 + if any(dislike in food for dislike in dislikes): + continue + + # 过滤过敏食物 + if any(allergy in food for allergy in allergies): + continue + + # 应用用户偏好 + if preferences: + if 'taste' in preferences: + taste = preferences['taste'] + if taste == 'sweet' and not self._is_sweet_food(food): + rec['score'] *= 0.8 + elif taste == 'spicy' and not self._is_spicy_food(food): + rec['score'] *= 0.8 + + filtered.append(rec) + + return sorted(filtered, key=lambda x: x['score'], reverse=True) + + def _generate_recommendation_reasoning(self, recommendations: List[Dict], user_data: UserData) -> str: + """生成推荐理由""" + if not recommendations: + return "暂无推荐" + + top_rec = recommendations[0] + reasons = top_rec.get('reasons', []) + + if reasons: + return f"推荐{top_rec['food']},理由:{'; '.join(reasons[:2])}" + else: + return f"推荐{top_rec['food']},基于您的个人偏好" + + def _calculate_recommendation_confidence(self, user_data: UserData) -> float: + """计算推荐置信度""" + meal_count = len(user_data.meals) + feedback_count = len(user_data.feedback) + + # 基于数据量计算置信度 + if meal_count >= 15 and feedback_count >= 5: + return 0.9 + elif meal_count >= 10 and feedback_count >= 3: + return 0.7 + elif meal_count >= 5: + return 0.5 + else: + return 0.3 + + def _extract_user_features(self, user_data: UserData) -> np.ndarray: + """提取用户特征向量""" + features = [] + + # 基础特征 + profile = user_data.profile + features.extend([ + profile.get('age', 25), + profile.get('height', 165), + profile.get('weight', 60), + len(profile.get('allergies', [])), + len(profile.get('dislikes', [])) + ]) + + # 口味偏好特征 + taste_prefs = profile.get('taste_preferences', {}) + features.extend([ + taste_prefs.get('sweet', 3), + taste_prefs.get('salty', 3), + taste_prefs.get('spicy', 3), + taste_prefs.get('sour', 3), + taste_prefs.get('bitter', 3), + taste_prefs.get('umami', 3) + ]) + + # 餐食特征 + features.extend([ + len(user_data.meals), + np.mean([meal.get('satisfaction_score', 3) for meal in user_data.meals]) if user_data.meals else 3, + len(user_data.feedback) + ]) + + return np.array(features) + + def _find_similar_users(self, user_features: np.ndarray) -> List[str]: + """找到相似用户""" + # 这里简化实现,实际应该基于用户聚类模型 + return [] + + def _get_user_data_by_id(self, user_id: str) -> Optional[UserData]: + """根据ID获取用户数据""" + # 这里需要从数据管理器获取,简化实现 + return None + + def _is_sweet_food(self, food: str) -> bool: + """判断是否为甜食""" + sweet_keywords = ['甜', '糖', '蜂蜜', '果', '蛋糕', '巧克力', '冰淇淋'] + return any(keyword in food for keyword in sweet_keywords) + + def _is_spicy_food(self, food: str) -> bool: + """判断是否为辣食""" + spicy_keywords = ['辣', '椒', '麻', '辛', '咖喱', '辣椒'] + return any(keyword in food for keyword in spicy_keywords) + + def _load_meal_templates(self) -> Dict[str, List[Dict]]: + """加载基础餐食搭配模板(作为备选方案)""" + return { + "breakfast": [ + {"name": "经典早餐", "categories": ["主食", "蛋白质", "饮品"]}, + {"name": "健康早餐", "categories": ["谷物", "水果", "蛋白质"]}, + {"name": "中式早餐", "categories": ["主食", "蛋白质", "蔬菜"]} + ], + "lunch": [ + {"name": "均衡午餐", "categories": ["主食", "蛋白质", "蔬菜"]}, + {"name": "轻食午餐", "categories": ["主食", "蛋白质", "蔬菜"]}, + {"name": "丰盛午餐", "categories": ["主食", "蛋白质", "蔬菜", "汤品"]} + ], + "dinner": [ + {"name": "清淡晚餐", "categories": ["主食", "蛋白质", "蔬菜"]}, + {"name": "温馨晚餐", "categories": ["主食", "蛋白质", "蔬菜"]} + ], + "snack": [ + {"name": "健康小食", "categories": ["水果", "坚果"]}, + {"name": "休闲小食", "categories": ["饮品", "小食"]} + ] + } + + def _load_food_database(self) -> Dict[str, Dict]: + """加载食物数据库""" + return { + '米饭': {'calories': 130, 'protein': 2.7, 'carbs': 28, 'fat': 0.3, 'category': '主食'}, + '面条': {'calories': 131, 'protein': 5, 'carbs': 25, 'fat': 1.1, 'category': '主食'}, + '鸡蛋': {'calories': 155, 'protein': 13, 'carbs': 1.1, 'fat': 11, 'category': '蛋白质'}, + '鸡肉': {'calories': 165, 'protein': 31, 'carbs': 0, 'fat': 3.6, 'category': '蛋白质'}, + '鱼肉': {'calories': 206, 'protein': 22, 'carbs': 0, 'fat': 12, 'category': '蛋白质'}, + '豆腐': {'calories': 76, 'protein': 8, 'carbs': 2, 'fat': 4.8, 'category': '蛋白质'}, + '菠菜': {'calories': 23, 'protein': 2.9, 'carbs': 3.6, 'fat': 0.4, 'category': '蔬菜'}, + '西兰花': {'calories': 34, 'protein': 2.8, 'carbs': 7, 'fat': 0.4, 'category': '蔬菜'}, + '苹果': {'calories': 52, 'protein': 0.3, 'carbs': 14, 'fat': 0.2, 'category': '水果'}, + '香蕉': {'calories': 89, 'protein': 1.1, 'carbs': 23, 'fat': 0.3, 'category': '水果'}, + '牛奶': {'calories': 42, 'protein': 3.4, 'carbs': 5, 'fat': 1, 'category': '乳制品'}, + '燕麦': {'calories': 389, 'protein': 17, 'carbs': 66, 'fat': 7, 'category': '主食'} + } + + def _load_or_train_models(self): + """加载或训练模型""" + try: + # 尝试加载现有模型 + tfidf_path = self.model_path / 'tfidf_vectorizer.pkl' + if tfidf_path.exists(): + self.tfidf_vectorizer = joblib.load(tfidf_path) + self.logger.info("TF-IDF向量化器加载成功") + + # 构建食物相似性矩阵 + self._build_food_similarity_matrix() + + except Exception as e: + self.logger.error(f"模型加载失败: {e}") + # 如果加载失败,使用默认配置 + self._initialize_default_models() + + def _build_food_similarity_matrix(self): + """构建食物相似性矩阵""" + try: + if not self.tfidf_vectorizer: + self._initialize_default_models() + + # 获取所有食物名称 + food_names = list(self.food_database.keys()) + + # 使用TF-IDF计算相似性 + food_features = self.tfidf_vectorizer.transform(food_names) + similarity_matrix = cosine_similarity(food_features) + + # 构建相似性字典 + self.food_similarity_matrix = {} + for i, food1 in enumerate(food_names): + similarities = {} + for j, food2 in enumerate(food_names): + if i != j: + similarities[food2] = similarity_matrix[i][j] + + # 按相似性排序 + sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True) + self.food_similarity_matrix[food1] = dict(sorted_similarities[:5]) # 只保留前5个相似食物 + + self.logger.info("食物相似性矩阵构建完成") + + except Exception as e: + self.logger.error(f"构建食物相似性矩阵失败: {e}") + self.food_similarity_matrix = {} + + def _initialize_default_models(self): + """初始化默认模型""" + try: + # 初始化TF-IDF向量化器 + self.tfidf_vectorizer = TfidfVectorizer( + max_features=1000, + stop_words=None, # 中文不需要英文停用词 + ngram_range=(1, 2) + ) + + # 使用食物名称训练 + food_names = list(self.food_database.keys()) + self.tfidf_vectorizer.fit(food_names) + + # 保存模型 + tfidf_path = self.model_path / 'tfidf_vectorizer.pkl' + joblib.dump(self.tfidf_vectorizer, tfidf_path) + + self.logger.info("默认模型初始化完成") + + except Exception as e: + self.logger.error(f"默认模型初始化失败: {e}") + + def _find_similar_foods(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]: + """查找相似食物""" + target_food = input_data.get('food', '') + + if not target_food or not self.food_similarity_matrix: + return self._create_error_result("无法找到相似食物") + + similar_foods = self.food_similarity_matrix.get(target_food, {}) + + return { + 'success': True, + 'target_food': target_food, + 'similar_foods': list(similar_foods.items()), + 'confidence': 0.8 + } + + def _update_user_preferences(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]: + """更新用户偏好""" + feedback_data = input_data.get('feedback', {}) + + # 更新用户偏好模型 + # 这里可以实现更复杂的偏好学习算法 + + return { + 'success': True, + 'message': '用户偏好更新成功', + 'confidence': 0.7 + } + + def _retrain_models(self, input_data: Dict, user_data: UserData) -> Dict[str, Any]: + """重新训练模型""" + try: + # 检查是否有足够的数据进行重训练 + total_samples = len(user_data.meals) + len(user_data.feedback) + + if total_samples < self.model_update_threshold: + return { + 'success': False, + 'message': f'数据不足,需要至少{self.model_update_threshold}个样本', + 'current_samples': total_samples + } + + # 重新训练模型 + self._load_or_train_models() + + return { + 'success': True, + 'message': '模型重训练完成', + 'confidence': 0.9 + } + + except Exception as e: + return self._create_error_result(f"模型重训练失败: {str(e)}") + + def _create_error_result(self, error_message: str) -> Dict[str, Any]: + """创建错误结果""" + return { + 'success': False, + 'error': error_message, + 'message': f'推荐失败: {error_message}', + 'confidence': 0.0 + } + + def cleanup(self) -> bool: + """清理资源""" + try: + self.logger.info("推荐引擎清理完成") + return True + except Exception as e: + self.logger.error(f"推荐引擎清理失败: {e}") + return False + + +# 便捷函数 +def generate_meal_recommendations(user_id: str, meal_type: str, preferences: Dict = None, context: Dict = None) -> Optional[Dict]: + """生成餐食推荐""" + from core.base import get_app_core + + if preferences is None: + preferences = {} + if context is None: + context = {} + + app = get_app_core() + input_data = { + 'type': 'meal_recommendation', + 'meal_type': meal_type, + 'preferences': preferences, + 'context': context + } + + result = app.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id) + return result.result if result else None + + +def find_similar_foods(user_id: str, food: str) -> Optional[Dict]: + """查找相似食物""" + from core.base import get_app_core + + app = get_app_core() + input_data = { + 'type': 'food_similarity', + 'food': food + } + + result = app.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id) + return result.result if result else None + + +def update_user_preferences(user_id: str, feedback: Dict) -> Optional[Dict]: + """更新用户偏好""" + from core.base import get_app_core + + app = get_app_core() + input_data = { + 'type': 'preference_update', + 'feedback': feedback + } + + result = app.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id) + return result.result if result else None + + +def retrain_recommendation_model(user_id: str) -> Optional[Dict]: + """重新训练推荐模型""" + from core.base import get_app_core + + app = get_app_core() + input_data = { + 'type': 'model_retrain' + } + + result = app.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id) + return result.result if result else None + + +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" + + result = generate_meal_recommendations(test_user_id, "lunch", {"taste": "sweet"}) + if result and result.get('success'): + recommendations = result.get('recommendations', []) + print(f"✅ 餐食推荐成功,推荐了{len(recommendations)}种食物") + for rec in recommendations[:3]: + print(f" - {rec['food']}: {rec.get('score', 0):.2f}") + + # 测试相似食物查找 + result = find_similar_foods(test_user_id, "米饭") + if result and result.get('success'): + similar_foods = result.get('similar_foods', []) + print(f"✅ 相似食物查找成功,找到{len(similar_foods)}种相似食物") + + # 清理应用 + cleanup_app() + print("✅ 应用清理完成") + else: + print("❌ 应用初始化失败") diff --git a/package.json b/package.json new file mode 100644 index 0000000..0843456 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9e99d50 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/smart_food/smart_database.py b/smart_food/smart_database.py new file mode 100644 index 0000000..96cd4b7 --- /dev/null +++ b/smart_food/smart_database.py @@ -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("智能食物数据库测试完成!") diff --git a/start.py b/start.py new file mode 100644 index 0000000..334fe78 --- /dev/null +++ b/start.py @@ -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)