auto-save 2026-05-14 11:04 (~6)
This commit is contained in:
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "bcc7ce0",
|
||||
"message": "auto-save 2026-05-13 02:25 (~1)",
|
||||
"ts": "2026-05-13T02:25:54+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "dad1819",
|
||||
@@ -3307,6 +3300,13 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 5 项未提交变更 · 最近提交:auto-save 2026-05-14 10:51 (~7)",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T10:59:27+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 10:59 (~6)",
|
||||
"hash": "3aceb22",
|
||||
"files_changed": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4
RULES.md
4
RULES.md
@@ -2,8 +2,8 @@
|
||||
|
||||
## 启动
|
||||
- 前端 dev:`cd web && pnpm dev`(Next.js 16,端口 4290)
|
||||
- 后端 dev:`cd api && uvicorn main:app --port 4291 --reload`(FastAPI,端口 4291,重任务用)
|
||||
- 说明:以上为规划脚手架,未实际生成;开发会话第一步起骨架时落地
|
||||
- 后端 dev:`cd api && uvicorn main:app --host 127.0.0.1 --port 4291`(FastAPI,端口 4291,重任务用)
|
||||
- 注意:后端不要带 `--reload` 跑长下载 / 抽帧 / 音频任务;reload 会等待后台任务结束,导致 4291 端口占用但新请求卡住。
|
||||
|
||||
## 立项决策快索引
|
||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||
|
||||
@@ -10,13 +10,15 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env # 按需填 LLM_API_KEY / MINIMAX_API_KEY
|
||||
uvicorn main:app --port 4291 --reload
|
||||
uvicorn main:app --host 127.0.0.1 --port 4291
|
||||
```
|
||||
|
||||
不要在长下载、抽帧、音频处理时带 `--reload` 跑后端;reload 会等待后台任务结束,表现为端口还在但新请求卡住。
|
||||
|
||||
## 路由
|
||||
|
||||
- `GET /health` — 健康检查 + 配置状态
|
||||
- `POST /jobs` `{url}` — 创建 job,后台跑下载/拆轨/抽帧
|
||||
- `POST /jobs` `{url}` — 创建 job,后台下载源视频,视频就绪后可手动解析或提取音频
|
||||
- `GET /jobs/{id}` — 当前状态 + 产物
|
||||
- `POST /jobs/{id}/transcribe` — 触发音频提取 + ASR + 翻译 + SKG 文案改写;配置 MiniMax 后生成配音。前端 Audio 节点提供“提取音频 / 重新提取音频”按钮,不依赖抽帧完成
|
||||
- `GET /jobs/{id}/video.mp4` — 原视频
|
||||
|
||||
@@ -1155,7 +1155,7 @@ def ffprobe_meta(mp4: Path) -> dict:
|
||||
|
||||
|
||||
def pipeline_download(job_id: str) -> None:
|
||||
"""阶段 1:仅下载(或上传跳过),落 source.mp4,停在 downloaded 等用户点解析。"""
|
||||
"""阶段 1:仅下载(或上传跳过),落 source.mp4,停在 downloaded 等用户点解析/提取音频。"""
|
||||
job = JOBS[job_id]
|
||||
d = job_dir(job_id)
|
||||
try:
|
||||
@@ -1567,7 +1567,7 @@ async def pipeline_transcribe(job_id: str) -> None:
|
||||
mp4 = d / "source.mp4"
|
||||
if not mp4.exists():
|
||||
raise RuntimeError("source.mp4 不存在,视频导入完成后再提取音频")
|
||||
update(job, status="transcribing", message="ffmpeg 提取音频轨…", progress=max(job.progress, 45), error="")
|
||||
update(job, status="transcribing", message="ffmpeg 提取音频轨…", progress=max(45, min(job.progress, 70)), error="")
|
||||
run([
|
||||
"ffmpeg", "-y", "-i", str(mp4),
|
||||
"-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le",
|
||||
@@ -2020,7 +2020,7 @@ async def trigger_transcribe(job_id: str, bg: BackgroundTasks) -> Job:
|
||||
raise HTTPException(409, f"video not ready, got {job.status}")
|
||||
if job.status in {"splitting", "transcribing"} or job.audio_script.status == "rewriting":
|
||||
raise HTTPException(409, f"job is busy, got {job.status}")
|
||||
update(job, status="transcribing", progress=max(job.progress, 45), error="", message="准备提取音频…")
|
||||
update(job, status="transcribing", progress=max(45, min(job.progress, 70)), error="", message="准备提取音频…")
|
||||
bg.add_task(pipeline_transcribe, job_id)
|
||||
return job
|
||||
|
||||
|
||||
@@ -530,8 +530,8 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>后端开发服务</td>
|
||||
<td><code>cd api && source .venv/bin/activate && uvicorn main:app --port 4291 --reload</code></td>
|
||||
<td>FastAPI,所有任务状态、视频、关键帧、清洗、元素、分镜保存都在 <code>api/main.py</code>。</td>
|
||||
<td><code>cd api && source .venv/bin/activate && uvicorn main:app --host 127.0.0.1 --port 4291</code></td>
|
||||
<td>FastAPI,所有任务状态、视频、关键帧、清洗、元素、分镜保存都在 <code>api/main.py</code>。长下载 / 抽帧 / 音频处理期间不要带 <code>--reload</code>,否则 reload 会等待后台任务结束并让新请求卡住。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>测试页面</td>
|
||||
@@ -792,7 +792,7 @@ SubjectAsset {
|
||||
<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>DELETE /jobs/{id}</code></td><td><code>deleteJob</code></td><td>从任务队列、URL 和磁盘 <code>jobs/<id></code> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。</td></tr>
|
||||
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze?frames=&target=&mode=&quality=</code></td><td><code>analyzeJob</code></td><td>拆轨 + 目标化抽关键帧。默认 <code>frames=12</code>;<code>target</code> 支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值;当前 UI 默认 <code>transparent_human</code>。透明骨架人目标会先扩大本地候选池,再调用 Vision 按 6 个分数验收;不合格候选自动丢弃并抽下一候选。<code>mode=append</code> 追加新关键帧;<code>quality=auto</code> 根据本机算力和视频时长自动选择快速、精细或极准。多个抽帧请求进入后端队列顺序处理。</td></tr>
|
||||
<tr><td>音频文案轨</td><td><code>POST /jobs/{id}/transcribe</code></td><td><code>triggerTranscribe</code></td><td>读取拆轨得到的 <code>audio.wav</code>,先 ASR 得到英文时间戳段落,再翻译中文,随后按 <code>AUDIO_PRODUCT_BRIEF</code> 生成 <code>audio_script.rewritten_text</code>;配置 <code>MINIMAX_API_KEY</code> 后调用 MiniMax T2A 生成 <code>audio_script.voice_url</code>。前端在抽帧完成且尚无 transcript 时会自动触发一次;Audio 节点也提供“开始/重新处理音频”按钮。</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>;随后 ASR 得到英文时间戳段落,再翻译中文,并按 <code>AUDIO_PRODUCT_BRIEF</code> 生成 <code>audio_script.rewritten_text</code>;配置 <code>MINIMAX_API_KEY</code> 后调用 MiniMax T2A 生成 <code>audio_script.voice_url</code>。前端不自动触发,用户在 Audio 节点点击“提取音频 / 重新提取音频”即可启动,不依赖抽帧完成。</td></tr>
|
||||
<tr><td>原始音频文件</td><td><code>GET /jobs/{id}/audio.wav</code></td><td><code>sourceAudioUrl</code></td><td>返回拆轨得到的 wav;底部 <code>AudioStrip</code> 拉取该文件,用 Web Audio API 解码并计算波形峰值,只读展示,不参与改写。</td></tr>
|
||||
<tr><td>改写配音文件</td><td><code>GET /jobs/{id}/audio-script.mp3</code></td><td><code>apiAssetUrl(job.audio_script.voice_url)</code></td><td>返回 MiniMax T2A 生成的 mp3。没有配置 MiniMax 或生成失败时该文件不存在,但改写文案仍会保存在 <code>audio_script.rewritten_text</code>。</td></tr>
|
||||
<tr><td>手动加帧</td><td><code>POST /jobs/{id}/frames?t=</code></td><td><code>addManualFrame</code></td><td>按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。</td></tr>
|
||||
@@ -841,7 +841,7 @@ SubjectAsset {
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="tag gray">Audio / ASR / Rewrite</span></td>
|
||||
<td>独立声音文案轨:从 <code>audio.wav</code> 提取原始口播、翻译中文、改写成 SKG 产品语境口播;MiniMax T2A 配置后生成配音 mp3。抽帧完成后自动触发一次,也可在主画布 <code>AudioNode</code> 手动开始/重新处理。<code>AudioNode</code> 用“改前 · 原音频 / 改后 · SKG 口播”摘要展示;底部 <code>AudioStrip</code> 吸附屏幕底端,可拖拽调整高度,按时间段展示英文、中文翻译和波形;侧栏 Rewrite 展开后显示完整审核视图。</td>
|
||||
<td>独立声音文案轨:从 <code>source.mp4</code> 直接提取 <code>audio.wav</code>,再提取原始口播、翻译中文、改写成 SKG 产品语境口播;MiniMax T2A 配置后生成配音 mp3。不再等待抽帧完成,用户在主画布 <code>AudioNode</code> 手动点击“提取音频 / 重新提取音频”启动。<code>AudioNode</code> 用“改前 · 原音频 / 改后 · SKG 口播”摘要展示;底部 <code>AudioStrip</code> 吸附屏幕底端,可拖拽调整高度,按时间段展示英文、中文翻译和波形;侧栏 Rewrite 展开后显示完整审核视图。</td>
|
||||
<td>不要阻断视觉素材管线。</td>
|
||||
<td><code>AudioNode</code>、<code>AudioStrip</code>、<code>ASRNode</code>、<code>TranslateNode</code>、<code>RewriteNode</code>、<code>pipeline_transcribe</code>、<code>AudioScript</code></td>
|
||||
</tr>
|
||||
@@ -920,13 +920,25 @@ SubjectAsset {
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 音频处理支持自动触发和手动重试</h3>
|
||||
<h3>2026-05-14 · 修复 ReactFlow Hydration 和后端 reload 卡住</h3>
|
||||
<span class="tag violet">Canvas</span>
|
||||
<span class="tag blue">Dev Server</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>刷新页面时 Next 报 Hydration mismatch,ReactFlow 节点服务端默认宽度和客户端 localStorage 里的用户调整宽度不一致;同时提交抖音链接时若后端正在 <code>--reload</code> 等后台任务结束,会出现 4291 端口占用但请求卡住。</p>
|
||||
<p><strong>改动:</strong><code>web/app/page.tsx</code> 增加 <code>clientReady</code>,ReactFlow 和底部音频条只在浏览器挂载后渲染,避免服务端 HTML 和客户端本地布局缓存不一致。后端启动说明改为不带 <code>--reload</code> 的稳定命令,并更新 <code>RULES.md</code>、<code>api/README.md</code> 和本页运行入口。</p>
|
||||
<p><strong>影响:</strong><code>web/app/page.tsx</code>、<code>RULES.md</code>、<code>api/README.md</code>、<code>docs/source-analysis.html</code>。本地 4291 后端已按无 reload 模式重启。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 音频处理改为音频卡片手动触发</h3>
|
||||
<span class="tag gray">Audio</span>
|
||||
<span class="tag blue">Workflow</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>后端已有 <code>/transcribe</code> 接口,但前端没有入口调用,用户不知道什么时候音频开始工作。</p>
|
||||
<p><strong>改动:</strong>前端在 job 进入 <code>frames_extracted</code> 且没有 transcript 时自动调用一次 <code>triggerTranscribe</code>;<code>AudioNode</code> 增加“开始音频处理 / 重新处理音频”按钮。后端触发接口会立即把 job 状态置为 <code>transcribing</code>,让轮询、节点状态和底部音频条能立刻进入运行态。</p>
|
||||
<p><strong>问题:</strong>等待抽帧完成后自动启动音频,不符合“先把声音文案拿出来审核”的工作流;用户需要在音频卡片上直接触发。</p>
|
||||
<p><strong>改动:</strong>移除前端抽帧完成后的自动转写逻辑;<code>AudioNode</code> 保留并固定显示“提取音频 / 重新提取音频”按钮。后端 <code>/transcribe</code> 不再要求 <code>frames_extracted</code>,视频就绪后可直接从 <code>source.mp4</code> 拆出 <code>audio.wav</code>,并继续 ASR、翻译、SKG 改写和 MiniMax 配音。</p>
|
||||
<p><strong>影响:</strong><code>web/app/page.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>api/main.py</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
</article>
|
||||
@@ -975,7 +987,7 @@ SubjectAsset {
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>点击视频抽帧时,后端 4291 端口能连接但 <code>/health</code> 和后续请求长时间不返回,前端看起来像按钮没有反应。</p>
|
||||
<p><strong>原因:</strong><code>pipeline_download</code> 和 <code>pipeline_analyze</code> 声明为 async background task,但内部实际是同步 <code>yt-dlp</code>、<code>ffmpeg</code> 和 Vision 验收;Starlette 会在事件循环里执行 async background task,导致长抽帧把 API 主循环堵住。</p>
|
||||
<p><strong>改动:</strong>下载和抽帧 pipeline 改为普通同步函数,让 FastAPI/Starlette 按线程池后台任务执行;<code>analyze_queue_worker</code> 也改为同步 worker。服务启动恢复时,如果磁盘里有重启前遗留的 <code>downloading</code>、<code>splitting</code> 或 <code>transcribing</code> 运行态,会恢复成可重试状态,避免按钮一直 disabled。</p>
|
||||
<p><strong>改动:</strong>下载和抽帧 pipeline 改为普通同步函数,让 FastAPI/Starlette 按线程池后台任务执行;<code>analyze_queue_worker</code> 也改为同步 worker。服务启动恢复时,如果磁盘里有重启前遗留的 <code>downloading</code>、<code>splitting</code> 或 <code>transcribing</code> 运行态,会恢复成可重试状态,避免按钮一直 disabled。开发运行时改用不带 <code>--reload</code> 的后端命令,避免重载等待后台任务。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>docs/source-analysis.html</code>。已重启本地 4291 后端并验证 <code>/health</code> 立即返回;遗留的 <code>8b37e65521a6</code> job 已恢复为 <code>downloaded</code>,可重新点击抽帧。</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -96,6 +96,7 @@ const EDGES_RAW: Array<[string, string]> = [
|
||||
|
||||
export default function Home() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [clientReady, setClientReady] = useState(false)
|
||||
const [jobs, setJobs] = useState<Job[]>([])
|
||||
const [activeJobId, setActiveJobId] = useState<string | null>(null)
|
||||
const job = useMemo(() => jobs.find((j) => j.id === activeJobId) ?? null, [jobs, activeJobId])
|
||||
@@ -122,6 +123,10 @@ export default function Home() {
|
||||
const flowRef = useRef<any>(null)
|
||||
const lastVideoPanelFocusKey = useRef("")
|
||||
|
||||
useEffect(() => {
|
||||
setClientReady(true)
|
||||
}, [])
|
||||
|
||||
const setSelectedFramesForJob = useCallback((jobId: string, updater: Set<number> | ((prev: Set<number>) => Set<number>)) => {
|
||||
setSelectedFramesByJob((prev) => {
|
||||
const current = new Set(prev[jobId] ?? [])
|
||||
@@ -985,26 +990,30 @@ export default function Home() {
|
||||
{/* 右区:DAG 节点流图(原顶部 storyboard dock 已删除) */}
|
||||
<section className="relative flex-1 min-h-0 flex flex-col">
|
||||
<div className="relative flex-1 min-h-0">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={(instance) => { flowRef.current = instance }}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={NODE_TYPES}
|
||||
colorMode={resolvedTheme === "light" ? "light" : "dark"}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.12 }}
|
||||
minZoom={0.2}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={28} size={1.4} />
|
||||
<Controls position="bottom-left" />
|
||||
<MiniMap position="bottom-right" pannable zoomable nodeStrokeWidth={2} />
|
||||
</ReactFlow>
|
||||
{clientReady ? (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={(instance) => { flowRef.current = instance }}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={NODE_TYPES}
|
||||
colorMode={resolvedTheme === "light" ? "light" : "dark"}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.12 }}
|
||||
minZoom={0.2}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={28} size={1.4} />
|
||||
<Controls position="bottom-left" />
|
||||
<MiniMap position="bottom-right" pannable zoomable nodeStrokeWidth={2} />
|
||||
</ReactFlow>
|
||||
) : (
|
||||
<div className="h-full w-full" suppressHydrationWarning />
|
||||
)}
|
||||
</div>
|
||||
<AudioStrip job={job} />
|
||||
{clientReady && <AudioStrip job={job} />}
|
||||
</section>
|
||||
|
||||
<Toaster theme="system" position="top-center" />
|
||||
|
||||
Reference in New Issue
Block a user