Files
meijiaka-zy/docs/migrate-avatars-to-local.md
T

9.6 KiB
Raw Blame History

迁移方案:废弃云端 mjk_avatars 表,数字人元数据全量迁移到本地存储

状态:方案已调整(2026-04-17) — 原方案中提到的 Celery 架构已完全移除,形象克隆现由 app/scheduler/handlers/avatar_handler.pyAsync Engine Scheduler)统一调度。云端仍保留 avatars 表作为形象克隆的持久化记录。

方案目标

目标 说明
贯彻设计理念 真正做到轻量云 + 全本地业务数据,云端只记日志不存业务数据
统一接口日志 所有接口请求统一记录到 mjk_interface_request_logs,按接口统计积分消耗
简化后端代码 删除大量 CRUD、状态管理、定时任务代码,后端更干净
用户掌控数据 所有数字人元数据存在用户本地,云端只记克隆请求的消耗积分

存储结构变化

变化前(现状)

云端 PostgreSQL: mjk_avatars
  └─ 存储所有数字人元数据 (name/voice_id/element_id/status 等)
前端本地:
  └─ 只做缓存,从云端同步

变化后(目标)

云端 PostgreSQL:
  ├─ mjk_interface_request_logs ← 只记:avatar_clone 请求 + 消耗积分 + 状态
  └─ mjk_avatars ← 废弃,不再写入新数据(存量可保留可删除)
用户本地磁盘:
  └─ ~/Documents/Meijiaka/avatars/{avatar_id}/
      ├─ meta.json      ← 完整数字人元数据(JSON
      └─ source.mp4     ← 原始上传视频

本地存储结构定义

目录结构

~/Documents/Meijiaka/
└── avatars/
    └── {avatar_id}/              # avatar_id = avt_{16位随机hex}
        ├── meta.json             # 元数据(JSON 格式)
        └── source.mp4            # 原始上传视频

meta.json 结构

{
  "id": "avt_xxxxxxxxxxxxxxxx",
  "name": "我的数字人",
  "voiceId": "klingai-voice-id-string",
  "elementId": 12345678,
  "voiceTaskId": "kling-task-id-string",
  "elementTaskId": "kling-task-id-string",
  "videoUrl": "https://domain.com/path/to/source.mp4",
  "trialUrl": "https://domain.com/path/to/trial.wav",
  "status": "succeed",
  "failReason": null,
  "createdAt": "2026-04-16T10:00:00.000Z",
  "updatedAt": "2026-04-16T10:05:00.000Z"
}

代码改动清单

后端 Python

操作 文件 改动说明
🆕 新增 app/models/interface_request_logs.py SQLAlchemy 模型 InterfaceRequestLogs
🆕 新增 app/crud/interface_request_logs.py CRUDcreate / update
✏️ 修改 app/models/__init__.py 删除 Avatar 导入,新增 InterfaceRequestLogs
✏️ 修改 app/api/v1/avatar.py 完全重写
• 保留:POST /clone / GET /tasks/{id} / GET /clone/stream / POST /tasks/{id}/retry / DELETE /{id}
• 删除:GET /library / PATCH /{id} / /health
✏️ 修改 app/scheduler/handlers/avatar_handler.py 精简:删除所有对 mjk_avatars 读写,只记接口日志,进度放 Redis Registry
删除 app/models/avatar.py 模型废弃,删除
删除 app/crud/avatar.py CRUD 废弃,删除
删除 app/tasks/avatar_clone.py 逻辑已合并到 avatar_handler,删除

Rust Tauritauri-app/src-tauri/src/persistence.rs

新增以下 IPC 命令:

/// 列出所有本地数字人(按创建时间倒序)
#[tauri::command]
pub fn list_avatars(app: AppHandle) -> Result<Vec<AvatarMeta>, String>;

/// 保存数字人元数据
#[tauri::command]
pub fn save_avatar(app: AppHandle, avatar_id: String, meta: AvatarMeta) -> Result<(), String>;

/// 获取单个数字人元数据
#[tauri::command]
pub fn get_avatar(app: AppHandle, avatar_id: String) -> Result<Option<AvatarMeta>, String>;

/// 删除数字人(删除整个本地目录)
#[tauri::command]
pub fn delete_avatar(app: AppHandle, avatar_id: String) -> Result<(), String>;

/// 更新数字人名称
#[tauri::command]
pub fn update_avatar_name(app: AppHandle, avatar_id: String, name: String) -> Result<(), String>;

lib.rs 注册新命令。

前端 TypeScript

模块 改动
Avatar 列表 原:GET /avatar/library 从后端获取 → 现在:调用 Tauri IPC 从本地读取
创建克隆 流程变化:
1. 前端生成 avatar_id
2. POST /avatar/clone → 获取 task_id
3. 前端创建本地目录 + 写入初始 meta.json (status=pending)
4. SSE 监听进度
5. 完成后 → 前端把 voice_id/element_id 写入本地 meta.json
6. 完成
删除 Avatar 流程变化:
1. 前端调用 DELETE /avatar/{avatar_id}(后端负责删除 Kling 远程资源)
2. 后端记删除日志到接口日志
3. 前端调用 IPC 删除本地目录
重命名 Avatar 原:调用后端 PATCH → 现在:前端直接修改本地 meta.json,无需请求后端
选择数字人生成视频 用法不变:从本地读取 voice_id/element_id → 传给后端视频生成接口

工作流对比

改动前(当前)

用户提交克隆
  → POST /clone → 后端写 mjk_avatars (status=pending) → 派发任务
  → Async Engine Scheduler (avatar_handler) 每一步都更新 `avatars` 表
  → 前端 SSE 轮询读 `avatars` 表拿进度
  → 完成后 Scheduler 更新 status=succeed 写入 voice_id/element_id
  → 前端从 `avatars` 表读结果 → 缓存到本地
  → 列表从 `avatars` 表读取

改动后(目标)

用户提交克隆
  → 前端生成 avatar_id → 创建本地 meta.json (status=pending)
  → POST /clone → 后端:
      1. 在 mjk_interface_request_logs 插入记录
         interface_type=avatar_clone, status=pending, started_at=now, cost_credits=X
      2. 注册到 Async Engine Scheduler (Redis Registry)
      3. 返回 {task_id, avatar_id}
  → Async Engine Scheduler (avatar_handler) 执行:
      1. 调用 Kling 创建音色 → 轮询 → 获取 voice_id
      2. 调用 Kling 创建主体 → 轮询 → 获取 element_id
      3. 更新 Redis Registry 状态为 completed,写入结果
      4. 更新接口日志: status=success/failed, finished_at=now
  → 前端 SSE 从 TaskCache 获取结果
  → 完成后前端将 voice_id/element_id 写入本地 meta.json
  → 列表展示直接从本地读取,不请求后端

接口日志记录规则

mjk_interface_request_logsavatar_clone 的记录:

时机 操作 字段值
刚收到请求 插入新记录 interface_type=avatar_clone, status=pending, started_at=NOW, cost_credits = 克隆一次所需积分
任务完成成功 更新记录 status=success, finished_at=NOW
任务失败 更新记录 status=failed, finished_at=NOW, error_message=错误原因

积分在请求创建时即扣除,因为无论成功失败,KlingAI 开始处理后会计费。


存量数据迁移策略

渐进迁移(对用户友好)

  1. 保留云端表mjk_avatars 保留不删除,存量数据继续存在
  2. 前端自动迁移:用户首次打开形象库时:
    • 前端检查:如果后端有数据但本地没有 → 提示用户"将云端数字人同步到本地"
    • 用户确认后,前端逐个拉取数据写入本地
    • 同步完成后,后续只使用本地数据
  3. 下线旧表:稳定运行一段时间后,可在维护窗口物理删除 mjk_avatars

回滚方案

  • 迁移过程中如果出问题,随时切回原逻辑(表保留,代码只需恢复删除部分)

优缺点总结

优点 说明
完全符合需求 云端只存接口请求记录和消耗积分,不存用户业务数据
云端存储成本极低 只有接口日志,每条几KB,用户增长成本可控
后端代码大幅简化 删除了整个 Avatar CRUD、状态机管理、定时任务恢复,代码减少约 300 行
用户完全掌控数据 所有数字人元数据存储在用户本地磁盘
形象库展示更快 本地读取文件比查询数据库快很多
兼容存量数据 渐进迁移,可回滚
缺点 说明 应对
用户换电脑需要迁移 用户需要自行迁移数据,或重新克隆 后续可增加导出/导入功能解决
本地硬盘损坏数据丢失 这是"全本地"设计的必然结果 符合项目初始"轻量云+全本地"设计理念,用户自担数据安全

执行步骤(按顺序)

  1. 数据库

    • 生成 Alembic 迁移:所有表重命名加 mjk_ 前缀 + 新建 mjk_interface_request_logs
    • 修改所有 Python 模型中的 __tablename__
  2. 后端代码

    • 新建 interface_request_logs 模型和 CRUD
    • 重写 app/api/v1/avatar.py
    • 精简 app/tasks/avatar_tasks.py
    • 删除废弃文件
  3. Rust Tauri

    • persistence.rs 新增 avatar 相关 IPC 命令
    • lib.rs 注册命令
  4. 前端代码

    • 修改形象库:从本地读取
    • 修改创建流程:完成后写入本地
    • 修改删除流程:删除云端 Kling 资源后删除本地
    • 修改重命名:直接本地修改
  5. 测试验证

    • 创建克隆 → 检查本地文件生成 → 检查接口日志写入
    • 列表展示 → 删除 → 重命名 全流程测试

相关文档


版本:v1.0 创建日期:2026-04-16