优化脚本

This commit is contained in:
Azmat@qq.com
2026-08-25 16:40:22 +08:00
parent e2ec2d14af
commit df6784b90c
22 changed files with 893 additions and 156 deletions
+48 -13
View File
@@ -2,17 +2,17 @@ import { useEffect, useMemo, useRef, useState } from "react";
import {
AlertCircle,
ArrowLeft,
Boxes,
Clapperboard,
Download,
ImagePlus,
LayoutPanelTop,
RefreshCw,
ScanSearch,
ScrollText,
SlidersHorizontal,
Sparkles,
Play,
Upload,
UsersRound,
WandSparkles,
X,
Columns2,
@@ -31,10 +31,10 @@ import type { NavigateFn } from "./route-config";
const QUICK_JOB_KEY = "airshelf:quick-create-job";
const PROGRESS_STEPS = [
{ label: "识别商品与卖点", icon: ScanSearch },
{ label: "推荐脚本方向", icon: ScrollText },
{ label: "匹配模特与场景", icon: UsersRound },
{ label: "生成故事板与视频", icon: Clapperboard },
{ label: "脚本", icon: ScrollText },
{ label: "资产", icon: Boxes },
{ label: "故事板", icon: LayoutPanelTop },
{ label: "视频", icon: Clapperboard },
];
const QUICK_RATIOS = [
{ value: "9:16", label: "9:16 竖屏" },
@@ -70,6 +70,14 @@ function historyTitle(item: QuickCreateJob) {
return (item.title || item.product_name || "极速成片").replace(/ · 极速成片$/, "");
}
function historyBadge(item: QuickCreateJob) {
if (item.status === "cancelled") return "已取消";
if (item.status === "succeeded" || item.result?.video_url || item.result?.video_segments?.some((clip) => clip.video_url)) {
return "已完成";
}
return "未完成";
}
function savedJobId() {
try {
return localStorage.getItem(QUICK_JOB_KEY) || "";
@@ -298,6 +306,33 @@ export function QuickCreatePage({
setImages([]);
}
async function retryGeneration() {
if (job?.id && job.status === "failed") {
setSubmitting(true);
setServiceUnavailable(false);
setUnavailableMessage("");
cancelRequestedRef.current = false;
try {
const next = await api.retryQuickCreate(job.id);
setJob(next);
setJobId(next.id);
try {
localStorage.setItem(QUICK_JOB_KEY, next.id);
} catch {
/* ignore */
}
onNotify?.("success", "已从上次进度继续生成");
onProjectCreated?.();
} catch (error) {
onNotify?.("error", error instanceof Error ? error.message : "继续生成失败");
} finally {
setSubmitting(false);
}
return;
}
await startGeneration();
}
async function startGeneration() {
if (!name.trim() || (!images.length && !savedImages.length)) {
onNotify?.("info", "请先填写商品名称并上传商品图片");
@@ -401,11 +436,11 @@ export function QuickCreatePage({
const isGenerating = submitting || job?.status === "queued" || job?.status === "running";
const isComplete = job?.status === "succeeded";
const isCancelled = job?.status === "cancelled";
const isFailed = job?.status === "failed" || isCancelled || serviceUnavailable;
const isFailed = !isGenerating && (job?.status === "failed" || isCancelled || serviceUnavailable);
const displayUrls = [...savedImages.map((image) => image.url), ...imagePreviews];
const imageCount = savedImages.length + images.length;
const canRetry = Boolean(name.trim() && imageCount && selectedVideoModel);
const activePhase = submitting ? 0 : Math.max(0, Math.min(3, job?.phase_index ?? 0));
const activePhase = submitting ? 0 : Math.max(0, Math.min(4, job?.phase_index ?? 0));
const result = job?.result;
const videoClips = result?.video_segments?.length
? result.video_segments
@@ -434,7 +469,6 @@ export function QuickCreatePage({
<button type="button" className="image-back-button" onClick={onBack} aria-label={backLabel}><ArrowLeft /></button>
<div><h1>极速成片</h1></div>
</div>
<div className="project-builder-status"><strong>AI 自动编排</strong><span>· 无需逐步配置</span></div>
</header>
<div className={shellClass} id="quickCreateShell">
@@ -534,7 +568,7 @@ export function QuickCreatePage({
{PROGRESS_STEPS.map((step, index) => {
const Icon = step.icon;
const done = index < activePhase;
const active = isGenerating && index === activePhase;
const active = isGenerating && index === activePhase && activePhase < 4;
return <div key={step.label} className={`quick-progress-node${active ? " active" : ""}${done ? " done" : ""}`}><span className="quick-progress-dot"><Icon /></span><strong>{step.label}</strong></div>;
})}
</div>
@@ -575,10 +609,10 @@ export function QuickCreatePage({
<div className="quick-state quick-state-failed">
<span className="quick-failed-icon"><AlertCircle /></span>
<h2>{serviceUnavailable ? "极速成片暂不可用" : isCancelled ? "已取消本次生成" : "本次生成未完成"}</h2>
<p>{serviceUnavailable ? (unavailableMessage || "后台生成服务未就绪,请确认 Celery worker 已启动后再试。") : job?.error_message || "生成过程中遇到问题,可以重试或重新开始。"}</p>
<p>{serviceUnavailable ? (unavailableMessage || "后台生成服务未就绪,请确认 Celery worker 已启动后再试。") : job?.error_message || "生成还没走完。点重试会从上次进度继续,已生成的故事板不会重做。"}</p>
<div className="quick-failed-actions">
{canRetry && !serviceUnavailable ? (
<button type="button" className="primary-action" onClick={() => void startGeneration()}><RefreshCw />重试</button>
<button type="button" className="primary-action" onClick={() => void retryGeneration()}><RefreshCw />重试</button>
) : null}
<button type="button" className={canRetry && !serviceUnavailable ? "secondary-action" : "primary-action"} onClick={resetResult}>
<RefreshCw />重新开始
@@ -605,6 +639,7 @@ export function QuickCreatePage({
const videoUrl = item.result?.video_url || item.result?.video_segments?.[0]?.video_url || "";
const duration = item.result?.duration_seconds || item.settings?.total_duration || 15;
const scenes = item.result?.video_segments?.length || Math.max(1, Math.round(duration / 15));
const badge = historyBadge(item);
return (
<article key={item.id} className="quick-history-card">
<button
@@ -617,7 +652,7 @@ export function QuickCreatePage({
<small>{formatClock(duration)}</small>
</button>
<div className="quick-history-copy">
<span className="quick-history-badge">已完成</span>
<span className={`quick-history-badge${badge === "已完成" ? "" : " is-wait"}`}>{badge}</span>
<h3>{historyTitle(item)}</h3>
<p>极速成片 · {scenes}场 · {item.result?.aspect_ratio || item.settings?.aspect_ratio || "9:16"} · {(item.result?.resolution || item.settings?.resolution || "720p").toUpperCase()} · {item.result?.video_model || item.settings?.video_model_label || "智能模型"}</p>
</div>