GitHub Action commited on
Commit ·
59bd45e
0
Parent(s):
Deploy clean version of Nora
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +75 -0
- .env.example +33 -0
- .gitattributes +35 -0
- .github/workflows/sync.yml +38 -0
- .gitignore +61 -0
- .kiro/specs/voice-text-processor/design.md +514 -0
- .kiro/specs/voice-text-processor/requirements.md +139 -0
- .kiro/specs/voice-text-processor/tasks.md +204 -0
- Dockerfile +31 -0
- HOTFIX_DOCKER_BUILD.md +123 -0
- HOTFIX_NULL_ERROR.md +129 -0
- HUGGINGFACE_DEPLOY.md +176 -0
- HUGGINGFACE_FIX_SUMMARY.md +223 -0
- PRD.md +155 -0
- PROJECT_STRUCTURE.md +155 -0
- README.md +175 -0
- README_HF.md +131 -0
- app/__init__.py +1 -0
- app/asr_service.py +202 -0
- app/config.py +226 -0
- app/image_service.py +441 -0
- app/logging_config.py +196 -0
- app/main.py +1132 -0
- app/models.py +118 -0
- app/semantic_parser.py +326 -0
- app/storage.py +508 -0
- app/user_config.py +211 -0
- data/.gitkeep +1 -0
- deployment/DEPLOYMENT.md +133 -0
- deployment/DEPLOY_CHECKLIST.md +137 -0
- deployment/Dockerfile +31 -0
- deployment/README_HF.md +99 -0
- deployment/README_MODELSCOPE.md +126 -0
- deployment/app_modelscope.py +187 -0
- deployment/configuration.json +5 -0
- deployment/deploy_to_hf.bat +109 -0
- deployment/deploy_to_hf.sh +101 -0
- deployment/ms_deploy.json +29 -0
- deployment/requirements_hf.txt +17 -0
- deployment/requirements_modelscope.txt +17 -0
- docs/API_配置说明.md +113 -0
- docs/FEATURE_SUMMARY.md +368 -0
- docs/README.md +103 -0
- docs/ROADMAP.md +422 -0
- docs/功能架构图.md +375 -0
- docs/后端启动问题排查.md +368 -0
- docs/局域网访问修复完成.md +187 -0
- docs/局域网访问快速修复.md +157 -0
- docs/局域网访问指南.md +269 -0
- docs/局域网访问问题排查.md +195 -0
.dockerignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
env/
|
| 13 |
+
venv/
|
| 14 |
+
ENV/
|
| 15 |
+
.venv
|
| 16 |
+
|
| 17 |
+
# Testing
|
| 18 |
+
.pytest_cache
|
| 19 |
+
.hypothesis
|
| 20 |
+
.coverage
|
| 21 |
+
htmlcov/
|
| 22 |
+
*.log
|
| 23 |
+
|
| 24 |
+
# IDE
|
| 25 |
+
.vscode
|
| 26 |
+
.idea
|
| 27 |
+
*.swp
|
| 28 |
+
*.swo
|
| 29 |
+
*~
|
| 30 |
+
|
| 31 |
+
# Documentation (keep only essential)
|
| 32 |
+
docs/
|
| 33 |
+
PRD.md
|
| 34 |
+
PROJECT_STRUCTURE.md
|
| 35 |
+
局域网访问修复完成.md
|
| 36 |
+
|
| 37 |
+
# Deployment files (not needed in container)
|
| 38 |
+
deployment/
|
| 39 |
+
scripts/start_local.py
|
| 40 |
+
scripts/start_local.bat
|
| 41 |
+
scripts/test_lan_access.bat
|
| 42 |
+
scripts/build_and_deploy.sh
|
| 43 |
+
scripts/build_and_deploy.bat
|
| 44 |
+
|
| 45 |
+
# Frontend source (only need dist)
|
| 46 |
+
frontend/node_modules
|
| 47 |
+
frontend/src
|
| 48 |
+
frontend/components
|
| 49 |
+
frontend/services
|
| 50 |
+
frontend/utils
|
| 51 |
+
frontend/.env.local
|
| 52 |
+
frontend/package.json
|
| 53 |
+
frontend/package-lock.json
|
| 54 |
+
frontend/tsconfig.json
|
| 55 |
+
frontend/vite.config.ts
|
| 56 |
+
frontend/index.tsx
|
| 57 |
+
frontend/index.css
|
| 58 |
+
frontend/types.ts
|
| 59 |
+
frontend/App.tsx
|
| 60 |
+
frontend/test-*.html
|
| 61 |
+
|
| 62 |
+
# Tests
|
| 63 |
+
tests/
|
| 64 |
+
|
| 65 |
+
# Logs
|
| 66 |
+
logs/
|
| 67 |
+
|
| 68 |
+
# OS
|
| 69 |
+
.DS_Store
|
| 70 |
+
Thumbs.db
|
| 71 |
+
|
| 72 |
+
# Temporary files
|
| 73 |
+
*.tmp
|
| 74 |
+
*.bak
|
| 75 |
+
*.swp
|
.env.example
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Voice Text Processor Configuration
|
| 2 |
+
# Copy this file to .env and fill in your values
|
| 3 |
+
|
| 4 |
+
# Required: Zhipu AI API Key (for semantic parsing)
|
| 5 |
+
# 获取方式: https://open.bigmodel.cn/ -> API Keys
|
| 6 |
+
ZHIPU_API_KEY=your_zhipu_api_key_here
|
| 7 |
+
|
| 8 |
+
# Required: MiniMax API Key (for image generation)
|
| 9 |
+
# 获取方式: https://platform.minimax.io/ -> API Keys
|
| 10 |
+
# 格式示例: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 11 |
+
MINIMAX_API_KEY=your_minimax_api_key_here
|
| 12 |
+
|
| 13 |
+
# Optional: MiniMax Group ID (已废弃,保留用于兼容性)
|
| 14 |
+
MINIMAX_GROUP_ID=your_group_id_here
|
| 15 |
+
|
| 16 |
+
# Optional: Data storage directory (default: data/)
|
| 17 |
+
DATA_DIR=data
|
| 18 |
+
|
| 19 |
+
# Optional: Maximum audio file size in bytes (default: 10485760 = 10MB)
|
| 20 |
+
MAX_AUDIO_SIZE=10485760
|
| 21 |
+
|
| 22 |
+
# Optional: Logging level (default: INFO)
|
| 23 |
+
# Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
| 24 |
+
LOG_LEVEL=INFO
|
| 25 |
+
|
| 26 |
+
# Optional: Log file path (default: logs/app.log)
|
| 27 |
+
LOG_FILE=logs/app.log
|
| 28 |
+
|
| 29 |
+
# Optional: Server host (default: 0.0.0.0)
|
| 30 |
+
HOST=0.0.0.0
|
| 31 |
+
|
| 32 |
+
# Optional: Server port (default: 8000)
|
| 33 |
+
PORT=8000
|
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/sync.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face hub
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
branches: [main]
|
| 5 |
+
workflow_dispatch:
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
sync-to-hub:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
steps:
|
| 11 |
+
- uses: actions/checkout@v3
|
| 12 |
+
with:
|
| 13 |
+
fetch-depth: 0
|
| 14 |
+
lfs: true
|
| 15 |
+
- name: Push to hub
|
| 16 |
+
env:
|
| 17 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 18 |
+
run: |
|
| 19 |
+
# 1. 配置身份
|
| 20 |
+
git config --global user.email "bot@github.com"
|
| 21 |
+
git config --global user.name "GitHub Action"
|
| 22 |
+
|
| 23 |
+
# 2. 彻底移除二进制文件及其索引
|
| 24 |
+
rm -rf generated_images
|
| 25 |
+
git rm -r --cached generated_images || echo "Already removed"
|
| 26 |
+
|
| 27 |
+
# 3. 创建一个全新的、没有历史记录的临时分支
|
| 28 |
+
git checkout --orphan temp-branch
|
| 29 |
+
|
| 30 |
+
# 4. 只添加当前的代码文件
|
| 31 |
+
git add .
|
| 32 |
+
git commit -m "Deploy clean version of Nora"
|
| 33 |
+
|
| 34 |
+
# 5. 强制推送到 Hugging Face 的 main 分支
|
| 35 |
+
# 注意:这会覆盖 HF 上的所有历史,非常适合解决当前死锁
|
| 36 |
+
git push --force https://kernel14:$HF_TOKEN@huggingface.co/spaces/kernel14/Nora temp-branch:main
|
| 37 |
+
|
| 38 |
+
|
.gitignore
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
downloads/
|
| 10 |
+
eggs/
|
| 11 |
+
.eggs/
|
| 12 |
+
lib/
|
| 13 |
+
lib64/
|
| 14 |
+
parts/
|
| 15 |
+
sdist/
|
| 16 |
+
var/
|
| 17 |
+
wheels/
|
| 18 |
+
*.egg-info/
|
| 19 |
+
.installed.cfg
|
| 20 |
+
*.egg
|
| 21 |
+
|
| 22 |
+
# Virtual environments
|
| 23 |
+
venv/
|
| 24 |
+
env/
|
| 25 |
+
ENV/
|
| 26 |
+
.venv
|
| 27 |
+
|
| 28 |
+
# IDE
|
| 29 |
+
.vscode/
|
| 30 |
+
.idea/
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
*~
|
| 34 |
+
|
| 35 |
+
# Environment variables
|
| 36 |
+
.env
|
| 37 |
+
|
| 38 |
+
# Logs
|
| 39 |
+
logs/
|
| 40 |
+
*.log
|
| 41 |
+
|
| 42 |
+
# Data files
|
| 43 |
+
data/*.json
|
| 44 |
+
|
| 45 |
+
# Testing
|
| 46 |
+
.pytest_cache/
|
| 47 |
+
.coverage
|
| 48 |
+
htmlcov/
|
| 49 |
+
.hypothesis/
|
| 50 |
+
|
| 51 |
+
# OS
|
| 52 |
+
.DS_Store
|
| 53 |
+
Thumbs.db
|
| 54 |
+
|
| 55 |
+
# Frontend (开发时忽略,但部署时需要 dist)
|
| 56 |
+
frontend/node_modules/
|
| 57 |
+
# 注意:frontend/dist/ 不要忽略,部署需要它!
|
| 58 |
+
|
| 59 |
+
# Docker(不要忽略 Dockerfile)
|
| 60 |
+
# Dockerfile 需要提交
|
| 61 |
+
|
.kiro/specs/voice-text-processor/design.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design Document: Voice Text Processor
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
本系统是一个基于 FastAPI 的 REST API 服务,用于处理用户的语音录音或文字输入,通过智谱 API 进行语音识别和语义解析,提取情绪、灵感和待办事项等结构化数据,并持久化到本地 JSON 文件。
|
| 6 |
+
|
| 7 |
+
系统采用分层架构设计:
|
| 8 |
+
- **API 层**:FastAPI 路由和请求处理
|
| 9 |
+
- **服务层**:业务逻辑处理(ASR、语义解析)
|
| 10 |
+
- **存储层**:JSON 文件持久化
|
| 11 |
+
|
| 12 |
+
核心工作流程:
|
| 13 |
+
1. 接收用户输入(音频文件或文本)
|
| 14 |
+
2. 如果是音频,调用智谱 ASR API 转写为文本
|
| 15 |
+
3. 调用 GLM-4-Flash API 进行语义解析
|
| 16 |
+
4. 提取情绪、灵感、待办数据
|
| 17 |
+
5. 持久化到对应的 JSON 文件
|
| 18 |
+
6. 返回结构化响应
|
| 19 |
+
|
| 20 |
+
## Architecture
|
| 21 |
+
|
| 22 |
+
系统采用三层架构:
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
┌─────────────────────────────────────┐
|
| 26 |
+
│ API Layer (FastAPI) │
|
| 27 |
+
│ - POST /api/process │
|
| 28 |
+
│ - Request validation │
|
| 29 |
+
│ - Response formatting │
|
| 30 |
+
└──────────────┬──────────────────────┘
|
| 31 |
+
│
|
| 32 |
+
┌──────────────▼──────────────────────┐
|
| 33 |
+
│ Service Layer │
|
| 34 |
+
│ - ASRService │
|
| 35 |
+
│ - SemanticParserService │
|
| 36 |
+
│ - StorageService │
|
| 37 |
+
└──────────────┬──────────────────────┘
|
| 38 |
+
│
|
| 39 |
+
┌──────────────▼──────────────────────┐
|
| 40 |
+
│ External Services │
|
| 41 |
+
│ - Zhipu ASR API │
|
| 42 |
+
│ - GLM-4-Flash API │
|
| 43 |
+
│ - Local JSON Files │
|
| 44 |
+
└─────────────────────────────────────┘
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 模块职责
|
| 48 |
+
|
| 49 |
+
**API Layer**:
|
| 50 |
+
- 处理 HTTP 请求和响应
|
| 51 |
+
- 输入验证(文件格式、大小、文本编码)
|
| 52 |
+
- 错误处理和状态码映射
|
| 53 |
+
- 请求日志记录
|
| 54 |
+
|
| 55 |
+
**Service Layer**:
|
| 56 |
+
- `ASRService`: 封装智谱 ASR API 调用,处理音频转文字
|
| 57 |
+
- `SemanticParserService`: 封装 GLM-4-Flash API 调用,执行语义解析
|
| 58 |
+
- `StorageService`: 管理 JSON 文件读写,生成唯一 ID 和时间戳
|
| 59 |
+
|
| 60 |
+
**Configuration**:
|
| 61 |
+
- 环境变量管理(API 密钥、文件路径、大小限制)
|
| 62 |
+
- 启动时配置验证
|
| 63 |
+
|
| 64 |
+
## Components and Interfaces
|
| 65 |
+
|
| 66 |
+
### 1. API Endpoint
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
@app.post("/api/process")
|
| 70 |
+
async def process_input(
|
| 71 |
+
audio: Optional[UploadFile] = File(None),
|
| 72 |
+
text: Optional[str] = Body(None)
|
| 73 |
+
) -> ProcessResponse
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**输入**:
|
| 77 |
+
- `audio`: 音频文件(multipart/form-data),支持 mp3, wav, m4a
|
| 78 |
+
- `text`: 文本内容(application/json),UTF-8 编码
|
| 79 |
+
|
| 80 |
+
**输出**:
|
| 81 |
+
```python
|
| 82 |
+
class ProcessResponse(BaseModel):
|
| 83 |
+
record_id: str
|
| 84 |
+
timestamp: str
|
| 85 |
+
mood: Optional[MoodData]
|
| 86 |
+
inspirations: List[InspirationData]
|
| 87 |
+
todos: List[TodoData]
|
| 88 |
+
error: Optional[str]
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 2. ASRService
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
class ASRService:
|
| 95 |
+
def __init__(self, api_key: str):
|
| 96 |
+
self.api_key = api_key
|
| 97 |
+
self.client = httpx.AsyncClient()
|
| 98 |
+
|
| 99 |
+
async def transcribe(self, audio_file: bytes) -> str:
|
| 100 |
+
"""
|
| 101 |
+
调用智谱 ASR API 进行语音识别
|
| 102 |
+
|
| 103 |
+
参数:
|
| 104 |
+
audio_file: 音频文件字节流
|
| 105 |
+
|
| 106 |
+
返回:
|
| 107 |
+
转写后的文本内容
|
| 108 |
+
|
| 109 |
+
异常:
|
| 110 |
+
ASRServiceError: API 调用失败或识别失败
|
| 111 |
+
"""
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 3. SemanticParserService
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
class SemanticParserService:
|
| 118 |
+
def __init__(self, api_key: str):
|
| 119 |
+
self.api_key = api_key
|
| 120 |
+
self.client = httpx.AsyncClient()
|
| 121 |
+
self.system_prompt = (
|
| 122 |
+
"你是一个数据转换器。请将文本解析为 JSON 格式。"
|
| 123 |
+
"维度包括:1.情绪(type,intensity,keywords); "
|
| 124 |
+
"2.灵感(core_idea,tags,category); "
|
| 125 |
+
"3.待办(task,time,location)。"
|
| 126 |
+
"必须严格遵循 JSON 格式返回。"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
async def parse(self, text: str) -> ParsedData:
|
| 130 |
+
"""
|
| 131 |
+
调用 GLM-4-Flash API 进行语义解析
|
| 132 |
+
|
| 133 |
+
参数:
|
| 134 |
+
text: 待解析的文本内容
|
| 135 |
+
|
| 136 |
+
返回:
|
| 137 |
+
ParsedData 对象,包含 mood, inspirations, todos
|
| 138 |
+
|
| 139 |
+
异常:
|
| 140 |
+
SemanticParserError: API 调用失败或解析失败
|
| 141 |
+
"""
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
### 4. StorageService
|
| 145 |
+
|
| 146 |
+
```python
|
| 147 |
+
class StorageService:
|
| 148 |
+
def __init__(self, data_dir: str):
|
| 149 |
+
self.data_dir = Path(data_dir)
|
| 150 |
+
self.records_file = self.data_dir / "records.json"
|
| 151 |
+
self.moods_file = self.data_dir / "moods.json"
|
| 152 |
+
self.inspirations_file = self.data_dir / "inspirations.json"
|
| 153 |
+
self.todos_file = self.data_dir / "todos.json"
|
| 154 |
+
|
| 155 |
+
def save_record(self, record: RecordData) -> str:
|
| 156 |
+
"""
|
| 157 |
+
保存完整记录到 records.json
|
| 158 |
+
|
| 159 |
+
参数:
|
| 160 |
+
record: 记录数据对象
|
| 161 |
+
|
| 162 |
+
返回:
|
| 163 |
+
生成的唯一 record_id
|
| 164 |
+
|
| 165 |
+
异常:
|
| 166 |
+
StorageError: 文件写入失败
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
def append_mood(self, mood: MoodData, record_id: str) -> None:
|
| 170 |
+
"""追加情绪数据到 moods.json"""
|
| 171 |
+
|
| 172 |
+
def append_inspirations(self, inspirations: List[InspirationData], record_id: str) -> None:
|
| 173 |
+
"""追加灵感数据到 inspirations.json"""
|
| 174 |
+
|
| 175 |
+
def append_todos(self, todos: List[TodoData], record_id: str) -> None:
|
| 176 |
+
"""追加待办数据到 todos.json"""
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
## Data Models
|
| 180 |
+
|
| 181 |
+
### 核心数据结构
|
| 182 |
+
|
| 183 |
+
```python
|
| 184 |
+
class MoodData(BaseModel):
|
| 185 |
+
type: Optional[str] = None
|
| 186 |
+
intensity: Optional[int] = Field(None, ge=1, le=10)
|
| 187 |
+
keywords: List[str] = []
|
| 188 |
+
|
| 189 |
+
class InspirationData(BaseModel):
|
| 190 |
+
core_idea: str = Field(..., max_length=20)
|
| 191 |
+
tags: List[str] = Field(default_factory=list, max_items=5)
|
| 192 |
+
category: Literal["工作", "生活", "学习", "创意"]
|
| 193 |
+
|
| 194 |
+
class TodoData(BaseModel):
|
| 195 |
+
task: str
|
| 196 |
+
time: Optional[str] = None
|
| 197 |
+
location: Optional[str] = None
|
| 198 |
+
status: str = "pending"
|
| 199 |
+
|
| 200 |
+
class ParsedData(BaseModel):
|
| 201 |
+
mood: Optional[MoodData] = None
|
| 202 |
+
inspirations: List[InspirationData] = []
|
| 203 |
+
todos: List[TodoData] = []
|
| 204 |
+
|
| 205 |
+
class RecordData(BaseModel):
|
| 206 |
+
record_id: str
|
| 207 |
+
timestamp: str
|
| 208 |
+
input_type: Literal["audio", "text"]
|
| 209 |
+
original_text: str
|
| 210 |
+
parsed_data: ParsedData
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### 存储格式
|
| 214 |
+
|
| 215 |
+
**records.json**:
|
| 216 |
+
```json
|
| 217 |
+
[
|
| 218 |
+
{
|
| 219 |
+
"record_id": "uuid-string",
|
| 220 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
| 221 |
+
"input_type": "audio",
|
| 222 |
+
"original_text": "转写后的文本",
|
| 223 |
+
"parsed_data": {
|
| 224 |
+
"mood": {...},
|
| 225 |
+
"inspirations": [...],
|
| 226 |
+
"todos": [...]
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
]
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
**moods.json**:
|
| 233 |
+
```json
|
| 234 |
+
[
|
| 235 |
+
{
|
| 236 |
+
"record_id": "uuid-string",
|
| 237 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
| 238 |
+
"type": "开心",
|
| 239 |
+
"intensity": 8,
|
| 240 |
+
"keywords": ["愉快", "放松"]
|
| 241 |
+
}
|
| 242 |
+
]
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
**inspirations.json**:
|
| 246 |
+
```json
|
| 247 |
+
[
|
| 248 |
+
{
|
| 249 |
+
"record_id": "uuid-string",
|
| 250 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
| 251 |
+
"core_idea": "新的项目想法",
|
| 252 |
+
"tags": ["创新", "技术"],
|
| 253 |
+
"category": "工作"
|
| 254 |
+
}
|
| 255 |
+
]
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
**todos.json**:
|
| 259 |
+
```json
|
| 260 |
+
[
|
| 261 |
+
{
|
| 262 |
+
"record_id": "uuid-string",
|
| 263 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
| 264 |
+
"task": "完成报告",
|
| 265 |
+
"time": "明天下午",
|
| 266 |
+
"location": "办公室",
|
| 267 |
+
"status": "pending"
|
| 268 |
+
}
|
| 269 |
+
]
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
## Correctness Properties
|
| 274 |
+
|
| 275 |
+
属性(Property)是关于系统行为的特征或规则,应该在所有有效执行中保持为真。属性是人类可读规范和机器可验证正确性保证之间的桥梁。
|
| 276 |
+
|
| 277 |
+
### Property 1: 音频格式验证
|
| 278 |
+
*For any* 提交的文件,如果文件扩展名是 mp3、wav 或 m4a,系统应该接受该文件;如果是其他格式,系统应该拒绝并返回错误。
|
| 279 |
+
**Validates: Requirements 1.1**
|
| 280 |
+
|
| 281 |
+
### Property 2: UTF-8 文本接受
|
| 282 |
+
*For any* UTF-8 编码的文本字符串(包括中文、emoji、特殊字符),系统应该正确接受并处理。
|
| 283 |
+
**Validates: Requirements 1.2**
|
| 284 |
+
|
| 285 |
+
### Property 3: 无效输入错误处理
|
| 286 |
+
*For any* 空输入或格式无效的输入,系统应该返回包含 error 字段的 JSON 响应,而不是崩溃或返回成功状态。
|
| 287 |
+
**Validates: Requirements 1.3, 9.1**
|
| 288 |
+
|
| 289 |
+
### Property 4: 解析结果结构完整性
|
| 290 |
+
*For any* 成功的语义解析结果,返回的 JSON 应该包含 mood、inspirations、todos 三个字段,即使某些字段为空值或空数组。
|
| 291 |
+
**Validates: Requirements 3.3**
|
| 292 |
+
|
| 293 |
+
### Property 5: 缺失维度处理
|
| 294 |
+
*For any* 不包含特定维度信息的文本,解析结果中该维度应该返回 null(对于 mood)或空数组(对于 inspirations 和 todos)。
|
| 295 |
+
**Validates: Requirements 3.4**
|
| 296 |
+
|
| 297 |
+
### Property 6: 情绪数据结构验证
|
| 298 |
+
*For any* 解析出的情绪数据,应该包含 type(字符串)、intensity(1-10 的整数)、keywords(字符串数组)三个字段,且 intensity 必须在有效范围内。
|
| 299 |
+
**Validates: Requirements 4.1, 4.2, 4.3**
|
| 300 |
+
|
| 301 |
+
### Property 7: 灵感数据结构验证
|
| 302 |
+
*For any* 解析出的灵感数据,应该包含 core_idea(长度 ≤ 20)、tags(数组长度 ≤ 5)、category(枚举值:工作/生活/学习/创意)三个字段,且所有约束都被满足。
|
| 303 |
+
**Validates: Requirements 5.1, 5.2, 5.3**
|
| 304 |
+
|
| 305 |
+
### Property 8: 待办数据结构验证
|
| 306 |
+
*For any* 解析出的待办数据,应该包含 task(必需)、time(可选)、location(可选)、status(默认为 "pending")四个字段。
|
| 307 |
+
**Validates: Requirements 6.1, 6.2, 6.3, 6.4**
|
| 308 |
+
|
| 309 |
+
### Property 9: 数据持久化完整性
|
| 310 |
+
*For any* 成功处理的记录,应该在 records.json 中保存完整记录,并且如果包含情绪/灵感/待办数据,应该同时追加到对应的 moods.json、inspirations.json、todos.json 文件中。
|
| 311 |
+
**Validates: Requirements 7.1, 7.2, 7.3, 7.4**
|
| 312 |
+
|
| 313 |
+
### Property 10: 文件初始化
|
| 314 |
+
*For any* 不存在的 JSON 文件,当首次写入时,系统应该创建该文件并初始化为空数组 `[]`。
|
| 315 |
+
**Validates: Requirements 7.5**
|
| 316 |
+
|
| 317 |
+
### Property 11: 唯一 ID 生成
|
| 318 |
+
*For any* 两条不同的记录,生成的 record_id 应该是唯一的(不重复)。
|
| 319 |
+
**Validates: Requirements 7.7**
|
| 320 |
+
|
| 321 |
+
### Property 12: 成功响应格式
|
| 322 |
+
*For any* 成功处理的请求,HTTP 响应应该返回 200 状态码,并且响应 JSON 包含 record_id、timestamp、mood、inspirations、todos 字段。
|
| 323 |
+
**Validates: Requirements 8.4, 8.6**
|
| 324 |
+
|
| 325 |
+
### Property 13: 错误响应格式
|
| 326 |
+
*For any* 处理失败的请求,HTTP 响应应该返回适当的错误状态码(400 或 500),并且响应 JSON 包含 error 字段,描述具体错误信息。
|
| 327 |
+
**Validates: Requirements 8.5, 9.1, 9.3**
|
| 328 |
+
|
| 329 |
+
### Property 14: 错误日志记录
|
| 330 |
+
*For any* 系统发生的错误,应该在日志文件中记录该错误,包含时间戳和错误堆栈信息。
|
| 331 |
+
**Validates: Requirements 9.5**
|
| 332 |
+
|
| 333 |
+
### Property 15: 敏感信息保护
|
| 334 |
+
*For any* 日志输出,不应该包含敏感信息(如 API 密钥、用户密码等)。
|
| 335 |
+
**Validates: Requirements 10.5**
|
| 336 |
+
|
| 337 |
+
## Error Handling
|
| 338 |
+
|
| 339 |
+
### 错误分类
|
| 340 |
+
|
| 341 |
+
**1. 输入验证错误(HTTP 400)**:
|
| 342 |
+
- 音频文件格式不支持
|
| 343 |
+
- 音频文件大小超过限制
|
| 344 |
+
- 文本内容为空
|
| 345 |
+
- 请求格式错误(既没有 audio 也没有 text)
|
| 346 |
+
|
| 347 |
+
**2. 外部服务错误(HTTP 500)**:
|
| 348 |
+
- 智谱 ASR API 调用失败
|
| 349 |
+
- GLM-4-Flash API 调用失败
|
| 350 |
+
- API 返回非预期格式
|
| 351 |
+
|
| 352 |
+
**3. 存储错误(HTTP 500)**:
|
| 353 |
+
- JSON 文件写入失败
|
| 354 |
+
- 磁盘空间不足
|
| 355 |
+
- 文件权限错误
|
| 356 |
+
|
| 357 |
+
**4. 配置错误(启动时失败)**:
|
| 358 |
+
- API 密钥缺失
|
| 359 |
+
- 数据目录不可访问
|
| 360 |
+
- 必需配置项缺失
|
| 361 |
+
|
| 362 |
+
### 错误处理策略
|
| 363 |
+
|
| 364 |
+
```python
|
| 365 |
+
class APIError(Exception):
|
| 366 |
+
"""API 层错误基类"""
|
| 367 |
+
def __init__(self, message: str, status_code: int):
|
| 368 |
+
self.message = message
|
| 369 |
+
self.status_code = status_code
|
| 370 |
+
|
| 371 |
+
class ASRServiceError(APIError):
|
| 372 |
+
"""ASR 服务错误"""
|
| 373 |
+
def __init__(self, message: str = "语音识别服务不可用"):
|
| 374 |
+
super().__init__(message, 500)
|
| 375 |
+
|
| 376 |
+
class SemanticParserError(APIError):
|
| 377 |
+
"""语义解析服务错误"""
|
| 378 |
+
def __init__(self, message: str = "语义解析服务不可用"):
|
| 379 |
+
super().__init__(message, 500)
|
| 380 |
+
|
| 381 |
+
class StorageError(APIError):
|
| 382 |
+
"""存储错误"""
|
| 383 |
+
def __init__(self, message: str = "数据存储失败"):
|
| 384 |
+
super().__init__(message, 500)
|
| 385 |
+
|
| 386 |
+
class ValidationError(APIError):
|
| 387 |
+
"""输入验证错误"""
|
| 388 |
+
def __init__(self, message: str):
|
| 389 |
+
super().__init__(message, 400)
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
### 错误响应格式
|
| 393 |
+
|
| 394 |
+
```json
|
| 395 |
+
{
|
| 396 |
+
"error": "具体错误描述",
|
| 397 |
+
"detail": "详细错误信息(可选)",
|
| 398 |
+
"timestamp": "2024-01-01T12:00:00Z"
|
| 399 |
+
}
|
| 400 |
+
```
|
| 401 |
+
|
| 402 |
+
### 日志记录
|
| 403 |
+
|
| 404 |
+
使用 Python logging 模块:
|
| 405 |
+
- **INFO**: 正常请求处理流程
|
| 406 |
+
- **WARNING**: 可恢复的异常情况(如 API 重试)
|
| 407 |
+
- **ERROR**: 错误情况,包含完整堆栈信息
|
| 408 |
+
- **DEBUG**: 详细调试信息(开发环境)
|
| 409 |
+
|
| 410 |
+
日志格式:
|
| 411 |
+
```
|
| 412 |
+
[2024-01-01 12:00:00] [ERROR] [request_id: xxx] ASR API call failed: Connection timeout
|
| 413 |
+
Traceback: ...
|
| 414 |
+
```
|
| 415 |
+
|
| 416 |
+
## Testing Strategy
|
| 417 |
+
|
| 418 |
+
本系统采用双重测试策略:单元测试和基于属性的测试(Property-Based Testing)。
|
| 419 |
+
|
| 420 |
+
### 单元测试
|
| 421 |
+
|
| 422 |
+
单元测试用于验证特定示例、边缘情况和错误条件:
|
| 423 |
+
|
| 424 |
+
**测试范围**:
|
| 425 |
+
- API 端点的请求/响应处理
|
| 426 |
+
- 各服务类的 mock 测试(模拟外部 API)
|
| 427 |
+
- 数据模型的验证逻辑
|
| 428 |
+
- 错误处理流程
|
| 429 |
+
- 配置加载和验证
|
| 430 |
+
|
| 431 |
+
**示例测试用例**:
|
| 432 |
+
- 测试 POST /api/process 端点存在
|
| 433 |
+
- 测试接受 multipart/form-data 格式
|
| 434 |
+
- 测试接受 application/json 格式
|
| 435 |
+
- 测试 ASR API 调用失败时的错误处理
|
| 436 |
+
- 测试 GLM-4-Flash API 调用失败时的错误处理
|
| 437 |
+
- 测试文件写入失败时的错误处理
|
| 438 |
+
- 测试配置缺失时启动失败
|
| 439 |
+
- 测试空音频识别的边缘情况
|
| 440 |
+
- 测试无情绪信息文本的边缘情况
|
| 441 |
+
- 测试无灵感信息文本的边缘情况
|
| 442 |
+
- 测试无待办信息文本的边缘情况
|
| 443 |
+
|
| 444 |
+
### 基于属性的测试(Property-Based Testing)
|
| 445 |
+
|
| 446 |
+
基于属性的测试用于验证通用属性在所有输入下都成立。
|
| 447 |
+
|
| 448 |
+
**测试库**: 使用 `hypothesis` 库(Python 的 PBT 框架)
|
| 449 |
+
|
| 450 |
+
**配置**:
|
| 451 |
+
- 每个属性测试运行最少 100 次迭代
|
| 452 |
+
- 每个测试必须引用设计文档中的属性编号
|
| 453 |
+
- 标签格式:`# Feature: voice-text-processor, Property N: [property text]`
|
| 454 |
+
|
| 455 |
+
**属性测试覆盖**:
|
| 456 |
+
- Property 1: 音频格式验证
|
| 457 |
+
- Property 2: UTF-8 文本接受
|
| 458 |
+
- Property 3: 无效输入错误处理
|
| 459 |
+
- Property 4: 解析结果结构完整性
|
| 460 |
+
- Property 5: 缺失维度处理
|
| 461 |
+
- Property 6: 情绪数据结构验证
|
| 462 |
+
- Property 7: 灵感数据结构验证
|
| 463 |
+
- Property 8: 待办数据结构验证
|
| 464 |
+
- Property 9: 数据持久化完整性
|
| 465 |
+
- Property 10: 文件初始化
|
| 466 |
+
- Property 11: 唯一 ID 生成
|
| 467 |
+
- Property 12: 成功响应格式
|
| 468 |
+
- Property 13: 错误响应格式
|
| 469 |
+
- Property 14: 错误日志记录
|
| 470 |
+
- Property 15: 敏感信息保护
|
| 471 |
+
|
| 472 |
+
**测试策略**:
|
| 473 |
+
- 使用 hypothesis 生成随机输入(文件名、文本、数据结构)
|
| 474 |
+
- 使用 pytest-mock 模拟外部 API 调用
|
| 475 |
+
- 使用临时文件系统进行存储测试
|
| 476 |
+
- 验证所有属性在随机输入下都成立
|
| 477 |
+
|
| 478 |
+
**示例属性测试**:
|
| 479 |
+
```python
|
| 480 |
+
from hypothesis import given, strategies as st
|
| 481 |
+
import pytest
|
| 482 |
+
|
| 483 |
+
@given(st.text(min_size=1))
|
| 484 |
+
def test_property_2_utf8_text_acceptance(text):
|
| 485 |
+
"""
|
| 486 |
+
Feature: voice-text-processor, Property 2: UTF-8 文本接受
|
| 487 |
+
For any UTF-8 encoded text string, the system should accept and process it.
|
| 488 |
+
"""
|
| 489 |
+
response = client.post("/api/process", json={"text": text})
|
| 490 |
+
assert response.status_code in [200, 500] # 接受输入,可能解析失败但不应拒绝
|
| 491 |
+
|
| 492 |
+
@given(st.lists(st.text(), min_size=1, max_size=10))
|
| 493 |
+
def test_property_11_unique_id_generation(texts):
|
| 494 |
+
"""
|
| 495 |
+
Feature: voice-text-processor, Property 11: 唯一 ID 生成
|
| 496 |
+
For any two different records, the generated record_ids should be unique.
|
| 497 |
+
"""
|
| 498 |
+
record_ids = []
|
| 499 |
+
for text in texts:
|
| 500 |
+
response = client.post("/api/process", json={"text": text})
|
| 501 |
+
if response.status_code == 200:
|
| 502 |
+
record_ids.append(response.json()["record_id"])
|
| 503 |
+
|
| 504 |
+
# 所有 ID 应该唯一
|
| 505 |
+
assert len(record_ids) == len(set(record_ids))
|
| 506 |
+
```
|
| 507 |
+
|
| 508 |
+
### 测试覆盖目标
|
| 509 |
+
|
| 510 |
+
- 代码覆盖率:≥ 80%
|
| 511 |
+
- 属性测试:覆盖所有 15 个正确性属性
|
| 512 |
+
- 单元测试:覆盖所有边缘情况和错误路径
|
| 513 |
+
- 集成测试:端到端流程测试(音频 → 转写 → 解析 → 存储)
|
| 514 |
+
|
.kiro/specs/voice-text-processor/requirements.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
这是一个治愈系记录助手的后端核心模块。系统接收语音录音或文字输入,通过智谱 API 进行语音转写和语义解析,输出包含情绪、灵感、待办的结构化 JSON 数据,并持久化到本地文件系统。
|
| 6 |
+
|
| 7 |
+
## Glossary
|
| 8 |
+
|
| 9 |
+
- **System**: 治愈系记录助手后端系统
|
| 10 |
+
- **ASR_Service**: 智谱 API 语音识别服务
|
| 11 |
+
- **Semantic_Parser**: GLM-4-Flash 语义解析引擎
|
| 12 |
+
- **Storage_Manager**: 本地 JSON 文件存储管理器
|
| 13 |
+
- **Record**: 用户输入的单次记录(语音或文字)
|
| 14 |
+
- **Mood**: 情绪数据结构(type, intensity, keywords)
|
| 15 |
+
- **Inspiration**: 灵感数据结构(core_idea, tags, category)
|
| 16 |
+
- **Todo**: 待办事项数据结构(task, time, location, status)
|
| 17 |
+
|
| 18 |
+
## Requirements
|
| 19 |
+
|
| 20 |
+
### Requirement 1: 接收用户输入
|
| 21 |
+
|
| 22 |
+
**User Story:** 作为用户,我想要提交语音录音或文字内容,以便系统能够处理我的记录。
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN 用户提交音频文件,THE System SHALL 接受常见音频格式(mp3, wav, m4a)
|
| 27 |
+
2. WHEN 用户提交文字内容,THE System SHALL 接受 UTF-8 编码的文本字符串
|
| 28 |
+
3. WHEN 输入数据为空或格式无效,THE System SHALL 返回明确的错误信息
|
| 29 |
+
4. WHEN 音频文件大小超过 10MB,THE System SHALL 拒绝处理并返回文件过大错误
|
| 30 |
+
|
| 31 |
+
### Requirement 2: 语音转文字
|
| 32 |
+
|
| 33 |
+
**User Story:** 作为用户,我想要系统将我的语音录音转换为文字,以便进行后续的语义分析。
|
| 34 |
+
|
| 35 |
+
#### Acceptance Criteria
|
| 36 |
+
|
| 37 |
+
1. WHEN 接收到音频文件,THE ASR_Service SHALL 调用智谱 ASR API 进行语音识别
|
| 38 |
+
2. WHEN 语音识别成功,THE ASR_Service SHALL 返回转写后的文本内容
|
| 39 |
+
3. IF 智谱 API 调用失败,THEN THE System SHALL 记录错误日志并返回转写失败错误
|
| 40 |
+
4. WHEN 音频内容无法识别,THE ASR_Service SHALL 返回空文本并标记为识别失败
|
| 41 |
+
|
| 42 |
+
### Requirement 3: 语义解析
|
| 43 |
+
|
| 44 |
+
**User Story:** 作为用户,我想要系统从我的文本中提取情绪、灵感和待办事项,以便获得结构化的记录数据。
|
| 45 |
+
|
| 46 |
+
#### Acceptance Criteria
|
| 47 |
+
|
| 48 |
+
1. WHEN 接收到文本内容,THE Semantic_Parser SHALL 调用 GLM-4-Flash API 进行语义解析
|
| 49 |
+
2. WHEN 调用 GLM-4-Flash,THE System SHALL 使用指定的 System Prompt:"你是一个数据转换器。请将文本解析为 JSON 格式。维度包括:1.情绪(type,intensity,keywords); 2.灵感(core_idea,tags,category); 3.待办(task,time,location)。必须严格遵循 JSON 格式返回。"
|
| 50 |
+
3. WHEN 解析成功,THE Semantic_Parser SHALL 返回包含 mood、inspirations、todos 的 JSON 结构
|
| 51 |
+
4. WHEN 文本中不包含某个维度的信息,THE Semantic_Parser SHALL 返回该维度的空值或空数组
|
| 52 |
+
5. IF GLM-4-Flash API 调用失败,THEN THE System SHALL 记录错误日志并返回解析失败错误
|
| 53 |
+
|
| 54 |
+
### Requirement 4: 情绪数据提取
|
| 55 |
+
|
| 56 |
+
**User Story:** 作为用户,我想要系统识别我的情绪状态,以便追踪我的情绪变化。
|
| 57 |
+
|
| 58 |
+
#### Acceptance Criteria
|
| 59 |
+
|
| 60 |
+
1. WHEN 解析情绪数据,THE Semantic_Parser SHALL 提取情绪类型(type)
|
| 61 |
+
2. WHEN 解析情绪数据,THE Semantic_Parser SHALL 提取情绪强度(intensity),范围为 1-10 的整数
|
| 62 |
+
3. WHEN 解析情绪数据,THE Semantic_Parser SHALL 提取情绪关键词(keywords),以字符串数组形式返回
|
| 63 |
+
4. WHEN 文本中不包含明确的情绪信息,THE Semantic_Parser SHALL 返回 null 或默认值
|
| 64 |
+
|
| 65 |
+
### Requirement 5: 灵感数据提取
|
| 66 |
+
|
| 67 |
+
**User Story:** 作为用户,我想要系统捕捉我的灵感想法,以便日后回顾和整理。
|
| 68 |
+
|
| 69 |
+
#### Acceptance Criteria
|
| 70 |
+
|
| 71 |
+
1. WHEN 解析灵感数据,THE Semantic_Parser SHALL 提取核心观点(core_idea),长度不超过 20 个字符
|
| 72 |
+
2. WHEN 解析灵感数据,THE Semantic_Parser SHALL 提取标签(tags),以字符串数组形式返回,最多 5 个标签
|
| 73 |
+
3. WHEN 解析灵感数据,THE Semantic_Parser SHALL 提取分类(category),值为"工作"、"生活"、"学习"或"创意"之一
|
| 74 |
+
4. WHEN 文本中包含多个灵感,THE Semantic_Parser SHALL 返回灵感数组
|
| 75 |
+
5. WHEN 文本中不包含灵感信息,THE Semantic_Parser SHALL 返回空数组
|
| 76 |
+
|
| 77 |
+
### Requirement 6: 待办事项提取
|
| 78 |
+
|
| 79 |
+
**User Story:** 作为用户,我想要系统识别我提到的待办事项,以便自动创建任务清单。
|
| 80 |
+
|
| 81 |
+
#### Acceptance Criteria
|
| 82 |
+
|
| 83 |
+
1. WHEN 解析待办数据,THE Semantic_Parser SHALL 提取任务描述(task)
|
| 84 |
+
2. WHEN 解析待办数据,THE Semantic_Parser SHALL 提取时间信息(time),保留原始表达(如"明晚"、"下周三")
|
| 85 |
+
3. WHEN 解析待办数据,THE Semantic_Parser SHALL 提取地点信息(location)
|
| 86 |
+
4. WHEN 创建新待办事项,THE System SHALL 设置状态(status)为"pending"
|
| 87 |
+
5. WHEN 文本中包含多个待办事项,THE Semantic_Parser SHALL 返回待办数组
|
| 88 |
+
6. WHEN 文本中不包含待办信息,THE Semantic_Parser SHALL 返回空数组
|
| 89 |
+
|
| 90 |
+
### Requirement 7: 数据持久化
|
| 91 |
+
|
| 92 |
+
**User Story:** 作为用户,我想要系统保存我的���录数据,以便日后查询和分析。
|
| 93 |
+
|
| 94 |
+
#### Acceptance Criteria
|
| 95 |
+
|
| 96 |
+
1. WHEN 解析完成后,THE Storage_Manager SHALL 将完整记录保存到 records.json 文件
|
| 97 |
+
2. WHEN 提取到情绪数据,THE Storage_Manager SHALL 将情绪信息追加到 moods.json 文件
|
| 98 |
+
3. WHEN 提取到灵感数据,THE Storage_Manager SHALL 将灵感信息追加到 inspirations.json 文件
|
| 99 |
+
4. WHEN 提取到待办数据,THE Storage_Manager SHALL 将待办信息追加到 todos.json 文件
|
| 100 |
+
5. WHEN JSON 文件不存在,THE Storage_Manager SHALL 创建新文件并初始化为空数组
|
| 101 |
+
6. WHEN 写入文件失败,THE System SHALL 记录错误日志并返回存储失败错误
|
| 102 |
+
7. WHEN 保存记录时,THE System SHALL 为每条记录生成唯一 ID 和时间戳
|
| 103 |
+
|
| 104 |
+
### Requirement 8: API 接口设计
|
| 105 |
+
|
| 106 |
+
**User Story:** 作为前端开发者,我想要调用清晰的 REST API,以便集成后端功能。
|
| 107 |
+
|
| 108 |
+
#### Acceptance Criteria
|
| 109 |
+
|
| 110 |
+
1. THE System SHALL 提供 POST /api/process 接口接收用户输入
|
| 111 |
+
2. WHEN 请求包含音频文件,THE System SHALL 接受 multipart/form-data 格式
|
| 112 |
+
3. WHEN 请求包含文字内容,THE System SHALL 接受 application/json 格式
|
| 113 |
+
4. WHEN 处理成功,THE System SHALL 返回 HTTP 200 状态码和结构化 JSON 响应
|
| 114 |
+
5. WHEN 处理失败,THE System SHALL 返回适当的 HTTP 错误状态码(400/500)和错误信息
|
| 115 |
+
6. THE System SHALL 在响应中包含 record_id 和 timestamp 字段
|
| 116 |
+
|
| 117 |
+
### Requirement 9: 错误处理
|
| 118 |
+
|
| 119 |
+
**User Story:** 作为用户,我想要在系统出错时获得清晰的错误提示,以便了解问题所在。
|
| 120 |
+
|
| 121 |
+
#### Acceptance Criteria
|
| 122 |
+
|
| 123 |
+
1. WHEN 任何步骤发生错误,THE System SHALL 返回包含 error 字段的 JSON 响应
|
| 124 |
+
2. WHEN 智谱 API 调用失败,THE System SHALL 返回"语音识别服务不可用"或"语义解析服务不可用"错误
|
| 125 |
+
3. WHEN 输入验证失败,THE System SHALL 返回具体的验证错误信息
|
| 126 |
+
4. WHEN 文件操作失败,THE System SHALL 返回"数据存储失败"错误
|
| 127 |
+
5. THE System SHALL 记录所有错误到日志文件,包含时间戳和错误堆栈
|
| 128 |
+
|
| 129 |
+
### Requirement 10: 配置管理
|
| 130 |
+
|
| 131 |
+
**User Story:** 作为系统管理员,我想要配置 API 密钥和系统参数,以便灵活部署系统。
|
| 132 |
+
|
| 133 |
+
#### Acceptance Criteria
|
| 134 |
+
|
| 135 |
+
1. THE System SHALL 从环境变量或配置文件读取智谱 API 密钥
|
| 136 |
+
2. THE System SHALL 支持配置数据文件存储路径
|
| 137 |
+
3. THE System SHALL 支持配置音频文件大小限制
|
| 138 |
+
4. WHEN 必需的配置项缺失,THE System SHALL 在启动时报错并拒绝启动
|
| 139 |
+
5. THE System SHALL 不在日志中输出敏感信息(如 API 密钥)
|
.kiro/specs/voice-text-processor/tasks.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan: Voice Text Processor
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
本实现计划将语音文本处理系统分解为离散的编码步骤。实现顺序遵循从核心基础设施到业务逻辑,再到集成测试的渐进式方法。每个任务都引用具体的需求条款,确保完整的需求覆盖。
|
| 6 |
+
|
| 7 |
+
## Tasks
|
| 8 |
+
|
| 9 |
+
- [x] 1. 设置项目结构和核心配置
|
| 10 |
+
- 创建项目目录结构(app/, tests/, data/)
|
| 11 |
+
- 设置 FastAPI 应用和基础配置
|
| 12 |
+
- 实现配置管理模块(从环境变量读取 API 密钥、数据路径、文件大小限制)
|
| 13 |
+
- 配置日志系统(格式、级别、文件输出)
|
| 14 |
+
- 添加启动时配置验证(缺失必需配置时拒绝启动)
|
| 15 |
+
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
|
| 16 |
+
|
| 17 |
+
- [x] 2. 实现数据模型和验证
|
| 18 |
+
- [x] 2.1 创建 Pydantic 数据模型
|
| 19 |
+
- 实现 MoodData 模型(type, intensity 1-10, keywords)
|
| 20 |
+
- 实现 InspirationData 模型(core_idea ≤20 字符, tags ≤5, category 枚举)
|
| 21 |
+
- 实现 TodoData 模型(task, time, location, status 默认 "pending")
|
| 22 |
+
- 实现 ParsedData 模型(mood, inspirations, todos)
|
| 23 |
+
- 实现 RecordData 模型(record_id, timestamp, input_type, original_text, parsed_data)
|
| 24 |
+
- 实现 ProcessResponse 模型(record_id, timestamp, mood, inspirations, todos, error)
|
| 25 |
+
- _Requirements: 4.1, 4.2, 4.3, 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4_
|
| 26 |
+
|
| 27 |
+
- [x] 2.2 编写数据模型属性测试
|
| 28 |
+
- **Property 6: 情绪数据结构验证**
|
| 29 |
+
- **Validates: Requirements 4.1, 4.2, 4.3**
|
| 30 |
+
|
| 31 |
+
- [x] 2.3 编写数据模型属性测试
|
| 32 |
+
- **Property 7: 灵感数据结构验证**
|
| 33 |
+
- **Validates: Requirements 5.1, 5.2, 5.3**
|
| 34 |
+
|
| 35 |
+
- [x] 2.4 编写数据模型属性测试
|
| 36 |
+
- **Property 8: 待办数据结构验证**
|
| 37 |
+
- **Validates: Requirements 6.1, 6.2, 6.3, 6.4**
|
| 38 |
+
|
| 39 |
+
- [x] 3. 实现存储服务(StorageService)
|
| 40 |
+
- [x] 3.1 实现 JSON 文件存储管理器
|
| 41 |
+
- 实现 save_record 方法(保存到 records.json,生成唯一 UUID)
|
| 42 |
+
- 实现 append_mood 方法(追加到 moods.json)
|
| 43 |
+
- 实现 append_inspirations 方法(追加到 inspirations.json)
|
| 44 |
+
- 实现 append_todos 方法(追加到 todos.json)
|
| 45 |
+
- 实现文件初始化逻辑(不存在时创建并初始化为空数组)
|
| 46 |
+
- 实现错误处理(文件写入失败时抛出 StorageError)
|
| 47 |
+
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7_
|
| 48 |
+
|
| 49 |
+
- [x] 3.2 编写存储服务属性测试
|
| 50 |
+
- **Property 9: 数据持久化完整性**
|
| 51 |
+
- **Validates: Requirements 7.1, 7.2, 7.3, 7.4**
|
| 52 |
+
|
| 53 |
+
- [x] 3.3 编写存储服务属性测试
|
| 54 |
+
- **Property 10: 文件初始化**
|
| 55 |
+
- **Validates: Requirements 7.5**
|
| 56 |
+
|
| 57 |
+
- [x] 3.4 编写存储服务属性测试
|
| 58 |
+
- **Property 11: 唯一 ID 生成**
|
| 59 |
+
- **Validates: Requirements 7.7**
|
| 60 |
+
|
| 61 |
+
- [x] 3.5 编写存储服务单元测试
|
| 62 |
+
- 测试文件写入失败的错误处理
|
| 63 |
+
- 测试并发写入的安全性
|
| 64 |
+
- _Requirements: 7.6_
|
| 65 |
+
|
| 66 |
+
- [x] 4. 检查点 - 确保存储层测试通过
|
| 67 |
+
- 确保所有测试通过,如有问题请询问用户。
|
| 68 |
+
|
| 69 |
+
- [x] 5. 实现 ASR 服务(ASRService)
|
| 70 |
+
- [x] 5.1 实现语音识别服务
|
| 71 |
+
- 创建 ASRService 类,初始化 httpx.AsyncClient
|
| 72 |
+
- 实现 transcribe 方法(调用智谱 ASR API)
|
| 73 |
+
- 处理 API 响应,提取转写文本
|
| 74 |
+
- 实现错误处理(API 调用失败时抛出 ASRServiceError)
|
| 75 |
+
- 处理空识别结果(返回空字符串并标记)
|
| 76 |
+
- 记录错误日志(包含时间戳和堆栈)
|
| 77 |
+
- _Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5_
|
| 78 |
+
|
| 79 |
+
- [x] 5.2 编写 ASR 服务单元测试
|
| 80 |
+
- 测试 API 调用成功场景(使用 mock)
|
| 81 |
+
- 测试 API 调用失败场景(使用 mock)
|
| 82 |
+
- 测试空识别结果的边缘情况
|
| 83 |
+
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
| 84 |
+
|
| 85 |
+
- [x] 6. 实现语义解析服务(SemanticParserService)
|
| 86 |
+
- [x] 6.1 实现语义解析服务
|
| 87 |
+
- 创建 SemanticParserService 类,初始化 httpx.AsyncClient
|
| 88 |
+
- 配置 System Prompt(数据转换器提示词)
|
| 89 |
+
- 实现 parse 方法(调用 GLM-4-Flash API)
|
| 90 |
+
- 解析 API 返回的 JSON 结构
|
| 91 |
+
- 处理缺失维度(返回 null 或空数组)
|
| 92 |
+
- 实现错误处理(API 调用失败时抛出 SemanticParserError)
|
| 93 |
+
- 记录错误日志(包含时间戳和堆栈)
|
| 94 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 9.2, 9.5_
|
| 95 |
+
|
| 96 |
+
- [x] 6.2 编写语义解析服务属性测试
|
| 97 |
+
- **Property 4: 解析结果结构完整性**
|
| 98 |
+
- **Validates: Requirements 3.3**
|
| 99 |
+
|
| 100 |
+
- [x] 6.3 编写语义解析服务属性测试
|
| 101 |
+
- **Property 5: 缺失维度处理**
|
| 102 |
+
- **Validates: Requirements 3.4**
|
| 103 |
+
|
| 104 |
+
- [x] 6.4 编写语义解析服务单元测试
|
| 105 |
+
- 测试 API 调用成功场景(使用 mock)
|
| 106 |
+
- 测试 API 调用失败场景(使用 mock)
|
| 107 |
+
- 测试 System Prompt 正确使用
|
| 108 |
+
- 测试无情绪信息文本的边缘情况
|
| 109 |
+
- 测试无灵感信息文本的边缘情况
|
| 110 |
+
- 测试无待办信息文本的边缘情况
|
| 111 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
| 112 |
+
|
| 113 |
+
- [x] 7. 检查点 - 确保服务层测试通过
|
| 114 |
+
- 确保所有测试通过,如有问题请询问用户。
|
| 115 |
+
|
| 116 |
+
- [x] 8. 实现 API 端点和请求处理
|
| 117 |
+
- [x] 8.1 实现 POST /api/process 端点
|
| 118 |
+
- 创建 FastAPI 路由处理器
|
| 119 |
+
- 实现输入验证(音频格式、文件大小、文本编码)
|
| 120 |
+
- 处理 multipart/form-data 格式(音频文件)
|
| 121 |
+
- 处理 application/json 格式(文本内容)
|
| 122 |
+
- 实现请求日志记录
|
| 123 |
+
- _Requirements: 1.1, 1.2, 8.1, 8.2, 8.3_
|
| 124 |
+
|
| 125 |
+
- [x] 8.2 实现业务逻辑编排
|
| 126 |
+
- 如果是音频输入,调用 ASRService.transcribe
|
| 127 |
+
- 调用 SemanticParserService.parse 进行语义解析
|
| 128 |
+
- 生成 record_id 和 timestamp
|
| 129 |
+
- 调用 StorageService 保存数据
|
| 130 |
+
- 构建成功响应(HTTP 200,包含 record_id, timestamp, mood, inspirations, todos)
|
| 131 |
+
- _Requirements: 7.7, 8.4, 8.6_
|
| 132 |
+
|
| 133 |
+
- [x] 8.3 实现错误处理和响应
|
| 134 |
+
- 捕获 ValidationError,返回 HTTP 400 和错误信息
|
| 135 |
+
- 捕获 ASRServiceError,返回 HTTP 500 和"语音识别服务不可用"
|
| 136 |
+
- 捕获 SemanticParserError,返回 HTTP 500 和"语义解析服务不可用"
|
| 137 |
+
- 捕获 StorageError,返回 HTTP 500 和"数据存储失败"
|
| 138 |
+
- 所有错误响应包含 error 字段和 timestamp
|
| 139 |
+
- 记录所有错误到日志文件
|
| 140 |
+
- _Requirements: 1.3, 8.5, 9.1, 9.2, 9.3, 9.4, 9.5_
|
| 141 |
+
|
| 142 |
+
- [x] 8.4 编写 API 端点属性测试
|
| 143 |
+
- **Property 1: 音频格式验证**
|
| 144 |
+
- **Validates: Requirements 1.1**
|
| 145 |
+
|
| 146 |
+
- [x] 8.5 编写 API 端点属性测试
|
| 147 |
+
- **Property 2: UTF-8 文本接受**
|
| 148 |
+
- **Validates: Requirements 1.2**
|
| 149 |
+
|
| 150 |
+
- [x] 8.6 编写 API 端点属性测试
|
| 151 |
+
- **Property 3: 无效输入错误处理**
|
| 152 |
+
- **Validates: Requirements 1.3, 9.1**
|
| 153 |
+
|
| 154 |
+
- [x] 8.7 编写 API 端点属性测试
|
| 155 |
+
- **Property 12: 成功响应格式**
|
| 156 |
+
- **Validates: Requirements 8.4, 8.6**
|
| 157 |
+
|
| 158 |
+
- [x] 8.8 编写 API 端点属性测试
|
| 159 |
+
- **Property 13: 错误响应格式**
|
| 160 |
+
- **Validates: Requirements 8.5, 9.1, 9.3**
|
| 161 |
+
|
| 162 |
+
- [x] 8.9 编写 API 端点单元测试
|
| 163 |
+
- 测试 POST /api/process 端点存在
|
| 164 |
+
- 测试接受 multipart/form-data 格式
|
| 165 |
+
- 测试接受 application/json 格式
|
| 166 |
+
- _Requirements: 8.1, 8.2, 8.3_
|
| 167 |
+
|
| 168 |
+
- [x] 9. 实现日志安全性和错误日志
|
| 169 |
+
- [x] 9.1 实现日志过滤器
|
| 170 |
+
- 创建日志过滤器,屏蔽敏感信息(API 密钥、密码等)
|
| 171 |
+
- 配置日志格式(包含 request_id, timestamp, level, message)
|
| 172 |
+
- 确保错误日志包含完整堆栈信息
|
| 173 |
+
- _Requirements: 9.5, 10.5_
|
| 174 |
+
|
| 175 |
+
- [x] 9.2 编写日志属性测试
|
| 176 |
+
- **Property 14: 错误日志记录**
|
| 177 |
+
- **Validates: Requirements 9.5**
|
| 178 |
+
|
| 179 |
+
- [-] 9.3 编写日志属性测试
|
| 180 |
+
- **Property 15: 敏感信息保护**
|
| 181 |
+
- **Validates: Requirements 10.5**
|
| 182 |
+
|
| 183 |
+
- [x] 10. 检查点 - 确保所有测试通过
|
| 184 |
+
- 确保所有测试通过,如有问题请询问用户。
|
| 185 |
+
|
| 186 |
+
- [x] 11. 集成测试
|
| 187 |
+
- [x] 11.1 编写端到端集成测试
|
| 188 |
+
- 测试完整流程:音频上传 → ASR → 语义解析 → 存储 → 响应
|
| 189 |
+
- 测试完整流程:文本提交 → 语义解析 → 存储 → 响应
|
| 190 |
+
- 测试错误场景的端到端处理
|
| 191 |
+
- _Requirements: 所有需求_
|
| 192 |
+
|
| 193 |
+
- [x] 12. 最终检查点
|
| 194 |
+
- 确保所有测试通过,代码覆盖率达到 80% 以上,如有问题请询问用户。
|
| 195 |
+
|
| 196 |
+
## Notes
|
| 197 |
+
|
| 198 |
+
- 所有任务均为必需任务,确保全面的测试覆盖
|
| 199 |
+
- 每个任务都引用了具体的需求条款,确保可追溯性
|
| 200 |
+
- 检查点任务确保增量验证
|
| 201 |
+
- 属性测试验证通用正确性属性(使用 hypothesis 库,最少 100 次迭代)
|
| 202 |
+
- 单元测试验证特定示例和边缘情况
|
| 203 |
+
- 所有外部 API 调用使用 mock 进行测试
|
| 204 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 安装系统依赖
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# 复制依赖文件
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
|
| 13 |
+
# 安装 Python 依赖
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# 复制应用代码
|
| 17 |
+
COPY app/ ./app/
|
| 18 |
+
COPY data/ ./data/
|
| 19 |
+
COPY frontend/dist/ ./frontend/dist/
|
| 20 |
+
|
| 21 |
+
# 复制启动脚本
|
| 22 |
+
COPY start.py .
|
| 23 |
+
|
| 24 |
+
# 创建必要的目录
|
| 25 |
+
RUN mkdir -p generated_images logs
|
| 26 |
+
|
| 27 |
+
# 暴露端口
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# 启动应用
|
| 31 |
+
CMD ["python", "start.py"]
|
HOTFIX_DOCKER_BUILD.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔧 紧急修复:Docker 构建失败
|
| 2 |
+
|
| 3 |
+
## 🐛 问题描述
|
| 4 |
+
Hugging Face Space 构建失败:
|
| 5 |
+
```
|
| 6 |
+
ERROR: failed to calculate checksum of ref: "/generated_images": not found
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## 🔍 问题原因
|
| 10 |
+
1. `Dockerfile` 尝试复制 `generated_images/` 目录
|
| 11 |
+
2. 但该目录在 GitHub 仓库中被 `.github/workflows/sync.yml` 删除了
|
| 12 |
+
3. Docker 构建时找不到该目录,导致失败
|
| 13 |
+
|
| 14 |
+
## ✅ 已修复
|
| 15 |
+
|
| 16 |
+
### 1. 简化 Dockerfile
|
| 17 |
+
**文件**:`Dockerfile`
|
| 18 |
+
|
| 19 |
+
**修改前**:
|
| 20 |
+
```dockerfile
|
| 21 |
+
COPY generated_images/ ./generated_images/
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
**修改后**:
|
| 25 |
+
```dockerfile
|
| 26 |
+
# 只创建空目录,不复制文件
|
| 27 |
+
RUN mkdir -p generated_images logs
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### 2. 修改默认配置
|
| 31 |
+
**文件**:`app/user_config.py` 和 `app/storage.py`
|
| 32 |
+
|
| 33 |
+
**修改前**:
|
| 34 |
+
```python
|
| 35 |
+
"image_url": "generated_images/default_character.jpeg",
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
**修改后**:
|
| 39 |
+
```python
|
| 40 |
+
"image_url": "", # 空字符串,前端会显示占位符
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**原因**:
|
| 44 |
+
- 不依赖 Git 仓库中的图片文件
|
| 45 |
+
- 用户首次使用时可以生成自己的 AI 形象
|
| 46 |
+
- 或者前端显示一个默认占位符
|
| 47 |
+
|
| 48 |
+
## 🚀 部署步骤
|
| 49 |
+
|
| 50 |
+
### 1. 提交修复
|
| 51 |
+
```bash
|
| 52 |
+
git add Dockerfile app/user_config.py app/storage.py
|
| 53 |
+
git commit -m "Fix: Remove dependency on generated_images directory in Docker build"
|
| 54 |
+
git push origin main
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### 2. 同步到 Hugging Face
|
| 58 |
+
1. 访问:https://huggingface.co/spaces/kernel14/Nora
|
| 59 |
+
2. Settings → Sync from GitHub → **Sync now**
|
| 60 |
+
|
| 61 |
+
### 3. 等待重新构建
|
| 62 |
+
- 查看 **Logs** 标签页
|
| 63 |
+
- 应该能看到构建成功
|
| 64 |
+
|
| 65 |
+
## ✅ 验证修复
|
| 66 |
+
|
| 67 |
+
构建成功后,访问:
|
| 68 |
+
```
|
| 69 |
+
https://kernel14-nora.hf.space/
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
应该能看到:
|
| 73 |
+
- ✅ 前端正常加载
|
| 74 |
+
- ✅ AI 形象位置显示占位符(或默认图标)
|
| 75 |
+
- ✅ 可以点击 ✨ 按钮生成自定义形象
|
| 76 |
+
- ✅ 所有功能正常工作
|
| 77 |
+
|
| 78 |
+
## 📝 技术说明
|
| 79 |
+
|
| 80 |
+
### 为什么不在 Docker 镜像中包含默认图片?
|
| 81 |
+
|
| 82 |
+
1. **Git 仓库限制**:
|
| 83 |
+
- 图片文件较大(几百 KB)
|
| 84 |
+
- 会增加仓库体积
|
| 85 |
+
- 被 `.github/workflows/sync.yml` 清理
|
| 86 |
+
|
| 87 |
+
2. **更好的方案**:
|
| 88 |
+
- 用户首次使用时生成个性化形象
|
| 89 |
+
- 或者使用 CDN 托管的默认图片
|
| 90 |
+
- 或者前端显示 SVG 占位符
|
| 91 |
+
|
| 92 |
+
3. **运行时生成**:
|
| 93 |
+
- 用户可以随时生成新形象
|
| 94 |
+
- 图片保存在容器的 `generated_images/` 目录
|
| 95 |
+
- 重启容器后会丢失(可以接受)
|
| 96 |
+
|
| 97 |
+
### 未来改进方向
|
| 98 |
+
|
| 99 |
+
1. **使用对象存储**:
|
| 100 |
+
- 将生成的图片上传到 S3/OSS
|
| 101 |
+
- 持久化存储,不会丢失
|
| 102 |
+
- 支持多实例共享
|
| 103 |
+
|
| 104 |
+
2. **内嵌默认图片**:
|
| 105 |
+
- 将默认图片转为 Base64
|
| 106 |
+
- 直接写在代码中
|
| 107 |
+
- 或者使用 SVG 矢量图
|
| 108 |
+
|
| 109 |
+
3. **CDN 托管**:
|
| 110 |
+
- 将默认图片放在 CDN
|
| 111 |
+
- 配置 URL 指向 CDN
|
| 112 |
+
- 加载更快
|
| 113 |
+
|
| 114 |
+
## 🎉 修复完成
|
| 115 |
+
|
| 116 |
+
修复后,Docker 构建应该能成功,Space 可以正常运行。
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
**修复时间**:2026-01-18
|
| 121 |
+
**影响范围**:Hugging Face Space Docker 构建
|
| 122 |
+
**严重程度**:高(导致构建失败)
|
| 123 |
+
**修复状态**:✅ 已完成
|
HOTFIX_NULL_ERROR.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔧 紧急修复:Python null 错误
|
| 2 |
+
|
| 3 |
+
## 🐛 问题描述
|
| 4 |
+
Hugging Face Space 部署后出现错误:
|
| 5 |
+
```
|
| 6 |
+
NameError: name 'null' is not defined
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## 🔍 问题原因
|
| 10 |
+
在 `app/storage.py` 中使用了 JavaScript 语法的 `null`,但 Python 中应该使用 `None`。
|
| 11 |
+
|
| 12 |
+
## ✅ 已修复
|
| 13 |
+
|
| 14 |
+
### 1. 修复 storage.py 中的 null
|
| 15 |
+
**文件**:`app/storage.py`
|
| 16 |
+
|
| 17 |
+
**修改位置**:
|
| 18 |
+
- 第 173-175 行:`_get_default_records()` 方法
|
| 19 |
+
- 第 315-317 行:`_get_default_todos()` 方法
|
| 20 |
+
|
| 21 |
+
**修改内容**:
|
| 22 |
+
```python
|
| 23 |
+
# 错误 ❌
|
| 24 |
+
"time": null,
|
| 25 |
+
"location": null,
|
| 26 |
+
|
| 27 |
+
# 正确 ✅
|
| 28 |
+
"time": None,
|
| 29 |
+
"location": None,
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### 2. 修复 Dockerfile
|
| 33 |
+
**文件**:`Dockerfile`
|
| 34 |
+
|
| 35 |
+
**问题**:未复制 `generated_images/` 目录,导致默认角色图片 404
|
| 36 |
+
|
| 37 |
+
**修改**:
|
| 38 |
+
```dockerfile
|
| 39 |
+
# 添加这行
|
| 40 |
+
COPY generated_images/ ./generated_images/
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## 🚀 部署步骤
|
| 44 |
+
|
| 45 |
+
### 1. 提交修复
|
| 46 |
+
```bash
|
| 47 |
+
git add app/storage.py Dockerfile
|
| 48 |
+
git commit -m "Fix: Replace null with None in Python code"
|
| 49 |
+
git push origin main
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 2. 同步到 Hugging Face
|
| 53 |
+
1. 访问:https://huggingface.co/spaces/kernel14/Nora
|
| 54 |
+
2. Settings → Sync from GitHub → **Sync now**
|
| 55 |
+
|
| 56 |
+
### 3. 等待重新构建
|
| 57 |
+
- 查看 **Logs** 标签页
|
| 58 |
+
- 等待构建完成
|
| 59 |
+
|
| 60 |
+
## ✅ 验证修复
|
| 61 |
+
|
| 62 |
+
访问以下 API 端点,应该都能正常返回:
|
| 63 |
+
|
| 64 |
+
1. **健康检查**:
|
| 65 |
+
```
|
| 66 |
+
https://kernel14-nora.hf.space/health
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
2. **获取记录**:
|
| 70 |
+
```
|
| 71 |
+
https://kernel14-nora.hf.space/api/records
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
3. **获取心情**:
|
| 75 |
+
```
|
| 76 |
+
https://kernel14-nora.hf.space/api/moods
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
4. **获取待办**:
|
| 80 |
+
```
|
| 81 |
+
https://kernel14-nora.hf.space/api/todos
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
5. **默认角色图片**:
|
| 85 |
+
```
|
| 86 |
+
https://kernel14-nora.hf.space/generated_images/default_character.jpeg
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 📝 技术说明
|
| 90 |
+
|
| 91 |
+
### Python vs JavaScript 的 null/None
|
| 92 |
+
|
| 93 |
+
| 语言 | 空值表示 |
|
| 94 |
+
|------|---------|
|
| 95 |
+
| JavaScript | `null` |
|
| 96 |
+
| Python | `None` |
|
| 97 |
+
| JSON | `null` |
|
| 98 |
+
|
| 99 |
+
在 Python 代码中:
|
| 100 |
+
- ✅ 使用 `None`
|
| 101 |
+
- ❌ 不要使用 `null`
|
| 102 |
+
|
| 103 |
+
在 JSON 字符串中(如 AI 提示):
|
| 104 |
+
- ✅ 使用 `"null"`(字符串形式)
|
| 105 |
+
- ✅ 这是正确的,因为是 JSON 格式
|
| 106 |
+
|
| 107 |
+
### 为什么会出现这个错误?
|
| 108 |
+
|
| 109 |
+
1. **复制粘贴错误**:可能从 JSON 示例中复制了代码
|
| 110 |
+
2. **语言混淆**:在多语言项目中容易混淆语法
|
| 111 |
+
3. **IDE 未检测**:某些 IDE 可能不会立即标记这个错误
|
| 112 |
+
|
| 113 |
+
### 如何避免?
|
| 114 |
+
|
| 115 |
+
1. **使用 Linter**:配置 pylint 或 flake8
|
| 116 |
+
2. **类型检查**:使用 mypy 进行类型检查
|
| 117 |
+
3. **单元测试**:编写测试覆盖默认数据生成
|
| 118 |
+
4. **代码审查**:提交前仔细检查
|
| 119 |
+
|
| 120 |
+
## 🎉 修复完成
|
| 121 |
+
|
| 122 |
+
修复后,Space 应该能正常运行,所有 API 端点都能正常响应。
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
**修复时间**:2026-01-18
|
| 127 |
+
**影响范围**:Hugging Face Space 部署
|
| 128 |
+
**严重程度**:高(导致服务无法启动)
|
| 129 |
+
**修复状态**:✅ 已完成
|
HUGGINGFACE_DEPLOY.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Hugging Face Spaces 部署指南
|
| 2 |
+
|
| 3 |
+
## ✅ 部署前检查清单
|
| 4 |
+
|
| 5 |
+
### 1. 根目录必需文件
|
| 6 |
+
|
| 7 |
+
确保以下文件在**根目录**(不是子目录):
|
| 8 |
+
|
| 9 |
+
- ✅ `Dockerfile` - Docker 构建配置
|
| 10 |
+
- ✅ `start.py` - 应用启动脚本
|
| 11 |
+
- ✅ `requirements.txt` - Python 依赖
|
| 12 |
+
- ✅ `README_HF.md` - Hugging Face 专用 README(带 frontmatter)
|
| 13 |
+
|
| 14 |
+
### 2. 前端构建文件
|
| 15 |
+
|
| 16 |
+
确保前端已构建:
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
cd frontend
|
| 20 |
+
npm install
|
| 21 |
+
npm run build
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
检查 `frontend/dist/` 目录是否存在且包含:
|
| 25 |
+
- ✅ `index.html`
|
| 26 |
+
- ✅ `assets/` 目录(包含 JS 和 CSS 文件)
|
| 27 |
+
|
| 28 |
+
### 3. 环境变量配置
|
| 29 |
+
|
| 30 |
+
在 Hugging Face Space 的 **Settings → Variables and secrets** 中配置:
|
| 31 |
+
|
| 32 |
+
**必需:**
|
| 33 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 34 |
+
|
| 35 |
+
**可选:**
|
| 36 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 37 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 38 |
+
|
| 39 |
+
### 4. README 配置
|
| 40 |
+
|
| 41 |
+
确保 `README_HF.md` 包含正确的 frontmatter:
|
| 42 |
+
|
| 43 |
+
```yaml
|
| 44 |
+
---
|
| 45 |
+
title: Nora - 治愈系记录助手
|
| 46 |
+
emoji: 🌟
|
| 47 |
+
colorFrom: purple
|
| 48 |
+
colorTo: pink
|
| 49 |
+
sdk: docker
|
| 50 |
+
pinned: false
|
| 51 |
+
license: mit
|
| 52 |
+
---
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## 🔧 部署步骤
|
| 56 |
+
|
| 57 |
+
### 方法 1:通过 GitHub 同步(推荐)
|
| 58 |
+
|
| 59 |
+
1. **提交所有更改到 GitHub**:
|
| 60 |
+
```bash
|
| 61 |
+
git add .
|
| 62 |
+
git commit -m "Fix: Add required files to root directory for HF deployment"
|
| 63 |
+
git push origin main
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
2. **在 Hugging Face Space 中同步**:
|
| 67 |
+
- 进入你的 Space:https://huggingface.co/spaces/kernel14/Nora
|
| 68 |
+
- 点击 **Settings**
|
| 69 |
+
- 找到 **Sync from GitHub** 部分
|
| 70 |
+
- 点击 **Sync now**
|
| 71 |
+
|
| 72 |
+
3. **等待构建完成**:
|
| 73 |
+
- 查看 **Logs** 标签页
|
| 74 |
+
- 等待 Docker 构建完成(可能需要 5-10 分钟)
|
| 75 |
+
|
| 76 |
+
### 方法 2:直接上传文件
|
| 77 |
+
|
| 78 |
+
1. **在 Hugging Face Space 中上传文件**:
|
| 79 |
+
- 进入 **Files** 标签页
|
| 80 |
+
- 上传以下文件到根目录:
|
| 81 |
+
- `Dockerfile`
|
| 82 |
+
- `start.py`
|
| 83 |
+
- `requirements.txt`
|
| 84 |
+
- `README_HF.md`(重命名为 `README.md`)
|
| 85 |
+
|
| 86 |
+
2. **上传应用代码**:
|
| 87 |
+
- 上传 `app/` 目录
|
| 88 |
+
- 上传 `data/` 目录
|
| 89 |
+
- 上传 `frontend/dist/` 目录
|
| 90 |
+
|
| 91 |
+
3. **触发重新构建**:
|
| 92 |
+
- 点击 **Factory reboot**
|
| 93 |
+
|
| 94 |
+
## 🐛 常见问题
|
| 95 |
+
|
| 96 |
+
### 问题 1:Space 显示 "Missing app file"
|
| 97 |
+
|
| 98 |
+
**原因**:根目录缺少 `Dockerfile` 或 `start.py`
|
| 99 |
+
|
| 100 |
+
**解决方案**:
|
| 101 |
+
1. 确认根目录有 `Dockerfile` 和 `start.py`
|
| 102 |
+
2. 如果使用 GitHub 同步,确保这些文件已提交并推送
|
| 103 |
+
3. Factory reboot 重启 Space
|
| 104 |
+
|
| 105 |
+
### 问题 2:Docker 构建失败
|
| 106 |
+
|
| 107 |
+
**原因**:依赖安装失败或文件路径错误
|
| 108 |
+
|
| 109 |
+
**解决方案**:
|
| 110 |
+
1. 查看 **Logs** 标签页的详细错误信息
|
| 111 |
+
2. 检查 `requirements.txt` 是否正确
|
| 112 |
+
3. 检查 `Dockerfile` 中的路径是否正确
|
| 113 |
+
|
| 114 |
+
### 问题 3:前端无法加载
|
| 115 |
+
|
| 116 |
+
**原因**:`frontend/dist/` 目录不存在或未包含在 Docker 镜像中
|
| 117 |
+
|
| 118 |
+
**解决方案**:
|
| 119 |
+
1. 本地运行 `cd frontend && npm run build`
|
| 120 |
+
2. 确认 `frontend/dist/` 目录存在
|
| 121 |
+
3. 提交并推送到 GitHub
|
| 122 |
+
4. 重新同步 Space
|
| 123 |
+
|
| 124 |
+
### 问题 4:API 调用失败
|
| 125 |
+
|
| 126 |
+
**原因**:未配置环境变量
|
| 127 |
+
|
| 128 |
+
**解决方案**:
|
| 129 |
+
1. 在 Space Settings 中配置 `ZHIPU_API_KEY`
|
| 130 |
+
2. Factory reboot 重启 Space
|
| 131 |
+
3. 检查 Logs 确认环境变量已加载
|
| 132 |
+
|
| 133 |
+
## 📊 验证部署
|
| 134 |
+
|
| 135 |
+
部署成功后,访问你的 Space URL,应该能看到:
|
| 136 |
+
|
| 137 |
+
1. ✅ 前端页面正常加载
|
| 138 |
+
2. ✅ AI 角色形象显示
|
| 139 |
+
3. ✅ 可以进行文本输入
|
| 140 |
+
4. ✅ 可以查看心情、灵感、待办数据
|
| 141 |
+
|
| 142 |
+
测试 API 端点:
|
| 143 |
+
- `https://你的space.hf.space/health` - 应该返回健康状态
|
| 144 |
+
- `https://你的space.hf.space/docs` - 应该显示 API 文档
|
| 145 |
+
|
| 146 |
+
## 🔄 更新部署
|
| 147 |
+
|
| 148 |
+
当你更新代码后:
|
| 149 |
+
|
| 150 |
+
1. **提交到 GitHub**:
|
| 151 |
+
```bash
|
| 152 |
+
git add .
|
| 153 |
+
git commit -m "Update: 描述你的更改"
|
| 154 |
+
git push origin main
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
2. **同步到 Hugging Face**:
|
| 158 |
+
- 在 Space Settings 中点击 **Sync now**
|
| 159 |
+
- 或者等待自动同步(如果已配置)
|
| 160 |
+
|
| 161 |
+
3. **重启 Space**(如果需要):
|
| 162 |
+
- 点击 **Factory reboot**
|
| 163 |
+
|
| 164 |
+
## 📚 相关文档
|
| 165 |
+
|
| 166 |
+
- [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces)
|
| 167 |
+
- [Docker SDK 文档](https://huggingface.co/docs/hub/spaces-sdks-docker)
|
| 168 |
+
- [项目完整文档](README.md)
|
| 169 |
+
|
| 170 |
+
## 🆘 需要帮助?
|
| 171 |
+
|
| 172 |
+
如果遇到问题:
|
| 173 |
+
|
| 174 |
+
1. 查看 Space 的 **Logs** 标签页
|
| 175 |
+
2. 检查 **Community** 标签页的讨论
|
| 176 |
+
3. 在 GitHub 仓库提 Issue
|
HUGGINGFACE_FIX_SUMMARY.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ✅ Hugging Face Spaces 部署问题已修复
|
| 2 |
+
|
| 3 |
+
## 🎯 问题描述
|
| 4 |
+
Hugging Face Space 显示错误:
|
| 5 |
+
```
|
| 6 |
+
This Space is missing an app file. An app file is required for the Space to build and run properly.
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## 🔍 问题原因
|
| 10 |
+
之前为了整理项目结构,将部署文件移到了 `deployment/` 目录,但 Hugging Face Spaces 要求关键文件必须在**根目录**。
|
| 11 |
+
|
| 12 |
+
## 🔧 已完成的修复
|
| 13 |
+
|
| 14 |
+
### 1. 复制关键文件到根目录
|
| 15 |
+
- ✅ `Dockerfile` - 从 `deployment/Dockerfile` 复制到根目录
|
| 16 |
+
- ✅ `start.py` - 从 `scripts/start.py` 复制到根目录
|
| 17 |
+
- ✅ `README_HF.md` - 创建了带 frontmatter 的 Hugging Face 专用 README
|
| 18 |
+
|
| 19 |
+
### 2. 创建部署工具
|
| 20 |
+
- ✅ `.dockerignore` - 优化 Docker 构建,排除不必要的文件
|
| 21 |
+
- ✅ `HUGGINGFACE_DEPLOY.md` - 完整的部署指南
|
| 22 |
+
- ✅ `scripts/prepare_hf_deploy.bat` - 自动化部署准备脚本
|
| 23 |
+
|
| 24 |
+
### 3. 验证文件结构
|
| 25 |
+
根目录现在包含所有必需文件:
|
| 26 |
+
```
|
| 27 |
+
项目根目录/
|
| 28 |
+
├── Dockerfile ✅ Docker 构建配置
|
| 29 |
+
├── start.py ✅ 应用启动脚本
|
| 30 |
+
├── requirements.txt ✅ Python 依赖
|
| 31 |
+
├── README_HF.md ✅ HF 专用 README(带 frontmatter)
|
| 32 |
+
├── app/ ✅ 应用代码
|
| 33 |
+
├── data/ ✅ 数据目录
|
| 34 |
+
├── frontend/dist/ ✅ 前端构建文件
|
| 35 |
+
└── generated_images/ ✅ 图片目录
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## 🚀 立即部署
|
| 39 |
+
|
| 40 |
+
### 方法 1:使用自动化脚本(推荐)
|
| 41 |
+
|
| 42 |
+
运行准备脚本:
|
| 43 |
+
```bash
|
| 44 |
+
scripts\prepare_hf_deploy.bat
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
这会自动:
|
| 48 |
+
- ✅ 检查所有必需文件
|
| 49 |
+
- ✅ 构建前端(如果需要)
|
| 50 |
+
- ✅ 生成部署清单
|
| 51 |
+
- ✅ 显示下一步操作
|
| 52 |
+
|
| 53 |
+
### 方法 2:手动操作
|
| 54 |
+
|
| 55 |
+
#### 步骤 1:确认文件存在
|
| 56 |
+
```bash
|
| 57 |
+
# 检查根目录文件
|
| 58 |
+
dir Dockerfile
|
| 59 |
+
dir start.py
|
| 60 |
+
dir requirements.txt
|
| 61 |
+
dir README_HF.md
|
| 62 |
+
|
| 63 |
+
# 检查前端构建
|
| 64 |
+
dir frontend\dist\index.html
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
#### 步骤 2:提交到 GitHub
|
| 68 |
+
```bash
|
| 69 |
+
git add .
|
| 70 |
+
git commit -m "Fix: Add required files to root directory for HF deployment"
|
| 71 |
+
git push origin main
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
#### 步骤 3:同步到 Hugging Face
|
| 75 |
+
1. 访问:https://huggingface.co/spaces/kernel14/Nora
|
| 76 |
+
2. 点击 **Settings** 标签
|
| 77 |
+
3. 找到 **Sync from GitHub** 部分
|
| 78 |
+
4. 点击 **Sync now** 按钮
|
| 79 |
+
|
| 80 |
+
#### 步骤 4:配置环境变量
|
| 81 |
+
1. 在 Settings 中找到 **Variables and secrets**
|
| 82 |
+
2. 添加环境变量:
|
| 83 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥(必需)
|
| 84 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥(可选)
|
| 85 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID(可选)
|
| 86 |
+
3. 点击 **Factory reboot** 重启 Space
|
| 87 |
+
|
| 88 |
+
#### 步骤 5:等待构建完成
|
| 89 |
+
1. 切换到 **Logs** 标签页
|
| 90 |
+
2. 观察 Docker 构建过程
|
| 91 |
+
3. 等待显示 "Running on http://0.0.0.0:7860"
|
| 92 |
+
|
| 93 |
+
## ✅ 验证部署
|
| 94 |
+
|
| 95 |
+
部署成功后,测试以下功能:
|
| 96 |
+
|
| 97 |
+
### 1. 访问主页
|
| 98 |
+
```
|
| 99 |
+
https://kernel14-nora.hf.space/
|
| 100 |
+
```
|
| 101 |
+
应该看到:
|
| 102 |
+
- ✅ 前端页面正常加载
|
| 103 |
+
- ✅ AI 角色形象显示
|
| 104 |
+
- ✅ 输入框可用
|
| 105 |
+
|
| 106 |
+
### 2. 测试 API
|
| 107 |
+
```
|
| 108 |
+
https://kernel14-nora.hf.space/health
|
| 109 |
+
```
|
| 110 |
+
应该返回:
|
| 111 |
+
```json
|
| 112 |
+
{
|
| 113 |
+
"status": "healthy",
|
| 114 |
+
"data_dir": "data",
|
| 115 |
+
"max_audio_size": 10485760
|
| 116 |
+
}
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 3. 查看 API 文档
|
| 120 |
+
```
|
| 121 |
+
https://kernel14-nora.hf.space/docs
|
| 122 |
+
```
|
| 123 |
+
应该显示完整的 API 文档
|
| 124 |
+
|
| 125 |
+
### 4. 测试功能
|
| 126 |
+
- ✅ 文本输入和处理
|
| 127 |
+
- ✅ 查看心情、灵感、待办
|
| 128 |
+
- ✅ AI 对话功能
|
| 129 |
+
- ✅ 心情气泡池
|
| 130 |
+
|
| 131 |
+
## 🐛 故障排查
|
| 132 |
+
|
| 133 |
+
### 问题 1:仍然显示 "Missing app file"
|
| 134 |
+
|
| 135 |
+
**可能原因**:
|
| 136 |
+
- 文件未正确提交到 GitHub
|
| 137 |
+
- GitHub 同步未完成
|
| 138 |
+
|
| 139 |
+
**解决方案**:
|
| 140 |
+
1. 检查 GitHub 仓库根目录是否有 `Dockerfile` 和 `start.py`
|
| 141 |
+
2. 在 HF Space 中手动触发同步
|
| 142 |
+
3. 查看 Logs 标签页的详细错误
|
| 143 |
+
|
| 144 |
+
### 问题 2:Docker 构建失败
|
| 145 |
+
|
| 146 |
+
**可能原因**:
|
| 147 |
+
- 依赖安装失败
|
| 148 |
+
- 文件路径错误
|
| 149 |
+
|
| 150 |
+
**解决方案**:
|
| 151 |
+
1. 查看 Logs 标签页的详细错误信息
|
| 152 |
+
2. 检查 `requirements.txt` 是否正确
|
| 153 |
+
3. 确认 `frontend/dist/` 目录存在
|
| 154 |
+
|
| 155 |
+
### 问题 3:前端无法加载
|
| 156 |
+
|
| 157 |
+
**可能原因**:
|
| 158 |
+
- `frontend/dist/` 目录不存在或为空
|
| 159 |
+
- 前端构建文件未提交
|
| 160 |
+
|
| 161 |
+
**解决方案**:
|
| 162 |
+
1. 本地运行:`cd frontend && npm run build`
|
| 163 |
+
2. 确认 `frontend/dist/` 包含 `index.html` 和 `assets/`
|
| 164 |
+
3. 提交并推送到 GitHub
|
| 165 |
+
4. 重新同步 Space
|
| 166 |
+
|
| 167 |
+
### 问题 4:API 调用失败
|
| 168 |
+
|
| 169 |
+
**可能原因**:
|
| 170 |
+
- 未配置 `ZHIPU_API_KEY`
|
| 171 |
+
- API 密钥无效或配额不足
|
| 172 |
+
|
| 173 |
+
**解决方案**:
|
| 174 |
+
1. 在 Space Settings 中配置环境变量
|
| 175 |
+
2. 访问 https://open.bigmodel.cn/ 检查 API 密钥和配额
|
| 176 |
+
3. Factory reboot 重启 Space
|
| 177 |
+
|
| 178 |
+
## 📊 部署状态检查
|
| 179 |
+
|
| 180 |
+
运行以下命令检查本地准备情况:
|
| 181 |
+
```bash
|
| 182 |
+
scripts\prepare_hf_deploy.bat
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
查看生成的 `deploy_checklist.txt` 文件。
|
| 186 |
+
|
| 187 |
+
## 📚 相关文档
|
| 188 |
+
|
| 189 |
+
- [HUGGINGFACE_DEPLOY.md](HUGGINGFACE_DEPLOY.md) - 完整部署指南
|
| 190 |
+
- [README_HF.md](README_HF.md) - Hugging Face Space 的 README
|
| 191 |
+
- [deployment/DEPLOYMENT.md](deployment/DEPLOYMENT.md) - 通用部署文档
|
| 192 |
+
|
| 193 |
+
## 🎉 成功标志
|
| 194 |
+
|
| 195 |
+
当看到以下内容时,说明部署成功:
|
| 196 |
+
|
| 197 |
+
1. ✅ Space 状态显示为 "Running"
|
| 198 |
+
2. ✅ 可以访问主页并看到 UI
|
| 199 |
+
3. ✅ API 端点正常响应
|
| 200 |
+
4. ✅ 可以进行文本输入和查看数据
|
| 201 |
+
5. ✅ Logs 中没有错误信息
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
## 📝 技术说明
|
| 206 |
+
|
| 207 |
+
### 为什么需要文件在根目录?
|
| 208 |
+
|
| 209 |
+
Hugging Face Spaces 的构建系统会在根目录查找以下文件:
|
| 210 |
+
|
| 211 |
+
1. **Dockerfile** - 用于 Docker SDK 的 Space
|
| 212 |
+
2. **app.py** - 用于 Gradio/Streamlit SDK 的 Space
|
| 213 |
+
3. **README.md** - 带 frontmatter 的配置文件
|
| 214 |
+
|
| 215 |
+
如果这些文件不在根目录,构建系统会报错 "Missing app file"。
|
| 216 |
+
|
| 217 |
+
### 我们的解决方案
|
| 218 |
+
|
| 219 |
+
- 保留 `deployment/` 目录用于备份和文档
|
| 220 |
+
- 在根目录创建必需文件的副本
|
| 221 |
+
- 使用 `.dockerignore` 优化构建,避免包含不必要的文件
|
| 222 |
+
|
| 223 |
+
这样既保持了项目结构的整洁,又满足了 Hugging Face 的要求。
|
PRD.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
|
| 3 |
+
# 产品概述
|
| 4 |
+
|
| 5 |
+
一款通过 **iOS 原生 (SwiftUI)** 构建,结合 **BLE 蓝牙硬件** 震动提醒与 **AI 语义解析** 的治愈系记录助手。用户通过 APP 或配套硬件录音,系统自动将内容拆解为灵感、心情与待办,并通过 RAG 技术实现历史记忆的回溯。
|
| 6 |
+
|
| 7 |
+
# 核心交互逻辑
|
| 8 |
+
|
| 9 |
+
## 硬件交互:蓝牙协议
|
| 10 |
+
|
| 11 |
+
由于使用 iOS 原生开发,手机充当“网关”角色,负责硬件与云端的中转。
|
| 12 |
+
|
| 13 |
+
- **连接流程 (Local Only)**:
|
| 14 |
+
- **无需 API 接口**。iOS APP 使用 `CBCentralManager` 扫描硬件 UUID。
|
| 15 |
+
- 硬件作为外设 (Peripheral) 被手机连接。
|
| 16 |
+
- **指令交互**:
|
| 17 |
+
- **录音阶段**:硬件按下录音键,通过蓝牙特征值 (Characteristic) 将音频数据包流式传输或发送结束信号至 iOS。
|
| 18 |
+
- **震动反馈**:
|
| 19 |
+
- **轻微短振(心跳感)**:iOS 检测到录音启动,向蓝牙写入 `0x01` 指令。
|
| 20 |
+
- **急促振动(提醒感)**:iOS 的待办逻辑触发,向蓝牙写入 `0x02` 指令。
|
| 21 |
+
|
| 22 |
+
## AI:调用智谱原生api
|
| 23 |
+
|
| 24 |
+
- **语音转写**:iOS 使用 `URLSession` 调用智谱 **ASR API** 上传音频,实时获取转写文字。
|
| 25 |
+
- **语义理解**:iOS 调用 **GLM-4-Flash API**,通过 Prompt 约束 AI 返回标准 JSON(包含情绪、灵感、待办)。
|
| 26 |
+
- **形象定制**:登录时调用 **CogView API** 生成固定形象,图片下载后由 iOS 进行本地持久化存储。
|
| 27 |
+
|
| 28 |
+
# **技术架构 (iOS Native)**
|
| 29 |
+
|
| 30 |
+
## **前端:SwiftUI**
|
| 31 |
+
|
| 32 |
+
- **状态管理**:使用 `@Observable` (iOS 17+) 实时同步 AI 解析出的心情颜色和形象气泡。
|
| 33 |
+
- **持久化**:使用 **SwiftData** 存储本地 JSON 结构的记录(`records`, `moods`, `todos`, `inspirations`)。
|
| 34 |
+
- **安全性**:智谱 API Key 存储在 **Keychain** 中,避免硬编码。
|
| 35 |
+
|
| 36 |
+
## **AI 引擎 (智谱 API 集成)**
|
| 37 |
+
|
| 38 |
+
| **模块** | **API 模型** | **职责** |
|
| 39 |
+
| --- | --- | --- |
|
| 40 |
+
| **ASR** | 智谱语音识别 | 硬件原始音频转文字 |
|
| 41 |
+
| **NLP** | GLM-4-Flash | 解析 JSON 结构、RAG 历史回溯对话 |
|
| 42 |
+
| **图像** | CogView-3 | 登录时一次性生成固定猫咪形象 |
|
| 43 |
+
|
| 44 |
+
# AI形象生成
|
| 45 |
+
|
| 46 |
+
## 设置
|
| 47 |
+
|
| 48 |
+
- **初始化生成**:用户注册/首次登录时,系统引导用户输入关键词(或默认随机),调用 **GLM-Image (CogView)** 生成 1-3 张插画。
|
| 49 |
+
- **持久化存储**:生成的图片 URL 存储在用户配置中,不再随每次录音改变。
|
| 50 |
+
- **按需修改**:在“设置”提供修改接口,用户可以消耗积分或次数重新生成。
|
| 51 |
+
|
| 52 |
+
## 生成逻辑
|
| 53 |
+
|
| 54 |
+
为了保证品牌统一性,系统预设为”**治愈系插画猫咪**”,通过映射逻辑处理用户输入。
|
| 55 |
+
|
| 56 |
+
- **提示词生成逻辑 (Prompt Engineering)**
|
| 57 |
+
|
| 58 |
+
| **用户输入维度** | **映射逻辑 (Internal Tags)** | **示例** |
|
| 59 |
+
| --- | --- | --- |
|
| 60 |
+
| **颜色** | 主色调 & 环境色 | 温暖粉 -> `soft pastel pink fur, rose-colored aesthetic` |
|
| 61 |
+
| **性格** | 构图 & 眼神光 | 活泼 -> `big curious eyes, dynamic paw gesture, energetic aura` |
|
| 62 |
+
| **形象** | 配饰 & 特征 | 戴眼镜 -> `wearing tiny round glasses, scholarly look` |
|
| 63 |
+
|
| 64 |
+
【陪伴式朋友】【温柔照顾型长辈】【引导型 老师】
|
| 65 |
+
|
| 66 |
+
**系统底座提示词 (System Base Prompt):**
|
| 67 |
+
|
| 68 |
+
> "A masterpiece cute stylized cat illustration, [Color] theme, [Personality] facial expression and posture, [Description]. Japanese watercolor style, clean minimalist background, high quality, soft studio lighting, 4k."
|
| 69 |
+
>
|
| 70 |
+
|
| 71 |
+
## 技术架构
|
| 72 |
+
|
| 73 |
+
### 前端:iOS Native (SwiftUI)
|
| 74 |
+
|
| 75 |
+
- **UI 渲染**:利用 `SwiftUI` 实现毛玻璃效果与治愈系猫咪插画的流畅加载。
|
| 76 |
+
- **状态管理**:使用 `Combine` 或 `Observation` 框架同步心情颜色变化。
|
| 77 |
+
- **硬件接口**:`CoreBluetooth`。
|
| 78 |
+
|
| 79 |
+
### 后端:FastAPI (Python)
|
| 80 |
+
|
| 81 |
+
- **API 核心**:处理 ASR、NLP、RAG 和 Image Generation。
|
| 82 |
+
- **存储**:本地 JSON 文件系统(`records.json`, `moods.json`, `todos.json`, `inspirations.json`)。
|
| 83 |
+
|
| 84 |
+
### AI 引擎 (智谱全家桶)
|
| 85 |
+
|
| 86 |
+
- **ASR**:语音转文字。
|
| 87 |
+
- **GLM-4-Flash**:语义解析与 RAG 问答。
|
| 88 |
+
- **GLM-Image (CogView)**:基于情绪映射生成的静态形象。
|
| 89 |
+
|
| 90 |
+
# 核心功能模块
|
| 91 |
+
|
| 92 |
+
### 首页 - 录音与实时处理
|
| 93 |
+
|
| 94 |
+
- **功能描述:**
|
| 95 |
+
- 支持语音录音(5-30 秒)或文字直接输入。
|
| 96 |
+
- **静态形象展示**:页面中心展示常驻形象。
|
| 97 |
+
- 实时处理:完成录音后自动触发后端 ASR 与 NLP 流程。
|
| 98 |
+
- **结果速览**:展示最近一次分析的**原文及摘要**(提取出的情绪、灵感标签或待办任务)。
|
| 99 |
+
- **数据存储:** * 音频文件:`data/audio/{timestamp}.wav`
|
| 100 |
+
- 完整记录索引:`data/records.json`(包含关联的 JSON ID 和音频路径)。
|
| 101 |
+
|
| 102 |
+
### 灵感看板页面
|
| 103 |
+
|
| 104 |
+
- **功能描述:**
|
| 105 |
+
- **瀑布流展示**:以卡片形式展示所有灵感。
|
| 106 |
+
- **核心要素**:显示 AI 总结的核心观点、自动生成的标签、所属分类(工作/生活/学习/创意)。
|
| 107 |
+
- **筛选排序**:支持按分类筛选及时间顺序/倒序排列。
|
| 108 |
+
- **数据结构:** `inspirations.json` 存储核心观点、关键字及原文引用。
|
| 109 |
+
|
| 110 |
+
### 心情日记页面
|
| 111 |
+
|
| 112 |
+
- **功能描述:**
|
| 113 |
+
- **情绪可视化**:展示情绪分布柱状图(如:本周 60% 平静,20% 喜悦)。
|
| 114 |
+
- **记录列表**:显示每条记录的情绪类型、强度(1-10)及当时的心情关键词。
|
| 115 |
+
- **筛选**:可单独查看“喜”或“哀”等特定情绪的历史。
|
| 116 |
+
- **数据结构:** `moods.json` 记录 `type`, `intensity`, `keywords` 等字段。
|
| 117 |
+
|
| 118 |
+
### 待办清单页面
|
| 119 |
+
|
| 120 |
+
- **功能描述:**
|
| 121 |
+
- **任务管理**:从输入中自动提取出的任务(包含时间、地点、内容)。
|
| 122 |
+
- **状态切换**:支持手动勾选“已完成”。
|
| 123 |
+
- **统计**:显示待办/已完成的数量对比。
|
| 124 |
+
- **数据结构:** `todos.json` 包含任务描述、时间实体及完成状态。
|
| 125 |
+
|
| 126 |
+
### AI 对话页面
|
| 127 |
+
|
| 128 |
+
- **功能描述:**
|
| 129 |
+
- **智能检索**:用户询问“我上周关于论文有什么灵感?”时,系统通过 RAG 技术检索 `records.json` 并回答。
|
| 130 |
+
- **快捷指令**:提供“总结今日心情”、“还有哪些待办”等快捷按钮。
|
| 131 |
+
- **技术实现:** 基于 **GLM-4-Flash** 进行上下文理解与 RAG 检索。
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
# 业务流程与数据流
|
| 136 |
+
|
| 137 |
+
iOS 端在请求 GLM-4 时,使用以下 System Prompt 确保数据可被解析:
|
| 138 |
+
|
| 139 |
+
> "你是一个数据转换器。请将文本解析为 JSON 格式。维度包括:1.情绪(type,intensity); 2.灵感(core_idea,tags); 3.待办(task,time,location)。必须严格遵循 JSON 格式返回。"
|
| 140 |
+
>
|
| 141 |
+
|
| 142 |
+
### NLP 语义解析策略
|
| 143 |
+
|
| 144 |
+
| **提取维度** | **逻辑** | **去向** |
|
| 145 |
+
| --- | --- | --- |
|
| 146 |
+
| **情绪** | 识别情感极性与 1-10 的强度值 | `moods.json` |
|
| 147 |
+
| **灵感** | 提炼 20 字以内的核心观点 + 3个标签 | `inspirations.json` |
|
| 148 |
+
| **待办** | 识别时间词(如“明晚”)、地点与动词短语 | `todos.json` |
|
| 149 |
+
|
| 150 |
+
# 技术栈总结
|
| 151 |
+
|
| 152 |
+
- **开发语言**:Swift 6.0 / SwiftUI
|
| 153 |
+
- **核心框架**:CoreBluetooth (硬件), SwiftData (存储), CoreHaptics (震动)
|
| 154 |
+
- **AI 接口**:智谱 API (HTTP/HTTPS 请求)
|
| 155 |
+
- **数据存储**:iOS Local SandBox (音频文件 + 结构化数据)
|
PROJECT_STRUCTURE.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 项目目录结构
|
| 2 |
+
|
| 3 |
+
```
|
| 4 |
+
Inspiration-Record-APP/
|
| 5 |
+
├── app/ # 后端应用代码
|
| 6 |
+
│ ├── __init__.py
|
| 7 |
+
│ ├── main.py # FastAPI 主应用
|
| 8 |
+
│ ├── config.py # 配置管理
|
| 9 |
+
│ ├── models.py # 数据模型
|
| 10 |
+
│ ├── storage.py # 数据存储
|
| 11 |
+
│ ├── asr_service.py # 语音识别服务
|
| 12 |
+
│ ├── semantic_parser.py # 语义解析服务
|
| 13 |
+
│ ├── image_service.py # 图像生成服务
|
| 14 |
+
│ ├── user_config.py # 用户配置管理
|
| 15 |
+
│ └── logging_config.py # 日志配置
|
| 16 |
+
│
|
| 17 |
+
├── frontend/ # 前端应用
|
| 18 |
+
│ ├── components/ # React 组件
|
| 19 |
+
│ ├── services/ # API 服务
|
| 20 |
+
│ ├── utils/ # 工具函数
|
| 21 |
+
│ ├── dist/ # 构建产物(部署需要)
|
| 22 |
+
│ ├── App.tsx # 主应用组件
|
| 23 |
+
│ ├── index.tsx # 入口文件
|
| 24 |
+
│ ├── types.ts # TypeScript 类型定义
|
| 25 |
+
│ ├── package.json # 前端依赖
|
| 26 |
+
│ └── vite.config.ts # Vite 配置
|
| 27 |
+
│
|
| 28 |
+
├── data/ # 数据存储目录
|
| 29 |
+
│ ├── moods.json # 心情数据
|
| 30 |
+
│ ├── inspirations.json # 灵感数据
|
| 31 |
+
│ ├── todos.json # 待办数据
|
| 32 |
+
│ ├── records.json # 记录数据
|
| 33 |
+
│ └── user_config.json # 用户配置
|
| 34 |
+
│
|
| 35 |
+
├── generated_images/ # AI 生成的图片
|
| 36 |
+
│ └── default_character.jpeg # 默认形象
|
| 37 |
+
│
|
| 38 |
+
├── logs/ # 日志文件
|
| 39 |
+
│ └── app.log
|
| 40 |
+
│
|
| 41 |
+
├── tests/ # 测试文件
|
| 42 |
+
│ ├── test_*.py # 单元测试
|
| 43 |
+
│ ├── test_api.html # API 测试页面
|
| 44 |
+
│ ├── test_chat_api.py # 聊天 API 测试
|
| 45 |
+
│ └── test_default_character.py # 默认形象测试
|
| 46 |
+
│
|
| 47 |
+
├── scripts/ # 脚本文件
|
| 48 |
+
│ ├── start_local.py # 本地启动脚本(8000端口)
|
| 49 |
+
│ ├── start_local.bat # Windows 启动脚本
|
| 50 |
+
│ ├── start.py # 通用启动脚本(7860端口)
|
| 51 |
+
│ ├── build_and_deploy.bat # 构建并部署脚本
|
| 52 |
+
│ └── build_and_deploy.sh # Linux/Mac 部署脚本
|
| 53 |
+
│
|
| 54 |
+
├── deployment/ # 部署配置文件
|
| 55 |
+
│ ├── Dockerfile # Docker 配置
|
| 56 |
+
│ ├── app_modelscope.py # ModelScope 入口
|
| 57 |
+
│ ├── configuration.json # ModelScope 配置
|
| 58 |
+
│ ├── ms_deploy.json # ModelScope 部署配置
|
| 59 |
+
│ ├── requirements_hf.txt # Hugging Face 依赖
|
| 60 |
+
│ ├── requirements_modelscope.txt # ModelScope 依赖
|
| 61 |
+
│ ├── README_HF.md # Hugging Face 说明
|
| 62 |
+
│ ├── README_MODELSCOPE.md # ModelScope 说明
|
| 63 |
+
│ ├── DEPLOY_CHECKLIST.md # 部署检查清单
|
| 64 |
+
│ ├── DEPLOYMENT.md # 部署指南
|
| 65 |
+
│ ├── deploy_to_hf.bat # 部署到 HF 脚本
|
| 66 |
+
│ └── deploy_to_hf.sh # 部署到 HF 脚本
|
| 67 |
+
│
|
| 68 |
+
├── docs/ # 文档目录
|
| 69 |
+
│ ├── README.md # 项目文档
|
| 70 |
+
│ ├── FEATURE_SUMMARY.md # 功能总结
|
| 71 |
+
│ ├── API_配置说明.md # API 配置说明
|
| 72 |
+
│ ├── 局域网访问指南.md # 局域网访问指南
|
| 73 |
+
│ ├── 功能架构图.md # 架构图
|
| 74 |
+
│ ├── 后端启动问题排查.md # 故障排查
|
| 75 |
+
│ ├── 心情气泡池功能说明.md
|
| 76 |
+
│ ├── 心情气泡池快速开始.md
|
| 77 |
+
│ └── 语音录制问题排查.md
|
| 78 |
+
│
|
| 79 |
+
├── .github/ # GitHub 配置
|
| 80 |
+
│ └── workflows/
|
| 81 |
+
│ └── sync.yml # 自动同步工作流
|
| 82 |
+
│
|
| 83 |
+
├── .env # 环境变量(本地)
|
| 84 |
+
├── .env.example # 环境变量示例
|
| 85 |
+
├── .gitignore # Git 忽略文件
|
| 86 |
+
├── requirements.txt # Python 依赖(开发环境)
|
| 87 |
+
├── pytest.ini # Pytest 配置
|
| 88 |
+
├── PRD.md # 产品需求文档
|
| 89 |
+
└── README.md # 项目说明
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
## 目录说明
|
| 93 |
+
|
| 94 |
+
### 核心目录
|
| 95 |
+
|
| 96 |
+
- **app/** - 后端 FastAPI 应用,包含所有业务逻辑
|
| 97 |
+
- **frontend/** - 前端 React 应用,使用 TypeScript + Vite
|
| 98 |
+
- **data/** - 运行时数据存储,JSON 格式
|
| 99 |
+
- **generated_images/** - AI 生成的角色图片
|
| 100 |
+
|
| 101 |
+
### 开发目录
|
| 102 |
+
|
| 103 |
+
- **tests/** - 所有测试文件,包括单元测试和集成测试
|
| 104 |
+
- **scripts/** - 开发和部署脚本
|
| 105 |
+
- **logs/** - 应用日志文件
|
| 106 |
+
|
| 107 |
+
### 部署目录
|
| 108 |
+
|
| 109 |
+
- **deployment/** - 所有部署相关的配置文件
|
| 110 |
+
- Hugging Face Spaces 部署
|
| 111 |
+
- ModelScope 部署
|
| 112 |
+
- Docker 部署
|
| 113 |
+
|
| 114 |
+
### 文档目录
|
| 115 |
+
|
| 116 |
+
- **docs/** - 项目文档和使用指南
|
| 117 |
+
|
| 118 |
+
## 快速开始
|
| 119 |
+
|
| 120 |
+
### 本地开发
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
# 1. 安装依赖
|
| 124 |
+
pip install -r requirements.txt
|
| 125 |
+
cd frontend && npm install && cd ..
|
| 126 |
+
|
| 127 |
+
# 2. 构建前端
|
| 128 |
+
cd frontend && npm run build && cd ..
|
| 129 |
+
|
| 130 |
+
# 3. 启动服务器
|
| 131 |
+
python scripts/start_local.py
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 部署
|
| 135 |
+
|
| 136 |
+
**Hugging Face:**
|
| 137 |
+
```bash
|
| 138 |
+
cd deployment
|
| 139 |
+
./deploy_to_hf.sh
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
**ModelScope:**
|
| 143 |
+
- 上传所有文件到 ModelScope
|
| 144 |
+
- 确保 `ms_deploy.json` 在根目录
|
| 145 |
+
|
| 146 |
+
## 文件清理说明
|
| 147 |
+
|
| 148 |
+
已删除的冗余文件:
|
| 149 |
+
- `app_gradio_old.py.bak` - 旧的 Gradio 备份文件
|
| 150 |
+
- `packages.txt` - 不再使用的包列表
|
| 151 |
+
|
| 152 |
+
已整理的文件:
|
| 153 |
+
- 脚本文件 → `scripts/`
|
| 154 |
+
- 部署文件 → `deployment/`
|
| 155 |
+
- 测试文件 → `tests/`
|
README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Nora - 治愈系记录助手
|
| 3 |
+
emoji: 🌟
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🌟 治愈系记录助手 - SoulMate AI Companion
|
| 12 |
+
|
| 13 |
+
一个温暖、治愈的 AI 陪伴应用,帮助你记录心情、捕捉灵感、管理待办。
|
| 14 |
+
|
| 15 |
+
目前已上线huggingface,体验链接:https://huggingface.co/spaces/kernel14/Nora
|
| 16 |
+
|
| 17 |
+
## ✨ 核心特性
|
| 18 |
+
|
| 19 |
+
- 🎤 **语音/文字快速记录** - 自动分类保存
|
| 20 |
+
- 🤖 **AI 语义解析** - 智能提取情绪、灵感和待办
|
| 21 |
+
- 💬 **AI 对话陪伴(RAG)** - 基于历史记录的个性化对话
|
| 22 |
+
- 🖼️ **AI 形象定制** - 生成专属治愈系角色(720 种组合)
|
| 23 |
+
- 🫧 **物理引擎心情池** - 基于 Matter.js 的动态气泡可视化
|
| 24 |
+
|
| 25 |
+
## 🚀 快速开始
|
| 26 |
+
|
| 27 |
+
### 在线使用
|
| 28 |
+
|
| 29 |
+
直接访问本 Space 即可使用完整功能!
|
| 30 |
+
|
| 31 |
+
### 配置 API 密钥
|
| 32 |
+
|
| 33 |
+
在 Space 的 **Settings → Repository secrets** 中配置:
|
| 34 |
+
|
| 35 |
+
**必需:**
|
| 36 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 37 |
+
- 获取地址:https://open.bigmodel.cn/
|
| 38 |
+
- 用途:语音识别、语义解析、AI 对话
|
| 39 |
+
|
| 40 |
+
**可选:**
|
| 41 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 42 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 43 |
+
- 获取地址:https://platform.minimaxi.com/
|
| 44 |
+
- 用途:AI 形象生成
|
| 45 |
+
|
| 46 |
+
## 📖 使用说明
|
| 47 |
+
|
| 48 |
+
1. **首页快速记录**
|
| 49 |
+
- 点击麦克风录音或在输入框输入文字
|
| 50 |
+
- AI 自动分析并分类保存
|
| 51 |
+
|
| 52 |
+
2. **查看分类数据**
|
| 53 |
+
- 点击顶部心情、灵感、待办图标
|
| 54 |
+
- 查看不同类型的记录
|
| 55 |
+
|
| 56 |
+
3. **与 AI 对话**
|
| 57 |
+
- 点击 AI 形象显示问候对话框
|
| 58 |
+
- 点击对话框中的聊天图标进入完整对话
|
| 59 |
+
- AI 基于你的历史记录提供个性化回复
|
| 60 |
+
|
| 61 |
+
4. **定制 AI 形象**
|
| 62 |
+
- 点击右下角 ✨ 按钮
|
| 63 |
+
- 选择颜色、性格、外观、角色
|
| 64 |
+
- 生成专属形象(需要 MiniMax API)
|
| 65 |
+
|
| 66 |
+
5. **心情气泡池**
|
| 67 |
+
- 点击顶部心情图标
|
| 68 |
+
- 左右滑动查看不同日期的心情卡片
|
| 69 |
+
- 点击卡片展开查看当天的气泡池
|
| 70 |
+
- 可以拖拽气泡,感受物理引擎效果
|
| 71 |
+
|
| 72 |
+
## 📊 API 端点
|
| 73 |
+
|
| 74 |
+
- `POST /api/process` - 处理文本/语音输入
|
| 75 |
+
- `POST /api/chat` - 与 AI 对话(RAG)
|
| 76 |
+
- `GET /api/records` - 获取所有记录
|
| 77 |
+
- `GET /api/moods` - 获取情绪数据
|
| 78 |
+
- `GET /api/inspirations` - 获取灵感
|
| 79 |
+
- `GET /api/todos` - 获取待办事项
|
| 80 |
+
- `POST /api/character/generate` - 生成角色形象
|
| 81 |
+
- `GET /health` - 健康检查
|
| 82 |
+
- `GET /docs` - API 文档
|
| 83 |
+
|
| 84 |
+
## 🔗 相关链接
|
| 85 |
+
|
| 86 |
+
- [GitHub 仓库](https://github.com/kernel-14/Nora)
|
| 87 |
+
- [详细文档](https://github.com/kernel-14/Nora/blob/main/README.md)
|
| 88 |
+
- [智谱 AI](https://open.bigmodel.cn/)
|
| 89 |
+
- [MiniMax](https://platform.minimaxi.com/)
|
| 90 |
+
- [Huggingface](https://huggingface.co/spaces/kernel14/Nora)
|
| 91 |
+
|
| 92 |
+
## 📝 技术栈
|
| 93 |
+
|
| 94 |
+
- **后端**: FastAPI + Python 3.11
|
| 95 |
+
- **前端**: React + TypeScript + Vite
|
| 96 |
+
- **物理引擎**: Matter.js
|
| 97 |
+
- **AI 服务**: 智谱 AI (GLM-4) + MiniMax
|
| 98 |
+
- **部署**: Hugging Face Spaces (Docker)
|
| 99 |
+
|
| 100 |
+
## 🔧 本地开发
|
| 101 |
+
|
| 102 |
+
### 启动后端服务
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# 安装依赖
|
| 106 |
+
pip install -r requirements.txt
|
| 107 |
+
|
| 108 |
+
# 配置环境变量(复制 .env.example 为 .env 并填写)
|
| 109 |
+
cp .env.example .env
|
| 110 |
+
|
| 111 |
+
# 启动服务(端口 8000)
|
| 112 |
+
python scripts/start_local.py
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### 构建前端
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
cd frontend
|
| 119 |
+
npm install
|
| 120 |
+
npm run build
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### 局域网访问
|
| 124 |
+
|
| 125 |
+
1. 启动后端后,会显示局域网访问地址(如 `http://192.168.1.100:8000/`)
|
| 126 |
+
2. 其他设备连接同一 WiFi 后,使用该地址访问
|
| 127 |
+
3. 如果无法访问,请参考 [局域网访问快速修复指南](docs/局域网访问快速修复.md)
|
| 128 |
+
|
| 129 |
+
**快速诊断**:
|
| 130 |
+
```bash
|
| 131 |
+
# Windows
|
| 132 |
+
scripts\test_lan_access.bat
|
| 133 |
+
|
| 134 |
+
# 或访问诊断页面
|
| 135 |
+
http://你的IP:8000/test-connection.html
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
## 🐛 故障排查
|
| 139 |
+
|
| 140 |
+
### 问题:其他设备访问显示 "Load failed"
|
| 141 |
+
|
| 142 |
+
**原因**:防火墙阻止、网络隔离或 API 地址配置错误
|
| 143 |
+
|
| 144 |
+
**解决方案**:
|
| 145 |
+
1. 运行诊断工具:`scripts\test_lan_access.bat`
|
| 146 |
+
2. 访问诊断页面:`http://你的IP:8000/test-connection.html`
|
| 147 |
+
3. 查看详细指南:[局域网访问快速修复](docs/局域网访问快速修复.md)
|
| 148 |
+
|
| 149 |
+
### 问题:语音识别失败
|
| 150 |
+
|
| 151 |
+
**原因**:未配置 ZHIPU_API_KEY 或 API 配额不足
|
| 152 |
+
|
| 153 |
+
**解决方案**:
|
| 154 |
+
1. 检查 `.env` 文件中的 `ZHIPU_API_KEY`
|
| 155 |
+
2. 访问 https://open.bigmodel.cn/ 检查配额
|
| 156 |
+
|
| 157 |
+
### 问题:AI 形象生成失败
|
| 158 |
+
|
| 159 |
+
**原因**:未配置 MINIMAX_API_KEY 或 API 配额不足
|
| 160 |
+
|
| 161 |
+
**解决方案**:
|
| 162 |
+
1. 检查 `.env` 文件中的 `MINIMAX_API_KEY` 和 `MINIMAX_GROUP_ID`
|
| 163 |
+
2. 访问 https://platform.minimaxi.com/ 检查配额
|
| 164 |
+
|
| 165 |
+
## 📚 文档
|
| 166 |
+
|
| 167 |
+
- [功能架构图](docs/功能架构图.md)
|
| 168 |
+
- [API 配置说明](docs/API_配置说明.md)
|
| 169 |
+
- [局域网访问指南](docs/局域网访问指南.md)
|
| 170 |
+
- [局域网访问快速修复](docs/局域网访问快速修复.md)
|
| 171 |
+
- [心情气泡池功能说明](docs/心情气泡池功能说明.md)
|
| 172 |
+
|
| 173 |
+
## 📄 License
|
| 174 |
+
|
| 175 |
+
MIT License
|
README_HF.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Nora - 治愈系记录助手
|
| 3 |
+
emoji: 🌟
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🌟 治愈系记录助手 - SoulMate AI Companion
|
| 12 |
+
|
| 13 |
+
一个温暖、治愈的 AI 陪伴应用,帮助你记录心情、捕捉灵感、管理待办。
|
| 14 |
+
|
| 15 |
+
## ✨ 核心特性
|
| 16 |
+
|
| 17 |
+
- 🎤 **语音/文字快速记录** - 自动分类保存
|
| 18 |
+
- 🤖 **AI 语义解析** - 智能提取情绪、灵感和待办
|
| 19 |
+
- 💬 **AI 对话陪伴(RAG)** - 基于历史记录的个性化对话
|
| 20 |
+
- 🖼️ **AI 形象定制** - 生成专属治愈系角色(720 种组合)
|
| 21 |
+
- 🫧 **物理引擎心情池** - 基于 Matter.js 的动态气泡可视化
|
| 22 |
+
|
| 23 |
+
## 🚀 快速开始
|
| 24 |
+
|
| 25 |
+
### 在线使用
|
| 26 |
+
|
| 27 |
+
直接访问本 Space 即可使用完整功能!
|
| 28 |
+
|
| 29 |
+
### ⚙️ 配置 API 密钥
|
| 30 |
+
|
| 31 |
+
在 Space 的 **Settings → Variables and secrets** 中配置:
|
| 32 |
+
|
| 33 |
+
**必需:**
|
| 34 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 35 |
+
- 获取地址:https://open.bigmodel.cn/
|
| 36 |
+
- 用途:语音识别、语义解析、AI 对话
|
| 37 |
+
|
| 38 |
+
**可选:**
|
| 39 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 40 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 41 |
+
- 获取地址:https://platform.minimaxi.com/
|
| 42 |
+
- 用途:AI 形象生成
|
| 43 |
+
|
| 44 |
+
配置后,点击 **Factory reboot** 重启 Space 使配置生效。
|
| 45 |
+
|
| 46 |
+
## 📖 使用说明
|
| 47 |
+
|
| 48 |
+
1. **首页快速记录**
|
| 49 |
+
- 点击麦克风录音或在输入框输入文字
|
| 50 |
+
- AI 自动分析并分类保存
|
| 51 |
+
|
| 52 |
+
2. **查看分类数据**
|
| 53 |
+
- 点击顶部心情、灵感、待办图标
|
| 54 |
+
- 查看不同类型的记录
|
| 55 |
+
|
| 56 |
+
3. **与 AI 对话**
|
| 57 |
+
- 点击 AI 形象显示问候对话框
|
| 58 |
+
- 点击对话框中的聊天图标进入完整对话
|
| 59 |
+
- AI 基于你的历史记录提供个性化回复
|
| 60 |
+
|
| 61 |
+
4. **定制 AI 形象**
|
| 62 |
+
- 点击右下角 ✨ 按钮
|
| 63 |
+
- 选择颜色、性格、外观、角色
|
| 64 |
+
- 生成专属形象(需要 MiniMax API)
|
| 65 |
+
|
| 66 |
+
5. **心情气泡池**
|
| 67 |
+
- 点击顶部心情图标
|
| 68 |
+
- 左右滑动查看不同日期的心情卡片
|
| 69 |
+
- 点击卡片展开查看当天的气泡池
|
| 70 |
+
- 可以拖拽气泡,感受物理引擎效果
|
| 71 |
+
|
| 72 |
+
## 📊 API 端点
|
| 73 |
+
|
| 74 |
+
- `POST /api/process` - 处理文本/语音输入
|
| 75 |
+
- `POST /api/chat` - 与 AI 对话(RAG)
|
| 76 |
+
- `GET /api/records` - 获取所有记录
|
| 77 |
+
- `GET /api/moods` - 获取情绪数据
|
| 78 |
+
- `GET /api/inspirations` - 获取灵感
|
| 79 |
+
- `GET /api/todos` - 获取待办事项
|
| 80 |
+
- `POST /api/character/generate` - 生成角色形象
|
| 81 |
+
- `GET /health` - 健康检查
|
| 82 |
+
- `GET /docs` - API 文档
|
| 83 |
+
|
| 84 |
+
## 🔗 相关链接
|
| 85 |
+
|
| 86 |
+
- [GitHub 仓库](https://github.com/kernel-14/Nora)
|
| 87 |
+
- [完整文档](https://github.com/kernel-14/Nora/blob/main/README.md)
|
| 88 |
+
- [智谱 AI](https://open.bigmodel.cn/)
|
| 89 |
+
- [MiniMax](https://platform.minimaxi.com/)
|
| 90 |
+
|
| 91 |
+
## 📝 技术栈
|
| 92 |
+
|
| 93 |
+
- **后端**: FastAPI + Python 3.11
|
| 94 |
+
- **前端**: React + TypeScript + Vite
|
| 95 |
+
- **物理引擎**: Matter.js
|
| 96 |
+
- **AI 服务**: 智谱 AI (GLM-4) + MiniMax
|
| 97 |
+
- **部署**: Hugging Face Spaces (Docker)
|
| 98 |
+
|
| 99 |
+
## 🐛 故障排查
|
| 100 |
+
|
| 101 |
+
### 问题:语音识别失败
|
| 102 |
+
|
| 103 |
+
**原因**:未配置 ZHIPU_API_KEY 或 API 配额不足
|
| 104 |
+
|
| 105 |
+
**解决方案**:
|
| 106 |
+
1. 在 Space Settings 中配置 `ZHIPU_API_KEY`
|
| 107 |
+
2. 访问 https://open.bigmodel.cn/ 检查配额
|
| 108 |
+
3. Factory reboot 重启 Space
|
| 109 |
+
|
| 110 |
+
### 问题:AI 形象生成失败
|
| 111 |
+
|
| 112 |
+
**原因**:未配置 MINIMAX_API_KEY 或 API 配额不足
|
| 113 |
+
|
| 114 |
+
**解决方案**:
|
| 115 |
+
1. 在 Space Settings 中配置 `MINIMAX_API_KEY` 和 `MINIMAX_GROUP_ID`
|
| 116 |
+
2. 访问 https://platform.minimaxi.com/ 检查配额
|
| 117 |
+
3. Factory reboot 重启 Space
|
| 118 |
+
|
| 119 |
+
### 问题:Space 构建失败
|
| 120 |
+
|
| 121 |
+
**原因**:缺少必要的文件或配置
|
| 122 |
+
|
| 123 |
+
**检查清单**:
|
| 124 |
+
- ✅ 根目录有 `Dockerfile`
|
| 125 |
+
- ✅ 根目录有 `start.py`
|
| 126 |
+
- ✅ 根目录有 `requirements.txt`
|
| 127 |
+
- ✅ `frontend/dist/` 目录存在且包含构建文件
|
| 128 |
+
|
| 129 |
+
## 📄 License
|
| 130 |
+
|
| 131 |
+
MIT License
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Voice Text Processor Application"""
|
app/asr_service.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ASR (Automatic Speech Recognition) service for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module implements the ASRService class for transcribing audio files
|
| 4 |
+
to text using the Zhipu AI GLM-ASR-2512 API.
|
| 5 |
+
|
| 6 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Optional
|
| 11 |
+
import httpx
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ASRServiceError(Exception):
|
| 18 |
+
"""Exception raised when ASR service operations fail.
|
| 19 |
+
|
| 20 |
+
This exception is raised when the Zhipu ASR API call fails,
|
| 21 |
+
such as due to network issues, API errors, or invalid responses.
|
| 22 |
+
|
| 23 |
+
Requirements: 2.3
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, message: str = "语音识别服务不可用"):
|
| 27 |
+
"""Initialize ASRServiceError.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
message: Error message describing the failure
|
| 31 |
+
"""
|
| 32 |
+
super().__init__(message)
|
| 33 |
+
self.message = message
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ASRService:
|
| 37 |
+
"""Service for transcribing audio files using Zhipu AI ASR API.
|
| 38 |
+
|
| 39 |
+
This service handles audio file transcription by calling the Zhipu AI
|
| 40 |
+
GLM-ASR-2512 API. It manages API authentication, request formatting,
|
| 41 |
+
response parsing, and error handling.
|
| 42 |
+
|
| 43 |
+
Attributes:
|
| 44 |
+
api_key: Zhipu AI API key for authentication
|
| 45 |
+
client: Async HTTP client for making API requests
|
| 46 |
+
api_url: Zhipu AI ASR API endpoint URL
|
| 47 |
+
model: ASR model identifier
|
| 48 |
+
|
| 49 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, api_key: str):
|
| 53 |
+
"""Initialize the ASR service.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
api_key: Zhipu AI API key for authentication
|
| 57 |
+
"""
|
| 58 |
+
self.api_key = api_key
|
| 59 |
+
self.client = httpx.AsyncClient(timeout=30.0)
|
| 60 |
+
self.api_url = "https://api.z.ai/api/paas/v4/audio/transcriptions"
|
| 61 |
+
self.model = "glm-asr-2512"
|
| 62 |
+
|
| 63 |
+
async def close(self):
|
| 64 |
+
"""Close the HTTP client.
|
| 65 |
+
|
| 66 |
+
This should be called when the service is no longer needed
|
| 67 |
+
to properly clean up resources.
|
| 68 |
+
"""
|
| 69 |
+
await self.client.aclose()
|
| 70 |
+
|
| 71 |
+
async def transcribe(self, audio_file: bytes, filename: str = "audio.mp3") -> str:
|
| 72 |
+
"""Transcribe audio file to text using Zhipu ASR API.
|
| 73 |
+
|
| 74 |
+
This method sends the audio file to the Zhipu AI ASR API and returns
|
| 75 |
+
the transcribed text. It handles API errors, empty recognition results,
|
| 76 |
+
and logs all errors with timestamps and stack traces.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
audio_file: Audio file content as bytes
|
| 80 |
+
filename: Name of the audio file (for API request)
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
Transcribed text content. Returns empty string if audio cannot
|
| 84 |
+
be recognized (empty recognition result).
|
| 85 |
+
|
| 86 |
+
Raises:
|
| 87 |
+
ASRServiceError: If API call fails or returns invalid response
|
| 88 |
+
|
| 89 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
|
| 90 |
+
"""
|
| 91 |
+
try:
|
| 92 |
+
# Prepare request headers
|
| 93 |
+
headers = {
|
| 94 |
+
"Authorization": f"Bearer {self.api_key}"
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Prepare multipart form data
|
| 98 |
+
files = {
|
| 99 |
+
"file": (filename, audio_file, "audio/mpeg")
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
data = {
|
| 103 |
+
"model": self.model,
|
| 104 |
+
"stream": "false"
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
logger.info(f"Calling Zhipu ASR API for file: {filename}")
|
| 108 |
+
|
| 109 |
+
# Make API request
|
| 110 |
+
response = await self.client.post(
|
| 111 |
+
self.api_url,
|
| 112 |
+
headers=headers,
|
| 113 |
+
files=files,
|
| 114 |
+
data=data
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# Check response status
|
| 118 |
+
if response.status_code != 200:
|
| 119 |
+
error_msg = f"ASR API returned status {response.status_code}"
|
| 120 |
+
try:
|
| 121 |
+
error_detail = response.json()
|
| 122 |
+
error_msg += f": {error_detail}"
|
| 123 |
+
except Exception:
|
| 124 |
+
error_msg += f": {response.text}"
|
| 125 |
+
|
| 126 |
+
logger.error(
|
| 127 |
+
f"ASR API call failed: {error_msg}",
|
| 128 |
+
exc_info=True,
|
| 129 |
+
extra={"timestamp": logger.makeRecord(
|
| 130 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 131 |
+
).created}
|
| 132 |
+
)
|
| 133 |
+
raise ASRServiceError(f"语音识别服务不可用: {error_msg}")
|
| 134 |
+
|
| 135 |
+
# Parse response
|
| 136 |
+
try:
|
| 137 |
+
result = response.json()
|
| 138 |
+
except Exception as e:
|
| 139 |
+
error_msg = f"Failed to parse ASR API response: {str(e)}"
|
| 140 |
+
logger.error(
|
| 141 |
+
error_msg,
|
| 142 |
+
exc_info=True,
|
| 143 |
+
extra={"timestamp": logger.makeRecord(
|
| 144 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 145 |
+
).created}
|
| 146 |
+
)
|
| 147 |
+
raise ASRServiceError(f"语音识别服务不可用: 响应格式无效")
|
| 148 |
+
|
| 149 |
+
# Extract transcribed text
|
| 150 |
+
text = result.get("text", "")
|
| 151 |
+
|
| 152 |
+
# Handle empty recognition result
|
| 153 |
+
if not text or text.strip() == "":
|
| 154 |
+
logger.warning(
|
| 155 |
+
f"ASR returned empty text for file: {filename}. "
|
| 156 |
+
"Audio content may be unrecognizable."
|
| 157 |
+
)
|
| 158 |
+
return ""
|
| 159 |
+
|
| 160 |
+
logger.info(
|
| 161 |
+
f"ASR transcription successful for {filename}. "
|
| 162 |
+
f"Text length: {len(text)} characters"
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return text
|
| 166 |
+
|
| 167 |
+
except ASRServiceError:
|
| 168 |
+
# Re-raise ASRServiceError as-is
|
| 169 |
+
raise
|
| 170 |
+
|
| 171 |
+
except httpx.TimeoutException as e:
|
| 172 |
+
error_msg = f"ASR API request timeout: {str(e)}"
|
| 173 |
+
logger.error(
|
| 174 |
+
error_msg,
|
| 175 |
+
exc_info=True,
|
| 176 |
+
extra={"timestamp": logger.makeRecord(
|
| 177 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 178 |
+
).created}
|
| 179 |
+
)
|
| 180 |
+
raise ASRServiceError("语音识别服务不可用: 请求超时")
|
| 181 |
+
|
| 182 |
+
except httpx.RequestError as e:
|
| 183 |
+
error_msg = f"ASR API request failed: {str(e)}"
|
| 184 |
+
logger.error(
|
| 185 |
+
error_msg,
|
| 186 |
+
exc_info=True,
|
| 187 |
+
extra={"timestamp": logger.makeRecord(
|
| 188 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 189 |
+
).created}
|
| 190 |
+
)
|
| 191 |
+
raise ASRServiceError(f"语音识别服务不可用: 网络错误")
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
error_msg = f"Unexpected error in ASR service: {str(e)}"
|
| 195 |
+
logger.error(
|
| 196 |
+
error_msg,
|
| 197 |
+
exc_info=True,
|
| 198 |
+
extra={"timestamp": logger.makeRecord(
|
| 199 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 200 |
+
).created}
|
| 201 |
+
)
|
| 202 |
+
raise ASRServiceError(f"语音识别服务不可用: {str(e)}")
|
app/config.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration management module for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module handles loading configuration from environment variables,
|
| 4 |
+
validating required settings, and providing configuration access throughout
|
| 5 |
+
the application.
|
| 6 |
+
|
| 7 |
+
Requirements: 10.1, 10.2, 10.3, 10.4, 10.5
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional
|
| 13 |
+
from pydantic import BaseModel, Field, field_validator
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Config(BaseModel):
|
| 18 |
+
"""Application configuration loaded from environment variables."""
|
| 19 |
+
|
| 20 |
+
# API Keys
|
| 21 |
+
zhipu_api_key: str = Field(
|
| 22 |
+
...,
|
| 23 |
+
description="Zhipu AI API key for ASR and GLM-4-Flash services"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
minimax_api_key: Optional[str] = Field(
|
| 27 |
+
default=None,
|
| 28 |
+
description="MiniMax API key for image generation (optional)"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
minimax_group_id: Optional[str] = Field(
|
| 32 |
+
default=None,
|
| 33 |
+
description="MiniMax Group ID (optional)"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Data storage paths
|
| 37 |
+
data_dir: Path = Field(
|
| 38 |
+
default=Path("data"),
|
| 39 |
+
description="Directory for storing JSON data files"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# File size limits (in bytes)
|
| 43 |
+
max_audio_size: int = Field(
|
| 44 |
+
default=10 * 1024 * 1024, # 10 MB default
|
| 45 |
+
description="Maximum audio file size in bytes"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Logging configuration
|
| 49 |
+
log_level: str = Field(
|
| 50 |
+
default="INFO",
|
| 51 |
+
description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
log_file: Optional[Path] = Field(
|
| 55 |
+
default=Path("logs/app.log"),
|
| 56 |
+
description="Log file path"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Server configuration
|
| 60 |
+
host: str = Field(
|
| 61 |
+
default="0.0.0.0",
|
| 62 |
+
description="Server host"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
port: int = Field(
|
| 66 |
+
default=8000,
|
| 67 |
+
description="Server port"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
@field_validator("log_level")
|
| 71 |
+
@classmethod
|
| 72 |
+
def validate_log_level(cls, v: str) -> str:
|
| 73 |
+
"""Validate log level is one of the standard levels."""
|
| 74 |
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
| 75 |
+
v_upper = v.upper()
|
| 76 |
+
if v_upper not in valid_levels:
|
| 77 |
+
raise ValueError(f"log_level must be one of {valid_levels}")
|
| 78 |
+
return v_upper
|
| 79 |
+
|
| 80 |
+
@field_validator("max_audio_size")
|
| 81 |
+
@classmethod
|
| 82 |
+
def validate_max_audio_size(cls, v: int) -> int:
|
| 83 |
+
"""Validate max audio size is positive."""
|
| 84 |
+
if v <= 0:
|
| 85 |
+
raise ValueError("max_audio_size must be positive")
|
| 86 |
+
return v
|
| 87 |
+
|
| 88 |
+
@field_validator("data_dir", "log_file")
|
| 89 |
+
@classmethod
|
| 90 |
+
def convert_to_path(cls, v) -> Path:
|
| 91 |
+
"""Convert string paths to Path objects."""
|
| 92 |
+
if isinstance(v, str):
|
| 93 |
+
return Path(v)
|
| 94 |
+
return v
|
| 95 |
+
|
| 96 |
+
class Config:
|
| 97 |
+
"""Pydantic configuration."""
|
| 98 |
+
frozen = True # Make config immutable
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def load_config() -> Config:
|
| 102 |
+
"""Load configuration from environment variables.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Config: Validated configuration object
|
| 106 |
+
|
| 107 |
+
Raises:
|
| 108 |
+
ValueError: If required configuration is missing or invalid
|
| 109 |
+
|
| 110 |
+
Environment Variables:
|
| 111 |
+
ZHIPU_API_KEY: Required. API key for Zhipu AI services
|
| 112 |
+
MINIMAX_API_KEY: Optional. API key for MiniMax image generation
|
| 113 |
+
MINIMAX_GROUP_ID: Optional. MiniMax Group ID
|
| 114 |
+
DATA_DIR: Optional. Directory for data storage (default: data/)
|
| 115 |
+
MAX_AUDIO_SIZE: Optional. Max audio file size in bytes (default: 10MB)
|
| 116 |
+
LOG_LEVEL: Optional. Logging level (default: INFO)
|
| 117 |
+
LOG_FILE: Optional. Log file path (default: logs/app.log)
|
| 118 |
+
HOST: Optional. Server host (default: 0.0.0.0)
|
| 119 |
+
PORT: Optional. Server port (default: 8000)
|
| 120 |
+
"""
|
| 121 |
+
# Load environment variables from .env file
|
| 122 |
+
load_dotenv()
|
| 123 |
+
|
| 124 |
+
# Load from environment variables
|
| 125 |
+
config_dict = {
|
| 126 |
+
"zhipu_api_key": os.getenv("ZHIPU_API_KEY"),
|
| 127 |
+
"minimax_api_key": os.getenv("MINIMAX_API_KEY"),
|
| 128 |
+
"minimax_group_id": os.getenv("MINIMAX_GROUP_ID"),
|
| 129 |
+
"data_dir": os.getenv("DATA_DIR", "data"),
|
| 130 |
+
"max_audio_size": int(os.getenv("MAX_AUDIO_SIZE", str(10 * 1024 * 1024))),
|
| 131 |
+
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
| 132 |
+
"log_file": os.getenv("LOG_FILE", "logs/app.log"),
|
| 133 |
+
"host": os.getenv("HOST", "0.0.0.0"),
|
| 134 |
+
"port": int(os.getenv("PORT", "8000")),
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
# Validate required fields
|
| 138 |
+
if not config_dict["zhipu_api_key"]:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"ZHIPU_API_KEY environment variable is required. "
|
| 141 |
+
"Please set it before starting the application."
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# Create and validate config
|
| 145 |
+
try:
|
| 146 |
+
config = Config(**config_dict)
|
| 147 |
+
except Exception as e:
|
| 148 |
+
raise ValueError(f"Configuration validation failed: {e}")
|
| 149 |
+
|
| 150 |
+
# Ensure data directory exists
|
| 151 |
+
config.data_dir.mkdir(parents=True, exist_ok=True)
|
| 152 |
+
|
| 153 |
+
# Ensure log directory exists
|
| 154 |
+
if config.log_file:
|
| 155 |
+
config.log_file.parent.mkdir(parents=True, exist_ok=True)
|
| 156 |
+
|
| 157 |
+
return config
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def validate_config(config: Config) -> None:
|
| 161 |
+
"""Validate configuration at startup.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
config: Configuration object to validate
|
| 165 |
+
|
| 166 |
+
Raises:
|
| 167 |
+
ValueError: If configuration is invalid or required resources are unavailable
|
| 168 |
+
"""
|
| 169 |
+
# Check data directory is writable
|
| 170 |
+
if not os.access(config.data_dir, os.W_OK):
|
| 171 |
+
raise ValueError(
|
| 172 |
+
f"Data directory {config.data_dir} is not writable. "
|
| 173 |
+
"Please check permissions."
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Check log directory is writable
|
| 177 |
+
if config.log_file and not os.access(config.log_file.parent, os.W_OK):
|
| 178 |
+
raise ValueError(
|
| 179 |
+
f"Log directory {config.log_file.parent} is not writable. "
|
| 180 |
+
"Please check permissions."
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Validate API key format (basic check)
|
| 184 |
+
if len(config.zhipu_api_key) < 10:
|
| 185 |
+
raise ValueError(
|
| 186 |
+
"ZHIPU_API_KEY appears to be invalid (too short). "
|
| 187 |
+
"Please check your API key."
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# Global config instance (loaded on import)
|
| 192 |
+
_config: Optional[Config] = None
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def get_config() -> Config:
|
| 196 |
+
"""Get the global configuration instance.
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
Config: The application configuration
|
| 200 |
+
|
| 201 |
+
Raises:
|
| 202 |
+
RuntimeError: If configuration has not been initialized
|
| 203 |
+
"""
|
| 204 |
+
global _config
|
| 205 |
+
if _config is None:
|
| 206 |
+
raise RuntimeError(
|
| 207 |
+
"Configuration not initialized. Call init_config() first."
|
| 208 |
+
)
|
| 209 |
+
return _config
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def init_config() -> Config:
|
| 213 |
+
"""Initialize the global configuration.
|
| 214 |
+
|
| 215 |
+
This should be called once at application startup.
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
Config: The initialized configuration
|
| 219 |
+
|
| 220 |
+
Raises:
|
| 221 |
+
ValueError: If configuration is invalid
|
| 222 |
+
"""
|
| 223 |
+
global _config
|
| 224 |
+
_config = load_config()
|
| 225 |
+
validate_config(_config)
|
| 226 |
+
return _config
|
app/image_service.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image Generation service for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module implements the ImageGenerationService class for generating
|
| 4 |
+
cat character images using the MiniMax Text-to-Image API.
|
| 5 |
+
|
| 6 |
+
Requirements: PRD - AI形象生成模块
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import httpx
|
| 11 |
+
from typing import Optional, Dict, List
|
| 12 |
+
import time
|
| 13 |
+
import json
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ImageGenerationError(Exception):
|
| 20 |
+
"""Exception raised when image generation operations fail.
|
| 21 |
+
|
| 22 |
+
This exception is raised when the MiniMax API call fails,
|
| 23 |
+
such as due to network issues, API errors, or invalid responses.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, message: str = "图像生成服务不可用"):
|
| 27 |
+
"""Initialize ImageGenerationError.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
message: Error message describing the failure
|
| 31 |
+
"""
|
| 32 |
+
super().__init__(message)
|
| 33 |
+
self.message = message
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ImageGenerationService:
|
| 37 |
+
"""Service for generating cat character images using MiniMax API.
|
| 38 |
+
|
| 39 |
+
This service handles image generation by calling the MiniMax Text-to-Image API
|
| 40 |
+
to create healing-style cat illustrations based on user preferences
|
| 41 |
+
(color, personality, appearance).
|
| 42 |
+
|
| 43 |
+
Attributes:
|
| 44 |
+
api_key: MiniMax API key for authentication
|
| 45 |
+
group_id: MiniMax group ID for authentication
|
| 46 |
+
client: Async HTTP client for making API requests
|
| 47 |
+
api_url: MiniMax API endpoint URL
|
| 48 |
+
model: Model identifier (text-to-image-v2)
|
| 49 |
+
|
| 50 |
+
Requirements: PRD - AI形象生成模块
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
# 颜色映射
|
| 54 |
+
COLOR_MAPPING = {
|
| 55 |
+
"温暖粉": "soft pastel pink fur, rose-colored aesthetic",
|
| 56 |
+
"天空蓝": "light sky blue fur, serene blue atmosphere",
|
| 57 |
+
"薄荷绿": "mint green fur, fresh green ambiance",
|
| 58 |
+
"奶油黄": "cream yellow fur, warm golden glow",
|
| 59 |
+
"薰衣草紫": "lavender purple fur, gentle purple tones",
|
| 60 |
+
"珊瑚橙": "coral orange fur, warm peachy atmosphere",
|
| 61 |
+
"纯白": "pure white fur, clean minimalist aesthetic",
|
| 62 |
+
"浅灰": "light gray fur, soft neutral tones"
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# 性格映射
|
| 66 |
+
PERSONALITY_MAPPING = {
|
| 67 |
+
"活泼": "big curious eyes, dynamic paw gesture, energetic aura, playful expression",
|
| 68 |
+
"温柔": "soft gentle eyes, calm posture, peaceful expression, caring demeanor",
|
| 69 |
+
"聪明": "intelligent eyes, thoughtful expression, wise appearance, attentive look",
|
| 70 |
+
"慵懒": "relaxed eyes, lounging posture, comfortable expression, laid-back vibe",
|
| 71 |
+
"勇敢": "confident eyes, strong posture, determined expression, courageous stance",
|
| 72 |
+
"害羞": "shy eyes, timid posture, gentle expression, reserved demeanor"
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# 形象特征映射
|
| 76 |
+
APPEARANCE_MAPPING = {
|
| 77 |
+
"戴眼镜": "wearing tiny round glasses, scholarly look",
|
| 78 |
+
"戴帽子": "wearing a cute small hat, fashionable style",
|
| 79 |
+
"戴围巾": "wearing a cozy scarf, warm appearance",
|
| 80 |
+
"戴蝴蝶结": "wearing a cute bow tie, elegant look",
|
| 81 |
+
"无配饰": "natural appearance, simple and pure"
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# 角色类型映射
|
| 85 |
+
ROLE_MAPPING = {
|
| 86 |
+
"陪伴式朋友": "friendly companion, approachable and warm",
|
| 87 |
+
"温柔照顾型长辈": "caring elder figure, nurturing and protective",
|
| 88 |
+
"引导型老师": "wise teacher figure, knowledgeable and patient"
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# 系统底座提示词
|
| 92 |
+
BASE_PROMPT = (
|
| 93 |
+
"A masterpiece cute stylized cat illustration, {color} theme, "
|
| 94 |
+
"{personality} facial expression and posture, {appearance}. "
|
| 95 |
+
"{role}. Japanese watercolor style, clean minimalist background, "
|
| 96 |
+
"high quality, soft studio lighting, 4k, healing aesthetic, "
|
| 97 |
+
"adorable and heartwarming"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
def __init__(self, api_key: str, group_id: Optional[str] = None):
|
| 101 |
+
"""Initialize the image generation service.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
api_key: MiniMax API key for authentication
|
| 105 |
+
group_id: MiniMax group ID (optional, for compatibility)
|
| 106 |
+
"""
|
| 107 |
+
self.api_key = api_key
|
| 108 |
+
self.group_id = group_id # 保留但不使用
|
| 109 |
+
self.client = httpx.AsyncClient(timeout=120.0) # 图像生成需要更长时间
|
| 110 |
+
self.api_url = "https://api.minimaxi.com/v1/image_generation"
|
| 111 |
+
self.model = "image-01"
|
| 112 |
+
|
| 113 |
+
async def close(self):
|
| 114 |
+
"""Close the HTTP client.
|
| 115 |
+
|
| 116 |
+
This should be called when the service is no longer needed
|
| 117 |
+
to properly clean up resources.
|
| 118 |
+
"""
|
| 119 |
+
await self.client.aclose()
|
| 120 |
+
|
| 121 |
+
async def download_image(self, url: str, save_path: str) -> str:
|
| 122 |
+
"""Download image from URL and save to local file.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
url: Image URL to download
|
| 126 |
+
save_path: Local file path to save the image
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
Absolute path to the saved image file
|
| 130 |
+
|
| 131 |
+
Raises:
|
| 132 |
+
ImageGenerationError: If download fails
|
| 133 |
+
"""
|
| 134 |
+
try:
|
| 135 |
+
logger.info(f"Downloading image from: {url}")
|
| 136 |
+
|
| 137 |
+
# 创建保存目录(如果不存在)
|
| 138 |
+
save_path_obj = Path(save_path)
|
| 139 |
+
save_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
| 140 |
+
|
| 141 |
+
# 下载图像
|
| 142 |
+
response = await self.client.get(url, timeout=60.0)
|
| 143 |
+
|
| 144 |
+
if response.status_code != 200:
|
| 145 |
+
error_msg = f"Failed to download image: HTTP {response.status_code}"
|
| 146 |
+
logger.error(error_msg)
|
| 147 |
+
raise ImageGenerationError(error_msg)
|
| 148 |
+
|
| 149 |
+
# 保存到文件
|
| 150 |
+
with open(save_path, 'wb') as f:
|
| 151 |
+
f.write(response.content)
|
| 152 |
+
|
| 153 |
+
abs_path = str(save_path_obj.absolute())
|
| 154 |
+
logger.info(f"Image saved to: {abs_path}")
|
| 155 |
+
|
| 156 |
+
return abs_path
|
| 157 |
+
|
| 158 |
+
except ImageGenerationError:
|
| 159 |
+
raise
|
| 160 |
+
except Exception as e:
|
| 161 |
+
error_msg = f"Failed to download image: {str(e)}"
|
| 162 |
+
logger.error(error_msg)
|
| 163 |
+
raise ImageGenerationError(error_msg)
|
| 164 |
+
|
| 165 |
+
def build_prompt(
|
| 166 |
+
self,
|
| 167 |
+
color: str = "温暖粉",
|
| 168 |
+
personality: str = "温柔",
|
| 169 |
+
appearance: str = "无配饰",
|
| 170 |
+
role: str = "陪伴式朋友"
|
| 171 |
+
) -> str:
|
| 172 |
+
"""Build the complete prompt for image generation.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
color: Color preference (温暖粉/天空蓝/薄荷绿等)
|
| 176 |
+
personality: Personality trait (活泼/温柔/聪明等)
|
| 177 |
+
appearance: Appearance feature (戴眼镜/戴帽子等)
|
| 178 |
+
role: Character role (陪伴式朋友/温柔照顾型长辈等)
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
Complete prompt string for CogView API
|
| 182 |
+
"""
|
| 183 |
+
# 获取映射值,如果没有则使用默认值
|
| 184 |
+
color_desc = self.COLOR_MAPPING.get(color, self.COLOR_MAPPING["温暖粉"])
|
| 185 |
+
personality_desc = self.PERSONALITY_MAPPING.get(
|
| 186 |
+
personality,
|
| 187 |
+
self.PERSONALITY_MAPPING["温柔"]
|
| 188 |
+
)
|
| 189 |
+
appearance_desc = self.APPEARANCE_MAPPING.get(
|
| 190 |
+
appearance,
|
| 191 |
+
self.APPEARANCE_MAPPING["无配饰"]
|
| 192 |
+
)
|
| 193 |
+
role_desc = self.ROLE_MAPPING.get(
|
| 194 |
+
role,
|
| 195 |
+
self.ROLE_MAPPING["陪伴式朋友"]
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# 构建完整提示词
|
| 199 |
+
prompt = self.BASE_PROMPT.format(
|
| 200 |
+
color=color_desc,
|
| 201 |
+
personality=personality_desc,
|
| 202 |
+
appearance=appearance_desc,
|
| 203 |
+
role=role_desc
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
logger.info(f"Generated prompt: {prompt[:100]}...")
|
| 207 |
+
return prompt
|
| 208 |
+
|
| 209 |
+
async def generate_image(
|
| 210 |
+
self,
|
| 211 |
+
color: str = "温暖粉",
|
| 212 |
+
personality: str = "温柔",
|
| 213 |
+
appearance: str = "无配饰",
|
| 214 |
+
role: str = "陪伴式朋友",
|
| 215 |
+
aspect_ratio: str = "1:1",
|
| 216 |
+
n: int = 1,
|
| 217 |
+
response_format: str = "url"
|
| 218 |
+
) -> Dict[str, str]:
|
| 219 |
+
"""Generate a cat character image using MiniMax API.
|
| 220 |
+
|
| 221 |
+
This method sends a request to the MiniMax API with the constructed
|
| 222 |
+
prompt and returns the generated image URL or base64 data.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
color: Color preference
|
| 226 |
+
personality: Personality trait
|
| 227 |
+
appearance: Appearance feature
|
| 228 |
+
role: Character role
|
| 229 |
+
aspect_ratio: Image aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)
|
| 230 |
+
n: Number of images to generate (1-4)
|
| 231 |
+
response_format: Response format ("url" or "base64")
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
Dictionary containing:
|
| 235 |
+
- url: Image URL (if response_format="url")
|
| 236 |
+
- data: Base64 image data (if response_format="base64")
|
| 237 |
+
- prompt: Used prompt
|
| 238 |
+
- task_id: Task ID from MiniMax
|
| 239 |
+
|
| 240 |
+
Raises:
|
| 241 |
+
ImageGenerationError: If API call fails or returns invalid response
|
| 242 |
+
"""
|
| 243 |
+
try:
|
| 244 |
+
# 构建提示词
|
| 245 |
+
prompt = self.build_prompt(color, personality, appearance, role)
|
| 246 |
+
|
| 247 |
+
# 准备请求
|
| 248 |
+
headers = {
|
| 249 |
+
"Authorization": f"Bearer {self.api_key.strip()}",
|
| 250 |
+
"Content-Type": "application/json"
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
payload = {
|
| 254 |
+
"model": self.model,
|
| 255 |
+
"prompt": prompt,
|
| 256 |
+
"aspect_ratio": aspect_ratio,
|
| 257 |
+
"response_format": "url",
|
| 258 |
+
"n": n,
|
| 259 |
+
"prompt_optimizer": True
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
logger.info(
|
| 263 |
+
f"Calling MiniMax API for image generation. "
|
| 264 |
+
f"Aspect ratio: {aspect_ratio}, Count: {n}"
|
| 265 |
+
)
|
| 266 |
+
logger.debug(f"API URL: {self.api_url}")
|
| 267 |
+
logger.debug(f"API Key (first 20 chars): {self.api_key[:20]}...")
|
| 268 |
+
logger.debug(f"Payload: {json.dumps(payload, ensure_ascii=False)}")
|
| 269 |
+
|
| 270 |
+
# 发送请求
|
| 271 |
+
response = await self.client.post(
|
| 272 |
+
self.api_url,
|
| 273 |
+
headers=headers,
|
| 274 |
+
json=payload
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# 检查响应状态
|
| 278 |
+
if response.status_code != 200:
|
| 279 |
+
error_msg = f"MiniMax API returned status {response.status_code}"
|
| 280 |
+
try:
|
| 281 |
+
error_detail = response.json()
|
| 282 |
+
error_msg += f": {json.dumps(error_detail, ensure_ascii=False)}"
|
| 283 |
+
except Exception:
|
| 284 |
+
error_msg += f": {response.text}"
|
| 285 |
+
|
| 286 |
+
logger.error(f"Image generation API call failed: {error_msg}")
|
| 287 |
+
logger.error(f"Request URL: {self.api_url}")
|
| 288 |
+
logger.error(f"Request headers: Authorization=Bearer {self.api_key[:20]}..., Content-Type=application/json")
|
| 289 |
+
logger.error(f"Request payload: {json.dumps(payload, ensure_ascii=False)}")
|
| 290 |
+
raise ImageGenerationError(f"图像生成服务不可用: {error_msg}")
|
| 291 |
+
|
| 292 |
+
# 解析响应
|
| 293 |
+
try:
|
| 294 |
+
result = response.json()
|
| 295 |
+
logger.info(f"API Response (full): {json.dumps(result, indent=2, ensure_ascii=False)}")
|
| 296 |
+
except Exception as e:
|
| 297 |
+
error_msg = f"Failed to parse MiniMax API response: {str(e)}"
|
| 298 |
+
logger.error(error_msg)
|
| 299 |
+
logger.error(f"Raw response text: {response.text}")
|
| 300 |
+
raise ImageGenerationError(f"图像生成服务不可用: 响应格式无效")
|
| 301 |
+
|
| 302 |
+
# 提取图像 URL
|
| 303 |
+
try:
|
| 304 |
+
# MiniMax 实际返回格式:
|
| 305 |
+
# {
|
| 306 |
+
# "id": "task_id",
|
| 307 |
+
# "data": {"image_urls": [...]},
|
| 308 |
+
# "metadata": {...},
|
| 309 |
+
# "base_resp": {"status_code": 0, "status_msg": "success"}
|
| 310 |
+
# }
|
| 311 |
+
|
| 312 |
+
# 先检查是否有 base_resp
|
| 313 |
+
if "base_resp" in result:
|
| 314 |
+
base_resp = result.get("base_resp", {})
|
| 315 |
+
status_code = base_resp.get("status_code", -1)
|
| 316 |
+
error_msg = base_resp.get("status_msg", "Unknown error")
|
| 317 |
+
|
| 318 |
+
# status_code = 0 表示成功
|
| 319 |
+
if status_code != 0:
|
| 320 |
+
logger.error(f"MiniMax API error: {status_code} - {error_msg}")
|
| 321 |
+
raise ImageGenerationError(f"图像生成失败: {error_msg}")
|
| 322 |
+
|
| 323 |
+
logger.info(f"MiniMax API success: {status_code} - {error_msg}")
|
| 324 |
+
|
| 325 |
+
# 提取 task_id(可能在 id 或 task_id 字段)
|
| 326 |
+
task_id = result.get("id") or result.get("task_id", "")
|
| 327 |
+
|
| 328 |
+
# 提取图像数据
|
| 329 |
+
if "data" in result:
|
| 330 |
+
data = result["data"]
|
| 331 |
+
logger.info(f"Data field keys: {list(data.keys()) if isinstance(data, dict) else 'not a dict'}")
|
| 332 |
+
|
| 333 |
+
if isinstance(data, dict):
|
| 334 |
+
# 尝试多个可能的字段名
|
| 335 |
+
urls = None
|
| 336 |
+
if "image_urls" in data:
|
| 337 |
+
urls = data["image_urls"]
|
| 338 |
+
logger.info("Found image_urls field")
|
| 339 |
+
elif "url" in data:
|
| 340 |
+
urls = data["url"]
|
| 341 |
+
logger.info("Found url field")
|
| 342 |
+
|
| 343 |
+
if urls:
|
| 344 |
+
# 如果只生成一张,返回单个 URL
|
| 345 |
+
image_url = urls[0] if n == 1 else urls
|
| 346 |
+
logger.info(f"Image generation successful. URLs: {urls}")
|
| 347 |
+
|
| 348 |
+
return {
|
| 349 |
+
"url": image_url,
|
| 350 |
+
"prompt": prompt,
|
| 351 |
+
"task_id": task_id,
|
| 352 |
+
"metadata": result.get("metadata", {})
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
# 如果到这里还没有返回,说明响应格式不符合预期
|
| 356 |
+
logger.error(f"Could not extract image URLs from response: {json.dumps(result, ensure_ascii=False)}")
|
| 357 |
+
raise ImageGenerationError("API 响应格式错误: 无法提取图像 URL")
|
| 358 |
+
|
| 359 |
+
except (KeyError, IndexError) as e:
|
| 360 |
+
error_msg = f"Invalid API response structure: {str(e)}, Response: {json.dumps(result, ensure_ascii=False)}"
|
| 361 |
+
logger.error(error_msg)
|
| 362 |
+
raise ImageGenerationError(f"图像生成服务不可用: 响应结构无效")
|
| 363 |
+
|
| 364 |
+
except ImageGenerationError:
|
| 365 |
+
# Re-raise ImageGenerationError as-is
|
| 366 |
+
raise
|
| 367 |
+
|
| 368 |
+
except httpx.TimeoutException as e:
|
| 369 |
+
error_msg = f"MiniMax API request timeout: {str(e)}"
|
| 370 |
+
logger.error(error_msg)
|
| 371 |
+
raise ImageGenerationError("图像生成服务不可用: 请求超时")
|
| 372 |
+
|
| 373 |
+
except httpx.RequestError as e:
|
| 374 |
+
error_msg = f"MiniMax API request failed: {str(e)}"
|
| 375 |
+
logger.error(error_msg)
|
| 376 |
+
raise ImageGenerationError(f"图像生成服务不可用: 网络错误")
|
| 377 |
+
|
| 378 |
+
except Exception as e:
|
| 379 |
+
error_msg = f"Unexpected error in image generation service: {str(e)}"
|
| 380 |
+
logger.error(error_msg, exc_info=True)
|
| 381 |
+
raise ImageGenerationError(f"图像生成服务不可用: {str(e)}")
|
| 382 |
+
|
| 383 |
+
async def generate_multiple_images(
|
| 384 |
+
self,
|
| 385 |
+
color: str = "温暖粉",
|
| 386 |
+
personality: str = "温柔",
|
| 387 |
+
appearance: str = "无配饰",
|
| 388 |
+
role: str = "陪伴式朋友",
|
| 389 |
+
count: int = 3,
|
| 390 |
+
aspect_ratio: str = "1:1"
|
| 391 |
+
) -> List[Dict[str, str]]:
|
| 392 |
+
"""Generate multiple cat character images.
|
| 393 |
+
|
| 394 |
+
This method generates multiple images with the same parameters,
|
| 395 |
+
allowing users to choose their favorite one.
|
| 396 |
+
|
| 397 |
+
Args:
|
| 398 |
+
color: Color preference
|
| 399 |
+
personality: Personality trait
|
| 400 |
+
appearance: Appearance feature
|
| 401 |
+
role: Character role
|
| 402 |
+
count: Number of images to generate (1-4)
|
| 403 |
+
aspect_ratio: Image aspect ratio
|
| 404 |
+
|
| 405 |
+
Returns:
|
| 406 |
+
List of dictionaries, each containing url, prompt, and task_id
|
| 407 |
+
|
| 408 |
+
Raises:
|
| 409 |
+
ImageGenerationError: If any API call fails
|
| 410 |
+
"""
|
| 411 |
+
if count < 1 or count > 4:
|
| 412 |
+
raise ValueError("Count must be between 1 and 4")
|
| 413 |
+
|
| 414 |
+
try:
|
| 415 |
+
# MiniMax 支持一次生成多张图像
|
| 416 |
+
result = await self.generate_image(
|
| 417 |
+
color=color,
|
| 418 |
+
personality=personality,
|
| 419 |
+
appearance=appearance,
|
| 420 |
+
role=role,
|
| 421 |
+
aspect_ratio=aspect_ratio,
|
| 422 |
+
n=count
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
# 将结果转换为列表格式
|
| 426 |
+
urls = result['url'] if isinstance(result['url'], list) else [result['url']]
|
| 427 |
+
|
| 428 |
+
images = []
|
| 429 |
+
for i, url in enumerate(urls):
|
| 430 |
+
images.append({
|
| 431 |
+
"url": url,
|
| 432 |
+
"prompt": result['prompt'],
|
| 433 |
+
"task_id": result['task_id'],
|
| 434 |
+
"index": i
|
| 435 |
+
})
|
| 436 |
+
|
| 437 |
+
return images
|
| 438 |
+
|
| 439 |
+
except ImageGenerationError as e:
|
| 440 |
+
logger.error(f"Failed to generate images: {e.message}")
|
| 441 |
+
raise
|
app/logging_config.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging configuration for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module sets up the logging system with proper formatting, levels,
|
| 4 |
+
and file output. It also includes a filter to prevent sensitive information
|
| 5 |
+
from being logged.
|
| 6 |
+
|
| 7 |
+
Requirements: 10.5, 9.5
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
import re
|
| 12 |
+
from typing import Optional
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from contextvars import ContextVar
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Context variable to store request_id across async calls
|
| 18 |
+
request_id_var: ContextVar[Optional[str]] = ContextVar('request_id', default=None)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class RequestIdFilter(logging.Filter):
|
| 22 |
+
"""Filter to add request_id to log records.
|
| 23 |
+
|
| 24 |
+
This filter adds the request_id from context to each log record,
|
| 25 |
+
making it available in the log format.
|
| 26 |
+
|
| 27 |
+
Requirements: 9.5
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def filter(self, record: logging.LogRecord) -> bool:
|
| 31 |
+
"""Add request_id to log record.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
record: Log record to enhance
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
bool: Always True (we modify but don't reject records)
|
| 38 |
+
"""
|
| 39 |
+
# Get request_id from context, default to empty string if not set
|
| 40 |
+
record.request_id = request_id_var.get() or '-'
|
| 41 |
+
return True
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class SensitiveDataFilter(logging.Filter):
|
| 45 |
+
"""Filter to remove sensitive information from log records.
|
| 46 |
+
|
| 47 |
+
This filter masks API keys, passwords, and other sensitive data
|
| 48 |
+
to prevent them from appearing in logs.
|
| 49 |
+
|
| 50 |
+
Requirements: 10.5
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
# Patterns to detect and mask sensitive data
|
| 54 |
+
SENSITIVE_PATTERNS = [
|
| 55 |
+
# API keys (various formats)
|
| 56 |
+
(re.compile(r'(api[_-]?key["\s:=]+)([a-zA-Z0-9_-]{10,})', re.IGNORECASE), r'\1***REDACTED***'),
|
| 57 |
+
(re.compile(r'(zhipu[_-]?api[_-]?key["\s:=]+)([a-zA-Z0-9_-]{10,})', re.IGNORECASE), r'\1***REDACTED***'),
|
| 58 |
+
# Bearer tokens
|
| 59 |
+
(re.compile(r'(bearer\s+)([a-zA-Z0-9_-]{10,})', re.IGNORECASE), r'\1***REDACTED***'),
|
| 60 |
+
# Passwords
|
| 61 |
+
(re.compile(r'(password["\s:=]+)([^\s"]+)', re.IGNORECASE), r'\1***REDACTED***'),
|
| 62 |
+
# Authorization headers (capture the whole value)
|
| 63 |
+
(re.compile(r'(authorization["\s:=]+)([^\s"]+)', re.IGNORECASE), r'\1***REDACTED***'),
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
def filter(self, record: logging.LogRecord) -> bool:
|
| 67 |
+
"""Filter log record to mask sensitive data.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
record: Log record to filter
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
bool: Always True (we modify but don't reject records)
|
| 74 |
+
"""
|
| 75 |
+
# Mask sensitive data in the message
|
| 76 |
+
if hasattr(record, 'msg') and isinstance(record.msg, str):
|
| 77 |
+
record.msg = self._mask_sensitive_data(record.msg)
|
| 78 |
+
|
| 79 |
+
# Mask sensitive data in arguments
|
| 80 |
+
if hasattr(record, 'args') and record.args:
|
| 81 |
+
if isinstance(record.args, dict):
|
| 82 |
+
record.args = {
|
| 83 |
+
k: self._mask_sensitive_data(str(v)) if isinstance(v, str) else v
|
| 84 |
+
for k, v in record.args.items()
|
| 85 |
+
}
|
| 86 |
+
elif isinstance(record.args, tuple):
|
| 87 |
+
record.args = tuple(
|
| 88 |
+
self._mask_sensitive_data(str(arg)) if isinstance(arg, str) else arg
|
| 89 |
+
for arg in record.args
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
def _mask_sensitive_data(self, text: str) -> str:
|
| 95 |
+
"""Mask sensitive data in text using regex patterns.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
text: Text to mask
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
str: Text with sensitive data masked
|
| 102 |
+
"""
|
| 103 |
+
for pattern, replacement in self.SENSITIVE_PATTERNS:
|
| 104 |
+
text = pattern.sub(replacement, text)
|
| 105 |
+
return text
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def setup_logging(
|
| 109 |
+
log_level: str = "INFO",
|
| 110 |
+
log_file: Optional[Path] = None,
|
| 111 |
+
log_format: Optional[str] = None
|
| 112 |
+
) -> None:
|
| 113 |
+
"""Set up logging configuration for the application.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
| 117 |
+
log_file: Optional path to log file. If None, logs only to console.
|
| 118 |
+
log_format: Optional custom log format string
|
| 119 |
+
|
| 120 |
+
Requirements: 10.5, 9.5
|
| 121 |
+
"""
|
| 122 |
+
# Default log format with request_id, timestamp, level, and message
|
| 123 |
+
if log_format is None:
|
| 124 |
+
log_format = "[%(asctime)s] [%(levelname)s] [%(request_id)s] [%(name)s] %(message)s"
|
| 125 |
+
|
| 126 |
+
# Date format
|
| 127 |
+
date_format = "%Y-%m-%d %H:%M:%S"
|
| 128 |
+
|
| 129 |
+
# Create formatter
|
| 130 |
+
formatter = logging.Formatter(log_format, datefmt=date_format)
|
| 131 |
+
|
| 132 |
+
# Get root logger
|
| 133 |
+
root_logger = logging.getLogger()
|
| 134 |
+
root_logger.setLevel(getattr(logging, log_level.upper()))
|
| 135 |
+
|
| 136 |
+
# Remove existing handlers
|
| 137 |
+
root_logger.handlers.clear()
|
| 138 |
+
|
| 139 |
+
# Add filters
|
| 140 |
+
request_id_filter = RequestIdFilter()
|
| 141 |
+
sensitive_filter = SensitiveDataFilter()
|
| 142 |
+
|
| 143 |
+
# Console handler
|
| 144 |
+
console_handler = logging.StreamHandler()
|
| 145 |
+
console_handler.setFormatter(formatter)
|
| 146 |
+
console_handler.addFilter(request_id_filter)
|
| 147 |
+
console_handler.addFilter(sensitive_filter)
|
| 148 |
+
root_logger.addHandler(console_handler)
|
| 149 |
+
|
| 150 |
+
# File handler (if log file specified)
|
| 151 |
+
if log_file:
|
| 152 |
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
| 153 |
+
file_handler.setFormatter(formatter)
|
| 154 |
+
file_handler.addFilter(request_id_filter)
|
| 155 |
+
file_handler.addFilter(sensitive_filter)
|
| 156 |
+
root_logger.addHandler(file_handler)
|
| 157 |
+
|
| 158 |
+
# Log startup message
|
| 159 |
+
logger = logging.getLogger(__name__)
|
| 160 |
+
logger.info(f"Logging initialized at level {log_level}")
|
| 161 |
+
if log_file:
|
| 162 |
+
logger.info(f"Logging to file: {log_file}")
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def get_logger(name: str) -> logging.Logger:
|
| 166 |
+
"""Get a logger instance for a module.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
name: Logger name (typically __name__)
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
logging.Logger: Logger instance
|
| 173 |
+
"""
|
| 174 |
+
return logging.getLogger(name)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def set_request_id(request_id: str) -> None:
|
| 178 |
+
"""Set the request_id in the current context.
|
| 179 |
+
|
| 180 |
+
This should be called at the beginning of each request to ensure
|
| 181 |
+
all log messages include the request_id.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
request_id: Unique identifier for the request
|
| 185 |
+
|
| 186 |
+
Requirements: 9.5
|
| 187 |
+
"""
|
| 188 |
+
request_id_var.set(request_id)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def clear_request_id() -> None:
|
| 192 |
+
"""Clear the request_id from the current context.
|
| 193 |
+
|
| 194 |
+
This should be called at the end of each request to clean up.
|
| 195 |
+
"""
|
| 196 |
+
request_id_var.set(None)
|
app/main.py
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main FastAPI application for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module initializes the FastAPI application, sets up configuration,
|
| 4 |
+
logging, and defines the application lifecycle.
|
| 5 |
+
|
| 6 |
+
Requirements: 10.1, 10.2, 10.3, 10.4, 10.5
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import uuid
|
| 11 |
+
from contextlib import asynccontextmanager
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Optional
|
| 14 |
+
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
|
| 15 |
+
from fastapi.responses import JSONResponse
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from fastapi.staticfiles import StaticFiles
|
| 18 |
+
|
| 19 |
+
from app.config import init_config, get_config
|
| 20 |
+
from app.logging_config import setup_logging, set_request_id, clear_request_id
|
| 21 |
+
from app.models import ProcessResponse, RecordData, ParsedData
|
| 22 |
+
from app.storage import StorageService, StorageError
|
| 23 |
+
from app.asr_service import ASRService, ASRServiceError
|
| 24 |
+
from app.semantic_parser import SemanticParserService, SemanticParserError
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@asynccontextmanager
|
| 31 |
+
async def lifespan(app: FastAPI):
|
| 32 |
+
"""Application lifespan manager.
|
| 33 |
+
|
| 34 |
+
This handles startup and shutdown events for the application.
|
| 35 |
+
On startup, it initializes configuration and logging.
|
| 36 |
+
|
| 37 |
+
Requirements: 10.4 - Startup configuration validation
|
| 38 |
+
"""
|
| 39 |
+
# Startup
|
| 40 |
+
logger.info("Starting Voice Text Processor application...")
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
# Initialize configuration (will raise ValueError if invalid)
|
| 44 |
+
config = init_config()
|
| 45 |
+
logger.info("Configuration loaded and validated successfully")
|
| 46 |
+
|
| 47 |
+
# Setup logging with config values
|
| 48 |
+
setup_logging(
|
| 49 |
+
log_level=config.log_level,
|
| 50 |
+
log_file=config.log_file
|
| 51 |
+
)
|
| 52 |
+
logger.info("Logging system configured")
|
| 53 |
+
|
| 54 |
+
# Log configuration (without sensitive data)
|
| 55 |
+
logger.info(f"Data directory: {config.data_dir}")
|
| 56 |
+
logger.info(f"Max audio size: {config.max_audio_size} bytes")
|
| 57 |
+
logger.info(f"Log level: {config.log_level}")
|
| 58 |
+
|
| 59 |
+
except ValueError as e:
|
| 60 |
+
# Configuration validation failed - refuse to start
|
| 61 |
+
logger.error(f"Configuration validation failed: {e}")
|
| 62 |
+
logger.error("Application startup aborted due to configuration errors")
|
| 63 |
+
raise RuntimeError(f"Configuration error: {e}") from e
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Unexpected error during startup: {e}", exc_info=True)
|
| 66 |
+
raise RuntimeError(f"Startup error: {e}") from e
|
| 67 |
+
|
| 68 |
+
logger.info("Application startup complete")
|
| 69 |
+
|
| 70 |
+
yield
|
| 71 |
+
|
| 72 |
+
# Shutdown
|
| 73 |
+
logger.info("Shutting down Voice Text Processor application...")
|
| 74 |
+
logger.info("Application shutdown complete")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Create FastAPI application
|
| 78 |
+
app = FastAPI(
|
| 79 |
+
title="Voice Text Processor",
|
| 80 |
+
description="治愈系记录助手后端核心模块 - 语音和文本处理服务",
|
| 81 |
+
version="1.0.0",
|
| 82 |
+
lifespan=lifespan
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Add CORS middleware
|
| 86 |
+
app.add_middleware(
|
| 87 |
+
CORSMiddleware,
|
| 88 |
+
allow_origins=[
|
| 89 |
+
"http://localhost:5173",
|
| 90 |
+
"http://localhost:3000",
|
| 91 |
+
"http://172.18.16.245:5173", # 允许从电脑 IP 访问
|
| 92 |
+
"*" # 开发环境允许所有来源(生产环境应该限制)
|
| 93 |
+
],
|
| 94 |
+
allow_credentials=True,
|
| 95 |
+
allow_methods=["*"],
|
| 96 |
+
allow_headers=["*"],
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Mount static files for generated images
|
| 100 |
+
from pathlib import Path
|
| 101 |
+
from fastapi import Request
|
| 102 |
+
|
| 103 |
+
generated_images_dir = Path("generated_images")
|
| 104 |
+
generated_images_dir.mkdir(exist_ok=True)
|
| 105 |
+
app.mount("/generated_images", StaticFiles(directory="generated_images"), name="generated_images")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def get_base_url(request: Request) -> str:
|
| 109 |
+
"""获取请求的基础 URL(支持局域网访问)"""
|
| 110 |
+
# 使用请求的 host 来构建 URL
|
| 111 |
+
scheme = request.url.scheme # http 或 https
|
| 112 |
+
host = request.headers.get("host", "localhost:8000")
|
| 113 |
+
return f"{scheme}://{host}"
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@app.get("/api/status")
|
| 117 |
+
async def root():
|
| 118 |
+
"""API status endpoint."""
|
| 119 |
+
return {
|
| 120 |
+
"service": "Voice Text Processor",
|
| 121 |
+
"status": "running",
|
| 122 |
+
"version": "1.0.0"
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@app.get("/health")
|
| 127 |
+
async def health_check():
|
| 128 |
+
"""Health check endpoint."""
|
| 129 |
+
try:
|
| 130 |
+
config = get_config()
|
| 131 |
+
return {
|
| 132 |
+
"status": "healthy",
|
| 133 |
+
"data_dir": str(config.data_dir),
|
| 134 |
+
"max_audio_size": config.max_audio_size
|
| 135 |
+
}
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logger.error(f"Health check failed: {e}")
|
| 138 |
+
return JSONResponse(
|
| 139 |
+
status_code=503,
|
| 140 |
+
content={
|
| 141 |
+
"status": "unhealthy",
|
| 142 |
+
"error": str(e)
|
| 143 |
+
}
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Validation error class
|
| 148 |
+
class ValidationError(Exception):
|
| 149 |
+
"""Exception raised when input validation fails.
|
| 150 |
+
|
| 151 |
+
Requirements: 1.3, 8.5, 9.1
|
| 152 |
+
"""
|
| 153 |
+
def __init__(self, message: str):
|
| 154 |
+
super().__init__(message)
|
| 155 |
+
self.message = message
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# Supported audio formats
|
| 159 |
+
SUPPORTED_AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".webm"}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.post("/api/process", response_model=ProcessResponse)
|
| 163 |
+
async def process_input(
|
| 164 |
+
audio: Optional[UploadFile] = File(None),
|
| 165 |
+
text: Optional[str] = Form(None)
|
| 166 |
+
) -> ProcessResponse:
|
| 167 |
+
"""Process user input (audio or text) and extract structured data.
|
| 168 |
+
|
| 169 |
+
This endpoint accepts either an audio file or text content, performs
|
| 170 |
+
speech recognition (if audio), semantic parsing, and stores the results.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
audio: Audio file (multipart/form-data) in mp3, wav, or m4a format
|
| 174 |
+
text: Text content (application/json) in UTF-8 encoding
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
ProcessResponse containing record_id, timestamp, mood, inspirations, todos
|
| 178 |
+
|
| 179 |
+
Raises:
|
| 180 |
+
HTTPException: With appropriate status code and error message
|
| 181 |
+
|
| 182 |
+
Requirements: 1.1, 1.2, 1.3, 7.7, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 9.1, 9.2, 9.3, 9.4, 9.5
|
| 183 |
+
"""
|
| 184 |
+
request_id = str(uuid.uuid4())
|
| 185 |
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
| 186 |
+
|
| 187 |
+
# Set request_id in logging context
|
| 188 |
+
set_request_id(request_id)
|
| 189 |
+
|
| 190 |
+
logger.info(f"Processing request - audio: {audio is not None}, text: {text is not None}")
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
# Input validation
|
| 194 |
+
if audio is None and text is None:
|
| 195 |
+
raise ValidationError("请提供音频文件或文本内容")
|
| 196 |
+
|
| 197 |
+
if audio is not None and text is not None:
|
| 198 |
+
raise ValidationError("请只提供音频文件或文本内容中的一种")
|
| 199 |
+
|
| 200 |
+
# Get configuration
|
| 201 |
+
config = get_config()
|
| 202 |
+
|
| 203 |
+
# Initialize services
|
| 204 |
+
storage_service = StorageService(str(config.data_dir))
|
| 205 |
+
asr_service = ASRService(config.zhipu_api_key)
|
| 206 |
+
parser_service = SemanticParserService(config.zhipu_api_key)
|
| 207 |
+
|
| 208 |
+
original_text = ""
|
| 209 |
+
input_type = "text"
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
# Handle audio input
|
| 213 |
+
if audio is not None:
|
| 214 |
+
input_type = "audio"
|
| 215 |
+
|
| 216 |
+
# Validate audio format
|
| 217 |
+
filename = audio.filename or "audio"
|
| 218 |
+
file_ext = "." + filename.split(".")[-1].lower() if "." in filename else ""
|
| 219 |
+
|
| 220 |
+
if file_ext not in SUPPORTED_AUDIO_FORMATS:
|
| 221 |
+
raise ValidationError(
|
| 222 |
+
f"不支持的音频格式: {file_ext}. "
|
| 223 |
+
f"支持的格式: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Read audio file
|
| 227 |
+
audio_content = await audio.read()
|
| 228 |
+
|
| 229 |
+
# Validate audio file size
|
| 230 |
+
if len(audio_content) > config.max_audio_size:
|
| 231 |
+
raise ValidationError(
|
| 232 |
+
f"音频文件过大: {len(audio_content)} bytes. "
|
| 233 |
+
f"最大允许: {config.max_audio_size} bytes"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
logger.info(
|
| 237 |
+
f"Audio file received: {filename}, "
|
| 238 |
+
f"size: {len(audio_content)} bytes"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Transcribe audio to text
|
| 242 |
+
try:
|
| 243 |
+
original_text = await asr_service.transcribe(audio_content, filename)
|
| 244 |
+
logger.info(
|
| 245 |
+
f"ASR transcription successful. "
|
| 246 |
+
f"Text length: {len(original_text)}"
|
| 247 |
+
)
|
| 248 |
+
except ASRServiceError as e:
|
| 249 |
+
logger.error(
|
| 250 |
+
f"ASR service error: {e.message}",
|
| 251 |
+
exc_info=True
|
| 252 |
+
)
|
| 253 |
+
raise
|
| 254 |
+
|
| 255 |
+
# Handle text input
|
| 256 |
+
else:
|
| 257 |
+
# Validate text encoding (UTF-8)
|
| 258 |
+
# Accept whitespace-only text as valid UTF-8, but reject None or empty string
|
| 259 |
+
if text is None or text == "":
|
| 260 |
+
raise ValidationError("文本内容不能为空")
|
| 261 |
+
|
| 262 |
+
original_text = text
|
| 263 |
+
logger.info(
|
| 264 |
+
f"Text input received. "
|
| 265 |
+
f"Length: {len(original_text)}"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Perform semantic parsing
|
| 269 |
+
try:
|
| 270 |
+
parsed_data = await parser_service.parse(original_text)
|
| 271 |
+
logger.info(
|
| 272 |
+
f"Semantic parsing successful. "
|
| 273 |
+
f"Mood: {'present' if parsed_data.mood else 'none'}, "
|
| 274 |
+
f"Inspirations: {len(parsed_data.inspirations)}, "
|
| 275 |
+
f"Todos: {len(parsed_data.todos)}"
|
| 276 |
+
)
|
| 277 |
+
except SemanticParserError as e:
|
| 278 |
+
logger.error(
|
| 279 |
+
f"Semantic parser error: {e.message}",
|
| 280 |
+
exc_info=True
|
| 281 |
+
)
|
| 282 |
+
raise
|
| 283 |
+
|
| 284 |
+
# Generate record ID and timestamp
|
| 285 |
+
record_id = str(uuid.uuid4())
|
| 286 |
+
record_timestamp = datetime.utcnow().isoformat() + "Z"
|
| 287 |
+
|
| 288 |
+
# Create record data
|
| 289 |
+
record = RecordData(
|
| 290 |
+
record_id=record_id,
|
| 291 |
+
timestamp=record_timestamp,
|
| 292 |
+
input_type=input_type,
|
| 293 |
+
original_text=original_text,
|
| 294 |
+
parsed_data=parsed_data
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# Save to storage
|
| 298 |
+
try:
|
| 299 |
+
storage_service.save_record(record)
|
| 300 |
+
logger.info(f"Record saved: {record_id}")
|
| 301 |
+
|
| 302 |
+
# Save mood if present
|
| 303 |
+
if parsed_data.mood:
|
| 304 |
+
storage_service.append_mood(
|
| 305 |
+
parsed_data.mood,
|
| 306 |
+
record_id,
|
| 307 |
+
record_timestamp
|
| 308 |
+
)
|
| 309 |
+
logger.info(f"Mood data saved")
|
| 310 |
+
|
| 311 |
+
# Save inspirations if present
|
| 312 |
+
if parsed_data.inspirations:
|
| 313 |
+
storage_service.append_inspirations(
|
| 314 |
+
parsed_data.inspirations,
|
| 315 |
+
record_id,
|
| 316 |
+
record_timestamp
|
| 317 |
+
)
|
| 318 |
+
logger.info(
|
| 319 |
+
f"{len(parsed_data.inspirations)} "
|
| 320 |
+
f"inspiration(s) saved"
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# Save todos if present
|
| 324 |
+
if parsed_data.todos:
|
| 325 |
+
storage_service.append_todos(
|
| 326 |
+
parsed_data.todos,
|
| 327 |
+
record_id,
|
| 328 |
+
record_timestamp
|
| 329 |
+
)
|
| 330 |
+
logger.info(
|
| 331 |
+
f"{len(parsed_data.todos)} "
|
| 332 |
+
f"todo(s) saved"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
except StorageError as e:
|
| 336 |
+
logger.error(
|
| 337 |
+
f"Storage error: {str(e)}",
|
| 338 |
+
exc_info=True
|
| 339 |
+
)
|
| 340 |
+
raise
|
| 341 |
+
|
| 342 |
+
# Build success response
|
| 343 |
+
response = ProcessResponse(
|
| 344 |
+
record_id=record_id,
|
| 345 |
+
timestamp=record_timestamp,
|
| 346 |
+
mood=parsed_data.mood,
|
| 347 |
+
inspirations=parsed_data.inspirations,
|
| 348 |
+
todos=parsed_data.todos
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
logger.info(f"Request processed successfully")
|
| 352 |
+
|
| 353 |
+
return response
|
| 354 |
+
|
| 355 |
+
finally:
|
| 356 |
+
# Clean up services
|
| 357 |
+
await asr_service.close()
|
| 358 |
+
await parser_service.close()
|
| 359 |
+
# Clear request_id from context
|
| 360 |
+
clear_request_id()
|
| 361 |
+
|
| 362 |
+
except ValidationError as e:
|
| 363 |
+
# Input validation error - HTTP 400
|
| 364 |
+
logger.warning(
|
| 365 |
+
f"Validation error: {e.message}",
|
| 366 |
+
exc_info=True
|
| 367 |
+
)
|
| 368 |
+
clear_request_id()
|
| 369 |
+
return JSONResponse(
|
| 370 |
+
status_code=400,
|
| 371 |
+
content={
|
| 372 |
+
"error": e.message,
|
| 373 |
+
"timestamp": timestamp
|
| 374 |
+
}
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
except ASRServiceError as e:
|
| 378 |
+
# ASR service error - HTTP 500
|
| 379 |
+
logger.error(
|
| 380 |
+
f"ASR service unavailable: {e.message}",
|
| 381 |
+
exc_info=True
|
| 382 |
+
)
|
| 383 |
+
clear_request_id()
|
| 384 |
+
return JSONResponse(
|
| 385 |
+
status_code=500,
|
| 386 |
+
content={
|
| 387 |
+
"error": "语音识别服务不可用",
|
| 388 |
+
"detail": e.message,
|
| 389 |
+
"timestamp": timestamp
|
| 390 |
+
}
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
except SemanticParserError as e:
|
| 394 |
+
# Semantic parser error - HTTP 500
|
| 395 |
+
logger.error(
|
| 396 |
+
f"Semantic parser unavailable: {e.message}",
|
| 397 |
+
exc_info=True
|
| 398 |
+
)
|
| 399 |
+
clear_request_id()
|
| 400 |
+
return JSONResponse(
|
| 401 |
+
status_code=500,
|
| 402 |
+
content={
|
| 403 |
+
"error": "语义解析服务不可用",
|
| 404 |
+
"detail": e.message,
|
| 405 |
+
"timestamp": timestamp
|
| 406 |
+
}
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
except StorageError as e:
|
| 410 |
+
# Storage error - HTTP 500
|
| 411 |
+
logger.error(
|
| 412 |
+
f"Storage error: {str(e)}",
|
| 413 |
+
exc_info=True
|
| 414 |
+
)
|
| 415 |
+
clear_request_id()
|
| 416 |
+
return JSONResponse(
|
| 417 |
+
status_code=500,
|
| 418 |
+
content={
|
| 419 |
+
"error": "数据存储失败",
|
| 420 |
+
"detail": str(e),
|
| 421 |
+
"timestamp": timestamp
|
| 422 |
+
}
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
except Exception as e:
|
| 426 |
+
# Unexpected error - HTTP 500
|
| 427 |
+
logger.error(
|
| 428 |
+
f"Unexpected error: {str(e)}",
|
| 429 |
+
exc_info=True
|
| 430 |
+
)
|
| 431 |
+
clear_request_id()
|
| 432 |
+
return JSONResponse(
|
| 433 |
+
status_code=500,
|
| 434 |
+
content={
|
| 435 |
+
"error": "服务器内部错误",
|
| 436 |
+
"detail": str(e),
|
| 437 |
+
"timestamp": timestamp
|
| 438 |
+
}
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
@app.get("/api/records")
|
| 443 |
+
async def get_records():
|
| 444 |
+
"""Get all records."""
|
| 445 |
+
try:
|
| 446 |
+
config = get_config()
|
| 447 |
+
storage_service = StorageService(str(config.data_dir))
|
| 448 |
+
records = storage_service._read_json_file(storage_service.records_file)
|
| 449 |
+
return {"records": records}
|
| 450 |
+
except Exception as e:
|
| 451 |
+
logger.error(f"Failed to get records: {e}")
|
| 452 |
+
return JSONResponse(
|
| 453 |
+
status_code=500,
|
| 454 |
+
content={"error": str(e)}
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
@app.get("/api/moods")
|
| 459 |
+
async def get_moods():
|
| 460 |
+
"""Get all moods from both moods.json and records.json."""
|
| 461 |
+
try:
|
| 462 |
+
config = get_config()
|
| 463 |
+
storage_service = StorageService(str(config.data_dir))
|
| 464 |
+
|
| 465 |
+
# 1. 读取 moods.json
|
| 466 |
+
moods_from_file = storage_service._read_json_file(storage_service.moods_file)
|
| 467 |
+
logger.info(f"Loaded {len(moods_from_file)} moods from moods.json")
|
| 468 |
+
|
| 469 |
+
# 2. 从 records.json 中提取心情数据
|
| 470 |
+
records = storage_service._read_json_file(storage_service.records_file)
|
| 471 |
+
moods_from_records = []
|
| 472 |
+
|
| 473 |
+
for record in records:
|
| 474 |
+
# 检查 parsed_data 中是否有 mood
|
| 475 |
+
parsed_data = record.get("parsed_data", {})
|
| 476 |
+
mood_data = parsed_data.get("mood")
|
| 477 |
+
|
| 478 |
+
if mood_data and mood_data.get("type"):
|
| 479 |
+
# 构造心情对象
|
| 480 |
+
mood_obj = {
|
| 481 |
+
"record_id": record["record_id"],
|
| 482 |
+
"timestamp": record["timestamp"],
|
| 483 |
+
"type": mood_data.get("type"),
|
| 484 |
+
"intensity": mood_data.get("intensity", 5),
|
| 485 |
+
"keywords": mood_data.get("keywords", []),
|
| 486 |
+
"original_text": record.get("original_text", "") # 添加原文
|
| 487 |
+
}
|
| 488 |
+
moods_from_records.append(mood_obj)
|
| 489 |
+
|
| 490 |
+
logger.info(f"Extracted {len(moods_from_records)} moods from records.json")
|
| 491 |
+
|
| 492 |
+
# 3. 合并两个来源的心情数据(去重,优先使用 records 中的数据)
|
| 493 |
+
# 同时需要补充 moods.json 中缺失的 original_text
|
| 494 |
+
mood_dict = {}
|
| 495 |
+
|
| 496 |
+
# 先添加 moods.json 中的数据
|
| 497 |
+
for mood in moods_from_file:
|
| 498 |
+
mood_dict[mood["record_id"]] = mood
|
| 499 |
+
# 如果没有 original_text,设置为空字符串
|
| 500 |
+
if "original_text" not in mood:
|
| 501 |
+
mood["original_text"] = ""
|
| 502 |
+
|
| 503 |
+
# 再添加/覆盖 records.json 中的数据(包含 original_text)
|
| 504 |
+
for mood in moods_from_records:
|
| 505 |
+
mood_dict[mood["record_id"]] = mood
|
| 506 |
+
|
| 507 |
+
# 转换为列表并按时间排序(最新的在前)
|
| 508 |
+
all_moods = list(mood_dict.values())
|
| 509 |
+
all_moods.sort(key=lambda x: x["timestamp"], reverse=True)
|
| 510 |
+
|
| 511 |
+
logger.info(f"Total unique moods: {len(all_moods)}")
|
| 512 |
+
|
| 513 |
+
return {"moods": all_moods}
|
| 514 |
+
except Exception as e:
|
| 515 |
+
logger.error(f"Failed to get moods: {e}", exc_info=True)
|
| 516 |
+
return JSONResponse(
|
| 517 |
+
status_code=500,
|
| 518 |
+
content={"error": str(e)}
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
@app.get("/api/inspirations")
|
| 523 |
+
async def get_inspirations():
|
| 524 |
+
"""Get all inspirations."""
|
| 525 |
+
try:
|
| 526 |
+
config = get_config()
|
| 527 |
+
storage_service = StorageService(str(config.data_dir))
|
| 528 |
+
inspirations = storage_service._read_json_file(storage_service.inspirations_file)
|
| 529 |
+
return {"inspirations": inspirations}
|
| 530 |
+
except Exception as e:
|
| 531 |
+
logger.error(f"Failed to get inspirations: {e}")
|
| 532 |
+
return JSONResponse(
|
| 533 |
+
status_code=500,
|
| 534 |
+
content={"error": str(e)}
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
@app.get("/api/todos")
|
| 539 |
+
async def get_todos():
|
| 540 |
+
"""Get all todos."""
|
| 541 |
+
try:
|
| 542 |
+
config = get_config()
|
| 543 |
+
storage_service = StorageService(str(config.data_dir))
|
| 544 |
+
todos = storage_service._read_json_file(storage_service.todos_file)
|
| 545 |
+
return {"todos": todos}
|
| 546 |
+
except Exception as e:
|
| 547 |
+
logger.error(f"Failed to get todos: {e}")
|
| 548 |
+
return JSONResponse(
|
| 549 |
+
status_code=500,
|
| 550 |
+
content={"error": str(e)}
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
@app.patch("/api/todos/{todo_id}")
|
| 555 |
+
async def update_todo(todo_id: str, status: str = Form(...)):
|
| 556 |
+
"""Update todo status."""
|
| 557 |
+
try:
|
| 558 |
+
config = get_config()
|
| 559 |
+
storage_service = StorageService(str(config.data_dir))
|
| 560 |
+
todos = storage_service._read_json_file(storage_service.todos_file)
|
| 561 |
+
|
| 562 |
+
# Find and update todo
|
| 563 |
+
updated = False
|
| 564 |
+
for todo in todos:
|
| 565 |
+
if todo.get("record_id") == todo_id or str(hash(todo.get("task", ""))) == todo_id:
|
| 566 |
+
todo["status"] = status
|
| 567 |
+
updated = True
|
| 568 |
+
break
|
| 569 |
+
|
| 570 |
+
if not updated:
|
| 571 |
+
return JSONResponse(
|
| 572 |
+
status_code=404,
|
| 573 |
+
content={"error": "Todo not found"}
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
storage_service._write_json_file(storage_service.todos_file, todos)
|
| 577 |
+
return {"success": True}
|
| 578 |
+
except Exception as e:
|
| 579 |
+
logger.error(f"Failed to update todo: {e}")
|
| 580 |
+
return JSONResponse(
|
| 581 |
+
status_code=500,
|
| 582 |
+
content={"error": str(e)}
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
@app.post("/api/chat")
|
| 587 |
+
async def chat_with_ai(text: str = Form(...)):
|
| 588 |
+
"""Chat with AI assistant using RAG with records.json as knowledge base.
|
| 589 |
+
|
| 590 |
+
This endpoint provides conversational AI that has context about the user's
|
| 591 |
+
previous records, moods, inspirations, and todos.
|
| 592 |
+
"""
|
| 593 |
+
try:
|
| 594 |
+
config = get_config()
|
| 595 |
+
storage_service = StorageService(str(config.data_dir))
|
| 596 |
+
|
| 597 |
+
# Load user's records as RAG knowledge base
|
| 598 |
+
records = storage_service._read_json_file(storage_service.records_file)
|
| 599 |
+
|
| 600 |
+
# Build context from recent records (last 10)
|
| 601 |
+
recent_records = records[-10:] if len(records) > 10 else records
|
| 602 |
+
context_parts = []
|
| 603 |
+
|
| 604 |
+
for record in recent_records:
|
| 605 |
+
original_text = record.get('original_text', '')
|
| 606 |
+
timestamp = record.get('timestamp', '')
|
| 607 |
+
|
| 608 |
+
# Add parsed data context
|
| 609 |
+
parsed_data = record.get('parsed_data', {})
|
| 610 |
+
mood = parsed_data.get('mood')
|
| 611 |
+
inspirations = parsed_data.get('inspirations', [])
|
| 612 |
+
todos = parsed_data.get('todos', [])
|
| 613 |
+
|
| 614 |
+
context_entry = f"[{timestamp}] 用户说: {original_text}"
|
| 615 |
+
|
| 616 |
+
if mood:
|
| 617 |
+
context_entry += f"\n情绪: {mood.get('type')} (强度: {mood.get('intensity')})"
|
| 618 |
+
|
| 619 |
+
if inspirations:
|
| 620 |
+
ideas = [insp.get('core_idea') for insp in inspirations]
|
| 621 |
+
context_entry += f"\n灵感: {', '.join(ideas)}"
|
| 622 |
+
|
| 623 |
+
if todos:
|
| 624 |
+
tasks = [todo.get('task') for todo in todos]
|
| 625 |
+
context_entry += f"\n待办: {', '.join(tasks)}"
|
| 626 |
+
|
| 627 |
+
context_parts.append(context_entry)
|
| 628 |
+
|
| 629 |
+
# Build system prompt with context
|
| 630 |
+
context_text = "\n\n".join(context_parts) if context_parts else "暂无历史记录"
|
| 631 |
+
|
| 632 |
+
system_prompt = f"""你是一个温柔、善解人意的AI陪伴助手。你的名字叫小喵。
|
| 633 |
+
你会用温暖、治愈的语气和用户聊天,给予他们情感支持和陪伴。
|
| 634 |
+
回复要简短、自然、有温度。
|
| 635 |
+
|
| 636 |
+
你可以参考用户的历史记录来提供更贴心的回复:
|
| 637 |
+
|
| 638 |
+
{context_text}
|
| 639 |
+
|
| 640 |
+
请基于这些背景信息,用温暖、理解的语气回复用户。如果用户提到之前的事情,你可以自然地关联起来。"""
|
| 641 |
+
|
| 642 |
+
try:
|
| 643 |
+
import httpx
|
| 644 |
+
|
| 645 |
+
# 增加超时时间,添加重试逻辑
|
| 646 |
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 647 |
+
response = await client.post(
|
| 648 |
+
"https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
| 649 |
+
headers={
|
| 650 |
+
"Authorization": f"Bearer {config.zhipu_api_key}",
|
| 651 |
+
"Content-Type": "application/json"
|
| 652 |
+
},
|
| 653 |
+
json={
|
| 654 |
+
"model": "glm-4-flash",
|
| 655 |
+
"messages": [
|
| 656 |
+
{
|
| 657 |
+
"role": "system",
|
| 658 |
+
"content": system_prompt
|
| 659 |
+
},
|
| 660 |
+
{
|
| 661 |
+
"role": "user",
|
| 662 |
+
"content": text
|
| 663 |
+
}
|
| 664 |
+
],
|
| 665 |
+
"temperature": 0.8,
|
| 666 |
+
"top_p": 0.9
|
| 667 |
+
}
|
| 668 |
+
)
|
| 669 |
+
|
| 670 |
+
if response.status_code == 200:
|
| 671 |
+
result = response.json()
|
| 672 |
+
ai_response = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 673 |
+
logger.info(f"AI chat successful with RAG context")
|
| 674 |
+
return {"response": ai_response}
|
| 675 |
+
else:
|
| 676 |
+
logger.error(f"AI chat failed: {response.status_code} {response.text}")
|
| 677 |
+
return {"response": "抱歉,我现在有点累了,稍后再聊好吗?"}
|
| 678 |
+
|
| 679 |
+
except httpx.TimeoutException:
|
| 680 |
+
logger.error(f"AI API timeout")
|
| 681 |
+
return {"response": "抱歉,网络有点慢,请稍后再试~"}
|
| 682 |
+
except httpx.ConnectError:
|
| 683 |
+
logger.error(f"AI API connection error")
|
| 684 |
+
return {"response": "抱歉,无法连接到AI服务,请检查网络连接~"}
|
| 685 |
+
except Exception as e:
|
| 686 |
+
logger.error(f"AI API call error: {e}")
|
| 687 |
+
return {"response": "抱歉,我现在有点累了,稍后再聊好吗?"}
|
| 688 |
+
|
| 689 |
+
except Exception as e:
|
| 690 |
+
logger.error(f"Chat error: {e}")
|
| 691 |
+
return {"response": "抱歉,我现在有点累了,稍后再聊好吗?"}
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
@app.get("/api/user/config")
|
| 695 |
+
async def get_user_config(request: Request):
|
| 696 |
+
"""Get user configuration including character image."""
|
| 697 |
+
try:
|
| 698 |
+
from app.user_config import UserConfig
|
| 699 |
+
from pathlib import Path
|
| 700 |
+
import os
|
| 701 |
+
|
| 702 |
+
config = get_config()
|
| 703 |
+
user_config = UserConfig(str(config.data_dir))
|
| 704 |
+
user_data = user_config.load_config()
|
| 705 |
+
|
| 706 |
+
base_url = get_base_url(request)
|
| 707 |
+
|
| 708 |
+
# 如果没有保存的图片,尝试加载默认形象或最新的本地图���
|
| 709 |
+
if not user_data.get('character', {}).get('image_url'):
|
| 710 |
+
generated_images_dir = Path("generated_images")
|
| 711 |
+
default_image = generated_images_dir / "default_character.jpeg"
|
| 712 |
+
|
| 713 |
+
# 优先使用默认形象
|
| 714 |
+
if default_image.exists():
|
| 715 |
+
logger.info("Loading default character image")
|
| 716 |
+
user_config.save_character_image(
|
| 717 |
+
image_url=str(default_image),
|
| 718 |
+
prompt="默认治愈系小猫形象",
|
| 719 |
+
preferences={
|
| 720 |
+
"color": "薰衣草紫",
|
| 721 |
+
"personality": "温柔",
|
| 722 |
+
"appearance": "无配饰",
|
| 723 |
+
"role": "陪伴式朋友"
|
| 724 |
+
}
|
| 725 |
+
)
|
| 726 |
+
user_data = user_config.load_config()
|
| 727 |
+
logger.info("Default character image loaded successfully")
|
| 728 |
+
|
| 729 |
+
# 如果没有默认形象,尝试加载最新的本地图片
|
| 730 |
+
elif generated_images_dir.exists():
|
| 731 |
+
# 获取所有图片文件
|
| 732 |
+
image_files = list(generated_images_dir.glob("character_*.jpeg"))
|
| 733 |
+
if image_files:
|
| 734 |
+
# 按修改时间排序,获取最新的
|
| 735 |
+
latest_image = max(image_files, key=lambda p: p.stat().st_mtime)
|
| 736 |
+
|
| 737 |
+
# 构建 URL 路径(使用动态 base_url)
|
| 738 |
+
image_url = f"{base_url}/generated_images/{latest_image.name}"
|
| 739 |
+
|
| 740 |
+
# 从文件名提取偏好设置
|
| 741 |
+
# 格式: character_颜色_性格_时间戳.jpeg
|
| 742 |
+
parts = latest_image.stem.split('_')
|
| 743 |
+
if len(parts) >= 3:
|
| 744 |
+
color = parts[1]
|
| 745 |
+
personality = parts[2]
|
| 746 |
+
|
| 747 |
+
# 更新配置
|
| 748 |
+
user_config.save_character_image(
|
| 749 |
+
image_url=str(latest_image),
|
| 750 |
+
prompt=f"Character with {color} and {personality}",
|
| 751 |
+
preferences={
|
| 752 |
+
"color": color,
|
| 753 |
+
"personality": personality,
|
| 754 |
+
"appearance": "无配饰",
|
| 755 |
+
"role": "陪伴式朋友"
|
| 756 |
+
}
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
# 重新加载配置
|
| 760 |
+
user_data = user_config.load_config()
|
| 761 |
+
|
| 762 |
+
logger.info(f"Loaded latest local image: {latest_image.name}")
|
| 763 |
+
|
| 764 |
+
# 如果 image_url 是本地路径,转换为 URL
|
| 765 |
+
image_url = user_data.get('character', {}).get('image_url')
|
| 766 |
+
if image_url and not image_url.startswith('http'):
|
| 767 |
+
# 本地路径,转换为 URL(处理 Windows 和 Unix 路径)
|
| 768 |
+
image_path = Path(image_url)
|
| 769 |
+
if image_path.exists():
|
| 770 |
+
# 使用正斜杠构建 URL(使用动态 base_url)
|
| 771 |
+
user_data['character']['image_url'] = f"{base_url}/generated_images/{image_path.name}"
|
| 772 |
+
else:
|
| 773 |
+
# 如果路径不存在,尝试只使用文件名
|
| 774 |
+
filename = image_path.name
|
| 775 |
+
full_path = Path("generated_images") / filename
|
| 776 |
+
if full_path.exists():
|
| 777 |
+
user_data['character']['image_url'] = f"{base_url}/generated_images/{filename}"
|
| 778 |
+
logger.info(f"Converted path to URL: {filename}")
|
| 779 |
+
|
| 780 |
+
return user_data
|
| 781 |
+
except Exception as e:
|
| 782 |
+
logger.error(f"Failed to get user config: {e}")
|
| 783 |
+
return JSONResponse(
|
| 784 |
+
status_code=500,
|
| 785 |
+
content={"error": str(e)}
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
@app.post("/api/character/generate")
|
| 790 |
+
async def generate_character(
|
| 791 |
+
request: Request,
|
| 792 |
+
color: str = Form(...),
|
| 793 |
+
personality: str = Form(...),
|
| 794 |
+
appearance: str = Form(...),
|
| 795 |
+
role: str = Form(...)
|
| 796 |
+
):
|
| 797 |
+
"""Generate AI character image based on preferences.
|
| 798 |
+
|
| 799 |
+
Args:
|
| 800 |
+
color: Color preference (温暖粉/天空蓝/薄荷绿等)
|
| 801 |
+
personality: Personality trait (活泼/温柔/聪明等)
|
| 802 |
+
appearance: Appearance feature (戴眼镜/戴帽子等)
|
| 803 |
+
role: Character role (陪伴式朋友/温柔照顾型长辈等)
|
| 804 |
+
|
| 805 |
+
Returns:
|
| 806 |
+
JSON with image_url, prompt, and preferences
|
| 807 |
+
"""
|
| 808 |
+
try:
|
| 809 |
+
from app.image_service import ImageGenerationService, ImageGenerationError
|
| 810 |
+
from app.user_config import UserConfig
|
| 811 |
+
from datetime import datetime
|
| 812 |
+
from pathlib import Path
|
| 813 |
+
import httpx
|
| 814 |
+
|
| 815 |
+
config = get_config()
|
| 816 |
+
|
| 817 |
+
# 检查是否配置了 MiniMax API
|
| 818 |
+
minimax_api_key = getattr(config, 'minimax_api_key', None)
|
| 819 |
+
|
| 820 |
+
if not minimax_api_key:
|
| 821 |
+
logger.warning("MiniMax API key not configured")
|
| 822 |
+
return JSONResponse(
|
| 823 |
+
status_code=400,
|
| 824 |
+
content={
|
| 825 |
+
"error": "MiniMax API 未配置",
|
| 826 |
+
"detail": "请在 .env 文件中配置 MINIMAX_API_KEY。访问 https://platform.minimaxi.com/ 获取 API 密钥。"
|
| 827 |
+
}
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
# 初始化服务
|
| 831 |
+
image_service = ImageGenerationService(
|
| 832 |
+
api_key=minimax_api_key,
|
| 833 |
+
group_id=getattr(config, 'minimax_group_id', None)
|
| 834 |
+
)
|
| 835 |
+
user_config = UserConfig(str(config.data_dir))
|
| 836 |
+
|
| 837 |
+
try:
|
| 838 |
+
logger.info(
|
| 839 |
+
f"Generating character image: "
|
| 840 |
+
f"color={color}, personality={personality}, "
|
| 841 |
+
f"appearance={appearance}, role={role}"
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
# 生成图像
|
| 845 |
+
result = await image_service.generate_image(
|
| 846 |
+
color=color,
|
| 847 |
+
personality=personality,
|
| 848 |
+
appearance=appearance,
|
| 849 |
+
role=role,
|
| 850 |
+
aspect_ratio="1:1",
|
| 851 |
+
n=1
|
| 852 |
+
)
|
| 853 |
+
|
| 854 |
+
# 下载图片到本地
|
| 855 |
+
generated_images_dir = Path("generated_images")
|
| 856 |
+
generated_images_dir.mkdir(exist_ok=True)
|
| 857 |
+
|
| 858 |
+
# 生成文件名:character_颜色_性格_时间戳.jpeg
|
| 859 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 860 |
+
filename = f"character_{color}_{personality}_{timestamp}.jpeg"
|
| 861 |
+
local_path = generated_images_dir / filename
|
| 862 |
+
|
| 863 |
+
logger.info(f"Downloading image to: {local_path}")
|
| 864 |
+
|
| 865 |
+
# 下载图片
|
| 866 |
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 867 |
+
response = await client.get(result['url'])
|
| 868 |
+
if response.status_code == 200:
|
| 869 |
+
with open(local_path, 'wb') as f:
|
| 870 |
+
f.write(response.content)
|
| 871 |
+
logger.info(f"Image saved to: {local_path}")
|
| 872 |
+
else:
|
| 873 |
+
logger.error(f"Failed to download image: HTTP {response.status_code}")
|
| 874 |
+
# 如果下载失败,仍然使用远程 URL
|
| 875 |
+
local_path = None
|
| 876 |
+
|
| 877 |
+
# 保存到用户配置
|
| 878 |
+
preferences = {
|
| 879 |
+
"color": color,
|
| 880 |
+
"personality": personality,
|
| 881 |
+
"appearance": appearance,
|
| 882 |
+
"role": role
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
# 使用本地路径(如果下载成功)
|
| 886 |
+
image_url = str(local_path) if local_path else result['url']
|
| 887 |
+
|
| 888 |
+
user_config.save_character_image(
|
| 889 |
+
image_url=image_url,
|
| 890 |
+
prompt=result['prompt'],
|
| 891 |
+
revised_prompt=result.get('metadata', {}).get('revised_prompt'),
|
| 892 |
+
preferences=preferences
|
| 893 |
+
)
|
| 894 |
+
|
| 895 |
+
logger.info(f"Character image generated and saved: {image_url}")
|
| 896 |
+
|
| 897 |
+
# 返回 HTTP URL(使用动态 base_url)
|
| 898 |
+
base_url = get_base_url(request)
|
| 899 |
+
if local_path:
|
| 900 |
+
http_url = f"{base_url}/generated_images/{local_path.name}"
|
| 901 |
+
else:
|
| 902 |
+
http_url = image_url
|
| 903 |
+
|
| 904 |
+
return {
|
| 905 |
+
"success": True,
|
| 906 |
+
"image_url": http_url,
|
| 907 |
+
"prompt": result['prompt'],
|
| 908 |
+
"preferences": preferences,
|
| 909 |
+
"task_id": result.get('task_id')
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
finally:
|
| 913 |
+
await image_service.close()
|
| 914 |
+
|
| 915 |
+
except ImageGenerationError as e:
|
| 916 |
+
logger.error(f"Image generation error: {e.message}")
|
| 917 |
+
|
| 918 |
+
# 提供更友好的错误信息
|
| 919 |
+
error_detail = e.message
|
| 920 |
+
if "invalid api key" in e.message.lower():
|
| 921 |
+
error_detail = "API 密钥无效,请检查 MINIMAX_API_KEY 配置是否正确"
|
| 922 |
+
elif "quota" in e.message.lower() or "配额" in e.message:
|
| 923 |
+
error_detail = "API 配额不足,请充值或等待配额恢复"
|
| 924 |
+
elif "timeout" in e.message.lower() or "超时" in e.message:
|
| 925 |
+
error_detail = "请求超时,请检查网络连接后重试"
|
| 926 |
+
|
| 927 |
+
return JSONResponse(
|
| 928 |
+
status_code=500,
|
| 929 |
+
content={
|
| 930 |
+
"error": "图像生成失败",
|
| 931 |
+
"detail": error_detail
|
| 932 |
+
}
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
except Exception as e:
|
| 936 |
+
logger.error(f"Failed to generate character: {e}", exc_info=True)
|
| 937 |
+
return JSONResponse(
|
| 938 |
+
status_code=500,
|
| 939 |
+
content={
|
| 940 |
+
"error": "生成角色形象失败",
|
| 941 |
+
"detail": str(e)
|
| 942 |
+
}
|
| 943 |
+
)
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
@app.get("/api/character/history")
|
| 947 |
+
async def get_character_history(request: Request):
|
| 948 |
+
"""Get list of all generated character images.
|
| 949 |
+
|
| 950 |
+
Returns:
|
| 951 |
+
JSON with list of historical character images
|
| 952 |
+
"""
|
| 953 |
+
try:
|
| 954 |
+
from pathlib import Path
|
| 955 |
+
import os
|
| 956 |
+
|
| 957 |
+
base_url = get_base_url(request)
|
| 958 |
+
generated_images_dir = Path("generated_images")
|
| 959 |
+
|
| 960 |
+
if not generated_images_dir.exists():
|
| 961 |
+
return {"images": []}
|
| 962 |
+
|
| 963 |
+
# 获取所有图片文件
|
| 964 |
+
image_files = []
|
| 965 |
+
for file in generated_images_dir.glob("character_*.jpeg"):
|
| 966 |
+
# 解析文件名:character_颜色_性格_时间戳.jpeg
|
| 967 |
+
parts = file.stem.split("_")
|
| 968 |
+
if len(parts) >= 4:
|
| 969 |
+
color = parts[1]
|
| 970 |
+
personality = parts[2]
|
| 971 |
+
timestamp = "_".join(parts[3:])
|
| 972 |
+
|
| 973 |
+
# 获取文件信息
|
| 974 |
+
stat = file.stat()
|
| 975 |
+
|
| 976 |
+
image_files.append({
|
| 977 |
+
"filename": file.name,
|
| 978 |
+
"url": f"{base_url}/generated_images/{file.name}",
|
| 979 |
+
"color": color,
|
| 980 |
+
"personality": personality,
|
| 981 |
+
"timestamp": timestamp,
|
| 982 |
+
"created_at": stat.st_ctime,
|
| 983 |
+
"size": stat.st_size
|
| 984 |
+
})
|
| 985 |
+
|
| 986 |
+
# 按创建时间倒序排列(最新的在前)
|
| 987 |
+
image_files.sort(key=lambda x: x["created_at"], reverse=True)
|
| 988 |
+
|
| 989 |
+
logger.info(f"Found {len(image_files)} historical character images")
|
| 990 |
+
|
| 991 |
+
return {"images": image_files}
|
| 992 |
+
|
| 993 |
+
except Exception as e:
|
| 994 |
+
logger.error(f"Error getting character history: {e}", exc_info=True)
|
| 995 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 996 |
+
|
| 997 |
+
|
| 998 |
+
@app.post("/api/character/select")
|
| 999 |
+
async def select_character(
|
| 1000 |
+
request: Request,
|
| 1001 |
+
filename: str = Form(...)
|
| 1002 |
+
):
|
| 1003 |
+
"""Select a historical character image as current.
|
| 1004 |
+
|
| 1005 |
+
Args:
|
| 1006 |
+
filename: Filename of the character image to select
|
| 1007 |
+
|
| 1008 |
+
Returns:
|
| 1009 |
+
JSON with success status and image URL
|
| 1010 |
+
"""
|
| 1011 |
+
try:
|
| 1012 |
+
from app.user_config import UserConfig
|
| 1013 |
+
from pathlib import Path
|
| 1014 |
+
|
| 1015 |
+
config = get_config()
|
| 1016 |
+
user_config = UserConfig(str(config.data_dir))
|
| 1017 |
+
|
| 1018 |
+
# 验证文件存在
|
| 1019 |
+
image_path = Path("generated_images") / filename
|
| 1020 |
+
if not image_path.exists():
|
| 1021 |
+
raise HTTPException(status_code=404, detail="图片文件不存在")
|
| 1022 |
+
|
| 1023 |
+
# 解析文件名获取偏好设置
|
| 1024 |
+
parts = filename.replace(".jpeg", "").split("_")
|
| 1025 |
+
if len(parts) >= 4:
|
| 1026 |
+
color = parts[1]
|
| 1027 |
+
personality = parts[2]
|
| 1028 |
+
|
| 1029 |
+
preferences = {
|
| 1030 |
+
"color": color,
|
| 1031 |
+
"personality": personality,
|
| 1032 |
+
"appearance": "未知",
|
| 1033 |
+
"role": "未知"
|
| 1034 |
+
}
|
| 1035 |
+
else:
|
| 1036 |
+
preferences = {}
|
| 1037 |
+
|
| 1038 |
+
# 更新用户配置
|
| 1039 |
+
image_url = str(image_path)
|
| 1040 |
+
user_config.save_character_image(
|
| 1041 |
+
image_url=image_url,
|
| 1042 |
+
prompt=f"历史形象: {filename}",
|
| 1043 |
+
preferences=preferences
|
| 1044 |
+
)
|
| 1045 |
+
|
| 1046 |
+
logger.info(f"Selected historical character: {filename}")
|
| 1047 |
+
|
| 1048 |
+
# 返回 HTTP URL(使用动态 base_url)
|
| 1049 |
+
base_url = get_base_url(request)
|
| 1050 |
+
http_url = f"{base_url}/generated_images/{filename}"
|
| 1051 |
+
|
| 1052 |
+
return {
|
| 1053 |
+
"success": True,
|
| 1054 |
+
"image_url": http_url,
|
| 1055 |
+
"filename": filename,
|
| 1056 |
+
"preferences": preferences
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
except HTTPException:
|
| 1060 |
+
raise
|
| 1061 |
+
except Exception as e:
|
| 1062 |
+
logger.error(f"Error selecting character: {e}", exc_info=True)
|
| 1063 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1064 |
+
|
| 1065 |
+
|
| 1066 |
+
@app.post("/api/character/preferences")
|
| 1067 |
+
async def update_character_preferences(
|
| 1068 |
+
color: Optional[str] = Form(None),
|
| 1069 |
+
personality: Optional[str] = Form(None),
|
| 1070 |
+
appearance: Optional[str] = Form(None),
|
| 1071 |
+
role: Optional[str] = Form(None)
|
| 1072 |
+
):
|
| 1073 |
+
"""Update character preferences without generating new image.
|
| 1074 |
+
|
| 1075 |
+
Args:
|
| 1076 |
+
color: Color preference (optional)
|
| 1077 |
+
personality: Personality trait (optional)
|
| 1078 |
+
appearance: Appearance feature (optional)
|
| 1079 |
+
role: Character role (optional)
|
| 1080 |
+
|
| 1081 |
+
Returns:
|
| 1082 |
+
JSON with updated preferences
|
| 1083 |
+
"""
|
| 1084 |
+
try:
|
| 1085 |
+
from app.user_config import UserConfig
|
| 1086 |
+
|
| 1087 |
+
config = get_config()
|
| 1088 |
+
user_config = UserConfig(str(config.data_dir))
|
| 1089 |
+
|
| 1090 |
+
# 更新偏好设置
|
| 1091 |
+
user_config.update_character_preferences(
|
| 1092 |
+
color=color,
|
| 1093 |
+
personality=personality,
|
| 1094 |
+
appearance=appearance,
|
| 1095 |
+
role=role
|
| 1096 |
+
)
|
| 1097 |
+
|
| 1098 |
+
# 返回更新后的配置
|
| 1099 |
+
updated_config = user_config.load_config()
|
| 1100 |
+
|
| 1101 |
+
return {
|
| 1102 |
+
"success": True,
|
| 1103 |
+
"preferences": updated_config['character']['preferences']
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
+
except Exception as e:
|
| 1107 |
+
logger.error(f"Failed to update preferences: {e}")
|
| 1108 |
+
return JSONResponse(
|
| 1109 |
+
status_code=500,
|
| 1110 |
+
content={"error": str(e)}
|
| 1111 |
+
)
|
| 1112 |
+
|
| 1113 |
+
|
| 1114 |
+
if __name__ == "__main__":
|
| 1115 |
+
import uvicorn
|
| 1116 |
+
|
| 1117 |
+
# Load config for server settings
|
| 1118 |
+
try:
|
| 1119 |
+
config = init_config()
|
| 1120 |
+
setup_logging(log_level=config.log_level, log_file=config.log_file)
|
| 1121 |
+
|
| 1122 |
+
# Run server
|
| 1123 |
+
uvicorn.run(
|
| 1124 |
+
"app.main:app",
|
| 1125 |
+
host=config.host,
|
| 1126 |
+
port=config.port,
|
| 1127 |
+
reload=False,
|
| 1128 |
+
log_level=config.log_level.lower()
|
| 1129 |
+
)
|
| 1130 |
+
except Exception as e:
|
| 1131 |
+
print(f"Failed to start application: {e}")
|
| 1132 |
+
exit(1)
|
app/models.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data models for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module defines all Pydantic data models used throughout the application
|
| 4 |
+
for data validation, serialization, and API request/response handling.
|
| 5 |
+
|
| 6 |
+
Requirements: 4.1, 4.2, 4.3, 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Optional, List, Literal
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class MoodData(BaseModel):
|
| 14 |
+
"""Mood data structure.
|
| 15 |
+
|
| 16 |
+
Represents the emotional state extracted from user input.
|
| 17 |
+
|
| 18 |
+
Attributes:
|
| 19 |
+
type: The type/name of the emotion (e.g., "开心", "焦虑")
|
| 20 |
+
intensity: Emotion intensity on a scale of 1-10
|
| 21 |
+
keywords: List of keywords associated with the emotion
|
| 22 |
+
|
| 23 |
+
Requirements: 4.1, 4.2, 4.3
|
| 24 |
+
"""
|
| 25 |
+
type: Optional[str] = None
|
| 26 |
+
intensity: Optional[int] = Field(None, ge=1, le=10)
|
| 27 |
+
keywords: List[str] = Field(default_factory=list)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class InspirationData(BaseModel):
|
| 31 |
+
"""Inspiration data structure.
|
| 32 |
+
|
| 33 |
+
Represents an idea or inspiration extracted from user input.
|
| 34 |
+
|
| 35 |
+
Attributes:
|
| 36 |
+
core_idea: The core idea/concept (max 20 characters)
|
| 37 |
+
tags: List of tags for categorization (max 5 tags)
|
| 38 |
+
category: Category of the inspiration
|
| 39 |
+
|
| 40 |
+
Requirements: 5.1, 5.2, 5.3
|
| 41 |
+
"""
|
| 42 |
+
core_idea: str = Field(..., max_length=20)
|
| 43 |
+
tags: List[str] = Field(default_factory=list, max_length=5)
|
| 44 |
+
category: Literal["工作", "生活", "学习", "创意"]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TodoData(BaseModel):
|
| 48 |
+
"""Todo item data structure.
|
| 49 |
+
|
| 50 |
+
Represents a task/todo item extracted from user input.
|
| 51 |
+
|
| 52 |
+
Attributes:
|
| 53 |
+
task: Description of the task
|
| 54 |
+
time: Time information (preserved as original expression)
|
| 55 |
+
location: Location information
|
| 56 |
+
status: Task status (defaults to "pending")
|
| 57 |
+
|
| 58 |
+
Requirements: 6.1, 6.2, 6.3, 6.4
|
| 59 |
+
"""
|
| 60 |
+
task: str
|
| 61 |
+
time: Optional[str] = None
|
| 62 |
+
location: Optional[str] = None
|
| 63 |
+
status: str = "pending"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ParsedData(BaseModel):
|
| 67 |
+
"""Parsed data structure.
|
| 68 |
+
|
| 69 |
+
Contains all structured data extracted from semantic parsing.
|
| 70 |
+
|
| 71 |
+
Attributes:
|
| 72 |
+
mood: Extracted mood data (optional)
|
| 73 |
+
inspirations: List of extracted inspirations
|
| 74 |
+
todos: List of extracted todo items
|
| 75 |
+
"""
|
| 76 |
+
mood: Optional[MoodData] = None
|
| 77 |
+
inspirations: List[InspirationData] = Field(default_factory=list)
|
| 78 |
+
todos: List[TodoData] = Field(default_factory=list)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class RecordData(BaseModel):
|
| 82 |
+
"""Complete record data structure.
|
| 83 |
+
|
| 84 |
+
Represents a complete user input record with all metadata and parsed data.
|
| 85 |
+
|
| 86 |
+
Attributes:
|
| 87 |
+
record_id: Unique identifier for the record
|
| 88 |
+
timestamp: ISO 8601 timestamp of when the record was created
|
| 89 |
+
input_type: Type of input (audio or text)
|
| 90 |
+
original_text: The original or transcribed text
|
| 91 |
+
parsed_data: Structured data extracted from the text
|
| 92 |
+
"""
|
| 93 |
+
record_id: str
|
| 94 |
+
timestamp: str
|
| 95 |
+
input_type: Literal["audio", "text"]
|
| 96 |
+
original_text: str
|
| 97 |
+
parsed_data: ParsedData
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class ProcessResponse(BaseModel):
|
| 101 |
+
"""API response model for /api/process endpoint.
|
| 102 |
+
|
| 103 |
+
Represents the response returned to clients after processing input.
|
| 104 |
+
|
| 105 |
+
Attributes:
|
| 106 |
+
record_id: Unique identifier for the processed record
|
| 107 |
+
timestamp: ISO 8601 timestamp of when processing completed
|
| 108 |
+
mood: Extracted mood data (optional)
|
| 109 |
+
inspirations: List of extracted inspirations
|
| 110 |
+
todos: List of extracted todo items
|
| 111 |
+
error: Error message if processing failed (optional)
|
| 112 |
+
"""
|
| 113 |
+
record_id: str
|
| 114 |
+
timestamp: str
|
| 115 |
+
mood: Optional[MoodData] = None
|
| 116 |
+
inspirations: List[InspirationData] = Field(default_factory=list)
|
| 117 |
+
todos: List[TodoData] = Field(default_factory=list)
|
| 118 |
+
error: Optional[str] = None
|
app/semantic_parser.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Semantic Parser service for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module implements the SemanticParserService class for parsing text
|
| 4 |
+
into structured data (mood, inspirations, todos) using the GLM-4-Flash API.
|
| 5 |
+
|
| 6 |
+
Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 9.2, 9.5
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import json
|
| 11 |
+
from typing import Optional
|
| 12 |
+
import httpx
|
| 13 |
+
|
| 14 |
+
from app.models import ParsedData, MoodData, InspirationData, TodoData
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class SemanticParserError(Exception):
|
| 21 |
+
"""Exception raised when semantic parsing operations fail.
|
| 22 |
+
|
| 23 |
+
This exception is raised when the GLM-4-Flash API call fails,
|
| 24 |
+
such as due to network issues, API errors, or invalid responses.
|
| 25 |
+
|
| 26 |
+
Requirements: 3.5
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, message: str = "语义解析服务不可用"):
|
| 30 |
+
"""Initialize SemanticParserError.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
message: Error message describing the failure
|
| 34 |
+
"""
|
| 35 |
+
super().__init__(message)
|
| 36 |
+
self.message = message
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class SemanticParserService:
|
| 40 |
+
"""Service for parsing text into structured data using GLM-4-Flash API.
|
| 41 |
+
|
| 42 |
+
This service handles semantic parsing by calling the GLM-4-Flash API
|
| 43 |
+
to extract mood, inspirations, and todos from text. It manages API
|
| 44 |
+
authentication, request formatting, response parsing, and error handling.
|
| 45 |
+
|
| 46 |
+
Attributes:
|
| 47 |
+
api_key: Zhipu AI API key for authentication
|
| 48 |
+
client: Async HTTP client for making API requests
|
| 49 |
+
api_url: GLM-4-Flash API endpoint URL
|
| 50 |
+
model: Model identifier
|
| 51 |
+
system_prompt: System prompt for data conversion
|
| 52 |
+
|
| 53 |
+
Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 9.2, 9.5
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, api_key: str):
|
| 57 |
+
"""Initialize the semantic parser service.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
api_key: Zhipu AI API key for authentication
|
| 61 |
+
|
| 62 |
+
Requirements: 3.1, 3.2
|
| 63 |
+
"""
|
| 64 |
+
self.api_key = api_key
|
| 65 |
+
self.client = httpx.AsyncClient(timeout=30.0)
|
| 66 |
+
self.api_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
|
| 67 |
+
self.model = "glm-4-flash"
|
| 68 |
+
|
| 69 |
+
# System prompt as specified in requirements
|
| 70 |
+
self.system_prompt = (
|
| 71 |
+
"你是一个专业的文本语义分析助手。请将用户输入的文本解析为结构化的 JSON 数据。\n\n"
|
| 72 |
+
"你需要提取以下三个维度的信息:\n\n"
|
| 73 |
+
"1. **情绪 (mood)**:\n"
|
| 74 |
+
" - type: 情绪类型(如:喜悦、焦虑、平静、忧虑、兴奋、悲伤等中文词汇)\n"
|
| 75 |
+
" - intensity: 情绪强度(1-10的整数,10表示最强烈)\n"
|
| 76 |
+
" - keywords: 情绪关键词列表(3-5个中文词)\n\n"
|
| 77 |
+
"2. **灵感 (inspirations)**:数组,每个元素包含:\n"
|
| 78 |
+
" - core_idea: 核心观点或想法(20字以内的中文)\n"
|
| 79 |
+
" - tags: 相关标签列表(3-5个中文词)\n"
|
| 80 |
+
" - category: 所属分类(必须是:工作、生活、学习、创意 之一)\n\n"
|
| 81 |
+
"3. **待办 (todos)**:数组,每个元素包含:\n"
|
| 82 |
+
" - task: 任务描述(中文)\n"
|
| 83 |
+
" - time: 时间信息(如:明天、下周、周五等,如果没有则为null)\n"
|
| 84 |
+
" - location: 地点信息(如果没有则为null)\n"
|
| 85 |
+
" - status: 状态(默认为\"pending\")\n\n"
|
| 86 |
+
"**重要规则**:\n"
|
| 87 |
+
"- 如果文本中没有某个维度的信息,mood 返回 null,inspirations 和 todos 返回空数组 []\n"
|
| 88 |
+
"- 必须返回有效的 JSON 格式,不要添加任何其他说明文字\n"
|
| 89 |
+
"- 所有字段名使用英文,内容使用中文\n"
|
| 90 |
+
"- 直接返回 JSON,不要用 markdown 代码块包裹\n\n"
|
| 91 |
+
"返回格式示例:\n"
|
| 92 |
+
"{\n"
|
| 93 |
+
" \"mood\": {\"type\": \"焦虑\", \"intensity\": 7, \"keywords\": [\"压力\", \"疲惫\", \"放松\"]},\n"
|
| 94 |
+
" \"inspirations\": [{\"core_idea\": \"晚霞可以缓解压力\", \"tags\": [\"自然\", \"治愈\"], \"category\": \"生活\"}],\n"
|
| 95 |
+
" \"todos\": [{\"task\": \"整理文档\", \"time\": \"明天\", \"location\": null, \"status\": \"pending\"}]\n"
|
| 96 |
+
"}"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
async def close(self):
|
| 100 |
+
"""Close the HTTP client.
|
| 101 |
+
|
| 102 |
+
This should be called when the service is no longer needed
|
| 103 |
+
to properly clean up resources.
|
| 104 |
+
"""
|
| 105 |
+
await self.client.aclose()
|
| 106 |
+
|
| 107 |
+
async def parse(self, text: str) -> ParsedData:
|
| 108 |
+
"""Parse text into structured data using GLM-4-Flash API.
|
| 109 |
+
|
| 110 |
+
This method sends the text to the GLM-4-Flash API with the configured
|
| 111 |
+
system prompt and returns structured data containing mood, inspirations,
|
| 112 |
+
and todos. It handles API errors, missing dimensions, and logs all errors
|
| 113 |
+
with timestamps and stack traces.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
text: Text content to parse
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
ParsedData object containing mood (optional), inspirations (list),
|
| 120 |
+
and todos (list). Missing dimensions return null or empty arrays.
|
| 121 |
+
|
| 122 |
+
Raises:
|
| 123 |
+
SemanticParserError: If API call fails or returns invalid response
|
| 124 |
+
|
| 125 |
+
Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 9.2, 9.5
|
| 126 |
+
"""
|
| 127 |
+
try:
|
| 128 |
+
# Prepare request headers
|
| 129 |
+
headers = {
|
| 130 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 131 |
+
"Content-Type": "application/json"
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# Prepare request payload
|
| 135 |
+
payload = {
|
| 136 |
+
"model": self.model,
|
| 137 |
+
"messages": [
|
| 138 |
+
{
|
| 139 |
+
"role": "system",
|
| 140 |
+
"content": self.system_prompt
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"role": "user",
|
| 144 |
+
"content": text
|
| 145 |
+
}
|
| 146 |
+
],
|
| 147 |
+
"temperature": 0.7,
|
| 148 |
+
"top_p": 0.9
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
logger.info(f"Calling GLM-4-Flash API for semantic parsing. Text length: {len(text)}")
|
| 152 |
+
|
| 153 |
+
# Make API request
|
| 154 |
+
response = await self.client.post(
|
| 155 |
+
self.api_url,
|
| 156 |
+
headers=headers,
|
| 157 |
+
json=payload
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Check response status
|
| 161 |
+
if response.status_code != 200:
|
| 162 |
+
error_msg = f"GLM-4-Flash API returned status {response.status_code}"
|
| 163 |
+
try:
|
| 164 |
+
error_detail = response.json()
|
| 165 |
+
error_msg += f": {error_detail}"
|
| 166 |
+
except Exception:
|
| 167 |
+
error_msg += f": {response.text}"
|
| 168 |
+
|
| 169 |
+
logger.error(
|
| 170 |
+
f"Semantic parsing API call failed: {error_msg}",
|
| 171 |
+
exc_info=True,
|
| 172 |
+
extra={"timestamp": logger.makeRecord(
|
| 173 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 174 |
+
).created}
|
| 175 |
+
)
|
| 176 |
+
raise SemanticParserError(f"语义解析服务不可用: {error_msg}")
|
| 177 |
+
|
| 178 |
+
# Parse response
|
| 179 |
+
try:
|
| 180 |
+
result = response.json()
|
| 181 |
+
except Exception as e:
|
| 182 |
+
error_msg = f"Failed to parse GLM-4-Flash API response: {str(e)}"
|
| 183 |
+
logger.error(
|
| 184 |
+
error_msg,
|
| 185 |
+
exc_info=True,
|
| 186 |
+
extra={"timestamp": logger.makeRecord(
|
| 187 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 188 |
+
).created}
|
| 189 |
+
)
|
| 190 |
+
raise SemanticParserError(f"语义解析服务不可用: 响应格式无效")
|
| 191 |
+
|
| 192 |
+
# Extract content from response
|
| 193 |
+
try:
|
| 194 |
+
content = result["choices"][0]["message"]["content"]
|
| 195 |
+
except (KeyError, IndexError) as e:
|
| 196 |
+
error_msg = f"Invalid API response structure: {str(e)}"
|
| 197 |
+
logger.error(
|
| 198 |
+
error_msg,
|
| 199 |
+
exc_info=True,
|
| 200 |
+
extra={"timestamp": logger.makeRecord(
|
| 201 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 202 |
+
).created}
|
| 203 |
+
)
|
| 204 |
+
raise SemanticParserError(f"语义解析服务不可用: 响应结构无效")
|
| 205 |
+
|
| 206 |
+
# Parse JSON from content
|
| 207 |
+
try:
|
| 208 |
+
# Try to extract JSON from markdown code blocks if present
|
| 209 |
+
if "```json" in content:
|
| 210 |
+
json_start = content.find("```json") + 7
|
| 211 |
+
json_end = content.find("```", json_start)
|
| 212 |
+
content = content[json_start:json_end].strip()
|
| 213 |
+
elif "```" in content:
|
| 214 |
+
json_start = content.find("```") + 3
|
| 215 |
+
json_end = content.find("```", json_start)
|
| 216 |
+
content = content[json_start:json_end].strip()
|
| 217 |
+
|
| 218 |
+
parsed_json = json.loads(content)
|
| 219 |
+
except json.JSONDecodeError as e:
|
| 220 |
+
error_msg = f"Failed to parse JSON from API response: {str(e)}"
|
| 221 |
+
logger.error(
|
| 222 |
+
error_msg,
|
| 223 |
+
exc_info=True,
|
| 224 |
+
extra={"timestamp": logger.makeRecord(
|
| 225 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 226 |
+
).created}
|
| 227 |
+
)
|
| 228 |
+
raise SemanticParserError(f"语义解析服务不可用: JSON 解析失败")
|
| 229 |
+
|
| 230 |
+
# Extract and validate mood data
|
| 231 |
+
mood = None
|
| 232 |
+
if "mood" in parsed_json and parsed_json["mood"]:
|
| 233 |
+
try:
|
| 234 |
+
mood_data = parsed_json["mood"]
|
| 235 |
+
if isinstance(mood_data, dict):
|
| 236 |
+
mood = MoodData(
|
| 237 |
+
type=mood_data.get("type"),
|
| 238 |
+
intensity=mood_data.get("intensity"),
|
| 239 |
+
keywords=mood_data.get("keywords", [])
|
| 240 |
+
)
|
| 241 |
+
except Exception as e:
|
| 242 |
+
logger.warning(f"Failed to parse mood data: {str(e)}")
|
| 243 |
+
mood = None
|
| 244 |
+
|
| 245 |
+
# Extract and validate inspirations
|
| 246 |
+
inspirations = []
|
| 247 |
+
if "inspirations" in parsed_json and parsed_json["inspirations"]:
|
| 248 |
+
for insp_data in parsed_json["inspirations"]:
|
| 249 |
+
try:
|
| 250 |
+
if isinstance(insp_data, dict):
|
| 251 |
+
inspiration = InspirationData(
|
| 252 |
+
core_idea=insp_data.get("core_idea", ""),
|
| 253 |
+
tags=insp_data.get("tags", []),
|
| 254 |
+
category=insp_data.get("category", "生活")
|
| 255 |
+
)
|
| 256 |
+
inspirations.append(inspiration)
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.warning(f"Failed to parse inspiration data: {str(e)}")
|
| 259 |
+
continue
|
| 260 |
+
|
| 261 |
+
# Extract and validate todos
|
| 262 |
+
todos = []
|
| 263 |
+
if "todos" in parsed_json and parsed_json["todos"]:
|
| 264 |
+
for todo_data in parsed_json["todos"]:
|
| 265 |
+
try:
|
| 266 |
+
if isinstance(todo_data, dict):
|
| 267 |
+
todo = TodoData(
|
| 268 |
+
task=todo_data.get("task", ""),
|
| 269 |
+
time=todo_data.get("time"),
|
| 270 |
+
location=todo_data.get("location"),
|
| 271 |
+
status=todo_data.get("status", "pending")
|
| 272 |
+
)
|
| 273 |
+
todos.append(todo)
|
| 274 |
+
except Exception as e:
|
| 275 |
+
logger.warning(f"Failed to parse todo data: {str(e)}")
|
| 276 |
+
continue
|
| 277 |
+
|
| 278 |
+
logger.info(
|
| 279 |
+
f"Semantic parsing successful. "
|
| 280 |
+
f"Mood: {'present' if mood else 'none'}, "
|
| 281 |
+
f"Inspirations: {len(inspirations)}, "
|
| 282 |
+
f"Todos: {len(todos)}"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return ParsedData(
|
| 286 |
+
mood=mood,
|
| 287 |
+
inspirations=inspirations,
|
| 288 |
+
todos=todos
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
except SemanticParserError:
|
| 292 |
+
# Re-raise SemanticParserError as-is
|
| 293 |
+
raise
|
| 294 |
+
|
| 295 |
+
except httpx.TimeoutException as e:
|
| 296 |
+
error_msg = f"GLM-4-Flash API request timeout: {str(e)}"
|
| 297 |
+
logger.error(
|
| 298 |
+
error_msg,
|
| 299 |
+
exc_info=True,
|
| 300 |
+
extra={"timestamp": logger.makeRecord(
|
| 301 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 302 |
+
).created}
|
| 303 |
+
)
|
| 304 |
+
raise SemanticParserError("语义解析服务不可用: 请求超时")
|
| 305 |
+
|
| 306 |
+
except httpx.RequestError as e:
|
| 307 |
+
error_msg = f"GLM-4-Flash API request failed: {str(e)}"
|
| 308 |
+
logger.error(
|
| 309 |
+
error_msg,
|
| 310 |
+
exc_info=True,
|
| 311 |
+
extra={"timestamp": logger.makeRecord(
|
| 312 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 313 |
+
).created}
|
| 314 |
+
)
|
| 315 |
+
raise SemanticParserError(f"语义解析服务不可用: 网络错误")
|
| 316 |
+
|
| 317 |
+
except Exception as e:
|
| 318 |
+
error_msg = f"Unexpected error in semantic parser service: {str(e)}"
|
| 319 |
+
logger.error(
|
| 320 |
+
error_msg,
|
| 321 |
+
exc_info=True,
|
| 322 |
+
extra={"timestamp": logger.makeRecord(
|
| 323 |
+
logger.name, logging.ERROR, "", 0, error_msg, (), None
|
| 324 |
+
).created}
|
| 325 |
+
)
|
| 326 |
+
raise SemanticParserError(f"语义解析服务不可用: {str(e)}")
|
app/storage.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Storage service for JSON file persistence.
|
| 2 |
+
|
| 3 |
+
This module implements the StorageService class for managing JSON file storage
|
| 4 |
+
of records, moods, inspirations, and todos.
|
| 5 |
+
|
| 6 |
+
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import uuid
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import List, Optional
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
from app.models import RecordData, MoodData, InspirationData, TodoData
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class StorageError(Exception):
|
| 19 |
+
"""Exception raised when storage operations fail.
|
| 20 |
+
|
| 21 |
+
This exception is raised when file operations (read/write) fail,
|
| 22 |
+
such as due to permission issues, disk space, or I/O errors.
|
| 23 |
+
|
| 24 |
+
Requirements: 7.6
|
| 25 |
+
"""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class StorageService:
|
| 30 |
+
"""Service for managing JSON file storage.
|
| 31 |
+
|
| 32 |
+
This service handles persistence of records, moods, inspirations, and todos
|
| 33 |
+
to separate JSON files. It ensures file initialization, generates unique IDs,
|
| 34 |
+
and handles errors appropriately.
|
| 35 |
+
|
| 36 |
+
Attributes:
|
| 37 |
+
data_dir: Directory path for storing JSON files
|
| 38 |
+
records_file: Path to records.json
|
| 39 |
+
moods_file: Path to moods.json
|
| 40 |
+
inspirations_file: Path to inspirations.json
|
| 41 |
+
todos_file: Path to todos.json
|
| 42 |
+
|
| 43 |
+
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, data_dir: str):
|
| 47 |
+
"""Initialize the storage service.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
data_dir: Directory path for storing JSON files
|
| 51 |
+
"""
|
| 52 |
+
self.data_dir = Path(data_dir)
|
| 53 |
+
self.records_file = self.data_dir / "records.json"
|
| 54 |
+
self.moods_file = self.data_dir / "moods.json"
|
| 55 |
+
self.inspirations_file = self.data_dir / "inspirations.json"
|
| 56 |
+
self.todos_file = self.data_dir / "todos.json"
|
| 57 |
+
|
| 58 |
+
# Ensure data directory exists
|
| 59 |
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
|
| 61 |
+
def _ensure_file_exists(self, file_path: Path) -> None:
|
| 62 |
+
"""Ensure a JSON file exists and is initialized with default data.
|
| 63 |
+
|
| 64 |
+
If the file doesn't exist, creates it with sample Chinese data.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
file_path: Path to the JSON file
|
| 68 |
+
|
| 69 |
+
Raises:
|
| 70 |
+
StorageError: If file creation fails
|
| 71 |
+
|
| 72 |
+
Requirements: 7.5
|
| 73 |
+
"""
|
| 74 |
+
if not file_path.exists():
|
| 75 |
+
try:
|
| 76 |
+
# 根据文件类型提供不同的默认数据
|
| 77 |
+
default_data = []
|
| 78 |
+
|
| 79 |
+
if file_path.name == 'records.json':
|
| 80 |
+
default_data = self._get_default_records()
|
| 81 |
+
elif file_path.name == 'moods.json':
|
| 82 |
+
default_data = self._get_default_moods()
|
| 83 |
+
elif file_path.name == 'inspirations.json':
|
| 84 |
+
default_data = self._get_default_inspirations()
|
| 85 |
+
elif file_path.name == 'todos.json':
|
| 86 |
+
default_data = self._get_default_todos()
|
| 87 |
+
elif file_path.name == 'user_config.json':
|
| 88 |
+
default_data = self._get_default_user_config()
|
| 89 |
+
|
| 90 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 91 |
+
json.dump(default_data, f, ensure_ascii=False, indent=2)
|
| 92 |
+
except Exception as e:
|
| 93 |
+
raise StorageError(
|
| 94 |
+
f"Failed to initialize file {file_path}: {str(e)}"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def _get_default_records(self) -> list:
|
| 98 |
+
"""获取默认的记录数据"""
|
| 99 |
+
from datetime import datetime, timedelta
|
| 100 |
+
now = datetime.now()
|
| 101 |
+
|
| 102 |
+
return [
|
| 103 |
+
{
|
| 104 |
+
"record_id": "welcome-1",
|
| 105 |
+
"timestamp": (now - timedelta(hours=2)).isoformat() + "Z",
|
| 106 |
+
"input_type": "text",
|
| 107 |
+
"original_text": "今天天气真好,阳光洒在窗台上,心情也跟着明朗起来。决定下午去公园散散步,感受一下大自然的美好。",
|
| 108 |
+
"parsed_data": {
|
| 109 |
+
"mood": {
|
| 110 |
+
"type": "喜悦",
|
| 111 |
+
"intensity": 8,
|
| 112 |
+
"keywords": ["阳光", "明朗", "美好"]
|
| 113 |
+
},
|
| 114 |
+
"inspirations": [
|
| 115 |
+
{
|
| 116 |
+
"core_idea": "享受自然的美好时光",
|
| 117 |
+
"tags": ["自然", "散步", "放松"],
|
| 118 |
+
"category": "生活"
|
| 119 |
+
}
|
| 120 |
+
],
|
| 121 |
+
"todos": [
|
| 122 |
+
{
|
| 123 |
+
"task": "去公园散步",
|
| 124 |
+
"time": "下午",
|
| 125 |
+
"location": "公园",
|
| 126 |
+
"status": "pending"
|
| 127 |
+
}
|
| 128 |
+
]
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"record_id": "welcome-2",
|
| 133 |
+
"timestamp": (now - timedelta(hours=5)).isoformat() + "Z",
|
| 134 |
+
"input_type": "text",
|
| 135 |
+
"original_text": "刚看完���本很棒的书,书中的一句话让我印象深刻:'生活不是等待暴风雨过去,而是学会在雨中跳舞。'这句话给了我很多启发。",
|
| 136 |
+
"parsed_data": {
|
| 137 |
+
"mood": {
|
| 138 |
+
"type": "平静",
|
| 139 |
+
"intensity": 7,
|
| 140 |
+
"keywords": ["启发", "思考", "感悟"]
|
| 141 |
+
},
|
| 142 |
+
"inspirations": [
|
| 143 |
+
{
|
| 144 |
+
"core_idea": "学会在困难中保持积极",
|
| 145 |
+
"tags": ["人生哲理", "积极心态", "成长"],
|
| 146 |
+
"category": "学习"
|
| 147 |
+
}
|
| 148 |
+
],
|
| 149 |
+
"todos": []
|
| 150 |
+
}
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"record_id": "welcome-3",
|
| 154 |
+
"timestamp": (now - timedelta(days=1, hours=3)).isoformat() + "Z",
|
| 155 |
+
"input_type": "text",
|
| 156 |
+
"original_text": "和好朋友聊了很久,她分享了最近的生活和工作。虽然大家都很忙,但能抽时间见面真的很珍贵。友谊需要用心维护。",
|
| 157 |
+
"parsed_data": {
|
| 158 |
+
"mood": {
|
| 159 |
+
"type": "温暖",
|
| 160 |
+
"intensity": 9,
|
| 161 |
+
"keywords": ["友谊", "珍贵", "陪伴"]
|
| 162 |
+
},
|
| 163 |
+
"inspirations": [
|
| 164 |
+
{
|
| 165 |
+
"core_idea": "珍惜身边的朋友",
|
| 166 |
+
"tags": ["友情", "陪伴", "珍惜"],
|
| 167 |
+
"category": "生活"
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"todos": [
|
| 171 |
+
{
|
| 172 |
+
"task": "定期和朋友联系",
|
| 173 |
+
"time": None,
|
| 174 |
+
"location": None,
|
| 175 |
+
"status": "pending"
|
| 176 |
+
}
|
| 177 |
+
]
|
| 178 |
+
}
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"record_id": "welcome-4",
|
| 182 |
+
"timestamp": (now - timedelta(days=2)).isoformat() + "Z",
|
| 183 |
+
"input_type": "text",
|
| 184 |
+
"original_text": "今天完成了一个困扰我很久的项目,虽然过程很辛苦,但看到成果的那一刻,所有的付出都值得了。成就感满满!",
|
| 185 |
+
"parsed_data": {
|
| 186 |
+
"mood": {
|
| 187 |
+
"type": "兴奋",
|
| 188 |
+
"intensity": 10,
|
| 189 |
+
"keywords": ["成就感", "完成", "满足"]
|
| 190 |
+
},
|
| 191 |
+
"inspirations": [],
|
| 192 |
+
"todos": []
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"record_id": "welcome-5",
|
| 197 |
+
"timestamp": (now - timedelta(days=3)).isoformat() + "Z",
|
| 198 |
+
"input_type": "text",
|
| 199 |
+
"original_text": "最近工作压力有点大,总是担心做不好。但转念一想,每个人都会遇到困难,重要的是保持积极的心态,一步一步来。",
|
| 200 |
+
"parsed_data": {
|
| 201 |
+
"mood": {
|
| 202 |
+
"type": "焦虑",
|
| 203 |
+
"intensity": 6,
|
| 204 |
+
"keywords": ["压力", "担心", "积极"]
|
| 205 |
+
},
|
| 206 |
+
"inspirations": [
|
| 207 |
+
{
|
| 208 |
+
"core_idea": "保持积极心态面对压力",
|
| 209 |
+
"tags": ["心态", "压力管理", "成长"],
|
| 210 |
+
"category": "工作"
|
| 211 |
+
}
|
| 212 |
+
],
|
| 213 |
+
"todos": []
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
def _get_default_moods(self) -> list:
|
| 219 |
+
"""获取默认的心情数据"""
|
| 220 |
+
from datetime import datetime, timedelta
|
| 221 |
+
now = datetime.now()
|
| 222 |
+
|
| 223 |
+
return [
|
| 224 |
+
{
|
| 225 |
+
"record_id": "welcome-1",
|
| 226 |
+
"timestamp": (now - timedelta(hours=2)).isoformat() + "Z",
|
| 227 |
+
"type": "喜悦",
|
| 228 |
+
"intensity": 8,
|
| 229 |
+
"keywords": ["阳光", "明朗", "美好"]
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"record_id": "welcome-2",
|
| 233 |
+
"timestamp": (now - timedelta(hours=5)).isoformat() + "Z",
|
| 234 |
+
"type": "平静",
|
| 235 |
+
"intensity": 7,
|
| 236 |
+
"keywords": ["启发", "思考", "感悟"]
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"record_id": "welcome-3",
|
| 240 |
+
"timestamp": (now - timedelta(days=1, hours=3)).isoformat() + "Z",
|
| 241 |
+
"type": "温暖",
|
| 242 |
+
"intensity": 9,
|
| 243 |
+
"keywords": ["友谊", "珍贵", "陪伴"]
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"record_id": "welcome-4",
|
| 247 |
+
"timestamp": (now - timedelta(days=2)).isoformat() + "Z",
|
| 248 |
+
"type": "兴奋",
|
| 249 |
+
"intensity": 10,
|
| 250 |
+
"keywords": ["成就感", "完成", "满足"]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"record_id": "welcome-5",
|
| 254 |
+
"timestamp": (now - timedelta(days=3)).isoformat() + "Z",
|
| 255 |
+
"type": "焦虑",
|
| 256 |
+
"intensity": 6,
|
| 257 |
+
"keywords": ["压力", "担心", "积极"]
|
| 258 |
+
}
|
| 259 |
+
]
|
| 260 |
+
|
| 261 |
+
def _get_default_inspirations(self) -> list:
|
| 262 |
+
"""获取默认的灵感数据"""
|
| 263 |
+
from datetime import datetime, timedelta
|
| 264 |
+
now = datetime.now()
|
| 265 |
+
|
| 266 |
+
return [
|
| 267 |
+
{
|
| 268 |
+
"record_id": "welcome-1",
|
| 269 |
+
"timestamp": (now - timedelta(hours=2)).isoformat() + "Z",
|
| 270 |
+
"core_idea": "享受自然的美好时光",
|
| 271 |
+
"tags": ["自然", "散步", "放松"],
|
| 272 |
+
"category": "生活"
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"record_id": "welcome-2",
|
| 276 |
+
"timestamp": (now - timedelta(hours=5)).isoformat() + "Z",
|
| 277 |
+
"core_idea": "学会在困难中保持积极",
|
| 278 |
+
"tags": ["人生哲理", "积极心态", "成长"],
|
| 279 |
+
"category": "学习"
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"record_id": "welcome-3",
|
| 283 |
+
"timestamp": (now - timedelta(days=1, hours=3)).isoformat() + "Z",
|
| 284 |
+
"core_idea": "珍惜身边的朋友",
|
| 285 |
+
"tags": ["友情", "陪伴", "珍惜"],
|
| 286 |
+
"category": "生活"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"record_id": "welcome-5",
|
| 290 |
+
"timestamp": (now - timedelta(days=3)).isoformat() + "Z",
|
| 291 |
+
"core_idea": "保持积极心态面对压力",
|
| 292 |
+
"tags": ["心态", "压力管理", "成长"],
|
| 293 |
+
"category": "工作"
|
| 294 |
+
}
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
def _get_default_todos(self) -> list:
|
| 298 |
+
"""获取默认的待办数据"""
|
| 299 |
+
from datetime import datetime, timedelta
|
| 300 |
+
now = datetime.now()
|
| 301 |
+
|
| 302 |
+
return [
|
| 303 |
+
{
|
| 304 |
+
"record_id": "welcome-1",
|
| 305 |
+
"timestamp": (now - timedelta(hours=2)).isoformat() + "Z",
|
| 306 |
+
"task": "去公园散步",
|
| 307 |
+
"time": "下午",
|
| 308 |
+
"location": "公园",
|
| 309 |
+
"status": "pending"
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"record_id": "welcome-3",
|
| 313 |
+
"timestamp": (now - timedelta(days=1, hours=3)).isoformat() + "Z",
|
| 314 |
+
"task": "定期和朋友联系",
|
| 315 |
+
"time": None,
|
| 316 |
+
"location": None,
|
| 317 |
+
"status": "pending"
|
| 318 |
+
}
|
| 319 |
+
]
|
| 320 |
+
|
| 321 |
+
def _get_default_user_config(self) -> dict:
|
| 322 |
+
"""获取默认的用户配置"""
|
| 323 |
+
return {
|
| 324 |
+
"character": {
|
| 325 |
+
"image_url": "", # 空字符串,前端会显示占位符
|
| 326 |
+
"prompt": "默认形象:薰衣草紫色温柔猫咪",
|
| 327 |
+
"preferences": {
|
| 328 |
+
"color": "薰衣草紫",
|
| 329 |
+
"personality": "温柔",
|
| 330 |
+
"appearance": "无配饰",
|
| 331 |
+
"role": "陪伴式朋友"
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
def _read_json_file(self, file_path: Path) -> List:
|
| 337 |
+
"""Read and parse a JSON file.
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
file_path: Path to the JSON file
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
List of records from the JSON file
|
| 344 |
+
|
| 345 |
+
Raises:
|
| 346 |
+
StorageError: If file reading or parsing fails
|
| 347 |
+
"""
|
| 348 |
+
self._ensure_file_exists(file_path)
|
| 349 |
+
try:
|
| 350 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 351 |
+
return json.load(f)
|
| 352 |
+
except Exception as e:
|
| 353 |
+
raise StorageError(
|
| 354 |
+
f"Failed to read file {file_path}: {str(e)}"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
def _write_json_file(self, file_path: Path, data: List) -> None:
|
| 358 |
+
"""Write data to a JSON file.
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
file_path: Path to the JSON file
|
| 362 |
+
data: List of records to write
|
| 363 |
+
|
| 364 |
+
Raises:
|
| 365 |
+
StorageError: If file writing fails
|
| 366 |
+
|
| 367 |
+
Requirements: 7.6
|
| 368 |
+
"""
|
| 369 |
+
try:
|
| 370 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 371 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 372 |
+
except Exception as e:
|
| 373 |
+
raise StorageError(
|
| 374 |
+
f"Failed to write file {file_path}: {str(e)}"
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
def save_record(self, record: RecordData) -> str:
|
| 378 |
+
"""Save a complete record to records.json.
|
| 379 |
+
|
| 380 |
+
Generates a unique UUID for the record if not already set,
|
| 381 |
+
and appends the record to the records.json file.
|
| 382 |
+
|
| 383 |
+
Args:
|
| 384 |
+
record: RecordData object to save
|
| 385 |
+
|
| 386 |
+
Returns:
|
| 387 |
+
The unique record_id (UUID string)
|
| 388 |
+
|
| 389 |
+
Raises:
|
| 390 |
+
StorageError: If file writing fails
|
| 391 |
+
|
| 392 |
+
Requirements: 7.1, 7.7
|
| 393 |
+
"""
|
| 394 |
+
# Generate unique UUID if not set
|
| 395 |
+
if not record.record_id:
|
| 396 |
+
record.record_id = str(uuid.uuid4())
|
| 397 |
+
|
| 398 |
+
# Read existing records
|
| 399 |
+
records = self._read_json_file(self.records_file)
|
| 400 |
+
|
| 401 |
+
# Append new record
|
| 402 |
+
records.append(record.model_dump())
|
| 403 |
+
|
| 404 |
+
# Write back to file
|
| 405 |
+
self._write_json_file(self.records_file, records)
|
| 406 |
+
|
| 407 |
+
return record.record_id
|
| 408 |
+
|
| 409 |
+
def append_mood(self, mood: MoodData, record_id: str, timestamp: str) -> None:
|
| 410 |
+
"""Append mood data to moods.json.
|
| 411 |
+
|
| 412 |
+
Args:
|
| 413 |
+
mood: MoodData object to append
|
| 414 |
+
record_id: Associated record ID
|
| 415 |
+
timestamp: ISO 8601 timestamp
|
| 416 |
+
|
| 417 |
+
Raises:
|
| 418 |
+
StorageError: If file writing fails
|
| 419 |
+
|
| 420 |
+
Requirements: 7.2
|
| 421 |
+
"""
|
| 422 |
+
# Read existing moods
|
| 423 |
+
moods = self._read_json_file(self.moods_file)
|
| 424 |
+
|
| 425 |
+
# Create mood entry with metadata
|
| 426 |
+
mood_entry = {
|
| 427 |
+
"record_id": record_id,
|
| 428 |
+
"timestamp": timestamp,
|
| 429 |
+
**mood.model_dump()
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
# Append new mood
|
| 433 |
+
moods.append(mood_entry)
|
| 434 |
+
|
| 435 |
+
# Write back to file
|
| 436 |
+
self._write_json_file(self.moods_file, moods)
|
| 437 |
+
|
| 438 |
+
def append_inspirations(
|
| 439 |
+
self,
|
| 440 |
+
inspirations: List[InspirationData],
|
| 441 |
+
record_id: str,
|
| 442 |
+
timestamp: str
|
| 443 |
+
) -> None:
|
| 444 |
+
"""Append inspiration data to inspirations.json.
|
| 445 |
+
|
| 446 |
+
Args:
|
| 447 |
+
inspirations: List of InspirationData objects to append
|
| 448 |
+
record_id: Associated record ID
|
| 449 |
+
timestamp: ISO 8601 timestamp
|
| 450 |
+
|
| 451 |
+
Raises:
|
| 452 |
+
StorageError: If file writing fails
|
| 453 |
+
|
| 454 |
+
Requirements: 7.3
|
| 455 |
+
"""
|
| 456 |
+
if not inspirations:
|
| 457 |
+
return
|
| 458 |
+
|
| 459 |
+
# Read existing inspirations
|
| 460 |
+
all_inspirations = self._read_json_file(self.inspirations_file)
|
| 461 |
+
|
| 462 |
+
# Create inspiration entries with metadata
|
| 463 |
+
for inspiration in inspirations:
|
| 464 |
+
inspiration_entry = {
|
| 465 |
+
"record_id": record_id,
|
| 466 |
+
"timestamp": timestamp,
|
| 467 |
+
**inspiration.model_dump()
|
| 468 |
+
}
|
| 469 |
+
all_inspirations.append(inspiration_entry)
|
| 470 |
+
|
| 471 |
+
# Write back to file
|
| 472 |
+
self._write_json_file(self.inspirations_file, all_inspirations)
|
| 473 |
+
|
| 474 |
+
def append_todos(
|
| 475 |
+
self,
|
| 476 |
+
todos: List[TodoData],
|
| 477 |
+
record_id: str,
|
| 478 |
+
timestamp: str
|
| 479 |
+
) -> None:
|
| 480 |
+
"""Append todo data to todos.json.
|
| 481 |
+
|
| 482 |
+
Args:
|
| 483 |
+
todos: List of TodoData objects to append
|
| 484 |
+
record_id: Associated record ID
|
| 485 |
+
timestamp: ISO 8601 timestamp
|
| 486 |
+
|
| 487 |
+
Raises:
|
| 488 |
+
StorageError: If file writing fails
|
| 489 |
+
|
| 490 |
+
Requirements: 7.4
|
| 491 |
+
"""
|
| 492 |
+
if not todos:
|
| 493 |
+
return
|
| 494 |
+
|
| 495 |
+
# Read existing todos
|
| 496 |
+
all_todos = self._read_json_file(self.todos_file)
|
| 497 |
+
|
| 498 |
+
# Create todo entries with metadata
|
| 499 |
+
for todo in todos:
|
| 500 |
+
todo_entry = {
|
| 501 |
+
"record_id": record_id,
|
| 502 |
+
"timestamp": timestamp,
|
| 503 |
+
**todo.model_dump()
|
| 504 |
+
}
|
| 505 |
+
all_todos.append(todo_entry)
|
| 506 |
+
|
| 507 |
+
# Write back to file
|
| 508 |
+
self._write_json_file(self.todos_file, all_todos)
|
app/user_config.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User configuration management for Voice Text Processor.
|
| 2 |
+
|
| 3 |
+
This module handles user-specific configurations, including
|
| 4 |
+
the generated cat character image settings.
|
| 5 |
+
|
| 6 |
+
Requirements: PRD - AI形象生成模块
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
from typing import Optional, Dict, List
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class UserConfig:
|
| 19 |
+
"""User configuration manager.
|
| 20 |
+
|
| 21 |
+
This class manages user-specific settings, particularly
|
| 22 |
+
the generated cat character image configuration.
|
| 23 |
+
|
| 24 |
+
Attributes:
|
| 25 |
+
config_dir: Directory for storing user configurations
|
| 26 |
+
config_file: Path to the user config JSON file
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, config_dir: str = "data"):
|
| 30 |
+
"""Initialize user configuration manager.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
config_dir: Directory for storing configurations
|
| 34 |
+
"""
|
| 35 |
+
self.config_dir = config_dir
|
| 36 |
+
self.config_file = os.path.join(config_dir, "user_config.json")
|
| 37 |
+
|
| 38 |
+
# 确保目录存在
|
| 39 |
+
os.makedirs(config_dir, exist_ok=True)
|
| 40 |
+
|
| 41 |
+
# 初始化配置文件
|
| 42 |
+
if not os.path.exists(self.config_file):
|
| 43 |
+
self._init_config_file()
|
| 44 |
+
|
| 45 |
+
def _init_config_file(self):
|
| 46 |
+
"""Initialize the configuration file with default values."""
|
| 47 |
+
default_config = {
|
| 48 |
+
"user_id": "default_user",
|
| 49 |
+
"created_at": datetime.utcnow().isoformat() + "Z",
|
| 50 |
+
"character": {
|
| 51 |
+
"image_url": "", # 空字符串,前端会显示占位符
|
| 52 |
+
"prompt": "默认治愈系小猫形象",
|
| 53 |
+
"revised_prompt": "一只薰衣草紫色的温柔猫咪,治愈系风格,温暖的陪伴者",
|
| 54 |
+
"preferences": {
|
| 55 |
+
"color": "薰衣草紫",
|
| 56 |
+
"personality": "温柔",
|
| 57 |
+
"appearance": "无配饰",
|
| 58 |
+
"role": "陪伴式朋友"
|
| 59 |
+
},
|
| 60 |
+
"generated_at": datetime.utcnow().isoformat() + "Z",
|
| 61 |
+
"generation_count": 0
|
| 62 |
+
},
|
| 63 |
+
"settings": {
|
| 64 |
+
"theme": "light",
|
| 65 |
+
"language": "zh-CN"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
| 70 |
+
json.dump(default_config, f, ensure_ascii=False, indent=2)
|
| 71 |
+
|
| 72 |
+
logger.info(f"Initialized user config file: {self.config_file}")
|
| 73 |
+
|
| 74 |
+
def load_config(self) -> Dict:
|
| 75 |
+
"""Load user configuration from file.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Dictionary containing user configuration
|
| 79 |
+
"""
|
| 80 |
+
try:
|
| 81 |
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
| 82 |
+
config = json.load(f)
|
| 83 |
+
return config
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.error(f"Failed to load user config: {str(e)}")
|
| 86 |
+
# 返回默认配置
|
| 87 |
+
self._init_config_file()
|
| 88 |
+
return self.load_config()
|
| 89 |
+
|
| 90 |
+
def save_config(self, config: Dict):
|
| 91 |
+
"""Save user configuration to file.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
config: Configuration dictionary to save
|
| 95 |
+
"""
|
| 96 |
+
try:
|
| 97 |
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
| 98 |
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
| 99 |
+
logger.info("User config saved successfully")
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"Failed to save user config: {str(e)}")
|
| 102 |
+
raise
|
| 103 |
+
|
| 104 |
+
def get_character_config(self) -> Dict:
|
| 105 |
+
"""Get character configuration.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
Dictionary containing character settings
|
| 109 |
+
"""
|
| 110 |
+
config = self.load_config()
|
| 111 |
+
return config.get("character", {})
|
| 112 |
+
|
| 113 |
+
def save_character_image(
|
| 114 |
+
self,
|
| 115 |
+
image_url: str,
|
| 116 |
+
prompt: str,
|
| 117 |
+
revised_prompt: Optional[str] = None,
|
| 118 |
+
preferences: Optional[Dict] = None
|
| 119 |
+
):
|
| 120 |
+
"""Save generated character image configuration.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
image_url: URL of the generated image
|
| 124 |
+
prompt: Prompt used for generation
|
| 125 |
+
revised_prompt: AI-revised prompt (optional)
|
| 126 |
+
preferences: User preferences used (optional)
|
| 127 |
+
"""
|
| 128 |
+
config = self.load_config()
|
| 129 |
+
|
| 130 |
+
# 更新角色配置
|
| 131 |
+
config["character"]["image_url"] = image_url
|
| 132 |
+
config["character"]["prompt"] = prompt
|
| 133 |
+
config["character"]["revised_prompt"] = revised_prompt or prompt
|
| 134 |
+
config["character"]["generated_at"] = datetime.utcnow().isoformat() + "Z"
|
| 135 |
+
config["character"]["generation_count"] += 1
|
| 136 |
+
|
| 137 |
+
if preferences:
|
| 138 |
+
config["character"]["preferences"] = preferences
|
| 139 |
+
|
| 140 |
+
self.save_config(config)
|
| 141 |
+
logger.info(f"Character image saved: {image_url[:50]}...")
|
| 142 |
+
|
| 143 |
+
def get_character_image_url(self) -> Optional[str]:
|
| 144 |
+
"""Get the current character image URL.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
Image URL or None if not set
|
| 148 |
+
"""
|
| 149 |
+
character = self.get_character_config()
|
| 150 |
+
return character.get("image_url")
|
| 151 |
+
|
| 152 |
+
def get_character_preferences(self) -> Dict:
|
| 153 |
+
"""Get character generation preferences.
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Dictionary containing color, personality, appearance, role
|
| 157 |
+
"""
|
| 158 |
+
character = self.get_character_config()
|
| 159 |
+
return character.get("preferences", {
|
| 160 |
+
"color": "温暖粉",
|
| 161 |
+
"personality": "温柔",
|
| 162 |
+
"appearance": "无配饰",
|
| 163 |
+
"role": "陪伴式朋友"
|
| 164 |
+
})
|
| 165 |
+
|
| 166 |
+
def update_character_preferences(
|
| 167 |
+
self,
|
| 168 |
+
color: Optional[str] = None,
|
| 169 |
+
personality: Optional[str] = None,
|
| 170 |
+
appearance: Optional[str] = None,
|
| 171 |
+
role: Optional[str] = None
|
| 172 |
+
):
|
| 173 |
+
"""Update character generation preferences.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
color: Color preference (optional)
|
| 177 |
+
personality: Personality trait (optional)
|
| 178 |
+
appearance: Appearance feature (optional)
|
| 179 |
+
role: Character role (optional)
|
| 180 |
+
"""
|
| 181 |
+
config = self.load_config()
|
| 182 |
+
preferences = config["character"]["preferences"]
|
| 183 |
+
|
| 184 |
+
if color:
|
| 185 |
+
preferences["color"] = color
|
| 186 |
+
if personality:
|
| 187 |
+
preferences["personality"] = personality
|
| 188 |
+
if appearance:
|
| 189 |
+
preferences["appearance"] = appearance
|
| 190 |
+
if role:
|
| 191 |
+
preferences["role"] = role
|
| 192 |
+
|
| 193 |
+
self.save_config(config)
|
| 194 |
+
logger.info("Character preferences updated")
|
| 195 |
+
|
| 196 |
+
def get_generation_count(self) -> int:
|
| 197 |
+
"""Get the number of times character has been generated.
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
Generation count
|
| 201 |
+
"""
|
| 202 |
+
character = self.get_character_config()
|
| 203 |
+
return character.get("generation_count", 0)
|
| 204 |
+
|
| 205 |
+
def has_character_image(self) -> bool:
|
| 206 |
+
"""Check if user has a character image set.
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
True if character image exists, False otherwise
|
| 210 |
+
"""
|
| 211 |
+
return self.get_character_image_url() is not None
|
data/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file ensures the data directory is tracked by git
|
deployment/DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 部署指南
|
| 2 |
+
|
| 3 |
+
## 部署到 Hugging Face Spaces
|
| 4 |
+
|
| 5 |
+
### 前置准备
|
| 6 |
+
|
| 7 |
+
1. **构建前端**
|
| 8 |
+
```bash
|
| 9 |
+
cd frontend
|
| 10 |
+
npm install
|
| 11 |
+
npm run build
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
2. **验证构建产物**
|
| 15 |
+
- 确保 `frontend/dist/` 目录存在
|
| 16 |
+
- 包含 `index.html` 和 `assets/` 文件夹
|
| 17 |
+
|
| 18 |
+
### 自动部署(推荐)
|
| 19 |
+
|
| 20 |
+
**Windows:**
|
| 21 |
+
```bash
|
| 22 |
+
build_and_deploy.bat
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
**Linux/Mac:**
|
| 26 |
+
```bash
|
| 27 |
+
chmod +x build_and_deploy.sh
|
| 28 |
+
./build_and_deploy.sh
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 手动部署
|
| 32 |
+
|
| 33 |
+
1. **构建前端**
|
| 34 |
+
```bash
|
| 35 |
+
cd frontend
|
| 36 |
+
npm run build
|
| 37 |
+
cd ..
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
2. **提交更改**
|
| 41 |
+
```bash
|
| 42 |
+
git add .
|
| 43 |
+
git commit -m "Deploy: Update frontend build"
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
3. **推送到 Hugging Face**
|
| 47 |
+
```bash
|
| 48 |
+
git push hf main
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 配置 Hugging Face Secrets
|
| 52 |
+
|
| 53 |
+
在 Space 的 Settings → Repository secrets 中添加:
|
| 54 |
+
|
| 55 |
+
**必需:**
|
| 56 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 57 |
+
- 获取:https://open.bigmodel.cn/
|
| 58 |
+
|
| 59 |
+
**可选:**
|
| 60 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 61 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 62 |
+
- 获取:https://platform.minimaxi.com/
|
| 63 |
+
|
| 64 |
+
### 访问应用
|
| 65 |
+
|
| 66 |
+
部署成功后,访问:
|
| 67 |
+
- **前端应用**: `https://your-space.hf.space/app`
|
| 68 |
+
- **Gradio 界面**: `https://your-space.hf.space/gradio`
|
| 69 |
+
- **API 文档**: `https://your-space.hf.space/docs`
|
| 70 |
+
|
| 71 |
+
### 文件结构
|
| 72 |
+
|
| 73 |
+
```
|
| 74 |
+
.
|
| 75 |
+
├── app.py # Hugging Face 入口文件
|
| 76 |
+
├── app/ # FastAPI 后端
|
| 77 |
+
│ ├── main.py # 主应用
|
| 78 |
+
│ └── ...
|
| 79 |
+
├── frontend/
|
| 80 |
+
│ ├── dist/ # 构建产物(需要提交)
|
| 81 |
+
│ │ ├── index.html
|
| 82 |
+
│ │ └── assets/
|
| 83 |
+
│ └── ...
|
| 84 |
+
├── requirements_hf.txt # Python 依赖
|
| 85 |
+
└── README_HF.md # Hugging Face 说明
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### 故障排查
|
| 89 |
+
|
| 90 |
+
**问题:前端 404**
|
| 91 |
+
- 检查 `frontend/dist/` 是否存在
|
| 92 |
+
- 确认已运行 `npm run build`
|
| 93 |
+
- 查看 Space 日志确认文件已上传
|
| 94 |
+
|
| 95 |
+
**问题:API 调用失败**
|
| 96 |
+
- 检查 Secrets 是否正确配置
|
| 97 |
+
- 查看 Space 日志中的错误信息
|
| 98 |
+
- 确认 API 密钥有效
|
| 99 |
+
|
| 100 |
+
**问题:静态资源加载失败**
|
| 101 |
+
- 检查 `frontend/dist/assets/` 是否存在
|
| 102 |
+
- 确认 CSS 和 JS 文件已生成
|
| 103 |
+
- 查看浏览器控制台的网络请求
|
| 104 |
+
|
| 105 |
+
### 本地测试
|
| 106 |
+
|
| 107 |
+
在部署前本地测试:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
# 构建前端
|
| 111 |
+
cd frontend && npm run build && cd ..
|
| 112 |
+
|
| 113 |
+
# 运行应用
|
| 114 |
+
python app.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
访问 `http://localhost:7860/app` 测试前端应用。
|
| 118 |
+
|
| 119 |
+
### 更新部署
|
| 120 |
+
|
| 121 |
+
每次修改前端代码后:
|
| 122 |
+
|
| 123 |
+
1. 重新构建:`cd frontend && npm run build && cd ..`
|
| 124 |
+
2. 提交更改:`git add . && git commit -m "Update"`
|
| 125 |
+
3. 推送:`git push hf main`
|
| 126 |
+
|
| 127 |
+
### 注意事项
|
| 128 |
+
|
| 129 |
+
- ✅ `frontend/dist/` 必须提交到 Git(不要在 .gitignore 中忽略)
|
| 130 |
+
- ✅ 每次修改前端代码都需要重新构建
|
| 131 |
+
- ✅ Hugging Face Spaces 会自动重启应用
|
| 132 |
+
- ⚠️ 首次部署可能需要 5-10 分钟
|
| 133 |
+
- ⚠️ 免费 Space 可能会在不活跃时休眠
|
deployment/DEPLOY_CHECKLIST.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces 部署检查清单
|
| 2 |
+
|
| 3 |
+
## 📋 部署前检查
|
| 4 |
+
|
| 5 |
+
### 1. 依赖版本确认
|
| 6 |
+
- [ ] `requirements_hf.txt` 中 `huggingface-hub==0.23.5`
|
| 7 |
+
- [ ] `requirements_hf.txt` 中 `gradio==4.44.0`
|
| 8 |
+
- [ ] `README_HF.md` frontmatter 中 `sdk_version: "4.44.0"`
|
| 9 |
+
|
| 10 |
+
### 2. 文件结构确认
|
| 11 |
+
- [ ] `app.py` 存在且正确
|
| 12 |
+
- [ ] `frontend/dist/` 已构建(运行 `cd frontend && npm run build`)
|
| 13 |
+
- [ ] `data/` 目录存在
|
| 14 |
+
- [ ] `generated_images/` 目录存在
|
| 15 |
+
|
| 16 |
+
### 3. 环境变量配置
|
| 17 |
+
在 Space Settings → Repository secrets 中配置:
|
| 18 |
+
- [ ] `ZHIPU_API_KEY` - 必需
|
| 19 |
+
- [ ] `MINIMAX_API_KEY` - 可选
|
| 20 |
+
- [ ] `MINIMAX_GROUP_ID` - 可选
|
| 21 |
+
|
| 22 |
+
## 🚀 部署步骤
|
| 23 |
+
|
| 24 |
+
### 方法 1: 使用 deploy_to_hf.sh (推荐)
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
# 1. 确保脚本可执行
|
| 28 |
+
chmod +x deploy_to_hf.sh
|
| 29 |
+
|
| 30 |
+
# 2. 运行部署脚本
|
| 31 |
+
./deploy_to_hf.sh
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 方法 2: 手动部署
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
# 1. 构建前端
|
| 38 |
+
cd frontend
|
| 39 |
+
npm install
|
| 40 |
+
npm run build
|
| 41 |
+
cd ..
|
| 42 |
+
|
| 43 |
+
# 2. 提交到 Git
|
| 44 |
+
git add .
|
| 45 |
+
git commit -m "Deploy to Hugging Face Spaces"
|
| 46 |
+
|
| 47 |
+
# 3. 推送到 Hugging Face
|
| 48 |
+
git push hf main
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## 🐛 常见问题
|
| 52 |
+
|
| 53 |
+
### ImportError: cannot import name 'HfFolder'
|
| 54 |
+
|
| 55 |
+
**原因:** `gradio` 和 `huggingface_hub` 版本不兼容
|
| 56 |
+
|
| 57 |
+
**解决方法:**
|
| 58 |
+
1. 确认 `requirements_hf.txt` 版本正确
|
| 59 |
+
2. 在 Space Settings 中点击 "Factory reboot"
|
| 60 |
+
3. 查看 Container logs 确认安装的版本
|
| 61 |
+
|
| 62 |
+
### 前端 404 错误
|
| 63 |
+
|
| 64 |
+
**原因:** 前端未构建或未正确挂载
|
| 65 |
+
|
| 66 |
+
**解决方法:**
|
| 67 |
+
1. 本地运行 `cd frontend && npm run build`
|
| 68 |
+
2. 确认 `frontend/dist/` 目录存在且有内容
|
| 69 |
+
3. 提交并推送 `frontend/dist/` 到仓库
|
| 70 |
+
|
| 71 |
+
### API 调用失败
|
| 72 |
+
|
| 73 |
+
**原因:** 环境变量未配置
|
| 74 |
+
|
| 75 |
+
**解决方法:**
|
| 76 |
+
1. 在 Space Settings → Repository secrets 添加 `ZHIPU_API_KEY`
|
| 77 |
+
2. 重启 Space
|
| 78 |
+
3. 查看 Logs 确认 API 密钥已加载
|
| 79 |
+
|
| 80 |
+
## 📊 部署后验证
|
| 81 |
+
|
| 82 |
+
### 1. 健康检查
|
| 83 |
+
访问 `https://your-space.hf.space/health` 应返回:
|
| 84 |
+
```json
|
| 85 |
+
{
|
| 86 |
+
"status": "healthy",
|
| 87 |
+
"timestamp": "..."
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 2. API 文档
|
| 92 |
+
访问 `https://your-space.hf.space/docs` 查看 API 文档
|
| 93 |
+
|
| 94 |
+
### 3. 前端访问
|
| 95 |
+
访问 `https://your-space.hf.space/` 应显示应用界面
|
| 96 |
+
|
| 97 |
+
### 4. 功能测试
|
| 98 |
+
- [ ] 首页输入框可以输入文字
|
| 99 |
+
- [ ] 点击麦克风可以录音(需要浏览器权限)
|
| 100 |
+
- [ ] 点击 AI 形象显示对话框
|
| 101 |
+
- [ ] 底部导航可以切换页面
|
| 102 |
+
|
| 103 |
+
## 🔄 更新部署
|
| 104 |
+
|
| 105 |
+
### 代码更新
|
| 106 |
+
```bash
|
| 107 |
+
git add .
|
| 108 |
+
git commit -m "Update: description"
|
| 109 |
+
git push hf main
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### 强制重建
|
| 113 |
+
如果遇到缓存问题:
|
| 114 |
+
1. 进入 Space Settings
|
| 115 |
+
2. 点击 "Factory reboot"
|
| 116 |
+
3. 等待重新构建完成
|
| 117 |
+
|
| 118 |
+
## 📝 版本兼容性
|
| 119 |
+
|
| 120 |
+
### 已测试的稳定组合
|
| 121 |
+
|
| 122 |
+
| gradio | huggingface-hub | Python | 状态 |
|
| 123 |
+
|--------|----------------|--------|------|
|
| 124 |
+
| 4.44.0 | 0.23.5 | 3.11 | ✅ 推荐 |
|
| 125 |
+
| 4.36.1 | 0.23.0 | 3.11 | ✅ 可用 |
|
| 126 |
+
| 5.x | latest | 3.11 | ❌ 不兼容 |
|
| 127 |
+
|
| 128 |
+
### 不兼容的组合
|
| 129 |
+
|
| 130 |
+
- `gradio==4.x` + `huggingface-hub>=0.24.0` → HfFolder 错误
|
| 131 |
+
- `gradio==5.x` + `huggingface-hub<0.24.0` → 版本冲突
|
| 132 |
+
|
| 133 |
+
## 🔗 相关资源
|
| 134 |
+
|
| 135 |
+
- [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces)
|
| 136 |
+
- [Gradio 文档](https://www.gradio.app/docs)
|
| 137 |
+
- [项目 README](./README.md)
|
deployment/Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 安装系统依赖
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# 复制依赖文件
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
|
| 13 |
+
# 安装 Python 依赖
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# 复制应用代码
|
| 17 |
+
COPY app/ ./app/
|
| 18 |
+
COPY data/ ./data/
|
| 19 |
+
COPY frontend/dist/ ./frontend/dist/
|
| 20 |
+
|
| 21 |
+
# 复制启动脚本
|
| 22 |
+
COPY start.py .
|
| 23 |
+
|
| 24 |
+
# 创建必要的目录
|
| 25 |
+
RUN mkdir -p generated_images logs
|
| 26 |
+
|
| 27 |
+
# 暴露端口
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# 启动应用
|
| 31 |
+
CMD ["python", "start.py"]
|
deployment/README_HF.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Nora - 治愈系记录助手
|
| 3 |
+
emoji: 🌟
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🌟 治愈系记录助手 - SoulMate AI Companion
|
| 12 |
+
|
| 13 |
+
一个温暖、治愈的 AI 陪伴应用,帮助你记录心情、捕捉灵感、管理待办。
|
| 14 |
+
|
| 15 |
+
## ✨ 核心特性
|
| 16 |
+
|
| 17 |
+
- 🎤 **语音/文字快速记录** - 自动分类保存
|
| 18 |
+
- 🤖 **AI 语义解析** - 智能提取情绪、灵感和待办
|
| 19 |
+
- 💬 **AI 对话陪伴(RAG)** - 基于历史记录的个性化对话
|
| 20 |
+
- 🖼️ **AI 形象定制** - 生成专属治愈系角色(720 种组合)
|
| 21 |
+
- 🫧 **物理引擎心情池** - 基于 Matter.js 的动态气泡可视化
|
| 22 |
+
|
| 23 |
+
## 🚀 快速开始
|
| 24 |
+
|
| 25 |
+
### 在线使用
|
| 26 |
+
|
| 27 |
+
直接访问本 Space 即可使用完整功能!
|
| 28 |
+
|
| 29 |
+
### 配置 API 密钥
|
| 30 |
+
|
| 31 |
+
在 Space 的 **Settings → Repository secrets** 中配置:
|
| 32 |
+
|
| 33 |
+
**必需:**
|
| 34 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 35 |
+
- 获取地址:https://open.bigmodel.cn/
|
| 36 |
+
- 用途:语音识别、语义解析、AI 对话
|
| 37 |
+
|
| 38 |
+
**可选:**
|
| 39 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 40 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 41 |
+
- 获取地址:https://platform.minimaxi.com/
|
| 42 |
+
- 用途:AI 形象生成
|
| 43 |
+
|
| 44 |
+
## 📖 使用说明
|
| 45 |
+
|
| 46 |
+
1. **首页快速记录**
|
| 47 |
+
- 点击麦克风录音或在输入框输入文字
|
| 48 |
+
- AI 自动分析并分类保存
|
| 49 |
+
|
| 50 |
+
2. **查看分类数据**
|
| 51 |
+
- 点击顶部心情、灵感、待办图标
|
| 52 |
+
- 查看不同类型的记录
|
| 53 |
+
|
| 54 |
+
3. **与 AI 对话**
|
| 55 |
+
- 点击 AI 形象显示问候对话框
|
| 56 |
+
- 点击对话框中的聊天图标进入完整对话
|
| 57 |
+
- AI 基于你的历史记录提供个性化回复
|
| 58 |
+
|
| 59 |
+
4. **定制 AI 形象**
|
| 60 |
+
- 点击右下角 ✨ 按钮
|
| 61 |
+
- 选择颜色、性格、外观、角色
|
| 62 |
+
- 生成专属形象(需要 MiniMax API)
|
| 63 |
+
|
| 64 |
+
5. **心情气泡池**
|
| 65 |
+
- 点击顶部心情图标
|
| 66 |
+
- 左右滑动查看不同日期的心情卡片
|
| 67 |
+
- 点击卡片展开查看当天的气泡池
|
| 68 |
+
- 可以拖拽气泡,感受物理引擎效果
|
| 69 |
+
|
| 70 |
+
## 📊 API 端点
|
| 71 |
+
|
| 72 |
+
- `POST /api/process` - 处理文本/语音输入
|
| 73 |
+
- `POST /api/chat` - 与 AI 对话(RAG)
|
| 74 |
+
- `GET /api/records` - 获取所有记录
|
| 75 |
+
- `GET /api/moods` - 获取情绪数据
|
| 76 |
+
- `GET /api/inspirations` - 获取灵感
|
| 77 |
+
- `GET /api/todos` - 获取待办事项
|
| 78 |
+
- `POST /api/character/generate` - 生成角色形象
|
| 79 |
+
- `GET /health` - 健康检查
|
| 80 |
+
- `GET /docs` - API 文档
|
| 81 |
+
|
| 82 |
+
## 🔗 相关链接
|
| 83 |
+
|
| 84 |
+
- [GitHub 仓库](https://github.com/kernel-14/Nora)
|
| 85 |
+
- [详细文档](https://github.com/kernel-14/Nora/blob/main/README.md)
|
| 86 |
+
- [智谱 AI](https://open.bigmodel.cn/)
|
| 87 |
+
- [MiniMax](https://platform.minimaxi.com/)
|
| 88 |
+
|
| 89 |
+
## 📝 技术栈
|
| 90 |
+
|
| 91 |
+
- **后端**: FastAPI + Python 3.11
|
| 92 |
+
- **前端**: React + TypeScript + Vite
|
| 93 |
+
- **物理引擎**: Matter.js
|
| 94 |
+
- **AI 服务**: 智谱 AI (GLM-4) + MiniMax
|
| 95 |
+
- **部署**: Hugging Face Spaces (Docker)
|
| 96 |
+
|
| 97 |
+
## 📄 License
|
| 98 |
+
|
| 99 |
+
MIT License
|
deployment/README_MODELSCOPE.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🌟 治愈系记录助手 - SoulMate AI Companion
|
| 2 |
+
|
| 3 |
+
一个温暖、治愈的 AI 陪伴应用,帮助你记录心情、捕捉灵感、管理待办。
|
| 4 |
+
|
| 5 |
+
## ✨ 核心特性
|
| 6 |
+
|
| 7 |
+
- 🎤 **语音/文字快速记录** - 自动分类保存
|
| 8 |
+
- 🤖 **AI 语义解析** - 智能提取情绪、灵感和待办
|
| 9 |
+
- 💬 **AI 对话陪伴(RAG)** - 基于历史记录的个性化对话
|
| 10 |
+
- 🖼️ **AI 形象定制** - 生成专属治愈系角色(720 种组合)
|
| 11 |
+
- 🫧 **物理引擎心情池** - 基于 Matter.js 的动态气泡可视化
|
| 12 |
+
|
| 13 |
+
## 🚀 快速开始
|
| 14 |
+
|
| 15 |
+
### 在线使用
|
| 16 |
+
|
| 17 |
+
直接访问本应用即可使用完整功能!
|
| 18 |
+
|
| 19 |
+
### 配置 API 密钥
|
| 20 |
+
|
| 21 |
+
在 ModelScope 的环境变量中配置:
|
| 22 |
+
|
| 23 |
+
**必需:**
|
| 24 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 25 |
+
- 获取地址:https://open.bigmodel.cn/
|
| 26 |
+
- 用途:语音识别、语义解析、AI 对话
|
| 27 |
+
|
| 28 |
+
**可选:**
|
| 29 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 30 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 31 |
+
- 获取地址:https://platform.minimaxi.com/
|
| 32 |
+
- 用途:AI 形象生成
|
| 33 |
+
|
| 34 |
+
## 📖 使用说明
|
| 35 |
+
|
| 36 |
+
1. **首页快速记录**
|
| 37 |
+
- 点击麦克风录音或在输入框输入文字
|
| 38 |
+
- AI 自动分析并分类保存
|
| 39 |
+
|
| 40 |
+
2. **查看分类数据**
|
| 41 |
+
- 点击顶部心情、灵感、待办图标
|
| 42 |
+
- 查看不同类型的记录
|
| 43 |
+
|
| 44 |
+
3. **与 AI 对话**
|
| 45 |
+
- 点击 AI 形象显示问候对话框
|
| 46 |
+
- 点击对话框中的聊天图标进入完整对话
|
| 47 |
+
- AI 基于你的历史记录提供个性化回复
|
| 48 |
+
|
| 49 |
+
4. **定制 AI 形象**
|
| 50 |
+
- 点击右下角 ✨ 按钮
|
| 51 |
+
- 选择颜色、性格、外观、角色
|
| 52 |
+
- 生成专属形象(需要 MiniMax API)
|
| 53 |
+
|
| 54 |
+
5. **心情气泡池**
|
| 55 |
+
- 点击顶部心情图标
|
| 56 |
+
- 左右滑动查看不同日期的心情卡片
|
| 57 |
+
- 点击卡片展开查看当天的气泡池
|
| 58 |
+
- 可以拖拽气泡,感受物理引擎效果
|
| 59 |
+
|
| 60 |
+
## 📊 API 端点
|
| 61 |
+
|
| 62 |
+
- `POST /api/process` - 处理文本/语音输入
|
| 63 |
+
- `POST /api/chat` - 与 AI 对话(RAG)
|
| 64 |
+
- `GET /api/records` - 获取所有记录
|
| 65 |
+
- `GET /api/moods` - 获取情绪数据
|
| 66 |
+
- `GET /api/inspirations` - 获取灵感
|
| 67 |
+
- `GET /api/todos` - 获取待办事项
|
| 68 |
+
- `POST /api/character/generate` - 生成角色形象
|
| 69 |
+
- `GET /health` - 健康检查
|
| 70 |
+
- `GET /docs` - API 文档
|
| 71 |
+
|
| 72 |
+
## 🔗 相关链接
|
| 73 |
+
|
| 74 |
+
- [GitHub 仓库](https://github.com/kernel-14/Nora)
|
| 75 |
+
- [详细文档](https://github.com/kernel-14/Nora/blob/main/README.md)
|
| 76 |
+
- [智谱 AI](https://open.bigmodel.cn/)
|
| 77 |
+
- [MiniMax](https://platform.minimaxi.com/)
|
| 78 |
+
|
| 79 |
+
## 📝 技术栈
|
| 80 |
+
|
| 81 |
+
- **后端**: FastAPI + Python 3.11
|
| 82 |
+
- **前端**: React + TypeScript + Vite
|
| 83 |
+
- **物理引擎**: Matter.js
|
| 84 |
+
- **AI 服务**: 智谱 AI (GLM-4) + MiniMax
|
| 85 |
+
- **部署**: ModelScope (Gradio)
|
| 86 |
+
|
| 87 |
+
## 📄 License
|
| 88 |
+
|
| 89 |
+
MIT License
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 🚀 部署到 ModelScope
|
| 94 |
+
|
| 95 |
+
### 方法一:通过 Git 导入
|
| 96 |
+
|
| 97 |
+
1. 在 ModelScope 创建新的应用空间
|
| 98 |
+
2. 选择 "从 Git 导入"
|
| 99 |
+
3. 输入仓库地址:`https://github.com/kernel-14/Nora.git`
|
| 100 |
+
4. 选择 Gradio SDK
|
| 101 |
+
5. 配置环境变量(见上方配置说明)
|
| 102 |
+
6. 点击创建
|
| 103 |
+
|
| 104 |
+
### 方法二:手动上传
|
| 105 |
+
|
| 106 |
+
1. 克隆本仓库到本地
|
| 107 |
+
2. 在 ModelScope 创建新的应用空间
|
| 108 |
+
3. 上传所有文件
|
| 109 |
+
4. 确保 `configuration.json` 和 `app_modelscope.py` 在根目录
|
| 110 |
+
5. 配置环境变量
|
| 111 |
+
6. 启动应用
|
| 112 |
+
|
| 113 |
+
### 文件说明
|
| 114 |
+
|
| 115 |
+
- `app_modelscope.py` - ModelScope 入口文件
|
| 116 |
+
- `configuration.json` - ModelScope 配置文件
|
| 117 |
+
- `requirements_modelscope.txt` - Python 依赖(使用兼容的 Gradio 版本)
|
| 118 |
+
- `app/` - FastAPI 后端代码
|
| 119 |
+
- `frontend/dist/` - 前端构建产物
|
| 120 |
+
- `data/` - 数据存储目录
|
| 121 |
+
|
| 122 |
+
### 注意事项
|
| 123 |
+
|
| 124 |
+
- 确保 `frontend/dist/` 目录已包含构建好的前端文件
|
| 125 |
+
- 环境变量必须正确配置才能使用 AI 功能
|
| 126 |
+
- ModelScope 使用 Gradio 4.44.1 版本以避免依赖冲突
|
deployment/app_modelscope.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ModelScope 部署入口文件
|
| 3 |
+
使用 Gradio 包装 FastAPI 应用
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import gradio as gr
|
| 10 |
+
|
| 11 |
+
# 添加项目根目录到 Python 路径
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
# 设置环境变量
|
| 15 |
+
os.environ.setdefault("DATA_DIR", "data")
|
| 16 |
+
os.environ.setdefault("LOG_LEVEL", "INFO")
|
| 17 |
+
|
| 18 |
+
# 确保数据目录存在
|
| 19 |
+
data_dir = Path("data")
|
| 20 |
+
data_dir.mkdir(exist_ok=True)
|
| 21 |
+
|
| 22 |
+
generated_images_dir = Path("generated_images")
|
| 23 |
+
generated_images_dir.mkdir(exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# 导入 FastAPI 应用
|
| 26 |
+
from app.main import app as fastapi_app
|
| 27 |
+
from fastapi.staticfiles import StaticFiles
|
| 28 |
+
from fastapi.responses import FileResponse
|
| 29 |
+
|
| 30 |
+
# 挂载前端静态文件
|
| 31 |
+
frontend_dist = Path(__file__).parent / "frontend" / "dist"
|
| 32 |
+
if frontend_dist.exists():
|
| 33 |
+
# 挂载静态资源(CSS, JS)
|
| 34 |
+
assets_dir = frontend_dist / "assets"
|
| 35 |
+
if assets_dir.exists():
|
| 36 |
+
fastapi_app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
|
| 37 |
+
print(f"✅ 前端资源文件已挂载: {assets_dir}")
|
| 38 |
+
|
| 39 |
+
print(f"✅ 前端应用已挂载: {frontend_dist}")
|
| 40 |
+
else:
|
| 41 |
+
print(f"⚠️ 前端构建目录不存在: {frontend_dist}")
|
| 42 |
+
|
| 43 |
+
# 重写根路由以服务前端
|
| 44 |
+
@fastapi_app.get("/", include_in_schema=False)
|
| 45 |
+
async def serve_root():
|
| 46 |
+
"""服务前端应用首页"""
|
| 47 |
+
if frontend_dist.exists():
|
| 48 |
+
index_file = frontend_dist / "index.html"
|
| 49 |
+
if index_file.exists():
|
| 50 |
+
return FileResponse(index_file)
|
| 51 |
+
return {
|
| 52 |
+
"service": "SoulMate AI Companion",
|
| 53 |
+
"status": "running",
|
| 54 |
+
"version": "1.0.0",
|
| 55 |
+
"message": "Welcome! Visit /docs for API documentation."
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# 添加 catch-all 路由用于 SPA
|
| 59 |
+
@fastapi_app.get("/{full_path:path}", include_in_schema=False)
|
| 60 |
+
async def serve_spa(full_path: str):
|
| 61 |
+
"""服务前端应用(SPA 路由支持)"""
|
| 62 |
+
# 如果是 API 路径,跳过
|
| 63 |
+
if full_path.startswith("api/") or full_path == "docs" or full_path == "openapi.json" or full_path == "health":
|
| 64 |
+
from fastapi import HTTPException
|
| 65 |
+
raise HTTPException(status_code=404, detail="Not found")
|
| 66 |
+
|
| 67 |
+
# 返回前端 index.html
|
| 68 |
+
if frontend_dist.exists():
|
| 69 |
+
index_file = frontend_dist / "index.html"
|
| 70 |
+
if index_file.exists():
|
| 71 |
+
return FileResponse(index_file)
|
| 72 |
+
|
| 73 |
+
return {"error": "Frontend not found"}
|
| 74 |
+
|
| 75 |
+
# 创建 Gradio 界面(用于 ModelScope 的展示)
|
| 76 |
+
with gr.Blocks(
|
| 77 |
+
title="治愈系记录助手 - SoulMate AI Companion",
|
| 78 |
+
theme=gr.themes.Soft(
|
| 79 |
+
primary_hue="purple",
|
| 80 |
+
secondary_hue="pink",
|
| 81 |
+
),
|
| 82 |
+
) as demo:
|
| 83 |
+
|
| 84 |
+
gr.Markdown("""
|
| 85 |
+
# 🌟 治愈系记录助手 - SoulMate AI Companion
|
| 86 |
+
|
| 87 |
+
一个温暖、治愈的 AI 陪伴应用,帮助你记录心情、捕捉灵感、管理待办。
|
| 88 |
+
|
| 89 |
+
### ✨ 核心特性
|
| 90 |
+
- 🎤 **语音/文字快速记录** - 自动分类保存
|
| 91 |
+
- 🤖 **AI 语义解析** - 智能提取情绪、灵感和待办
|
| 92 |
+
- 💬 **AI 对话陪伴(RAG)** - 基于历史记录的个性化对话
|
| 93 |
+
- 🖼️ **AI 形象定制** - 生成专属治愈系角色(720 种组合)
|
| 94 |
+
- 🫧 **物理引擎心情池** - 基于 Matter.js 的动态气泡可视化
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
### 🚀 开始使用
|
| 99 |
+
|
| 100 |
+
**🎯 前端应用地址:** 点击上方的 "App" 标签页访问完整应用
|
| 101 |
+
|
| 102 |
+
**📚 API 文档:** [FastAPI Swagger Docs →](/docs)
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
### 📖 使用说明
|
| 107 |
+
|
| 108 |
+
1. **首页快速记录**
|
| 109 |
+
- 点击麦克风录音或在输入框输入文字
|
| 110 |
+
- AI 自动分析并分类保存
|
| 111 |
+
|
| 112 |
+
2. **查看分类数据**
|
| 113 |
+
- 点击顶部心情、灵感、待办图标
|
| 114 |
+
- 查看不同类型的记录
|
| 115 |
+
|
| 116 |
+
3. **与 AI 对话**
|
| 117 |
+
- 点击 AI 形象显示问候对话框
|
| 118 |
+
- 点击对话框中的聊天图标进入完整对话
|
| 119 |
+
- AI 基于你的历史记录提供个性化回复
|
| 120 |
+
|
| 121 |
+
4. **定制 AI 形象**
|
| 122 |
+
- 点击右下角 ✨ 按钮
|
| 123 |
+
- 选择颜色、性格、外观、角色
|
| 124 |
+
- 生成专属形象(需要 MiniMax API)
|
| 125 |
+
|
| 126 |
+
5. **心情气泡池**
|
| 127 |
+
- 点击顶部心情图标
|
| 128 |
+
- 左右滑动查看不同日期的心情卡片
|
| 129 |
+
- 点击卡片展开查看当天的气泡池
|
| 130 |
+
- 可以拖拽气泡,感受物理引擎效果
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
### ⚙️ 配置说明
|
| 135 |
+
|
| 136 |
+
需要在 ModelScope 的环境变量中配置:
|
| 137 |
+
|
| 138 |
+
**必需:**
|
| 139 |
+
- `ZHIPU_API_KEY` - 智谱 AI API 密钥
|
| 140 |
+
- 获取地址:https://open.bigmodel.cn/
|
| 141 |
+
- 用途:语音识别、语义解析、AI 对话
|
| 142 |
+
|
| 143 |
+
**可选:**
|
| 144 |
+
- `MINIMAX_API_KEY` - MiniMax API 密钥
|
| 145 |
+
- `MINIMAX_GROUP_ID` - MiniMax Group ID
|
| 146 |
+
- 获取地址:https://platform.minimaxi.com/
|
| 147 |
+
- 用途:AI 形象生成
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
### 🔗 相关链接
|
| 152 |
+
- [GitHub 仓库](https://github.com/kernel-14/Nora)
|
| 153 |
+
- [详细文档](https://github.com/kernel-14/Nora/blob/main/README.md)
|
| 154 |
+
- [智谱 AI](https://open.bigmodel.cn/)
|
| 155 |
+
- [MiniMax](https://platform.minimaxi.com/)
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
### 📊 API 端点
|
| 160 |
+
|
| 161 |
+
- `POST /api/process` - 处理文本/语音输入
|
| 162 |
+
- `POST /api/chat` - 与 AI 对话(RAG)
|
| 163 |
+
- `GET /api/records` - 获取所有记录
|
| 164 |
+
- `GET /api/moods` - 获取情绪数据
|
| 165 |
+
- `GET /api/inspirations` - 获取灵感
|
| 166 |
+
- `GET /api/todos` - 获取待办事项
|
| 167 |
+
- `POST /api/character/generate` - 生成角色形象
|
| 168 |
+
- `GET /health` - 健康检查
|
| 169 |
+
- `GET /docs` - API 文档
|
| 170 |
+
""")
|
| 171 |
+
|
| 172 |
+
# 挂载 FastAPI 到 Gradio
|
| 173 |
+
app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")
|
| 174 |
+
|
| 175 |
+
# 如果直接运行此文件
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
import uvicorn
|
| 178 |
+
print("=" * 50)
|
| 179 |
+
print("🌟 治愈系记录助手 - SoulMate AI Companion")
|
| 180 |
+
print("=" * 50)
|
| 181 |
+
print(f"📍 前端应用: http://0.0.0.0:7860/")
|
| 182 |
+
print(f"📚 Gradio 界面: http://0.0.0.0:7860/gradio")
|
| 183 |
+
print(f"📖 API 文档: http://0.0.0.0:7860/docs")
|
| 184 |
+
print(f"🔍 健康检查: http://0.0.0.0:7860/health")
|
| 185 |
+
print("=" * 50)
|
| 186 |
+
|
| 187 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
deployment/configuration.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "Gradio",
|
| 3 |
+
"task": "chat",
|
| 4 |
+
"allow_remote_code": true
|
| 5 |
+
}
|
deployment/deploy_to_hf.bat
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
chcp 65001 >nul
|
| 3 |
+
echo 🚀 开始部署到 Hugging Face Spaces...
|
| 4 |
+
echo.
|
| 5 |
+
|
| 6 |
+
REM 检查是否已登录
|
| 7 |
+
huggingface-cli whoami >nul 2>&1
|
| 8 |
+
if errorlevel 1 (
|
| 9 |
+
echo ❌ 请先登录 Hugging Face CLI
|
| 10 |
+
echo 运行: huggingface-cli login
|
| 11 |
+
pause
|
| 12 |
+
exit /b 1
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
REM 获取用户名
|
| 16 |
+
for /f "tokens=2" %%i in ('huggingface-cli whoami ^| findstr "username:"') do set USERNAME=%%i
|
| 17 |
+
echo ✅ 已登录为: %USERNAME%
|
| 18 |
+
echo.
|
| 19 |
+
|
| 20 |
+
REM 询问 Space 名称
|
| 21 |
+
set /p SPACE_NAME="请输入 Space 名称 (默认: soulmate-ai-companion): "
|
| 22 |
+
if "%SPACE_NAME%"=="" set SPACE_NAME=soulmate-ai-companion
|
| 23 |
+
|
| 24 |
+
echo.
|
| 25 |
+
echo 📦 准备文件...
|
| 26 |
+
|
| 27 |
+
REM 构建前端
|
| 28 |
+
echo 🔨 构建前端...
|
| 29 |
+
cd frontend
|
| 30 |
+
call npm install
|
| 31 |
+
call npm run build
|
| 32 |
+
cd ..
|
| 33 |
+
|
| 34 |
+
if not exist "frontend\dist" (
|
| 35 |
+
echo ❌ 前端构建失败
|
| 36 |
+
pause
|
| 37 |
+
exit /b 1
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
echo ✅ 前端构建完成
|
| 41 |
+
echo.
|
| 42 |
+
|
| 43 |
+
REM 创建临时目录
|
| 44 |
+
set TEMP_DIR=temp_hf_deploy
|
| 45 |
+
if exist %TEMP_DIR% rmdir /s /q %TEMP_DIR%
|
| 46 |
+
mkdir %TEMP_DIR%
|
| 47 |
+
|
| 48 |
+
REM 复制文件
|
| 49 |
+
echo 📋 复制文件...
|
| 50 |
+
copy app.py %TEMP_DIR%\
|
| 51 |
+
copy requirements_hf.txt %TEMP_DIR%\requirements.txt
|
| 52 |
+
copy README_HF.md %TEMP_DIR%\README.md
|
| 53 |
+
copy .gitattributes %TEMP_DIR%\
|
| 54 |
+
xcopy /E /I /Y app %TEMP_DIR%\app
|
| 55 |
+
xcopy /E /I /Y frontend\dist %TEMP_DIR%\frontend
|
| 56 |
+
mkdir %TEMP_DIR%\data
|
| 57 |
+
mkdir %TEMP_DIR%\generated_images
|
| 58 |
+
|
| 59 |
+
REM 创建或克隆 Space
|
| 60 |
+
echo 🌐 准备 Space...
|
| 61 |
+
set SPACE_URL=https://huggingface.co/spaces/%USERNAME%/%SPACE_NAME%
|
| 62 |
+
|
| 63 |
+
huggingface-cli repo info spaces/%USERNAME%/%SPACE_NAME% >nul 2>&1
|
| 64 |
+
if errorlevel 1 (
|
| 65 |
+
echo 🆕 创建新 Space...
|
| 66 |
+
huggingface-cli repo create %SPACE_NAME% --type space --space_sdk gradio
|
| 67 |
+
) else (
|
| 68 |
+
echo ✅ Space 已存在
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
cd %TEMP_DIR%
|
| 72 |
+
git clone %SPACE_URL% .
|
| 73 |
+
|
| 74 |
+
REM 复制文件到仓库
|
| 75 |
+
echo 📤 准备上传...
|
| 76 |
+
copy ..\app.py .
|
| 77 |
+
copy ..\requirements_hf.txt requirements.txt
|
| 78 |
+
copy ..\README_HF.md README.md
|
| 79 |
+
copy ..\.gitattributes .
|
| 80 |
+
xcopy /E /I /Y ..\app app
|
| 81 |
+
xcopy /E /I /Y ..\frontend\dist frontend
|
| 82 |
+
if not exist data mkdir data
|
| 83 |
+
if not exist generated_images mkdir generated_images
|
| 84 |
+
|
| 85 |
+
REM 提交并推送
|
| 86 |
+
echo 🚀 上传到 Hugging Face...
|
| 87 |
+
git add .
|
| 88 |
+
git commit -m "Deploy to Hugging Face Spaces"
|
| 89 |
+
git push
|
| 90 |
+
|
| 91 |
+
cd ..
|
| 92 |
+
rmdir /s /q %TEMP_DIR%
|
| 93 |
+
|
| 94 |
+
echo.
|
| 95 |
+
echo ✅ 部署完成!
|
| 96 |
+
echo.
|
| 97 |
+
echo 📍 Space URL: %SPACE_URL%
|
| 98 |
+
echo.
|
| 99 |
+
echo ⚙️ 下一步:
|
| 100 |
+
echo 1. 访问 %SPACE_URL%
|
| 101 |
+
echo 2. 点击 Settings → Repository secrets
|
| 102 |
+
echo 3. 添加环境变量:
|
| 103 |
+
echo - ZHIPU_API_KEY (必需)
|
| 104 |
+
echo - MINIMAX_API_KEY (可选)
|
| 105 |
+
echo - MINIMAX_GROUP_ID (可选)
|
| 106 |
+
echo.
|
| 107 |
+
echo 🎉 完成后即可使用!
|
| 108 |
+
echo.
|
| 109 |
+
pause
|
deployment/deploy_to_hf.sh
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Hugging Face Spaces 快速部署脚本
|
| 4 |
+
|
| 5 |
+
echo "🚀 开始部署到 Hugging Face Spaces..."
|
| 6 |
+
|
| 7 |
+
# 检查是否已登录
|
| 8 |
+
if ! huggingface-cli whoami &> /dev/null; then
|
| 9 |
+
echo "❌ 请先登录 Hugging Face CLI"
|
| 10 |
+
echo "运行: huggingface-cli login"
|
| 11 |
+
exit 1
|
| 12 |
+
fi
|
| 13 |
+
|
| 14 |
+
# 获取用户名
|
| 15 |
+
USERNAME=$(huggingface-cli whoami | grep "username:" | awk '{print $2}')
|
| 16 |
+
echo "✅ 已登录为: $USERNAME"
|
| 17 |
+
|
| 18 |
+
# 询问 Space 名称
|
| 19 |
+
read -p "请输入 Space 名称 (默认: soulmate-ai-companion): " SPACE_NAME
|
| 20 |
+
SPACE_NAME=${SPACE_NAME:-soulmate-ai-companion}
|
| 21 |
+
|
| 22 |
+
echo "📦 准备文件..."
|
| 23 |
+
|
| 24 |
+
# 构建前端
|
| 25 |
+
echo "🔨 构建前端..."
|
| 26 |
+
cd frontend
|
| 27 |
+
npm install
|
| 28 |
+
npm run build
|
| 29 |
+
cd ..
|
| 30 |
+
|
| 31 |
+
if [ ! -d "frontend/dist" ]; then
|
| 32 |
+
echo "❌ 前端构建失败"
|
| 33 |
+
exit 1
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
echo "✅ 前端构建完成"
|
| 37 |
+
|
| 38 |
+
# 创建临时目录
|
| 39 |
+
TEMP_DIR="temp_hf_deploy"
|
| 40 |
+
rm -rf $TEMP_DIR
|
| 41 |
+
mkdir -p $TEMP_DIR
|
| 42 |
+
|
| 43 |
+
# 复制文件
|
| 44 |
+
echo "📋 复制文件..."
|
| 45 |
+
cp app.py $TEMP_DIR/
|
| 46 |
+
cp requirements_hf.txt $TEMP_DIR/requirements.txt
|
| 47 |
+
cp README_HF.md $TEMP_DIR/README.md
|
| 48 |
+
cp .gitattributes $TEMP_DIR/
|
| 49 |
+
cp -r app $TEMP_DIR/
|
| 50 |
+
cp -r frontend/dist $TEMP_DIR/frontend/
|
| 51 |
+
mkdir -p $TEMP_DIR/data
|
| 52 |
+
mkdir -p $TEMP_DIR/generated_images
|
| 53 |
+
|
| 54 |
+
# 创建或克隆 Space
|
| 55 |
+
echo "🌐 准备 Space..."
|
| 56 |
+
SPACE_URL="https://huggingface.co/spaces/$USERNAME/$SPACE_NAME"
|
| 57 |
+
|
| 58 |
+
if huggingface-cli repo info "spaces/$USERNAME/$SPACE_NAME" &> /dev/null; then
|
| 59 |
+
echo "✅ Space 已存在,克隆中..."
|
| 60 |
+
cd $TEMP_DIR
|
| 61 |
+
git clone $SPACE_URL .
|
| 62 |
+
else
|
| 63 |
+
echo "🆕 创建新 Space..."
|
| 64 |
+
huggingface-cli repo create $SPACE_NAME --type space --space_sdk gradio
|
| 65 |
+
cd $TEMP_DIR
|
| 66 |
+
git clone $SPACE_URL .
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
# 复制文件到仓库
|
| 70 |
+
echo "📤 准备上传..."
|
| 71 |
+
cp ../app.py .
|
| 72 |
+
cp ../requirements_hf.txt ./requirements.txt
|
| 73 |
+
cp ../README_HF.md ./README.md
|
| 74 |
+
cp ../.gitattributes .
|
| 75 |
+
cp -r ../app .
|
| 76 |
+
cp -r ../frontend/dist ./frontend/
|
| 77 |
+
mkdir -p data generated_images
|
| 78 |
+
|
| 79 |
+
# 提交并推送
|
| 80 |
+
echo "🚀 上传到 Hugging Face..."
|
| 81 |
+
git add .
|
| 82 |
+
git commit -m "Deploy to Hugging Face Spaces"
|
| 83 |
+
git push
|
| 84 |
+
|
| 85 |
+
cd ..
|
| 86 |
+
rm -rf $TEMP_DIR
|
| 87 |
+
|
| 88 |
+
echo ""
|
| 89 |
+
echo "✅ 部署完成!"
|
| 90 |
+
echo ""
|
| 91 |
+
echo "📍 Space URL: $SPACE_URL"
|
| 92 |
+
echo ""
|
| 93 |
+
echo "⚙️ 下一步:"
|
| 94 |
+
echo "1. 访问 $SPACE_URL"
|
| 95 |
+
echo "2. 点击 Settings → Repository secrets"
|
| 96 |
+
echo "3. 添加环境变量:"
|
| 97 |
+
echo " - ZHIPU_API_KEY (必需)"
|
| 98 |
+
echo " - MINIMAX_API_KEY (可选)"
|
| 99 |
+
echo " - MINIMAX_GROUP_ID (可选)"
|
| 100 |
+
echo ""
|
| 101 |
+
echo "🎉 完成后即可使用!"
|
deployment/ms_deploy.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://modelscope.cn/api/v1/studios/deploy_schema.json",
|
| 3 |
+
"sdk_type": "gradio",
|
| 4 |
+
"sdk_version": "4.44.1",
|
| 5 |
+
"resource_configuration": "platform/2v-cpu-16g-mem",
|
| 6 |
+
"base_image": "ubuntu22.04-py311-torch2.3.1-modelscope1.31.0",
|
| 7 |
+
"environment_variables": [
|
| 8 |
+
{
|
| 9 |
+
"name": "ZHIPU_API_KEY",
|
| 10 |
+
"value": ""
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"name": "MINIMAX_API_KEY",
|
| 14 |
+
"value": ""
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"name": "MINIMAX_GROUP_ID",
|
| 18 |
+
"value": ""
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"name": "DATA_DIR",
|
| 22 |
+
"value": "data"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"name": "LOG_LEVEL",
|
| 26 |
+
"value": "INFO"
|
| 27 |
+
}
|
| 28 |
+
]
|
| 29 |
+
}
|
deployment/requirements_hf.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Requirements
|
| 2 |
+
# Using latest stable versions
|
| 3 |
+
|
| 4 |
+
# Core Gradio - use latest version which is compatible with new huggingface-hub
|
| 5 |
+
gradio==5.9.1
|
| 6 |
+
|
| 7 |
+
# Core dependencies (compatible with Python 3.11+)
|
| 8 |
+
fastapi==0.115.0
|
| 9 |
+
uvicorn[standard]==0.32.0
|
| 10 |
+
pydantic==2.10.0
|
| 11 |
+
pydantic-settings==2.6.0
|
| 12 |
+
httpx==0.27.0
|
| 13 |
+
python-multipart==0.0.12
|
| 14 |
+
python-dotenv==1.0.1
|
| 15 |
+
|
| 16 |
+
# Additional dependencies
|
| 17 |
+
aiofiles==24.1.0
|
deployment/requirements_modelscope.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ModelScope 部署依赖
|
| 2 |
+
# 使用兼容的 Gradio 版本
|
| 3 |
+
|
| 4 |
+
# Gradio - 使用稳定版本
|
| 5 |
+
gradio==4.44.1
|
| 6 |
+
|
| 7 |
+
# Core dependencies (compatible with Python 3.11+)
|
| 8 |
+
fastapi==0.115.0
|
| 9 |
+
uvicorn[standard]==0.32.0
|
| 10 |
+
pydantic==2.10.0
|
| 11 |
+
pydantic-settings==2.6.0
|
| 12 |
+
httpx==0.27.0
|
| 13 |
+
python-multipart==0.0.12
|
| 14 |
+
python-dotenv==1.0.1
|
| 15 |
+
|
| 16 |
+
# Additional dependencies
|
| 17 |
+
aiofiles==24.1.0
|
docs/API_配置说明.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API 配置说明
|
| 2 |
+
|
| 3 |
+
## 自动检测 API 地址
|
| 4 |
+
|
| 5 |
+
前端应用会自动检测运行环境并配置正确的 API 地址。
|
| 6 |
+
|
| 7 |
+
### 支持的环境
|
| 8 |
+
|
| 9 |
+
#### 1. 生产环境(自动检测)
|
| 10 |
+
|
| 11 |
+
**Hugging Face Spaces:**
|
| 12 |
+
- 域名包含:`hf.space`, `huggingface.co`, `gradio.live`
|
| 13 |
+
- API 地址:使用相同的协议和域名
|
| 14 |
+
- 示例:`https://huggingface.co/spaces/kernel14/Nora`
|
| 15 |
+
- 前端:`https://huggingface.co/spaces/kernel14/Nora`
|
| 16 |
+
- API:`https://huggingface.co/spaces/kernel14/Nora/api/...`
|
| 17 |
+
|
| 18 |
+
**ModelScope:**
|
| 19 |
+
- 域名包含:`modelscope.cn`
|
| 20 |
+
- API 地址:使用相同的协议和域名
|
| 21 |
+
- 示例:`https://modelscope.cn/studios/xxx/yyy`
|
| 22 |
+
- 前端:`https://modelscope.cn/studios/xxx/yyy`
|
| 23 |
+
- API:`https://modelscope.cn/studios/xxx/yyy/api/...`
|
| 24 |
+
|
| 25 |
+
#### 2. 局域网访问
|
| 26 |
+
|
| 27 |
+
**通过 IP 地址访问:**
|
| 28 |
+
- 前端:`http://192.168.1.100:5173`
|
| 29 |
+
- API:`http://192.168.1.100:8000`
|
| 30 |
+
|
| 31 |
+
**通过主机名访问:**
|
| 32 |
+
- 前端:`http://mycomputer.local:5173`
|
| 33 |
+
- API:`http://mycomputer.local:8000`
|
| 34 |
+
|
| 35 |
+
#### 3. 本地开发
|
| 36 |
+
|
| 37 |
+
**默认配置:**
|
| 38 |
+
- 前端:`http://localhost:5173`
|
| 39 |
+
- API:`http://localhost:8000`
|
| 40 |
+
|
| 41 |
+
### 环境变量配置(可选)
|
| 42 |
+
|
| 43 |
+
如果需要手动指定 API 地址,可以在前端项目中创建 `.env.local` 文件:
|
| 44 |
+
|
| 45 |
+
```env
|
| 46 |
+
VITE_API_URL=https://your-custom-api-url.com
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 检测逻辑
|
| 50 |
+
|
| 51 |
+
```typescript
|
| 52 |
+
const getApiBaseUrl = () => {
|
| 53 |
+
// 1. 优先使用环境变量
|
| 54 |
+
if (import.meta.env.VITE_API_URL) {
|
| 55 |
+
return import.meta.env.VITE_API_URL;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// 2. 检测生产环境(Hugging Face, ModelScope)
|
| 59 |
+
if (hostname.includes('hf.space') ||
|
| 60 |
+
hostname.includes('huggingface.co') ||
|
| 61 |
+
hostname.includes('modelscope.cn')) {
|
| 62 |
+
return `${protocol}//${hostname}`;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// 3. 检测局域网访问
|
| 66 |
+
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
| 67 |
+
return `${protocol}//${hostname}:8000`;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// 4. 默认本地开发
|
| 71 |
+
return 'http://localhost:8000';
|
| 72 |
+
};
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### 调试
|
| 76 |
+
|
| 77 |
+
打开浏览器控制台,查看 API 地址:
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
🔗 API Base URL: https://huggingface.co/spaces/kernel14/Nora
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### 常见问题
|
| 84 |
+
|
| 85 |
+
**Q: 为什么其他设备无法访问?**
|
| 86 |
+
|
| 87 |
+
A: 确保:
|
| 88 |
+
1. 后端服务器绑定到 `0.0.0.0` 而不是 `127.0.0.1`
|
| 89 |
+
2. 防火墙允许端口 8000
|
| 90 |
+
3. 使用正确的 IP 地址访问
|
| 91 |
+
|
| 92 |
+
**Q: Hugging Face 上 API 调用失败?**
|
| 93 |
+
|
| 94 |
+
A: 检查:
|
| 95 |
+
1. 浏览器控制台的 API 地址是否正确
|
| 96 |
+
2. 是否配置了必需的环境变量(`ZHIPU_API_KEY`)
|
| 97 |
+
3. 查看 Space 的日志是否有错误
|
| 98 |
+
|
| 99 |
+
**Q: 如何测试 API 连接?**
|
| 100 |
+
|
| 101 |
+
A: 访问以下地址:
|
| 102 |
+
- 健康检查:`/health`
|
| 103 |
+
- API 文档:`/docs`
|
| 104 |
+
- 测试页面:`/test_api.html`
|
| 105 |
+
|
| 106 |
+
### 部署检查清单
|
| 107 |
+
|
| 108 |
+
- [ ] 前端已重新构建(`npm run build`)
|
| 109 |
+
- [ ] `frontend/dist/` 已提交到 Git
|
| 110 |
+
- [ ] 环境变量已配置(Hugging Face Secrets / ModelScope 环境变量)
|
| 111 |
+
- [ ] Space 已重启
|
| 112 |
+
- [ ] 浏览器控制台显示正确的 API 地址
|
| 113 |
+
- [ ] 测试 API 调用是否成功
|
docs/FEATURE_SUMMARY.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Home Interaction Feature - Implementation Summary
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This document summarizes the implementation of the home page interaction feature for the SoulMate AI Companion application. The feature includes two complementary functionalities:
|
| 6 |
+
|
| 7 |
+
1. **Quick Recording** - Fast capture of thoughts, inspirations, and todos
|
| 8 |
+
2. **AI Chat (RAG-Enhanced)** - Intelligent conversation with context awareness
|
| 9 |
+
|
| 10 |
+
## Key Features
|
| 11 |
+
|
| 12 |
+
### 1. Home Page Quick Recording
|
| 13 |
+
|
| 14 |
+
**Purpose:** Enable users to quickly record their thoughts through voice or text input.
|
| 15 |
+
|
| 16 |
+
**Workflow:**
|
| 17 |
+
```
|
| 18 |
+
User Input (Voice/Text)
|
| 19 |
+
↓
|
| 20 |
+
Call /api/process
|
| 21 |
+
↓
|
| 22 |
+
AI Semantic Analysis
|
| 23 |
+
↓
|
| 24 |
+
Save to records.json
|
| 25 |
+
↓
|
| 26 |
+
Auto-split to:
|
| 27 |
+
- moods.json (emotions)
|
| 28 |
+
- inspirations.json (ideas)
|
| 29 |
+
- todos.json (tasks)
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
**Characteristics:**
|
| 33 |
+
- ✅ One-time processing
|
| 34 |
+
- ✅ Automatic categorization
|
| 35 |
+
- ✅ Structured data output
|
| 36 |
+
- ✅ No conversation context needed
|
| 37 |
+
|
| 38 |
+
### 2. AI Chat with RAG Enhancement
|
| 39 |
+
|
| 40 |
+
**Purpose:** Provide intelligent, warm companionship through context-aware conversations.
|
| 41 |
+
|
| 42 |
+
**Workflow:**
|
| 43 |
+
```
|
| 44 |
+
User Message
|
| 45 |
+
↓
|
| 46 |
+
Call /api/chat
|
| 47 |
+
↓
|
| 48 |
+
Load Recent Records (last 10)
|
| 49 |
+
↓
|
| 50 |
+
Build RAG Context
|
| 51 |
+
↓
|
| 52 |
+
AI Generates Personalized Response
|
| 53 |
+
↓
|
| 54 |
+
Return to User
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**Characteristics:**
|
| 58 |
+
- ✅ Each message calls API
|
| 59 |
+
- ✅ Uses RAG (Retrieval-Augmented Generation)
|
| 60 |
+
- ✅ Context from records.json
|
| 61 |
+
- ✅ Personalized, warm responses
|
| 62 |
+
- ✅ Conversation not saved
|
| 63 |
+
|
| 64 |
+
## Technical Implementation
|
| 65 |
+
|
| 66 |
+
### Backend Changes
|
| 67 |
+
|
| 68 |
+
#### File: `app/main.py`
|
| 69 |
+
|
| 70 |
+
**Updated `/api/chat` endpoint with RAG:**
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
@app.post("/api/chat")
|
| 74 |
+
async def chat_with_ai(text: str = Form(...)):
|
| 75 |
+
# Load user's records as RAG knowledge base
|
| 76 |
+
records = storage_service._read_json_file(storage_service.records_file)
|
| 77 |
+
recent_records = records[-10:] # Last 10 records
|
| 78 |
+
|
| 79 |
+
# Build context from records
|
| 80 |
+
context_parts = []
|
| 81 |
+
for record in recent_records:
|
| 82 |
+
context_entry = f"[{timestamp}] User said: {original_text}"
|
| 83 |
+
if mood:
|
| 84 |
+
context_entry += f"\nMood: {mood['type']}"
|
| 85 |
+
if inspirations:
|
| 86 |
+
context_entry += f"\nInspirations: {ideas}"
|
| 87 |
+
if todos:
|
| 88 |
+
context_entry += f"\nTodos: {tasks}"
|
| 89 |
+
context_parts.append(context_entry)
|
| 90 |
+
|
| 91 |
+
# Build system prompt with context
|
| 92 |
+
system_prompt = f"""You are a warm, empathetic AI companion.
|
| 93 |
+
You can reference the user's history to provide more caring responses:
|
| 94 |
+
|
| 95 |
+
{context_text}
|
| 96 |
+
|
| 97 |
+
Please respond with warmth and understanding based on this background."""
|
| 98 |
+
|
| 99 |
+
# Call AI API with context
|
| 100 |
+
response = await client.post(
|
| 101 |
+
"https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
| 102 |
+
json={
|
| 103 |
+
"model": "glm-4-flash",
|
| 104 |
+
"messages": [
|
| 105 |
+
{"role": "system", "content": system_prompt},
|
| 106 |
+
{"role": "user", "content": text}
|
| 107 |
+
]
|
| 108 |
+
}
|
| 109 |
+
)
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### Frontend Changes
|
| 113 |
+
|
| 114 |
+
#### New Component: `frontend/components/HomeInput.tsx`
|
| 115 |
+
|
| 116 |
+
**Features:**
|
| 117 |
+
- Large circular microphone button with gradient
|
| 118 |
+
- Text input field
|
| 119 |
+
- Real-time processing status
|
| 120 |
+
- Success/error animations
|
| 121 |
+
- Auto-refresh data on completion
|
| 122 |
+
|
| 123 |
+
**Key Functions:**
|
| 124 |
+
|
| 125 |
+
```typescript
|
| 126 |
+
// Voice recording
|
| 127 |
+
const startRecording = async () => {
|
| 128 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 129 |
+
const mediaRecorder = new MediaRecorder(stream);
|
| 130 |
+
// Recording logic...
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
// Process audio
|
| 134 |
+
const processAudio = async (audioBlob: Blob) => {
|
| 135 |
+
const file = new File([audioBlob], 'recording.webm');
|
| 136 |
+
await apiService.processInput(file);
|
| 137 |
+
setShowSuccess(true);
|
| 138 |
+
onRecordComplete();
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
// Process text
|
| 142 |
+
const processText = async () => {
|
| 143 |
+
await apiService.processInput(undefined, textInput);
|
| 144 |
+
setTextInput('');
|
| 145 |
+
setShowSuccess(true);
|
| 146 |
+
onRecordComplete();
|
| 147 |
+
};
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
#### Updated: `frontend/App.tsx`
|
| 151 |
+
|
| 152 |
+
Integrated HomeInput component into the home page:
|
| 153 |
+
|
| 154 |
+
```typescript
|
| 155 |
+
<div className="flex-1 flex flex-col items-center justify-center">
|
| 156 |
+
<AIEntity imageUrl={characterImageUrl} />
|
| 157 |
+
|
| 158 |
+
{/* Home Input Component */}
|
| 159 |
+
<div className="mt-8 w-full">
|
| 160 |
+
<HomeInput onRecordComplete={loadAllData} />
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## Feature Comparison
|
| 166 |
+
|
| 167 |
+
| Feature | Quick Recording | AI Chat |
|
| 168 |
+
|---------|----------------|---------|
|
| 169 |
+
| **Purpose** | Record thoughts | Intelligent companionship |
|
| 170 |
+
| **API Endpoint** | `/api/process` | `/api/chat` |
|
| 171 |
+
| **Call Frequency** | One-time | Per message |
|
| 172 |
+
| **Knowledge Base** | Not used | Uses RAG |
|
| 173 |
+
| **Output** | Structured data | Natural language |
|
| 174 |
+
| **Storage** | Auto-save to files | Not saved |
|
| 175 |
+
| **Context** | No context needed | Based on history |
|
| 176 |
+
|
| 177 |
+
## Files Modified/Created
|
| 178 |
+
|
| 179 |
+
### New Files
|
| 180 |
+
|
| 181 |
+
1. **frontend/components/HomeInput.tsx** - Home input component
|
| 182 |
+
2. **test_home_input.py** - Feature test script
|
| 183 |
+
3. **首页交互功能说明.md** - Detailed documentation (Chinese)
|
| 184 |
+
4. **新功能实现总结.md** - Implementation summary (Chinese)
|
| 185 |
+
5. **快速开始-新功能.md** - Quick start guide (Chinese)
|
| 186 |
+
6. **功能架构图.md** - Architecture diagrams (Chinese)
|
| 187 |
+
7. **FEATURE_SUMMARY.md** - This file
|
| 188 |
+
|
| 189 |
+
### Modified Files
|
| 190 |
+
|
| 191 |
+
1. **app/main.py** - Updated `/api/chat` with RAG
|
| 192 |
+
2. **frontend/App.tsx** - Integrated HomeInput component
|
| 193 |
+
3. **README.md** - Updated documentation
|
| 194 |
+
|
| 195 |
+
## Usage Examples
|
| 196 |
+
|
| 197 |
+
### Example 1: Quick Recording
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
User Input:
|
| 201 |
+
"Today I'm feeling great. Had a new idea for an app. Need to buy books tomorrow."
|
| 202 |
+
|
| 203 |
+
System Processing:
|
| 204 |
+
✓ Call /api/process
|
| 205 |
+
✓ Semantic analysis
|
| 206 |
+
✓ Save to records.json
|
| 207 |
+
✓ Split to:
|
| 208 |
+
- moods.json: feeling great
|
| 209 |
+
- inspirations.json: new app idea
|
| 210 |
+
- todos.json: buy books tomorrow
|
| 211 |
+
✓ Show "Record Successful"
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
### Example 2: AI Chat with RAG
|
| 215 |
+
|
| 216 |
+
```
|
| 217 |
+
User: "What have I been doing lately?"
|
| 218 |
+
|
| 219 |
+
AI (based on history):
|
| 220 |
+
"From your records, you've been working on a project. Although work
|
| 221 |
+
has been tiring, you felt accomplished after completing it. You also
|
| 222 |
+
plan to wake up early tomorrow for a run. Great plans!"
|
| 223 |
+
|
| 224 |
+
User: "How's my mood been?"
|
| 225 |
+
|
| 226 |
+
AI:
|
| 227 |
+
"Your mood has had ups and downs. You felt tired during work, but
|
| 228 |
+
happy after completing tasks. Overall, you're a positive person who
|
| 229 |
+
finds joy in achievements even when tired. Keep it up!"
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
## Testing
|
| 233 |
+
|
| 234 |
+
### Run Test Script
|
| 235 |
+
|
| 236 |
+
```bash
|
| 237 |
+
# Ensure backend is running
|
| 238 |
+
python -m uvicorn app.main:app --reload
|
| 239 |
+
|
| 240 |
+
# Run tests in another terminal
|
| 241 |
+
python test_home_input.py
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
### Test Coverage
|
| 245 |
+
|
| 246 |
+
1. ✅ Home text input recording
|
| 247 |
+
2. ✅ AI chat without history
|
| 248 |
+
3. ✅ AI chat with RAG enhancement
|
| 249 |
+
4. ✅ Retrieve records
|
| 250 |
+
|
| 251 |
+
## Performance Considerations
|
| 252 |
+
|
| 253 |
+
### Frontend Optimizations
|
| 254 |
+
|
| 255 |
+
- Debounce input handling
|
| 256 |
+
- Optimistic updates
|
| 257 |
+
- Component lazy loading
|
| 258 |
+
- Result caching
|
| 259 |
+
|
| 260 |
+
### Backend Optimizations
|
| 261 |
+
|
| 262 |
+
- Async processing (async/await)
|
| 263 |
+
- Connection pool reuse
|
| 264 |
+
- Limit history records (10 items)
|
| 265 |
+
- Response compression
|
| 266 |
+
|
| 267 |
+
### RAG Optimizations
|
| 268 |
+
|
| 269 |
+
- Load only recent records
|
| 270 |
+
- Streamline context information
|
| 271 |
+
- Cache common queries
|
| 272 |
+
- Vector database (future enhancement)
|
| 273 |
+
|
| 274 |
+
## Security & Privacy
|
| 275 |
+
|
| 276 |
+
### API Key Protection
|
| 277 |
+
|
| 278 |
+
- Stored in `.env` file
|
| 279 |
+
- Not committed to version control
|
| 280 |
+
- Auto-filtered in logs
|
| 281 |
+
|
| 282 |
+
### Input Validation
|
| 283 |
+
|
| 284 |
+
- Frontend basic format validation
|
| 285 |
+
- Backend Pydantic model validation
|
| 286 |
+
- File size and format restrictions
|
| 287 |
+
|
| 288 |
+
### Data Privacy
|
| 289 |
+
|
| 290 |
+
- Local storage only
|
| 291 |
+
- No external data sharing
|
| 292 |
+
- Consider encryption for sensitive data
|
| 293 |
+
|
| 294 |
+
## Future Enhancements
|
| 295 |
+
|
| 296 |
+
### Short-term
|
| 297 |
+
|
| 298 |
+
- [ ] Multi-turn conversation history
|
| 299 |
+
- [ ] Voice synthesis (AI voice response)
|
| 300 |
+
- [ ] Emotion analysis visualization
|
| 301 |
+
- [ ] Smart recommendations
|
| 302 |
+
|
| 303 |
+
### Long-term
|
| 304 |
+
|
| 305 |
+
- [ ] Vector database for better RAG
|
| 306 |
+
- [ ] Semantic similarity search
|
| 307 |
+
- [ ] Knowledge graph
|
| 308 |
+
- [ ] Multi-modal support (images, video)
|
| 309 |
+
- [ ] User profiling
|
| 310 |
+
- [ ] Personalization engine
|
| 311 |
+
|
| 312 |
+
## Deployment
|
| 313 |
+
|
| 314 |
+
### Frontend
|
| 315 |
+
|
| 316 |
+
No additional configuration needed. HomeInput component is integrated into App.tsx.
|
| 317 |
+
|
| 318 |
+
### Backend
|
| 319 |
+
|
| 320 |
+
No additional configuration needed. RAG functionality is integrated into existing `/api/chat` endpoint.
|
| 321 |
+
|
| 322 |
+
### Requirements
|
| 323 |
+
|
| 324 |
+
- Python 3.8+
|
| 325 |
+
- Node.js 16+
|
| 326 |
+
- Zhipu AI API Key (required)
|
| 327 |
+
|
| 328 |
+
## Troubleshooting
|
| 329 |
+
|
| 330 |
+
### Issue: Voice recording not working
|
| 331 |
+
|
| 332 |
+
**Solution:**
|
| 333 |
+
- Check browser support (Chrome/Edge recommended)
|
| 334 |
+
- Allow microphone permissions
|
| 335 |
+
- Use HTTPS or localhost
|
| 336 |
+
|
| 337 |
+
### Issue: Records not saving
|
| 338 |
+
|
| 339 |
+
**Solution:**
|
| 340 |
+
- Check if backend is running: `curl http://localhost:8000/health`
|
| 341 |
+
- Check browser console for errors
|
| 342 |
+
- Check backend logs: `tail -f logs/app.log`
|
| 343 |
+
|
| 344 |
+
### Issue: AI chat not using history
|
| 345 |
+
|
| 346 |
+
**Solution:**
|
| 347 |
+
- Ensure records exist in `data/records.json`
|
| 348 |
+
- Ask more specific questions like "What did I do yesterday?"
|
| 349 |
+
- Check backend logs for "AI chat successful with RAG context"
|
| 350 |
+
|
| 351 |
+
## Conclusion
|
| 352 |
+
|
| 353 |
+
This implementation successfully adds two complementary features:
|
| 354 |
+
|
| 355 |
+
1. **Quick Recording** - Simple, direct, efficient thought capture
|
| 356 |
+
2. **AI Chat** - Intelligent, warm, personalized companionship
|
| 357 |
+
|
| 358 |
+
Through RAG technology, the AI chat can provide context-aware responses based on user history, creating a truly "understanding" companion experience.
|
| 359 |
+
|
| 360 |
+
The features work together to provide a complete recording and companionship experience:
|
| 361 |
+
- Quick recording for capturing thoughts
|
| 362 |
+
- AI chat for intelligent companionship
|
| 363 |
+
|
| 364 |
+
---
|
| 365 |
+
|
| 366 |
+
**Implementation Complete!** 🎉
|
| 367 |
+
|
| 368 |
+
For questions or further optimization needs, please refer to the detailed documentation or contact the development team.
|
docs/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 文档目录
|
| 2 |
+
|
| 3 |
+
本目录包含项目的详细技术文档。
|
| 4 |
+
|
| 5 |
+
## 📚 文档列表
|
| 6 |
+
|
| 7 |
+
### 核心文档
|
| 8 |
+
|
| 9 |
+
- **[功能架构图.md](功能架构图.md)** - 系统架构、数据流向、组件关系图
|
| 10 |
+
- **[FEATURE_SUMMARY.md](FEATURE_SUMMARY.md)** - 功能实现总结(英文)
|
| 11 |
+
|
| 12 |
+
### 故障排查
|
| 13 |
+
|
| 14 |
+
- **[后端启动问题排查.md](后端启动问题排查.md)** - 后端启动常见问题和解决方案
|
| 15 |
+
- **[语音录制问题排查.md](语音录制问题排查.md)** - 语音录制功能的使用和故障排查
|
| 16 |
+
|
| 17 |
+
## 🔗 相关文档
|
| 18 |
+
|
| 19 |
+
### 根目录文档
|
| 20 |
+
|
| 21 |
+
- **[README.md](../README.md)** - 项目主文档
|
| 22 |
+
- **[PRD.md](../PRD.md)** - 产品需求文档
|
| 23 |
+
|
| 24 |
+
### 测试文件
|
| 25 |
+
|
| 26 |
+
- **[test_home_input.py](../test_home_input.py)** - 首页输入功能测试
|
| 27 |
+
- **[test_audio_recording.html](../test_audio_recording.html)** - 音频录制测试页面
|
| 28 |
+
- **[诊断环境.py](../诊断环境.py)** - 环境诊断脚本
|
| 29 |
+
|
| 30 |
+
## 📖 快速导航
|
| 31 |
+
|
| 32 |
+
### 我想...
|
| 33 |
+
|
| 34 |
+
- **启动应用** → 查看 [README.md](../README.md) 的"快速开始"部分
|
| 35 |
+
- **解决启动问题** → 查看 [后端启动问题排查.md](后端启动问题排查.md)
|
| 36 |
+
- **了解语音录制** → 查看 [语音录制问题排查.md](语音录制问题排查.md)
|
| 37 |
+
- **了解系统架构** → 查看 [功能架构图.md](功能架构图.md)
|
| 38 |
+
- **查看功能实现** → 查看 [FEATURE_SUMMARY.md](FEATURE_SUMMARY.md)
|
| 39 |
+
|
| 40 |
+
## 🛠️ 工具和脚本
|
| 41 |
+
|
| 42 |
+
### 诊断工具
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
# 环境诊断
|
| 46 |
+
python 诊断环境.py
|
| 47 |
+
|
| 48 |
+
# 功能测试
|
| 49 |
+
python test_home_input.py
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 启动脚本
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Windows CMD
|
| 56 |
+
启动后端.bat
|
| 57 |
+
|
| 58 |
+
# PowerShell
|
| 59 |
+
.\启动后端.ps1
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### 测试页面
|
| 63 |
+
|
| 64 |
+
- 打开 `test_audio_recording.html` 测试音频录制功能
|
| 65 |
+
|
| 66 |
+
## 📝 文档维护
|
| 67 |
+
|
| 68 |
+
### 文档结构
|
| 69 |
+
|
| 70 |
+
```
|
| 71 |
+
项目根目录/
|
| 72 |
+
├── README.md # 主文档
|
| 73 |
+
├── PRD.md # 产品需求文档
|
| 74 |
+
├── docs/ # 详细文档目录
|
| 75 |
+
│ ├── README.md # 本文件
|
| 76 |
+
│ ├── 功能架构图.md # 架构文档
|
| 77 |
+
│ ├── 后端启动问题排查.md # 启动问题
|
| 78 |
+
│ ├── 语音录制问题排查.md # 录音问题
|
| 79 |
+
│ └── FEATURE_SUMMARY.md # 功能总结
|
| 80 |
+
├── test_home_input.py # 测试脚本
|
| 81 |
+
├── test_audio_recording.html # 测试页面
|
| 82 |
+
└── 诊断环境.py # 诊断脚本
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### 更新文档
|
| 86 |
+
|
| 87 |
+
如需更新文档,请:
|
| 88 |
+
1. 修改对应的 Markdown 文件
|
| 89 |
+
2. 确保链接正确
|
| 90 |
+
3. 更新本 README 的文档列表
|
| 91 |
+
|
| 92 |
+
## 🤝 贡献
|
| 93 |
+
|
| 94 |
+
欢迎改进文档!如果你发现:
|
| 95 |
+
- 文档有错误
|
| 96 |
+
- 说明不清楚
|
| 97 |
+
- 缺少重要信息
|
| 98 |
+
|
| 99 |
+
请提交 Issue 或 Pull Request。
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
**最后更新:** 2024-01-17
|
docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🗺️ 未来迭代计划 - Roadmap
|
| 2 |
+
|
| 3 |
+
## 📋 版本规划
|
| 4 |
+
|
| 5 |
+
### 当前版本:v1.0.0 ✅
|
| 6 |
+
**发布日期**:2026-01-18
|
| 7 |
+
|
| 8 |
+
**核心功能**:
|
| 9 |
+
- ✅ 语音/文本快速记录
|
| 10 |
+
- ✅ AI 语义解析(心情、灵感、待办)
|
| 11 |
+
- ✅ AI 对话陪伴(RAG)
|
| 12 |
+
- ✅ AI 形象定制(720 种组合)
|
| 13 |
+
- ✅ 物理引擎心情气泡池
|
| 14 |
+
- ✅ 多平台部署(Hugging Face, ModelScope, 本地)
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 🚀 v1.1.0 - 数据增强与可视化
|
| 19 |
+
**预计发布**:2026-02
|
| 20 |
+
|
| 21 |
+
### 核心目标
|
| 22 |
+
增强数据分析和可视化能力,让用户更好地了解自己的情绪变化和成长轨迹。
|
| 23 |
+
|
| 24 |
+
### 新功能
|
| 25 |
+
|
| 26 |
+
#### 1. 情绪趋势分析 📊
|
| 27 |
+
- [ ] **情绪时间线**
|
| 28 |
+
- 按日/周/月查看情绪变化曲线
|
| 29 |
+
- 识别情绪周期和模式
|
| 30 |
+
- 情绪高峰和低谷标注
|
| 31 |
+
|
| 32 |
+
- [ ] **情绪统计报告**
|
| 33 |
+
- 情绪类型分布饼图
|
| 34 |
+
- 情绪强度热力图
|
| 35 |
+
- 每周/每月情绪总结
|
| 36 |
+
|
| 37 |
+
- [ ] **情绪触发因素分析**
|
| 38 |
+
- 关键词云图
|
| 39 |
+
- 高频触发场景识别
|
| 40 |
+
- 情绪关联事件分析
|
| 41 |
+
|
| 42 |
+
#### 2. 灵感知识图谱 🕸️
|
| 43 |
+
- [ ] **灵感关联网络**
|
| 44 |
+
- 基于标签的灵感连接
|
| 45 |
+
- 可视化灵感演化路径
|
| 46 |
+
- 发现灵感之间的隐藏联系
|
| 47 |
+
|
| 48 |
+
- [ ] **灵感分类优化**
|
| 49 |
+
- 自动分类(工作/生活/学习/创意)
|
| 50 |
+
- 自定义标签系统
|
| 51 |
+
- 灵感收藏夹
|
| 52 |
+
|
| 53 |
+
- [ ] **灵感搜索增强**
|
| 54 |
+
- 全文搜索
|
| 55 |
+
- 标签筛选
|
| 56 |
+
- 时间范围筛选
|
| 57 |
+
|
| 58 |
+
#### 3. 待办智能管理 ✅
|
| 59 |
+
- [ ] **待办优先级**
|
| 60 |
+
- AI 自动评估紧急程度
|
| 61 |
+
- 重要性标记
|
| 62 |
+
- 智能排序
|
| 63 |
+
|
| 64 |
+
- [ ] **待办提醒**
|
| 65 |
+
- 时间提醒
|
| 66 |
+
- 地点提醒(基于位置)
|
| 67 |
+
- 智能推荐最佳执行时间
|
| 68 |
+
|
| 69 |
+
- [ ] **待办统计**
|
| 70 |
+
- 完成率统计
|
| 71 |
+
- 拖延分析
|
| 72 |
+
- 效率趋势图
|
| 73 |
+
|
| 74 |
+
### 技术改进
|
| 75 |
+
- [ ] 数据导出功能(JSON/CSV)
|
| 76 |
+
- [ ] 数据备份与恢复
|
| 77 |
+
- [ ] 性能优化(大数据量处理)
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 🎨 v1.2.0 - 社交与分享
|
| 82 |
+
**预计发布**:2026-03
|
| 83 |
+
|
| 84 |
+
### 核心目标
|
| 85 |
+
构建温暖的社区氛围,让用户可以安全地分享和交流。
|
| 86 |
+
|
| 87 |
+
### 新功能
|
| 88 |
+
|
| 89 |
+
#### 1. 匿名社区 🌐
|
| 90 |
+
- [ ] **心情广场**
|
| 91 |
+
- 匿名分享心情
|
| 92 |
+
- 点赞和评论
|
| 93 |
+
- 情绪共鸣标记
|
| 94 |
+
|
| 95 |
+
- [ ] **灵感市集**
|
| 96 |
+
- 分享创意灵感
|
| 97 |
+
- 灵感收藏
|
| 98 |
+
- 灵感协作
|
| 99 |
+
|
| 100 |
+
- [ ] **治愈树洞**
|
| 101 |
+
- 完全匿名倾诉
|
| 102 |
+
- AI 温暖回复
|
| 103 |
+
- 用户互助支持
|
| 104 |
+
|
| 105 |
+
#### 2. 好友系统 👥
|
| 106 |
+
- [ ] **添加好友**
|
| 107 |
+
- 邀请码机制
|
| 108 |
+
- 好友申请
|
| 109 |
+
- 好友列表
|
| 110 |
+
|
| 111 |
+
- [ ] **私密分享**
|
| 112 |
+
- 向好友分享特定记录
|
| 113 |
+
- 好友可见的心情动态
|
| 114 |
+
- 互相鼓励和支持
|
| 115 |
+
|
| 116 |
+
- [ ] **小组功能**
|
| 117 |
+
- 创建兴趣小组
|
| 118 |
+
- 小组话题讨论
|
| 119 |
+
- 小组活动
|
| 120 |
+
|
| 121 |
+
#### 3. 成就系统 🏆
|
| 122 |
+
- [ ] **记录成就**
|
| 123 |
+
- 连续记录天数
|
| 124 |
+
- 记录总数里程碑
|
| 125 |
+
- 特殊成就徽章
|
| 126 |
+
|
| 127 |
+
- [ ] **成长勋章**
|
| 128 |
+
- 情绪管理大师
|
| 129 |
+
- 灵感收集家
|
| 130 |
+
- 行动派达人
|
| 131 |
+
|
| 132 |
+
- [ ] **每日打卡**
|
| 133 |
+
- 打卡日历
|
| 134 |
+
- 打卡奖励
|
| 135 |
+
- 打卡提醒
|
| 136 |
+
|
| 137 |
+
### 技术改进
|
| 138 |
+
- [ ] 用户认证系统
|
| 139 |
+
- [ ] 数据隐私保护
|
| 140 |
+
- [ ] 内容审核机制
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## 🧠 v1.3.0 - AI 能力升级
|
| 145 |
+
**预计发布**:2026-04
|
| 146 |
+
|
| 147 |
+
### 核心目标
|
| 148 |
+
提升 AI 的智能化水平,提供更个性化、更深入的陪伴体验。
|
| 149 |
+
|
| 150 |
+
### 新功能
|
| 151 |
+
|
| 152 |
+
#### 1. 智能对话增强 💬
|
| 153 |
+
- [ ] **多轮对话记忆**
|
| 154 |
+
- 记住对话上下文
|
| 155 |
+
- 长期记忆用户偏好
|
| 156 |
+
- 个性化对话风格
|
| 157 |
+
|
| 158 |
+
- [ ] **情感识别**
|
| 159 |
+
- 识别用户情绪状态
|
| 160 |
+
- 根据情绪调整回复风格
|
| 161 |
+
- 主动关怀和安慰
|
| 162 |
+
|
| 163 |
+
- [ ] **主动对话**
|
| 164 |
+
- AI 主动发起问候
|
| 165 |
+
- 定期情绪检查
|
| 166 |
+
- 特殊日期提醒
|
| 167 |
+
|
| 168 |
+
#### 2. 个性化推荐 🎯
|
| 169 |
+
- [ ] **内容推荐**
|
| 170 |
+
- 推荐相关灵感
|
| 171 |
+
- 推荐治愈内容
|
| 172 |
+
- 推荐行动建议
|
| 173 |
+
|
| 174 |
+
- [ ] **习惯分析**
|
| 175 |
+
- 识别用户习惯模式
|
| 176 |
+
- 提供改善建议
|
| 177 |
+
- 个性化目标设定
|
| 178 |
+
|
| 179 |
+
- [ ] **智能提醒**
|
| 180 |
+
- 基于历史数据的智能提醒
|
| 181 |
+
- 最佳记录时间推荐
|
| 182 |
+
- 情绪调节建议
|
| 183 |
+
|
| 184 |
+
#### 3. AI 形象进化 🎭
|
| 185 |
+
- [ ] **动态表情**
|
| 186 |
+
- 根据对话内容变化表情
|
| 187 |
+
- 情绪同步动画
|
| 188 |
+
- 互动动作
|
| 189 |
+
|
| 190 |
+
- [ ] **语音对话**
|
| 191 |
+
- AI 语音回复
|
| 192 |
+
- 语音情感表达
|
| 193 |
+
- 多种声音选择
|
| 194 |
+
|
| 195 |
+
- [ ] **3D 形象**
|
| 196 |
+
- 3D 角色模型
|
| 197 |
+
- 更丰富的动画
|
| 198 |
+
- 场景互动
|
| 199 |
+
|
| 200 |
+
### 技术改进
|
| 201 |
+
- [ ] 升级到更强大的 AI 模型
|
| 202 |
+
- [ ] 本地 AI 模型支持(隐私保护)
|
| 203 |
+
- [ ] 多模态输入(图片、视频)
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## 📱 v1.4.0 - 移动端与硬件
|
| 208 |
+
**预计发布**:2026-05
|
| 209 |
+
|
| 210 |
+
### 核心目标
|
| 211 |
+
扩展到移动端和智能硬件,提供无处不在的陪伴体验。
|
| 212 |
+
|
| 213 |
+
### 新功能
|
| 214 |
+
|
| 215 |
+
#### 1. 移动端应用 📱
|
| 216 |
+
- [ ] **原生 App**
|
| 217 |
+
- iOS 应用
|
| 218 |
+
- Android 应用
|
| 219 |
+
- 离线功能
|
| 220 |
+
|
| 221 |
+
- [ ] **移动端优化**
|
| 222 |
+
- 触摸手势优化
|
| 223 |
+
- 移动端专属 UI
|
| 224 |
+
- 省电模式
|
| 225 |
+
|
| 226 |
+
- [ ] **快捷记录**
|
| 227 |
+
- 桌面小组件
|
| 228 |
+
- 快捷指令
|
| 229 |
+
- 语音唤醒
|
| 230 |
+
|
| 231 |
+
#### 2. 智能硬件集成 ⌚
|
| 232 |
+
- [ ] **可穿戴设备**
|
| 233 |
+
- 智能手表集成
|
| 234 |
+
- 心率监测
|
| 235 |
+
- 情绪预警
|
| 236 |
+
|
| 237 |
+
- [ ] **智能音箱**
|
| 238 |
+
- 语音交互
|
| 239 |
+
- 定时播报
|
| 240 |
+
- 环境音乐
|
| 241 |
+
|
| 242 |
+
- [ ] **IoT 设备**
|
| 243 |
+
- 智能灯光(情绪灯)
|
| 244 |
+
- 智能香薰
|
| 245 |
+
- 环境传感器
|
| 246 |
+
|
| 247 |
+
#### 3. 跨平台同步 ☁️
|
| 248 |
+
- [ ] **云端同步**
|
| 249 |
+
- 实时数据同步
|
| 250 |
+
- 多设备无缝切换
|
| 251 |
+
- 冲突解决
|
| 252 |
+
|
| 253 |
+
- [ ] **离线模式**
|
| 254 |
+
- 离线记录
|
| 255 |
+
- 离线 AI 对话
|
| 256 |
+
- 自动同步
|
| 257 |
+
|
| 258 |
+
### 技术改进
|
| 259 |
+
- [ ] React Native / Flutter 移动端开发
|
| 260 |
+
- [ ] 蓝牙/WiFi 硬件通信
|
| 261 |
+
- [ ] 云端数据库
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
## 🌟 v2.0.0 - 生态系统
|
| 266 |
+
**预计发布**:2026-Q3
|
| 267 |
+
|
| 268 |
+
### 核心目标
|
| 269 |
+
构建完整的心理健康生态系统,提供全方位的支持。
|
| 270 |
+
|
| 271 |
+
### 新功能
|
| 272 |
+
|
| 273 |
+
#### 1. 专业服务对接 🏥
|
| 274 |
+
- [ ] **心理咨询师入驻**
|
| 275 |
+
- 在线预约
|
| 276 |
+
- 视频咨询
|
| 277 |
+
- 专业评估
|
| 278 |
+
|
| 279 |
+
- [ ] **心理测评**
|
| 280 |
+
- 标准化量表
|
| 281 |
+
- AI 辅助评估
|
| 282 |
+
- 报告生成
|
| 283 |
+
|
| 284 |
+
- [ ] **危机干预**
|
| 285 |
+
- 自动识别危机信号
|
| 286 |
+
- 紧急联系人通知
|
| 287 |
+
- 专业资源推荐
|
| 288 |
+
|
| 289 |
+
#### 2. 内容生态 📚
|
| 290 |
+
- [ ] **治愈内容库**
|
| 291 |
+
- 冥想音频
|
| 292 |
+
- 正念练习
|
| 293 |
+
- 心理学文章
|
| 294 |
+
|
| 295 |
+
- [ ] **课程体系**
|
| 296 |
+
- 情绪管理课程
|
| 297 |
+
- 压力应对课程
|
| 298 |
+
- 自我成长课程
|
| 299 |
+
|
| 300 |
+
- [ ] **创作者平台**
|
| 301 |
+
- 内容创作工具
|
| 302 |
+
- 创作者激励
|
| 303 |
+
- 内容分发
|
| 304 |
+
|
| 305 |
+
#### 3. 企业版 🏢
|
| 306 |
+
- [ ] **团队版功能**
|
| 307 |
+
- 团队情绪监测
|
| 308 |
+
- 团队氛围分析
|
| 309 |
+
- 管理者仪表盘
|
| 310 |
+
|
| 311 |
+
- [ ] **企业服务**
|
| 312 |
+
- 员工关怀计划
|
| 313 |
+
- 心理健康培训
|
| 314 |
+
- 数据报告
|
| 315 |
+
|
| 316 |
+
### 技术改进
|
| 317 |
+
- [ ] 微服务架构
|
| 318 |
+
- [ ] 大数据分析平台
|
| 319 |
+
- [ ] AI 模型训练平台
|
| 320 |
+
|
| 321 |
+
---
|
| 322 |
+
|
| 323 |
+
## 🔮 未来展望
|
| 324 |
+
|
| 325 |
+
### 长期愿景
|
| 326 |
+
打造一个温暖、智能、专业的心理健康陪伴平台,让每个人都能:
|
| 327 |
+
- 🌈 更好地理解和管理自己的情绪
|
| 328 |
+
- 💡 记录和实现自己的灵感与目标
|
| 329 |
+
- 🤝 在安全的环境中获得支持和陪伴
|
| 330 |
+
- 🌱 持续成长,成为更好的自己
|
| 331 |
+
|
| 332 |
+
### 技术方向
|
| 333 |
+
- **AI 技术**:更智能的情感理解和对话能力
|
| 334 |
+
- **隐私保护**:端到端加密、本地 AI 模型
|
| 335 |
+
- **多模态**:支持图片、视频、音频等多种输入
|
| 336 |
+
- **个性化**:深度学习用户偏好,提供定制化体验
|
| 337 |
+
- **开放生态**:API 开放、插件系统、第三方集成
|
| 338 |
+
|
| 339 |
+
### 研究方向
|
| 340 |
+
- 情绪识别算法优化
|
| 341 |
+
- 个性化推荐系统
|
| 342 |
+
- 心理健康预警模型
|
| 343 |
+
- 人机交互体验研究
|
| 344 |
+
|
| 345 |
+
---
|
| 346 |
+
|
| 347 |
+
## 📊 迭代原则
|
| 348 |
+
|
| 349 |
+
### 1. 用户优先
|
| 350 |
+
- 所有功能基于用户真实需求
|
| 351 |
+
- 持续收集用户反馈
|
| 352 |
+
- 快速迭代优化
|
| 353 |
+
|
| 354 |
+
### 2. 隐私安全
|
| 355 |
+
- 数据加密存储
|
| 356 |
+
- 用户数据自主权
|
| 357 |
+
- 透明的隐私政策
|
| 358 |
+
|
| 359 |
+
### 3. 温暖治愈
|
| 360 |
+
- 保持温暖的设计风格
|
| 361 |
+
- 避免过度商业化
|
| 362 |
+
- 关注用户心理健康
|
| 363 |
+
|
| 364 |
+
### 4. 技术创新
|
| 365 |
+
- 采用前沿 AI 技术
|
| 366 |
+
- 优化用户体验
|
| 367 |
+
- 保持技术领先
|
| 368 |
+
|
| 369 |
+
### 5. 可持续发展
|
| 370 |
+
- 合理的商业模式
|
| 371 |
+
- 社会责任
|
| 372 |
+
- 长期价值创造
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## 🤝 参与贡献
|
| 377 |
+
|
| 378 |
+
我们欢迎社区贡献!你可以通过以下方式参与:
|
| 379 |
+
|
| 380 |
+
### 功能建议
|
| 381 |
+
- 在 GitHub Issues 提交功能建议
|
| 382 |
+
- 参与功能讨论和投票
|
| 383 |
+
- 分享你的使用体验
|
| 384 |
+
|
| 385 |
+
### 代码贡献
|
| 386 |
+
- Fork 项目并提交 PR
|
| 387 |
+
- 修复 Bug
|
| 388 |
+
- 优化性能
|
| 389 |
+
- 添加新功能
|
| 390 |
+
|
| 391 |
+
### 内容贡献
|
| 392 |
+
- 分享治愈内容
|
| 393 |
+
- 编写使用教程
|
| 394 |
+
- 翻译文档
|
| 395 |
+
|
| 396 |
+
### 测试反馈
|
| 397 |
+
- 参与 Beta 测试
|
| 398 |
+
- 报告 Bug
|
| 399 |
+
- 提供改进建议
|
| 400 |
+
|
| 401 |
+
---
|
| 402 |
+
|
| 403 |
+
## 📞 联系我们
|
| 404 |
+
|
| 405 |
+
- **GitHub**:https://github.com/kernel-14/Nora
|
| 406 |
+
- **Issues**:https://github.com/kernel-14/Nora/issues
|
| 407 |
+
- **Discussions**:https://github.com/kernel-14/Nora/discussions
|
| 408 |
+
|
| 409 |
+
---
|
| 410 |
+
|
| 411 |
+
## 📝 更新日志
|
| 412 |
+
|
| 413 |
+
### v1.0.0 (2026-01-18)
|
| 414 |
+
- ✅ 初始版本发布
|
| 415 |
+
- ✅ 核心功能实现
|
| 416 |
+
- ✅ 多平台部署支持
|
| 417 |
+
|
| 418 |
+
---
|
| 419 |
+
|
| 420 |
+
**注意**:本路线图会根据实际情况和用户反馈进行调整。具体功能和发布时间可能会有变化。
|
| 421 |
+
|
| 422 |
+
**最后更新**:2026-01-18
|
docs/功能架构图.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 首页交互功能架构图
|
| 2 |
+
|
| 3 |
+
## 整体架构
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 7 |
+
│ 用户界面 │
|
| 8 |
+
│ │
|
| 9 |
+
│ ┌──────────────┐ ┌──────────────┐ │
|
| 10 |
+
│ │ 首页记录 │ │ AI 对话 │ │
|
| 11 |
+
│ │ │ │ │ │
|
| 12 |
+
│ │ 🎤 语音输入 │ │ 💬 聊天界面 │ │
|
| 13 |
+
│ │ ⌨️ 文字输入 │ │ 📝 消息输入 │ │
|
| 14 |
+
│ └──────────────┘ └──────────────┘ │
|
| 15 |
+
│ │ │ │
|
| 16 |
+
└─────────┼──────────────────────────────┼─────────────────────┘
|
| 17 |
+
│ │
|
| 18 |
+
▼ ▼
|
| 19 |
+
┌─────────────────────┐ ┌─────────────────────┐
|
| 20 |
+
│ /api/process │ │ /api/chat │
|
| 21 |
+
│ │ │ │
|
| 22 |
+
│ 1. 接收输入 │ │ 1. 接收消息 │
|
| 23 |
+
│ 2. ASR 转文字 │ │ 2. 加载历史记录 │
|
| 24 |
+
│ 3. 语义分析 │ │ 3. 构建 RAG 上下文 │
|
| 25 |
+
│ 4. 保存记录 │ │ 4. 调用 AI API │
|
| 26 |
+
│ 5. 拆分分类 │ │ 5. 返回回复 │
|
| 27 |
+
└─────────────────────┘ └─────────────────────┘
|
| 28 |
+
│ │
|
| 29 |
+
▼ │
|
| 30 |
+
┌─────────────────────┐ │
|
| 31 |
+
│ 数据存储 │◄─────────────────┘
|
| 32 |
+
│ │ (读取历史)
|
| 33 |
+
│ 📄 records.json │
|
| 34 |
+
│ 😊 moods.json │
|
| 35 |
+
│ 💡 inspirations.json│
|
| 36 |
+
│ ✅ todos.json │
|
| 37 |
+
└─────────────────────┘
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## 首页记录流程
|
| 41 |
+
|
| 42 |
+
```
|
| 43 |
+
用户输入
|
| 44 |
+
│
|
| 45 |
+
├─ 语音 ──► MediaRecorder ──► Blob ──► File
|
| 46 |
+
│ │
|
| 47 |
+
└─ 文字 ─────────────────────────────────┤
|
| 48 |
+
│
|
| 49 |
+
▼
|
| 50 |
+
FormData (audio/text)
|
| 51 |
+
│
|
| 52 |
+
▼
|
| 53 |
+
POST /api/process
|
| 54 |
+
│
|
| 55 |
+
┌───────────────────────┴───────────────────────┐
|
| 56 |
+
│ │
|
| 57 |
+
▼ ▼
|
| 58 |
+
audio != null? text != null?
|
| 59 |
+
│ │
|
| 60 |
+
▼ │
|
| 61 |
+
ASR Service (智谱 AI) │
|
| 62 |
+
│ │
|
| 63 |
+
└───────────────────┬───────────────────────────┘
|
| 64 |
+
│
|
| 65 |
+
▼
|
| 66 |
+
original_text
|
| 67 |
+
│
|
| 68 |
+
▼
|
| 69 |
+
Semantic Parser (GLM-4-Flash)
|
| 70 |
+
│
|
| 71 |
+
┌───────────────────┼───────────────────┐
|
| 72 |
+
│ │ │
|
| 73 |
+
▼ ▼ ▼
|
| 74 |
+
mood inspirations todos
|
| 75 |
+
│ │ │
|
| 76 |
+
└───────────────────┴───────────────────┘
|
| 77 |
+
│
|
| 78 |
+
▼
|
| 79 |
+
Storage Service
|
| 80 |
+
│
|
| 81 |
+
┌───────────────────┼───────────────────┐
|
| 82 |
+
│ │ │
|
| 83 |
+
▼ ▼ ▼
|
| 84 |
+
moods.json inspirations.json todos.json
|
| 85 |
+
│ │ │
|
| 86 |
+
└───────────────────┴───────────────────┘
|
| 87 |
+
│
|
| 88 |
+
▼
|
| 89 |
+
records.json (完整记录)
|
| 90 |
+
│
|
| 91 |
+
▼
|
| 92 |
+
返回 ProcessResponse
|
| 93 |
+
│
|
| 94 |
+
▼
|
| 95 |
+
前端显示"记录成功"
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## AI 对话流程(RAG 增强)
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
用户消息
|
| 102 |
+
│
|
| 103 |
+
▼
|
| 104 |
+
POST /api/chat
|
| 105 |
+
│
|
| 106 |
+
├─ text: "我最近在做什么?"
|
| 107 |
+
│
|
| 108 |
+
▼
|
| 109 |
+
Storage Service
|
| 110 |
+
│
|
| 111 |
+
├─ 读取 records.json
|
| 112 |
+
│
|
| 113 |
+
▼
|
| 114 |
+
recent_records = records[-10:] (最近 10 条)
|
| 115 |
+
│
|
| 116 |
+
▼
|
| 117 |
+
构建上下文
|
| 118 |
+
│
|
| 119 |
+
├─ for each record:
|
| 120 |
+
│ ├─ 提取 original_text
|
| 121 |
+
│ ├─ 提取 mood (type, intensity)
|
| 122 |
+
│ ├─ 提取 inspirations (core_idea)
|
| 123 |
+
│ └─ 提取 todos (task)
|
| 124 |
+
│
|
| 125 |
+
▼
|
| 126 |
+
context_text = """
|
| 127 |
+
[2024-01-17T10:00:00Z] 用户说: 今天工作很累
|
| 128 |
+
情绪: 疲惫 (强度: 7)
|
| 129 |
+
待办: 明天早起跑步
|
| 130 |
+
|
| 131 |
+
[2024-01-17T14:00:00Z] 用户说: 完成了项目很开心
|
| 132 |
+
情绪: 开心 (强度: 8)
|
| 133 |
+
灵感: 项目完成的成就感
|
| 134 |
+
...
|
| 135 |
+
"""
|
| 136 |
+
│
|
| 137 |
+
▼
|
| 138 |
+
system_prompt = f"""
|
| 139 |
+
你是一个温柔、善解人意的AI陪伴助手。
|
| 140 |
+
你可以参考用户的历史记录来提供更贴心的回复:
|
| 141 |
+
|
| 142 |
+
{context_text}
|
| 143 |
+
|
| 144 |
+
请基于这些背景信息,用温暖、理解的语气回复用户。
|
| 145 |
+
"""
|
| 146 |
+
│
|
| 147 |
+
▼
|
| 148 |
+
GLM-4-Flash API
|
| 149 |
+
│
|
| 150 |
+
├─ model: "glm-4-flash"
|
| 151 |
+
├─ messages: [
|
| 152 |
+
│ {role: "system", content: system_prompt},
|
| 153 |
+
│ {role: "user", content: "我最近在做什么?"}
|
| 154 |
+
│ ]
|
| 155 |
+
├─ temperature: 0.8
|
| 156 |
+
└─ top_p: 0.9
|
| 157 |
+
│
|
| 158 |
+
▼
|
| 159 |
+
AI 生成回复
|
| 160 |
+
│
|
| 161 |
+
├─ "从你的记录来看,你最近在忙一个项目,
|
| 162 |
+
│ 虽然工作很累,但完成后很有成就感呢!
|
| 163 |
+
│ 你还计划明天早起去跑步,保持健康的习惯真棒!"
|
| 164 |
+
│
|
| 165 |
+
▼
|
| 166 |
+
返回 {response: "..."}
|
| 167 |
+
│
|
| 168 |
+
▼
|
| 169 |
+
前端显示 AI 回复
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
## 数据流向
|
| 173 |
+
|
| 174 |
+
```
|
| 175 |
+
┌─────────────┐
|
| 176 |
+
│ 用户输入 │
|
| 177 |
+
└──────┬──────┘
|
| 178 |
+
│
|
| 179 |
+
▼
|
| 180 |
+
┌─────────────┐
|
| 181 |
+
│ HomeInput │ (前端组件)
|
| 182 |
+
│ Component │
|
| 183 |
+
└──────┬──────┘
|
| 184 |
+
│
|
| 185 |
+
▼
|
| 186 |
+
┌─────────────┐
|
| 187 |
+
│ API Service │ (前端服务层)
|
| 188 |
+
└──────┬──────┘
|
| 189 |
+
│
|
| 190 |
+
▼
|
| 191 |
+
┌─────────────┐
|
| 192 |
+
│ FastAPI │ (后端 API)
|
| 193 |
+
│ /api/process│
|
| 194 |
+
└──────┬──────┘
|
| 195 |
+
│
|
| 196 |
+
├─► ASR Service ──► 智谱 AI (语音转文字)
|
| 197 |
+
│
|
| 198 |
+
├─► Semantic Parser ──► GLM-4-Flash (语义分析)
|
| 199 |
+
│
|
| 200 |
+
└─► Storage Service ──► JSON 文件 (数据存储)
|
| 201 |
+
│
|
| 202 |
+
├─► records.json
|
| 203 |
+
├─► moods.json
|
| 204 |
+
├─► inspirations.json
|
| 205 |
+
└─► todos.json
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
## RAG 知识库结构
|
| 209 |
+
|
| 210 |
+
```
|
| 211 |
+
records.json (知识库)
|
| 212 |
+
│
|
| 213 |
+
├─ Record 1
|
| 214 |
+
│ ├─ record_id: "uuid-1"
|
| 215 |
+
│ ├─ timestamp: "2024-01-17T10:00:00Z"
|
| 216 |
+
│ ├─ original_text: "今天工作很累"
|
| 217 |
+
│ └─ parsed_data:
|
| 218 |
+
│ ├─ mood: {type: "疲惫", intensity: 7}
|
| 219 |
+
│ ├─ inspirations: []
|
| 220 |
+
│ └─ todos: []
|
| 221 |
+
│
|
| 222 |
+
├─ Record 2
|
| 223 |
+
│ ├─ record_id: "uuid-2"
|
| 224 |
+
│ ├─ timestamp: "2024-01-17T14:00:00Z"
|
| 225 |
+
│ ├─ original_text: "完成了项目很开心"
|
| 226 |
+
│ └─ parsed_data:
|
| 227 |
+
│ ├─ mood: {type: "开心", intensity: 8}
|
| 228 |
+
│ ├─ inspirations: [{core_idea: "项目完成"}]
|
| 229 |
+
│ └─ todos: []
|
| 230 |
+
│
|
| 231 |
+
└─ Record 3
|
| 232 |
+
├─ record_id: "uuid-3"
|
| 233 |
+
├─ timestamp: "2024-01-17T18:00:00Z"
|
| 234 |
+
├─ original_text: "明天要早起跑步"
|
| 235 |
+
└─ parsed_data:
|
| 236 |
+
├─ mood: null
|
| 237 |
+
├─ inspirations: []
|
| 238 |
+
└─ todos: [{task: "早起跑步", time: "明天"}]
|
| 239 |
+
|
| 240 |
+
↓ (RAG 提取)
|
| 241 |
+
|
| 242 |
+
AI 对话上下文:
|
| 243 |
+
"""
|
| 244 |
+
[2024-01-17T10:00:00Z] 用户说: 今天工作很累
|
| 245 |
+
情绪: 疲惫 (强度: 7)
|
| 246 |
+
|
| 247 |
+
[2024-01-17T14:00:00Z] 用户说: 完成了项目很开心
|
| 248 |
+
情绪: 开心 (强度: 8)
|
| 249 |
+
灵感: 项目完成
|
| 250 |
+
|
| 251 |
+
[2024-01-17T18:00:00Z] 用户说: 明天要早起跑步
|
| 252 |
+
待办: 早起跑步
|
| 253 |
+
"""
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
## 组件关系图
|
| 257 |
+
|
| 258 |
+
```
|
| 259 |
+
App.tsx
|
| 260 |
+
│
|
| 261 |
+
├─► HomeInput.tsx (首页输入)
|
| 262 |
+
│ │
|
| 263 |
+
│ ├─► VoiceRecording (语音录制)
|
| 264 |
+
│ │ └─► MediaRecorder API
|
| 265 |
+
│ │
|
| 266 |
+
│ ├─► TextInput (文字输入)
|
| 267 |
+
│ │ └─► Input Element
|
| 268 |
+
│ │
|
| 269 |
+
│ └─► apiService.processInput()
|
| 270 |
+
│ └─► POST /api/process
|
| 271 |
+
│
|
| 272 |
+
├─► MoodView.tsx (心情页面)
|
| 273 |
+
│ └─► ChatDialog.tsx
|
| 274 |
+
│ └─► apiService.chatWithAI()
|
| 275 |
+
│ └─► POST /api/chat (RAG)
|
| 276 |
+
│
|
| 277 |
+
├─► InspirationView.tsx (灵感页面)
|
| 278 |
+
│ └─► ChatDialog.tsx
|
| 279 |
+
│ └─► apiService.chatWithAI()
|
| 280 |
+
│ └─► POST /api/chat (RAG)
|
| 281 |
+
│
|
| 282 |
+
└─► TodoView.tsx (待办页面)
|
| 283 |
+
└─► ChatDialog.tsx
|
| 284 |
+
└─► apiService.chatWithAI()
|
| 285 |
+
└─► POST /api/chat (RAG)
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
## API 端点对比
|
| 289 |
+
|
| 290 |
+
```
|
| 291 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 292 |
+
│ /api/process │
|
| 293 |
+
├─────────────────────────────────────────────────────────────┤
|
| 294 |
+
│ 输入: audio (File) 或 text (string) │
|
| 295 |
+
│ 处理: │
|
| 296 |
+
│ 1. ASR 转文字 (如果是音频) │
|
| 297 |
+
│ 2. 语义分析 (GLM-4-Flash) │
|
| 298 |
+
│ 3. 保存到 records.json │
|
| 299 |
+
│ 4. 拆分到 moods/inspirations/todos.json │
|
| 300 |
+
│ 输出: ProcessResponse { │
|
| 301 |
+
│ record_id, timestamp, mood, inspirations, todos │
|
| 302 |
+
│ } │
|
| 303 |
+
└─────────────────────────────────────────────────────────────┘
|
| 304 |
+
|
| 305 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 306 |
+
│ /api/chat │
|
| 307 |
+
├─────────────────────────────────────────────────────────────┤
|
| 308 |
+
│ 输入: text (string) │
|
| 309 |
+
│ 处理: │
|
| 310 |
+
│ 1. 加载 records.json (最近 10 条) │
|
| 311 |
+
│ 2. 提取情绪、灵感、待办信息 │
|
| 312 |
+
│ 3. 构建 RAG 上下文 │
|
| 313 |
+
│ 4. 调用 GLM-4-Flash API │
|
| 314 |
+
│ 5. 生成个性化回复 │
|
| 315 |
+
│ 输出: { │
|
| 316 |
+
│ response: "AI 的回复内容" │
|
| 317 |
+
│ } │
|
| 318 |
+
└─────────────────────────────────────────────────────────────┘
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
## 技术栈
|
| 322 |
+
|
| 323 |
+
```
|
| 324 |
+
前端
|
| 325 |
+
├─ React 19
|
| 326 |
+
├─ TypeScript
|
| 327 |
+
├─ Vite
|
| 328 |
+
├─ Tailwind CSS
|
| 329 |
+
└─ Lucide Icons
|
| 330 |
+
|
| 331 |
+
后端
|
| 332 |
+
├─ FastAPI
|
| 333 |
+
├─ Pydantic
|
| 334 |
+
├─ Uvicorn
|
| 335 |
+
├─ httpx (异步 HTTP)
|
| 336 |
+
└─ Python 3.8+
|
| 337 |
+
|
| 338 |
+
AI 服务
|
| 339 |
+
├─ 智谱 AI (ASR)
|
| 340 |
+
├─ GLM-4-Flash (语义分析)
|
| 341 |
+
└─ GLM-4-Flash (对话生成)
|
| 342 |
+
|
| 343 |
+
数据存储
|
| 344 |
+
└─ JSON 文件
|
| 345 |
+
├─ records.json
|
| 346 |
+
├─ moods.json
|
| 347 |
+
├─ inspirations.json
|
| 348 |
+
└─ todos.json
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
## 性能优化点
|
| 352 |
+
|
| 353 |
+
```
|
| 354 |
+
前端优化
|
| 355 |
+
├─ 防抖处理 (输入延迟)
|
| 356 |
+
├─ 乐观更新 (立即反馈)
|
| 357 |
+
├─ 组件懒加载
|
| 358 |
+
└─ 缓存机制
|
| 359 |
+
|
| 360 |
+
后端优化
|
| 361 |
+
├─ 异步处理 (async/await)
|
| 362 |
+
├─ 连接池复用
|
| 363 |
+
├─ 限制历史记录数量 (10 条)
|
| 364 |
+
└─ 响应压缩
|
| 365 |
+
|
| 366 |
+
RAG 优化
|
| 367 |
+
├─ 只加载最近记录
|
| 368 |
+
├─ 精简上下文信息
|
| 369 |
+
├─ 缓存常见问题
|
| 370 |
+
└─ 向量数据库 (未来)
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
这个架构图展示了整个系统的工作流程和数据流向,帮助理解两种功能的区别和联系。
|
docs/后端启动问题排查.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 后端启动问题排查指南
|
| 2 |
+
|
| 3 |
+
## 问题:ModuleNotFoundError: No module named 'app'
|
| 4 |
+
|
| 5 |
+
### 错误信息
|
| 6 |
+
```
|
| 7 |
+
ModuleNotFoundError: No module named 'app'
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
### 原因分析
|
| 11 |
+
|
| 12 |
+
这个错误通常由以下原因引起:
|
| 13 |
+
|
| 14 |
+
1. **在错误的目录运行命令**
|
| 15 |
+
- 必须在项目根目录运行
|
| 16 |
+
- 不能在 `app/` 目录内运行
|
| 17 |
+
|
| 18 |
+
2. **Python 路径问题**
|
| 19 |
+
- Python 找不到 `app` 模块
|
| 20 |
+
- PYTHONPATH 未正确设置
|
| 21 |
+
|
| 22 |
+
3. **虚拟环境问题**
|
| 23 |
+
- 未激活正确的虚拟环境
|
| 24 |
+
- 依赖未安装
|
| 25 |
+
|
| 26 |
+
## 解决方案
|
| 27 |
+
|
| 28 |
+
### 方案 1:使用启动脚本(推荐)
|
| 29 |
+
|
| 30 |
+
**Windows CMD:**
|
| 31 |
+
```bash
|
| 32 |
+
启动后端.bat
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
**PowerShell:**
|
| 36 |
+
```bash
|
| 37 |
+
.\启动后端.ps1
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
这些脚本会:
|
| 41 |
+
- ✅ 检查当前目录是否正确
|
| 42 |
+
- ✅ 自动激活虚拟环境
|
| 43 |
+
- ✅ 使用正确的命令启动
|
| 44 |
+
|
| 45 |
+
### 方案 2:手动启动
|
| 46 |
+
|
| 47 |
+
#### 步骤 1:确认在项目根目录
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# 检查当前目录
|
| 51 |
+
pwd
|
| 52 |
+
|
| 53 |
+
# 应该看到这些文件/目录
|
| 54 |
+
ls
|
| 55 |
+
# app/
|
| 56 |
+
# frontend/
|
| 57 |
+
# data/
|
| 58 |
+
# requirements.txt
|
| 59 |
+
# README.md
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
#### 步骤 2:激活虚拟环境(如果有)
|
| 63 |
+
|
| 64 |
+
**Windows:**
|
| 65 |
+
```bash
|
| 66 |
+
# CMD
|
| 67 |
+
venv\Scripts\activate.bat
|
| 68 |
+
|
| 69 |
+
# PowerShell
|
| 70 |
+
venv\Scripts\Activate.ps1
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
**Linux/Mac:**
|
| 74 |
+
```bash
|
| 75 |
+
source venv/bin/activate
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
#### 步骤 3:启动服务器
|
| 79 |
+
|
| 80 |
+
**使用 python -m(推荐):**
|
| 81 |
+
```bash
|
| 82 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**或者直接使用 uvicorn:**
|
| 86 |
+
```bash
|
| 87 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### 方案 3:不使用 reload 模式
|
| 91 |
+
|
| 92 |
+
如果 `--reload` 参数导致问题,可以不使用:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
**注意:** 不使用 reload 模式时,修改代码后需要手动重启服务器。
|
| 99 |
+
|
| 100 |
+
### 方案 4:设置 PYTHONPATH
|
| 101 |
+
|
| 102 |
+
如果上述方法都不行,手动设置 PYTHONPATH:
|
| 103 |
+
|
| 104 |
+
**Windows CMD:**
|
| 105 |
+
```bash
|
| 106 |
+
set PYTHONPATH=%CD%
|
| 107 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**PowerShell:**
|
| 111 |
+
```powershell
|
| 112 |
+
$env:PYTHONPATH = $PWD
|
| 113 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
**Linux/Mac:**
|
| 117 |
+
```bash
|
| 118 |
+
export PYTHONPATH=$PWD
|
| 119 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## 验证步骤
|
| 123 |
+
|
| 124 |
+
### 1. 检查目录结构
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
# 应该看到这个结构
|
| 128 |
+
项目根目录/
|
| 129 |
+
├── app/
|
| 130 |
+
│ ├── __init__.py
|
| 131 |
+
│ ├── main.py
|
| 132 |
+
│ ├── config.py
|
| 133 |
+
│ └── ...
|
| 134 |
+
├── frontend/
|
| 135 |
+
├── data/
|
| 136 |
+
└── requirements.txt
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### 2. 检查 Python 环境
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
# 检查 Python 版本
|
| 143 |
+
python --version
|
| 144 |
+
# 应该是 Python 3.8+
|
| 145 |
+
|
| 146 |
+
# 检查 uvicorn 是否安装
|
| 147 |
+
python -c "import uvicorn; print(uvicorn.__version__)"
|
| 148 |
+
# 应该显示版本号
|
| 149 |
+
|
| 150 |
+
# 检查 FastAPI 是否安装
|
| 151 |
+
python -c "import fastapi; print(fastapi.__version__)"
|
| 152 |
+
# 应该显示版本号
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
### 3. 测试导入
|
| 156 |
+
|
| 157 |
+
```bash
|
| 158 |
+
# 测试能否导入 app 模块
|
| 159 |
+
python -c "import app.main; print('OK')"
|
| 160 |
+
# 应该显示 OK
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
如果这一步失败,说明模块路径有问题。
|
| 164 |
+
|
| 165 |
+
### 4. 检查依赖
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
# 安装/更新依赖
|
| 169 |
+
pip install -r requirements.txt
|
| 170 |
+
|
| 171 |
+
# 或者单独安装
|
| 172 |
+
pip install fastapi uvicorn python-multipart httpx pydantic
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
## 常见错误和解决方法
|
| 176 |
+
|
| 177 |
+
### 错误 1:在 app/ 目录内运行
|
| 178 |
+
|
| 179 |
+
**错误操作:**
|
| 180 |
+
```bash
|
| 181 |
+
cd app
|
| 182 |
+
python -m uvicorn main:app --reload
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
**正确操作:**
|
| 186 |
+
```bash
|
| 187 |
+
# 回到项目根目录
|
| 188 |
+
cd ..
|
| 189 |
+
python -m uvicorn app.main:app --reload
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
### 错误 2:虚拟环境未激活
|
| 193 |
+
|
| 194 |
+
**症状:**
|
| 195 |
+
- 提示找不到 uvicorn
|
| 196 |
+
- 提示找不到 fastapi
|
| 197 |
+
|
| 198 |
+
**解决:**
|
| 199 |
+
```bash
|
| 200 |
+
# 激活虚拟环境
|
| 201 |
+
venv\Scripts\activate.bat # Windows CMD
|
| 202 |
+
venv\Scripts\Activate.ps1 # PowerShell
|
| 203 |
+
source venv/bin/activate # Linux/Mac
|
| 204 |
+
|
| 205 |
+
# 然后重新启动
|
| 206 |
+
python -m uvicorn app.main:app --reload
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
### 错误 3:端口被占用
|
| 210 |
+
|
| 211 |
+
**错误信息:**
|
| 212 |
+
```
|
| 213 |
+
OSError: [Errno 48] Address already in use
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
**解决方法:**
|
| 217 |
+
|
| 218 |
+
**方法 1:使用其他端口**
|
| 219 |
+
```bash
|
| 220 |
+
python -m uvicorn app.main:app --port 8001
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
**方法 2:关闭占用端口的进程**
|
| 224 |
+
|
| 225 |
+
Windows:
|
| 226 |
+
```bash
|
| 227 |
+
# 查找占用 8000 端口的进程
|
| 228 |
+
netstat -ano | findstr :8000
|
| 229 |
+
|
| 230 |
+
# 关闭进程(替换 PID)
|
| 231 |
+
taskkill /PID <PID> /F
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
Linux/Mac:
|
| 235 |
+
```bash
|
| 236 |
+
# 查找并关闭
|
| 237 |
+
lsof -ti:8000 | xargs kill -9
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### 错误 4:权限问题
|
| 241 |
+
|
| 242 |
+
**错误信息:**
|
| 243 |
+
```
|
| 244 |
+
PermissionError: [Errno 13] Permission denied
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
**解决方法:**
|
| 248 |
+
|
| 249 |
+
1. 以管理员身份运行
|
| 250 |
+
2. 检查文件权限
|
| 251 |
+
3. 使用其他端口(> 1024)
|
| 252 |
+
|
| 253 |
+
## 推荐的启动方式
|
| 254 |
+
|
| 255 |
+
### 开发环境
|
| 256 |
+
|
| 257 |
+
**方式 1:使用启动脚本**
|
| 258 |
+
```bash
|
| 259 |
+
# Windows
|
| 260 |
+
启动后端.bat
|
| 261 |
+
|
| 262 |
+
# PowerShell
|
| 263 |
+
.\启动后端.ps1
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
**方式 2:手动启动(带 reload)**
|
| 267 |
+
```bash
|
| 268 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
### 生产环境
|
| 272 |
+
|
| 273 |
+
**使用 gunicorn(Linux):**
|
| 274 |
+
```bash
|
| 275 |
+
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
**使用 uvicorn(Windows):**
|
| 279 |
+
```bash
|
| 280 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
## 调试技巧
|
| 284 |
+
|
| 285 |
+
### 1. 查看详细日志
|
| 286 |
+
|
| 287 |
+
```bash
|
| 288 |
+
python -m uvicorn app.main:app --log-level debug
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
### 2. 检查配置
|
| 292 |
+
|
| 293 |
+
```bash
|
| 294 |
+
# 查看环境变量
|
| 295 |
+
python -c "from app.config import get_config; print(get_config())"
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
### 3. 测试 API
|
| 299 |
+
|
| 300 |
+
```bash
|
| 301 |
+
# 启动后测试
|
| 302 |
+
curl http://localhost:8000/health
|
| 303 |
+
|
| 304 |
+
# 或在浏览器访问
|
| 305 |
+
http://localhost:8000/docs
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
## 完整的启动检查清单
|
| 309 |
+
|
| 310 |
+
- [ ] 在项目根目录(不是 app/ 目录)
|
| 311 |
+
- [ ] 虚拟环境已激活(如果使用)
|
| 312 |
+
- [ ] 依赖已安装(pip install -r requirements.txt)
|
| 313 |
+
- [ ] .env 文件已配置
|
| 314 |
+
- [ ] 端口 8000 未被占用
|
| 315 |
+
- [ ] Python 版本 >= 3.8
|
| 316 |
+
- [ ] 可以导入 app 模块(python -c "import app.main")
|
| 317 |
+
|
| 318 |
+
## 快速诊断命令
|
| 319 |
+
|
| 320 |
+
运行这个命令进行快速诊断:
|
| 321 |
+
|
| 322 |
+
```bash
|
| 323 |
+
python -c "
|
| 324 |
+
import sys
|
| 325 |
+
import os
|
| 326 |
+
print('Python 版本:', sys.version)
|
| 327 |
+
print('当前目录:', os.getcwd())
|
| 328 |
+
print('app 目录存在:', os.path.exists('app'))
|
| 329 |
+
print('main.py 存在:', os.path.exists('app/main.py'))
|
| 330 |
+
try:
|
| 331 |
+
import uvicorn
|
| 332 |
+
print('uvicorn 已安装:', uvicorn.__version__)
|
| 333 |
+
except:
|
| 334 |
+
print('uvicorn 未安装')
|
| 335 |
+
try:
|
| 336 |
+
import fastapi
|
| 337 |
+
print('fastapi 已安装:', fastapi.__version__)
|
| 338 |
+
except:
|
| 339 |
+
print('fastapi 未安装')
|
| 340 |
+
try:
|
| 341 |
+
import app.main
|
| 342 |
+
print('app.main 可导入: OK')
|
| 343 |
+
except Exception as e:
|
| 344 |
+
print('app.main 导入失败:', e)
|
| 345 |
+
"
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
## 总结
|
| 349 |
+
|
| 350 |
+
最常见的问题是**在错误的目录运行命令**。
|
| 351 |
+
|
| 352 |
+
**解决方法:**
|
| 353 |
+
1. 确保在项目根目录
|
| 354 |
+
2. 使用提供的启动脚本
|
| 355 |
+
3. 使用 `python -m uvicorn` 而不是直接 `uvicorn`
|
| 356 |
+
|
| 357 |
+
如果问题仍然存在,请:
|
| 358 |
+
1. 运行快速诊断命令
|
| 359 |
+
2. 检查完整的启动检查清单
|
| 360 |
+
3. 查看详细的错误日志
|
| 361 |
+
|
| 362 |
+
---
|
| 363 |
+
|
| 364 |
+
**需要帮助?** 请提供:
|
| 365 |
+
- 完整的错误信息
|
| 366 |
+
- 当前目录(pwd)
|
| 367 |
+
- Python 版本(python --version)
|
| 368 |
+
- 快速诊断命令的输出
|
docs/局域网访问修复完成.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ✅ 局域网访问问题已修复
|
| 2 |
+
|
| 3 |
+
## 🎯 问题描述
|
| 4 |
+
从其他设备访问 `http://172.18.16.245:8000/` 时显示 "Load failed"
|
| 5 |
+
|
| 6 |
+
## 🔧 已完成的修复
|
| 7 |
+
|
| 8 |
+
### 1. 移除硬编码的 API 地址
|
| 9 |
+
**问题**:`frontend/.env.local` 中设置了 `VITE_API_URL=http://localhost:8000`,导致前端构建时将这个地址写死在代码中。其他设备访问时会尝试连接到 `localhost:8000`(它们自己的设备),而不是你的服务器。
|
| 10 |
+
|
| 11 |
+
**修复**:
|
| 12 |
+
- ✅ 注释掉了 `frontend/.env.local` 中的 `VITE_API_URL`
|
| 13 |
+
- ✅ 前端现在会自动检测 API 地址:
|
| 14 |
+
- 本地访问 → `http://localhost:8000`
|
| 15 |
+
- 局域网访问 → `http://172.18.16.245:8000`
|
| 16 |
+
- 生产环境 → 自动使用当前域名
|
| 17 |
+
|
| 18 |
+
### 2. 重新构建前端
|
| 19 |
+
**操作**:
|
| 20 |
+
```bash
|
| 21 |
+
cd frontend
|
| 22 |
+
npm run build
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
**结果**:
|
| 26 |
+
- ✅ 新的构建文件已生成在 `frontend/dist/`
|
| 27 |
+
- ✅ 包含了自动 API 地址检测逻辑
|
| 28 |
+
|
| 29 |
+
### 3. 创建诊断工具
|
| 30 |
+
**新增文件**:
|
| 31 |
+
- ✅ `frontend/dist/test-connection.html` - 网络连接诊断页面
|
| 32 |
+
- ✅ `scripts/test_lan_access.bat` - 快速测试脚本
|
| 33 |
+
- ✅ `docs/局域网访问快速修复.md` - 详细修复指南
|
| 34 |
+
- ✅ `docs/局域网访问问题排查.md` - 完整排查步骤
|
| 35 |
+
|
| 36 |
+
## 🚀 立即测试
|
| 37 |
+
|
| 38 |
+
### 步骤 1:启动后端
|
| 39 |
+
在主机上运行:
|
| 40 |
+
```bash
|
| 41 |
+
python scripts/start_local.py
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### 步骤 2:运行诊断
|
| 45 |
+
在主机上运行:
|
| 46 |
+
```bash
|
| 47 |
+
scripts\test_lan_access.bat
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
这会:
|
| 51 |
+
- ✅ 检查后端服务是否运行
|
| 52 |
+
- ✅ 显示你的 IP 地址
|
| 53 |
+
- ✅ 测试 API 端点
|
| 54 |
+
- ✅ 检查防火墙状态
|
| 55 |
+
|
| 56 |
+
### 步骤 3:在其他设备上测试
|
| 57 |
+
|
| 58 |
+
#### 方法 1:访问诊断页面(推荐)
|
| 59 |
+
在其他设备的浏览器中打开:
|
| 60 |
+
```
|
| 61 |
+
http://172.18.16.245:8000/test-connection.html
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
点击 "🚀 开始测试" 按钮,查看所有 API 是否可以访问。
|
| 65 |
+
|
| 66 |
+
#### 方法 2:直接访问主应用
|
| 67 |
+
在其他设备的浏览器中打开:
|
| 68 |
+
```
|
| 69 |
+
http://172.18.16.245:8000/
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
应该可以正常加载并显示数据。
|
| 73 |
+
|
| 74 |
+
## 🔥 如果仍然失败:检查防火墙
|
| 75 |
+
|
| 76 |
+
### 最常见的原因:Windows 防火墙阻止端口 8000
|
| 77 |
+
|
| 78 |
+
#### 快速测试(临时关闭防火墙)
|
| 79 |
+
以管理员身份运行 PowerShell:
|
| 80 |
+
```powershell
|
| 81 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
然后在其他设备上重新访问。如果可以访问了,说明是防火墙问题。
|
| 85 |
+
|
| 86 |
+
#### 添加防火墙规则(推荐)
|
| 87 |
+
以管理员身份运行 PowerShell:
|
| 88 |
+
```powershell
|
| 89 |
+
New-NetFirewallRule -DisplayName "Python FastAPI 8000" -Direction Inbound -LocalPort 8000 -Protocol TCP -Action Allow
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
#### 重新启用防火墙
|
| 93 |
+
```powershell
|
| 94 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## 📱 移动设备访问
|
| 98 |
+
|
| 99 |
+
如果从手机访问:
|
| 100 |
+
1. ✅ 确保手机连接的是**同一个 WiFi 网络**(不是移动数据)
|
| 101 |
+
2. ✅ 在手机浏览器中输入:`http://172.18.16.245:8000/`
|
| 102 |
+
3. ✅ 如果无法访问,先访问诊断页面:`http://172.18.16.245:8000/test-connection.html`
|
| 103 |
+
|
| 104 |
+
## 🐛 使用浏览器开发者工具调试
|
| 105 |
+
|
| 106 |
+
如果仍然有问题,请:
|
| 107 |
+
1. 在其他设备的浏览器中按 **F12** 打开开发者工具
|
| 108 |
+
2. 切换到 **Console** 标签
|
| 109 |
+
3. 刷新页面
|
| 110 |
+
4. 查看错误信息并告诉我
|
| 111 |
+
|
| 112 |
+
**常见错误**:
|
| 113 |
+
- `Failed to fetch` → 网络连接问题(检查防火墙)
|
| 114 |
+
- `net::ERR_CONNECTION_REFUSED` → 端口未开放(检查后端是否运行)
|
| 115 |
+
- `net::ERR_CONNECTION_TIMED_OUT` → 连接超时(检查网络连接)
|
| 116 |
+
|
| 117 |
+
## ✅ 成功标志
|
| 118 |
+
|
| 119 |
+
当以下测试都通过时,说明配置正确:
|
| 120 |
+
|
| 121 |
+
1. ✅ 诊断页面所有测试都显示绿色 ✅
|
| 122 |
+
2. ✅ 主应用可以正常加载
|
| 123 |
+
3. ✅ 可以看到 AI 角色形象
|
| 124 |
+
4. ✅ 可以进行语音输入和文本输入
|
| 125 |
+
5. ✅ 可以查看心情、灵感、待办数据
|
| 126 |
+
|
| 127 |
+
## 📚 相关文档
|
| 128 |
+
|
| 129 |
+
- [局域网访问快速修复](docs/局域网访问快速修复.md) - 详细的修复步骤
|
| 130 |
+
- [局域网访问问题排查](docs/局域网访问问题排查.md) - 完整的排查指南
|
| 131 |
+
- [局域网访问指南](docs/局域网访问指南.md) - 配置说明
|
| 132 |
+
|
| 133 |
+
## 🆘 仍然无法解决?
|
| 134 |
+
|
| 135 |
+
请提供以下信息:
|
| 136 |
+
|
| 137 |
+
1. **诊断页面的测试结果**(截图)
|
| 138 |
+
2. **浏览器控制台的错误信息**(F12 → Console 标签,截图或文字)
|
| 139 |
+
3. **主机上的测试结果**:
|
| 140 |
+
```bash
|
| 141 |
+
curl http://localhost:8000/health
|
| 142 |
+
```
|
| 143 |
+
4. **其他设备上的测试**:
|
| 144 |
+
- 能否 ping 通主机:`ping 172.18.16.245`
|
| 145 |
+
- 访问健康检查:`http://172.18.16.245:8000/health`
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## 📝 技术说明
|
| 150 |
+
|
| 151 |
+
### 为什么会出现这个问题?
|
| 152 |
+
|
| 153 |
+
1. **Vite 的环境变量机制**:
|
| 154 |
+
- Vite 在构建时会将 `import.meta.env.VITE_*` 变量替换为实际值
|
| 155 |
+
- 如果设置了 `VITE_API_URL=http://localhost:8000`,构建后的代码会包含这个硬编码的地址
|
| 156 |
+
- 其他设备访问时,会尝试连接到 `localhost:8000`(它们自己的设备)
|
| 157 |
+
|
| 158 |
+
2. **解决方案**:
|
| 159 |
+
- 不设置 `VITE_API_URL`,让前端在运行时动态检测
|
| 160 |
+
- 使用 `window.location.hostname` 获取当前访问的主机名
|
| 161 |
+
- 根据主机名自动构建正确的 API 地址
|
| 162 |
+
|
| 163 |
+
### API 地址检测逻辑
|
| 164 |
+
|
| 165 |
+
```typescript
|
| 166 |
+
function getApiBaseUrl() {
|
| 167 |
+
const currentHost = window.location.hostname;
|
| 168 |
+
const currentProtocol = window.location.protocol;
|
| 169 |
+
|
| 170 |
+
// 生产环境(Hugging Face, ModelScope)
|
| 171 |
+
if (currentHost.includes('hf.space') ||
|
| 172 |
+
currentHost.includes('huggingface.co') ||
|
| 173 |
+
currentHost.includes('modelscope.cn')) {
|
| 174 |
+
return `${currentProtocol}//${currentHost}`;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// 局域网访问(如 192.168.x.x, 172.x.x.x)
|
| 178 |
+
if (currentHost !== 'localhost' && currentHost !== '127.0.0.1') {
|
| 179 |
+
return `${currentProtocol}//${currentHost}:8000`;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// 本地开发
|
| 183 |
+
return 'http://localhost:8000';
|
| 184 |
+
}
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
这样,无论从哪个地址访问,都能自动使用正确的 API 地址。
|
docs/局域网访问快速修复.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 局域网访问快速修复指南
|
| 2 |
+
|
| 3 |
+
## 问题:其他设备访问显示 "Load failed"
|
| 4 |
+
|
| 5 |
+
### ✅ 已完成的修复
|
| 6 |
+
|
| 7 |
+
1. **移除了硬编码的 API 地址**
|
| 8 |
+
- 修改了 `frontend/.env.local`,移除了 `VITE_API_URL=http://localhost:8000`
|
| 9 |
+
- 重新构建了前端,现在会自动检测 API 地址
|
| 10 |
+
|
| 11 |
+
2. **前端已重新构建**
|
| 12 |
+
- 运行了 `npm run build`
|
| 13 |
+
- 新的构建文件已生成在 `frontend/dist/`
|
| 14 |
+
|
| 15 |
+
3. **创建了诊断工具**
|
| 16 |
+
- 访问 `http://172.18.16.245:8000/test-connection.html` 可以测试连接
|
| 17 |
+
|
| 18 |
+
## 🚀 立即测试
|
| 19 |
+
|
| 20 |
+
### 步骤 1:启动后端服务
|
| 21 |
+
|
| 22 |
+
在主机上运行:
|
| 23 |
+
```bash
|
| 24 |
+
python scripts/start_local.py
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
确认看到:
|
| 28 |
+
```
|
| 29 |
+
============================================================
|
| 30 |
+
🌟 治愈系记录助手 - SoulMate AI Companion
|
| 31 |
+
============================================================
|
| 32 |
+
📍 本地访问: http://localhost:8000/
|
| 33 |
+
📍 局域网访问: http://172.18.16.245:8000/
|
| 34 |
+
============================================================
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 步骤 2:在其他设备上测试
|
| 38 |
+
|
| 39 |
+
#### 测试 1:访问诊断页面
|
| 40 |
+
在其他设备的浏览器中打开:
|
| 41 |
+
```
|
| 42 |
+
http://172.18.16.245:8000/test-connection.html
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
点击 "🚀 开始测试" 按钮,查看所有 API 是否可以访问。
|
| 46 |
+
|
| 47 |
+
#### 测试 2:访问主应用
|
| 48 |
+
在其他设备的浏览器中打开:
|
| 49 |
+
```
|
| 50 |
+
http://172.18.16.245:8000/
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
应该可以正常加载并显示数据。
|
| 54 |
+
|
| 55 |
+
## 🔧 如果仍然失败
|
| 56 |
+
|
| 57 |
+
### 方案 1:检查防火墙(最常见原因)
|
| 58 |
+
|
| 59 |
+
#### Windows 防火墙快速测试
|
| 60 |
+
1. 临时关闭防火墙测试(以管理员身份运行 PowerShell):
|
| 61 |
+
```powershell
|
| 62 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
2. 在其他设备上重新访问 `http://172.18.16.245:8000/`
|
| 66 |
+
|
| 67 |
+
3. 如果可以访问了,说明是防火墙问题,需要添加规则:
|
| 68 |
+
```powershell
|
| 69 |
+
# 允许 Python 通过防火墙
|
| 70 |
+
New-NetFirewallRule -DisplayName "Python FastAPI 8000" -Direction Inbound -LocalPort 8000 -Protocol TCP -Action Allow
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
4. 重新启用防火墙:
|
| 74 |
+
```powershell
|
| 75 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### 方案 2:检查网络连接
|
| 79 |
+
|
| 80 |
+
在其他设备上测试能否 ping 通主机:
|
| 81 |
+
```bash
|
| 82 |
+
ping 172.18.16.245
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
如果 ping 不通:
|
| 86 |
+
- 确认两台设备在同一 WiFi 网络
|
| 87 |
+
- 检查路由器是否启用了 AP 隔离(需要在路由器设置中关闭)
|
| 88 |
+
- 确认主机 IP 地址是否正确(可能已变化)
|
| 89 |
+
|
| 90 |
+
### 方案 3:检查 IP 地址
|
| 91 |
+
|
| 92 |
+
主机 IP 可能已经变化,重新获取:
|
| 93 |
+
```bash
|
| 94 |
+
# Windows
|
| 95 |
+
ipconfig
|
| 96 |
+
|
| 97 |
+
# 查找 "IPv4 地址",例如:192.168.1.100
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
使用新的 IP 地址访问。
|
| 101 |
+
|
| 102 |
+
### 方案 4:使用浏览器开发者工具
|
| 103 |
+
|
| 104 |
+
在其他设备的浏览器中:
|
| 105 |
+
1. 按 F12 打开开发者工具
|
| 106 |
+
2. 切换到 "Console" 标签
|
| 107 |
+
3. 刷新页面
|
| 108 |
+
4. 查看具体的错误信息
|
| 109 |
+
|
| 110 |
+
**常见错误及解决方案**:
|
| 111 |
+
|
| 112 |
+
| 错误信息 | 原因 | 解决方案 |
|
| 113 |
+
|---------|------|---------|
|
| 114 |
+
| `Failed to fetch` | 网络连接失败 | 检查防火墙和网络连接 |
|
| 115 |
+
| `net::ERR_CONNECTION_REFUSED` | 端口未开放 | 检查后端是否运行,防火墙是否允许 |
|
| 116 |
+
| `net::ERR_CONNECTION_TIMED_OUT` | 连接超时 | 检查网络连接,可能是路由器 AP 隔离 |
|
| 117 |
+
| `CORS error` | CORS 配置问题 | 已配置,不应该出现此错误 |
|
| 118 |
+
|
| 119 |
+
## 📱 移动设备特别说明
|
| 120 |
+
|
| 121 |
+
如果从手机访问:
|
| 122 |
+
1. 确保手机连接的是同一个 WiFi 网络(不是移动数据)
|
| 123 |
+
2. 某些公共 WiFi 可能禁止设备间通信
|
| 124 |
+
3. 可以尝试使用手机的浏览器访问诊断页面
|
| 125 |
+
|
| 126 |
+
## ✅ 成功标志
|
| 127 |
+
|
| 128 |
+
当以下测试都通过时,说明配置正确:
|
| 129 |
+
|
| 130 |
+
1. ✅ 诊断页面所有测试都显示绿色 ✅
|
| 131 |
+
2. ✅ 主应用可以正常加载
|
| 132 |
+
3. ✅ 可以看到 AI 角色形象
|
| 133 |
+
4. ✅ 可以进行语音输入和文本输入
|
| 134 |
+
5. ✅ 可以查看心情、灵感、待办数据
|
| 135 |
+
|
| 136 |
+
## 🆘 仍然无法解决?
|
| 137 |
+
|
| 138 |
+
请提供以下信息:
|
| 139 |
+
|
| 140 |
+
1. **诊断页面的测试结果**(截图)
|
| 141 |
+
2. **浏览器控制台的错误信息**(截图或文字)
|
| 142 |
+
3. **主机上运行的结果**:
|
| 143 |
+
```bash
|
| 144 |
+
curl http://localhost:8000/health
|
| 145 |
+
```
|
| 146 |
+
4. **其他设备上的测试结果**:
|
| 147 |
+
- 能否 ping 通主机
|
| 148 |
+
- 访问 `http://172.18.16.245:8000/health` 的结果
|
| 149 |
+
5. **防火墙状态**:
|
| 150 |
+
```powershell
|
| 151 |
+
netsh advfirewall show allprofiles state
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## 📚 相关文档
|
| 155 |
+
|
| 156 |
+
- [局域网访问问题排查](./局域网访问问题排查.md) - 详细的排查步骤
|
| 157 |
+
- [局域网访问指南](./局域网访问指南.md) - 完整的配置指南
|
docs/局域网访问指南.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 局域网访问指南
|
| 2 |
+
|
| 3 |
+
## 🌐 让其他设备访问你的应用
|
| 4 |
+
|
| 5 |
+
### 快速开始
|
| 6 |
+
|
| 7 |
+
#### 1. 启动本地服务器
|
| 8 |
+
|
| 9 |
+
**Windows:**
|
| 10 |
+
```bash
|
| 11 |
+
start_local.bat
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
**Linux/Mac:**
|
| 15 |
+
```bash
|
| 16 |
+
python start_local.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
服务器会显示:
|
| 20 |
+
```
|
| 21 |
+
============================================================
|
| 22 |
+
🌟 治愈系记录助手 - SoulMate AI Companion
|
| 23 |
+
============================================================
|
| 24 |
+
📍 本地访问: http://localhost:8000/
|
| 25 |
+
📍 局域网访问: http://172.18.16.245:8000/
|
| 26 |
+
📚 API 文档: http://localhost:8000/docs
|
| 27 |
+
🔍 健康检查: http://localhost:8000/health
|
| 28 |
+
============================================================
|
| 29 |
+
💡 提示: 其他设备可以通过 http://172.18.16.245:8000/ 访问
|
| 30 |
+
============================================================
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
#### 2. 其他设备访问
|
| 34 |
+
|
| 35 |
+
在同一局域网内的其他设备(手机、平板、其他电脑)上:
|
| 36 |
+
|
| 37 |
+
1. 打开浏览器
|
| 38 |
+
2. 访问:`http://172.18.16.245:8000/`(使用你的实际 IP 地址)
|
| 39 |
+
3. 开始使用!
|
| 40 |
+
|
| 41 |
+
### 前置要求
|
| 42 |
+
|
| 43 |
+
#### 1. 构建前端
|
| 44 |
+
|
| 45 |
+
首次使用前,需要构建前端:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
cd frontend
|
| 49 |
+
npm install
|
| 50 |
+
npm run build
|
| 51 |
+
cd ..
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
#### 2. 配置防火墙
|
| 55 |
+
|
| 56 |
+
**Windows 防火墙:**
|
| 57 |
+
|
| 58 |
+
1. 打开 Windows Defender 防火墙
|
| 59 |
+
2. 点击"高级设置"
|
| 60 |
+
3. 选择"入站规则" → "新建规则"
|
| 61 |
+
4. 选择"端口" → 下一步
|
| 62 |
+
5. 选择"TCP",特定本地端口:`8000`
|
| 63 |
+
6. 允许连接
|
| 64 |
+
7. 应用到所有配置文件
|
| 65 |
+
8. 命名规则:`SoulMate AI - Port 8000`
|
| 66 |
+
|
| 67 |
+
**或使用命令行(管理员权限):**
|
| 68 |
+
```powershell
|
| 69 |
+
netsh advfirewall firewall add rule name="SoulMate AI - Port 8000" dir=in action=allow protocol=TCP localport=8000
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
**Linux (ufw):**
|
| 73 |
+
```bash
|
| 74 |
+
sudo ufw allow 8000/tcp
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**Mac:**
|
| 78 |
+
系统偏好设置 → 安全性与隐私 → 防火墙 → 防火墙选项 → 添加应用
|
| 79 |
+
|
| 80 |
+
#### 3. 确保在同一网络
|
| 81 |
+
|
| 82 |
+
- 所有设备连接到同一个 WiFi 或局域网
|
| 83 |
+
- 检查路由器是否启用了 AP 隔离(如果启用需要关闭)
|
| 84 |
+
|
| 85 |
+
### 故障排查
|
| 86 |
+
|
| 87 |
+
#### 问题 1: 其他设备无法访问
|
| 88 |
+
|
| 89 |
+
**检查清单:**
|
| 90 |
+
|
| 91 |
+
1. ✅ 服务器是否正在运行?
|
| 92 |
+
```bash
|
| 93 |
+
# 应该看到服务器日志
|
| 94 |
+
INFO: Uvicorn running on http://0.0.0.0:8000
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
2. ✅ 防火墙是否允许端口 8000?
|
| 98 |
+
```bash
|
| 99 |
+
# Windows: 测试端口
|
| 100 |
+
Test-NetConnection -ComputerName 172.18.16.245 -Port 8000
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
3. ✅ 设备是否在同一网络?
|
| 104 |
+
```bash
|
| 105 |
+
# 从其他设备 ping 服务器
|
| 106 |
+
ping 172.18.16.245
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
4. ✅ IP 地址是否正确?
|
| 110 |
+
```bash
|
| 111 |
+
# Windows: 查看 IP
|
| 112 |
+
ipconfig
|
| 113 |
+
|
| 114 |
+
# Linux/Mac: 查看 IP
|
| 115 |
+
ifconfig
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
#### 问题 2: API 调用失败
|
| 119 |
+
|
| 120 |
+
**检查浏览器控制台:**
|
| 121 |
+
|
| 122 |
+
1. 打开开发者工具(F12)
|
| 123 |
+
2. 查看 Console 标签
|
| 124 |
+
3. 应该看到:`🔗 API Base URL: http://172.18.16.245:8000`
|
| 125 |
+
4. 如果不正确,清除浏览器缓存并刷新
|
| 126 |
+
|
| 127 |
+
**测试 API 连接:**
|
| 128 |
+
|
| 129 |
+
访问:`http://172.18.16.245:8000/health`
|
| 130 |
+
|
| 131 |
+
应该返回:
|
| 132 |
+
```json
|
| 133 |
+
{
|
| 134 |
+
"status": "healthy",
|
| 135 |
+
"data_dir": "data",
|
| 136 |
+
"max_audio_size": 10485760
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
#### 问题 3: 没有显示默认形象
|
| 141 |
+
|
| 142 |
+
**检查:**
|
| 143 |
+
|
| 144 |
+
1. ✅ 默认形象文件是否存在?
|
| 145 |
+
```bash
|
| 146 |
+
# 应该存在这个文件
|
| 147 |
+
generated_images/default_character.jpeg
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
2. ✅ 用户配置是否正确?
|
| 151 |
+
```bash
|
| 152 |
+
# 查看配置文件
|
| 153 |
+
cat data/user_config.json
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
3. ✅ 图片 URL 是否正确?
|
| 157 |
+
- 访问:`http://172.18.16.245:8000/api/user/config`
|
| 158 |
+
- 检查 `character.image_url` 字段
|
| 159 |
+
|
| 160 |
+
4. ✅ 图片是否可访问?
|
| 161 |
+
- 访问:`http://172.18.16.245:8000/generated_images/default_character.jpeg`
|
| 162 |
+
- 应该能看到图片
|
| 163 |
+
|
| 164 |
+
### 性能优化
|
| 165 |
+
|
| 166 |
+
#### 1. 使用有线连接
|
| 167 |
+
|
| 168 |
+
- 服务器电脑使用网线连接路由器
|
| 169 |
+
- 减少 WiFi 干扰和延迟
|
| 170 |
+
|
| 171 |
+
#### 2. 关闭不必要的应用
|
| 172 |
+
|
| 173 |
+
- 释放 CPU 和内存资源
|
| 174 |
+
- 提高响应速度
|
| 175 |
+
|
| 176 |
+
#### 3. 使用现代浏览器
|
| 177 |
+
|
| 178 |
+
- Chrome 90+
|
| 179 |
+
- Firefox 88+
|
| 180 |
+
- Safari 14+
|
| 181 |
+
- Edge 90+
|
| 182 |
+
|
| 183 |
+
### 安全建议
|
| 184 |
+
|
| 185 |
+
⚠️ **注意:** 局域网访问仅适用于受信任的网络环境
|
| 186 |
+
|
| 187 |
+
1. **不要在公共 WiFi 上使用**
|
| 188 |
+
2. **定期更新 API 密钥**
|
| 189 |
+
3. **不要暴露到公网**
|
| 190 |
+
4. **使用强密码保护路由器**
|
| 191 |
+
|
| 192 |
+
### 高级配置
|
| 193 |
+
|
| 194 |
+
#### 自定义端口
|
| 195 |
+
|
| 196 |
+
编辑 `start_local.py`,修改端口号:
|
| 197 |
+
|
| 198 |
+
```python
|
| 199 |
+
uvicorn.run(
|
| 200 |
+
app,
|
| 201 |
+
host="0.0.0.0",
|
| 202 |
+
port=8888, # 改为你想要的端口
|
| 203 |
+
log_level="info"
|
| 204 |
+
)
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
同时需要修改防火墙规则允许新端口。
|
| 208 |
+
|
| 209 |
+
#### 使用环境变量
|
| 210 |
+
|
| 211 |
+
创建 `.env.local` 文件:
|
| 212 |
+
|
| 213 |
+
```env
|
| 214 |
+
HOST=0.0.0.0
|
| 215 |
+
PORT=8000
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
### 常见使用场景
|
| 219 |
+
|
| 220 |
+
#### 场景 1: 手机访问电脑上的应用
|
| 221 |
+
|
| 222 |
+
1. 电脑运行 `start_local.bat`
|
| 223 |
+
2. 手机连接同一 WiFi
|
| 224 |
+
3. 手机浏览器访问 `http://172.18.16.245:8000/`
|
| 225 |
+
4. 可以语音输入、查看心情、与 AI 对话
|
| 226 |
+
|
| 227 |
+
#### 场景 2: 平板作为展示屏
|
| 228 |
+
|
| 229 |
+
1. 电脑运行服务器
|
| 230 |
+
2. 平板访问应用
|
| 231 |
+
3. 全屏显示心情气泡池
|
| 232 |
+
4. 作为情绪可视化展示
|
| 233 |
+
|
| 234 |
+
#### 场景 3: 多人协作
|
| 235 |
+
|
| 236 |
+
1. 一台电脑运行服务器
|
| 237 |
+
2. 团队成员通过局域网访问
|
| 238 |
+
3. 共享灵感和待办事项
|
| 239 |
+
4. 实时同步数据
|
| 240 |
+
|
| 241 |
+
### 技术细节
|
| 242 |
+
|
| 243 |
+
#### API 地址自动检测
|
| 244 |
+
|
| 245 |
+
前端会自动检测访问地址并配置 API:
|
| 246 |
+
|
| 247 |
+
```typescript
|
| 248 |
+
// 访问: http://172.18.16.245:5173/
|
| 249 |
+
// API: http://172.18.16.245:8000/
|
| 250 |
+
|
| 251 |
+
// 访问: http://localhost:5173/
|
| 252 |
+
// API: http://localhost:8000/
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
#### CORS 配置
|
| 256 |
+
|
| 257 |
+
后端已配置允许所有来源(开发环境):
|
| 258 |
+
|
| 259 |
+
```python
|
| 260 |
+
app.add_middleware(
|
| 261 |
+
CORSMiddleware,
|
| 262 |
+
allow_origins=["*"],
|
| 263 |
+
allow_credentials=True,
|
| 264 |
+
allow_methods=["*"],
|
| 265 |
+
allow_headers=["*"],
|
| 266 |
+
)
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
生产环境应该限制具体的域名。
|
docs/局域网访问问题排查.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 局域网访问问题排查指南
|
| 2 |
+
|
| 3 |
+
## 问题描述
|
| 4 |
+
从其他设备访问 `http://172.18.16.245:8000/` 时显示 "Load failed"
|
| 5 |
+
|
| 6 |
+
## 排查步骤
|
| 7 |
+
|
| 8 |
+
### 1. 确认后端服务正在运行
|
| 9 |
+
在主机上运行:
|
| 10 |
+
```bash
|
| 11 |
+
python scripts/start_local.py
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
确认看到以下输出:
|
| 15 |
+
```
|
| 16 |
+
============================================================
|
| 17 |
+
🌟 治愈系记录助手 - SoulMate AI Companion
|
| 18 |
+
============================================================
|
| 19 |
+
📍 本地访问: http://localhost:8000/
|
| 20 |
+
📍 局域网访问: http://172.18.16.245:8000/
|
| 21 |
+
📚 API 文档: http://localhost:8000/docs
|
| 22 |
+
🔍 健康检查: http://localhost:8000/health
|
| 23 |
+
============================================================
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### 2. 测试后端 API 是否可访问
|
| 27 |
+
|
| 28 |
+
#### 在主机上测试(应该成功):
|
| 29 |
+
```bash
|
| 30 |
+
curl http://localhost:8000/health
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
#### 在其他设备上测试(关键):
|
| 34 |
+
打开浏览器访问:
|
| 35 |
+
```
|
| 36 |
+
http://172.18.16.245:8000/health
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
**预期结果**:应该看到 JSON 响应
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"status": "healthy",
|
| 43 |
+
"data_dir": "data",
|
| 44 |
+
"max_audio_size": 10485760
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
**如果失败**:说明网络连接有问题,继续下一步
|
| 49 |
+
|
| 50 |
+
### 3. 检查防火墙设置
|
| 51 |
+
|
| 52 |
+
#### Windows 防火墙
|
| 53 |
+
1. 打开 "Windows Defender 防火墙"
|
| 54 |
+
2. 点击 "允许应用通过防火墙"
|
| 55 |
+
3. 确保 Python 已被允许(专用网络和公用网络都勾选)
|
| 56 |
+
|
| 57 |
+
#### 或者临时关闭防火墙测试:
|
| 58 |
+
```powershell
|
| 59 |
+
# 以管理员身份运行 PowerShell
|
| 60 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
测试完成后记得重新开启:
|
| 64 |
+
```powershell
|
| 65 |
+
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### 4. 检查网络连接
|
| 69 |
+
|
| 70 |
+
#### 在其他设备上 ping 主机:
|
| 71 |
+
```bash
|
| 72 |
+
ping 172.18.16.245
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**预期结果**:应该能 ping 通
|
| 76 |
+
|
| 77 |
+
**如果失败**:
|
| 78 |
+
- 确认两台设备在同一局域网
|
| 79 |
+
- 检查主机的 IP 地址是否正确(可能已变化)
|
| 80 |
+
|
| 81 |
+
#### 获取当前 IP 地址:
|
| 82 |
+
```bash
|
| 83 |
+
# Windows
|
| 84 |
+
ipconfig
|
| 85 |
+
|
| 86 |
+
# 查找 "IPv4 地址" 或 "IPv4 Address"
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 5. 检查端口是否被占用
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
# Windows
|
| 93 |
+
netstat -ano | findstr :8000
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
如果端口被占用,更换端口或关闭占用端口的程序
|
| 97 |
+
|
| 98 |
+
### 6. 浏览器控制台检查
|
| 99 |
+
|
| 100 |
+
在其他设备的浏览器中:
|
| 101 |
+
1. 按 F12 打开开发者工具
|
| 102 |
+
2. 切换到 "Console" 标签
|
| 103 |
+
3. 刷新页面
|
| 104 |
+
4. 查看错误信息
|
| 105 |
+
|
| 106 |
+
**常见错误**:
|
| 107 |
+
- `Failed to fetch`: 网络连接问题
|
| 108 |
+
- `CORS error`: CORS 配置问题(但我们已经配置了)
|
| 109 |
+
- `404 Not Found`: API 路径错误
|
| 110 |
+
- `Timeout`: 请求超时
|
| 111 |
+
|
| 112 |
+
### 7. 测试 API 端点
|
| 113 |
+
|
| 114 |
+
在其他设备的浏览器中依次访问:
|
| 115 |
+
|
| 116 |
+
1. **健康检查**:`http://172.18.16.245:8000/health`
|
| 117 |
+
2. **API 状态**:`http://172.18.16.245:8000/api/status`
|
| 118 |
+
3. **前端页面**:`http://172.18.16.245:8000/`
|
| 119 |
+
|
| 120 |
+
记录每个请求的结果
|
| 121 |
+
|
| 122 |
+
### 8. 检查 CORS 配置
|
| 123 |
+
|
| 124 |
+
后端已配置允许所有来源:
|
| 125 |
+
```python
|
| 126 |
+
allow_origins=["*"]
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
但如果仍有问题,可以尝试更明确的配置:
|
| 130 |
+
```python
|
| 131 |
+
allow_origins=[
|
| 132 |
+
"http://localhost:5173",
|
| 133 |
+
"http://localhost:3000",
|
| 134 |
+
"http://172.18.16.245:5173",
|
| 135 |
+
"http://172.18.16.245:8000",
|
| 136 |
+
"*"
|
| 137 |
+
]
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## 快速诊断命令
|
| 141 |
+
|
| 142 |
+
在主机上运行以下命令,将结果发给我:
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
# 1. 检查服务是否运行
|
| 146 |
+
curl http://localhost:8000/health
|
| 147 |
+
|
| 148 |
+
# 2. 检查端口监听
|
| 149 |
+
netstat -ano | findstr :8000
|
| 150 |
+
|
| 151 |
+
# 3. 检查 IP 地址
|
| 152 |
+
ipconfig | findstr IPv4
|
| 153 |
+
|
| 154 |
+
# 4. 检查防火墙状态
|
| 155 |
+
netsh advfirewall show allprofiles state
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## 解决方案
|
| 159 |
+
|
| 160 |
+
### 方案 1:临时关闭防火墙测试
|
| 161 |
+
如果关闭防火墙后可以访问,说明是防火墙问题,需要添加防火墙规则
|
| 162 |
+
|
| 163 |
+
### 方案 2:添加防火墙规则
|
| 164 |
+
```powershell
|
| 165 |
+
# 以管理员身份运行
|
| 166 |
+
New-NetFirewallRule -DisplayName "Python FastAPI" -Direction Inbound -Program "C:\Path\To\Python\python.exe" -Action Allow
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
### 方案 3:使用不同的端口
|
| 170 |
+
如果 8000 端口有问题,可以尝试其他端口(如 8080, 5000)
|
| 171 |
+
|
| 172 |
+
修改 `scripts/start_local.py` 中的端口号
|
| 173 |
+
|
| 174 |
+
### 方案 4:检查路由器设置
|
| 175 |
+
某些路由器可能阻止设备间通信(AP 隔离),需要在路由器设置中关闭
|
| 176 |
+
|
| 177 |
+
## 成功标志
|
| 178 |
+
|
| 179 |
+
当以下所有测试都通过时,说明配置正确:
|
| 180 |
+
|
| 181 |
+
1. ✅ 主机可以访问 `http://localhost:8000/health`
|
| 182 |
+
2. ✅ 其他设备可以 ping 通 `172.18.16.245`
|
| 183 |
+
3. ✅ 其他设备可以访问 `http://172.18.16.245:8000/health`
|
| 184 |
+
4. ✅ 其他设备可以访问 `http://172.18.16.245:8000/`
|
| 185 |
+
5. ✅ 前端可以正常加载并显示数据
|
| 186 |
+
|
| 187 |
+
## 需要提供的信息
|
| 188 |
+
|
| 189 |
+
如果问题仍未解决,请提供:
|
| 190 |
+
|
| 191 |
+
1. 浏览器控制台的完整错误信息(截图或文字)
|
| 192 |
+
2. 主机上运行 `curl http://localhost:8000/health` 的结果
|
| 193 |
+
3. 其他设备上访问 `http://172.18.16.245:8000/health` 的结果
|
| 194 |
+
4. 防火墙状态
|
| 195 |
+
5. 两台设备是否在同一局域网
|