完成极速成片
This commit is contained in:
@@ -821,26 +821,46 @@ export function PipelinePage(props: {
|
||||
// 行38/流程步骤4 · 单卡生成 loading:按「busyKey」分别记录,各按钮互不影响(生成三视图不挡新增人物)
|
||||
const [genBusy, setGenBusy] = useState<Set<string>>(new Set());
|
||||
// 刷新后从后端「认领」在途出图任务:出图在 worker 跑、内存态 genBusy 丢了,据此重建占位卡 loading
|
||||
const [pendingGen, setPendingGen] = useState<Array<{ kind: string; label: string; is_triview: boolean }>>([]);
|
||||
const [pendingGen, setPendingGen] = useState<Array<{ kind: string; label: string; is_triview: boolean; triview_of?: string }>>([]);
|
||||
const pendingHas = (kind: string, label: string) =>
|
||||
pendingGen.some((p) => p.kind === kind && !p.is_triview && (p.label || "") === (label || ""));
|
||||
const pendingTriOf = (assetId?: string | null) =>
|
||||
Boolean(assetId && pendingGen.some((p) => p.is_triview && p.triview_of === assetId));
|
||||
const pendingTriFor = (kind: "person" | "scene", entity: AssetEntity) =>
|
||||
pendingTriOf(entity.group.adopted_asset)
|
||||
|| pendingGen.some((p) => p.is_triview && p.kind === kind && (p.label || "") === (entity.label || entity.name || ""));
|
||||
// 立绘出图后 seed 卡会换成 ent 卡,busyKey 必须两边都记,否则三视图还在跑、卡片已经换身份,转圈会丢。
|
||||
const entityGenKeys = (kind: "person" | "scene", ...names: Array<string | undefined | null>) => {
|
||||
const keys: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const name of names) {
|
||||
const tag = (name || "").trim();
|
||||
if (!tag || seen.has(tag)) continue;
|
||||
seen.add(tag);
|
||||
keys.push(`ent:${kind}:${tag}`, `seed:${kind}:${tag}`);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
// 在途出图集合的稳定指纹:集合变化(开工/出片完成)才变 → 给审核轮询当「重启信号」,
|
||||
// 让真人出片后自动送审的盾能被盯到变绿(否则审核轮询可能在送审前就停了)。仅集合变才变,不会每轮抖动。
|
||||
const pendingGenKey = useMemo(() => pendingGen.map((p) => `${p.kind}:${p.label}:${p.is_triview}`).join("|"), [pendingGen]);
|
||||
const pendingGenKey = useMemo(() => pendingGen.map((p) => `${p.kind}:${p.label}:${p.is_triview}:${p.triview_of || ""}`).join("|"), [pendingGen]);
|
||||
const isBusy = (k: string) => genBusy.has(k);
|
||||
const addBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.add(k); return n; });
|
||||
const delBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.delete(k); return n; });
|
||||
type GenResult = { id?: string; adopted_asset?: string | null } | null;
|
||||
async function genBaseAsset(kind: "product" | "person" | "scene", prompt: string, label: string | undefined, busyKey: string, referenceAssetId?: string): Promise<GenResult> {
|
||||
if (genBusy.has(busyKey)) return null; // 同一按钮防连点(不同按钮可并发)
|
||||
addBusy(busyKey);
|
||||
const triKey = kind === "person" ? `${busyKey}:tri` : "";
|
||||
if (triKey) addBusy(triKey);
|
||||
const aliasKeys = kind === "product"
|
||||
? [busyKey]
|
||||
: [...new Set([busyKey, ...entityGenKeys(kind, label)])];
|
||||
if (aliasKeys.some((k) => genBusy.has(k))) return null; // 同一按钮防连点(不同按钮可并发)
|
||||
aliasKeys.forEach(addBusy);
|
||||
const triKeys = kind === "person" ? aliasKeys.map((k) => `${k}:tri`) : [];
|
||||
triKeys.forEach(addBusy);
|
||||
try {
|
||||
return (await onGenerateBaseAsset(kind, prompt, label, referenceAssetId)) as GenResult;
|
||||
} finally {
|
||||
delBusy(busyKey);
|
||||
if (triKey) delBusy(triKey);
|
||||
aliasKeys.forEach(delBusy);
|
||||
triKeys.forEach(delBusy);
|
||||
}
|
||||
}
|
||||
// 据某一版立绘资产生成它配套的三视图(loading 用 busyKey)
|
||||
@@ -2831,7 +2851,7 @@ export function PipelinePage(props: {
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
prevPendingIdsRef.current = list.map((pending) => pending.id);
|
||||
setPendingGen(list.map((p) => ({ kind: p.kind, label: p.label, is_triview: p.is_triview })));
|
||||
setPendingGen(list.map((p) => ({ kind: p.kind, label: p.label, is_triview: p.is_triview, triview_of: p.triview_of })));
|
||||
// 没有在途出图、本地也没有生成在跑 → 没什么可等,停轮询;再点生成(genBusy 变)时本 effect 会重订阅恢复。
|
||||
if (list.length === 0 && genBusy.size === 0) { stopped = true; return; }
|
||||
} catch {
|
||||
@@ -3615,9 +3635,13 @@ export function PipelinePage(props: {
|
||||
const mainUrl = groupMainUrl(grp);
|
||||
const promptValue = assetPromptDraft[entity.key] ?? grp.prompt ?? "";
|
||||
const entBK = `ent:${kind}:${entity.key}`;
|
||||
const busy = isBusy(entBK) || isBusy(`${entBK}:tri`) || (!mainUrl && pendingHas(kind, entity.name));
|
||||
const genKeys = entityGenKeys(kind, entity.key, entity.name, entity.label);
|
||||
const portraitBusy = genKeys.some((k) => isBusy(k)) || (!mainUrl && pendingHas(kind, entity.name));
|
||||
const triBusy = kind === "person" && (genKeys.some((k) => isBusy(`${k}:tri`)) || pendingTriFor(kind, entity));
|
||||
const busy = portraitBusy || triBusy;
|
||||
const loadingText = kind === "person" && mainUrl ? "三视图生成中…" : "生成中…";
|
||||
const rs = (kind === "person" || kind === "scene") && grp.adopted_asset ? (reviews[grp.adopted_asset] || grp.adopted_asset_review || "") : "";
|
||||
const previewState = busy ? " generating" : mainUrl ? " ready" : " pending";
|
||||
const previewState = `${mainUrl ? " ready" : busy ? " generating" : " pending"}${busy ? " generating" : ""}`;
|
||||
return (
|
||||
<div className="as-gen" data-asset-kind={kind} data-asset-id={entity.key} key={entity.key}>
|
||||
{grp.id && (delArmed === grp.id ? (
|
||||
@@ -3632,14 +3656,15 @@ export function PipelinePage(props: {
|
||||
))}
|
||||
<div
|
||||
className={`as-gen-preview${previewState}`}
|
||||
style={mainUrl && !busy ? { ...mediaStyle(mainUrl), cursor: "pointer" } : { cursor: "pointer" }}
|
||||
style={mainUrl && kind !== "person" ? { ...mediaStyle(mainUrl), cursor: "pointer" } : { cursor: "pointer" }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="点击查看详情"
|
||||
onClick={() => openAssetDetail(kind, entity)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openAssetDetail(kind, entity); } }}
|
||||
>
|
||||
{busy ? <span className="asset-card-loading"><span className="asset-spinner" aria-hidden="true"></span><span>生成中…</span></span> : null}
|
||||
{kind === "person" && mainUrl ? <img className="as-gen-photo" src={mainUrl} alt="" /> : null}
|
||||
{busy ? <span className={`asset-card-loading${mainUrl ? " veil" : ""}`}><span className="asset-spinner" aria-hidden="true"></span><span>{loadingText}</span></span> : null}
|
||||
</div>
|
||||
<div className="as-gen-meta">
|
||||
<h4 onClick={() => openAssetDetail(kind, entity)}>{entity.name}</h4>
|
||||
@@ -3666,7 +3691,7 @@ export function PipelinePage(props: {
|
||||
<span>{kind === "scene" ? "场景库替换" : "模特库替换"}</span>
|
||||
</button>
|
||||
<button className="as-ai-btn" type="button" data-stop disabled={busy} onClick={() => { const p = (assetPromptDraft[entity.key] ?? grp.prompt ?? "").trim() || genPrompt; const ref = kind === "person" && grp.adopted_asset ? grp.adopted_asset : undefined; void genBaseAsset(kind, p, entity.name, entBK, ref); }}>
|
||||
<Sparkles /><span>{busy ? "生成中…" : "AI 生成"}</span>
|
||||
<Sparkles /><span>{busy ? "生成中…" : mainUrl ? "重新生成" : "AI 生成"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4602,9 +4627,10 @@ export function PipelinePage(props: {
|
||||
return false;
|
||||
})();
|
||||
const pBK = `addet-portrait:${entity.key}`;
|
||||
const genKeysPortrait = entityGenKeys(adDetail.kind, entity.key, entity.name, entity.label);
|
||||
// 没立绘时(尤其 seed 首次生成),立绘在 worker 跑 ~30s,本地 isBusy 早已落回 → 用 pendingHas 兜底转圈
|
||||
const busyPortrait = isBusy(pBK) || (!portraitUrl && pendingHas(adDetail.kind, entity.name));
|
||||
const busyTri = isBusy(`addet-tri:${entity.key}`) || isBusy(`${pBK}:tri`);
|
||||
const busyPortrait = isBusy(pBK) || genKeysPortrait.some((k) => isBusy(k)) || (!portraitUrl && pendingHas(adDetail.kind, entity.name));
|
||||
const busyTri = isBusy(`addet-tri:${entity.key}`) || isBusy(`${pBK}:tri`) || genKeysPortrait.some((k) => isBusy(`${k}:tri`)) || pendingTriOf(viewPortraitAsset) || pendingTriFor(adDetail.kind, entity);
|
||||
async function regenPortrait() {
|
||||
const prompt = adPrompt.trim() || grp.prompt || `${entity!.name},9:16 竖屏`;
|
||||
// 重跑立绘 → 追加新立绘并自动接力该版三视图。
|
||||
@@ -4674,15 +4700,16 @@ export function PipelinePage(props: {
|
||||
{!triUrl && (busyTri
|
||||
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">三视图生成中…</span></div>
|
||||
: <span className="ph-frame">正 / 侧 / 背 · 三视图</span>)}
|
||||
{triUrl && busyTri && <span className="sb-gen-veil" aria-hidden="true"><span className="spinner" /></span>}
|
||||
</div>
|
||||
{triUrl && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: triUrl, kind: "image", name: `${entity.name} · 三视图` })}>{zoomSvg}</button>}
|
||||
</div>
|
||||
</div>
|
||||
{!assetAlreadyHasTri && !busyTri && (
|
||||
{(!assetAlreadyHasTri || busyTri) && (
|
||||
<div className="asset-detail-tip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
|
||||
<span>{viewPortraitAsset ? "三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。" : "请先生成左侧「立绘」,有了立绘才能据它生成三视图。"}</span>
|
||||
<button className="ai-gen-btn" type="button" disabled={busyTri || !viewPortraitAsset} onClick={() => void regenTri()}>AI 生成三视图</button>
|
||||
<button className="ai-gen-btn" type="button" disabled={busyTri || !viewPortraitAsset} onClick={() => void regenTri()}>{busyTri ? "生成中…" : assetAlreadyHasTri ? "重新生成三视图" : "AI 生成三视图"}</button>
|
||||
</div>
|
||||
)}
|
||||
{triVersions.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user