feat: add backend document database

This commit is contained in:
2026-05-18 15:34:15 +08:00
parent 1c451c6ab3
commit 1ac9b1bde3
4 changed files with 99 additions and 17 deletions

View File

@@ -1,11 +1,5 @@
{
"entries": [
{
"files_changed": 3,
"message": "Codex 会话活跃 · 最近命令codex · 3 项未提交变更 · 最近提交auto-save 2026-05-15 17:50 (~1)",
"ts": "2026-05-15T09:54:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 4,
"hash": "a662130",
@@ -3257,6 +3251,13 @@
"message": "auto-save 2026-05-18 15:13 (~8)",
"hash": "2a1aa4c",
"files_changed": 8
},
{
"ts": "2026-05-18T15:29:47+08:00",
"type": "commit",
"message": "auto-save 2026-05-18 15:29 (+1, ~5)",
"hash": "1c451c6",
"files_changed": 6
}
]
}

View File

@@ -18,6 +18,7 @@ uvicorn main:app --host 127.0.0.1 --port 4291
## 路由
- `GET /health` — 健康检查 + 配置状态
- `GET /documents` — 后端数据库里的文档归类列表;一条 TK 链接或一次上传视频默认一个 document
- `POST /jobs` `{url}` — 创建 job后台下载源视频视频就绪后可手动解析或提取音频
- `GET /jobs/{id}` — 当前状态 + 产物;若原始音轨已拆出,会返回 `source_audio_url`
- `POST /jobs/{id}/transcribe` — 触发音频提取 + ASR + 翻译 + SKG 英文产品介绍文案;文案长度按原音频时长估算,配置 Azure OpenAI TTS 后从 Azure 音色池生成配音。前端 Audio 节点提供“提取音频 / 重新提取音频”按钮,可与抽帧并行,不自动触发
@@ -34,5 +35,6 @@ uvicorn main:app --host 127.0.0.1 --port 4291
- `ffmpeg` 系统二进制(拆轨 / 抽帧)
- `yt-dlp` 系统二进制(也可走 Python 包)
- SQLite 元数据数据库(默认 `APP_DB_URL=sqlite:///./jobs/app.db`);只存 document / job / media asset 元数据,原视频、音频、抽帧和生成文件继续放 `jobs/<jobId>/`
- OpenAI 兼容 LLM 网关ASR / 翻译 / 文案改写);如果 `/audio/transcriptions` 不可用,会用 `ASR_FALLBACK_MODEL` 走 Gemini 多模态音频识别
- Azure OpenAI TTS英文产品介绍文案配音使用 `AZURE_OPENAI_API_KEY` 或回退复用 `LLM_API_KEY`;默认音色池 `alloy,verse,shimmer`

View File

@@ -603,7 +603,7 @@
<tr><td><code>web/components/product-library-picker.tsx</code></td><td>SKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 <code>asset</code></td></tr>
<tr><td><code>web/components/storyboard-bar.tsx</code></td><td>顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。</td></tr>
<tr><td><code>web/components/storyboard-workbench.tsx</code></td><td>顶部分镜编排条下方的明细区4 图槽、改造目标、时长、自动保存。</td></tr>
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client是前后端数据契约镜像<code>RuntimeHealth</code> / <code>RuntimeModels</code> 读取 <code>GET /health</code>,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源。</td></tr>
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client是前后端数据契约镜像<code>RuntimeHealth</code> / <code>RuntimeModels</code> 读取 <code>GET /health</code>,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源<code>DocumentSummary</code> / <code>listDocuments</code> 预留给后续文档侧栏和多视频素材库</td></tr>
</tbody>
</table>
</div>
@@ -611,10 +611,12 @@
<h3>后端核心</h3>
<table>
<tbody>
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回。</td></tr>
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 <code>state.json</code> 时同步数据库元数据</td></tr>
<tr><td><code>api/database.py</code></td><td>后端数据库层:当前内置 SQLite维护 <code>documents</code><code>jobs</code><code>media_assets</code> 三类元数据;文档是顶层归类,一条 TK 链接或一次上传默认一个 document媒体文件仍保留在 <code>jobs/&lt;jobId&gt;/</code></td></tr>
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
<tr><td><code>api/character_library/skg-characters</code></td><td>内置相似主体形象库:从桌面 5 套策划形象导入,<code>manifest.json</code> 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图,用于相似主体高清视图包的创意方向选择。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/state.json</code></td><td>运行时状态文件,不在源码列表里,但刷新恢复依赖它</td></tr>
<tr><td><code>jobs/app.db</code></td><td>默认本地后端数据库,路径由 <code>APP_DB_URL</code> / <code>DATABASE_URL</code> 控制;生产模板默认 <code>sqlite:////data/jobs/app.db</code></td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/state.json</code></td><td>运行时状态文件,当前仍是兼容恢复真源;保存时同步写入数据库,后续迁移再逐步改成 DB 主读。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/audio.wav</code></td><td>拆轨得到的原始音频,当前只作为后端分析和后续必要预览的只读文件来源;主界面不再默认渲染底部音频条。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/frames</code></td><td>关键帧 jpg。注意 frame.index 是稳定 ID不等于数组下标。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/cleaned</code></td><td>清洗后待应用图片。</td></tr>
@@ -679,13 +681,38 @@ api/main.py
<h3>Job</h3>
<p>一个视频任务。前端维护多个 <code>jobs[]</code>,当前激活的是 <code>activeJobId</code>。URL 查询参数会持久化多个 job。</p>
<pre>Job {
id, url, status, progress, message,
id, document_id, source_kind, workflow_mode, storage_prefix,
url, status, progress, message,
video_url, source_audio_url, duration, width, height,
frames: KeyFrame[],
transcript: TranscriptSegment[],
audio_script: AudioScript,
storyboard_images?: StoryboardImage[],
product_refs?: ProductRefStateItem[]
}</pre>
</div>
<div class="card">
<h3>Document / DB</h3>
<p>文档是业务归类顶层,不等于物理文件。当前一条 TK 链接或一次上传视频默认生成一个 document不同来源或模式通过 <code>source_kind</code><code>workflow_mode</code> 区分。数据库只保存元数据、状态和文件索引,不保存视频/图片二进制。</p>
<pre>documents {
id, title, source_kind, workflow_mode,
source_url, primary_job_id, status,
storage_prefix, metadata_json,
created_at, updated_at
}
jobs {
id, document_id, source_kind, workflow_mode,
source_url, status, progress,
storage_path, state_path,
frame_count, video_count
}
media_assets {
id, document_id, job_id,
kind: video | audio | image,
role: source_video | keyframe | product_ref | subject_asset | first_frame | last_frame | generated_video,
path, url, frame_index, metadata_json
}</pre>
</div>
<div class="card">
@@ -882,10 +909,11 @@ ProductRefStateItem {
</thead>
<tbody>
<tr><td>网页登录</td><td><code>POST /auth/login</code><code>GET /auth/check</code><code>POST /auth/logout</code></td><td><code>web/app/login/page.tsx</code>、Nginx <code>auth_request</code></td><td>登录页提交账号密码到 <code>/api/auth/login</code>,后端设置 HttpOnly 会话 Cookie生产 Nginx 对工作台和 <code>/api/</code><code>/auth/check</code> 做统一校验,未登录页面跳 <code>/login/</code>API 返回 JSON 401。</td></tr>
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code><code>ModelTrace</code></td><td>返回 <code>models</code>ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 <code>REWRITE_MODEL</code><code>AUDIO_REWRITE_MODEL</code><code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。</td></tr>
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>所有 job 精简列表id/url/status/thumbnail/mtime…按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填全部历史;带 <code>limit</code> 可截断</td></tr>
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;前端“开始”队列会在 downloaded 后自动触发音频解析</td></tr>
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态当前上传后也加入第一步队列下载完成后自动解析音频</td></tr>
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code><code>ModelTrace</code></td><td>返回 <code>models</code>ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 <code>REWRITE_MODEL</code><code>AUDIO_REWRITE_MODEL</code><code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。<code>database</code> 回传 DB 是否启用、URL、schema 版本和 document/job/asset 计数。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。</td></tr>
<tr><td>文档列表</td><td><code>GET /documents</code></td><td>后续文档侧栏 / 素材库入口</td><td>从数据库读取 document 归类列表,包含 source_kind、workflow_mode、primary_job_id、storage_prefix、job_count、asset_count 和更新时间。当前前端还未接主入口,后端已可作为多视频/多上传文档管理的索引</td></tr>
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>所有 job 精简列表id/document_id/source_kind/workflow_mode/url/status/thumbnail/mtime…按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填全部历史;带 <code>limit</code> 可截断</td></tr>
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端自动建立 <code>document_id=job_id</code><code>source_kind=tiktok_link</code><code>workflow_mode=feed_recreation</code> 的 document并在状态保存时同步 DB</td></tr>
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态后端自动建立 <code>source_kind=upload</code><code>workflow_mode=uploaded_reference</code> 的 document。当前上传后也加入第一步队列下载完成后自动解析音频。</td></tr>
<tr><td>删除输入视频</td><td><code>DELETE /jobs/{id}</code></td><td><code>deleteJob</code></td><td>从任务队列、URL 和磁盘 <code>jobs/&lt;id&gt;</code> 目录移除整个 job包括源视频、关键帧、元素提取图和生成视频。</td></tr>
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze?frames=&amp;target=&amp;mode=&amp;quality=</code></td><td><code>analyzeJob</code></td><td>后续阶段保留的抽帧能力。默认 <code>frames=6</code><code>target</code> 支持人物随机、透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前“开始分析”和源视频旁的“自动抽帧 6 张”都会显式用 <code>target=random_subject</code><code>quality=accurate</code><code>mode=replace</code> 生成清晰人物候选里的随机参考帧池。</td></tr>
<tr><td>音频文案轨</td><td><code>POST /jobs/{id}/transcribe</code></td><td><code>triggerTranscribe</code></td><td>若尚未拆轨,先从 <code>source.mp4</code> 提取 <code>audio.wav</code> 并回填 <code>source_audio_url</code>;随后用 ASR 提取原始文案,翻译成中文,写入 <code>audio_script.source_text</code><code>source_zh</code> 和逐句 <code>transcript</code>。远端 <code>ASR_MODEL</code> 失败后先走本机 <code>LOCAL_ASR_BIN</code>/<code>LOCAL_ASR_MODEL</code>(默认 <code>mlx_whisper</code>),再尝试 <code>ASR_FALLBACK_MODEL</code>。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。再用 <code>ASR_FALLBACK_MODEL</code> 多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 <code>speaker_profile</code><code>rhythm_profile</code><code>background_audio_profile</code>。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。</td></tr>
@@ -926,8 +954,8 @@ ProductRefStateItem {
<tbody>
<tr>
<td><span class="tag blue">复刻工作表</span></td>
<td>承载当前第一步主路径:素材输入列按文件任务管理素材;点击“开始”后自动下载源视频,下载完成后触发音频提取原文案转写、中文翻译讲话人/节奏/背景音分析,并以工作表方式展示。</td>
<td>不要在当前开始流程里自动抽帧、自动写分镜、自动生成元素或自动合成视频;不要恢复右侧空白画布占位。</td>
<td>承载当前第一步主路径:素材输入列按文件任务管理素材;点击“开始”后自动下载源视频,下载完成后并行触发音频提取/原文案转写/翻译/讲话人节奏背景音分析,以及 6 张人物随机参考帧抽取,并以工作表方式展示。</td>
<td>不要在当前开始流程里自动写分镜、自动生成元素或自动合成视频;不要恢复右侧空白画布占位。</td>
<td><code>web/components/ad-recreation-board.tsx</code><code>web/app/page.tsx</code></td>
</tr>
<tr>
@@ -963,7 +991,7 @@ ProductRefStateItem {
<li>手动按时间戳加关键帧。</li>
<li>关键帧清洗水印,全图或区域清洗。</li>
<li>GPT 画面理解识别关键帧,输出 scene、objects、style、suggested_prompt并作为主体候选来源。</li>
<li>“开始”会在下载完成后自动触发音频处理,不再默认自动抽帧、Vision 扫描或保存分镜初稿。</li>
<li>“开始”会在下载完成后自动触发音频处理和 6 张人物随机参考帧抽取,不再默认 Vision 扫描或保存分镜初稿。</li>
<li>主体候选确认、改名、删除和主体资产包生成能力保留在底层旧面板和接口中,当前第一步主界面不主动展示。</li>
<li>分镜工作台 4 图槽和改造说明自动保存。</li>
<li>音频文案轨:点击开始或提取音频后提取原文案、中文翻译、讲话人、语速节奏、背景音乐/环境声/音效;结果集中在右侧工作表展示。</li>
@@ -1016,6 +1044,18 @@ ProductRefStateItem {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-18 · 新增后端文档数据库</h3>
<span class="tag violet">API</span>
<span class="tag cyan">Data</span>
</header>
<div class="body">
<p><strong>问题:</strong>多个 TK 视频和上传视频都只靠 <code>state.json</code> 和目录扫描管理,后续做文档归类、素材调度、批量查询和不同工作模式会越来越难。</p>
<p><strong>改动:</strong>新增 <code>api/database.py</code>,默认使用 <code>APP_DB_URL</code> / <code>DATABASE_URL</code><code>JOBS_DIR/app.db</code> 的 SQLite 数据库,创建 <code>documents</code><code>jobs</code><code>media_assets</code> 三张元数据表。<code>Job</code> 新增 <code>document_id</code><code>source_kind</code><code>workflow_mode</code><code>storage_prefix</code>;保存状态时同步 DB删除 job 时同步清理 DB新增 <code>GET /documents</code><code>/health.database</code></p>
<p><strong>影响:</strong>后续文件归类以 document 为顶层TK 链接默认 <code>source_kind=tiktok_link</code><code>workflow_mode=feed_recreation</code>;上传视频默认 <code>source_kind=upload</code><code>workflow_mode=uploaded_reference</code>。数据库只存元数据和文件索引,原视频、音频、抽帧、生图、视频候选继续放文件系统。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-18 · 自动抽帧改为 6 张人物随机</h3>

View File

@@ -187,6 +187,15 @@ export interface RuntimeHealth {
llm_configured?: boolean
auth_configured?: boolean
base_url?: string
database?: {
enabled: boolean
url?: string
schema_version?: number
documents?: number
jobs?: number
assets?: number
error?: string
}
models?: RuntimeModels
}
@@ -572,6 +581,10 @@ export interface ProductRefStateItem {
export interface Job {
id: string
url: string
document_id?: string
source_kind?: "tiktok_link" | "upload" | "unknown"
workflow_mode?: "feed_recreation" | "uploaded_reference"
storage_prefix?: string
status: JobStatus
progress: number
message?: string
@@ -594,6 +607,7 @@ export interface BackendHealth {
llm_configured: boolean
auth_configured?: boolean
base_url: string
database?: RuntimeHealth["database"]
models?: {
asr?: string
translate?: string
@@ -661,6 +675,9 @@ export async function deleteJob(id: string): Promise<{ ok: boolean; id: string }
export interface JobSummary {
id: string
document_id?: string
source_kind?: string
workflow_mode?: string
url: string
status: JobStatus
progress: number
@@ -676,6 +693,28 @@ export interface JobSummary {
mtime: number
}
export interface DocumentSummary {
id: string
title: string
source_kind: string
workflow_mode: string
source_url: string
primary_job_id: string
status: string
storage_prefix: string
job_count: number
asset_count: number
created_at: number
updated_at: number
}
export async function listDocuments(limit?: number): Promise<DocumentSummary[]> {
const qs = limit && limit > 0 ? `?limit=${limit}` : ""
const res = await fetch(`${API_BASE}/documents${qs}`)
if (!res.ok) throw new Error(`listDocuments ${res.status}`)
return res.json()
}
export async function listJobs(limit?: number): Promise<JobSummary[]> {
const qs = limit && limit > 0 ? `?limit=${limit}` : ""
const res = await fetch(`${API_BASE}/jobs${qs}`)