fix: allow retrying failed source analysis

This commit is contained in:
2026-05-18 16:49:51 +08:00
parent 07384c5e19
commit 061eb7d867
6 changed files with 73 additions and 6 deletions

View File

@@ -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-18TK 链接下载新增 `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 环境变量归一化保护。

View File

@@ -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:

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -1451,6 +1451,9 @@ function MaterialColumn({
onSubmitUrl: () => void
onStartProduction: () => void
}) {
const actionLabel = !url.trim() && job?.status === "failed"
? job.video_url ? "重新解析" : "重新下载"
: "开始分析"
return (
<section className="flex min-h-0 flex-col gap-3 rounded-lg border border-white/10 bg-white/[0.035] p-3 shadow-2xl">
<header className="shrink-0 border-b border-white/10 pb-3">
@@ -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}
</button>
<button
type="button"

View File

@@ -661,6 +661,15 @@ export async function createJob(tkUrl: string): Promise<Job> {
return res.json()
}
export async function retryJobDownload(id: string): Promise<Job> {
const res = await fetch(`${API_BASE}/jobs/${id}/download/retry`, { method: "POST" })
if (!res.ok) {
const text = await res.text().catch(() => "")
throw apiError("retryJobDownload", res.status, text)
}
return res.json()
}
export async function uploadJob(file: File): Promise<Job> {
const fd = new FormData()
fd.append("file", file)