diff --git a/.memory/status.md b/.memory/status.md index 0f85d6a..273b42a 100644 --- a/.memory/status.md +++ b/.memory/status.md @@ -58,6 +58,7 @@ GET /health GET /documents POST /jobs +POST /jobs/{id}/download/retry POST /jobs/upload GET /jobs GET /jobs/{id} @@ -99,6 +100,7 @@ POST /jobs/{id}/frames/{idx}/storyboard/video ## 最近变更 - 2026-05-18:TK 链接下载新增 `YTDLP_COOKIES_FILE` / `YTDLP_COOKIES_FROM_BROWSER` 支持;受限视频失败时前端提示上传 MP4 或配置后端 cookies 登录态。 +- 2026-05-18:素材输入端失败任务支持重新下载 / 重新解析;选中失败且无 `video_url` 的 TK 素材时调用后端重试接口,已有视频的失败任务会清掉自动触发标记并重新跑音频/视觉路。 - 2026-05-18:清理个人语音通道残留,`/health`、前端类型、环境模板和文档不再暴露相关字段或配置。 - 2026-05-18:新增后端数据库层,SQLite 默认落在 `APP_DB_URL` / `DATABASE_URL` 或 `JOBS_DIR/app.db`;`/documents` 返回文档归类列表,`/health.database` 返回 DB 状态。 - 2026-05-18:`VISION_MODEL`、`REWRITE_MODEL`、`AUDIO_REWRITE_MODEL` 切到 GPT 默认模型 `gpt-4o`,并加旧 Gemini 环境变量归一化保护。 diff --git a/api/main.py b/api/main.py index 582f4a8..7b6c83e 100644 --- a/api/main.py +++ b/api/main.py @@ -3214,6 +3214,31 @@ async def create_job(req: CreateJobReq, bg: BackgroundTasks) -> Job: return job +@app.post("/jobs/{job_id}/download/retry", response_model=Job) +async def retry_job_download(job_id: str, bg: BackgroundTasks) -> Job: + job = JOBS.get(job_id) + if not job: + raise HTTPException(404, "job not found") + if job.source_kind == "upload" or job.url.startswith("upload://"): + raise HTTPException(409, "uploaded videos cannot be redownloaded; upload the file again") + if job.status in {"downloading", "splitting", "transcribing"}: + raise HTTPException(409, f"job is busy: {job.status}") + + mp4 = job_dir(job_id) / "source.mp4" + if mp4.exists() and mp4.stat().st_size == 0: + mp4.unlink() + update( + job, + status="downloading", + progress=1, + error="", + message="重新提交下载…", + video_url="", + ) + bg.add_task(pipeline_download, job_id) + return job + + @app.post("/jobs/upload", response_model=Job) async def create_job_from_upload(bg: BackgroundTasks, file: UploadFile = File(...)) -> Job: if not file.filename: diff --git a/docs/source-analysis.html b/docs/source-analysis.html index e6bee04..08e6a89 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -588,7 +588,7 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。 - web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 6 张人物随机参考帧,形成“音频文案路 + 视频视觉路”同步推进;底部吸附音频条不再从主界面渲染。 + web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 6 张人物随机参考帧,形成“音频文案路 + 视频视觉路”同步推进;如果选中的 TK 素材下载失败且还没有 video_url,同一个按钮会调用 retryJobDownload 重新下载。底部吸附音频条不再从主界面渲染。 web/components/ad-recreation-board.tsx信息流广告复刻工作表:左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧顶部用“音频文案路、视频视觉路、主体资产、产品资产”四个状态条显示后台并行进度。源视频工作区展示视频下载状态和默认折叠的文案依据。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方是逐句时间轴;下一行铺开“关键帧 / 相似主体”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。关键帧区的主入口是“自动抽帧 6 张”,一键按人物定向随机目标重新抽取 6 张源视频参考帧,先筛出清晰人物 / 中心主体候选,再随机取样,缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,鼠标停留会通过固定浮层放大展示完整帧。“生成 10 张高清图”放在相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,可选择桌面导入的 5 套内置形象作为创意方向,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端调用 generateSubjectAssets 时按主体类型传 subject_style=transparent_humansource_actor,按需传 character_id,并使用 reconstruction_mode=similar;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,先按人物描述、镜头类型、首尾状态和产品佩戴需求,从相似主体 6/10 视图里自动挑选最多 5 张最相关主体视角,再传入 subject_images 和该行自动挑选的产品图 product_images;关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型。只有该行勾选“人物”时,才会传按需筛选后的相似主体参考图;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。 web/app/login/page.tsx生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 @@ -913,6 +913,7 @@ ProductRefStateItem { 文档列表GET /documents后续文档侧栏 / 素材库入口从数据库读取 document 归类列表,包含 source_kind、workflow_mode、primary_job_id、storage_prefix、job_count、asset_count 和更新时间。当前前端还未接主入口,后端已可作为多视频/多上传文档管理的索引。 历史列表GET /jobslistJobs所有 job 精简列表(id/document_id/source_kind/workflow_mode/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;后端自动建立 document_id=job_idsource_kind=tiktok_linkworkflow_mode=feed_recreation 的 document,并在状态保存时同步 DB。下载阶段优先使用 YTDLP_COOKIES_FILE,其次使用 YTDLP_COOKIES_FROM_BROWSER,TikTok 要求登录态时会把错误归一化为“上传 MP4 或配置后端 cookies”。 + 重试下载POST /jobs/{id}/download/retryretryJobDownload用于 TK 链接下载失败且没有 video_url 的素材;清空错误、重新进入下载状态,并在后台再次执行 pipeline_download。上传视频不能重下载,需要重新上传文件。 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态;后端自动建立 source_kind=uploadworkflow_mode=uploaded_reference 的 document。当前上传后也加入第一步队列,下载完成后自动解析音频。 删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob后续阶段保留的抽帧能力。默认 frames=6target 支持人物随机、透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前“开始分析”和源视频旁的“自动抽帧 6 张”都会显式用 target=random_subjectquality=accuratemode=replace 生成清晰人物候选里的随机参考帧池。 @@ -1044,6 +1045,18 @@ ProductRefStateItem {

变更记录

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

+
+
+

2026-05-18 · 素材输入失败后可重新下载

+ API + UI +
+
+

问题:TK 链接下载失败后,素材卡仍保留在列表里,但再次点击“开始分析”只会把 job 放进等待集合;因为没有重新触发下载,失败任务永远不会进入后续音频和抽帧流程。

+

改动:api/main.py 新增 POST /jobs/{id}/download/retry,对非上传型 TK job 清空错误并重新执行 pipeline_downloadweb/lib/api.ts 新增 retryJobDownloadweb/app/page.tsx 在选中失败素材时清掉音频/视觉自动触发标记,若无 video_url 则调用重试接口,有 video_url 则重新触发音频和抽帧;web/components/ad-recreation-board.tsx 把按钮文案切到“重新下载 / 重新解析”。

+

影响:用户无需删除素材再粘贴同一链接;配置 cookies 后可直接对原失败素材重新下载。上传视频如果失败仍需要重新上传文件。

+
+

2026-05-18 · TK 受限视频下载支持 cookies 登录态

diff --git a/web/app/page.tsx b/web/app/page.tsx index 80f70db..a100505 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -19,7 +19,7 @@ import { AdRecreationBoard } from "@/components/ad-recreation-board" import { addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage, deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, triggerTranscribe, describeFrame, updateStoryboard, copyProductLibraryAsset, - formatJobError, + formatJobError, retryJobDownload, type Job, type ImageRef, type KeyFrame, type ProductFusionShot, type StoryboardScene, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget, } from "@/lib/api" import { TRANSPARENT_HUMAN_NEGATIVE_PROMPT, TRANSPARENT_HUMAN_VIDEO_PROMPT } from "@/lib/workflow-target" @@ -574,15 +574,30 @@ export default function Home() { const handleStartProduction = useCallback(async (inputUrl?: string) => { const trimmed = inputUrl?.trim() const created = trimmed ? await handleSubmit(trimmed) : undefined - const target = created ?? job + let target = created ?? job if (!target) { toast.info("先粘贴视频链接或选择一个素材任务") return } + if (!created && target.status === "failed") { + autoTriggeredRef.current.delete(`${target.id}:audio`) + autoTriggeredRef.current.delete(`${target.id}:visual`) + } + if (!created && target.status === "failed" && !target.video_url) { + try { + target = await retryJobDownload(target.id) + updateJobInList(target) + toast.info("已重新提交下载;下载完成后会自动跑音频文案路和视觉抽帧路") + } catch (e) { + toast.error("重新下载失败:" + (e instanceof Error ? e.message : String(e))) + return + } + } setProductionJobIds((prev) => new Set(prev).add(target.id)) - toast.success("已进入并行素材分析:下载完成后自动跑音频文案路和视觉抽帧路") + if (target.video_url) toast.success("已进入并行素材分析:音频文案路和视觉抽帧路会同步推进") + else toast.success("已进入并行素材分析:下载完成后自动跑音频文案路和视觉抽帧路") void startProductionLanesForJob(target) - }, [handleSubmit, job, startProductionLanesForJob]) + }, [handleSubmit, job, startProductionLanesForJob, updateJobInList]) useEffect(() => { if (productionJobIds.size === 0) return diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index bb14014..45fba4a 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -1451,6 +1451,9 @@ function MaterialColumn({ onSubmitUrl: () => void onStartProduction: () => void }) { + const actionLabel = !url.trim() && job?.status === "failed" + ? job.video_url ? "重新解析" : "重新下载" + : "开始分析" return (
@@ -1476,7 +1479,7 @@ function MaterialColumn({ disabled={data.submitting || (!url.trim() && !job)} className="inline-flex h-10 items-center justify-center rounded-md bg-rose-600 px-3 text-[13px] font-semibold text-white transition hover:bg-rose-500 disabled:cursor-not-allowed disabled:opacity-45" > - 开始分析 + {actionLabel}