diff --git a/.memory/worklog.json b/.memory/worklog.json index 0a80c3f..056c1d9 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,18 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "6346329", - "message": "auto-save 2026-05-15 13:34 (~1)", - "ts": "2026-05-15T13:34:39+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 13:34 (~1)", - "ts": "2026-05-15T05:34:45Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "hash": "765745d", @@ -3260,6 +3247,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 21:58 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-17T22:03:37+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 22:03 (~3)", + "hash": "87015e9", + "files_changed": 3 + }, + { + "ts": "2026-05-17T14:08:30Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 22:03 (~3)", + "files_changed": 1 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 0acad47..00e00c6 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -649,8 +649,8 @@ api/main.py
你看到的区域音频解析结果表
-
主要源码AudioIntakePanel / AudioIntakeStatus / SourceReferenceBuildPanel in web/components/ad-recreation-board.tsx;复用 triggerTranscribeAudioScriptanalyzeJobgenerateSubjectAssets
-
适合怎么描述“原视频播放、12 张关键帧选择、相似主角 6 白底视图、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。
+
主要源码AudioIntakePanel / AudioIntakeStatus / SourceReferenceBuildPanel in web/components/ad-recreation-board.tsx;复用 triggerTranscribeAudioScriptanalyzeJobaddManualFramedeleteFramegenerateSubjectAssets
+
适合怎么描述“原视频播放尺寸、当前播放点手动抽帧、关键帧删除、相似主角 6 白底视图、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。
你看到的区域信息流复刻分镜工作台
@@ -879,7 +879,8 @@ ProductRefStateItem { 分镜脚本改写POST /jobs/{id}/script/rewriterewriteStoryboardScript根据原参考文案、当前新口播、分镜角色、时间段和作者想法改写中文口播。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。接口只返回 items[index,text],前端暂存在当前页面状态里,生成本条视频时写入 StoryboardScene.action。 原始音频文件GET /jobs/{id}/audio.wavsourceAudioUrl返回拆轨得到的 wav;当前主界面不再渲染底部吸附音频条,右侧复刻工作表会读取该文件生成参考图式横向响度波形,并和原视频、逐句时间轴联动。 改写配音文件GET /jobs/{id}/audio-script.mp3apiAssetUrl(job.audio_script.voice_url)后续新配音阶段保留的 MiniMax T2A 产物。当前第一步不默认生成该文件。 - 手动加帧POST /jobs/{id}/frames?t=addManualFrame按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。 + 手动加帧POST /jobs/{id}/frames?t=addManualFrame按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。当前主界面会把原版视频播放秒数传给 SourceReferenceBuildPanel 的“当前点抽帧”。 + 删除关键帧DELETE /jobs/{id}/frames/{idx}deleteFrame删除单张关键帧并清掉对应选择态;当前主界面每张缩略图右下角提供删除入口,方便手动抽错后直接修正。 Vision 识别POST /frames/{idx}/describedescribeFrame写入 frame.description,后续可从 objects 加候选元素。 清洗水印POST /frames/{idx}/cleanupcleanupFrame支持全图和区域清洗,生成 cleaned 待应用版本;前端批量清洗会顺序调用该接口,不自动覆盖原图。单帧清洗状态按 frame.index 隔离,清洗某一张不会禁用其他关键帧的清洗按钮。 应用清洗版POST /frames/{idx}/cleanup/applyapplyCleanedFrame把 cleaned 待应用版本覆盖到原关键帧,并保留首次原图备份;前端“一键替换待应用”会顺序调用该接口应用所有已有清洗版。 @@ -1002,6 +1003,18 @@ ProductRefStateItem {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-17 · 原版视频放大并支持当前点抽帧删除

+ UI + Workflow +
+
+

问题:原版视频区域偏小,只能看和播放;如果需要补某个具体画面,用户必须重新跑 12 帧抽取,并且抽错后缺少直接删除入口。

+

改动:AudioIntakePanel 放大原视频列和视频高度,并把逐句时间轴压成较窄侧栏;SourceReferenceBuildPanel 接收当前播放秒数,新增“当前点抽帧”按钮,复用 addManualFrame;关键帧缩略图右下角新增删除按钮,复用 deleteFrame,并展示全部已抽帧而不只截取前 12 张。

+

影响:web/components/ad-recreation-board.tsxdocs/source-analysis.html。后续描述这里时,应把原版视频旁的关键帧区理解为“人物参考帧池”,支持自动抽人物、按播放点补帧和单帧删除。

+
+

2026-05-17 · 主动抽帧改成人物主体并压缩时间轴侧栏

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index abb953d..2ad7e50 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -851,6 +851,8 @@ export function AdRecreationBoard({ selectedFrames={data.selectedFrames} onToggleFrame={data.onToggleFrame} onJobUpdate={data.onJobUpdate} + onAddFrame={data.onAddManualFrameForJob} + onDeleteFrame={data.onDeleteFrameForJob} /> onToggleFrame: (idx: number) => void onJobUpdate: (job: Job) => void + onAddFrame?: (jobId: string, t: number) => Promise | void + onDeleteFrame?: (jobId: string, idx: number) => Promise | void }) { const [currentTime, setCurrentTime] = useState(0) const [mediaDuration, setMediaDuration] = useState(0) @@ -1126,7 +1132,7 @@ function AudioIntakePanel({ />
-
+
} title="原版视频" /> @@ -1138,7 +1144,7 @@ function AudioIntakePanel({ ref={videoRef} controls playsInline - className="h-[238px] w-full bg-black object-contain" + className="h-[270px] w-full bg-black object-contain 2xl:h-[300px]" src={videoSrcUrl} onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)} onSeeked={(event) => setCurrentTime(event.currentTarget.currentTime)} @@ -1152,7 +1158,7 @@ function AudioIntakePanel({ }} /> ) : ( -
等待原视频
+
等待原视频
)}
@@ -1162,6 +1168,10 @@ function AudioIntakePanel({ selectedFrames={selectedFrames} onToggleFrame={onToggleFrame} onJobUpdate={onJobUpdate} + currentTime={currentTime} + duration={timelineDuration} + onAddFrame={onAddFrame} + onDeleteFrame={onDeleteFrame} />
@@ -1176,7 +1186,7 @@ function AudioIntakePanel({
原文
中文
-
+
{job.transcript.map((segment) => { const active = activeSegment?.index === segment.index return ( @@ -1213,14 +1223,24 @@ function SourceReferenceBuildPanel({ selectedFrames, onToggleFrame, onJobUpdate, + currentTime, + duration, + onAddFrame, + onDeleteFrame, }: { job: Job selectedFrames: Set onToggleFrame: (idx: number) => void onJobUpdate: (job: Job) => void + currentTime: number + duration: number + onAddFrame?: (jobId: string, t: number) => Promise | void + onDeleteFrame?: (jobId: string, idx: number) => Promise | void }) { const [extracting, setExtracting] = useState(false) + const [manualBusy, setManualBusy] = useState(false) const [subjectBusy, setSubjectBusy] = useState(false) + const [deletingFrame, setDeletingFrame] = useState(null) const frames = useMemo(() => [...job.frames].sort((a, b) => a.timestamp - b.timestamp), [job.frames]) const selectedReferenceFrames = useMemo( () => frames.filter((frame) => selectedFrames.has(frame.index)), @@ -1239,6 +1259,7 @@ function SourceReferenceBuildPanel({ return null }, [frames, selectedReferenceFrames]) const actorAssets = actorSource?.element.subject_assets ?? [] + const manualFrameTime = clampNumber(currentTime, 0, Math.max(duration, 1)) const extractKeyframes = async () => { setExtracting(true) @@ -1301,6 +1322,26 @@ function SourceReferenceBuildPanel({ } } + const addFrameAtCurrentTime = async () => { + if (!onAddFrame) return + setManualBusy(true) + try { + await onAddFrame(job.id, manualFrameTime) + } finally { + setManualBusy(false) + } + } + + const deleteReferenceFrame = async (idx: number) => { + if (!onDeleteFrame) return + setDeletingFrame(idx) + try { + await onDeleteFrame(job.id, idx) + } finally { + setDeletingFrame(null) + } + } + return (
@@ -1309,8 +1350,8 @@ function SourceReferenceBuildPanel({ {frames.length ? `${frames.length} 张` : "待抽帧"} · 已选 {selectedReferenceFrames.length}
-
-
+
+
+
- {frames.slice(0, 12).map((frame, index) => { + {frames.map((frame, index) => { const selected = selectedFrames.has(frame.index) return ( - {String(index + 1).padStart(2, "0")} {selected ? : } - + {onDeleteFrame && ( + + )} +
) })} {!frames.length && (
- 点击“抽人物 12 帧”后,这里会展示原视频里主体人物清晰的关键画面。 + 点击“抽人物 12 帧”,或播放到指定时间后用“当前点抽帧”补充人物参考。
)}