feat(core): 基础资产 Agent 化 — 商品三视图(image_edit 参考主图)+ 人物立绘/三视图异步生成 + 演员库/详情
- 商品三视图:有真实商品主图时走 image_edit,以主图为参考锁包装一致(品牌字/配色/外形/Logo) - 人物:据「某一版立绘」异步生成配套三视图(image_edit,worker 内跑),立绘/三视图各存版本可切换 - 基础资产出图改异步(run_base_asset_task / generate_triview_task),Web 层不被慢出图占住 - 前端:演员库浏览/新增、人物详情(立绘+三视图+版本切换/下载/查看大图)、按 busyKey 的单卡并发 loading(genBusy,替代全局 genBusyKey,各按钮互不阻塞) - dev:本地连云端 MySQL 复用连接(CONN_MAX_AGE/health check/connect_timeout),仅 development 生效 - 含 projects 测试补充 tsc + py_compile + 26 后端测试通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -441,6 +441,45 @@ export function App() {
|
||||
return pollImageTasks(mode, ids).catch(() => null);
|
||||
}
|
||||
|
||||
// 轮询一批 AITask(基础资产/三视图异步出图)直到终态;返回成功任务产出的 assets 与最后错误。
|
||||
// 轮询期间 Web 层是空闲的(只发轻量 status 请求),整站不卡;出图后调用方刷新项目即见新资产。
|
||||
async function pollAiTasks(ids: string[]): Promise<{ assets: Asset[]; error: string }> {
|
||||
const pending = new Set(ids);
|
||||
const TERMINAL = new Set(["succeeded", "failed", "cancelled", "compensating"]);
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const assets: Asset[] = [];
|
||||
let error = "";
|
||||
const deadline = Date.now() + 6 * 60 * 1000; // 兜底:6 分钟还没好就停轮询(图仍会在后台出完,刷新可见)
|
||||
while (pending.size > 0 && Date.now() < deadline) {
|
||||
await sleep(2500);
|
||||
const res = await api.generateImageStatus([...pending]).catch(() => null);
|
||||
if (!res) continue;
|
||||
for (const t of res.tasks) {
|
||||
if (!TERMINAL.has(t.status)) continue;
|
||||
pending.delete(t.id);
|
||||
if (t.status === "succeeded") assets.push(...(t.assets || []));
|
||||
else if (t.error_message) error = t.error_message;
|
||||
}
|
||||
}
|
||||
return { assets, error };
|
||||
}
|
||||
|
||||
// 基础资产/三视图异步出图统一入口:提交(秒回任务)→ 轮询出图 → 刷新项目 → 返回新资产 id 供链式(立绘→三视图)。
|
||||
async function submitAndPollAsset(submit: () => Promise<{ task: { id: string; status: string } } | null>, okText: string): Promise<string | null> {
|
||||
const submitted = await submit().catch((e) => { setNotice({ type: "error", text: e instanceof Error ? e.message : "提交失败" }); return null; });
|
||||
const taskId = submitted?.task?.id;
|
||||
if (!taskId) return null; // 提交失败(余额不足/无 worker 等),错误已由 submit 抛出处理
|
||||
const { assets, error } = await pollAiTasks([taskId]);
|
||||
await refreshProjectDetail();
|
||||
void loadData();
|
||||
if (assets.length === 0) {
|
||||
setNotice({ type: "error", text: error || "生成超时,稍后可在资产里查看" });
|
||||
return null;
|
||||
}
|
||||
setNotice({ type: "success", text: okText });
|
||||
return assets[0].id;
|
||||
}
|
||||
|
||||
async function onAuthed(payload: { token: string; user: User; team: Team; remember?: boolean }) {
|
||||
setToken(payload.token, payload.remember ?? true);
|
||||
setUser(payload.user);
|
||||
@@ -564,6 +603,7 @@ export function App() {
|
||||
<ProjectWizardPage
|
||||
products={products}
|
||||
projects={projects}
|
||||
assets={assets}
|
||||
preselectProductId={activeProductId}
|
||||
onBack={() => navigate("projects")}
|
||||
onCreate={async (payload) => {
|
||||
@@ -694,7 +734,11 @@ export function App() {
|
||||
}
|
||||
onAdoptVideoVersion={(segmentId, versionId) => action(() => api.adoptVideoVersion(pipelineProject.id, { video_segment_id: segmentId, version_id: versionId }), "已采用该版本")}
|
||||
onGenerateVoiceover={(payload) => action(() => api.generateVoiceover(pipelineProject.id, payload), "配音已生成")}
|
||||
onGenerateBaseAsset={(kind, prompt, label) => action(() => api.generateBaseAsset(pipelineProject.id, { kind, prompt, label }), "基础资产已生成")}
|
||||
onGenerateBaseAsset={async (kind, prompt, label) => {
|
||||
// 异步:提交→轮询出图→刷新;返回新资产 id 供「立绘→三视图」链式生成
|
||||
const assetId = await submitAndPollAsset(() => api.generateBaseAsset(pipelineProject.id, { kind, prompt, label }), "基础资产已生成");
|
||||
return assetId ? { adopted_asset: assetId } : null;
|
||||
}}
|
||||
onGenerateStoryboard={(prompt) =>
|
||||
action(async () => {
|
||||
// 异步故事板:提交(秒回)后轮询;后端在后台线程逐帧生成,poll 永远秒回,故每轮间隔等待
|
||||
@@ -711,7 +755,11 @@ export function App() {
|
||||
}
|
||||
onAdoptBaseAsset={(groupId, assetId) => action(() => api.adoptBaseAsset(pipelineProject.id, { group_id: groupId, asset_id: assetId }), "已采用该候选")}
|
||||
onAttachBaseAsset={(groupId, assetId) => action(() => api.attachBaseAsset(pipelineProject.id, { group_id: groupId, asset_id: assetId }), "已替换为所选演员")}
|
||||
onGenerateTriview={(portraitAssetId) => action(() => api.generateTriview(pipelineProject.id, { portrait_asset_id: portraitAssetId }), "三视图已据立绘生成")}
|
||||
onGenerateTriview={async (portraitAssetId) => {
|
||||
// 异步:image_edit 慢出图在 worker 跑,轮询期间整站不卡;出图后刷新即见三视图
|
||||
const assetId = await submitAndPollAsset(() => api.generateTriview(pipelineProject.id, { portrait_asset_id: portraitAssetId }), "三视图已据立绘生成");
|
||||
return assetId ? { id: assetId } : null;
|
||||
}}
|
||||
onGenerateActor={(prompt) => generateImages({ prompt, mode: "model", count: 1 })}
|
||||
onUploadActor={(file) => {
|
||||
const fd = new FormData();
|
||||
|
||||
Reference in New Issue
Block a user