From 7393fdbfa25ed3b8587c96d9fbd60c7ed4752e3a Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 11:05:01 +0800 Subject: [PATCH] auto-save 2026-05-14 11:04 (~6) --- .memory/worklog.json | 14 ++++++------ RULES.md | 4 ++-- api/README.md | 6 +++-- api/main.py | 6 ++--- docs/source-analysis.html | 28 ++++++++++++++++------- web/app/page.tsx | 47 +++++++++++++++++++++++---------------- 6 files changed, 64 insertions(+), 41 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 6049b08..d59b642 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/RULES.md b/RULES.md index 673612e..04244cf 100644 --- a/RULES.md +++ b/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` 七步管线拆解 diff --git a/api/README.md b/api/README.md index 1b50395..28ba9e3 100644 --- a/api/README.md +++ b/api/README.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` — 原视频 diff --git a/api/main.py b/api/main.py index c0601ce..358e62a 100644 --- a/api/main.py +++ b/api/main.py @@ -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 diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 31e6f5a..2675ea2 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -530,8 +530,8 @@ 后端开发服务 - cd api && source .venv/bin/activate && uvicorn main:app --port 4291 --reload - FastAPI,所有任务状态、视频、关键帧、清洗、元素、分镜保存都在 api/main.py。 + cd api && source .venv/bin/activate && uvicorn main:app --host 127.0.0.1 --port 4291 + FastAPI,所有任务状态、视频、关键帧、清洗、元素、分镜保存都在 api/main.py。长下载 / 抽帧 / 音频处理期间不要带 --reload,否则 reload 会等待后台任务结束并让新请求卡住。 测试页面 @@ -792,7 +792,7 @@ SubjectAsset { 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态。 删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob拆轨 + 目标化抽关键帧。默认 frames=12target 支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值;当前 UI 默认 transparent_human。透明骨架人目标会先扩大本地候选池,再调用 Vision 按 6 个分数验收;不合格候选自动丢弃并抽下一候选。mode=append 追加新关键帧;quality=auto 根据本机算力和视频时长自动选择快速、精细或极准。多个抽帧请求进入后端队列顺序处理。 - 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe读取拆轨得到的 audio.wav,先 ASR 得到英文时间戳段落,再翻译中文,随后按 AUDIO_PRODUCT_BRIEF 生成 audio_script.rewritten_text;配置 MINIMAX_API_KEY 后调用 MiniMax T2A 生成 audio_script.voice_url。前端在抽帧完成且尚无 transcript 时会自动触发一次;Audio 节点也提供“开始/重新处理音频”按钮。 + 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav;随后 ASR 得到英文时间戳段落,再翻译中文,并按 AUDIO_PRODUCT_BRIEF 生成 audio_script.rewritten_text;配置 MINIMAX_API_KEY 后调用 MiniMax T2A 生成 audio_script.voice_url。前端不自动触发,用户在 Audio 节点点击“提取音频 / 重新提取音频”即可启动,不依赖抽帧完成。 原始音频文件GET /jobs/{id}/audio.wavsourceAudioUrl返回拆轨得到的 wav;底部 AudioStrip 拉取该文件,用 Web Audio API 解码并计算波形峰值,只读展示,不参与改写。 改写配音文件GET /jobs/{id}/audio-script.mp3apiAssetUrl(job.audio_script.voice_url)返回 MiniMax T2A 生成的 mp3。没有配置 MiniMax 或生成失败时该文件不存在,但改写文案仍会保存在 audio_script.rewritten_text。 手动加帧POST /jobs/{id}/frames?t=addManualFrame按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。 @@ -841,7 +841,7 @@ SubjectAsset { Audio / ASR / Rewrite - 独立声音文案轨:从 audio.wav 提取原始口播、翻译中文、改写成 SKG 产品语境口播;MiniMax T2A 配置后生成配音 mp3。抽帧完成后自动触发一次,也可在主画布 AudioNode 手动开始/重新处理。AudioNode 用“改前 · 原音频 / 改后 · SKG 口播”摘要展示;底部 AudioStrip 吸附屏幕底端,可拖拽调整高度,按时间段展示英文、中文翻译和波形;侧栏 Rewrite 展开后显示完整审核视图。 + 独立声音文案轨:从 source.mp4 直接提取 audio.wav,再提取原始口播、翻译中文、改写成 SKG 产品语境口播;MiniMax T2A 配置后生成配音 mp3。不再等待抽帧完成,用户在主画布 AudioNode 手动点击“提取音频 / 重新提取音频”启动。AudioNode 用“改前 · 原音频 / 改后 · SKG 口播”摘要展示;底部 AudioStrip 吸附屏幕底端,可拖拽调整高度,按时间段展示英文、中文翻译和波形;侧栏 Rewrite 展开后显示完整审核视图。 不要阻断视觉素材管线。 AudioNodeAudioStripASRNodeTranslateNodeRewriteNodepipeline_transcribeAudioScript @@ -920,13 +920,25 @@ SubjectAsset {
-

2026-05-14 · 音频处理支持自动触发和手动重试

+

2026-05-14 · 修复 ReactFlow Hydration 和后端 reload 卡住

+ Canvas + Dev Server +
+
+

问题:刷新页面时 Next 报 Hydration mismatch,ReactFlow 节点服务端默认宽度和客户端 localStorage 里的用户调整宽度不一致;同时提交抖音链接时若后端正在 --reload 等后台任务结束,会出现 4291 端口占用但请求卡住。

+

改动:web/app/page.tsx 增加 clientReady,ReactFlow 和底部音频条只在浏览器挂载后渲染,避免服务端 HTML 和客户端本地布局缓存不一致。后端启动说明改为不带 --reload 的稳定命令,并更新 RULES.mdapi/README.md 和本页运行入口。

+

影响:web/app/page.tsxRULES.mdapi/README.mddocs/source-analysis.html。本地 4291 后端已按无 reload 模式重启。

+
+
+
+
+

2026-05-14 · 音频处理改为音频卡片手动触发

Audio Workflow
-

问题:后端已有 /transcribe 接口,但前端没有入口调用,用户不知道什么时候音频开始工作。

-

改动:前端在 job 进入 frames_extracted 且没有 transcript 时自动调用一次 triggerTranscribeAudioNode 增加“开始音频处理 / 重新处理音频”按钮。后端触发接口会立即把 job 状态置为 transcribing,让轮询、节点状态和底部音频条能立刻进入运行态。

+

问题:等待抽帧完成后自动启动音频,不符合“先把声音文案拿出来审核”的工作流;用户需要在音频卡片上直接触发。

+

改动:移除前端抽帧完成后的自动转写逻辑;AudioNode 保留并固定显示“提取音频 / 重新提取音频”按钮。后端 /transcribe 不再要求 frames_extracted,视频就绪后可直接从 source.mp4 拆出 audio.wav,并继续 ASR、翻译、SKG 改写和 MiniMax 配音。

影响:web/app/page.tsxweb/components/nodes/index.tsxapi/main.pydocs/source-analysis.html

@@ -975,7 +987,7 @@ SubjectAsset {

问题:点击视频抽帧时,后端 4291 端口能连接但 /health 和后续请求长时间不返回,前端看起来像按钮没有反应。

原因:pipeline_downloadpipeline_analyze 声明为 async background task,但内部实际是同步 yt-dlpffmpeg 和 Vision 验收;Starlette 会在事件循环里执行 async background task,导致长抽帧把 API 主循环堵住。

-

改动:下载和抽帧 pipeline 改为普通同步函数,让 FastAPI/Starlette 按线程池后台任务执行;analyze_queue_worker 也改为同步 worker。服务启动恢复时,如果磁盘里有重启前遗留的 downloadingsplittingtranscribing 运行态,会恢复成可重试状态,避免按钮一直 disabled。

+

改动:下载和抽帧 pipeline 改为普通同步函数,让 FastAPI/Starlette 按线程池后台任务执行;analyze_queue_worker 也改为同步 worker。服务启动恢复时,如果磁盘里有重启前遗留的 downloadingsplittingtranscribing 运行态,会恢复成可重试状态,避免按钮一直 disabled。开发运行时改用不带 --reload 的后端命令,避免重载等待后台任务。

影响:api/main.pydocs/source-analysis.html。已重启本地 4291 后端并验证 /health 立即返回;遗留的 8b37e65521a6 job 已恢复为 downloaded,可重新点击抽帧。

diff --git a/web/app/page.tsx b/web/app/page.tsx index 3a714ba..5940cce 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -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([]) const [activeJobId, setActiveJobId] = useState(null) const job = useMemo(() => jobs.find((j) => j.id === activeJobId) ?? null, [jobs, activeJobId]) @@ -122,6 +123,10 @@ export default function Home() { const flowRef = useRef(null) const lastVideoPanelFocusKey = useRef("") + useEffect(() => { + setClientReady(true) + }, []) + const setSelectedFramesForJob = useCallback((jobId: string, updater: Set | ((prev: Set) => Set)) => { setSelectedFramesByJob((prev) => { const current = new Set(prev[jobId] ?? []) @@ -985,26 +990,30 @@ export default function Home() { {/* 右区:DAG 节点流图(原顶部 storyboard dock 已删除) */}
- { 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 }} - > - - - - + {clientReady ? ( + { 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 }} + > + + + + + ) : ( +
+ )}
- + {clientReady && }