diff --git a/.env.example b/.env.example index 38e9dcc..eabf26a 100644 --- a/.env.example +++ b/.env.example @@ -1,37 +1,90 @@ -# NiuMa Studio 环境变量示例 -# 使用方法:复制本文件为 .env,然后填写自己的密钥。 -# 注意:.env 已被 .gitignore 忽略,不要把真实 API Key 提交到 Git。 +# NiuMa Studio / 牛马片场环境变量示例 +# 使用方法:复制本文件为 .env,然后只在 .env 里填写真实密钥。 +# 注意:不要把真实 API Key、Token、Cookie、账号密码提交到 Git。 # ======================== -# 基础配置 +# 1. 本地服务与数据目录 # ======================== -# 本地管理员令牌(用于调用管理接口时鉴权) -# 生产环境请换成随机长字符串,不要使用默认值 +# 本地管理接口令牌。建议改成随机长字符串。 LOCAL_ADMIN_TOKEN=change-me-to-a-random-string -# 数据库文件路径(本地默认存在 data/ 目录;Docker 内固定为 /app/data/workflow.sqlite3) +# 数据库文件。留空时使用 app/core/config.py 中的默认值:data/workflow.sqlite3。 DATABASE_PATH= -# 数据目录(任务记录、AI 分析结果等;留空则默认使用项目 data/ 目录) +# 数据目录。留空时使用项目根目录下的 data/。 DATA_DIR= -# FFmpeg 超时(秒),超过此时间未完成的视频处理会被取消 -FFMPEG_TIMEOUT=600 - -# Docker / 本地存储路径 -# 本地直接运行时默认沿用历史存储目录 E:\直播间切片工作流存储,避免影响已有任务和视频。 -# Docker Compose 会自动把这个 E 盘目录挂载到容器内的 /workspace/tasks。 -# 如果以后要换存储盘,可以同步修改 docker-compose.yml 里的 volumes 和这里的路径。 +# 任务产物目录。 +# 历史默认值是 E:\直播间切片工作流存储,用来兼容已有任务。 +# 如果你的电脑没有 E 盘,请改成真实存在的 Windows 目录,例如: +# STORAGE_ROOT=C:\NiuMaStudio\tasks +# TASKS_DIR=C:\NiuMaStudio\tasks STORAGE_ROOT=E:\直播间切片工作流存储 TASKS_DIR=E:\直播间切片工作流存储 +# 允许从哪些本地目录选择已有视频,多个路径用英文逗号分隔。 +# 留空时只允许项目任务目录和存储目录内的文件。 +ALLOWED_MEDIA_ROOTS= + +# 上传限制。 +MAX_UPLOAD_SIZE_BYTES=4294967296 +ALLOWED_UPLOAD_EXTENSIONS=.mp4,.mov,.mkv,.avi,.flv,.webm,.m4v,.ts,.wav,.mp3,.aac,.flac,.ogg,.wma + +# ======================== +# 2. FFmpeg / FFprobe +# ======================== + +# 所有这些命令都要求 Windows 能直接执行 ffmpeg / ffprobe。 +FFMPEG_TIMEOUT=600 +FFMPEG_AUDIO_EXTRACT_TIMEOUT=600 +FFMPEG_CUT_TIMEOUT=600 +FFMPEG_SUBTITLE_TIMEOUT=300 +FFMPEG_COVER_TIMEOUT=120 +FFMPEG_CHUNK_TIMEOUT=120 +FFPROBE_TIMEOUT=60 + +# ======================== +# 3. 转写配置 +# ======================== + +# 可选值:volcengine 或 local。 +# volcengine = 火山引擎远程转写。 +# local = 本地 faster-whisper。 +# 远程失败时当前不会自动切到本地,页面会提示手动改用本地模型。 +TRANSCRIPTION_PROVIDER=volcengine +TRANSCRIPTION_FALLBACK_PROVIDER= + +# 火山引擎 ASR。真实密钥只写在 .env,不要写进代码或文档。 +VOLCENGINE_ASR_API_URL=https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash +VOLCENGINE_ASR_API_KEY= +VOLCENGINE_ASR_APP_KEY= +VOLCENGINE_ASR_ACCESS_KEY= +VOLCENGINE_ASR_RESOURCE_ID=volc.bigasr.auc_turbo +VOLCENGINE_ASR_TIMEOUT_SECONDS=300 +VOLCENGINE_ASR_AUDIO_FORMAT=mp3 + +# 本地 faster-whisper。 +TRANSCRIPTION_MODEL=medium +TRANSCRIPTION_LANGUAGE=zh +TRANSCRIPTION_DEVICE=cpu +TRANSCRIPTION_COMPUTE_TYPE=int8 +TRANSCRIPTION_CPU_FALLBACK_MODEL=medium +TRANSCRIPTION_CHUNK_SECONDS=120 +TRANSCRIPTION_CHUNK_OVERLAP_SECONDS=5 + +# ======================== +# 4. AI 分析与发送中心文案 +# ======================== + +# 可选值:remote 或 local。 AI_DEFAULT_PROVIDER=remote +AI_PROVIDER=remote AI_REQUEST_TIMEOUT_SECONDS=120 -# 2. 分析文字稿,生成候选切片:远程 OpenAI-compatible API +# AI 分析候选片段:远程 OpenAI-compatible / DeepSeek。 AI_ANALYSIS_REMOTE_BASE_URL=https://api.deepseek.com -AI_ANALYSIS_REMOTE_API_KEY=请在这里填写你的文字稿分析 API Key +AI_ANALYSIS_REMOTE_API_KEY= AI_ANALYSIS_REMOTE_MODEL=deepseek-v4-flash AI_ANALYSIS_REMOTE_PROTOCOL=chat_completions AI_ANALYSIS_REMOTE_REASONING_EFFORT= @@ -39,9 +92,9 @@ AI_ANALYSIS_REMOTE_RESPONSES_PATH=/v1/responses AI_ANALYSIS_REMOTE_DISABLE_RESPONSE_STORAGE=true AI_ANALYSIS_REQUEST_TIMEOUT_SECONDS=120 -# 3. 发送中心生成发布文案:远程 OpenAI-compatible API +# 发送中心生成标题、简介、话题:远程 OpenAI-compatible / DeepSeek。 AI_PUBLISH_REMOTE_BASE_URL=https://api.deepseek.com -AI_PUBLISH_REMOTE_API_KEY=请在这里填写你的发布文案 API Key +AI_PUBLISH_REMOTE_API_KEY= AI_PUBLISH_REMOTE_MODEL=deepseek-v4-flash AI_PUBLISH_REMOTE_PROTOCOL=chat_completions AI_PUBLISH_REMOTE_REASONING_EFFORT= @@ -49,7 +102,18 @@ AI_PUBLISH_REMOTE_RESPONSES_PATH=/v1/responses AI_PUBLISH_REMOTE_DISABLE_RESPONSE_STORAGE=true AI_PUBLISH_REQUEST_TIMEOUT_SECONDS=120 -# 本地 Ollama API +# 兼容旧脚本的 AI_REMOTE_* 配置。新功能优先使用上面的 AI_ANALYSIS_* 和 AI_PUBLISH_*。 +AI_REMOTE_BASE_URL=https://api.deepseek.com +AI_REMOTE_API_KEY= +AI_REMOTE_MODEL=deepseek-v4-flash +AI_REMOTE_REVIEW_MODEL=deepseek-v4-flash +AI_REMOTE_PUBLISH_MODEL=deepseek-v4-flash +AI_REMOTE_PROTOCOL=chat_completions +AI_REMOTE_REASONING_EFFORT= +AI_REMOTE_RESPONSES_PATH=/v1/responses +AI_REMOTE_DISABLE_RESPONSE_STORAGE=true + +# 本地 Ollama。 AI_LOCAL_BASE_URL=http://127.0.0.1:11434/v1 AI_LOCAL_API_KEY=ollama AI_LOCAL_MODEL=qwen3:8b @@ -57,40 +121,18 @@ AI_LOCAL_PROTOCOL=chat_completions AI_LOCAL_FALLBACK_PROTOCOL= AI_LOCAL_HEALTH_TIMEOUT_SECONDS=30 -# 发送中心 opencli -# Docker 主页面固定使用 8001;Windows opencli 辅助服务由 scripts/start_docker_opencli.ps1 启动。 -OPENCLI_LOCAL_BASE_URL=http://127.0.0.1:8001 -OPENCLI_HOST_BRIDGE_URL=http://host.docker.internal:8765 - -# 额外 AI 运行默认值 +# 长文本处理参数。一般不用改。 AI_NETWORK_ACCESS=enabled AI_WINDOWS_WSL_SETUP_ACKNOWLEDGED=true AI_MODEL_CONTEXT_WINDOW=1000000 AI_MODEL_AUTO_COMPACT_TOKEN_LIMIT=900000 -# 1. 音频转写 -# 默认优先使用国内可直连的火山引擎远程转写;失败后会暂停并提示原因,不会自动退回本地 faster-whisper。 -TRANSCRIPTION_PROVIDER=volcengine -TRANSCRIPTION_FALLBACK_PROVIDER= -VOLCENGINE_ASR_API_URL=https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash -VOLCENGINE_ASR_API_KEY=请在这里填写你的火山引擎 App Key -VOLCENGINE_ASR_APP_KEY= -VOLCENGINE_ASR_ACCESS_KEY= -VOLCENGINE_ASR_RESOURCE_ID=volc.bigasr.auc_turbo -VOLCENGINE_ASR_TIMEOUT_SECONDS=300 -VOLCENGINE_ASR_AUDIO_FORMAT=mp3 +# ======================== +# 5. opencli / 发送中心 +# ======================== -# 本地 faster-whisper 配置 -# 只有在页面确认“改用本地模型转写”后才会使用,不作为默认自动兜底。 -# MVP 默认使用 CPU 稳定跑通,确认成功后再单独切换 CUDA 加速。 -# 如果要测试 RTX 显卡,可改为: -# TRANSCRIPTION_MODEL=large-v3 -# TRANSCRIPTION_DEVICE=cuda -# TRANSCRIPTION_COMPUTE_TYPE=float16 -TRANSCRIPTION_MODEL=medium -TRANSCRIPTION_LANGUAGE=zh -TRANSCRIPTION_DEVICE=cpu -TRANSCRIPTION_COMPUTE_TYPE=int8 -TRANSCRIPTION_CPU_FALLBACK_MODEL=medium -TRANSCRIPTION_CHUNK_SECONDS=120 -TRANSCRIPTION_CHUNK_OVERLAP_SECONDS=5 +# 本地直接运行时页面地址。 +OPENCLI_LOCAL_BASE_URL=http://127.0.0.1:8001 + +# Docker 内调用 Windows 宿主机 opencli 桥接服务时使用。 +OPENCLI_HOST_BRIDGE_URL=http://host.docker.internal:8765 diff --git a/.gitignore b/.gitignore index a79e1a7..e5b49c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ .venv/ __pycache__/ *.pyc +.pytest_cache/ +.ruff_cache/ # 本地真实配置与密钥:不要提交 API Key .env .env.* !.env.example +# 本地数据库、日志、截图、浏览器缓存、测试素材都不提交 data/* !data/.gitkeep @@ -25,6 +28,7 @@ tasks/* *.webm *.mp3 *.wav +*.bak_* .DS_Store Thumbs.db diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa14def --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## 2026-06-17 + +### 修复 + +- 修复 AI 分析和恢复历史分析时候选片段可能被误清空的问题。 +- 给字幕烧录 FFmpeg 调用补充超时配置,避免 Windows 本地进程长期卡住。 +- 给任务详情视频探测 FFprobe 调用补充超时保护,超时后降级显示未知时长。 +- 移除重复的 `LOCAL_ADMIN_TOKEN` 配置字段。 +- 将 Pydantic 校验器迁移到 v2 `field_validator` 写法,消除弃用 warning。 + +### 文档 + +- 重写 `README.md`,明确当前已实现能力、边界和 Windows 快速启动。 +- 新增 `docs/WINDOWS_SETUP.md`,提供新手 Windows 部署教程。 +- 重写架构、任务流、数据库、部署、AI 分析、切片、候选审核、字幕发送文档。 +- 同步 `docs/UI_REFERENCE.md`、`docs/PROJECT_GUIDE.md`、`DEVELOPMENT_LOG.md`、`NEXT_STEPS.md`。 +- 明确 `scheduled_at` 当前只是字段预留,没有真正定时调度器。 +- 明确发送中心是人工确认 + opencli 辅助投稿,不是无人值守发布。 + +### 配置与清理 + +- 更新 `.env.example`,补齐 Windows 路径、FFmpeg、AI、转写、opencli 配置说明。 +- 更新 `.gitignore`,补充 pytest/ruff 缓存和本地备份文件规则。 +- 给 `docker-compose.yml` 的 E 盘任务目录挂载补充说明。 +- 清理明确可再生成的本地日志、缓存和 `__pycache__` 产物。 diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index 1c531e2..a6e6d5f 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -1,5 +1,15 @@ # Development Log +## 2026-06-17 Windows 代码体检、文档重写与冗余清理 +- 在 `refactor/windows-codebase-audit-docs-cleanup` 分支执行本轮整理,开始前确认 `master` 工作区无未提交的跟踪文件修改。 +- 修复 AI 分析候选片段替换顺序:重新跑 AI 分析或恢复历史分析时,会用新结果替换当前候选,不再插入后立刻清空。 +- 为字幕 FFmpeg 烧录和任务详情 FFprobe 探测补充超时保护,降低 Windows 本地进程卡死风险。 +- 移除重复的 `LOCAL_ADMIN_TOKEN` 配置字段,并将 Pydantic 校验器迁移到 `field_validator`。 +- 新增 AI 候选片段替换和历史恢复回归测试,已先单独验证 `tests/test_versioning_rollback.py::TestAIAnalysisActive` 通过。 +- 重写或同步 `README.md`、`docs/WINDOWS_SETUP.md`、`docs/ARCHITECTURE.md`、`docs/TASK_FLOW.md`、`docs/DATABASE_SCHEMA.md`、`docs/DEPLOYMENT.md`、`docs/AI_ANALYSIS.md`、`docs/VIDEO_CUTTING.md`、`docs/CLIP_REVIEW.md`、`docs/SUBTITLE_AND_PUBLISH_PLAN.md`、`docs/UI_REFERENCE.md`、`.env.example` 和 `CHANGELOG.md`。 +- 文档统一说明:`05_clips` 是当前切片目录,`06_subtitled` 是字幕成片目录,`07_covers` 是封面帧目录;`scheduled_at` 只是字段预留,发送中心仍需人工确认。 +- 本轮不删除真实数据库、`.env`、任务目录、浏览器截图缓存或疑似含密钥备份文件;这些只列为后续需人工确认清理项。 + ## 2026-06-15 v1.3 分支整理与集成发布 - 新建并验证 `codex/branch-integration-20260611` 集成分支,按顺序整合 `fix/p0-security-and-stability`、`fix/p1-1-db-performance`、`fix/p1-2-query-refactor`、`feature/p1-3-job-queue`、`feature/p1-4-split-task-service`、`feature/p1-5-versioned-products-rollback`、`feature/p2-1-engineering` 和 `feature/p2-2-architecture-docs`。 - 已处理分支之间的冲突:数据库初始化同时保留 `oauth_states`、`workflow_jobs`、`cut_runs` 等结构;`task_service.py` 保留兼容出口,页面查询实际迁移到 `task_query_service.py`;任务队列、服务拆分、产物版本化和工程化配置已统一集成。 diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 1b7abee..580395e 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -1,5 +1,25 @@ # Next Steps +## 2026-06-17 本轮整理后怎么检查 +1. 在项目根目录运行:`.\.venv\Scripts\python.exe -m ruff check app tests`。 +2. 再运行:`.\.venv\Scripts\python.exe -m pytest -q`。 +3. 启动本地后台:`.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8001`。 +4. 打开 `http://127.0.0.1:8001/health`,确认返回健康状态。 +5. 打开首页、任务列表、任务详情、片段审核、字幕工作台、发送中心,确认页面能正常加载。 +6. 用一条短视频按顺序冒烟测试:上传视频、提取音频、转写、AI 分析、片段审核、生成切片、生成字幕、刷新发送队列。 + +## 2026-06-17 仍需人工确认的清理项 +1. `.env.backup_*` 可能含真实密钥,确认不需要后再删除。 +2. `data/screenshots/` 可能含浏览器缓存或登录痕迹,确认不需要后再删除。 +3. `data/workflow.sqlite3.bak_*` 是数据库备份,确认已有其他备份后再删除。 +4. `tasks/` 下的真实任务产物目录不要自动删除,确认不再需要后再清理。 +5. 根目录 `ChatGPT Image 2026年5月16日 11_54_05.png` 需要先确认是否是重复设计图,再决定是否删除或移动到 `docs/design/`。 + +## 2026-06-17 下一步建议 +1. 先完成本轮 PR 审核和合并,不急着继续大改业务流程。 +2. 下一轮优先做小范围体验增强:字幕批量处理、字幕文本编辑、发送失败重试入口、任务文件入口。 +3. 如果要继续清理冗余文件,建议单独开一个清理分支,只处理人工确认过的本地数据和历史设计素材。 + ## 2026-06-15 v1.3 合并后怎么检查 1. 打开项目后先确认左侧显示 `v1.3 分支整合版`。 2. 启动 Docker 主页面:`docker compose up --build`。 diff --git a/README.md b/README.md index c5056b3..c8b144d 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,176 @@ # 牛马片场 / NiuMa Studio -牛马片场是一个运行在 Windows 本地的 AI 高光生产后台,用来把直播录像、综艺访谈、长视频素材整理成可转写、可分析、可审核、可切割、可加字幕、可进入发送中心的短视频生产任务。 +牛马片场是一个运行在 Windows 本地的 AI 高光生产后台。它的目标是把直播录像、综艺访谈、长视频素材整理成一套可追踪的本地任务:转写、AI 分析、人工审核、自动切片、自动加字幕,再进入发送中心做发布前整理。 -当前版本:`1.3.0`。 +当前版本:`1.3.0` -v1.3 已经整合前期多个功能分支:在 v1.2 MVP 全流程基础上,补入本地安全边界、数据库性能优化、任务查询拆分、本地任务队列、任务服务拆分、产物版本化与失败回滚、CI / Ruff 工程化和架构文档。 +## 当前真实状态 -## 当前状态 +已实现: -- 后端:FastAPI 可启动,当前 API 版本为 `1.3.0`。 -- 前端:HTML + CSS + JavaScript + Jinja2 后台页面,已完成 Apple 风格全页面美化。 -- 数据库:SQLite,保存任务、候选片段、输出片段、字幕任务、发送任务和 AI 配置等信息。 -- 视频处理:已接入 FFmpeg / FFprobe,用于音频提取、切片、封面帧和字幕成片。 -- 转写:支持火山引擎远程转写和本地 faster-whisper。 -- AI 分析:支持远程 OpenAI-compatible / DeepSeek 和本地 Ollama;长视频会按小段分析再合并候选片段。 -- 发送中心:支持生成抖音 / B站待发送队列、AI 标题 / 简介 / 话题、候选封面帧,并通过 opencli 调用已登录 Chrome 辅助投稿。 -- 安全边界:不会绕过验证码、登录失效、平台风控或人工确认;不会保存账号密码、cookie 或真实 API Key。 -- 配置安全:真实 `.env` 已被 Git 忽略,不会提交真实 API Key。 -- 品牌说明:当前页面主名为“牛马片场”,英文代号为 `NiuMa Studio`,Docker 技术名为 `niuma-studio`。 +- Windows 本地 FastAPI 后台,前端使用 HTML、CSS、JavaScript、Jinja2。 +- SQLite 本地数据库,保存任务、候选片段、切片、字幕任务、发送任务和配置。 +- 上传本地视频,或选择允许目录中的本地 / NAS 视频。 +- 每个任务生成独立任务目录。 +- FFmpeg / FFprobe 音频提取、视频探测、切片、字幕烧录、封面帧提取。 +- 火山引擎远程转写和本地 faster-whisper 转写。 +- 远程 OpenAI-compatible / DeepSeek 或本地 Ollama 分析候选高光片段。 +- 片段审核页面,可编辑、启用、删除候选片段。 +- 切片输出到 `05_clips`,字幕成片输出到 `06_subtitled`,封面候选帧输出到 `07_covers`。 +- 发送中心生成抖音 / B站待发送任务、标题、简介、话题和封面候选。 +- 通过 opencli 调用已登录 Chrome 辅助投稿。 +- 轻量本地任务队列用于切片异步任务,不依赖 Celery / Redis。 -## 新手启动方式 +当前边界: -第一次使用请先阅读: +- 发送中心不是完全无人值守发布系统,遇到验证码、登录失效、平台风控、人工确认时必须人工处理。 +- `publish_jobs.scheduled_at` 只是计划发布时间字段预留,当前没有后台定时调度器。 +- OAuth、平台 API Provider、复杂封面模板、AI 生图封面属于预留或后续能力。 +- 不包含多用户权限系统,不建议直接暴露到公网。 -```text -docs/PROJECT_GUIDE.md -``` - -里面按”准备环境、启动项目、打开页面、测试功能”的顺序写好了。 - ---- +## 新手快速启动(Windows) -## 本地开发启动 +更详细的保姆级教程见 [docs/WINDOWS_SETUP.md](docs/WINDOWS_SETUP.md)。 -如果你要在本地直接运行(不通过 Docker),按以下步骤操作: +1. 安装 Python 3.12 或更高版本。 +2. 安装 FFmpeg,并确保 PowerShell 里能执行: -### 1. 准备环境 +```powershell +ffmpeg -version +ffprobe -version +``` -- 安装 Python 3.12 或更高版本 -- 安装 FFmpeg(视频处理必需) +3. 打开 PowerShell,进入项目目录: -### 2. 创建虚拟环境并安装依赖 +```powershell +cd "C:\Users\10578\Documents\New project 2" +``` -打开终端(PowerShell),进入项目根目录: +4. 创建并启用虚拟环境: ```powershell python -m venv .venv .\.venv\Scripts\Activate.ps1 -pip install -r requirements.txt ``` -### 3. 配置环境变量 +5. 安装依赖: ```powershell -copy .env.example .env +pip install -r requirements.txt ``` -然后打开 `.env`,填写你自己的 API Key 和本地路径。 - -### 4. 启动开发服务器 +6. 复制配置模板: ```powershell -uvicorn app.main:app --reload --port 8001 +copy .env.example .env ``` -### 5. 打开页面 - -浏览器访问: +7. 按需编辑 `.env`。如果电脑没有 `E:` 盘,请把 `STORAGE_ROOT` 和 `TASKS_DIR` 改成你真实存在的目录,例如: ```text -http://127.0.0.1:8001 +STORAGE_ROOT=C:\NiuMaStudio\tasks +TASKS_DIR=C:\NiuMaStudio\tasks ``` ---- - -## 运行测试 - -项目使用 pytest 进行测试。在终端中执行: +8. 启动后台: ```powershell -# 确保虚拟环境已激活 -.\.venv\Scripts\Activate.ps1 - -# 运行全部测试 -pytest -v +.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8001 +``` -# 只运行某个测试文件 -pytest -v tests/test_job_queue.py +9. 浏览器打开: -# 运行测试并显示覆盖率(需要先 pip install pytest-cov) -pytest --cov=app --cov-report=term-missing +```text +http://127.0.0.1:8001 ``` -> 测试环境使用独立的 SQLite 数据库,不会影响你的真实数据。 - ---- +看到任务列表页面,就说明前后端已经启动。 ## Docker 启动 -推荐启动方式:Docker 一键启动。 +Docker 是可选方式。当前 `docker-compose.yml` 默认把 Windows 的 `E:/直播间切片工作流存储` 挂载到容器内 `/workspace/tasks`。 -```powershell -docker compose up --build +如果你的电脑没有 E 盘,请先打开 `docker-compose.yml`,把这一行左侧路径改成真实存在的目录: + +```yaml +- E:/直播间切片工作流存储:/workspace/tasks ``` -启动后在浏览器打开: +启动命令: -```text -http://127.0.0.1:8001 +```powershell +docker compose up --build ``` -停止项目: +停止命令: ```powershell docker compose down ``` -Docker 启动说明: -- 容器内 Python 3.12,已预装 FFmpeg -- `.env` 文件会被自动加载(如果存在) -- 存储目录 `E:\直播间切片工作流存储` 会自动挂载到容器内 -- 代码目录和 prompts 目录以 volume 方式挂载,支持热更新 - ---- - -## 安全注意事项 - -### API Key 保护 - -- **真实 API Key 只能放在 `.env` 文件里**,绝对不能硬编码在代码中 -- `.env` 已写入 `.gitignore`,不会提交到 Git -- 提交代码前请确认没有无意中提交 `.env` 文件: - - ```powershell - git status - ``` - -- 仓库中只保留 `.env.example` 模板,方便以后重新配置 +## 真实任务流程 -### Git 提交安全 +完整说明见 [docs/TASK_FLOW.md](docs/TASK_FLOW.md)。 -- 每次提交前,确保没有包含以下内容: - - 真实的 API Key / Token / Secret - - `.env` 文件 - - 数据库文件(`*.sqlite3`、`*.db`) - - 视频 / 音频文件 - - 日志文件 - -### 运行环境安全 +```text +创建任务 +→ 上传 / 导入视频 +→ 提取 audio/source.wav +→ 转写 transcripts/transcript.md +→ AI 分析 analysis/candidate_clips.json + clip_candidates +→ 人工审核候选片段 +→ 切片 05_clips +→ 字幕成片 06_subtitled +→ 发送中心 publish_jobs + 07_covers +→ opencli 辅助浏览器投稿 +``` -- 本项目设计在**本地**运行,不要直接暴露到公网 -- `LOCAL_ADMIN_TOKEN` 用于管理接口鉴权,生产环境请使用随机长字符串 -- 定期更新依赖:`pip install --upgrade -r requirements.txt` +## 运行测试 -详细说明见: +在项目根目录执行: -```text -docs/SECURITY_AND_GIT.md +```powershell +.\.venv\Scripts\python.exe -m ruff check app tests +.\.venv\Scripts\python.exe -m pytest -q ``` -## 文档入口 +测试会使用本地测试数据库和测试目录,不应该影响真实任务数据。 -```text -docs/PROJECT_GUIDE.md 新手项目总览与启动说明 -docs/SECURITY_AND_GIT.md API Key、.env、Git 提交安全说明 -docs/ARCHITECTURE.md 系统架构 -docs/TASK_FLOW.md 任务处理流程 -docs/DATABASE_SCHEMA.md 数据库表结构 -docs/AI_ANALYSIS.md AI 分析配置与流程 -docs/CLIP_REVIEW.md 候选片段审核说明 -docs/VIDEO_CUTTING.md 自动切割说明 -docs/UI_REFERENCE.md UI 页面与设计参考 -VERSION 当前版本号 -DEVELOPMENT_LOG.md 开发记录 -NEXT_STEPS.md 下一步计划 -``` +## 安全注意事项 + +- 真实 API Key、Token、Cookie、账号密码只能放在 `.env`,不要写进代码或文档。 +- `.env`、数据库、日志、视频、音频、任务产物已被 Git 忽略。 +- `.env.backup_*` 这类本地备份文件也可能含密钥,清理前请确认自己还需不需要。 +- 项目默认本地使用,不建议开放公网访问。 +- 不要把浏览器缓存、Chrome Profile、平台 Cookie 提交到 Git。 ## 目录结构 ```text -app/ FastAPI 主应用 +app/ FastAPI 应用、路由、服务、模板、静态资源 app/core/ 配置读取 -app/db/ SQLite 数据库连接 -app/models/ 数据模型 -app/routers/ 页面路由与 API 路由 -app/services/ 任务、存储、转写、AI、切割等服务 -app/templates/ Jinja2 页面模板 -app/static/ CSS 与 JavaScript -data/ 本地数据库目录,真实数据不提交 +app/db/ SQLite 建表、迁移、种子数据 +app/models/ Pydantic 数据模型 +app/routers/ 页面和 API 路由 +app/services/ 任务、存储、转写、AI、切片、字幕、发送中心服务 +data/ 本地数据库和日志目录,真实数据不提交 tasks/ 任务产物目录,真实视频和切片不提交 -Dockerfile Docker 镜像构建文件 -docker-compose.yml Docker 一键启动配置 docs/ 项目文档 prompts/ AI 分析 Prompt -scripts/ 本地测试脚本 +scripts/ Windows 启动、opencli、手动诊断脚本 +tests/ pytest 自动化测试 ``` +## 文档入口 +- [docs/WINDOWS_SETUP.md](docs/WINDOWS_SETUP.md):Windows 新手安装与启动。 +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md):系统架构和模块边界。 +- [docs/TASK_FLOW.md](docs/TASK_FLOW.md):任务状态和完整工作流。 +- [docs/DATABASE_SCHEMA.md](docs/DATABASE_SCHEMA.md):SQLite 表结构。 +- [docs/AI_ANALYSIS.md](docs/AI_ANALYSIS.md):AI 分析配置与流程。 +- [docs/CLIP_REVIEW.md](docs/CLIP_REVIEW.md):候选片段审核。 +- [docs/VIDEO_CUTTING.md](docs/VIDEO_CUTTING.md):自动切片。 +- [docs/UI_REFERENCE.md](docs/UI_REFERENCE.md):页面和设计参考。 +- [docs/SECURITY_AND_GIT.md](docs/SECURITY_AND_GIT.md):密钥与 Git 安全。 +- [DEVELOPMENT_LOG.md](DEVELOPMENT_LOG.md):开发记录。 +- [NEXT_STEPS.md](NEXT_STEPS.md):下一步计划。 diff --git a/app/core/config.py b/app/core/config.py index d5297fb..062ba3b 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -66,7 +66,6 @@ class Settings: default_max_clip_minutes: int = 2 default_candidate_count: int = 8 default_cut_strategy: str = "accurate" - local_admin_token: str = _env("LOCAL_ADMIN_TOKEN", "") ffmpeg_timeout: int = int(_env("FFMPEG_TIMEOUT", "600")) ai_provider: str = _env_first(("AI_PROVIDER", "AI_DEFAULT_PROVIDER"), "remote") ai_default_provider: str = _env("AI_DEFAULT_PROVIDER", "remote") diff --git a/app/models/task.py b/app/models/task.py index 6239179..9e528fe 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Literal, Optional -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, field_validator class TaskStatus(str, Enum): @@ -131,7 +131,7 @@ class PublishBatchJobCreate(BaseModel): description: Optional[str] = Field(default="", max_length=2000) tags: Optional[str] = Field(default="", max_length=500) - @validator("output_clip_ids") + @field_validator("output_clip_ids") def validate_output_clip_ids(cls, value: list[str]) -> list[str]: if not value: raise ValueError("至少选择一条切片") @@ -220,7 +220,7 @@ class AIClipItem(BaseModel): confidence_score: float = Field(..., ge=0, le=1) selected_by_default: bool = True - @validator("start_time", "end_time") + @field_validator("start_time", "end_time") def validate_time_text(cls, value: str) -> str: parts = value.split(":") if len(parts) not in {2, 3}: diff --git a/app/services/ai_analysis_workflow_service.py b/app/services/ai_analysis_workflow_service.py index 366b567..b9ab00a 100644 --- a/app/services/ai_analysis_workflow_service.py +++ b/app/services/ai_analysis_workflow_service.py @@ -161,44 +161,58 @@ def _clear_clip_candidates(task_id: str) -> None: connection.commit() +def _insert_clip_candidate_rows(connection, task_id: str, clips: list[dict], now: str) -> None: + for index, clip in enumerate(clips, start=1): + clip_key = str(clip["clip_id"]) + database_id = f"{task_id}_{clip_key}"[:120] + selected_by_default = bool(clip.get("selected_by_default", True)) + connection.execute( + """ + INSERT INTO clip_candidates ( + id, task_id, clip_key, title, start_time, end_time, duration_seconds, + summary, reason, highlight_reason, spread_value, suggested_editing, + confidence_score, selected_by_default, enabled, reviewed, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?) + """, + ( + database_id or f"{task_id}_clip_{index:03d}", + task_id, + clip_key, + clip["title"], + clip["start_time"], + clip["end_time"], + clip["duration_seconds"], + clip["summary"], + clip["highlight_reason"], + clip["highlight_reason"], + clip["spread_value"], + clip["suggested_editing"], + clip["confidence_score"], + 1 if selected_by_default else 0, + 1 if selected_by_default else 0, + now, + now, + ), + ) + + def _insert_clip_candidates(task_id: str, clips: list[dict]) -> None: from app.services.task_service import _now_iso now = _now_iso() with get_connection() as connection: - for index, clip in enumerate(clips, start=1): - clip_key = str(clip["clip_id"]) - database_id = f"{task_id}_{clip_key}"[:120] - selected_by_default = bool(clip.get("selected_by_default", True)) - connection.execute( - """ - INSERT INTO clip_candidates ( - id, task_id, clip_key, title, start_time, end_time, duration_seconds, - summary, reason, highlight_reason, spread_value, suggested_editing, - confidence_score, selected_by_default, enabled, reviewed, created_at, updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?) - """, - ( - database_id or f"{task_id}_clip_{index:03d}", - task_id, - clip_key, - clip["title"], - clip["start_time"], - clip["end_time"], - clip["duration_seconds"], - clip["summary"], - clip["highlight_reason"], - clip["highlight_reason"], - clip["spread_value"], - clip["suggested_editing"], - clip["confidence_score"], - 1 if selected_by_default else 0, - 1 if selected_by_default else 0, - now, - now, - ), - ) + _insert_clip_candidate_rows(connection, task_id, clips, now) + connection.commit() + + +def _replace_clip_candidates(task_id: str, clips: list[dict]) -> None: + from app.services.task_service import _now_iso + + now = _now_iso() + with get_connection() as connection: + connection.execute("DELETE FROM clip_candidates WHERE task_id = ?", (task_id,)) + _insert_clip_candidate_rows(connection, task_id, clips, now) connection.commit() @@ -476,9 +490,7 @@ def restore_ai_analysis_run(task_id: str, run_id: str) -> dict: raise ValueError("这条历史记录已损坏,无法恢复") from exc _write_analysis_payload(task_id, payload) - # 先生成新结果,再清除旧的(安全顺序) - _insert_clip_candidates(task_id, payload.get("clips") or []) - _clear_clip_candidates(task_id) + _replace_clip_candidates(task_id, payload.get("clips") or []) # 切换 active 到被恢复的 run with get_connection() as connection: connection.execute( @@ -608,9 +620,7 @@ def process_task_ai_analysis(task_id: str, provider: str | None = None) -> dict: prompt_preset = get_task_ai_prompt_preset(task_id) provider_label = _ai_provider_label(used_provider) model_name = _ai_model_name(used_provider) - # 先插入新候选片段,成功后再清除旧的(避免"先删后建"导致丢失) - _insert_clip_candidates(task_id, analysis_payload["clips"]) - _clear_clip_candidates(task_id) + _replace_clip_candidates(task_id, analysis_payload["clips"]) # 插入新的 AI 分析历史 run(自动标记 is_active=1,旧 run 取消激活) analysis_run = _insert_ai_analysis_run( task_id=task_id, diff --git a/app/services/subtitle_workflow_service.py b/app/services/subtitle_workflow_service.py index 60a6544..e4e81fc 100644 --- a/app/services/subtitle_workflow_service.py +++ b/app/services/subtitle_workflow_service.py @@ -9,6 +9,7 @@ from typing import Any from uuid import uuid4 +from app.core.config import settings from app.services.storage_service import get_artifact_paths, resolve_video_file_path from app.services.transcript_service import read_transcript_range @@ -387,7 +388,14 @@ def render_subtitles_for_output_clip(task_id: str, output_clip_id: str) -> dict: "copy", str(output_path), ] - result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace") + result = subprocess.run( + command, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=settings.ffmpeg_subtitle_timeout, + ) if result.returncode != 0: raise RuntimeError(result.stderr.strip() or "FFmpeg 字幕生成失败") except Exception as exc: diff --git a/app/services/task_service.py b/app/services/task_service.py index 4d0d34e..31af44e 100644 --- a/app/services/task_service.py +++ b/app/services/task_service.py @@ -294,27 +294,31 @@ def _probe_video(path: Path | None) -> dict[str, str]: duration = None ffprobe = shutil.which("ffprobe") if ffprobe: - result = subprocess.run( - [ - ffprobe, - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - str(path), - ], - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - if result.returncode == 0: - try: - duration = float(result.stdout.strip()) - except ValueError: - duration = None + try: + result = subprocess.run( + [ + ffprobe, + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + str(path), + ], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=settings.ffprobe_timeout, + ) + if result.returncode == 0: + try: + duration = float(result.stdout.strip()) + except ValueError: + duration = None + except subprocess.TimeoutExpired: + duration = None return {"duration": _format_duration(duration), "video_size": _format_file_size(path.stat().st_size)} diff --git a/docker-compose.yml b/docker-compose.yml index 89fffe7..711ef52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,5 +25,6 @@ services: - ./app:/app/app - ./prompts:/app/prompts - ./data:/app/data + # Windows 宿主机任务目录。没有 E 盘时,请把左侧路径改成真实存在的目录。 - E:/直播间切片工作流存储:/workspace/tasks restart: unless-stopped diff --git a/docs/AI_ANALYSIS.md b/docs/AI_ANALYSIS.md index 7b1d7cf..eb58f24 100644 --- a/docs/AI_ANALYSIS.md +++ b/docs/AI_ANALYSIS.md @@ -1,104 +1,102 @@ -# AI 片段分析说明 +# AI 分析说明 -## 2026-05-23:AI Prompt 方案 +AI 分析负责把 `transcripts/transcript.md` 转成候选高光片段,并写入文件和数据库。 -任务详情页现在使用“AI Prompt 方案”管理 AI 分析 Prompt: +## 1. 前置条件 -- 全局共用 1、2、3 号方案,保存在 SQLite `ai_prompt_presets` 表。 -- 1 号方案默认使用直播切片分析专家 Prompt。 -- 每个任务通过 `ai_prompt_preset_id` 记录当前选中的方案。 -- AI 分析时读取当前任务选中的 Prompt,再替换 `{{MAX_CLIP_DURATION}}`、`{{TARGET_CLIP_COUNT}}`、`{{AI_PREFERENCE}}`、`{{TRANSCRIPT_TEXT}}`。 -- AI 输出 JSON 可以不包含 `task_id`,程序会自动补当前任务 ID 后继续校验和写入片段审核数据。 -- 点击远程 AI 分析前会弹出二次确认,避免误操作覆盖已有候选片段。 +任务必须已经完成转写,并存在: -## 1. 配置方式 +```text +transcripts/transcript.md +``` -项目根目录新增 `.env.example`。第一次使用时,把它复制为 `.env`,再填写真实配置。 +如果没有转写文件,AI 分析会停止并提示先转写。 -也可以在页面中配置:打开 `http://127.0.0.1:8001/system`,在“三类 AI 接口配置”里填写 `2. 分析文字稿,生成候选切片` 后保存。页面会把配置写入项目根目录 `.env`,真实 API Key 不会提交到 Git。 +## 2. Provider -页面保存规则: +当前支持: -- API Key 输入框会完整回显当前值,方便本机个人使用。 -- 保存时只更新页面相关配置键,会保留 `.env` 里的火山转写 Key、存储路径和其他无关配置。 -- 保存后当前运行中的服务会立即使用新配置;如果后续手动改 `.env`,建议重启服务。 +| provider | 说明 | +|---|---| +| `remote` | 远程 OpenAI-compatible / DeepSeek | +| `local` | 本地 Ollama | -远程文字稿分析接口配置: +`.env` 中的默认值: ```text AI_DEFAULT_PROVIDER=remote -AI_ANALYSIS_REMOTE_BASE_URL=https://api.deepseek.com -AI_ANALYSIS_REMOTE_API_KEY=你的文字稿分析 API Key -AI_ANALYSIS_REMOTE_MODEL=deepseek-v4-flash -AI_ANALYSIS_REMOTE_PROTOCOL=chat_completions -AI_ANALYSIS_REMOTE_REASONING_EFFORT= -AI_ANALYSIS_REMOTE_RESPONSES_PATH=/v1/responses -AI_ANALYSIS_REMOTE_DISABLE_RESPONSE_STORAGE=true -AI_ANALYSIS_REQUEST_TIMEOUT_SECONDS=120 ``` -本地 Ollama 配置: +远程分析主要配置: ```text -AI_LOCAL_BASE_URL=http://127.0.0.1:11434/v1 -AI_LOCAL_API_KEY=ollama -AI_LOCAL_MODEL=qwen3:8b -AI_LOCAL_PROTOCOL=chat_completions -AI_LOCAL_FALLBACK_PROTOCOL= +AI_ANALYSIS_REMOTE_BASE_URL +AI_ANALYSIS_REMOTE_API_KEY +AI_ANALYSIS_REMOTE_MODEL +AI_ANALYSIS_REMOTE_PROTOCOL ``` -系统状态页的本地 AI 配置已简化为 Ollama 模型下拉选择,默认可选 `qwen3:8b`、`gemma3:12b`、`qwen3:14b`。 +本地分析主要配置: -## 2. 分析链路 +```text +AI_LOCAL_BASE_URL +AI_LOCAL_API_KEY +AI_LOCAL_MODEL +AI_LOCAL_PROTOCOL +``` + +## 3. 真实分析流程 ```text -转写 Markdown -→ 用户点击“远程 AI 分析”或“本地 AI 分析” -→ 任务状态进入 ai_analyzing -→ 读取 prompts/clip_analysis_prompt.txt -→ 注入最大时长、候选数量、AI 偏好、转写文本 -→ Provider 调用 AI 接口 -→ 解析严格 JSON -→ Pydantic 校验字段 -→ 校验片段时长和转写时间范围 +读取 transcript.md +→ 选择 Prompt 方案 +→ 按长文本分块分析 +→ 合并候选片段 +→ 校验时间、字段和 JSON → 写入 analysis/candidate_clips.json -→ 写入 clip_candidates 表 -→ 任务状态进入 pending_review +→ 替换 clip_candidates 当前候选 +→ 写入 ai_analysis_runs 历史 +→ 任务进入 pending_review +``` + +远程和本地 provider 都会走长文本分块分析逻辑,不是整集一次性丢给模型。 + +## 4. 输出文件 + +```text +analysis/candidate_clips.json ``` -## 3. 失败处理 +该文件保存 AI 分析结果,供后续恢复、排查和人工审核参考。 -- 如果转写文本不存在,任务进入 `failed`,并记录错误。 -- 如果转写文本没有时间戳,任务进入 `failed`。 -- 如果 AI 第一次返回非法 JSON,程序会自动追加安全重试指令,再重试一次。 -- 如果重试后仍无法解析或校验,任务进入 `failed`,错误信息会显示在任务详情页。 -- 如果片段超过用户设置的最长时长,或起止时间超出转写文本范围,任务进入 `failed`。 +## 5. 数据库写入 -## 4. 如何测试 +- `clip_candidates`:当前可审核的候选片段。 +- `ai_analysis_runs`:每次分析的历史记录。 -不需要 API Key 的本地结构测试: +重新跑 AI 分析时,会用新结果替换当前候选片段。恢复历史 AI 分析时,也会把历史结果重新写回当前候选片段。 -```powershell -python scripts/test_ai_json_validation.py -python scripts/test_mock_transcript_analysis.py -``` +## 6. Prompt 方案 -远程 AI 连通性测试: +Prompt 方案保存在: -```powershell -python scripts/test_remote_ai_connection.py +```text +ai_prompt_presets +prompts/ ``` -本地 AI 连通性测试: +页面可以选择不同 Prompt。默认方案和综艺访谈方案由数据库初始化时写入。 -```powershell -python scripts/test_local_ai_connection.py -``` +## 7. 失败处理 + +- 没有转写文件:直接失败,提示先转写。 +- 远程 API 不可用:暂停远程分析,提示用户可手动改用本地 AI。 +- 模型返回非法 JSON:解析器会尽量修复常见格式问题,仍失败时任务进入 `failed`。 +- AI 候选过短:写入任务日志作为质量提醒,不阻止切片。 -页面测试: +## 8. 当前不做 -1. 启动项目。 -2. 打开任务详情页。 -3. 确认任务已经生成 `transcripts/transcript.md`。 -4. 点击“远程 AI 分析”或“本地 AI 分析”。 -5. 成功后进入片段审核页,查看候选片段列表。 +- 不自动选择最佳模型。 +- 不保证 AI 结果一定适合发布,仍需要人工审核。 +- 不把完整 API 响应保存到仓库。 +- 不在没有用户确认的情况下自动发布。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 986a7b6..15dffe0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,343 +1,146 @@ # 系统架构 -## 1. 当前架构概览 +本文档描述当前 v1.3.0 代码的真实结构。项目优先保证 Windows 本地可运行,不引入大型外部中间件。 -### 1.1 架构形态 - -当前 v1.3 为 **FastAPI 单体应用**,运行在 Windows 本地,所有组件打包在同一个进程中。 +## 1. 架构总览 ```text -┌─────────────────────────────────────────────────────────┐ -│ 浏览器 (127.0.0.1:8001) │ -└─────────────────────┬───────────────────────────────────┘ - │ HTTP -┌─────────────────────▼───────────────────────────────────┐ -│ FastAPI 单体应用 (uvicorn) │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ -│ │ routers/ │ │services/ │ │ services/ai/ │ │ -│ │ 页面+API │──│ 业务逻辑 │──│ AI Provider 抽象 │ │ -│ └──────────┘ └──────────┘ └────────────────────┘ │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ -│ │ models/ │ │ core/ │ │ db/ │ │ -│ │ Pydantic │ │ 配置管理 │ │ SQLite 连接+迁移 │ │ -│ └──────────┘ └──────────┘ └────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ -│ SQLite │ │ 本地文件系统 │ │ 外部 AI API │ -│ workflow │ │ 任务产物目录 │ │ DeepSeek / │ -│ .sqlite3 │ │ 视频/音频等 │ │ Ollama / │ -│ │ │ │ │ 火山引擎 │ -└─────────────┘ └──────────────┘ └──────────────────┘ -``` - -### 1.2 技术栈一览 - -| 层次 | 技术选型 | 说明 | -| --- | --- | --- | -| **Web 框架** | FastAPI + uvicorn | 异步 HTTP 服务,端口 8001 | -| **模板引擎** | Jinja2 | 后台页面渲染,Apple 风格 UI | -| **数据库** | SQLite | 单文件数据库,`data/workflow.sqlite3` | -| **数据校验** | Pydantic v2 | 请求/响应模型校验,AI 结果解析 | -| **文件存储** | 本地文件系统 | Windows 本地目录,默认 `E:\直播间切片工作流存储` | -| **视频处理** | FFmpeg / FFprobe | 音频提取、视频切割、字幕合成、封面帧 | -| **语音转写** | faster-whisper / 火山引擎 | 本地模型或远程 API,输出逐句时间戳 | -| **AI 分析** | DeepSeek API / Ollama | Provider 抽象层,支持 chat/completions 和 responses 协议 | -| **发布辅助** | opencli | 调用已登录 Chrome 辅助浏览器投稿 | -| **容器化** | Docker + docker-compose | 可选部署方式,开发/测试用 | - -### 1.3 核心设计决策 - -- **单体进程**:所有路由、服务、数据库访问在同一 Python 进程中,无独立 Worker。 -- **同步 FFmpeg**:视频处理通过 `subprocess` 同步调用,阻塞当前请求直到完成。 -- **无消息队列**:任务处理由前端按钮触发,无后台 Job Queue / Celery。 -- **无用户体系**:单用户本地使用,通过 `LOCAL_ADMIN_TOKEN` 做简易鉴权。 -- **无定时调度**:`publish_jobs.scheduled_at` 仅为字段预留,不自动发送。 - ---- - -## 2. 模块分层 - -```text -app/ -├── main.py ← FastAPI 应用入口,路由注册,中间件 -├── core/ -│ └── config.py ← 环境变量读取,Settings 数据类 -├── db/ -│ └── database.py ← SQLite 连接、建表、迁移、种子数据 -├── models/ -│ ├── task.py ← Task / ClipCandidate / OutputClip 等 Pydantic 模型 -│ └── settings.py ← 配置相关 Pydantic 模型 -├── routers/ -│ ├── pages.py ← 页面路由(Jinja2 模板渲染) -│ ├── tasks.py ← 任务 CRUD + 处理流程 API -│ ├── files.py ← 文件上传/路径选择 API -│ ├── media.py ← 媒体文件访问 API -│ ├── ai_prompts.py ← AI Prompt 方案管理 API -│ ├── publish.py ← 发送中心 API -│ └── settings.py ← 系统设置 API -└── services/ - ├── task_service.py ← 任务状态编排(含字幕渲染、发布队列集成) - ├── storage_service.py ← 任务目录与文件路径管理 - ├── transcript_service.py← 转写服务(faster-whisper + 火山引擎) - ├── video_cut_service.py ← FFmpeg 切割封装 - ├── ai_clip_service.py ← AI 片段分析编排 - ├── ai_config_service.py ← AI 配置读写 - ├── ai_prompt_preset_service.py ← Prompt 方案服务 - ├── publish_service.py ← 发送中心服务 - ├── publish_providers.py ← 发布平台 Provider - └── ai/ - ├── base.py ← AI Provider 抽象基类 - ├── local_model_provider.py ← Ollama 本地 Provider - ├── remote_responses_provider.py ← DeepSeek 远程 Provider - ├── ai_clip_analyzer.py ← AI 分析编排器 - └── diagnostics.py ← AI 连接诊断 -``` - ---- - -## 3. 数据存储 - -### 3.1 数据库 - -- **类型**:SQLite,单文件 `data/workflow.sqlite3` -- **连接方式**:`sqlite3.connect()`,每次请求 `@contextmanager` 获取连接 -- **迁移方式**:`init_db()` 启动时自动执行 `CREATE TABLE IF NOT EXISTS` + 逐列 ALTER TABLE 补齐 -- **种子数据**:启动时自动写入默认 AI Prompt 方案、字幕样式、平台配置 - -### 3.2 表结构(10 张表) - -| 表名 | 用途 | -| --- | --- | -| `tasks` | 任务主表,状态流转 | -| `clip_candidates` | AI 候选片段 | -| `output_clip` | 输出切片记录 | -| `ai_prompt_presets` | AI Prompt 方案(3 套) | -| `ai_analysis_runs` | AI 分析历史 | -| `subtitle_style_presets` | 字幕样式预设 | -| `subtitle_jobs` | 字幕任务 | -| `publish_platform_configs` | 平台 OAuth 配置 | -| `publish_accounts` | 发布账号 | -| `publish_jobs` | 发布任务队列 | - -详见 [DATABASE_SCHEMA.md](DATABASE_SCHEMA.md) - -### 3.3 文件存储 - -- 存储根目录由 `STORAGE_ROOT` / `TASKS_DIR` 环境变量配置,默认 `E:\直播间切片工作流存储` -- 每个任务一个子目录,以 `task_dir_name` 命名 -- 大文件(视频、音频)不入 Git、不入数据库,只存路径 - ---- - -## 4. AI Provider 架构 - -```text - ┌─────────────────────┐ - │ AI Provider 抽象 │ - │ (base.py) │ - └──────────┬──────────┘ - │ - ┌────────────────────┼────────────────────┐ - ▼ ▼ ▼ -┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐ -│ Remote Responses │ │ Local Model │ │ 火山引擎 ASR │ -│ (DeepSeek) │ │ (Ollama) │ │ (远程转写) │ -│ │ │ │ │ │ -│ chat/completions │ │ chat/ │ │ BigModel ASR │ -│ + /v1/responses │ │ completions │ │ Flash API │ -└──────────────────┘ └──────────────┘ └──────────────────┘ -``` - -- **Provider 抽象**:`BaseAIProvider` 定义统一接口,支持协议检测和自动降级 -- **远程**:OpenAI-compatible API,支持 chat/completions 和 responses 两种协议 -- **本地**:Ollama API,按小段拆分长文本后合并结果 -- **转写**:火山引擎(默认)或本地 faster-whisper,失败不自动降级,用户手动切换 - ---- - -## 5. 发送中心架构 - -```text -output_clip 生成 + 字幕完成 +浏览器 127.0.0.1:8001 │ ▼ -┌───────────────┐ -│ 发送中心页面 │ -│ /publish │ -└───────┬───────┘ +FastAPI 单体应用 │ - ▼ -┌───────────────────────────────────────────┐ -│ publish_service.py │ -│ │ -│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ -│ │ 队列刷新 │ │ AI 文案 │ │ 封面帧 │ │ -│ │ 双平台 │ │ 标题/简介│ │ 07_covers│ │ -│ └─────────┘ └─────────┘ └──────────┘ │ -│ │ -│ ┌─────────────────────────────────────┐ │ -│ │ opencli 辅助浏览器投稿 │ │ -│ │ 抖音 + B站 Chrome Profile │ │ -│ └─────────────────────────────────────┘ │ -└───────────────────────────────────────────┘ -``` - -**安全边界**:不绕过验证码、登录失效、风控和人工确认。 - ---- - -## 6. 部署架构 - -### 6.1 本地直接运行 - -```text -Windows 主机 -├── Python 3.12 + .venv -├── FFmpeg(系统安装) -├── uvicorn app.main:app --port 8001 -└── 浏览器 http://127.0.0.1:8001 + ├─ routers/ 页面路由与 API + ├─ services/ 任务、转写、AI、切片、字幕、发送中心 + ├─ models/ Pydantic 请求与结果模型 + ├─ db/ SQLite 建表、迁移、种子数据 + └─ templates/ Jinja2 页面 + │ + ├─ SQLite data/workflow.sqlite3 + ├─ 本地任务目录 tasks 或 STORAGE_ROOT + ├─ FFmpeg / FFprobe + ├─ 火山引擎 / faster-whisper + ├─ DeepSeek / OpenAI-compatible / Ollama + └─ opencli + 已登录 Chrome ``` -### 6.2 Docker 部署 +核心判断: + +- 这是 FastAPI 单体应用,不是前后端分离项目。 +- 前端不是 React / Vue,而是 Jinja2 模板 + 原生 JavaScript。 +- 数据库是 SQLite,不依赖 MySQL / PostgreSQL。 +- 切片异步任务使用本地 `workflow_jobs` 轻量队列,不使用 Celery / Redis。 +- 发送中心当前以 opencli 浏览器辅助为主,不绕过平台验证和人工确认。 + +## 2. 主要模块 + +| 目录或文件 | 作用 | +|---|---| +| `app/main.py` | FastAPI 应用入口、路由注册、中间件、静态资源挂载 | +| `app/core/config.py` | 读取 `.env` 和默认配置 | +| `app/db/database.py` | SQLite 表结构、迁移、默认数据 | +| `app/models/task.py` | 任务、候选片段、发布、AI 结果等模型 | +| `app/routers/pages.py` | Jinja2 页面入口 | +| `app/routers/tasks.py` | 任务创建、处理、切片、字幕相关 API | +| `app/routers/publish.py` | 发送中心 API | +| `app/services/storage_service.py` | 任务目录、上传文件、路径安全 | +| `app/services/transcript_service.py` | 音频转写和转写 Markdown 写入 | +| `app/services/ai_analysis_workflow_service.py` | AI 分析、候选片段写入、AI 历史恢复 | +| `app/services/video_cut_workflow_service.py` | 切片流程、cut run 版本化、失败回滚 | +| `app/services/subtitle_workflow_service.py` | 字幕样式、ASS 文件、字幕烧录 | +| `app/services/publish_service.py` | 发送队列、文案、封面帧、opencli 调用 | +| `app/services/job_service.py` | 本地轻量任务队列 | +| `app/templates/` | 后台页面模板 | +| `app/static/` | CSS、JS、图片、第三方前端资源 | + +`app/services/task_service.py` 目前仍是兼容门面,向旧代码导出任务相关函数;新增逻辑优先放在拆分后的服务文件中。 + +## 3. 任务目录结构 + +每个任务有独立目录。官方当前目录约定如下: ```text -Docker 容器 (niuma-studio) -├── Python 3.12 + FFmpeg(容器内预装) -├── uvicorn app.main:app --host 0.0.0.0 --port 8001 -├── 代码目录 volume 挂载(热更新) -├── 存储目录 volume 挂载(E:\ → /workspace/tasks) -└── opencli 桥接(host.docker.internal:8765) +任务目录/ +├─ source/ 原始视频 +├─ audio/source.wav 提取后的音频 +├─ transcripts/ transcript.md 和转写进度 +├─ analysis/ candidate_clips.json +├─ 05_clips/ 自动切片输出 +├─ 06_subtitled/ 字幕文件和带字幕视频 +├─ 07_covers/ 发送中心候选封面帧 +└─ logs/process.log 任务日志 ``` -详见 [DEPLOYMENT.md](DEPLOYMENT.md) +`clips/` 是兼容旧版本的历史目录,新文档和新产物说明统一使用 `05_clips/`。 ---- +## 4. 数据库 -## 7. 当前已实现功能 +当前 SQLite 由 `app/db/database.py` 初始化,真实表共 13 张: ```text -新建任务表单 -→ 上传视频 / 选择 NAS 路径 -→ FFmpeg 提取音频 -→ 转写(火山引擎远程 / faster-whisper 本地) -→ AI 候选片段分析(DeepSeek / Ollama) -→ 候选片段人工审核(启用/禁用/编辑时间) -→ FFmpeg 自动切割 → 05_clips/ -→ ASS 字幕 + FFmpeg 合成 → 06_subtitled/ -→ 发送中心队列 → opencli 辅助投稿(抖音 + B站) +tasks +clip_candidates +output_clip +ai_prompt_presets +ai_analysis_runs +subtitle_style_presets +subtitle_jobs +publish_platform_configs +publish_accounts +publish_jobs +oauth_states +workflow_jobs +cut_runs ``` ---- - -## 8. 架构演进路线 - -### 8.1 当前阶段:P2-1(已完成) - -- FastAPI 单体应用 -- SQLite 单文件数据库 -- 本地文件系统存储 -- FFmpeg 同步本地处理 -- 本地/远程 AI Provider -- opencli 发布辅助 -- 代码检查与 CI 流程 - -### 8.2 短期演进(P2-2 ~ P2-3) +启动时会执行 `CREATE TABLE IF NOT EXISTS` 和逐列迁移,尽量兼容旧数据库。 -**目标**:不改变单体形态,增强本地可靠性和安全边界。 +## 5. 工作流边界 -| 方向 | 具体措施 | -| --- | --- | -| **数据库** | SQLite 保持不变,增加 WAL 模式、备份脚本 | -| **Job Worker** | 引入本地后台 Job Queue(`threading` / `asyncio`),将耗时任务(转写、AI 分析、切割)异步化,不阻塞 HTTP 请求 | -| **安全增强** | `LOCAL_ADMIN_TOKEN` 鉴权强化,敏感操作确认对话框,操作日志记录 | -| **存储** | 支持 NAS 路径作为存储根目录,`STORAGE_ROOT` 可配置为网络路径 | -| **错误恢复** | 任务失败后可从中断点重试,而非从头开始 | - -### 8.3 中期演进(P3) - -**目标**:引入消息队列,API 与 Worker 分离,为远程访问做准备。 - -| 方向 | 具体措施 | -| --- | --- | -| **数据库** | 从 SQLite 迁移到 PostgreSQL,利用 JSONB、全文搜索、行级安全 | -| **消息队列** | 引入 Redis + RQ / Celery,任务处理从同步改为异步队列 | -| **Worker 分离** | API 服务与 Worker 进程独立部署,可横向扩展 Worker | -| **对象存储** | 支持 NAS / MinIO / S3 作为任务产物存储后端 | -| **配置管理** | 从 `.env` 文件迁移到结构化配置(YAML/TOML),支持多环境 | -| **健康检查** | 增加 Worker 心跳、任务超时检测、死信队列 | - -### 8.4 长期演进(P4+) - -**目标**:多用户支持,为团队协作和 SaaS 化打基础。 - -| 方向 | 具体措施 | -| --- | --- | -| **用户体系** | 用户注册/登录,JWT Token 鉴权,角色权限(admin/operator/viewer) | -| **多租户** | 按用户隔离任务数据、存储目录、AI 配额 | -| **发布账号托管** | 平台 OAuth Token 加密存储,自动刷新,权限范围最小化 | -| **任务配额** | 按用户/租户限制并发任务数、存储空间、AI 调用次数 | -| **审计日志** | 完整操作记录(谁、何时、做了什么、结果如何),不可篡改 | -| **监控告警** | Prometheus + Grafana,任务失败率、API 延迟、磁盘使用量告警 | -| **API 版本化** | `/api/v1/` → `/api/v2/`,向后兼容,废弃通知 | - ---- - -## 9. 当前明确不做的事 - -以下事项**暂不在任何阶段计划中**,等有明确需求后再评估: - -| 暂不做的 | 原因 | -| --- | --- | -| **SaaS 多租户** | 当前是个人本地工具,不需要租户隔离和计费系统 | -| **真实全自动发布** | 平台有验证码、风控、登录失效,全自动不可行也不安全 | -| **强依赖云部署** | 首版定位 Windows 本地工具,不应强制要求云服务器 | -| **移动端 App** | 核心工作流依赖 FFmpeg 和大文件处理,不适合移动端 | -| **实时直播流处理** | 当前是录播后处理,实时流需要完全不同的技术栈 | -| **多人协作编辑** | 当前是单人工作流,协作需要解决冲突合并和锁的问题 | -| **第三方平台 API 直接发布** | 抖音/B站开放平台 API 权限申请困难,opencli 浏览器辅助是务实选择 | - ---- - -## 10. 数据流转关系 +主任务状态只覆盖“从视频到切片”的主链路: ```text -source_video -→ {task_dir_name}/source/ -→ {task_dir_name}/audio/ -→ {task_dir_name}/transcripts/ -→ {task_dir_name}/analysis/ -→ 人工审核(片段审核页) -→ {task_dir_name}/05_clips/ ← 正式切片输出 -→ {task_dir_name}/06_subtitled/ ← 带字幕成片(字幕工作流,独立于主任务状态) -→ {task_dir_name}/07_covers/ ← 发送中心封面(发布工作流,独立于主任务状态) +pending_video +→ pending_processing +→ audio_extracting +→ transcribing +→ pending_ai +→ ai_analyzing +→ pending_review +→ cutting +→ completed / completed_with_errors / failed ``` ---- +字幕和发送中心是切片完成后的独立流程: -## 11. 服务接口一览 +- 字幕状态保存在 `subtitle_jobs`。 +- 发送任务状态保存在 `publish_jobs`。 +- `scheduled_at` 只是字段预留,没有定时调度器。 -| 服务文件 | 职责 | -| --- | --- | -| `transcript_service.py` | 本地 faster-whisper 转写、火山引擎远程转写、转写预览解析和进度管理 | -| `services/ai/` | AI 候选片段分析模块:Provider 抽象、远程 DeepSeek Provider、本地 Ollama Provider、AI JSON 解析和片段分析编排 | -| `video_cut_service.py` | FFmpeg 自动切割接口 | -| `storage_service.py` | 任务目录与文件路径管理接口(含 `task_dir_name` 分配、路径解析、视频文件校验) | -| `task_service.py` | 任务状态与业务编排接口(含字幕渲染、字幕样式、发布队列集成) | -| `publish_service.py` | 发送中心服务(opencli 队列管理、封面帧生成、AI 文案生成、内容安全清洗、平台发送脚本编排) | -| `ai_config_service.py` | 三类 AI 接口配置读写(音频转写 / 候选切片分析 / 发布文案生成) | +## 6. 外部依赖 ---- +| 能力 | 依赖 | 当前行为 | +|---|---|---| +| 视频探测 | FFprobe | 读取时长和文件大小,超时后降级显示未知 | +| 音频提取 | FFmpeg | 生成 `audio/source.wav` | +| 转写 | 火山引擎 / faster-whisper | 远程失败后提示手动改用本地 | +| AI 分析 | DeepSeek / OpenAI-compatible / Ollama | 长文本分块分析并合并候选 | +| 切片 | FFmpeg | 生成 `05_clips`,失败时保留旧活跃结果 | +| 字幕 | FFmpeg subtitles 滤镜 | 生成 `.ass` 和带字幕视频 | +| 发送中心 | opencli + Chrome | 辅助打开投稿页和填写信息,保留人工确认 | -## 12. 设计参考 +## 7. 安全边界 -UI 设计参考文件: +- 项目设计为本地单用户后台,不建议公网暴露。 +- `.env`、数据库、日志、视频、任务产物、浏览器缓存不提交 Git。 +- `LOCAL_ADMIN_TOKEN` 用于本地 API 写操作保护。 +- `ALLOWED_MEDIA_ROOTS` 控制可选择的外部媒体目录。 +- 不绕过抖音 / B站验证码、登录失效、风控或人工确认。 -```text -docs/design/live_streaming_slicing_workflow_ui_16x9.png -``` +## 8. 当前不做 -视觉方向:Apple 风格、简洁、高级、留白充足、轻量玻璃拟态、卡片式布局、蓝色作为主强调色,适合作为个人本地 AI 高光生产后台。 +- 不切换到 React / Vue。 +- 不引入 Celery / Redis / 大型调度系统。 +- 不做完全无人值守发布。 +- 不自动识别平台直播间和开播状态。 +- 不做多用户权限系统。 diff --git a/docs/CLIP_REVIEW.md b/docs/CLIP_REVIEW.md index 3457080..715a87d 100644 --- a/docs/CLIP_REVIEW.md +++ b/docs/CLIP_REVIEW.md @@ -1,120 +1,60 @@ -# 片段审核页说明 +# 候选片段审核说明 -## 页面入口 +候选片段审核位于 AI 分析之后、自动切片之前。它的作用是让用户确认哪些片段值得切。 -片段审核页入口: +## 1. 入口 ```text /tasks/{task_id}/clips/review ``` -旧入口 `/tasks/{task_id}/clips` 仍保留,方便兼容前面版本的链接。 +任务进入 `pending_review` 后,可以打开审核页面。 -## 真实数据来源 +## 2. 数据来源 -页面按 `task_id` 读取 SQLite 数据库中的 `clip_candidates` 表,不再使用静态 UI 或模拟候选片段。 +候选片段来自: -每条候选片段展示这些真实字段: +- `analysis/candidate_clips.json` +- `clip_candidates` 数据表 -- `enabled`:是否启用。 -- `title`:标题。 -- `start_time`:开始时间。 -- `end_time`:结束时间。 -- `duration_seconds`:片段时长。 -- `summary`:内容摘要。 -- `highlight_reason`:推荐理由。 -- `spread_value`:传播价值。 -- `suggested_editing`:剪辑建议。 -- AI 来源 / 模型:页面不再展示不易解释的置信度,改为展示本次候选片段由哪个 AI Provider 和模型生成。 +页面展示以数据库 `clip_candidates` 为准。 -## 人工编辑能力 +## 3. 可以编辑的内容 -当前支持在页面上修改: - -- 标题。 -- 开始时间。 -- 结束时间。 -- 是否启用。 -- 内容摘要。 - -保存后会写回 `clip_candidates` 表,并把 `reviewed` 标记为 `1`。 - -## 保存接口 - -单条更新接口: - -```text -POST /api/tasks/{task_id}/clips/{clip_id}/update -``` - -批量更新接口: - -```text -POST /api/tasks/{task_id}/clips/batch-update -``` - -保存时会做基础校验: - -- 时间格式必须是 `MM:SS` 或 `HH:MM:SS`。 -- `end_time` 必须大于 `start_time`。 -- `duration_seconds` 会根据起止时间自动重新计算。 -- 片段时长不能超过该任务的 `max_clip_duration`。 -- 校验失败时页面顶部会显示错误提示。 - -保存审核修改不会改变任务状态,任务会继续保持 `pending_review`,直到用户进入切割阶段。 +当前支持: -## 预览和转写抽屉 +- 修改标题。 +- 修改开始时间。 +- 修改结束时间。 +- 修改摘要。 +- 启用或停用候选片段。 +- 删除候选片段。 -- 左侧候选片段的“播放预览”按钮会控制右侧源视频播放器,自动跳到该片段开始时间,并在结束时间附近暂停。 -- 桌面端右侧预览栏约占页面内容区三分之一,并保持 sticky 固定;用户向下滚动候选片段列表时,播放器和审核操作按钮仍留在当前画面内。 -- 窄屏上下堆叠时,如果点击“播放预览”后播放器不在可视区域,页面会自动平滑滚到播放器位置。 -- “查看这一段转写”会打开右侧转写抽屉,按片段起止时间读取 `transcripts/transcript.md` 中的“逐句时间戳原文”。 -- “编辑出入点”会打开源监视器弹窗,用源视频、时间码、noUiSlider 蓝色双手柄范围和播放头辅助人工审核。 -- 源监视器支持设置当前播放位置为入点或出点、跳到入点或出点、预览当前片段、拖动左右手柄,以及用 1 / 5 / 15 秒步长微调;滑块会限制最短 1 秒,并遵守任务的最大片段时长。 -- 源监视器点击“应用到片段”后只回填当前页面的 `start_time` 和 `end_time`,仍需点击“保存修改”才会写回 SQLite。 -- 转写抽屉会优先使用页面上当前的起止时间,因此刚应用但未保存的出入点也可以立即核对原文。 -- 新增读取接口: +删除是软删除,会标记 `is_deleted = 1`,不是物理删除数据库行。 -```text -GET /api/tasks/{task_id}/clips/{clip_id}/transcript-excerpt -``` +## 4. 哪些片段会被切 -该接口也支持通过查询参数传入未保存的时间范围: +自动切片只读取: ```text -GET /api/tasks/{task_id}/clips/{clip_id}/transcript-excerpt?start_time=00:10:58&end_time=00:11:48 +enabled = 1 +is_deleted = 0 ``` -任务里的“单条最长 N 分钟”表示候选片段允许的最长时长,不代表 AI 必须按 N 分钟固定切片。AI 可以选择 6 秒、60 秒或更短的内容,人工审核时也可以改起止时间;保存时仍会校验不能超过该任务设置的最长时长。 - -## 筛选和排序 - -当前支持: - -- 全部片段。 -- 仅启用。 -- 高传播价值。 -- 按推荐分排序。 -- 按时间顺序排序。 +停用或删除的候选片段不会进入 `05_clips/`。 -## 后续切割流程 +## 5. 与 AI 历史的关系 -右侧审核操作区的“生成切片”按钮调用真实 FFmpeg 切割流程: +每次重新跑 AI 分析会替换当前候选片段,并写入一条 `ai_analysis_runs`。 -```text -POST /api/tasks/{task_id}/process/cuts -``` - -按钮会使用当前启用的候选片段生成输出文件,并在右侧“切片结果”区域展示输出记录。 - -## 2026-05-27 删除候选片段与紧凑审核 +恢复历史 AI 分析时: -片段审核页现在支持删除单条候选片段。点击候选卡片里的“删除”后,页面会立刻移除该卡片,并调用: - -```text -DELETE /api/tasks/{task_id}/clips/{clip_id} -``` +- 会把历史 payload 写回 `analysis/candidate_clips.json`。 +- 会用历史结果替换当前 `clip_candidates`。 +- 任务回到 `pending_review`。 -删除采用软删除方式,只把 `clip_candidates.is_deleted` 标记为 `1` 并写入 `deleted_at`,不会删除源视频、转写文件、AI 分析文件或已生成切片文件。候选片段列表、启用片段统计和自动切片流程默认排除已删除片段。 +## 6. 注意事项 -候选片段卡片也改为紧凑布局:标题、起止时间、摘要和常用操作默认展示;推荐理由、传播价值、剪辑建议和 AI 来源默认收进可展开区域,方便连续向下审核多个片段。 +- AI 候选片段不等于最终成片,仍建议人工检查开始和结束时间。 +- 如果片段太短或内容割裂,可以修改时间后再切。 +- 如果候选数量不合适,可以调整任务的候选数量或 Prompt 后重新分析。 diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 5235169..179c367 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,286 +1,183 @@ -# 数据库结构说明 - -## 2026-05-27:任务目录改为项目名 - -- `tasks` 表新增 `task_dir_name` 字段,用来记录任务在存储盘里的实际文件夹名。 -- `id` 仍是任务唯一 ID,用于数据库关联和网页地址;本地文件夹不再默认使用短 ID,而是使用 `task_dir_name`。 -- 新建任务时会根据 `task_name` 生成安全的 Windows 文件夹名;重名时自动追加序号,避免覆盖旧目录。 -- `DELETE /api/tasks/{task_id}` 现在会把 `is_deleted` 设为 `1`,写入 `deleted_at`,并把任务文件夹移动到存储根目录下的 `_回收站`;不会删除文件。 -- 一次性迁移脚本为 `scripts/migrate_task_dirs_to_project_names.py`,默认 dry-run,带 `--apply` 才会移动文件夹并更新路径字段。 - -## 2026-05-25:发布后台新增表 - -- 新增 `publish_platform_configs`:保存抖音 / B站开放平台应用配置、OAuth 地址、上传接口、创建 / 投稿接口和测试结果。 -- 新增 `publish_accounts`:保存发布账号、open_id / UID、access_token、refresh_token、授权状态和备注;不保存平台账号密码。 -- 新增 `publish_jobs`:保存每条切片的发布任务、视频来源、账号、标题、简介、标签、平台返回 ID、审核状态、错误码、错误信息和重试次数。 - -### publish_platform_configs 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `platform` | TEXT | 平台:`douyin` 或 `bilibili` | -| `client_key` | TEXT | Client Key / App Key | -| `client_secret` | TEXT | Client Secret,本地保存,页面脱敏展示 | -| `redirect_uri` | TEXT | OAuth 回调地址 | -| `upload_url` | TEXT | 视频上传接口 | -| `create_url` | TEXT | 创建视频 / 投稿接口 | -| `last_test_status` | TEXT | 最近一次配置检查状态 | -| `last_test_message` | TEXT | 最近一次配置检查说明 | - -### publish_accounts 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 账号记录 ID | -| `platform` | TEXT | 平台 | -| `account_name` | TEXT | 本地展示昵称 | -| `open_id` | TEXT | 开放平台 open_id | -| `access_token` | TEXT | 接口访问 token | -| `refresh_token` | TEXT | 刷新 token | -| `authorization_status` | TEXT | 授权状态:`manual` / `authorized` | -| `remark` | TEXT | 备注 | - -### publish_jobs 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 发布任务 ID | -| `task_id` | TEXT | 所属视频任务 ID | -| `output_clip_id` | TEXT | 所属输出切片 ID | -| `account_id` | TEXT | 发布账号 ID | -| `platform` | TEXT | 发布平台 | -| `publish_mode` | TEXT | `draft`、`manual_review`、`api_publish` 或 `opencli_publish` | -| `video_source` | TEXT | `original` 或 `subtitled` | -| `video_file_path` | TEXT | 本次发布使用的视频路径 | -| `title` | TEXT | 标题 | -| `description` | TEXT | 简介 / 正文 | -| `tags` | TEXT | 标签 | -| `status` | TEXT | `ready` / `publishing` / `published` / `failed` / `cancelled` | -| `audit_status` | TEXT | 平台审核状态 | -| `platform_item_id` | TEXT | 平台稿件 / 视频 ID | -| `platform_upload_id` | TEXT | 平台上传 ID | -| `error_code` | TEXT | 平台错误码 | -| `error_message` | TEXT | 错误说明 | -| `provider_response` | TEXT | 平台响应摘要 JSON | -| `retry_count` | INTEGER | 重试次数 | -| `scheduled_at` | TEXT | 计划发布时间(v1.2 仅字段预留,尚无后台定时调度器) | - -## 2026-05-23:AI Prompt 方案 - -- `tasks` 表新增 `ai_prompt_preset_id`,记录当前任务使用哪一套 AI 分析 Prompt。 -- 新增 `ai_prompt_presets` 表,用于保存全局共用的 1、2、3 号 Prompt 方案;2 号方案为空时会自动写入"综艺访谈完整上下文专家"Prompt,若 2 号已有内容且 3 号为空,则写入 3 号以避免覆盖已有 Prompt。 -- 新增 `ai_analysis_runs` 表,用于保存每一次 AI 分析历史,支持刷新后继续展示分析预览和恢复旧分析结果。 - -### ai_prompt_presets 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | Prompt 方案 ID,例如 `preset_001` | -| `slot` | INTEGER | 方案编号,当前固定为 1、2、3 | -| `name` | TEXT | 用户可编辑的方案名称 | -| `prompt_text` | TEXT | 完整 AI 分析 Prompt | -| `is_default` | INTEGER | 是否默认方案 | -| `created_at` | TEXT | 创建时间 | -| `updated_at` | TEXT | 更新时间 | - -### ai_analysis_runs 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 历史分析 ID | -| `task_id` | TEXT | 所属任务 ID | -| `run_number` | INTEGER | 第几次分析 | -| `provider` | TEXT | 实际使用的 AI 来源 | -| `provider_label` | TEXT | 页面展示用来源名称 | -| `model` | TEXT | 实际使用模型 | -| `ai_prompt_preset_id` | TEXT | 当次使用的 Prompt 方案 ID | -| `ai_prompt_preset_name` | TEXT | 当次使用的 Prompt 方案名称 | -| `requested_clip_count` | INTEGER | 当次请求输出的候选片段数量 | -| `clip_count` | INTEGER | 当次实际生成的候选片段数量 | -| `analysis_summary` | TEXT | 当次整体分析总结 | -| `fallback_notice` | TEXT | 远程降级本地等提示 | -| `analysis_payload_json` | TEXT | 完整 AI 分析结果 JSON | -| `created_at` | TEXT | 创建时间 | - -## 存储与数据库位置 - -当前数据库使用 SQLite,数据库文件默认位于项目目录: +# 数据库结构 + +当前数据库是 SQLite,默认路径为: ```text data/workflow.sqlite3 ``` -大型视频、音频、转写 Markdown 和后续输出文件不放进数据库,统一放在存储根目录下的任务目录中。存储根目录由 `STORAGE_ROOT` 或 `TASKS_DIR` 环境变量配置,默认值为 `E:\直播间切片工作流存储`。 - -任务产物路径由 `task_dir_name` 决定;`task_id` 只作为数据库关联、URL 和内部唯一 ID 使用,不再直接决定文件夹名。 - -## tasks 表 - -`tasks` 表用于保存直播视频处理任务的基础信息和状态。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 任务唯一 ID,创建时自动生成 | -| `task_name` | TEXT | 任务名称 | -| `task_dir_name` | TEXT | 存储盘实际任务文件夹名;正常任务通常等于项目名,已移入回收站的任务为 `_回收站\项目名` | -| `source_type` | TEXT | 视频来源:`upload` 或 `nas` | -| `platform` | TEXT | 平台类型:`douyin`、`bilibili`、`general` | -| `original_video_path` | TEXT | 本地上传视频路径,后续接真实上传后写入 | -| `nas_file_path` | TEXT | NAS / 本地已有视频路径 | -| `max_clip_duration` | INTEGER | 单条切片最长时长,单位:分钟;新建任务默认建议为 5 分钟 | -| `candidate_clip_count` | INTEGER | 希望 AI 输出的候选片段数量 | -| `ai_preference` | TEXT | AI 片段选择偏好 | -| `ai_prompt_preset_id` | TEXT | 当前使用的 AI Prompt 方案 ID | -| `status` | TEXT | 当前任务状态 | -| `progress` | INTEGER | 当前进度百分比,后续流水线推进时更新 | -| `error_message` | TEXT | 异常信息 | -| `is_deleted` | INTEGER | 是否已从页面列表隐藏,`1` 表示隐藏,文件不会被删除 | -| `deleted_at` | TEXT | 隐藏时间,ISO 格式 | -| `created_at` | TEXT | 创建时间,ISO 格式 | -| `updated_at` | TEXT | 更新时间,ISO 格式 | - -## 任务状态值 - -`status` 使用英文状态码保存,页面展示时再转换成中文。 - -| 状态码 | 中文展示 | -| --- | --- | -| `pending_video` | 待提交视频 | -| `pending_processing` | 待处理 | -| `audio_extracting` | 音频提取中 | -| `transcribing` | 转写中 | -| `pending_ai` | 待 AI 分析 | -| `ai_analyzing` | AI 分析中 | -| `pending_review` | AI 结果待检查 | -| `cutting` | 切割中 | -| `completed` | 已完成 | -| `completed_with_errors` | 部分完成,至少有一个切片成功,但也有切片失败 | -| `failed` | 失败 | - -注意:`completed` / `completed_with_errors` 表示"自动切割阶段结束",不是平台发布完成。字幕是 `subtitle_jobs` 独立流程,发送中心是 `publish_jobs` 独立流程,它们不直接混入 `tasks.status`。 - -## output_clip 表 - -`output_clip` 表用于保存每一条最终切片输出结果。视频文件本身仍保存在任务目录里,数据库只保存路径、状态和错误信息。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 输出记录唯一 ID | -| `task_id` | TEXT | 所属任务 ID | -| `clip_candidate_id` | TEXT | 来源候选片段 ID | -| `output_file_path` | TEXT | 输出视频完整路径 | -| `output_file_name` | TEXT | 输出视频文件名 | -| `status` | TEXT | 输出状态:`pending`、`processing`、`completed`、`failed` | -| `error_message` | TEXT | 单条切片失败原因 | -| `created_at` | TEXT | 创建时间,ISO 格式 | -| `updated_at` | TEXT | 更新时间,ISO 格式 | - -## subtitle_jobs 表 - -`subtitle_jobs` 表用于保存每条切片的字幕生成任务,是独立于 `tasks.status` 的字幕工作流。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 字幕任务 ID | -| `task_id` | TEXT | 所属任务 ID | -| `output_clip_id` | TEXT | 所属输出切片 ID | -| `style_preset_id` | TEXT | 字幕样式 ID | -| `status` | TEXT | 状态:`pending`、`processing`、`completed`、`failed` | -| `subtitle_file_path` | TEXT | 字幕文件路径(.ass) | -| `output_file_path` | TEXT | 带字幕视频输出路径 | -| `error_message` | TEXT | 错误信息 | -| `created_at` | TEXT | 创建时间 | -| `updated_at` | TEXT | 更新时间 | - -## subtitle_style_presets 表 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 样式 ID | -| `name` | TEXT | 样式名称 | -| `font_family` | TEXT | 字体 | -| `font_size` | INTEGER | 字号 | -| `position` | TEXT | 位置:`bottom_center`、`middle_lower`、`top_center` | -| `font_color` | TEXT | 字体颜色 | -| `stroke_color` | TEXT | 描边颜色 | -| `shadow_enabled` | INTEGER | 是否启用阴影 | -| `is_default` | INTEGER | 是否默认样式 | -| `created_at` | TEXT | 创建时间 | -| `updated_at` | TEXT | 更新时间 | - -## clip_candidates 表 - -`clip_candidates` 表用于保存 AI 分析生成、等待人工审核的候选短视频片段,也保存人工审核页写回的编辑结果。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | TEXT | 候选片段数据库 ID | -| `task_id` | TEXT | 所属任务 ID | -| `clip_key` | TEXT | AI 返回的片段 key,例如 `clip_001` | -| `title` | TEXT | 片段标题,可在审核页人工修改 | -| `start_time` | TEXT | 开始时间,保存为 `HH:MM:SS` | -| `end_time` | TEXT | 结束时间,保存为 `HH:MM:SS` | -| `duration_seconds` | INTEGER | 片段时长,单位秒,保存审核修改时自动重算 | -| `summary` | TEXT | 片段摘要,可在审核页人工修改 | -| `reason` | TEXT | 兼容旧字段,当前与推荐理由保持一致 | -| `highlight_reason` | TEXT | AI 推荐理由 | -| `spread_value` | TEXT | 传播价值 | -| `suggested_editing` | TEXT | 剪辑建议 | -| `confidence_score` | REAL | AI 置信度,范围 0 到 1 | -| `selected_by_default` | INTEGER | AI 是否建议默认启用 | -| `enabled` | INTEGER | 人工审核时是否启用,`1` 启用,`0` 禁用 | -| `reviewed` | INTEGER | 是否已人工修改或审核,保存后写为 `1` | -| `is_deleted` | INTEGER | 候选片段是否已从审核页隐藏,`1` 表示隐藏 | -| `deleted_at` | TEXT | 候选片段隐藏时间,ISO 格式 | -| `created_at` | TEXT | 创建时间,ISO 格式 | -| `updated_at` | TEXT | 更新时间,ISO 格式 | - -## 兼容说明 - -早期项目骨架曾使用过 `title`、`source_path`、`max_clip_minutes`、`target_clip_count` 等草稿字段。当前初始化逻辑会自动补齐新字段,并把旧字段数据迁移到当前字段中。 -为了不破坏已有本地数据库,旧字段不会被强制删除。后续代码以本文件列出的当前字段为准。 - -任务移入回收站采用软删除方式:`DELETE /api/tasks/{task_id}` 会把 `is_deleted` 改为 `1`、写入 `deleted_at`,并把该任务的存储目录移动到 `_回收站`。工作台、任务列表和片段审核总览默认不显示已移入回收站的任务,原视频、音频、转写、AI 分析文件、切片输出和字幕输出都会保留。 - -`clip_candidates.reason` 是早期推荐理由字段,当前审核页优先读取 `highlight_reason`。数据库初始化时会把已有 `reason` 自动补到 `highlight_reason`。 - -候选片段删除是软删除:`DELETE /api/tasks/{task_id}/clips/{clip_id}` 只更新数据库记录,不删除源视频、转写文件、AI 分析文件或已生成切片文件。 - -## 任务产物路径 - -任务产物路径当前由 `task_dir_name` 决定;`task_id` 只作为内部唯一 ID 使用,不再直接决定存储文件夹名。 - -历史任务可能仍兼容 `task_id` 目录,但新逻辑以 `task_dir_name` 为准。 - -正式任务目录结构: +测试环境会使用独立测试数据库,不应影响真实数据。 -```text -{task_dir_name}/ - source/ ← 上传源视频 - audio/ ← 提取的音频 source.wav - transcripts/ ← 转写结果 transcript.md - analysis/ ← AI 分析结果 candidate_clips.json - 05_clips/ ← 正式切片输出目录 - 06_subtitled/ ← 带字幕成片目录 - 07_covers/ ← 发送中心候选封面目录 - logs/ ← 处理日志 process.log -``` +## 1. 初始化方式 + +数据库初始化在 `app/db/database.py` 中完成: + +- `init_db()` 启动时执行。 +- 使用 `CREATE TABLE IF NOT EXISTS` 创建表。 +- 使用逐列 `ALTER TABLE` 迁移旧数据库。 +- 自动写入默认 AI Prompt、字幕样式、发布平台配置。 + +## 2. 当前真实表清单 + +当前代码会创建 13 张表: + +| 表名 | 作用 | +|---|---| +| `tasks` | 主任务表,保存任务名称、视频来源、状态、进度、错误信息 | +| `clip_candidates` | AI 候选片段,供人工审核和切片使用 | +| `output_clip` | 自动切片输出记录 | +| `ai_prompt_presets` | AI 分析 Prompt 方案 | +| `ai_analysis_runs` | 每次 AI 分析历史,可恢复 | +| `subtitle_style_presets` | 字幕样式预设 | +| `subtitle_jobs` | 字幕生成任务和结果 | +| `publish_platform_configs` | 抖音 / B站平台配置 | +| `publish_accounts` | 发布账号记录 | +| `publish_jobs` | 发送中心任务 | +| `oauth_states` | OAuth state 防重放记录,当前属于预留/安全基础设施 | +| `workflow_jobs` | 本地轻量任务队列,当前主要用于异步切片 | +| `cut_runs` | 每次切片运行记录,用于版本化和失败回滚 | + +## 3. 主任务表 tasks + +关键字段: + +- `id`:任务 ID。 +- `task_name`:任务名称。 +- `task_dir_name`:任务目录名。 +- `source_type`:`upload` 或 `nas`。 +- `platform`:素材平台类型,如 `general`、`douyin`、`bilibili`。 +- `original_video_path`:上传视频路径。 +- `nas_file_path`:本地或 NAS 已有视频路径。 +- `max_clip_duration`:候选片段最长分钟数。 +- `candidate_clip_count`:希望 AI 生成的候选数量。 +- `ai_prompt_preset_id`:使用的 Prompt 方案。 +- `status`:主流程状态。 +- `progress`:页面进度百分比。 +- `error_message`:失败原因。 +- `is_deleted` / `deleted_at`:软删除标记。 + +## 4. AI 相关表 + +### clip_candidates + +保存当前可审核、可切片的候选片段。 + +关键字段: + +- `clip_key`:AI 返回的片段 key。 +- `title`:片段标题。 +- `start_time` / `end_time`:片段时间范围。 +- `duration_seconds`:时长。 +- `summary`:内容摘要。 +- `highlight_reason`:高光原因。 +- `spread_value`:传播价值。 +- `suggested_editing`:剪辑建议。 +- `confidence_score`:AI 置信度。 +- `enabled`:是否参与切片。 +- `reviewed`:是否已审核。 +- `is_deleted` / `deleted_at`:软删除。 + +### ai_analysis_runs + +保存每次 AI 分析的完整 payload,可用于恢复历史结果。 + +关键字段: + +- `run_number`:第几次分析。 +- `provider`:`remote` 或 `local`。 +- `provider_label`:展示名称。 +- `model`:模型名。 +- `ai_prompt_preset_id` / `ai_prompt_preset_name`:Prompt 信息。 +- `requested_clip_count`:请求候选数量。 +- `clip_count`:实际候选数量。 +- `analysis_payload_json`:完整 AI 结果 JSON。 +- `is_active`:当前激活结果。 + +## 5. 切片与字幕表 + +### cut_runs + +每次点击生成切片都会创建一条记录。 + +- 成功时激活当前 run。 +- 新 run 成功后旧 run 的输出会变为非活跃。 +- 新 run 全部失败时,旧活跃输出保留。 + +### output_clip + +保存每条切片结果。 + +关键字段: + +- `clip_candidate_id`:来源候选片段。 +- `cut_run_id`:来源切片运行。 +- `output_file_path` / `output_file_name`:文件路径和文件名。 +- `status`:`completed` 或 `failed`。 +- `error_message`:失败原因。 +- `is_active`:是否为当前活跃结果。 + +### subtitle_jobs + +保存字幕生成记录。 + +关键字段: + +- `output_clip_id`:对应切片。 +- `subtitle_file_path`:ASS 字幕文件。 +- `output_file_path`:带字幕视频。 +- `status`:`pending`、`processing`、`completed`、`failed`。 +- `is_active`:是否为当前活跃字幕结果。 + +## 6. 发送中心表 + +### publish_jobs + +保存发送中心任务。 + +关键字段: + +- `platform`:`douyin` 或 `bilibili`。 +- `provider`:当前实际以 `opencli` 为主。 +- `output_clip_id`:来源切片。 +- `video_source`:`original` 或 `subtitled`。 +- `title` / `description` / `tags`:发布文案。 +- `cover_file_path` / `cover_time_seconds`:封面信息。 +- `status`:`ready`、`publishing`、`published`、`failed`、`cancelled`。 +- `scheduled_at`:计划发布时间字段预留,不会自动调度。 + +### publish_platform_configs / publish_accounts + +用于保存平台配置和账号记录。当前发送中心主要依赖 opencli 调用已登录 Chrome,平台 API 能力属于预留边界。 + +### oauth_states + +用于 OAuth state 安全校验。当前属于安全基础设施和后续平台授权能力预留。 + +## 7. 本地轻量队列表 workflow_jobs + +`workflow_jobs` 是本项目自己的轻量队列表,当前主要用于异步切片。 + +关键字段: + +- `job_type`:任务类型。 +- `task_id`:关联主任务。 +- `status`:`queued`、`running`、`completed`、`failed`。 +- `progress`:任务进度。 +- `payload_json`:输入参数。 +- `result_json`:结果。 +- `error_message`:失败原因。 + +它不是 Celery,也不需要 Redis。 + +## 8. 文件不入库原则 + +大文件不写进 SQLite: + +- 原始视频。 +- 音频。 +- 切片视频。 +- 字幕视频。 +- 封面图。 +- 日志文件。 -| 产物 | 路径 | -| --- | --- | -| 任务目录 | `{存储根目录}/{task_dir_name}/` | -| 上传源视频 | `source/原文件名` | -| 提取音频 | `audio/source.wav` | -| 转写 Markdown | `transcripts/transcript.md` | -| AI 分析文件 | `analysis/candidate_clips.json` | -| 输出切片 | `05_clips/` | -| 带字幕成片 | `06_subtitled/` | -| 候选封面 | `07_covers/` | -| 处理日志 | `logs/process.log` | - -## 2026-06-09 v1.2 补充说明 - -- 发送中心当前已有发送队列和 opencli 辅助投稿能力,但还没有真正的定时调度器。 -- `publish_jobs.scheduled_at` 当前只是字段预留,可以保存计划发布时间,但 v1.2 还没有后台定时调度器,不会自动按 `scheduled_at` 发送。 -- 平台发送依赖 opencli 辅助浏览器操作,不绕过验证码、登录失效、风控和人工确认。 -- 代码中仍存在兼容性 `clips` 子目录(`TASK_SUBDIRECTORIES` 同时包含 `clips` 和 `05_clips`),新任务的正式输出目录是 `05_clips`。旧 `clips` 目录为兼容保留,不建议删除。 +数据库只保存路径、状态和元数据。真实文件保存在任务目录中。 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7a50a66..4296b18 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,303 +1,155 @@ -# 部署说明 +# 部署与启动说明 -## 1. 当前部署模式 +当前项目优先支持 Windows 本地运行。部署方式分为本地 Python 启动和 Docker 启动。 -v1.3 支持两种部署方式: +## 1. 推荐方式:Windows 本地 Python -### 方式 A:Windows 本地直接运行(推荐日常使用) +适合日常开发、调试、连接本机 FFmpeg、Ollama、opencli。 -``` -你的 Windows 电脑 -├── Python 3.12(系统安装) -├── FFmpeg(系统安装,需在 PATH 中) -├── 项目代码(任意目录) -├── .venv(Python 虚拟环境) -└── 浏览器打开 http://127.0.0.1:8001 -``` - -**适用场景**:日常使用、开发调试、单机处理。 +详细步骤见 [WINDOWS_SETUP.md](WINDOWS_SETUP.md)。 -### 方式 B:Docker 容器运行(推荐测试/隔离环境) +核心命令: +```powershell +cd "C:\Users\10578\Documents\New project 2" +.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8001 ``` -你的 Windows 电脑 -├── Docker Desktop -├── 项目代码(任意目录) -├── 容器 niuma-studio -│ ├── Python 3.12 + FFmpeg(容器内预装) -│ └── uvicorn 监听 8001 端口 -└── 浏览器打开 http://127.0.0.1:8001 -``` - -**适用场景**:不想装 Python/FFmpeg、测试环境隔离、CI 验证。 - ---- - -## 2. 方式 A 详细步骤:Windows 本地直接运行 - -### 2.1 环境要求 -| 软件 | 最低版本 | 检查命令 | -| --- | --- | --- | -| Python | 3.12 | `python --version` | -| FFmpeg | 4.0+ | `ffmpeg -version` | -| Git | 2.30+ | `git --version` | +成功后打开: -### 2.2 获取代码 - -打开 PowerShell,进入你想放项目的目录: - -```powershell -git clone <仓库地址> "New project 2" -cd "New project 2" +```text +http://127.0.0.1:8001 ``` -### 2.3 创建虚拟环境并安装依赖 +## 2. Docker 方式 -```powershell -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -pip install -r requirements.txt -``` +适合隔离 Python 环境。当前 Compose 配置: -看到 `Successfully installed ...` 即为成功。 +- 容器端口:`8001` +- 容器内数据目录:`/app/data` +- 容器内任务目录:`/workspace/tasks` +- 宿主机默认挂载:`E:/直播间切片工作流存储` -### 2.4 配置环境变量 +启动: ```powershell -copy .env.example .env +docker compose up --build ``` -然后用记事本或 VS Code 打开 `.env`,按注释填写: - -- `STORAGE_ROOT`:任务产物存放目录(默认 `E:\直播间切片工作流存储`) -- `AI_ANALYSIS_REMOTE_API_KEY`:DeepSeek API Key(可选,用远程 AI 分析时需要) -- `VOLCENGINE_ASR_API_KEY`:火山引擎转写 Key(可选,用远程转写时需要) -- `LOCAL_ADMIN_TOKEN`:管理接口鉴权 Token(可留空或设随机字符串) - -### 2.5 启动服务 +停止: ```powershell -uvicorn app.main:app --reload --port 8001 +docker compose down ``` -看到类似以下输出说明启动成功: +如果电脑没有 E 盘,请先修改 `docker-compose.yml`: -```text -INFO: Uvicorn running on http://127.0.0.1:8001 -INFO: Application startup complete. +```yaml +volumes: + - 你的真实Windows目录:/workspace/tasks ``` -### 2.6 打开页面 - -浏览器访问: +示例: -```text -http://127.0.0.1:8001 +```yaml +volumes: + - C:/NiuMaStudio/tasks:/workspace/tasks ``` -### 2.7 停止服务 +## 3. 配置文件 -在终端按 `Ctrl + C`。 - ---- - -## 3. 方式 B 详细步骤:Docker 运行 - -### 3.1 环境要求 - -- Docker Desktop for Windows(已安装并启动) - -### 3.2 配置环境变量 - -与方式 A 相同,先 `copy .env.example .env` 并填写配置。 - -### 3.3 构建并启动 +复制模板: ```powershell -docker compose up --build -``` - -首次启动会下载基础镜像并安装依赖,等待几分钟。 - -看到以下输出说明启动成功: - -```text -niuma-studio | INFO: Uvicorn running on http://0.0.0.0:8001 -niuma-studio | INFO: Application startup complete. -``` - -### 3.4 打开页面 - -```text -http://127.0.0.1:8001 +copy .env.example .env ``` -### 3.5 停止容器 - -```powershell -docker compose down -``` +关键配置: -### 3.6 Docker 特有说明 +| 配置 | 说明 | +|---|---| +| `STORAGE_ROOT` | 任务产物根目录 | +| `TASKS_DIR` | 任务目录,通常和 `STORAGE_ROOT` 一样 | +| `DATABASE_PATH` | SQLite 数据库路径 | +| `TRANSCRIPTION_PROVIDER` | `volcengine` 或 `local` | +| `AI_DEFAULT_PROVIDER` | `remote` 或 `local` | +| `LOCAL_ADMIN_TOKEN` | 本地写接口保护令牌 | +| `OPENCLI_HOST_BRIDGE_URL` | Docker 调用宿主机 opencli 桥接时使用 | -- **存储目录**:`docker-compose.yml` 默认将 `E:\直播间切片工作流存储` 挂载到容器内 `/workspace/tasks`。如果你的存储目录在其他位置,请修改 `docker-compose.yml` 中的 `volumes` 配置。 -- **代码热更新**:`app/` 和 `prompts/` 目录以 volume 方式挂载,修改代码后容器自动重载。 -- **Ollama 连接**:如果 Ollama 在宿主机运行,容器内通过 `http://host.docker.internal:11434/v1` 访问。 -- **opencli 桥接**:容器内通过 `http://host.docker.internal:8765` 访问宿主机上的 opencli 桥接服务。 +真实密钥只写入 `.env`,不要写入代码、文档或 `.env.example`。 ---- +## 4. 外部依赖 -## 4. 当前部署的局限性 +| 能力 | 必需依赖 | 检查命令 | +|---|---|---| +| 视频探测 / 切片 / 字幕 | FFmpeg + FFprobe | `ffmpeg -version` / `ffprobe -version` | +| 火山引擎转写 | 火山引擎 API Key | 检查 `.env` | +| 本地转写 | faster-whisper 依赖 | 运行本地转写时验证 | +| 本地 AI | Ollama | `ollama list` | +| 发送中心浏览器辅助 | opencli + 已登录 Chrome | `opencli --help` | -这些是当前版本的**已知限制**,不是 Bug,会在后续版本逐步改善: +## 5. 后台任务说明 -| 局限 | 说明 | 影响 | -| --- | --- | --- | -| **单进程** | API 和视频处理在同一进程 | 处理大视频时页面可能卡住(请求阻塞) | -| **无后台队列** | 没有独立 Worker 进程 | 转写、AI 分析、切割都在请求线程中同步执行 | -| **单机存储** | 任务产物必须在本地磁盘 | 不能跨机器共享任务数据 | -| **无负载均衡** | 不支持多实例部署 | 只能一个人用,不能横向扩展 | -| **无 HTTPS** | 只有 HTTP | 只适合本地使用,不要暴露到公网 | -| **单用户** | 没有登录和用户隔离 | 谁打开浏览器都能操作所有任务 | +当前没有 Celery、Redis 或独立 Worker。 ---- +代码里存在 `workflow_jobs` 本地轻量队列表,当前主要用于异步切片任务。它运行在同一个 FastAPI 进程内,不需要额外部署服务。 -## 5. 未来部署场景(规划中) +转写流程使用 FastAPI `BackgroundTasks`。AI 分析会在线程池里执行。视频切片既有同步入口,也有异步队列入口。 -### 5.1 短期:Windows 本地 + NAS 存储 +## 6. 发送中心边界 -```text -Windows 主机(运行 FastAPI) -└── 存储目录指向 NAS 网络路径 - └── \\192.168.1.100\share\切片工作流存储 -``` +发送中心当前主要做: -只需修改 `.env` 中的 `STORAGE_ROOT` 为 NAS 路径即可。无架构变更。 +- 从已完成切片生成抖音 / B站待发送任务。 +- 生成标题、简介、话题。 +- 生成候选封面帧。 +- 调用 opencli 辅助已登录 Chrome 打开和填写投稿页。 -### 5.2 中期:API + Worker 分离 +发送中心当前不做: -```text -┌─────────────────┐ ┌─────────────────────┐ -│ API 服务器 │────▶│ Redis Queue │ -│ (FastAPI) │ │ (消息队列) │ -│ 只做调度+查询 │ └─────────┬───────────┘ -└─────────────────┘ │ - ▼ - ┌─────────────────────┐ - │ Worker 1 (Windows) │ - │ Worker 2 (Windows) │ - │ Worker 3 (Linux) │ - │ 各自处理耗时任务 │ - └─────────────────────┘ - │ - ▼ - ┌─────────────────────┐ - │ PostgreSQL │ - │ + MinIO / NAS │ - └─────────────────────┘ -``` +- 不保证完全无人值守发布。 +- 不绕过验证码。 +- 不绕过登录失效。 +- 不绕过平台风控。 +- 不自动按 `scheduled_at` 定时发布。 -- API 服务器:轻量 FastAPI,只做任务创建、状态查询、页面渲染 -- Worker:独立进程,从 Redis 取任务,执行 FFmpeg/转写/AI 分析 -- 共享存储:NAS 或 MinIO,所有 Worker 可见 -- 共享数据库:PostgreSQL,API 和 Worker 共同读写 +## 7. 健康检查 -### 5.3 长期:多用户平台化 +启动后可以访问: ```text -┌──────────────────────────────────────────┐ -│ 负载均衡 / 反向代理 │ -│ (Nginx / Traefik) │ -└────────┬─────────────┬───────────────────┘ - ▼ ▼ -┌─────────────┐ ┌─────────────┐ -│ API 实例 1 │ │ API 实例 2 │ ← 无状态,可横向扩展 -└──────┬──────┘ └──────┬──────┘ - │ │ - └───────┬───────┘ - ▼ - ┌─────────────────┐ - │ PostgreSQL + │ - │ Redis │ - └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ Worker 集群 │ ← 按需扩缩 - │ (Celery / RQ) │ - └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ MinIO / S3 │ ← 对象存储 - │ (共享任务产物) │ - └─────────────────┘ +http://127.0.0.1:8001/health ``` ---- - -## 6. 当前不做的事 - -以下部署方式**当前明确不推荐、不维护**: - -| 不推荐 | 原因 | -| --- | --- | -| 直接暴露到公网 | 无 HTTPS、无用户认证、无速率限制,极不安全 | -| 云服务器生产部署 | 当前架构不支持多用户、无监控告警、无自动恢复 | -| Kubernetes 部署 | 单体应用无益于 K8s,过度设计 | -| 多实例负载均衡 | SQLite 不支持并发写,多实例会数据冲突 | -| macOS / Linux 主机部署 | 未测试,opencli 和部分路径逻辑依赖 Windows | - -如果将来需要上云或上 K8s,需先完成 P3(数据库升级 + Worker 分离)。 - ---- +成功时应返回健康状态 JSON。 -## 7. 环境变量参考 +## 8. 常见问题 -完整环境变量列表见 `.env.example`。以下是最关键的几个: +### 启动时报 E 盘不存在 -| 变量 | 默认值 | 说明 | -| --- | --- | --- | -| `STORAGE_ROOT` | `E:\直播间切片工作流存储` | 任务产物根目录 | -| `TASKS_DIR` | 同 `STORAGE_ROOT` | 任务目录(优先级高于 STORAGE_ROOT) | -| `DATA_DIR` | 项目目录 `data/` | 数据库存放目录 | -| `DATABASE_PATH` | `data/workflow.sqlite3` | 数据库文件路径 | -| `AI_ANALYSIS_REMOTE_API_KEY` | 空 | DeepSeek API Key | -| `TRANSCRIPTION_PROVIDER` | `volcengine` | 转写引擎:`volcengine` 或 `faster_whisper` | -| `AI_PROVIDER` | `remote` | AI 分析引擎:`remote` 或 `local` | +原因:默认任务目录是历史 Windows 路径 `E:\直播间切片工作流存储`。 ---- +解决: -## 8. 健康检查 +- 本地 Python:修改 `.env` 中的 `STORAGE_ROOT` 和 `TASKS_DIR`。 +- Docker:修改 `docker-compose.yml` 的 volume 左侧路径。 -服务启动后,可以访问健康检查接口确认运行正常: +### 页面能打开,但视频处理失败 -```text -GET http://127.0.0.1:8001/health -``` +优先检查: -正常返回: - -```json -{"status": "ok", "app": "NiuMa Studio"} +```powershell +ffmpeg -version +ffprobe -version ``` ---- - -## 9. 备份建议 - -### 当前版本(单机 SQLite) - -需手动备份两类数据: - -1. **数据库**:复制 `data/workflow.sqlite3` 到安全位置 -2. **任务产物**:复制 `STORAGE_ROOT` 下所有任务目录到安全位置 +如果命令不可用,说明 FFmpeg 没有安装或没有加入 PATH。 -建议定期(如每周)执行备份脚本(待开发)。 +### AI 或转写失败 -### 后续版本(PostgreSQL) +优先检查: -- 数据库:`pg_dump` 定期导出 -- 任务产物:MinIO / S3 自带的版本管理和复制功能 -- 备份自动化:CI 定时任务或 K8s CronJob +- `.env` 里是否填了真实 API Key。 +- `TRANSCRIPTION_PROVIDER` 是否为 `volcengine` 或 `local`。 +- `AI_DEFAULT_PROVIDER` 是否为 `remote` 或 `local`。 +- 本地 Ollama 是否已经启动。 diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md index 6a3659b..aa0f73a 100644 --- a/docs/PROJECT_GUIDE.md +++ b/docs/PROJECT_GUIDE.md @@ -2,6 +2,8 @@ 这份文档给不熟悉代码和终端的新手使用。你只需要按顺序做,不需要理解每一行命令背后的原理。 +> 2026-06-17 更新:Windows 本地启动请优先看 [WINDOWS_SETUP.md](WINDOWS_SETUP.md)。本文仍作为项目总览保留,后续会继续逐步合并到新的 Windows 文档体系。 + ## 1. 项目是做什么的 项目中文名:牛马片场。 diff --git a/docs/SUBTITLE_AND_PUBLISH_PLAN.md b/docs/SUBTITLE_AND_PUBLISH_PLAN.md index 8cbd3a2..aae3a0f 100644 --- a/docs/SUBTITLE_AND_PUBLISH_PLAN.md +++ b/docs/SUBTITLE_AND_PUBLISH_PLAN.md @@ -1,82 +1,55 @@ -# 字幕与一键推送功能计划书 +# 字幕与发送中心状态说明 -## 1. 最终目标 +这份文档原本是历史计划,现在改为当前实现状态说明。完整主流程见 [TASK_FLOW.md](TASK_FLOW.md)。 -把当前“上传视频 → 转写 → AI 分析 → 片段审核 → 自动切片”的流程继续向后延伸,形成: +## 1. 字幕工作流当前状态 -```text -生成切片 -→ 切片后视频进入字幕工作台 -→ 人工查看 / 删除 / 返回修改剪切 -→ 一键自动加字幕 -→ 可选打码 -→ 发布前检查 -→ 一键推送到抖音 / B站 -``` - -长期目标是整条链路自动化,但每个自动动作都保留人工复核入口,避免错误切片、字幕或发布内容直接流出。 - -## 2. v1.1 已先搭建的内容 - -- 新增“字幕推送”入口:`/subtitles`。 -- 切片生成后,`output_clip` 表里的输出视频会作为字幕工作流的输入队列。 -- 字幕工作台展示每个已切片任务、每条输出切片、本地文件路径和视频预览。 -- 预留“删除”“修改剪切”“自动加字幕”操作入口。 -- 预留字幕样式模板:字体、字号、位置、文字颜色、描边颜色、阴影开关。 -- 预留打码流程:后续可接入人脸、昵称、手机号、二维码等区域标注。 -- 预留一键推送流程:抖音 / B站账号保存、标题模板、简介模板和一键推送按钮。 -- 任务列表新增“后续工作流”列,用来提示切片后是否已经进入“待加字幕”。 - -## 3. 后续真实实现建议 +已实现: -### 阶段 A:字幕渲染 +- 从已完成切片读取时间范围。 +- 从 `transcripts/transcript.md` 中提取对应字幕文本。 +- 生成 ASS 字幕文件。 +- 使用 FFmpeg `subtitles` 滤镜烧录成带字幕视频。 +- 输出到 `06_subtitled/`。 +- 写入 `subtitle_jobs`。 +- 支持字幕 job 版本化:新字幕成功后激活,失败时保留旧活跃字幕。 -- 新增 `subtitle_jobs` 表,记录每个切片的字幕状态、样式模板、输出路径和错误信息。 -- 从转写文件或候选片段时间范围提取字幕文本。 -- 使用 FFmpeg `subtitles` / `drawtext` 或 ASS 字幕文件生成带字幕视频。 -- 输出目录建议: +当前边界: -```text -E:\直播间切片工作流存储\{task_id}\06_subtitled\ -``` - -### 阶段 B:剪切后复核 +- 字幕依赖已有转写文本质量。 +- 复杂字幕模板和批量样式管理仍是后续优化。 +- AI 生图封面不属于当前范围。 -- 给每条切片增加“删除记录 / 删除文件”“返回片段审核修改时间”“重新切片”。 -- 删除需要二次确认,默认只隐藏记录;真正删除本地文件要单独确认。 -- 重新切片后自动刷新字幕队列。 +## 2. 发送中心当前状态 -### 阶段 C:打码 +已实现: -- 新增 `mosaic_regions` 表,记录切片 ID、起止时间、区域坐标、打码强度。 -- 页面上提供视频画面标注区域,保存后用 FFmpeg filter 处理。 -- 第一版可以先支持固定区域打码;后续再考虑自动人脸识别。 +- 从已完成切片生成抖音 / B站待发送任务。 +- 支持选择原始切片或带字幕成片。 +- 生成标题、简介、话题。 +- 提取候选封面帧到 `07_covers/`。 +- 保存发送任务到 `publish_jobs`。 +- 使用 opencli 辅助已登录 Chrome 打开投稿页并填写信息。 +- 记录 `ready`、`publishing`、`published`、`failed`、`cancelled` 状态。 -### 阶段 D:一键推送 +当前边界: -- 新增 `publish_accounts` 和 `publish_jobs` 表。 -- 账号登录方式先做“浏览器会话预留”,后续再判断是否使用官方 API。 -- 每条发布任务记录平台、标题、简介、标签、计划发布时间、发布状态和错误信息。 -- 抖音、B站发布前必须保留预览确认,避免误发。 +- 不是完全无人值守发布。 +- 不绕过验证码、登录失效、平台风控。 +- 平台页面变化可能导致 opencli 操作失败,需要人工处理。 +- `scheduled_at` 只是字段预留,没有后台定时调度器。 -## 4. 风险与注意事项 +## 3. 目录约定 -- 自动发布属于高风险动作,后续必须加二次确认、日志和失败重试。 -- 平台账号登录会涉及隐私和风控,不能把账号密码明文写入项目文件。 -- 打码涉及敏感信息保护,建议先做人工标注,再做自动识别。 -- 字幕样式需要适配竖屏、横屏和不同平台裁切比例。 - -## 5. 下一步最小开发顺序 +```text +05_clips/ 原始切片 +06_subtitled/ 字幕文件和带字幕成片 +07_covers/ 候选封面帧 +``` -1. 建表:`subtitle_jobs`、`subtitle_style_presets`、`publish_jobs`。 -2. 把字幕样式从浏览器本地保存改为保存到 SQLite。 -3. 接入 FFmpeg 生成带字幕视频。 -4. 在字幕工作台展示“原切片 / 带字幕成片”对比预览。 -5. 做打码区域保存和固定区域打码。 -6. 最后再接入抖音 / B站推送。 +## 4. 后续建议 -## 2026-05-24 落地进度 -- `/subtitles` 已改为字幕任务列表,按视频任务进入单独字幕工作台。 -- 已新增 `subtitle_style_presets` 和 `subtitle_jobs`,字幕样式与字幕生成状态保存到 SQLite。 -- 自动加字幕已接入 FFmpeg:从 `transcript.md` 按切片时间范围生成 `.ass` 字幕,再输出到 `06_subtitled`。 -- 页面已支持原切片与带字幕成片预览;“修改剪切”已提供弹窗预览,保存能力后续接入片段审核数据。 +- 增强字幕样式预设的页面管理能力。 +- 为发送中心增加更清晰的失败原因和重试入口。 +- 将 opencli 平台页面变化风险写入操作手册。 +- 如未来要实现真正定时发送,需要新增后台调度器,而不是只依赖 `scheduled_at` 字段。 diff --git a/docs/TASK_FLOW.md b/docs/TASK_FLOW.md index 51f2351..0968b08 100644 --- a/docs/TASK_FLOW.md +++ b/docs/TASK_FLOW.md @@ -1,169 +1,222 @@ -# 任务状态流转 - -## 1. 主任务状态列表(tasks.status) - -| 状态码 | 中文展示 | 说明 | -| --- | --- | --- | -| `pending_video` | 待提交视频 | 任务已创建,尚未上传视频 | -| `pending_processing` | 待处理 | 视频已上传,等待开始处理 | -| `audio_extracting` | 音频提取中 | FFmpeg 正在提取音频 | -| `transcribing` | 转写中 | faster-whisper 或火山引擎正在转写 | -| `pending_ai` | 待 AI 分析 | 转写完成,等待 AI 分析 | -| `ai_analyzing` | AI 分析中 | DeepSeek 或本地 Ollama 正在分析 | -| `pending_review` | AI 结果待检查 | AI 候选片段已生成,可在片段审核页检查 | -| `cutting` | 切割中 | FFmpeg 正在逐条切割视频 | -| `completed` | 已完成 | 所有启用片段切割成功,自动切割阶段结束 | -| `completed_with_errors` | 部分完成 | 至少一个片段切割成功,但也有片段失败 | -| `failed` | 失败 | 全部失败或前置阶段出现不可恢复错误 | - -## 2. 视频处理主流程 +# 任务流程与状态流转 + +本文档按当前 v1.3.0 代码描述真实流程。主任务负责从视频到切片;字幕和发送中心是切片后的独立工作流。 + +## 1. 主任务状态 + +| 状态码 | 中文含义 | 说明 | +|---|---|---| +| `pending_video` | 待提交视频 | 任务已创建,尚未上传或绑定视频 | +| `pending_processing` | 待处理 | 视频已准备好,等待提取音频和转写 | +| `audio_extracting` | 音频提取中 | FFmpeg 正在生成 `audio/source.wav` | +| `transcribing` | 转写中 | 火山引擎或 faster-whisper 正在转写 | +| `pending_ai` | 待 AI 分析 | 转写完成,等待 AI 生成候选片段 | +| `ai_analyzing` | AI 分析中 | 远程或本地模型正在分析文字稿 | +| `pending_review` | 待审核片段 | 候选片段已写入,可人工审核 | +| `cutting` | 切片中 | FFmpeg 正在生成短视频 | +| `completed` | 已完成 | 启用片段全部切片成功 | +| `completed_with_errors` | 部分完成 | 至少一条切片成功,同时有失败片段 | +| `failed` | 失败 | 前置阶段失败或全部切片失败 | + +主流程: ```text -pending_video ← 任务已创建,尚未上传视频 -→ pending_processing ← 视频已提交,等待处理 -→ audio_extracting ← FFmpeg 提取音频 -→ transcribing ← 语音转写(faster-whisper 本地 / 火山引擎远程) -→ pending_ai ← 转写完成,等待 AI 分析 -→ ai_analyzing ← AI 正在分析(DeepSeek 远程 / Ollama 本地) -→ pending_review ← AI 候选片段已生成 -→ cutting ← FFmpeg 逐条切割 -→ completed ← 所有启用片段切割成功 -→ completed_with_errors ← 至少一个成功,部分失败 -→ failed ← 全部失败或不可恢复错误 +pending_video +→ pending_processing +→ audio_extracting +→ transcribing +→ pending_ai +→ ai_analyzing +→ pending_review +→ cutting +→ completed / completed_with_errors / failed ``` -**重要说明:** -- `completed` / `completed_with_errors` 代表"自动切割阶段结束",不是平台发布完成。 -- 字幕和发布是独立于主任务状态的后续工作流。 +`completed` 只代表自动切片阶段结束,不代表已经发布到平台。 + +## 2. 创建任务 + +入口: + +- 上传视频:`POST /api/tasks/upload` +- 选择已有视频:通过任务创建接口保存 `nas_file_path` + +主要行为: + +- 创建 `tasks` 记录。 +- 分配 `task_dir_name`。 +- 上传文件写入 `source/`。 +- 已有视频路径必须在允许的媒体根目录内。 +- Windows 文件名会做非法字符清理,避免路径穿越。 -## 3. 失败流转 +## 3. 音频提取与转写 -任意处理阶段出现不可恢复错误时,任务进入: +入口: + +- 页面点击“开始处理 / 继续处理”。 +- 后端会先检查是否已有 `audio/source.wav`。 + +产物: ```text -failed +audio/source.wav +transcripts/transcript.md +transcripts/transcript_progress.json ``` -失败状态需要记录: +当前 provider: -- 出错阶段。 -- 错误信息。 -- 相关文件路径。 -- 是否可以重试。 +- `TRANSCRIPTION_PROVIDER=volcengine`:火山引擎远程转写。 +- `TRANSCRIPTION_PROVIDER=local`:本地 faster-whisper。 -## 4. 首版进度占比 +注意: -| 状态 | 进度 | -| --- | --- | -| pending_video | 0% | -| pending_processing | 5% | -| audio_extracting | 20% | -| transcribing | 40% | -| pending_ai | 55% | -| ai_analyzing | 65% | -| pending_review | 72% | -| cutting | 88% | -| completed / completed_with_errors | 100% | +- 远程转写失败后,当前不会自动切到本地模型;页面会提示用户手动改用本地转写。 +- 本地 faster-whisper 默认按 120 秒切块,重叠 5 秒。 +- `transcript.md` 是后续 AI 分析、字幕提取和片段审核复用的核心文件。 -## 5. AI 分析阶段 +## 4. AI 分析 -AI 分析阶段当前已接入真实接口入口: +入口: + +- 页面点击远程 AI 分析或本地 AI 分析。 + +产物: ```text -pending_ai -→ 用户点击"远程 AI 分析"或"本地 AI 分析" -→ ai_analyzing -→ 解析 AI 严格 JSON(兼容 Markdown 代码块、尾随逗号、Python 风格布尔值等) -→ Pydantic 字段校验 + 时间范围校验 + 片段时长校验 -→ 写入 clip_candidates -→ pending_review +analysis/candidate_clips.json +clip_candidates 表 +ai_analysis_runs 表 +``` + +当前 provider: + +- `remote`:OpenAI-compatible / DeepSeek。 +- `local`:本地 Ollama。 + +真实行为: + +- 读取 `transcripts/transcript.md`。 +- 按长文本分块分析,再合并、去重、排序候选片段。 +- 写入 `analysis/candidate_clips.json`。 +- 替换当前任务的 `clip_candidates`。 +- 保存一条 `ai_analysis_runs` 历史记录。 +- 任务进入 `pending_review`。 + +恢复历史 AI 分析时,会把历史 payload 重新写回 `candidate_clips.json`,并替换当前候选片段。 + +## 5. 人工审核候选片段 + +入口: + +```text +/tasks/{task_id}/clips/review ``` -- 远程 DeepSeek 使用完整逐句时间戳原文上下文,整集一次提交;如果旧任务文件仍包含分钟级转写,分析时会自动忽略分钟级重复内容。 -- 本地 Ollama 按约 3 分钟小段拆分,每段生成局部候选片段,再合并、去重、按置信度筛选。 -- AI 返回非法 JSON 时,程序会自动安全重试一次。重试后仍失败时,任务进入 `failed`。 -- 远程 AI 失败时不会自动降级到本地 AI,会暂停并显示原因,用户需手动点击"本地 AI 分析"。 +可做操作: -## 6. 转写阶段 +- 修改标题。 +- 修改开始时间和结束时间。 +- 修改摘要。 +- 启用或停用候选片段。 +- 软删除候选片段。 -转写阶段默认使用火山引擎远程转写: +只有启用且未删除的候选片段会进入切片流程。 + +## 6. 自动切片 + +入口: + +- 同步切片接口。 +- 异步切片接口,使用 `workflow_jobs` 本地轻量队列。 + +产物: ```text -用户点击"开始处理 / 继续处理" -→ 如果没有 audio/source.wav,先自动提取音频 -→ 如果已有 transcripts/transcript.md,直接提示转写已完成,不重复转写 -audio/source.wav -→ FFprobe 读取音频时长 -→ 火山引擎远程转写(默认)或本地 faster-whisper -→ 按分钟生成逐句时间戳原文 -→ 写入 transcripts/transcript.md(只保留"逐句时间戳原文") -→ pending_ai +05_clips/*.mp4 +output_clip 表 +cut_runs 表 ``` -- 远程转写失败时不会自动改用本地模型,页面会显示"改用本地模型转写"按钮。 -- 本地 faster-whisper 默认按 2 分钟切分音频,段与段之间重叠 5 秒。 -- 转写完成后进入 `pending_ai`。 +行为: -## 7. 自动切割阶段 +- 每次切片创建一条 `cut_runs`。 +- 切片成功后激活新 run,并让旧 run 的输出变为非活跃。 +- 如果新切片全部失败,旧的活跃切片结果会保留。 +- 至少一条成功时,任务进入 `completed` 或 `completed_with_errors`。 -- 用户在片段审核页点击"生成切片"后,任务进入 `cutting`。 -- 所有启用片段都切割成功时,任务进入 `completed`。 -- 至少一个片段成功、同时存在失败片段时,任务进入 `completed_with_errors`。 -- 所有片段都失败时,任务进入 `failed`。 -- 切割输出到 `05_clips/` 目录,结果逐条写入 `output_clip` 表。 +## 7. 字幕工作流 -## 8. 字幕工作流(独立于主任务状态) +字幕不直接改变 `tasks.status`,状态保存在 `subtitle_jobs`。 -字幕是 output_clip 生成之后的独立 `subtitle_jobs` 流程,不直接混入 `tasks.status`: +产物: ```text -output_clip 生成成功 -→ /subtitles 字幕工作台 -→ 选择切片,点击"自动加字幕" -→ 从转写文本按切片时间范围提取字幕行 -→ 生成 .ass 字幕文件 -→ FFmpeg subtitles 滤镜合成带字幕视频 -→ 输出到 06_subtitled/ -→ subtitle_jobs.status = completed / failed +06_subtitled/*.ass +06_subtitled/*_subtitled.mp4 +subtitle_jobs 表 ``` -字幕任务状态:`pending` → `processing` → `completed` / `failed` +流程: -## 9. 发送中心工作流(独立于主任务状态) +```text +选择已完成 output_clip +→ 从 transcript.md 按时间范围提取字幕文本 +→ 生成 ASS 字幕 +→ FFmpeg subtitles 滤镜烧录 +→ 激活新的 subtitle_job +``` + +失败时旧的活跃字幕结果会保留。 -发送中心是切片生成后的独立 `publish_jobs` 流程,不直接混入 `tasks.status`: +## 8. 发送中心工作流 + +发送中心不直接改变 `tasks.status`,状态保存在 `publish_jobs`。 + +产物: ```text -output_clip 生成成功 + 字幕完成 -→ /publish 发送中心 -→ 刷新发送队列(从已完成切片生成抖音 + B站双平台 opencli 任务) -→ publish_jobs.status = ready -→ 用户确认标题、话题、简介、封面帧 -→ 点击"发送此条" -→ opencli 辅助浏览器打开平台投稿页 -→ 自动填写标题、简介、上传视频、选择封面 -→ 点击发布,等待平台成功信号 -→ publish_jobs.status = publishing → published / failed +publish_jobs 表 +07_covers/*.jpg ``` -### 发送任务状态(publish_jobs.status) +流程: + +```text +刷新发送队列 +→ 从 completed output_clip 生成抖音 / B站任务 +→ 生成标题、简介、话题 +→ 生成候选封面帧 +→ 用户人工确认 +→ opencli 辅助浏览器投稿 +→ published / failed / cancelled +``` + +发送任务状态: | 状态 | 说明 | -| --- | --- | -| `ready` | 待发送,已整理好标题、封面和视频 | -| `publishing` | 发送中,opencli 正在操作浏览器 | -| `published` | 已发布,平台返回成功信号 | -| `failed` | 发送失败,error_message 记录具体原因 | -| `cancelled` | 已取消,用户主动取消 | +|---|---| +| `ready` | 待发送 | +| `publishing` | opencli 正在辅助浏览器操作 | +| `published` | 已标记发布成功 | +| `failed` | 发送失败 | +| `cancelled` | 用户取消 | + +## 9. scheduled_at 字段 + +`publish_jobs.scheduled_at` 当前只是字段预留: -### scheduled_at 字段说明 +- 可以保存计划发布时间。 +- 没有后台定时调度器。 +- 不会自动按时间发布。 +- 仍需要用户在发送中心手动触发。 -- `publish_jobs.scheduled_at` 当前只是字段预留,可以保存计划发布时间。 -- v1.2 还没有后台定时调度器,不会自动按 `scheduled_at` 发送。 -- 所有发送都需要用户手动在发送中心点击"发送此条"或"开始发送全部"触发。 +## 10. 平台安全边界 -### 安全边界 +opencli 只用于辅助操作已登录 Chrome: -- 平台发送依赖 opencli 辅助浏览器操作。 -- 不绕过验证码、登录失效、风控和人工确认。 -- 遇到平台验证提示时,任务会标记为 `failed` 并记录具体原因,等待人工处理。 +- 不绕过验证码。 +- 不绕过登录失效。 +- 不绕过平台风控。 +- 不替用户做无法确认的最终发布动作。 +- 失败时写入 `publish_jobs.error_message`,等待人工处理。 diff --git a/docs/UI_REFERENCE.md b/docs/UI_REFERENCE.md index 0bafe35..3f753d9 100644 --- a/docs/UI_REFERENCE.md +++ b/docs/UI_REFERENCE.md @@ -1,5 +1,12 @@ # UI 参考说明 +## 2026-06-17 更新:Windows 代码体检与文档同步 +- 本轮未调整页面结构、视觉样式或前端交互,只同步当前 v1.3.0 的真实功能口径。 +- 当前页面仍是 HTML + CSS + JavaScript + Jinja2,不引入 React / Vue。 +- 主页面范围保持:工作台、任务列表、新建任务、任务详情、片段审核、字幕工作台、发送中心、系统状态。 +- 发送中心仍定位为“人工确认 + opencli 辅助投稿”,不是完全无人值守发布;`scheduled_at` 只是字段预留,不会自动定时发布。 +- 任务产物目录口径统一为:`05_clips` 切片、`06_subtitled` 字幕成片、`07_covers` 候选封面帧。 + ## 2026-06-15 更新:v1.3 分支整合版 - 左侧状态说明更新为 `v1.3 分支整合版`,表示安全、性能、队列、服务拆分、版本化回滚、工程化和架构文档分支已完成统一集成。 - 本次只调整版本口径和状态说明,没有改变页面结构。 diff --git a/docs/VIDEO_CUTTING.md b/docs/VIDEO_CUTTING.md index 667afcc..490ee3e 100644 --- a/docs/VIDEO_CUTTING.md +++ b/docs/VIDEO_CUTTING.md @@ -1,106 +1,90 @@ -# 自动切割视频说明 +# 自动切片说明 -## 1. 切割流程 +自动切片负责把人工审核后的候选片段切成短视频文件。 -用户在片段审核页点击“生成切片”后,系统会: +## 1. 前置条件 -1. 读取当前 `task_id`。 -2. 从 `tasks` 表读取原始视频路径。 -3. 从 `clip_candidates` 表读取所有 `enabled = 1` 的候选片段。 -4. 把任务状态更新为 `cutting`。 -5. 调用 `app/services/video_cut_service.py` 逐条执行 FFmpeg 切割。 -6. 每条切片结果写入 `output_clip` 表。 -7. 全部结束后更新任务状态: - - 全部成功:`completed` - - 部分成功、部分失败:`completed_with_errors` - - 全部失败:`failed` +必须满足: -## 2. FFmpeg 依赖 +- 任务有可读取的原始视频。 +- AI 分析已生成候选片段。 +- 至少一条候选片段处于启用状态。 -系统调用本机 `ffmpeg` 命令。运行前会检查: +只有 `enabled = 1` 且未删除的候选片段会参与切片。 -- 原视频是否存在; -- 原视频路径是否是文件; -- 是否能在 Windows PATH 中找到 FFmpeg。 +## 2. 输出目录 -如果 FFmpeg 不可用,任务会失败,并在任务错误信息和日志中记录“FFmpeg 不可用”。 - -## 3. 输出目录 - -切片文件保存到: +当前官方输出目录是: ```text -E:\直播间切片工作流存储\{task_id}\05_clips\ +05_clips/ ``` -如果在测试或开发中临时把存储根目录改成项目内目录,则对应为: - -```text -tasks\{task_id}\05_clips\ -``` +历史兼容目录 `clips/` 仍可能被创建,但新文档、新功能和新产物统一使用 `05_clips/`。 -## 4. 文件命名 - -文件名格式: +## 3. 切片流程 ```text -01_片段标题_00-12-10_00-14-40.mp4 +读取启用候选片段 +→ 创建 cut_runs 记录 +→ 逐条调用 FFmpeg +→ 写入 output_clip +→ 激活成功 cut_run +→ 更新任务状态 ``` -系统会清理 Windows 不支持的字符,例如: +切片状态: -```text -< > : " / \ | ? * -``` +- 全部成功:`tasks.status = completed` +- 部分成功:`tasks.status = completed_with_errors` +- 全部失败:`tasks.status = failed` -如果同名文件已经存在,系统不会覆盖旧文件,会自动追加序号,例如: +## 4. 版本化和失败回滚 -```text -01_片段标题_00-12-10_00-14-40_2.mp4 -``` +每次生成切片都会创建新的 `cut_runs`。 -## 5. FFmpeg 策略 +成功规则: -首版默认策略是 `accurate`: +- 新 run 成功后会成为活跃 run。 +- 旧 run 的 `output_clip` 会标记为非活跃。 -- 使用重新编码方式输出; -- 切点更稳定、更适合审核后的最终输出; -- 速度比纯复制慢一些,但首版优先保证结果可用。 +失败规则: -代码中也预留了 `fast` 策略: +- 如果新 run 全部失败,不会覆盖旧的活跃切片。 +- 页面仍能看到上一轮成功的活跃结果。 -- 使用 `-c copy`; -- 速度快; -- 但切点可能受关键帧影响,不一定完全精确。 +## 5. FFmpeg -默认策略配置在: +切片依赖 Windows 能直接执行: -```text -app/core/config.py +```powershell +ffmpeg -version ``` -字段为: +相关超时配置: ```text -default_cut_strategy = "accurate" +FFMPEG_CUT_TIMEOUT=600 ``` -## 6. 常见错误 - -| 错误 | 页面/日志提示 | 处理方式 | -| --- | --- | --- | -| 原视频不存在 | 视频文件不存在 | 检查任务详情里的源视频路径 | -| 没有启用片段 | 没有任何启用片段 | 去片段审核页至少勾选一条片段 | -| FFmpeg 不可用 | FFmpeg 不可用 | 安装 FFmpeg 并加入 PATH | -| 片段时间非法 | 结束时间必须晚于开始时间 | 修改片段开始/结束时间 | -| FFmpeg 切割失败 | 会记录 stderr 摘要 | 查看任务详情错误信息或 `logs/process.log` | - -## 7. 测试方式 - -1. 准备一条短视频,创建任务并进入片段审核页。 -2. 确认有 2-3 条候选片段,并至少启用 1 条。 -3. 点击“保存修改”。 -4. 点击“生成切片”。 -5. 回到任务详情页,检查“已生成视频”列表。 -6. 打开任务目录下的 `05_clips`,确认有 `.mp4` 文件。 -7. 如果要测试错误处理,把某条片段的结束时间改得比开始时间更早,再点击生成切片。 +如果切片长期无响应,通常要检查: + +- FFmpeg 是否安装。 +- 原视频路径是否存在。 +- 路径是否包含无法访问的网络盘。 +- 候选片段时间是否超出视频范围。 + +## 6. 数据库表 + +相关表: + +- `clip_candidates`:输入候选片段。 +- `cut_runs`:每轮切片运行。 +- `output_clip`:每条输出切片。 + +## 7. 后续流程 + +切片完成后可以继续: + +- 在字幕工作台生成带字幕视频,输出到 `06_subtitled/`。 +- 在发送中心刷新队列,生成 `publish_jobs` 和封面帧。 diff --git a/docs/WINDOWS_SETUP.md b/docs/WINDOWS_SETUP.md new file mode 100644 index 0000000..ea3185d --- /dev/null +++ b/docs/WINDOWS_SETUP.md @@ -0,0 +1,178 @@ +# Windows 本地部署与启动说明 + +这份文档写给第一次运行“牛马片场”的 Windows 用户。下面所有命令都在 PowerShell 里执行。 + +## 1. 准备软件 + +需要安装: + +- Python 3.12 或更高版本。 +- FFmpeg,包含 `ffmpeg` 和 `ffprobe`。 +- 可选:Docker Desktop。 +- 可选:Ollama,用于本地 AI 分析。 +- 可选:opencli,用于发送中心辅助打开投稿页。 + +检查 Python: + +```powershell +python --version +``` + +成功时会看到类似: + +```text +Python 3.12.10 +``` + +检查 FFmpeg: + +```powershell +ffmpeg -version +ffprobe -version +``` + +成功时会看到版本信息。如果提示“不是内部或外部命令”,说明 FFmpeg 还没有加入 Windows PATH。 + +## 2. 进入项目目录 + +```powershell +cd "C:\Users\10578\Documents\New project 2" +``` + +这条命令的作用是进入项目根目录。成功后,PowerShell 左侧路径应该显示 `New project 2`。 + +## 3. 创建虚拟环境 + +```powershell +python -m venv .venv +``` + +这条命令会在项目里创建 `.venv` 文件夹,用来放 Python 依赖。成功时通常没有输出。 + +启用虚拟环境: + +```powershell +.\.venv\Scripts\Activate.ps1 +``` + +成功后,命令行前面会出现 `(.venv)`。 + +如果 PowerShell 提示脚本不能运行,可以只在当前窗口执行: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\.venv\Scripts\Activate.ps1 +``` + +## 4. 安装依赖 + +```powershell +pip install -r requirements.txt +``` + +这条命令会安装 FastAPI、pytest、faster-whisper 等依赖。成功后不会出现红色报错。 + +## 5. 准备配置文件 + +```powershell +copy .env.example .env +``` + +这条命令会复制一份本地配置。真实 API Key 只写进 `.env`,不要写进 `.env.example`。 + +重点检查 `.env`: + +- `STORAGE_ROOT`:任务产物根目录。 +- `TASKS_DIR`:任务目录,通常和 `STORAGE_ROOT` 一样。 +- `TRANSCRIPTION_PROVIDER`:只能填 `volcengine` 或 `local`。 +- `AI_DEFAULT_PROVIDER`:只能填 `remote` 或 `local`。 +- `VOLCENGINE_ASR_API_KEY`:火山引擎转写密钥。 +- `AI_ANALYSIS_REMOTE_API_KEY`:AI 分析密钥。 +- `AI_PUBLISH_REMOTE_API_KEY`:发送中心文案密钥。 + +如果电脑没有 E 盘,请把: + +```text +STORAGE_ROOT=E:\直播间切片工作流存储 +TASKS_DIR=E:\直播间切片工作流存储 +``` + +改成真实存在的目录,例如: + +```text +STORAGE_ROOT=C:\NiuMaStudio\tasks +TASKS_DIR=C:\NiuMaStudio\tasks +``` + +## 6. 启动项目 + +在项目根目录执行: + +```powershell +.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8001 +``` + +成功时会看到类似: + +```text +Uvicorn running on http://127.0.0.1:8001 +``` + +浏览器打开: + +```text +http://127.0.0.1:8001 +``` + +看到任务列表页面,就说明后端和前端都已启动。 + +## 7. 常用检查命令 + +检查代码风格: + +```powershell +.\.venv\Scripts\python.exe -m ruff check app tests +``` + +运行自动测试: + +```powershell +.\.venv\Scripts\python.exe -m pytest -q +``` + +检查 Git 当前状态: + +```powershell +git status +``` + +如果看到 `nothing to commit, working tree clean`,说明没有未保存的 Git 修改。 + +## 8. Docker 启动 + +Docker 是可选方式。当前 `docker-compose.yml` 默认挂载: + +```yaml +- E:/直播间切片工作流存储:/workspace/tasks +``` + +如果你的电脑没有 E 盘,请先把左侧路径改成真实存在的 Windows 目录。 + +启动: + +```powershell +docker compose up --build +``` + +停止: + +```powershell +docker compose down +``` + +## 9. 功能边界提醒 + +- 发送中心会辅助整理发布任务,但不是完全无人值守发布系统。 +- 遇到验证码、登录失效、平台风控、人工确认时,需要用户自己处理。 +- `scheduled_at` 当前只是保存计划发布时间,不会自动定时发送。 +- 不要把 `.env`、数据库、日志、视频、浏览器缓存提交到 Git。 diff --git a/tests/test_versioning_rollback.py b/tests/test_versioning_rollback.py index 3f93bbf..da21798 100644 --- a/tests/test_versioning_rollback.py +++ b/tests/test_versioning_rollback.py @@ -287,8 +287,10 @@ class TestSubtitleVersioning: @pytest.fixture(autouse=True) def setup_teardown(self): + from app.db.database import init_db from app.db.database import get_connection + init_db() yield with get_connection() as conn: conn.execute("DELETE FROM publish_jobs WHERE task_id LIKE 'test-%'") @@ -424,8 +426,10 @@ class TestAIAnalysisActive: @pytest.fixture(autouse=True) def setup_teardown(self): + from app.db.database import init_db from app.db.database import get_connection + init_db() yield with get_connection() as conn: conn.execute("DELETE FROM publish_jobs WHERE task_id LIKE 'test-%'") @@ -450,6 +454,39 @@ def _create_task(self, task_id: str): ) conn.commit() + def _insert_clip_candidate(self, task_id: str, clip_id: str, title: str = "旧候选"): + from app.db.database import get_connection + from app.services.task_service import _now_iso + + now = _now_iso() + with get_connection() as conn: + conn.execute( + """ + INSERT INTO clip_candidates ( + id, task_id, clip_key, title, start_time, end_time, duration_seconds, + summary, reason, highlight_reason, spread_value, suggested_editing, + confidence_score, selected_by_default, enabled, reviewed, created_at, updated_at + ) VALUES (?, ?, ?, ?, '00:00:01', '00:00:30', 29, '', '', '', '', '', 0.7, 1, 1, 0, ?, ?) + """, + (clip_id, task_id, clip_id, title, now, now), + ) + conn.commit() + + def _analysis_clip(self, clip_id: str, title: str) -> dict: + return { + "clip_id": clip_id, + "title": title, + "start_time": "00:00:10", + "end_time": "00:01:10", + "duration_seconds": 60, + "summary": f"{title} 摘要", + "highlight_reason": f"{title} 高光原因", + "spread_value": "情绪价值", + "suggested_editing": "保留关键反应镜头", + "confidence_score": 0.86, + "selected_by_default": True, + } + def test_insert_run_sets_active_and_deactivates_old(self): """新 AI run 应自动设为 active,旧 run 取消激活""" from app.services.ai_analysis_workflow_service import _insert_ai_analysis_run @@ -540,6 +577,93 @@ def test_list_runs_includes_all_with_active_flag(self): assert "remote" in providers assert "local" in providers + def test_process_ai_analysis_replaces_old_candidates_and_keeps_new(self, tmp_path): + """重新跑 AI 分析后,新候选片段应保留下来,旧候选应被替换""" + from app.models.task import AIClipAnalysisResult + from app.services.ai_analysis_workflow_service import process_task_ai_analysis + from app.services.task_service import list_clip_candidates + + task_id = "test-aiactive-process" + self._create_task(task_id) + self._insert_clip_candidate(task_id, f"{task_id}_old", "旧候选") + + transcript_path = tmp_path / "transcripts" / "transcript.md" + analysis_path = tmp_path / "analysis" / "candidate_clips.json" + log_path = tmp_path / "logs" / "process.log" + transcript_path.parent.mkdir(parents=True, exist_ok=True) + analysis_path.parent.mkdir(parents=True, exist_ok=True) + log_path.parent.mkdir(parents=True, exist_ok=True) + transcript_path.write_text("00:00:10 --> 00:01:10\n这是一段测试转写。", encoding="utf-8") + fake_paths = {"transcript_path": transcript_path, "analysis_path": analysis_path, "log_path": log_path} + fake_result = AIClipAnalysisResult( + task_id=task_id, + analysis_summary="新分析结果", + clips=[ + self._analysis_clip("new-001", "新候选 1"), + self._analysis_clip("new-002", "新候选 2"), + ], + ) + + with ( + patch("app.services.ai_analysis_workflow_service.get_artifact_paths", return_value=fake_paths), + patch("app.services.ai_analysis_workflow_service.append_task_log"), + patch("app.services.ai_analysis_workflow_service._analyze_with_provider", return_value=fake_result), + ): + result = process_task_ai_analysis(task_id, provider="local") + + clips = list_clip_candidates(task_id) + clip_titles = {clip["title"] for clip in clips} + assert result["status"] == "ok" + assert len(clips) == 2 + assert clip_titles == {"新候选 1", "新候选 2"} + assert analysis_path.exists() + + def test_restore_ai_analysis_run_replaces_old_candidates_and_keeps_restored(self, tmp_path): + """恢复历史 AI 分析时,恢复出来的新候选片段不应被清空""" + from app.services.ai_analysis_workflow_service import _insert_ai_analysis_run, restore_ai_analysis_run + from app.services.task_service import list_clip_candidates + + task_id = "test-aiactive-restore" + self._create_task(task_id) + self._insert_clip_candidate(task_id, f"{task_id}_old", "旧候选") + payload = { + "analysis_summary": "恢复的历史结果", + "clips": [ + self._analysis_clip("restored-001", "恢复候选 1"), + self._analysis_clip("restored-002", "恢复候选 2"), + ], + } + run = _insert_ai_analysis_run( + task_id=task_id, + analysis_payload=payload, + provider="remote", + provider_label="远程 AI", + model="test-model", + fallback_notice="", + prompt_preset={"id": "p1", "name": "测试"}, + requested_clip_count=5, + ) + + analysis_path = tmp_path / "analysis" / "candidate_clips.json" + log_path = tmp_path / "logs" / "process.log" + analysis_path.parent.mkdir(parents=True, exist_ok=True) + log_path.parent.mkdir(parents=True, exist_ok=True) + with ( + patch( + "app.services.ai_analysis_workflow_service.get_artifact_paths", + return_value={"analysis_path": analysis_path, "log_path": log_path}, + ), + patch("app.services.ai_analysis_workflow_service.append_task_log"), + ): + result = restore_ai_analysis_run(task_id, run["id"]) + + clips = list_clip_candidates(task_id) + clip_titles = {clip["title"] for clip in clips} + assert result["status"] == "ok" + assert len(clips) == 2 + assert clip_titles == {"恢复候选 1", "恢复候选 2"} + assert analysis_path.exists() + # --------------------------------------------------------------------------- # 数据库迁移测试