fix: allow retrying failed source analysis
This commit is contained in:
@@ -58,6 +58,7 @@
|
|||||||
GET /health
|
GET /health
|
||||||
GET /documents
|
GET /documents
|
||||||
POST /jobs
|
POST /jobs
|
||||||
|
POST /jobs/{id}/download/retry
|
||||||
POST /jobs/upload
|
POST /jobs/upload
|
||||||
GET /jobs
|
GET /jobs
|
||||||
GET /jobs/{id}
|
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:TK 链接下载新增 `YTDLP_COOKIES_FILE` / `YTDLP_COOKIES_FROM_BROWSER` 支持;受限视频失败时前端提示上传 MP4 或配置后端 cookies 登录态。
|
||||||
|
- 2026-05-18:素材输入端失败任务支持重新下载 / 重新解析;选中失败且无 `video_url` 的 TK 素材时调用后端重试接口,已有视频的失败任务会清掉自动触发标记并重新跑音频/视觉路。
|
||||||
- 2026-05-18:清理个人语音通道残留,`/health`、前端类型、环境模板和文档不再暴露相关字段或配置。
|
- 2026-05-18:清理个人语音通道残留,`/health`、前端类型、环境模板和文档不再暴露相关字段或配置。
|
||||||
- 2026-05-18:新增后端数据库层,SQLite 默认落在 `APP_DB_URL` / `DATABASE_URL` 或 `JOBS_DIR/app.db`;`/documents` 返回文档归类列表,`/health.database` 返回 DB 状态。
|
- 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 环境变量归一化保护。
|
- 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
|
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)
|
@app.post("/jobs/upload", response_model=Job)
|
||||||
async def create_job_from_upload(bg: BackgroundTasks, file: UploadFile = File(...)) -> Job:
|
async def create_job_from_upload(bg: BackgroundTasks, file: UploadFile = File(...)) -> Job:
|
||||||
if not file.filename:
|
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 {
|
import {
|
||||||
addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage,
|
addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage,
|
||||||
deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, triggerTranscribe, describeFrame, updateStoryboard, copyProductLibraryAsset,
|
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,
|
type Job, type ImageRef, type KeyFrame, type ProductFusionShot, type StoryboardScene, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { TRANSPARENT_HUMAN_NEGATIVE_PROMPT, TRANSPARENT_HUMAN_VIDEO_PROMPT } from "@/lib/workflow-target"
|
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 handleStartProduction = useCallback(async (inputUrl?: string) => {
|
||||||
const trimmed = inputUrl?.trim()
|
const trimmed = inputUrl?.trim()
|
||||||
const created = trimmed ? await handleSubmit(trimmed) : undefined
|
const created = trimmed ? await handleSubmit(trimmed) : undefined
|
||||||
const target = created ?? job
|
let target = created ?? job
|
||||||
if (!target) {
|
if (!target) {
|
||||||
toast.info("先粘贴视频链接或选择一个素材任务")
|
toast.info("先粘贴视频链接或选择一个素材任务")
|
||||||
return
|
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))
|
setProductionJobIds((prev) => new Set(prev).add(target.id))
|
||||||
toast.success("已进入并行素材分析:下载完成后自动跑音频文案路和视觉抽帧路")
|
if (target.video_url) toast.success("已进入并行素材分析:音频文案路和视觉抽帧路会同步推进")
|
||||||
|
else toast.success("已进入并行素材分析:下载完成后自动跑音频文案路和视觉抽帧路")
|
||||||
void startProductionLanesForJob(target)
|
void startProductionLanesForJob(target)
|
||||||
}, [handleSubmit, job, startProductionLanesForJob])
|
}, [handleSubmit, job, startProductionLanesForJob, updateJobInList])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (productionJobIds.size === 0) return
|
if (productionJobIds.size === 0) return
|
||||||
|
|||||||
@@ -1451,6 +1451,9 @@ function MaterialColumn({
|
|||||||
onSubmitUrl: () => void
|
onSubmitUrl: () => void
|
||||||
onStartProduction: () => void
|
onStartProduction: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const actionLabel = !url.trim() && job?.status === "failed"
|
||||||
|
? job.video_url ? "重新解析" : "重新下载"
|
||||||
|
: "开始分析"
|
||||||
return (
|
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">
|
<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">
|
<header className="shrink-0 border-b border-white/10 pb-3">
|
||||||
@@ -1476,7 +1479,7 @@ function MaterialColumn({
|
|||||||
disabled={data.submitting || (!url.trim() && !job)}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -661,6 +661,15 @@ export async function createJob(tkUrl: string): Promise<Job> {
|
|||||||
return res.json()
|
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> {
|
export async function uploadJob(file: File): Promise<Job> {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append("file", file)
|
fd.append("file", file)
|
||||||
|
|||||||
Reference in New Issue
Block a user