diff --git a/AGENTS.md b/AGENTS.md index ec68605..b26dc0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,10 +27,10 @@ meijiaka-zj/ ├── python-api/ # FastAPI 后端服务(AI 代理 + 认证 + 任务调度) │ ├── app/ -│ │ ├── api/v1/ # API 路由: auth, system, tasks, script, caption, voice, upload, vidu +│ │ ├── api/v1/ # API 路由: auth, system, tasks, script, caption, voice, upload, vidu, materials │ │ ├── ai/ # AI 模型路由、Provider、提示词模板 │ │ ├── core/ # 安全、配置加载、Token管理器、Redis客户端、异常处理 -│ │ ├── crud/ # 数据访问层(users, avatar) +│ │ ├── crud/ # 数据访问层(users) │ │ ├── db/ # 数据库配置(PostgreSQL + asyncpg + SQLAlchemy 2.0) │ │ ├── models/ # SQLAlchemy 模型(当前仅 users) │ │ ├── schemas/ # Pydantic 校验模型 @@ -39,10 +39,9 @@ meijiaka-zj/ │ │ ├── config.py # Pydantic Settings 配置管理 │ │ └── main.py # FastAPI 入口(含生命周期管理) │ ├── config/ # AI 模型配置文件(ai_models.yaml),支持热重载 -│ ├── alembic/ # 数据库迁移(当前无迁移文件,dev 模式自动建表) -│ ├── tests/ # 测试目录(当前仅 test_kling_tts.py) +│ ├── alembic/ # 数据库迁移 │ ├── pyproject.toml # Python 依赖和工具配置 -│ ├── requirements.lock # uv 锁定依赖版本(如缺失需执行 make update-lock) +│ ├── requirements.lock # uv 锁定依赖版本 │ ├── Makefile # 常用命令封装 │ ├── docker-compose.yml │ ├── Dockerfile @@ -82,7 +81,6 @@ meijiaka-zj/ │ │ │ │ ├── asset.rs │ │ │ │ ├── auth_state.rs │ │ │ │ ├── avatar.rs -│ │ │ │ ├── avatar_material.rs │ │ │ │ ├── product.rs │ │ │ │ ├── project.rs │ │ │ │ ├── video_compose.rs @@ -94,7 +92,6 @@ meijiaka-zj/ │ │ │ ├── project.rs │ │ │ ├── auth.rs │ │ │ ├── avatar.rs -│ │ │ ├── avatar_material.rs │ │ │ ├── voice.rs │ │ │ └── cache.rs │ │ ├── Cargo.toml @@ -110,13 +107,14 @@ meijiaka-zj/ │ └── .stylelintrc.json │ ├── docs/ # 项目文档(设计文档、API 参考、实施计划) -├── scripts/ # 数据修复/迁移脚本(非部署脚本) ├── package.json # 根级 package.json(monorepo 占位) ├── .python-version # 3.13 ├── CLAUDE.md # 开发者速查手册 └── AGENTS.md # 本文件 ``` +> **基础设施位置**:共享 PostgreSQL + Redis 在 **`/Users/0fun/work/docker-infra/`**(与项目平级,不在本项目内),必须先启动。 + ## 技术栈详解 ### 后端 (python-api) @@ -139,10 +137,10 @@ meijiaka-zj/ **后端架构说明**: - 后端为"轻量云账号 + 全本地业务数据"模式 -- 云端仅存储:用户账户信息 +- 云端仅存储:用户账户信息(`users` 表) - 业务数据(项目/脚本/媒体/成品)全部本地存储 - 任务调度使用**自定义 Async Engine**(基于 Redis 的槽位管理),**非 Celery** -- 当前活跃 API 模块:**8 个**:`auth`, `system`, `tasks`, `script`, `caption`, `voice`, `upload`, `vidu` +- 当前活跃 API 模块:**9 个**:`auth`, `system`, `tasks`, `script`, `caption`, `voice`, `upload`, `vidu`, `materials` - 调度器已注册 7 个 Handler:`Video`, `Avatar`, `Image`, `Subtitle`, `Copy`, `Script`, `TTS` ### 前端 (tauri-app) @@ -181,44 +179,78 @@ meijiaka-zj/ | `commands/voice.rs` | 音频文件保存/列表/删除 IPC 命令 | | `commands/product.rs` | 成品视频保存/列表/删除/重命名 IPC 命令 | | `commands/avatar.rs` | 头像列表本地存储 | -| `commands/avatar_material.rs` | 人物出镜素材 CRUD | | `commands/video_compose.rs` | 视频拼接、文件上传/下载 | ## 开发环境搭建 ### 1. 启动 Python 后端 +**必须分两步启动**:先启动共享基础设施(db/redis),再启动本项目 API + Scheduler。 + +#### 第 1 步:启动共享基础设施(仅需一次,保持运行) + ```bash -cd python-api - -# 方式一:Docker Compose(推荐) -cp .env.example .env -# 编辑 .env 填入实际的 AI API Keys +cd /Users/0fun/work/docker-infra docker-compose up -d - -# 方式二:本地开发(若 Docker 不可用) -# 启动 PostgreSQL 和 Redis -docker-compose up -d db redis - -# 安装依赖(使用 uv) -uv pip install -e ".[dev]" - -# 启动开发服务器(Docker API 会占用 8081 端口) -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - -# 另开终端启动 Async Engine Scheduler(必须同时启动,否则任务不会执行) -python -m app.scheduler.main ``` +这会创建: +- `meijiaka-db`(PostgreSQL 15,端口 5432) +- `meijiaka-redis`(Redis 7,端口 6379) +- 共享 Docker 网络 `meijiaka-network` +- 自动初始化数据库 `meijiaka_zj` + +#### 第 2 步:启动智剪后端(API + Scheduler) + +```bash +cd /Users/0fun/work/meijiaka-zj/python-api + +# 首次需准备环境变量 +cp .env.example .env +# 编辑 .env 填入实际的 AI API Keys + +# 启动 API + Scheduler(必须加 -p 参数,避免与其他项目冲突) +docker-compose -p meijiaka-zj up -d +``` + +> ⚠️ **必须加 `-p meijiaka-zj`**,否则容器名会和「智影」项目冲突。 + +启动后会得到 2 个容器: +- `meijiaka-zj-api` → API 服务,端口 **8081**→8000 +- `meijiaka-zj-scheduler` → Async Engine 调度器 + 后端服务地址: -- API: http://localhost:8081/api/v1(Docker 映射端口) +- API: http://localhost:8081/api/v1 - 文档: http://localhost:8081/docs - 健康检查: http://localhost:8081/health -**Docker Compose 服务组成**(2 个服务 + 外部网络依赖): -- `api`: FastAPI 开发服务器(端口 **8081**→8000) -- `scheduler`: Async Engine 统一调度器,处理所有第三方异步任务 -- 依赖外部网络 `meijiaka-network` 和外部 PostgreSQL/Redis 服务 +**Docker Compose 服务组成**: +| 容器 | 用途 | 映射端口 | +|------|------|----------| +| `meijiaka-zj-api` | FastAPI 开发服务器 | **8081**→8000 | +| `meijiaka-zj-scheduler` | Async Engine 统一调度器 | 无 | + +**关键配置(以 `docker-compose.yml` 为准)**: +- 数据库:`meijiaka_zj`(通过 `meijiaka-db` 连接) +- Redis DB:`1` +- 依赖外部网络 `meijiaka-network` + +**验证启动**: +```bash +docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +``` +预期看到 4 个容器:`meijiaka-db`、`meijiaka-redis`、`meijiaka-zj-api`、`meijiaka-zj-scheduler`。 + +**停止服务**: +```bash +# 停止智剪(保留基础设施) +cd /Users/0fun/work/meijiaka-zj/python-api +docker-compose -p meijiaka-zj down + +# 停止基础设施(通常保持运行) +cd /Users/0fun/work/docker-infra +docker-compose down +``` ### 2. 启动 Tauri 前端 @@ -248,13 +280,13 @@ make dev # 安装开发依赖 + pre-commit 钩子 make lint # ruff + mypy make format # black + ruff --fix make format-check # 检查格式但不修改 -make test # pytest -make test-cov # 覆盖率报告 +make test # pytest(当前无测试文件) +make test-cov # 覆盖率报告(当前无测试文件) make security # bandit + pip-audit make lint-semantic # 语义层禁词检查 -make ci # 运行所有 CI 检查(format-check + lint + lint-semantic + test + security) -make docker-run # Docker Compose 启动全部服务 -make scheduler # 启动 Async Engine Scheduler +make ci # 运行所有 CI 检查(format-check + lint + lint-semantic + security) +make docker-run # Docker Compose 启动 api + scheduler(需先启动 docker-infra) +make scheduler # 本地启动 Async Engine Scheduler(不推荐,优先用 Docker) make clean # 清理缓存文件 # 手动命令 @@ -326,16 +358,16 @@ npm run gen:api # 从 OpenAPI 生成 TypeScript 类型 - `replace_audio_track` // 音频替换 - `mix_audio_tracks` // 音频混音 - `standardize_audio` // 音频标准化 -- `compose_video` // 视频拼接(Phase 2) +- `compose_video` // 视频拼接 - `save_project_meta*` / `load_project_meta*` // 本地文件系统 - `save_project_segments*` / `load_project_segments*` - `save_project_asset` / `get_video_save_path` / `get_image_save_path` - `save_final_product` / `list_local_products` / `delete_local_product` / `rename_local_product` - `save_audio` / `list_project_audios` / `delete_audio` - `load_voice_materials` / `save_voice_material` / `delete_voice_material_cmd` -- `load_avatar_materials` / `upload_avatar_material` / `rename_avatar_material` / `delete_avatar_material` -- `query_avatar_cache` / `cache_avatar_video` / `save_avatar_poster` -- 头像缓存相关 API +- `load_avatars_list` / `save_avatars_list` +- `query_avatar_cache` / `cache_avatar_video` / `save_avatar_poster` / `delete_avatar_cache` / `get_cache_stats` +- 认证状态相关:`load_auth_state` / `save_auth_state` / `clear_auth_state` **添加新 API 流程**: 1. Python 端实现端点 @@ -444,7 +476,6 @@ Rust 层实现了 defense-in-depth 的本地存储系统:`src-tauri/src/storag - `~/Documents/Meijiaka-zj/products/` - `~/Documents/Meijiaka-zj/avatars.json` - `~/Documents/Meijiaka-zj/voices.json` - - `~/Documents/Meijiaka-zj/avatar_materials/` - `{app_config_dir}/auth.json` - `{app_data_dir}/avatars/` (缓存) @@ -458,7 +489,12 @@ Rust 层实现了 defense-in-depth 的本地存储系统:`src-tauri/src/storag users -- 用户账户信息(mobile, nickname, avatar_url) ``` -> 注:`avatars` 和 `model_usage_logs` 相关模型及 CRUD 在最近的重构中已移除或清理。`crud/avatar.py` 仍存在但可能处于未使用状态。 +**BaseModel 提供字段**: +- `id`: UUID 主键(字符串格式) +- `created_at`: 创建时间(UTC) +- `updated_at`: 更新时间(UTC) + +注意:当前 BaseModel **不包含软删除字段** (`deleted_at`)。 **业务数据本地存储**: - 项目/脚本/分镜 → 前端本地 JSON 文件(`~/Documents/Meijiaka-zj/projects/`) @@ -490,9 +526,7 @@ users -- 用户账户信息(mobile, nickname, avatar_url) │ └── images/ # 图片素材 ├── products/ # 成品视频目录 ├── avatars.json # 形象列表(本地) -├── voices.json # 私有音色素材库 -├── avatar_materials/ # 人物出镜视频素材 -└── avatar_materials_index.json # 人物出镜素材索引 +└── voices.json # 私有音色素材库 ``` 项目元数据 `meta.json` 关键字段: @@ -522,7 +556,7 @@ users -- 用户账户信息(mobile, nickname, avatar_url) ### 状态管理 -七个专门的 Zustand store: +八个专门的 Zustand store: | Store | 职责 | 持久化 | |-------|------|--------| @@ -540,7 +574,7 @@ users -- 用户账户信息(mobile, nickname, avatar_url) ### 核心原则 -1. **后端环境优先使用 Docker Compose**: 开发时通过 `docker-compose up -d` 启动后端。前端默认连接 `http://127.0.0.1:8081/api/v1`。 +1. **后端环境优先使用 Docker Compose**: 先启动共享基础设施 `docker-infra`,再启动本项目 `docker-compose -p meijiaka-zj up -d`。前端默认连接 `http://127.0.0.1:8081/api/v1`。 2. **接口契约优先**: 后端承诺无论使用什么 AI 模型,输出永远符合同一个 Schema 3. **类型单一来源**: 后端 Schema 是权威,前端通过 OpenAPI 生成类型 4. **Adapter 层隔离**: 前后端字段差异只允许在 Adapter 层处理 @@ -706,7 +740,7 @@ pytest --cov=app --cov-report=html --cov-report=term - 测试文件命名: `test_*.py` - 测试路径: `tests/` -> **注**:当前后端测试覆盖非常有限,仅 `tests/test_kling_tts.py` 一个测试文件,包含 `TTSService`、`VoiceCloneService` 和状态枚举的单元测试。大量模块暂无测试。 +> **注**:当前后端**暂无测试文件**。`pyproject.toml` 已配置测试环境,但 `tests/` 目录尚未创建。 ### 前端测试 @@ -752,12 +786,12 @@ npm run test:coverage ```bash # 数据库 (PostgreSQL) -DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/meijiaka +DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/meijiaka_zj # Redis REDIS_HOST=localhost REDIS_PORT=6379 -REDIS_DB=0 +REDIS_DB=1 # JWT 密钥(生产环境必须修改) SECRET_KEY=your-secret-key-here-change-in-production @@ -769,108 +803,58 @@ VOLCENGINE_CAPTION_APPID=your-caption-appid VOLCENGINE_CAPTION_TOKEN=your-caption-token KLINGAI_ACCESS_KEY=your-kling-access-key KLINGAI_SECRET_KEY=your-kling-secret-key -MINIMAX_API_KEY=your-minimax-key -VIDU_API_KEY=your-vidu-key -OPENAI_API_KEY=sk-your-openai-key +MINIMAX_API_KEY=sk-api-your-minimax-key +VIDU_API_KEY=your-vidu-api-key # 七牛云存储 -QINIU_ACCESS_KEY=your-qiniu-access-key -QINIU_SECRET_KEY=your-qiniu-secret-key -QINIU_VIDEO_BUCKET=media-liche -QINIU_IMAGE_BUCKET=img-liche +QINIU_ACCESS_KEY=your-qiniu-ak +QINIU_SECRET_KEY=your-qiniu-sk # AnyToCopy 文案提取 ANYTOCOPY_API_KEY=your-anytocopy-key ANYTOCOPY_API_SECRET=your-anytocopy-secret - -# CORS 允许的前端地址 -CORS_ORIGINS=http://localhost:1420,http://127.0.0.1:1420,http://localhost:8081 ``` -### Tauri 配置 (tauri.conf.json) +完整环境变量模板见 `python-api/.env.example`。 -```json -{ - "productName": "美家卡智剪", - "identifier": "cn.meijiaka.ai-jian", - "build": { - "devUrl": "http://localhost:1420", - "frontendDist": "../dist" - }, - "bundle": { - "externalBin": ["binaries/ffmpeg", "binaries/ffprobe"], - "resources": { - "fonts/*": "fonts/" - } - } -} -``` +### Tauri 前端配置 -### AI 模型配置 (config/ai_models.yaml) +- **产品标识**: `cn.meijiaka.ai-jian` +- **版本**: `0.1.0` +- **窗口**: 1440×960,最小 960×640,可调整大小 +- **Vite 端口**: 1420(`strictPort: true`) +- **Python API 地址**: `http://127.0.0.1:8081/api/v1` +- **Asset Protocol**: 已启用,允许访问 `$APPLOCALDATA/**`, `$APPDATA/**`, `$APPCONFIG/**`, `/**` -模型配置文件支持热重载,无需重启服务即可更新模型配置。主要配置项: +## 关键文件速查 -- **platforms**: AI 平台配置(mock, volcengine, klingai, minimax, vidu) -- **models**: 可用模型列表及其能力标签 [script, polish, chat, image, video_generation, image2video, lip_sync, image_generation] -- **task_defaults**: 任务类型到模型的默认映射 +| 文件 | 用途 | +|------|------| +| `python-api/app/main.py` | FastAPI 应用入口 | +| `python-api/app/config.py` | Pydantic Settings 配置管理 | +| `python-api/app/api/v1/router.py` | API 路由聚合 | +| `python-api/app/scheduler/main.py` | Async Engine 调度器入口 | +| `python-api/app/core/token_manager.py` | API Token 缓存与自动刷新 | +| `python-api/config/ai_models.yaml` | AI 模型配置(热重载)| +| `tauri-app/src/api/client.ts` | 智能路由 API 客户端 | +| `tauri-app/src/store/projectStore.ts` | 项目状态管理 | +| `tauri-app/src-tauri/src/lib.rs` | Rust 命令注册 | +| `tauri-app/src-tauri/src/storage/engine.rs` | 核心存储引擎 | +| `tauri-app/src-tauri/src/ffmpeg_cmd.rs` | FFmpeg 命令封装 | -## 视频创作流程 +## 附加文档 -1. **脚本生成** (Step 1) - AI 生成视频脚本和分镜,或用户直接粘贴文案 -2. **音频合成** (Step 2) - 声音克隆 + TTS 生成每段配音 -3. **视频生成** (Step 3) - 导入长视频并按脚本时长自动切割为片段 -4. **字幕压制** (Step 4) - 生成字幕并压制到视频中 -5. **封面制作** (Step 5) - 使用 Fabric.js 设计视频封面 -6. **视频合成** (Step 6) - FFmpeg 拼接视频片段,替换原声为 TTS 音频,导出最终视频 +项目 `docs/` 目录包含详细的深度开发文档: -## 常见问题 - -### Q: 火山方舟如何配置? - -1. 注册火山引擎账号并实名认证 -2. 创建 API Key -3. 开通模型并创建推理接入点 -4. 在 `.env` 中设置 `VOLCENGINE_API_KEY` - -### Q: 可灵 AI 如何配置? - -1. 前往可灵 AI 开发者平台 https://klingai.com/document-api -2. 获取 Access Key 和 Secret Key -3. 在 `.env` 中设置 `KLINGAI_ACCESS_KEY` 和 `KLINGAI_SECRET_KEY` - -### Q: FFmpeg 在哪里? - -Tauri 应用已嵌入 FFmpeg 二进制文件: -- 位置: `tauri-app/src-tauri/binaries/ffmpeg-*` 和 `ffprobe-*` -- 使用: Rust 层通过 `ffmpeg_cmd` 模块调用 -- 打包时会作为 `externalBin` 资源嵌入 - -### Q: 后端换了 AI 模型,输出格式变了怎么办? - -修改 `services/ai_response_utils.py` 中的标准化函数,增加新的字段映射,**不要**修改 API Schema。 - -### Q: 如何新增/修改提示词? - -1. 创建文件: `app/ai/prompts/my_prompt.txt` -2. 加载使用: `prompt = self._load_prompt("my_prompt")` -3. **禁止**: 在 Python 代码中直接写 `"""你是一位..."""` - -### Q: 项目数据是如何持久化的? - -- 项目元数据(`meta.json`)和分镜数据(`segments.json`)保存在 `~/Documents/Meijiaka-zj/projects/{project_id}/` -- 不通过 Zustand `persist` 保存项目数据,而是通过 `localProjectApi` 显式调用 Tauri IPC 写入文件 -- `projectStore` 的 `persist` 中间件仅保存少量 UI 状态 - -### Q: Async Engine 和 Celery 有什么区别? - -本项目使用**自定义 Async Engine** 替代 Celery: -- 基于 Redis 的槽位管理(SlotManager),限制各类型任务的并发数 -- 独立的 `scheduler` 进程(`python -m app.scheduler.main`) -- 每个 Handler 实现 `AsyncHandler` 接口,状态机驱动任务生命周期 -- 优势:更细粒度的并发控制、统一状态机、无 Celery 依赖 - ---- - -**最后更新**: 2026-04-22 -**架构模式**: 单机版(轻量云账号 + 全本地业务数据) -**项目状态**: 从 ai-meijiaka Fork 后的活跃重构中 +| 文档 | 主题 | +|------|------| +| `docs/unified-async-scheduler.md` | 统一异步调度器设计 | +| `docs/volcengine-video-caption-api.md` | 火山引擎字幕 API 对接 | +| `docs/kling-api-dev.md` | Kling AI API 开发文档 | +| `docs/vidu-tts-api.md` | Vidu TTS API 集成 | +| `docs/minimax-api-dev.md` | MiniMax API 开发文档 | +| `docs/anytocopy-api.md` | AnyToCopy API 集成 | +| `docs/anytocopy-integration.md` | AnyToCopy 服务集成说明 | +| `docs/qiniu-kodo-python-sdk-guide.md` | 七牛云 Kodo Python SDK 指南 | +| `docs/app-update-system.md` | 应用更新系统设计 | +| `docs/database-design.md` | 数据库设计文档 | diff --git a/CLAUDE.md b/CLAUDE.md index c783385..1d52946 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `app/ai/providers/*` - 具体实现(OpenAI、火山引擎、KlingAI 等) - `app/ai/prompts/` - 提示词模板文件 -支持的 AI 平台:火山方舟(推荐)、OpenAI、KlingAI(TTS/声音克隆)。 +支持的 AI 平台:火山方舟(推荐)、OpenAI、KlingAI(TTS/声音克隆)、MiniMax、Vidu。 ### Token 管理 @@ -65,7 +65,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **`JobRegistry`**: Redis-based 任务 CRUD - **`SlotManager`**: Redis Lua 原子脚本实现并发槽位抢占/释放 -已注册槽位:Video (18), Image (9), Script (10), Subtitle (5), Copy (5), TTS, Avatar (2) +已注册槽位:Video (18), Image (9), Script (10), Subtitle (5), Copy (5), TTS (10), Avatar (2) ### 本地存储结构(用户机器) @@ -132,20 +132,41 @@ meijiaka-zj/ ### 后端 (python-api) -项目使用 `uv` 进行依赖管理,并提供了 `Makefile` 封装常用命令: +项目使用 `uv` 进行依赖管理,并提供了 `Makefile` 封装常用命令。 +**首次启动:** +```bash +# 1. 先启动共享基础设施(只需一次) +cd /Users/0fun/work/docker-infra +docker-compose up -d + +# 2. 再启动本项目服务 +cd /Users/0fun/work/meijiaka-zj/python-api +make docker-run +``` + +**常用命令:** ```bash cd python-api -# 使用 uv 和 Makefile(推荐) +# 服务启动 +make docker-run # 启动 api + scheduler(共享 db/redis) +make docker-stop # 停止 api + scheduler +make docker-rebuild # 强制重建 api + scheduler(代码更新后使用) +make docker-rm # 删除 api + scheduler 容器 +make docker-logs # 查看 Docker 日志 +make docker-logs-api # 查看 api 日志 +make docker-logs-scheduler # 查看 scheduler 日志 + +# 开发工具 make dev # 安装开发依赖并配置 pre-commit -make docker-run # 使用 Docker Compose 启动所有服务(db, redis, api, scheduler) -make run # 启动 FastAPI 开发服务器 -make scheduler # 启动 Async Engine Scheduler make lint # 运行代码检查 (ruff + mypy) make lint-semantic # 语义层禁词检查 make format # 格式化代码 +make format-check # 检查代码格式(不修改) make test # 运行所有测试 +pytest tests/test_example.py # 运行单个测试文件 +pytest tests/test_example.py::test_case # 运行单个测试用例 make test-cov # 运行测试并生成覆盖率报告 make security # 运行安全扫描 (bandit + pip-audit) make ci # 运行所有 CI 检查 @@ -189,6 +210,7 @@ npm run stylelint # CSS 检查 # 测试 npm run test # 运行 Vitest +npm run test:ui # 运行 Vitest UI npm run test:coverage # 覆盖率报告 # 类型生成 @@ -286,9 +308,9 @@ chore: 构建/工具 ### 后端 (.env) 关键配置 ```bash -# 数据库 -DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/meijiaka -REDIS_URL=redis://localhost:6379/0 +# 数据库(Docker 内部使用容器名,本地测试用 localhost) +DATABASE_URL=postgresql+asyncpg://postgres:postgres@meijiaka-db:5432/meijiaka_zj +REDIS_URL=redis://meijiaka-redis:6379/1 # JWT 认证 SECRET_KEY=your-secret-key-here @@ -303,13 +325,13 @@ KLINGAI_SECRET_KEY=your-kling-secret-key OPENAI_API_KEY=sk-your-openai-key # CORS 配置 -CORS_ORIGINS=http://localhost:1420,http://127.0.0.1:1420,http://localhost:8080 +CORS_ORIGINS=http://localhost:1420,http://127.0.0.1:1420,http://localhost:8081 ``` ## 服务地址 -- API: http://localhost:8080/api/v1 -- 文档: http://localhost:8080/docs +- API: http://localhost:8081/api/v1 +- 文档: http://localhost:8081/docs - Vite 开发服务器: http://localhost:1420 ## 关键开发文件 @@ -337,7 +359,14 @@ CORS_ORIGINS=http://localhost:1420,http://127.0.0.1:1420,http://localhost:8080 |------|------| | `docs/unified-async-scheduler.md` | 统一异步调度器设计 | | `docs/volcengine-video-caption-api.md` | 火山引擎字幕 API 对接 | -| `docs/semantic-refactoring-plan.md` | 后端语义重构计划 | +| `docs/kling-api-dev.md` | Kling AI API 开发文档 | +| `docs/vidu-tts-api.md` | Vidu TTS API 集成 | +| `docs/minimax-api-dev.md` | MiniMax API 开发文档 | +| `docs/anytocopy-api.md` | AnyToCopy API 集成 | +| `docs/anytocopy-integration.md` | AnyToCopy 服务集成说明 | +| `docs/qiniu-kodo-python-sdk-guide.md` | 七牛云 Kodo Python SDK 指南 | +| `docs/app-update-system.md` | 应用更新系统设计 | +| `docs/database-design.md` | 数据库设计文档 | ## 视频创作核心流程 diff --git a/docs/cover-design-impl-plan-v2.md b/docs/cover-design-impl-plan-v2.md deleted file mode 100644 index e0cf530..0000000 --- a/docs/cover-design-impl-plan-v2.md +++ /dev/null @@ -1,667 +0,0 @@ -# 封面制作功能实施方案(Fabric.js 版) - -## 一、兼容性说明 - -Fabric.js 与现有架构**零冲突**: -- 纯前端库,无需修改 Rust/后端 -- React 组件内直接使用,无兼容问题 -- 字体复用项目已有的 `DouyinSans`(嵌入字体) -- PNG 导出通过 Fabric.js `toDataURL()` 直接生成 - ---- - -## 二、技术方案 - -### 方案:Fabric.js Canvas 预览 + PNG 导出 - -| 维度 | Fabric.js 方案 | -|------|----------------| -| 预览 | Fabric Canvas 实时渲染,所见即所得 | -| 导出 | `canvas.toDataURL('image/png')` → Rust 保存 | -| 文字控制 | 固定位置,禁止拖拽(`selectable: false`) | -| 模板切换 | 重建 Fabric 对象,换文字样式/位置 | -| 背景图 | `fabric.Image.fromURL()` 加载,居中裁剪填充 | - ---- - -## 三、详细设计 - -### 3.1 安装依赖 - -```bash -cd tauri-app -npm install fabric@6 -``` - -### 3.2 两种固定模板 - -#### 模板1:双标题居中 - -``` -┌────────────────────────┐ -│ │ -│ 「主标题文案」 │ ← 72px,金色 #FFD700,粗体,y=35% -│ │ -│ 「副标题文案」 │ ← 32px,白色 #FFFFFF,y=60% -│ │ -│ │ -└────────────────────────┘ -``` - -**Fabric 对象结构**: -```typescript -[ - fabric.Image(bgImage), // 背景图,锁定 - fabric.Text(mainTitle, { // 主标题 - fontSize: 72, - fill: '#FFD700', - fontWeight: 'bold', - textAlign: 'center', - originX: 'center', - originY: 'center', - left: 540, top: 672, // 1080*0.35*1920/1080... 实际按坐标 - selectable: false, - shadow: new fabric.Shadow({...}) // 黑色描边 - }), - fabric.Text(subtitle, { // 副标题 - fontSize: 32, - fill: '#FFFFFF', - textAlign: 'center', - originX: 'center', - originY: 'center', - left: 540, top: 1152, - selectable: false, - shadow: new fabric.Shadow({...}) - }) -] -``` - -#### 模板2:标题 + 标签列表 - -``` -┌────────────────────────┐ -│ │ -│ 「主标题文案」 │ ← 72px,金色 #FFD700,y=30% -│ │ -│ │ -│ 标签1 标签2 标签3 │ ← 28px,白色,带圆角背景,y=72% -│ 标签4 标签5 │ -│ │ -└────────────────────────┘ -``` - -**Fabric 对象结构**: -```typescript -[ - fabric.Image(bgImage), // 背景图 - fabric.Text(mainTitle, {...}), // 主标题 - fabric.Group([ // 标签组(多个 fabric.Rect + fabric.Text) - fabric.Rect({fill: 'rgba(0,0,0,0.5)', rx: 8, ry: 8}), - fabric.Text('标签1', {fill: '#FFFFFF', fontSize: 28}), - ... - ]) -] -``` - -### 3.3 界面布局 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 封面制作 │ -├───────────────────────────────┬─────────────────────────────┤ -│ │ │ -│ [模板选择] │ │ -│ ┌────┐ ┌────┐ │ ┌─────────────┐ │ -│ │模板1│ │模板2│ │ │ │ │ -│ │缩略图│ │缩略图│ │ │ Fabric │ │ -│ └────┘ └────┘ │ │ Canvas │ │ -│ │ │ 预览 │ │ -│ [背景图] │ │ │ │ -│ ┌─────────┐ [选择图片] │ └─────────────┘ │ -│ │ 缩略图 │ [清除] │ │ -│ └─────────┘ │ │ -│ │ │ -│ [主标题] │ │ -│ ┌───────────────────────┐ │ │ -│ │ 输入主标题文案... │ │ │ -│ └───────────────────────┘ │ │ -│ │ │ -│ [副标题/标签] │ │ -│ ┌───────────────────────┐ │ │ -│ │ 输入副标题或标签... │ │ │ -│ └───────────────────────┘ │ │ -│ │ │ -│ [生成封面图] │ │ -│ │ │ -└───────────────────────────────┴─────────────────────────────┘ -``` - ---- - -## 四、核心数据结构 - -```typescript -// 模板类型 -type CoverTemplate = 'dual-title' | 'title-tags'; - -// 封面配置(保存到 store) -interface CoverConfig { - template: CoverTemplate; - backgroundImage: string | null; // 本地图片路径 - mainTitle: string; - subtitle: string; // 模板1=副标题,模板2=逗号分隔的标签 -} - -// Fabric.js 模板定义 -interface FabricTemplateDef { - name: CoverTemplate; - width: number; // 1080 - height: number; // 1920 - mainTitle: { - fontSize: number; - fill: string; - top: number; // y坐标 - fontWeight: string; - shadow?: fabric.Shadow; - }; - subtitle: { - fontSize: number; - fill: string; - top: number; - }; -} -``` - ---- - -## 五、核心 Hook 设计 - -### 5.1 useCoverFabric Hook - -**新文件**:`src/hooks/useCoverFabric.ts` - -```typescript -import { useRef, useCallback, useEffect } from 'react'; -import * as fabric from 'fabric'; - -const CANVAS_WIDTH = 1080; -const CANVAS_HEIGHT = 1920; - -export function useCoverFabric() { - const canvasRef = useRef(null); - const fabricCanvasRef = useRef(null); - - // 初始化 Fabric Canvas - const initCanvas = useCallback(() => { - if (!canvasRef.current || fabricCanvasRef.current) return; - - const canvas = new fabric.Canvas(canvasRef.current, { - width: CANVAS_WIDTH, - height: CANVAS_HEIGHT, - backgroundColor: '#1a1a2e', - selection: false, // 禁用框选 - interactive: false, // 禁用所有交互(只读预览) - }); - - fabricCanvasRef.current = canvas; - }, []); - - // 渲染封面 - const renderCover = useCallback(async ( - config: CoverConfig, - previewScale: number = 1 - ) => { - const canvas = fabricCanvasRef.current; - if (!canvas) return; - - canvas.clear(); - - // 1. 绘制背景图 - if (config.backgroundImage) { - await loadBackgroundImage(canvas, config.backgroundImage); - } - - // 2. 根据模板添加文字 - const template = TEMPLATES[config.template]; - - // 主标题 - const mainText = new fabric.Text(config.mainTitle, { - left: CANVAS_WIDTH / 2, - top: template.mainTitle.top, - fontSize: template.mainTitle.fontSize, - fill: template.mainTitle.fill, - fontWeight: template.mainTitle.fontWeight, - fontFamily: '"DouyinSans", "PingFang SC", sans-serif', - textAlign: 'center', - originX: 'center', - originY: 'center', - selectable: false, - shadow: new fabric.Shadow({ - color: 'rgba(0,0,0,0.8)', - blur: 4, - offsetX: 2, - offsetY: 2, - }), - }); - canvas.add(mainText); - - // 副标题/标签 - if (config.template === 'dual-title') { - const subText = new fabric.Text(config.subtitle, { - left: CANVAS_WIDTH / 2, - top: template.subtitle.top, - fontSize: template.subtitle.fontSize, - fill: template.subtitle.fill, - fontFamily: '"DouyinSans", "PingFang SC", sans-serif', - textAlign: 'center', - originX: 'center', - originY: 'center', - selectable: false, - shadow: new fabric.Shadow({ - color: 'rgba(0,0,0,0.6)', - blur: 3, - offsetX: 1, - offsetY: 1, - }), - }); - canvas.add(subText); - } else { - // 标签列表:解析逗号分隔,自动换行布局 - renderTagList(canvas, config.subtitle, template); - } - - canvas.renderAll(); - }, []); - - // 导出 PNG - const exportPng = useCallback((): string => { - const canvas = fabricCanvasRef.current; - if (!canvas) return ''; - return canvas.toDataURL({ format: 'png', quality: 1.0 }); - }, []); - - // 销毁 - const destroy = useCallback(() => { - fabricCanvasRef.current?.dispose(); - fabricCanvasRef.current = null; - }, []); - - return { canvasRef, initCanvas, renderCover, exportPng, destroy }; -} - -// 加载背景图(居中裁剪填充) -async function loadBackgroundImage( - canvas: fabric.Canvas, - imagePath: string -): Promise { - return new Promise((resolve) => { - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.onload = () => { - const fabricImg = new fabric.Image(img); - // 计算缩放和裁剪(cover 模式) - const scale = Math.max( - CANVAS_WIDTH / fabricImg.width!, - CANVAS_HEIGHT / fabricImg.height! - ); - fabricImg.scale(scale); - fabricImg.set({ - left: (CANVAS_WIDTH - fabricImg.getScaledWidth()) / 2, - top: (CANVAS_HEIGHT - fabricImg.getScaledHeight()) / 2, - selectable: false, - evented: false, - }); - canvas.add(fabricImg); - canvas.sendObjectToBack(fabricImg); - resolve(); - }; - img.src = `asset://localhost/${encodeURIComponent(imagePath)}`; - }); -} - -// 渲染标签列表 -function renderTagList( - canvas: fabric.Canvas, - tagsText: string, - template: FabricTemplateDef -) { - const tags = tagsText.split(/[,,]/).map(t => t.trim()).filter(Boolean); - if (tags.length === 0) return; - - const tagHeight = 56; - const tagPadding = 20; - const gapX = 16; - const gapY = 16; - const maxLineWidth = CANVAS_WIDTH - 120; // 左右各60px边距 - - const groups: fabric.Group[] = []; - let currentX = 60; - let currentY = template.subtitle.top; - - for (const tag of tags) { - const text = new fabric.Text(tag, { - fontSize: 28, - fill: '#FFFFFF', - fontFamily: '"DouyinSans", "PingFang SC", sans-serif', - left: tagPadding, - top: tagHeight / 2, - originX: 'center', - originY: 'center', - }); - - const rect = new fabric.Rect({ - width: text.width! + tagPadding * 2, - height: tagHeight, - fill: 'rgba(0,0,0,0.5)', - rx: 8, - ry: 8, - }); - - const group = new fabric.Group([rect, text], { - left: currentX, - top: currentY, - selectable: false, - evented: false, - }); - - // 换行检查 - if (currentX + group.width! > CANVAS_WIDTH - 60 && currentX > 60) { - currentX = 60; - currentY += tagHeight + gapY; - group.set({ left: currentX, top: currentY }); - } - - currentX += group.width! + gapX; - groups.push(group); - } - - // 整体居中 - const allTags = new fabric.Group(groups, { - left: CANVAS_WIDTH / 2, - top: template.subtitle.top, - originX: 'center', - originY: 'top', - selectable: false, - evented: false, - }); - - canvas.add(allTags); -} - -// 模板定义 -const TEMPLATES: Record = { - 'dual-title': { - name: 'dual-title', - width: 1080, - height: 1920, - mainTitle: { fontSize: 72, fill: '#FFD700', top: 672, fontWeight: 'bold' }, - subtitle: { fontSize: 32, fill: '#FFFFFF', top: 1152 }, - }, - 'title-tags': { - name: 'title-tags', - width: 1080, - height: 1920, - mainTitle: { fontSize: 72, fill: '#FFD700', top: 576, fontWeight: 'bold' }, - subtitle: { fontSize: 28, fill: '#FFFFFF', top: 1382 }, - }, -}; -``` - ---- - -## 六、文件改动清单 - -| 文件 | 类型 | 说明 | -|------|------|------| -| `package.json` | 修改 | 添加 `fabric@6` 依赖 | -| `CoverDesign.tsx` | 重构 | 使用 Fabric.js 替换现有实现 | -| `CoverDesign.css` | 调整 | 适配新布局,预览区固定尺寸 | -| `src/hooks/useCoverFabric.ts` | **新增** | Fabric.js 渲染 Hook | -| `src/store/projectStore.ts` | 调整 | 更新 CoverConfig 类型 | - ---- - -## 七、CoverDesign.tsx 关键代码 - -```typescript -import { open } from '@tauri-apps/plugin-dialog'; -import { useCoverFabric } from '../../hooks/useCoverFabric'; - -export default function CoverDesign() { - const { canvasRef, initCanvas, renderCover, exportPng, destroy } = useCoverFabric(); - const [config, setConfig] = useState({ - template: 'dual-title', - backgroundImage: null, - mainTitle: '', - subtitle: '', - }); - - // 初始化 Fabric Canvas - useEffect(() => { - initCanvas(); - return destroy; - }, [initCanvas, destroy]); - - // 配置变化时重新渲染 - useEffect(() => { - renderCover(config); - }, [config, renderCover]); - - // 选择图片 - const handleSelectImage = async () => { - const selected = await open({ - multiple: false, - filters: [{ name: '图片', extensions: ['jpg', 'jpeg', 'png', 'webp'] }] - }); - if (selected) { - setConfig(prev => ({ ...prev, backgroundImage: selected as string })); - } - }; - - // 生成封面 - const handleGenerate = async () => { - const dataUrl = exportPng(); - const base64 = dataUrl.split(',')[1]; - - const result = await invoke('save_project_asset', { - projectId, - filename: `cover_${Date.now()}.png`, - base64Data: base64, - }); - - if (result.code === 200) { - setCoverPath(result.data); - setCoverConfig(config); - } - }; - - return ( -
- {/* 左侧配置 */} -
- {/* 模板选择 */} -
- -
- - -
-
- - {/* 背景图 */} -
- -
- {config.backgroundImage && ( - 背景 - )} - - {config.backgroundImage && ( - - )} -
-
- - {/* 主标题 */} -
- -