fix: allow retrying failed source analysis
This commit is contained in:
@@ -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 环境变量归一化保护。
|
||||
|
||||
25
api/main.py
25
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:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user