Commit 27de8a81d3cf692dba21dbf7d01b908984c99128

Authored by yibo
1 parent 21dc02c3

Add standalone 人衣图生成 admin tool: gpt-image-2 + wavespeed providers, auto-fallbac…

…k, stale-task recovery, pagination

Person reference photos (any subset of full-body/face x front/side/back) +
one garment image -> async background task generating front/side/view
composites of that person wearing the garment, via either GPT Image 2
(万界方舟) or wavespeed.ai's Seedream v4. Split into three pages
(admin-dressgen.html create / admin-dressgenlist.html list /
admin-dressgendetail.html detail) since the combined page's operations
column was easy to lose off-screen; the list page's action column is now
sticky and every thumbnail opens a click-to-zoom preview.

Default provider (image2) auto-falls-back to wavespeed per view on any
failure — an explicit, later product decision overriding this session's
earlier "no auto-switching around moderation" stance, recorded in the code
comment above WAVESPEED_API_KEY. A crashed/restarted process no longer
leaves a task stuck in "generating" forever (resumeDressgenTasks(), same
idea as T8's resumeDressupGenerations()). GET /api/admin/dressgen is now
paginated (?page=&pageSize=, defaults 20) instead of returning the whole
file as a bare array.

19 tests in dressgen-generate.test.js, including a real crash-and-restart
scenario (SIGKILL against a hanging mock, second process reading the same
on-disk store) and the auto-fallback path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Px7XHxSdWfd5b3vfNMwQjd
admin-batch-gen.html
... ... @@ -29,6 +29,7 @@
29 29 <a href="admin-characters.html">人物库</a>
30 30 <a href="admin-garments.html">衣服库</a>
31 31 <a href="admin-dance-refs.html">参考跳舞视频库</a>
  32 + <a href="admin-dressgenlist.html">人衣图生成</a>
32 33 <a href="live.html">← 返回直播频道</a>
33 34 </header>
34 35  
... ...
admin-characters.html
... ... @@ -32,6 +32,7 @@
32 32 <a href="admin-garments.html">衣服库</a>
33 33 <a href="admin-dance-refs.html">参考跳舞视频库</a>
34 34 <a href="admin-batch-gen.html">批量生成</a>
  35 + <a href="admin-dressgenlist.html">人衣图生成</a>
35 36 <a href="live.html">← 返回直播频道</a>
36 37 </header>
37 38  
... ...
admin-dance-refs.html
... ... @@ -33,6 +33,7 @@
33 33 <a href="admin-characters.html">人物库</a>
34 34 <a href="admin-garments.html">衣服库</a>
35 35 <a href="admin-batch-gen.html">批量生成</a>
  36 + <a href="admin-dressgenlist.html">人衣图生成</a>
36 37 <a href="live.html">← 返回直播频道</a>
37 38 </header>
38 39  
... ...
admin-dressgen.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="UTF-8" />
  5 +<title>新建人衣图任务</title>
  6 +<link rel="stylesheet" href="style.css" />
  7 +<style>
  8 + .admin-wrap { padding: 18px 24px; max-width: 1400px; }
  9 + .upload-section-label { font-weight: 600; margin: 14px 0 6px; font-size: 13px; }
  10 + .upload-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 800px; }
  11 + .upload-grid.garment { grid-template-columns: 1fr; max-width: 260px; }
  12 + .upload-slot { border: 1px dashed var(--border); border-radius: 6px; padding: 8px; text-align: center; }
  13 + .upload-slot label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 12px; }
  14 + .upload-slot img { width: 100%; height: 140px; object-fit: cover; border-radius: 4px; margin-top: 6px; display: none; }
  15 + .hint { color: var(--muted); font-size: 12px; }
  16 + .prompt-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-top: 8px; }
  17 + .prompt-grid .field label { font-weight: 600; font-size: 12px; }
  18 + .prompt-grid textarea { min-height: 130px; font-size: 12px; }
  19 +</style>
  20 +</head>
  21 +<body>
  22 +<header class="topbar">
  23 + <h1>新建人衣图任务</h1>
  24 + <div class="spacer"></div>
  25 + <a href="admin-dressgenlist.html">人衣图任务列表</a>
  26 + <a href="admin.html">视频库</a>
  27 + <a href="admin-characters.html">人物库</a>
  28 + <a href="admin-garments.html">衣服库</a>
  29 + <a href="admin-batch-gen.html">批量生成</a>
  30 + <a href="admin-dance-refs.html">参考跳舞视频库</a>
  31 + <a href="live.html">← 返回直播频道</a>
  32 +</header>
  33 +
  34 +<main class="admin-wrap">
  35 + <div class="card">
  36 + <div class="field"><label>人物定妆照(有几张传几张,至少 1 张)+ 新衣服 → 依次生成穿新衣服的全身正/侧/背 3 张图</label></div>
  37 +
  38 + <div class="upload-section-label">人物 · 全身(可选)</div>
  39 + <div class="upload-grid">
  40 + <div class="upload-slot"><label>全身正</label><input type="file" data-slot="bodyFront" accept="image/*" /><img data-preview="bodyFront" /></div>
  41 + <div class="upload-slot"><label>全身侧</label><input type="file" data-slot="bodySide" accept="image/*" /><img data-preview="bodySide" /></div>
  42 + <div class="upload-slot"><label>全身背</label><input type="file" data-slot="bodyBack" accept="image/*" /><img data-preview="bodyBack" /></div>
  43 + </div>
  44 +
  45 + <div class="upload-section-label">人物 · 脸部(可选)</div>
  46 + <div class="upload-grid">
  47 + <div class="upload-slot"><label>脸正</label><input type="file" data-slot="faceFront" accept="image/*" /><img data-preview="faceFront" /></div>
  48 + <div class="upload-slot"><label>脸侧</label><input type="file" data-slot="faceSide" accept="image/*" /><img data-preview="faceSide" /></div>
  49 + <div class="upload-slot"><label>脸背</label><input type="file" data-slot="faceBack" accept="image/*" /><img data-preview="faceBack" /></div>
  50 + </div>
  51 + <div class="hint">全身/脸部 6 张不用都传,传几张算几张,至少上传 1 张人物参考图。</div>
  52 +
  53 + <div class="upload-section-label">新衣服(必传)</div>
  54 + <div class="upload-grid garment">
  55 + <div class="upload-slot"><label>衣服图</label><input type="file" data-slot="garmentImage" accept="image/*" /><img data-preview="garmentImage" /></div>
  56 + </div>
  57 +
  58 + <div class="upload-section-label">生成 prompt(每个视角各一条,已填默认值,改不改都能直接提交)</div>
  59 + <div class="prompt-grid">
  60 + <div class="field"><label>正面 prompt</label><textarea id="promptFront"></textarea></div>
  61 + <div class="field"><label>侧面 prompt</label><textarea id="promptSide"></textarea></div>
  62 + <div class="field"><label>背面 prompt</label><textarea id="promptBack"></textarea></div>
  63 + </div>
  64 +
  65 + <div class="field" style="margin-top:12px;max-width:320px;">
  66 + <label>生成供应商</label>
  67 + <select id="providerSelect">
  68 + <option value="image2">GPT Image 2(万界方舟,默认,60-180秒/张;失败自动改用 Seedream v4)</option>
  69 + <option value="wavespeed">Seedream v4(wavespeed.ai,约30秒/张,更便宜)</option>
  70 + </select>
  71 + </div>
  72 +
  73 + <button id="submitBtn" style="margin-top:12px;">生成人衣图(提交后台任务)</button>
  74 + <span id="submitHint" class="hint"></span>
  75 + <div id="submitError" class="error-box" hidden></div>
  76 + </div>
  77 +</main>
  78 +
  79 +<script>
  80 +const _fetch = window.fetch;
  81 +window.fetch = async (...args) => {
  82 + const res = await _fetch(...args);
  83 + if (res.status === 401 && String(args[0]).startsWith("/api/admin/") && !String(args[0]).includes("/login")) {
  84 + location.href = "/admin-login.html?next=" + encodeURIComponent(location.pathname);
  85 + }
  86 + return res;
  87 +};
  88 +
  89 +const PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];
  90 +
  91 +document.querySelectorAll("[data-slot]").forEach((input) => {
  92 + input.addEventListener("change", () => {
  93 + const preview = document.querySelector(`[data-preview="${input.dataset.slot}"]`);
  94 + const file = input.files[0];
  95 + if (!file) {
  96 + preview.style.display = "none";
  97 + return;
  98 + }
  99 + preview.src = URL.createObjectURL(file);
  100 + preview.style.display = "block";
  101 + });
  102 +});
  103 +
  104 +async function loadDefaultPrompts() {
  105 + const res = await fetch("/api/admin/dressgen/prompts");
  106 + const prompts = await res.json();
  107 + document.getElementById("promptFront").value = prompts.front;
  108 + document.getElementById("promptSide").value = prompts.side;
  109 + document.getElementById("promptBack").value = prompts.back;
  110 +}
  111 +
  112 +async function uploadImage(file) {
  113 + const fd = new FormData();
  114 + fd.append("file", file);
  115 + const res = await fetch("/api/admin/upload", { method: "POST", body: fd });
  116 + if (!res.ok) throw new Error((await res.json()).error || "上传失败");
  117 + return (await res.json()).url;
  118 +}
  119 +
  120 +document.getElementById("submitBtn").addEventListener("click", async () => {
  121 + const errorBox = document.getElementById("submitError");
  122 + const submitBtn = document.getElementById("submitBtn");
  123 + const submitHint = document.getElementById("submitHint");
  124 + errorBox.hidden = true;
  125 +
  126 + const garmentFile = document.querySelector('[data-slot="garmentImage"]').files[0];
  127 + if (!garmentFile) {
  128 + errorBox.hidden = false;
  129 + errorBox.textContent = "衣服图必传";
  130 + return;
  131 + }
  132 + const personFiles = PERSON_SLOTS.filter((slot) => document.querySelector(`[data-slot="${slot}"]`).files[0]);
  133 + if (personFiles.length === 0) {
  134 + errorBox.hidden = false;
  135 + errorBox.textContent = "至少上传 1 张人物参考图(全身/脸部任意角度)";
  136 + return;
  137 + }
  138 +
  139 + submitBtn.disabled = true;
  140 + try {
  141 + submitHint.textContent = "上传中...";
  142 + const urls = { garmentImage: await uploadImage(garmentFile) };
  143 + for (const slot of personFiles) urls[slot] = await uploadImage(document.querySelector(`[data-slot="${slot}"]`).files[0]);
  144 +
  145 + submitHint.textContent = "已提交后台任务...";
  146 + const res = await fetch("/api/admin/dressgen", {
  147 + method: "POST",
  148 + headers: { "Content-Type": "application/json" },
  149 + body: JSON.stringify({
  150 + ...urls,
  151 + provider: document.getElementById("providerSelect").value,
  152 + prompts: {
  153 + front: document.getElementById("promptFront").value,
  154 + side: document.getElementById("promptSide").value,
  155 + back: document.getElementById("promptBack").value,
  156 + },
  157 + }),
  158 + });
  159 + const data = await res.json();
  160 + if (!res.ok) throw new Error(data.error || "提交失败");
  161 + location.href = `admin-dressgendetail.html?id=${data.id}`;
  162 + } catch (e) {
  163 + errorBox.hidden = false;
  164 + errorBox.textContent = e.message;
  165 + } finally {
  166 + submitBtn.disabled = false;
  167 + }
  168 +});
  169 +
  170 +loadDefaultPrompts();
  171 +</script>
  172 +</body>
  173 +</html>
... ...
admin-dressgendetail.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="UTF-8" />
  5 +<title>人衣图任务详情</title>
  6 +<link rel="stylesheet" href="style.css" />
  7 +<style>
  8 + .admin-wrap { padding: 18px 24px; max-width: 1200px; }
  9 + .task-header { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
  10 + .task-id-box { font-family: monospace; font-size: 15px; background: #f4f4f4; border-radius: 4px; padding: 4px 10px; }
  11 + .status-badge { padding: 3px 10px; border-radius: 4px; font-size: 13px; }
  12 + .status-queued, .status-generating { background: #fff8e1; color: #b26a00; }
  13 + .status-done { background: #eef7ee; color: #2e7d32; }
  14 + .status-partial { background: #fff8e1; color: #b26a00; }
  15 + .status-failed { background: #fdecea; color: #c62828; }
  16 + .hint { color: var(--muted); font-size: 12px; }
  17 + .inputs-grid { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px; }
  18 + .inputs-grid figure { margin: 0; text-align: center; }
  19 + .inputs-grid img { width: 84px; height: 118px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border); display: block; cursor: zoom-in; }
  20 + .inputs-grid figcaption { font-size: 11px; color: var(--muted); margin-top: 2px; }
  21 + .view-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 12px; }
  22 + .view-card { border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
  23 + .view-card h3 { margin: 0 0 8px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; }
  24 + .view-output { width: 100%; height: 320px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); display: block; cursor: zoom-in; }
  25 + .view-output-missing { width: 100%; height: 320px; border: 1px dashed var(--border); border-radius: 6px; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
  26 + .view-error { color: #c62828; font-size: 12px; margin-top: 8px; white-space: pre-wrap; word-break: break-word; background: #fdecea; border-radius: 4px; padding: 8px; }
  27 + .view-card textarea { width: 100%; min-height: 110px; font-size: 12px; margin-top: 8px; }
  28 + .view-card button { margin-top: 8px; width: 100%; }
  29 + #notFound { padding: 24px; }
  30 + #lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.82); display: flex; align-items: center; justify-content: center; cursor: zoom-out; z-index: 1000; }
  31 + #lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 6px; }
  32 +</style>
  33 +</head>
  34 +<body>
  35 +<header class="topbar">
  36 + <h1>人衣图任务详情</h1>
  37 + <div class="spacer"></div>
  38 + <a href="admin-dressgenlist.html">← 返回人衣图列表</a>
  39 + <a href="live.html">← 返回直播频道</a>
  40 +</header>
  41 +
  42 +<main class="admin-wrap" id="app" hidden>
  43 + <div class="card">
  44 + <div class="task-header">
  45 + <span class="task-id-box" id="taskId"></span>
  46 + <span class="status-badge" id="statusBadge"></span>
  47 + <span class="hint" id="providerLabel"></span>
  48 + <span class="hint" id="createdAt"></span>
  49 + <span class="spacer"></span>
  50 + <button id="retryAllBtn">重试所有未完成视角</button>
  51 + <button class="danger" id="deleteBtn">删除任务</button>
  52 + </div>
  53 + <div class="hint" id="pollHint" style="margin-top:6px;"></div>
  54 +
  55 + <div class="field" style="margin-top:14px;"><label>输入:人物参考图 + 衣服图</label></div>
  56 + <div class="inputs-grid" id="inputsGrid"></div>
  57 + </div>
  58 +
  59 + <div class="card">
  60 + <div class="field"><label>生成结果(正 / 侧 / 背,每张可单独重新生成)</label></div>
  61 + <div class="view-grid" id="viewGrid"></div>
  62 + </div>
  63 +</main>
  64 +
  65 +<div id="notFound" hidden>
  66 + <p>任务不存在或已被删除。<a href="admin-dressgenlist.html">返回列表</a></p>
  67 +</div>
  68 +
  69 +<script>
  70 +const _fetch = window.fetch;
  71 +window.fetch = async (...args) => {
  72 + const res = await _fetch(...args);
  73 + if (res.status === 401 && String(args[0]).startsWith("/api/admin/") && !String(args[0]).includes("/login")) {
  74 + location.href = "/admin-login.html?next=" + encodeURIComponent(location.pathname);
  75 + }
  76 + return res;
  77 +};
  78 +
  79 +function escapeHtml(s) {
  80 + const div = document.createElement("div");
  81 + div.textContent = s || "";
  82 + return div.innerHTML;
  83 +}
  84 +
  85 +function openLightbox(src) {
  86 + const box = document.createElement("div");
  87 + box.id = "lightbox";
  88 + box.innerHTML = `<img src="${src}" />`;
  89 + box.addEventListener("click", () => box.remove());
  90 + document.body.appendChild(box);
  91 +}
  92 +
  93 +const id = new URLSearchParams(location.search).get("id");
  94 +const PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];
  95 +const PERSON_CAPTION = { bodyFront: "全身正", bodySide: "全身侧", bodyBack: "全身背", faceFront: "脸正", faceSide: "脸侧", faceBack: "脸背" };
  96 +const VIEWS = ["front", "side", "back"];
  97 +const VIEW_LABEL = { front: "正面", side: "侧面", back: "背面" };
  98 +const STATUS_LABEL = { queued: "排队中", generating: "生成中", done: "完成", partial: "部分完成", failed: "失败" };
  99 +const PROVIDER_LABEL = { image2: "GPT Image 2", wavespeed: "Seedream v4" };
  100 +let record = null;
  101 +let pollTimer = null;
  102 +
  103 +if (!id) {
  104 + document.getElementById("notFound").hidden = false;
  105 +} else {
  106 + load();
  107 +}
  108 +
  109 +async function load() {
  110 + const res = await fetch(`/api/admin/dressgen/${id}`);
  111 + if (!res.ok) {
  112 + document.getElementById("notFound").hidden = false;
  113 + document.getElementById("app").hidden = true;
  114 + return;
  115 + }
  116 + record = await res.json();
  117 + document.getElementById("app").hidden = false;
  118 + render();
  119 + if (record.status === "queued" || record.status === "generating") startPolling();
  120 +}
  121 +
  122 +function startPolling() {
  123 + if (pollTimer) return;
  124 + pollTimer = setInterval(async () => {
  125 + await load();
  126 + if (record.status !== "queued" && record.status !== "generating" && pollTimer) {
  127 + clearInterval(pollTimer);
  128 + pollTimer = null;
  129 + }
  130 + }, 3000);
  131 +}
  132 +
  133 +function render() {
  134 + document.getElementById("taskId").textContent = record.id;
  135 + document.getElementById("statusBadge").className = `status-badge status-${record.status}`;
  136 + document.getElementById("statusBadge").textContent = STATUS_LABEL[record.status] || record.status;
  137 + document.getElementById("createdAt").textContent = new Date(record.createdAt).toLocaleString();
  138 + document.getElementById("providerLabel").textContent = `默认供应商: ${PROVIDER_LABEL[record.provider] || record.provider || "GPT Image 2"}`;
  139 + document.getElementById("pollHint").textContent =
  140 + record.status === "queued" || record.status === "generating" ? "任务进行中,页面每 3 秒自动刷新一次…" : "";
  141 + document.getElementById("retryAllBtn").disabled = !(record.status === "partial" || record.status === "failed");
  142 +
  143 + document.getElementById("inputsGrid").innerHTML =
  144 + PERSON_SLOTS.filter((f) => record[f])
  145 + .map((f) => `<figure><img src="${escapeHtml(record[f])}" /><figcaption>${PERSON_CAPTION[f]}</figcaption></figure>`)
  146 + .join("") + `<figure><img src="${escapeHtml(record.garmentImage)}" /><figcaption>衣服</figcaption></figure>`;
  147 + document.querySelectorAll("#inputsGrid img").forEach((img) => img.addEventListener("click", () => openLightbox(img.src)));
  148 +
  149 + document.getElementById("viewGrid").innerHTML = VIEWS.map((view) => {
  150 + const vs = (record.viewStatus && record.viewStatus[view]) || (record.outputImages[view] ? "done" : "failed");
  151 + const url = record.outputImages[view];
  152 + const viewProvider = record.viewProvider && record.viewProvider[view];
  153 + let body;
  154 + if (vs === "done" && url) body = `<img class="view-output" src="${escapeHtml(url)}" />`;
  155 + else if (vs === "queued") body = `<div class="view-output-missing">排队中</div>`;
  156 + else if (vs === "generating") body = `<div class="view-output-missing">生成中…</div>`;
  157 + else body = `<div class="view-output-missing">失败</div>`;
  158 + const providerHint = viewProvider ? `<div class="hint">实际使用: ${PROVIDER_LABEL[viewProvider] || viewProvider}</div>` : "";
  159 + const errorBox = record.errors[view] ? `<div class="view-error">${escapeHtml(record.errors[view])}</div>` : "";
  160 + const busy = vs === "queued" || vs === "generating";
  161 + return `
  162 + <div class="view-card" data-view="${view}">
  163 + <h3>${VIEW_LABEL[view]}<span class="status-badge status-${vs}" style="font-size:11px;">${STATUS_LABEL[vs] || vs}</span></h3>
  164 + ${body}
  165 + ${providerHint}
  166 + ${errorBox}
  167 + <textarea data-prompt="${view}">${escapeHtml(record.prompts[view])}</textarea>
  168 + <button data-regen="${view}" ${busy ? "disabled" : ""}>重新生成这张(可先改上面的 prompt)</button>
  169 + </div>`;
  170 + }).join("");
  171 +
  172 + document.querySelectorAll(".view-output").forEach((img) => img.addEventListener("click", () => openLightbox(img.src)));
  173 + document.querySelectorAll("[data-regen]").forEach((b) =>
  174 + b.addEventListener("click", async () => {
  175 + const view = b.dataset.regen;
  176 + const prompt = document.querySelector(`[data-prompt="${view}"]`).value;
  177 + b.disabled = true;
  178 + await fetch(`/api/admin/dressgen/${id}/retry/${view}`, {
  179 + method: "POST",
  180 + headers: { "Content-Type": "application/json" },
  181 + body: JSON.stringify({ prompt }),
  182 + });
  183 + await load();
  184 + })
  185 + );
  186 +}
  187 +
  188 +document.getElementById("retryAllBtn").addEventListener("click", async () => {
  189 + document.getElementById("retryAllBtn").disabled = true;
  190 + await fetch(`/api/admin/dressgen/${id}/retry`, { method: "POST" });
  191 + await load();
  192 +});
  193 +
  194 +document.getElementById("deleteBtn").addEventListener("click", async () => {
  195 + if (!confirm("确认删除这条任务?")) return;
  196 + await fetch(`/api/admin/dressgen/${id}`, { method: "DELETE" });
  197 + location.href = "admin-dressgenlist.html";
  198 +});
  199 +</script>
  200 +</body>
  201 +</html>
... ...
admin-dressgenlist.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="UTF-8" />
  5 +<title>人衣图任务列表</title>
  6 +<link rel="stylesheet" href="style.css" />
  7 +<style>
  8 + .admin-wrap { padding: 18px 24px; max-width: 1400px; }
  9 + table.admin-table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff; margin-top: 12px; }
  10 + table.admin-table th, table.admin-table td { border-bottom: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: middle; }
  11 + table.admin-table th { color: var(--muted); font-weight: 600; white-space: nowrap; }
  12 + /* Operations column stays reachable without hunting for it when the table
  13 + scrolls horizontally (many thumbnail columns on a narrow window) —
  14 + that's what made "详情" hard to find before this page existed. */
  15 + table.admin-table th:last-child, table.admin-table td:last-child { position: sticky; right: 0; background: #fff; box-shadow: -4px 0 6px -4px rgba(0,0,0,.15); white-space: nowrap; }
  16 + .task-id { font-family: monospace; font-size: 11px; color: var(--muted); }
  17 + .hint { color: var(--muted); font-size: 12px; }
  18 + .thumb-group { display: flex; flex-wrap: wrap; gap: 4px; max-width: 160px; }
  19 + .thumb-group figure { margin: 0; text-align: center; }
  20 + .thumb-group img { width: 40px; height: 56px; object-fit: cover; border-radius: 3px; border: 1px solid var(--border); display: block; }
  21 + .thumb-group figcaption { font-size: 9px; color: var(--muted); }
  22 + .garment-thumb { width: 56px; height: 78px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border); }
  23 + .output-thumb { width: 72px; height: 100px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border); }
  24 + .output-missing { width: 72px; height: 100px; border: 1px dashed var(--border); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--muted); text-align: center; }
  25 + img.clickable { cursor: zoom-in; }
  26 + .status-badge { padding: 2px 8px; border-radius: 4px; font-size: 12px; }
  27 + .status-queued, .status-generating { background: #fff8e1; color: #b26a00; }
  28 + .status-done { background: #eef7ee; color: #2e7d32; }
  29 + .status-partial { background: #fff8e1; color: #b26a00; }
  30 + .status-failed { background: #fdecea; color: #c62828; }
  31 + .row-actions { display: flex; flex-direction: column; gap: 4px; }
  32 + .row-actions button { font-size: 12px; padding: 3px 8px; }
  33 + #lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.82); display: flex; align-items: center; justify-content: center; cursor: zoom-out; z-index: 1000; }
  34 + #lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 6px; }
  35 +</style>
  36 +</head>
  37 +<body>
  38 +<header class="topbar">
  39 + <h1>人衣图任务列表</h1>
  40 + <div class="spacer"></div>
  41 + <a href="admin-dressgen.html">+ 新建任务</a>
  42 + <a href="admin.html">视频库</a>
  43 + <a href="admin-characters.html">人物库</a>
  44 + <a href="admin-garments.html">衣服库</a>
  45 + <a href="admin-batch-gen.html">批量生成</a>
  46 + <a href="admin-dance-refs.html">参考跳舞视频库</a>
  47 + <a href="live.html">← 返回直播频道</a>
  48 +</header>
  49 +
  50 +<main class="admin-wrap">
  51 + <div class="card">
  52 + <div class="field"><label>人衣图任务列表(点任务号或"详情"进详情页;点图片可放大预览;调试时把任务号发我)</label></div>
  53 + <div class="hint" id="countHint"></div>
  54 + <div style="overflow-x:auto;">
  55 + <table class="admin-table">
  56 + <thead>
  57 + <tr>
  58 + <th>任务号</th>
  59 + <th>供应商</th>
  60 + <th>人物参考图</th>
  61 + <th>衣服图</th>
  62 + <th>生成 · 正面</th>
  63 + <th>生成 · 侧面</th>
  64 + <th>生成 · 背面</th>
  65 + <th>状态</th>
  66 + <th>时间</th>
  67 + <th>操作</th>
  68 + </tr>
  69 + </thead>
  70 + <tbody id="tbody"></tbody>
  71 + </table>
  72 + </div>
  73 + <div style="margin-top:10px;display:flex;gap:8px;align-items:center;">
  74 + <button id="prevPageBtn">上一页</button>
  75 + <button id="nextPageBtn">下一页</button>
  76 + </div>
  77 + </div>
  78 +</main>
  79 +
  80 +<script>
  81 +const _fetch = window.fetch;
  82 +window.fetch = async (...args) => {
  83 + const res = await _fetch(...args);
  84 + if (res.status === 401 && String(args[0]).startsWith("/api/admin/") && !String(args[0]).includes("/login")) {
  85 + location.href = "/admin-login.html?next=" + encodeURIComponent(location.pathname);
  86 + }
  87 + return res;
  88 +};
  89 +
  90 +function escapeHtml(s) {
  91 + const div = document.createElement("div");
  92 + div.textContent = s || "";
  93 + return div.innerHTML;
  94 +}
  95 +
  96 +const PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];
  97 +const PERSON_CAPTION = { bodyFront: "全身正", bodySide: "全身侧", bodyBack: "全身背", faceFront: "脸正", faceSide: "脸侧", faceBack: "脸背" };
  98 +const STATUS_LABEL = { queued: "排队中", generating: "生成中", done: "完成", partial: "部分完成", failed: "失败" };
  99 +const PROVIDER_LABEL = { image2: "GPT Image 2", wavespeed: "Seedream v4" };
  100 +const PAGE_SIZE = 20;
  101 +let records = [];
  102 +let total = 0;
  103 +let page = 1;
  104 +let pollTimer = null;
  105 +
  106 +function anyInProgress() {
  107 + return records.some((r) => r.status === "queued" || r.status === "generating");
  108 +}
  109 +
  110 +function startPolling() {
  111 + if (pollTimer) return;
  112 + pollTimer = setInterval(async () => {
  113 + await load();
  114 + if (!anyInProgress()) {
  115 + clearInterval(pollTimer);
  116 + pollTimer = null;
  117 + }
  118 + }, 3000);
  119 +}
  120 +
  121 +async function load() {
  122 + const res = await fetch(`/api/admin/dressgen?page=${page}&pageSize=${PAGE_SIZE}`);
  123 + const data = await res.json();
  124 + records = data.items; // already newest-first from the server
  125 + total = data.total;
  126 + render();
  127 +}
  128 +
  129 +function goToPage(p) {
  130 + const maxPage = Math.max(1, Math.ceil(total / PAGE_SIZE));
  131 + page = Math.min(Math.max(1, p), maxPage);
  132 + load();
  133 +}
  134 +
  135 +function outputCell(r, view) {
  136 + const vs = (r.viewStatus && r.viewStatus[view]) || (r.outputImages && r.outputImages[view] ? "done" : "failed");
  137 + const url = r.outputImages && r.outputImages[view];
  138 + const promptTitle = escapeHtml((r.prompts && r.prompts[view]) || "");
  139 + if (vs === "done" && url) return `<img class="output-thumb clickable" src="${escapeHtml(url)}" title="${promptTitle}" />`;
  140 + if (vs === "queued") return `<div class="output-missing">排队中</div>`;
  141 + if (vs === "generating") return `<div class="output-missing">生成中…</div>`;
  142 + const err = escapeHtml((r.errors && r.errors[view]) || "失败");
  143 + return `<div class="output-missing" title="${err}">失败</div>`;
  144 +}
  145 +
  146 +function render() {
  147 + const maxPage = Math.max(1, Math.ceil(total / PAGE_SIZE));
  148 + document.getElementById("countHint").textContent = `共 ${total} 条,第 ${page}/${maxPage} 页`;
  149 + document.getElementById("prevPageBtn").disabled = page <= 1;
  150 + document.getElementById("nextPageBtn").disabled = page >= maxPage;
  151 + document.getElementById("tbody").innerHTML = records
  152 + .map((r) => {
  153 + const personThumbs = PERSON_SLOTS.filter((f) => r[f])
  154 + .map((f) => `<figure><img class="clickable" src="${escapeHtml(r[f])}" /><figcaption>${PERSON_CAPTION[f]}</figcaption></figure>`)
  155 + .join("");
  156 + const canRetry = r.status === "partial" || r.status === "failed";
  157 + const actions = `
  158 + <div class="row-actions">
  159 + <a href="admin-dressgendetail.html?id=${r.id}">详情</a>
  160 + ${canRetry ? `<button data-retry="${r.id}">重试失败视角</button>` : ""}
  161 + <button class="danger" data-del="${r.id}">删除</button>
  162 + </div>`;
  163 + return `
  164 + <tr>
  165 + <td><a class="task-id" href="admin-dressgendetail.html?id=${r.id}" title="${r.id}">${r.id.slice(0, 8)}</a></td>
  166 + <td>${PROVIDER_LABEL[r.provider] || r.provider || "GPT Image 2"}</td>
  167 + <td><div class="thumb-group">${personThumbs}</div></td>
  168 + <td><img class="garment-thumb clickable" src="${escapeHtml(r.garmentImage)}" /></td>
  169 + <td>${outputCell(r, "front")}</td>
  170 + <td>${outputCell(r, "side")}</td>
  171 + <td>${outputCell(r, "back")}</td>
  172 + <td><span class="status-badge status-${r.status}">${STATUS_LABEL[r.status] || r.status}</span></td>
  173 + <td>${new Date(r.createdAt).toLocaleString()}</td>
  174 + <td>${actions}</td>
  175 + </tr>`;
  176 + })
  177 + .join("");
  178 +
  179 + document.querySelectorAll("#tbody img.clickable").forEach((img) =>
  180 + img.addEventListener("click", () => openLightbox(img.src))
  181 + );
  182 + document.querySelectorAll("[data-del]").forEach((b) =>
  183 + b.addEventListener("click", async () => {
  184 + if (!confirm("确认删除这条任务?")) return;
  185 + const res = await fetch(`/api/admin/dressgen/${b.dataset.del}`, { method: "DELETE" });
  186 + if (res.ok) await load();
  187 + })
  188 + );
  189 + document.querySelectorAll("[data-retry]").forEach((b) =>
  190 + b.addEventListener("click", async () => {
  191 + b.disabled = true;
  192 + const res = await fetch(`/api/admin/dressgen/${b.dataset.retry}/retry`, { method: "POST" });
  193 + if (res.ok) {
  194 + await load();
  195 + startPolling();
  196 + } else {
  197 + b.disabled = false;
  198 + }
  199 + })
  200 + );
  201 +}
  202 +
  203 +function openLightbox(src) {
  204 + const box = document.createElement("div");
  205 + box.id = "lightbox";
  206 + box.innerHTML = `<img src="${src}" />`;
  207 + box.addEventListener("click", () => box.remove());
  208 + document.body.appendChild(box);
  209 +}
  210 +
  211 +document.getElementById("prevPageBtn").addEventListener("click", () => goToPage(page - 1));
  212 +document.getElementById("nextPageBtn").addEventListener("click", () => goToPage(page + 1));
  213 +
  214 +load().then(() => {
  215 + if (anyInProgress()) startPolling();
  216 +});
  217 +</script>
  218 +</body>
  219 +</html>
... ...
admin-garments.html
... ... @@ -32,6 +32,7 @@
32 32 <a href="admin-characters.html">人物库</a>
33 33 <a href="admin-dance-refs.html">参考跳舞视频库</a>
34 34 <a href="admin-batch-gen.html">批量生成</a>
  35 + <a href="admin-dressgenlist.html">人衣图生成</a>
35 36 <a href="live.html">← 返回直播频道</a>
36 37 </header>
37 38  
... ...
admin.html
... ... @@ -36,6 +36,7 @@
36 36 <a href="admin-garments.html">衣服库</a>
37 37 <a href="admin-dance-refs.html">参考跳舞视频库</a>
38 38 <a href="admin-batch-gen.html">批量生成</a>
  39 + <a href="admin-dressgenlist.html">人衣图生成</a>
39 40 <a href="live.html" target="_blank">视频直播间 ↗</a>
40 41 <a href="dance.html" target="_blank">跳舞直播间 ↗</a>
41 42 </header>
... ...
docs/todo/dressgen-admin-tool.md 0 → 100644
  1 +# 人衣图生成 admin 工具 — 现状与待办
  2 +
  3 +独立于主线换装跳舞直播间任务序列(T1-T39,见 `dev-plan*.md`)之外的一个
  4 +standalone 工具:`admin-dressgen.html`(新建任务)+
  5 +`admin-dressgenlist.html`(任务列表)+ `admin-dressgendetail.html`(任务
  6 +详情)。输入人物定妆照(全身/脸部 × 正/侧/背,传几张算几张,至少 1
  7 +张)+ 1 张新衣服图,依次生成这位人物穿新衣服的全身正/侧/背 3 张图。
  8 +2026-09-03 当天从头做到这里,记录一下现状,避免下次接着做时重新踩一遍
  9 +已经踩过的坑。
  10 +
  11 +---
  12 +
  13 +## 1. 已完成
  14 +
  15 +- **异步任务队列**(不是同步阻塞接口——最早的同步版本真的把前端代理
  16 + 拖到 502 超时过一次):`POST /api/admin/dressgen` 立即返回
  17 + `status:"queued"` 的任务行,`runDressGenViews()`(server.js)在响应
  18 + 发出后 fire-and-forget 依次(不并发)跑 3 个视角,每一步都写回
  19 + `dressGenRecords`(`dress-gen.json`),前端轮询 `GET
  20 + /api/admin/dressgen/:id` 看进度。
  21 +- **重试**:`POST /api/admin/dressgen/:id/retry`(整体重试,只重跑没成
  22 + 功的视角)、`POST /api/admin/dressgen/:id/retry/:view`(单张重试,
  23 + 可选带 `{prompt}` 覆盖那一张的 prompt)——都复用任务自己存的图片/
  24 + prompt,不用重新上传、不用重新填 prompt。两个重试接口都额外接受可选
  25 + `{provider}` 覆盖,手动把某个任务/某一张换供应商重跑。
  26 +- **两个供应商都已接入并验证成功**:
  27 + - GPT Image 2(万界方舟):`https://maas-openapi.wanjiedata.com
  28 + /api/v1/images/edits`,`model:"GPT Image 2"`(带空格,写错会被 WAF
  29 + 拦 410 或报 unauthorized),`rejectUnauthorized:false`(万界这个
  30 + host 的证书链在这台机器上验证不过,`-k`),300s 超时,60-180秒/张。
  31 + - Seedream v4(wavespeed.ai 的 `bytedance/seedream-v4/edit`):上传
  32 + `POST /api/v3/media/upload/binary` → 提交 `POST /api/v3/{model}` →
  33 + 轮询 `GET /api/v3/predictions/{id}/result`,约 30 秒/张,$0.027/次,
  34 + 无 TLS 证书问题、无内容审核。`.env` 已配置 `WAVESPEED_API_KEY`。
  35 + - **默认行为(2026-09-03 当天二次确认的产品决策,覆盖了更早的"不做
  36 + 自动切换"那版)**:新任务默认用 image2;某一视角 image2 失败(不
  37 + 区分技术故障还是审核拒绝)→ 自动改用 wavespeed 重试一次,成功的话
  38 + 该视角 `viewProvider[view]` 记为 `"wavespeed"`,前端详情页会显示
  39 + "实际使用: Seedream v4"。手动重试接口仍可传 `{provider}` 强制指定。
  40 + **这条决策的取舍写在 `server.js` 里 `WAVESPEED_API_KEY` 声明上方的
  41 + 注释里,包含为什么会改主意——下次改这块之前先看那段注释。**
  42 +- **17 个自动化测试全过**(`dressgen-generate.test.js`:任务创建即时
  43 + 返回、轮询到终态、per-view 自定义 prompt、单视角失败不拖累其它两个、
  44 + 整体/单视角重试(含 provider 覆盖)、非法路径防护、wavespeed 供应商
  45 + 路径、image2→wavespeed 自动降级等)。
  46 +- **前端拆成三个页面**(原来是"新建+列表"揉在一张页面里,"详情"按钮在
  47 + 宽表格右侧、窄窗口下需要横向滚动才能看到,容易被当成"消失了"):
  48 + - `admin-dressgen.html`:新建任务表单,提交后跳到该任务的详情页。
  49 + - `admin-dressgenlist.html`:任务列表,操作列(详情/重试/删除)用
  50 + `position: sticky; right: 0` 钉住,横向滚动时始终可见;所有缩略图
  51 + (人物参考图/衣服图/生成结果)点击可放大预览(一个简单的
  52 + lightbox,点哪张放大哪张,点遮罩关闭)。
  53 + - `admin-dressgendetail.html`:单任务详情(原 `admin-dressgen-task.
  54 + html` 改名),生成结果图也支持点击放大,每张视角显示"实际使用"的
  55 + 供应商。
  56 + - 其它 admin 页面导航栏的"人衣图生成"链接都改指向
  57 + `admin-dressgenlist.html`(列表作为默认落地页,列表页右上角
  58 + "+新建任务" 去创建页)。
  59 +
  60 +## 2. 待办
  61 +
  62 +- [ ] 生成结果目前是完全独立的列表(`dress-gen.json`),不会进
  63 + `characters.json`/`garments.json`(人物库/衣服库)。**现状确认
  64 + (2026-09-03):人物库现在每个人物只有单张 `imageUrl`,不是"一个
  65 + 人物对应多张可复用资产图"的模型**——`characters.json` 唯一一条
  66 + 种子记录也只有一个 `imageUrl` 字段,没有相册/资产列表这回事。
  67 + "存为新人物"要做成什么形态(人物库本身要不要先改成多资产模型、
  68 + 用 dressgen 的哪张视角图当定妆照等)还没定,做之前需要先对齐。
  69 +
  70 +## 3. 已完成(第二批,2026-09-03 当天晚些时候)
  71 +
  72 +- **任务僵死检测**:`server.js` 的 `resumeDressgenTasks()`,进程启动时
  73 + (`resumeGenerations()`/`resumeDressupGenerations()` 旁边)扫一遍
  74 + `dressGenRecords`,把仍停在 `queued`/`generating` 的视角标记为
  75 + `failed`(错误信息"进程重启,任务丢失,请点重试",前端详情页会照常
  76 + 显示),并按 T8 `generationJobs` 同款逻辑重算整条任务的 `status`。
  77 + 没有可恢复的 in-flight 请求 id(image2/wavespeed 调用是
  78 + `runDressGenViews` 里直接 await 的 HTTP 往返,进程一退什么都不剩),
  79 + 所以是标失败而不是真的续跑——用户点"重试"即可,跟真实生成失败的
  80 + 体验一致。测试:`dressgen-generate.test.js` 里专门起了一个独立
  81 + cwd/mock(mock 故意不响应,制造一个真实卡在 generating 的视角),
  82 + `SIGKILL` 模拟崩溃,重新起一个指向同一份 `dress-gen.json` 的进程,
  83 + 断言恢复后的状态。
  84 +- **列表分页**:`GET /api/admin/dressgen` 现在接受 `?page=&pageSize=`
  85 + (默认 `pageSize=20`,上限 100),按 `createdAt` 倒序(最新的在前)
  86 + 切片,返回 `{items, total, page, pageSize}`(之前是裸数组,前端自己
  87 + `.reverse()`——这是个破坏性的响应格式变更,前端和测试已同步改掉)。
  88 + `admin-dressgenlist.html` 加了"上一页/下一页",操作列还是 sticky,
  89 + 互不冲突。
  90 +
  91 +## 3. 相关文件
  92 +
  93 +- `server.js`:`IMAGE2_*`/`WAVESPEED_*` 常量、`generateDressImage`/
  94 + `generateDressImageViaImage2`/`generateDressImageViaWavespeed`/
  95 + `runDressGenViews`(含自动降级分支)/`dressGenImageAbsPaths`、
  96 + `/api/admin/dressgen*` 路由
  97 +- `dressup-store.js`:`dressGenRecords`
  98 +- `dress-gen.json`:数据文件
  99 +- `admin-dressgen.html`(新建)/ `admin-dressgenlist.html`(列表)/
  100 + `admin-dressgendetail.html`(详情):前端
  101 +- `dressgen-generate.test.js`:测试
  102 +- `.env`:`IMAGE2_API_KEY` / `IMAGE2_API_BASE` / `WAVESPEED_API_KEY` /
  103 + `WAVESPEED_API_BASE`
... ...
dress-gen.json 0 → 100644
  1 +[
  2 + {
  3 + "id": "725d721b-32ca-4eeb-b262-3c74d1315d44",
  4 + "createdAt": 1788430838504,
  5 + "garmentImage": "/uploads/202609/31946cdf-87b9-4ae7-b2bc-739872d383cc.png",
  6 + "prompts": {
  7 + "front": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的正面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  8 + "side": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的侧面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  9 + "back": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的背面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536."
  10 + },
  11 + "bodyFront": "/uploads/202609/7d190e15-6306-4245-8db2-275e14db33b6.png",
  12 + "bodySide": "/uploads/202609/ed7747a4-09be-4b22-8f2d-c2a80d4e2d51.png",
  13 + "bodyBack": null,
  14 + "faceFront": "/uploads/202609/32d90d15-9c76-483b-8a71-1fc82476bcce.png",
  15 + "faceSide": "/uploads/202609/a5cb5ada-435d-484c-9daf-a25f89b8bf03.png",
  16 + "faceBack": null,
  17 + "outputImages": {
  18 + "front": "/uploads/202609/bddb0291-a063-430c-93c2-063dde3e3194.png",
  19 + "side": "/uploads/202609/195494fd-015e-43c4-9e6e-5ca4e5a2da0c.png",
  20 + "back": "/uploads/202609/026e2bd4-d8d1-483c-8536-0ba7cf003e23.png"
  21 + },
  22 + "errors": {
  23 + "front": null,
  24 + "side": null,
  25 + "back": null
  26 + },
  27 + "status": "done",
  28 + "provider": "wavespeed",
  29 + "viewStatus": {
  30 + "front": "done",
  31 + "side": "done",
  32 + "back": "done"
  33 + }
  34 + },
  35 + {
  36 + "id": "3a765740-27b4-4c5c-861b-4c439d4d2739",
  37 + "createdAt": 1788433218590,
  38 + "garmentImage": "/uploads/202609/66564e87-5de9-400e-b676-667fc1503e09.png",
  39 + "prompts": {
  40 + "front": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的正面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  41 + "side": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的侧面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  42 + "back": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的背面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536."
  43 + },
  44 + "bodyFront": "/uploads/202609/930c143c-0c0f-4a8a-86eb-c9140c88bda2.png",
  45 + "bodySide": "/uploads/202609/fc21ef8f-6cb6-4716-bd67-898b12729492.png",
  46 + "bodyBack": null,
  47 + "faceFront": "/uploads/202609/a1709278-b6b6-4a50-8da7-87e148d0cc80.png",
  48 + "faceSide": "/uploads/202609/102131f9-8b96-4665-abdf-d1991eb6dd10.png",
  49 + "faceBack": null,
  50 + "status": "done",
  51 + "viewStatus": {
  52 + "front": "done",
  53 + "side": "done",
  54 + "back": "done"
  55 + },
  56 + "outputImages": {
  57 + "front": "/uploads/202609/192a2405-80a8-454a-bd11-0c69e7104192.png",
  58 + "side": "/uploads/202609/bb470070-ddf5-4c6f-9f35-29e6a3dc6bb7.png",
  59 + "back": "/uploads/202609/1422156f-7e06-43f6-b70c-1736316a1b42.png"
  60 + },
  61 + "errors": {
  62 + "front": null,
  63 + "side": null,
  64 + "back": null
  65 + },
  66 + "provider": "wavespeed"
  67 + },
  68 + {
  69 + "id": "7aedd360-2d18-49f0-819c-daf217c3e5c1",
  70 + "createdAt": 1788435468569,
  71 + "garmentImage": "/uploads/202609/24b0d70c-3b4d-4b1b-8dd5-69dda68368e2.png",
  72 + "prompts": {
  73 + "front": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的正面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  74 + "side": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的侧面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536.",
  75 + "back": "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的背面全身人物形象图。鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536."
  76 + },
  77 + "bodyFront": "/uploads/202609/f0a8a9ba-d13f-4249-b124-4a14f323b7d0.png",
  78 + "bodySide": null,
  79 + "bodyBack": null,
  80 + "faceFront": null,
  81 + "faceSide": null,
  82 + "faceBack": null,
  83 + "status": "done",
  84 + "viewStatus": {
  85 + "front": "done",
  86 + "side": "done",
  87 + "back": "done"
  88 + },
  89 + "outputImages": {
  90 + "front": "/uploads/202609/9ad0387f-757c-47e1-aca4-f8af16c297c5.png",
  91 + "side": "/uploads/202609/34ffe8b7-9e0f-446c-bf51-b0666144d786.png",
  92 + "back": "/uploads/202609/300a073e-a7bb-46c4-a969-7ec038b7e4ad.png"
  93 + },
  94 + "errors": {
  95 + "front": null,
  96 + "side": null,
  97 + "back": null
  98 + }
  99 + }
  100 +]
0 101 \ No newline at end of file
... ...
dressgen-generate.test.js 0 → 100644
  1 +// Integration test for the standalone "人衣图生成" admin tool
  2 +// (admin-dressgen.html + admin-dressgen-task.html): however many person
  3 +// reference photos the admin actually uploads (any subset of full-body/face
  4 +// × front/side/back, at least 1) + 1 garment image -> 3 new full-body images
  5 +// (front/side/back) of that person wearing the garment. This is a real
  6 +// background task: POST /api/admin/dressgen returns the row (status
  7 +// "queued") immediately and the 3 sequential gpt-image-2 calls run
  8 +// fire-and-forget after the response — a blocking synchronous version of
  9 +// this endpoint hit a real 502 from a front-end proxy timing out on the
  10 +// ~3.5 minutes 3 sequential ~70s calls can take (CODEX-IMAGEGEN.md), which
  11 +// is why this is async now. Tests poll GET /api/admin/dressgen/:id until
  12 +// the task reaches a terminal status, the same way a real client would.
  13 +//
  14 +// Runs server.js against a mock gpt-image-2 relay (IMAGE2_API_BASE points
  15 +// at it, over plain http — insecureHttpsRequest dispatches on the URL's own
  16 +// protocol for exactly this reason) so the whole upload -> task -> 3x
  17 +// edit-call -> save-outputs path runs offline. This does NOT prove the real
  18 +// 万界方舟 relay actually speaks this contract at /api/v1/images/edits — see
  19 +// generateDressImage()'s doc comment in server.js: that path's *existence*
  20 +// and the model-id/host/TLS quirks are confirmed (an OPTIONS/GET probe plus
  21 +// a working curl for the sibling /images/generations endpoint), but nobody
  22 +// has yet made a real authenticated call to /images/edits itself. Its
  23 +// request shape here mirrors OpenAI's own public /v1/images/edits contract,
  24 +// which the vendor is expected (per the user, not yet verified end-to-end)
  25 +// to mirror too.
  26 +const test = require("node:test");
  27 +const assert = require("node:assert/strict");
  28 +const fs = require("node:fs");
  29 +const path = require("node:path");
  30 +const os = require("node:os");
  31 +const http = require("node:http");
  32 +const { spawn } = require("node:child_process");
  33 +
  34 +const PORT = 25934 + Math.floor(Math.random() * 1000);
  35 +const BASE = `http://localhost:${PORT}`;
  36 +const ADMIN_PASSWORD = "test-password-only-for-this-run";
  37 +const IMAGE2_API_KEY = "sk-mock-image2-key";
  38 +let cookie, child, cwd, mockImage2, mockImage2Port;
  39 +let editRequests;
  40 +
  41 +const TINY_VALID_PNG = Buffer.from(
  42 + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4nGPkEpFjYGBgYgADAALmAEAUQs4PAAAAAElFTkSuQmCC",
  43 + "base64"
  44 +);
  45 +// A second, distinguishable 1x1 PNG, so the "generated" output isn't
  46 +// byte-identical to an input (would mask a wiring bug that echoes an input
  47 +// back instead of the mock's own response).
  48 +const OUTPUT_PNG = Buffer.from(
  49 + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAAg+lmCAAAAAXNSR0IArs4c6QAAAAtJREFUCNdj+M8AAAICAQD8p9y9AAAAAElFTkSuQmCC",
  50 + "base64"
  51 +);
  52 +
  53 +function waitForServer(url, timeoutMs = 10000) {
  54 + const deadline = Date.now() + timeoutMs;
  55 + return (async function poll() {
  56 + while (Date.now() < deadline) {
  57 + try {
  58 + await fetch(url);
  59 + return;
  60 + } catch {
  61 + await new Promise((r) => setTimeout(r, 100));
  62 + }
  63 + }
  64 + throw new Error("server did not come up in time");
  65 + })();
  66 +}
  67 +
  68 +async function waitUntilTerminal(id, timeoutMs = 8000) {
  69 + const deadline = Date.now() + timeoutMs;
  70 + while (Date.now() < deadline) {
  71 + const record = await fetch(`${BASE}/api/admin/dressgen/${id}`, { headers: { Cookie: cookie } }).then((r) => r.json());
  72 + if (["done", "partial", "failed"].includes(record.status)) return record;
  73 + await new Promise((r) => setTimeout(r, 50));
  74 + }
  75 + throw new Error("task did not reach a terminal status in time");
  76 +}
  77 +
  78 +// "success": every call succeeds. "fail-all": every call fails. "fail-2nd":
  79 +// only the 2nd call (by arrival order — calls are sequential, so this is
  80 +// deterministic) fails.
  81 +let editMode = "success";
  82 +let concurrentCallsSeen = false;
  83 +let inFlight = 0;
  84 +
  85 +// wavespeed mock state — a separate provider path exercised by its own
  86 +// tests (provider:"wavespeed"), sharing the same mock http server as
  87 +// IMAGE2 (routing is by path, same trick as the rest of this file).
  88 +let wavespeedUploadCount = 0;
  89 +let wavespeedSubmitBody = null;
  90 +let wavespeedShouldFail = false;
  91 +function BASE_MOCK_URL() {
  92 + return `http://localhost:${mockImage2Port}`;
  93 +}
  94 +
  95 +function startMockImage2() {
  96 + return new Promise((resolve) => {
  97 + mockImage2 = http.createServer((req, res) => {
  98 + const chunks = [];
  99 + req.on("data", (c) => chunks.push(c));
  100 + req.on("end", () => {
  101 + if (req.method === "POST" && req.url === "/api/v3/media/upload/binary") {
  102 + wavespeedUploadCount++;
  103 + res.writeHead(200, { "Content-Type": "application/json" });
  104 + res.end(JSON.stringify({ code: 200, message: "success", data: { type: "image", download_url: `${BASE_MOCK_URL()}/uploaded-${wavespeedUploadCount}.png`, filename: `uploaded-${wavespeedUploadCount}.png`, size: 123 } }));
  105 + return;
  106 + }
  107 + if (req.method === "POST" && req.url === "/api/v3/bytedance/seedream-v4/edit") {
  108 + wavespeedSubmitBody = JSON.parse(Buffer.concat(chunks).toString("utf8"));
  109 + res.writeHead(200, { "Content-Type": "application/json" });
  110 + res.end(JSON.stringify({ code: 200, message: "success", data: { id: "ws-task-1", status: "created", urls: { get: `${BASE_MOCK_URL()}/api/v3/predictions/ws-task-1/result` } } }));
  111 + return;
  112 + }
  113 + if (req.method === "GET" && req.url === "/api/v3/predictions/ws-task-1/result") {
  114 + if (wavespeedShouldFail) {
  115 + res.writeHead(200, { "Content-Type": "application/json" });
  116 + res.end(JSON.stringify({ code: 200, message: "success", data: { id: "ws-task-1", status: "failed", error: "mock: wavespeed content rejected" } }));
  117 + return;
  118 + }
  119 + res.writeHead(200, { "Content-Type": "application/json" });
  120 + res.end(JSON.stringify({ code: 200, message: "success", data: { id: "ws-task-1", status: "completed", outputs: [`${BASE_MOCK_URL()}/wavespeed-output.png`] } }));
  121 + return;
  122 + }
  123 + if (req.method === "GET" && req.url === "/wavespeed-output.png") {
  124 + res.writeHead(200, { "Content-Type": "image/png" });
  125 + res.end(OUTPUT_PNG);
  126 + return;
  127 + }
  128 + if (req.method === "POST" && req.url === "/api/v1/images/edits") {
  129 + inFlight++;
  130 + if (inFlight > 1) concurrentCallsSeen = true;
  131 + const bodyText = Buffer.concat(chunks).toString("latin1");
  132 + editRequests.push({
  133 + headers: req.headers,
  134 + imageCount: (bodyText.match(/name="image\[\]"/g) || []).length,
  135 + promptMatch: bodyText.match(/name="prompt"\r\n\r\n([^\r]*)/),
  136 + modelMatch: bodyText.match(/name="model"\r\n\r\n([^\r]*)/),
  137 + sizeMatch: bodyText.match(/name="size"\r\n\r\n([^\r]*)/),
  138 + });
  139 + const callNumber = editRequests.length;
  140 + const shouldFail = editMode === "fail-all" || (editMode === "fail-2nd" && callNumber === 2);
  141 + setTimeout(() => {
  142 + inFlight--;
  143 + if (shouldFail) {
  144 + res.writeHead(400, { "Content-Type": "application/json" });
  145 + res.end(JSON.stringify({ error: "mock: content rejected" }));
  146 + return;
  147 + }
  148 + res.writeHead(200, { "Content-Type": "application/json" });
  149 + res.end(JSON.stringify({ data: [{ b64_json: OUTPUT_PNG.toString("base64") }] }));
  150 + }, 20); // small delay so a concurrent implementation would overlap calls
  151 + return;
  152 + }
  153 + res.writeHead(404, { "Content-Type": "application/json" });
  154 + res.end(JSON.stringify({ error: "not found in mock" }));
  155 + });
  156 + });
  157 + mockImage2.listen(0, () => {
  158 + mockImage2Port = mockImage2.address().port;
  159 + resolve();
  160 + });
  161 + });
  162 +}
  163 +
  164 +test.before(async () => {
  165 + await startMockImage2();
  166 +
  167 + cwd = fs.mkdtempSync(path.join(os.tmpdir(), "dressgen-generate-"));
  168 + for (const f of ["server.js", "ingest.js", "uploads.js", "store.js", "dressup-store.js", "prompt-template.js", "db.js", "pricing.js"]) {
  169 + fs.copyFileSync(path.join(__dirname, f), path.join(cwd, f));
  170 + }
  171 + fs.writeFileSync(path.join(cwd, "dress-gen.json"), "[]");
  172 +
  173 + child = spawn(process.execPath, ["server.js"], {
  174 + cwd,
  175 + env: {
  176 + ...process.env,
  177 + PORT: String(PORT),
  178 + H3_CONSOLE_BASE: "",
  179 + H3_CONSOLE_TOKEN: "",
  180 + ADMIN_PASSWORD,
  181 + IMAGE2_API_KEY,
  182 + IMAGE2_API_BASE: `http://localhost:${mockImage2Port}`,
  183 + WAVESPEED_API_KEY: "ws-mock-key",
  184 + WAVESPEED_API_BASE: `http://localhost:${mockImage2Port}`,
  185 + WAVESPEED_POLL_INTERVAL_MS: "50",
  186 + },
  187 + stdio: "pipe",
  188 + });
  189 + await waitForServer(BASE);
  190 +
  191 + const loginRes = await fetch(`${BASE}/api/admin/login`, {
  192 + method: "POST",
  193 + headers: { "Content-Type": "application/json" },
  194 + body: JSON.stringify({ password: ADMIN_PASSWORD }),
  195 + });
  196 + cookie = (loginRes.headers.get("set-cookie") || "").split(";")[0];
  197 +});
  198 +
  199 +test.after(() => {
  200 + child.kill();
  201 + mockImage2.close();
  202 + fs.rmSync(cwd, { recursive: true, force: true });
  203 +});
  204 +
  205 +async function uploadTinyPng(name) {
  206 + const fd = new FormData();
  207 + fd.append("file", new Blob([TINY_VALID_PNG], { type: "image/png" }), name);
  208 + const res = await fetch(`${BASE}/api/admin/upload`, { method: "POST", headers: { Cookie: cookie }, body: fd });
  209 + assert.equal(res.status, 200);
  210 + return (await res.json()).url;
  211 +}
  212 +
  213 +async function createTask(body) {
  214 + const res = await fetch(`${BASE}/api/admin/dressgen`, {
  215 + method: "POST",
  216 + headers: { "Content-Type": "application/json", Cookie: cookie },
  217 + body: JSON.stringify(body),
  218 + });
  219 + return { status: res.status, record: await res.json() };
  220 +}
  221 +
  222 +test("GET /api/admin/dressgen/prompts returns one default prompt per view, front/side/back all distinct", async () => {
  223 + const res = await fetch(`${BASE}/api/admin/dressgen/prompts`, { headers: { Cookie: cookie } });
  224 + assert.equal(res.status, 200);
  225 + const prompts = await res.json();
  226 + assert.ok(prompts.front && prompts.side && prompts.back);
  227 + assert.notEqual(prompts.front, prompts.side);
  228 + assert.notEqual(prompts.side, prompts.back);
  229 +});
  230 +
  231 +test("POST /api/admin/dressgen returns immediately with a queued task id (doesn't block on the 3 upstream calls)", async () => {
  232 + editMode = "success";
  233 + editRequests = [];
  234 + const bodyFront = await uploadTinyPng("body-front.png");
  235 + const faceFront = await uploadTinyPng("face-front.png");
  236 + const garmentImage = await uploadTinyPng("garment.png");
  237 +
  238 + const { status, record } = await createTask({ bodyFront, faceFront, garmentImage });
  239 + assert.equal(status, 200);
  240 + assert.ok(record.id);
  241 + assert.equal(record.status, "queued");
  242 + assert.equal(record.outputImages.front, null, "must not have waited for generation to finish");
  243 + assert.equal(editRequests.length, 0, "the edit calls happen after the response, not before it");
  244 +
  245 + const final = await waitUntilTerminal(record.id);
  246 + assert.equal(final.status, "done");
  247 + assert.equal(editRequests.length, 3, "one call per view even with fewer person refs");
  248 + for (const r of editRequests) {
  249 + assert.equal(r.imageCount, 3, "2 uploaded person refs + garment = 3 images per call");
  250 + assert.equal(r.modelMatch && r.modelMatch[1], "GPT Image 2", "model id must be the literal vendor string, with spaces");
  251 + assert.equal(r.sizeMatch && r.sizeMatch[1], "1024x1536");
  252 + }
  253 + assert.equal(concurrentCallsSeen, false, "calls must be sequential, not concurrent");
  254 + for (const view of ["front", "side", "back"]) {
  255 + assert.equal(final.viewStatus[view], "done");
  256 + const served = await fetch(`${BASE}${final.outputImages[view]}`);
  257 + assert.ok(Buffer.from(await served.arrayBuffer()).equals(OUTPUT_PNG));
  258 + }
  259 +
  260 + const list = await fetch(`${BASE}/api/admin/dressgen?pageSize=100`, { headers: { Cookie: cookie } }).then((r) => r.json());
  261 + assert.ok(list.items.some((r) => r.id === record.id));
  262 + assert.ok(list.total >= 1);
  263 +
  264 + const fetched = await fetch(`${BASE}/api/admin/dressgen/${record.id}`, { headers: { Cookie: cookie } });
  265 + assert.equal(fetched.status, 200);
  266 +});
  267 +
  268 +test("GET /api/admin/dressgen/:id 404s for an unknown id", async () => {
  269 + const res = await fetch(`${BASE}/api/admin/dressgen/does-not-exist`, { headers: { Cookie: cookie } });
  270 + assert.equal(res.status, 404);
  271 +});
  272 +
  273 +test("POST /api/admin/dressgen: a missing garmentImage is rejected synchronously, no task created", async () => {
  274 + const bodyFront = await uploadTinyPng("body-front2.png");
  275 + const { status } = await createTask({ bodyFront });
  276 + assert.equal(status, 400);
  277 +});
  278 +
  279 +test("POST /api/admin/dressgen: zero person refs is rejected synchronously (need at least one)", async () => {
  280 + const garmentImage = await uploadTinyPng("garment2.png");
  281 + const { status } = await createTask({ garmentImage });
  282 + assert.equal(status, 400);
  283 +});
  284 +
  285 +test("POST /api/admin/dressgen: a per-view custom prompt is sent verbatim to that view's edit call, others keep their own default", async () => {
  286 + editMode = "success";
  287 + editRequests = [];
  288 + const bodyFront = await uploadTinyPng("body-front3.png");
  289 + const garmentImage = await uploadTinyPng("garment3.png");
  290 + // ASCII-only on purpose: the mock decodes the raw multipart body as
  291 + // latin1 (binary-safe for the image bytes elsewhere in the same body),
  292 + // which would mangle a UTF-8/Chinese string — not a real server
  293 + // limitation, just this test's own byte-extraction trick.
  294 + const customSidePrompt = "CUSTOM_SIDE_PROMPT_ONLY_FOR_THIS_TEST";
  295 +
  296 + const { status, record } = await createTask({ bodyFront, garmentImage, prompts: { side: customSidePrompt } });
  297 + assert.equal(status, 200);
  298 + assert.equal(record.prompts.side, customSidePrompt);
  299 + assert.notEqual(record.prompts.front, record.prompts.side);
  300 +
  301 + await waitUntilTerminal(record.id);
  302 + const sidePromptSent = editRequests.map((r) => r.promptMatch && r.promptMatch[1]).find((p) => p === customSidePrompt);
  303 + assert.ok(sidePromptSent, "the custom side prompt must actually reach the edit call, not just the stored record");
  304 +});
  305 +
  306 +test("one view failing (2nd, sequential) doesn't lose the other two — task settles as 'partial'", async () => {
  307 + // Both providers must fail for this view to genuinely fail, now that
  308 + // image2 auto-falls-back to wavespeed (see the fallback test above).
  309 + editMode = "fail-2nd";
  310 + wavespeedShouldFail = true;
  311 + editRequests = [];
  312 + const bodyFront = await uploadTinyPng("body-front4.png");
  313 + const garmentImage = await uploadTinyPng("garment4.png");
  314 +
  315 + const { record } = await createTask({ bodyFront, garmentImage });
  316 + const final = await waitUntilTerminal(record.id);
  317 + assert.equal(final.status, "partial");
  318 + assert.ok(final.outputImages.front);
  319 + assert.equal(final.outputImages.side, null);
  320 + assert.equal(final.viewStatus.side, "failed");
  321 + assert.match(final.errors.side, /wavespeed generation failed/, "fallback ran and its failure is what's recorded");
  322 + assert.ok(final.outputImages.back);
  323 + editMode = "success";
  324 + wavespeedShouldFail = false;
  325 +});
  326 +
  327 +test("all 3 views failing -> task settles as 'failed'", async () => {
  328 + editMode = "fail-all";
  329 + wavespeedShouldFail = true; // both providers must fail — see note above
  330 + editRequests = [];
  331 + const bodyFront = await uploadTinyPng("body-front5.png");
  332 + const garmentImage = await uploadTinyPng("garment5.png");
  333 +
  334 + const { record } = await createTask({ bodyFront, garmentImage });
  335 + const final = await waitUntilTerminal(record.id);
  336 + assert.equal(final.status, "failed");
  337 + for (const view of ["front", "side", "back"]) {
  338 + assert.equal(final.outputImages[view], null);
  339 + assert.match(final.errors[view], /wavespeed generation failed/);
  340 + }
  341 + wavespeedShouldFail = false;
  342 +});
  343 +
  344 +test("POST /api/admin/dressgen: rejects a non-/uploads/ path (defense against arbitrary local file reads)", async () => {
  345 + const garmentImage = await uploadTinyPng("garment6.png");
  346 + const { status } = await createTask({ bodyFront: "/etc/passwd", garmentImage });
  347 + assert.equal(status, 400);
  348 +});
  349 +
  350 +test("POST /api/admin/dressgen/:id/retry: whole-task retry reuses the original images/prompts, only redoes the failed view", async () => {
  351 + // Pick up the failed-all task from the previous test and fix the mock.
  352 + const failedRes = await fetch(`${BASE}/api/admin/dressgen?pageSize=100`, { headers: { Cookie: cookie } }).then((r) => r.json());
  353 + const failedTask = failedRes.items.find((r) => r.status === "failed"); // items are already newest-first
  354 + assert.ok(failedTask, "expected a failed task from the earlier all-fail test");
  355 +
  356 + editMode = "success";
  357 + editRequests = [];
  358 + const res = await fetch(`${BASE}/api/admin/dressgen/${failedTask.id}/retry`, { method: "POST", headers: { Cookie: cookie } });
  359 + assert.equal(res.status, 200);
  360 + const kicked = await res.json();
  361 + assert.equal(kicked.status, "generating");
  362 +
  363 + const final = await waitUntilTerminal(failedTask.id);
  364 + assert.equal(final.status, "done");
  365 + assert.equal(editRequests.length, 3, "retry reprocesses all 3 views since all 3 had failed");
  366 + // No new images were uploaded for the retry — it must be the original ones.
  367 + assert.equal(final.garmentImage, failedTask.garmentImage);
  368 + assert.equal(final.bodyFront, failedTask.bodyFront);
  369 +});
  370 +
  371 +test("POST /api/admin/dressgen/:id/retry/:view: regenerates only that one view, leaves the others untouched", async () => {
  372 + editMode = "fail-2nd";
  373 + wavespeedShouldFail = true; // both providers must fail to get a genuinely partial task
  374 + editRequests = [];
  375 + const bodyFront = await uploadTinyPng("body-front6.png");
  376 + const garmentImage = await uploadTinyPng("garment7.png");
  377 + const { record } = await createTask({ bodyFront, garmentImage });
  378 + const partial = await waitUntilTerminal(record.id);
  379 + assert.equal(partial.status, "partial");
  380 + const originalFront = partial.outputImages.front;
  381 + wavespeedShouldFail = false;
  382 +
  383 + editMode = "success";
  384 + editRequests = [];
  385 + const res = await fetch(`${BASE}/api/admin/dressgen/${record.id}/retry/side`, { method: "POST", headers: { Cookie: cookie } });
  386 + assert.equal(res.status, 200);
  387 +
  388 + const final = await waitUntilTerminal(record.id);
  389 + assert.equal(final.status, "done");
  390 + assert.equal(editRequests.length, 1, "per-view retry only calls the API once, for that view");
  391 + assert.equal(final.outputImages.front, originalFront, "the already-succeeded front view must be untouched by a side-only retry");
  392 + assert.ok(final.outputImages.side);
  393 +});
  394 +
  395 +test("POST /api/admin/dressgen/:id/retry/:view: an optional prompt override is used for that call and persisted on the record", async () => {
  396 + editMode = "fail-2nd";
  397 + editRequests = [];
  398 + const bodyFront = await uploadTinyPng("body-front7.png");
  399 + const garmentImage = await uploadTinyPng("garment8.png");
  400 + const { record } = await createTask({ bodyFront, garmentImage });
  401 + await waitUntilTerminal(record.id);
  402 +
  403 + editMode = "success";
  404 + editRequests = [];
  405 + const overridePrompt = "REGENERATE_SIDE_WITH_THIS_EXACT_PROMPT";
  406 + const res = await fetch(`${BASE}/api/admin/dressgen/${record.id}/retry/side`, {
  407 + method: "POST",
  408 + headers: { "Content-Type": "application/json", Cookie: cookie },
  409 + body: JSON.stringify({ prompt: overridePrompt }),
  410 + });
  411 + assert.equal(res.status, 200);
  412 + const kicked = await res.json();
  413 + assert.equal(kicked.prompts.side, overridePrompt, "the override must be persisted on the record immediately");
  414 +
  415 + const final = await waitUntilTerminal(record.id);
  416 + assert.equal(final.prompts.side, overridePrompt);
  417 + const sidePromptSent = editRequests.map((r) => r.promptMatch && r.promptMatch[1]).find((p) => p === overridePrompt);
  418 + assert.ok(sidePromptSent, "the override must actually reach the edit call");
  419 +});
  420 +
  421 +test("POST /api/admin/dressgen/:id/retry: unknown id 404s", async () => {
  422 + const res = await fetch(`${BASE}/api/admin/dressgen/does-not-exist/retry`, { method: "POST", headers: { Cookie: cookie } });
  423 + assert.equal(res.status, 404);
  424 +});
  425 +
  426 +// provider:"wavespeed" — the second provider (2026-09-03), pickable per
  427 +// task via the admin UI, or used automatically as an image2-failure
  428 +// fallback (see the auto-fallback test below, and server.js's
  429 +// WAVESPEED_API_KEY doc comment for the history of that decision).
  430 +// Exercises the real upload -> submit -> poll -> fetch-output contract
  431 +// against the mock wavespeed routes added to the same mock server.
  432 +test("POST /api/admin/dressgen with provider:'wavespeed' uploads every image, submits once per view, and downloads the completed output", async () => {
  433 + editMode = "success";
  434 + wavespeedShouldFail = false;
  435 + wavespeedUploadCount = 0;
  436 + wavespeedSubmitBody = null;
  437 + const bodyFront = await uploadTinyPng("ws-body-front.png");
  438 + const garmentImage = await uploadTinyPng("ws-garment.png");
  439 +
  440 + const { status, record } = await createTask({ bodyFront, garmentImage, provider: "wavespeed" });
  441 + assert.equal(status, 200);
  442 + assert.equal(record.provider, "wavespeed");
  443 +
  444 + const final = await waitUntilTerminal(record.id);
  445 + assert.equal(final.status, "done");
  446 + assert.equal(wavespeedUploadCount, 6, "2 images (body+garment) uploaded once per each of the 3 sequential view calls");
  447 + assert.ok(Array.isArray(wavespeedSubmitBody.images) && wavespeedSubmitBody.images.length === 2, "submit body must carry the uploaded image URLs, not local paths");
  448 + assert.ok(wavespeedSubmitBody.prompt, "submit body must carry the view's prompt");
  449 +
  450 + for (const view of ["front", "side", "back"]) {
  451 + const served = await fetch(`${BASE}${final.outputImages[view]}`);
  452 + assert.ok(Buffer.from(await served.arrayBuffer()).equals(OUTPUT_PNG));
  453 + }
  454 +});
  455 +
  456 +test("POST /api/admin/dressgen with provider:'wavespeed': a failed generation is recorded per view same as the image2 path", async () => {
  457 + wavespeedShouldFail = true;
  458 + const bodyFront = await uploadTinyPng("ws-body-front2.png");
  459 + const garmentImage = await uploadTinyPng("ws-garment2.png");
  460 +
  461 + const { record } = await createTask({ bodyFront, garmentImage, provider: "wavespeed" });
  462 + const final = await waitUntilTerminal(record.id);
  463 + assert.equal(final.status, "failed");
  464 + for (const view of ["front", "side", "back"]) {
  465 + assert.equal(final.outputImages[view], null);
  466 + assert.match(final.errors[view], /wavespeed generation failed/);
  467 + }
  468 + wavespeedShouldFail = false;
  469 +});
  470 +
  471 +test("POST /api/admin/dressgen: default provider (image2) failing auto-falls-back to wavespeed per view, no admin action needed", async () => {
  472 + editMode = "fail-all";
  473 + wavespeedShouldFail = false;
  474 + const bodyFront = await uploadTinyPng("fallback-body-front.png");
  475 + const garmentImage = await uploadTinyPng("fallback-garment.png");
  476 +
  477 + const { status, record } = await createTask({ bodyFront, garmentImage }); // no provider -> defaults to image2
  478 + assert.equal(status, 200);
  479 + assert.equal(record.provider, "image2");
  480 +
  481 + const final = await waitUntilTerminal(record.id);
  482 + assert.equal(final.status, "done", "image2 failed on every view but wavespeed picked up each one");
  483 + for (const view of ["front", "side", "back"]) {
  484 + assert.ok(final.outputImages[view], `${view} must have an output despite image2 failing`);
  485 + assert.equal(final.errors[view], null);
  486 + assert.equal(final.viewProvider[view], "wavespeed", "record must reflect which provider actually produced this view");
  487 + }
  488 + editMode = "success";
  489 +});
  490 +
  491 +test("POST /api/admin/dressgen: an unrecognized provider value falls back to the default (image2), not an error", async () => {
  492 + editMode = "success";
  493 + editRequests = [];
  494 + const bodyFront = await uploadTinyPng("ws-body-front3.png");
  495 + const garmentImage = await uploadTinyPng("ws-garment3.png");
  496 +
  497 + const { status, record } = await createTask({ bodyFront, garmentImage, provider: "not-a-real-provider" });
  498 + assert.equal(status, 200);
  499 + assert.equal(record.provider, "image2");
  500 + await waitUntilTerminal(record.id);
  501 +});
  502 +
  503 +// Pagination (2026-09-03) — GET /api/admin/dressgen used to return the
  504 +// whole file as a bare array; by this point in the suite there are well
  505 +// over 2 records, so this doesn't need to create its own.
  506 +test("GET /api/admin/dressgen: paginates, newest first, page 1 and page 2 don't overlap", async () => {
  507 + const page1 = await fetch(`${BASE}/api/admin/dressgen?page=1&pageSize=2`, { headers: { Cookie: cookie } }).then((r) => r.json());
  508 + assert.equal(page1.items.length, 2);
  509 + assert.equal(page1.page, 1);
  510 + assert.equal(page1.pageSize, 2);
  511 + assert.ok(page1.total >= 10, "the suite has created well more than 2 tasks by now");
  512 +
  513 + const page2 = await fetch(`${BASE}/api/admin/dressgen?page=2&pageSize=2`, { headers: { Cookie: cookie } }).then((r) => r.json());
  514 + assert.equal(page2.items.length, 2);
  515 + const page1Ids = new Set(page1.items.map((r) => r.id));
  516 + assert.ok(page2.items.every((r) => !page1Ids.has(r.id)), "page 2 must not repeat page 1's items");
  517 + assert.ok(page1.items[0].createdAt >= page1.items[1].createdAt, "newest-first within a page");
  518 + assert.ok(page1.items[1].createdAt >= page2.items[0].createdAt, "newest-first across pages too");
  519 +});
  520 +
  521 +// Stale-task recovery (2026-09-03): a record can be left sitting in
  522 +// "generating" forever if the process dies mid-view — nothing resumes an
  523 +// in-flight HTTP call across a restart. Own isolated server + mock (a mock
  524 +// that never responds, so the task is provably still "generating" when
  525 +// killed) sharing one on-disk dress-gen.json across two child processes,
  526 +// simulating a crash-and-restart.
  527 +test("a task stuck in 'generating' when the process restarts is marked failed on the next boot, not stuck forever", async () => {
  528 + let hangingMock;
  529 + const hangingPort = await new Promise((resolve) => {
  530 + hangingMock = http.createServer((req, res) => {
  531 + if (req.method === "POST" && req.url === "/api/v1/images/edits") return; // never responds
  532 + res.writeHead(404).end();
  533 + });
  534 + hangingMock.listen(0, () => resolve(hangingMock.address().port));
  535 + });
  536 +
  537 + const port2 = 26934 + Math.floor(Math.random() * 1000);
  538 + const base2 = `http://localhost:${port2}`;
  539 + const cwd2 = fs.mkdtempSync(path.join(os.tmpdir(), "dressgen-restart-"));
  540 + for (const f of ["server.js", "ingest.js", "uploads.js", "store.js", "dressup-store.js", "prompt-template.js", "db.js", "pricing.js"]) {
  541 + fs.copyFileSync(path.join(__dirname, f), path.join(cwd2, f));
  542 + }
  543 + fs.writeFileSync(path.join(cwd2, "dress-gen.json"), "[]");
  544 + fs.mkdirSync(path.join(cwd2, "uploads"), { recursive: true });
  545 + fs.writeFileSync(path.join(cwd2, "uploads", "restart-body.png"), TINY_VALID_PNG);
  546 + fs.writeFileSync(path.join(cwd2, "uploads", "restart-garment.png"), TINY_VALID_PNG);
  547 +
  548 + const env2 = {
  549 + ...process.env,
  550 + PORT: String(port2),
  551 + H3_CONSOLE_BASE: "",
  552 + H3_CONSOLE_TOKEN: "",
  553 + ADMIN_PASSWORD,
  554 + IMAGE2_API_KEY,
  555 + IMAGE2_API_BASE: `http://localhost:${hangingPort}`,
  556 + WAVESPEED_API_KEY: "", // no fallback — this test wants a plain stuck "generating" view
  557 + };
  558 +
  559 + let child2 = spawn(process.execPath, ["server.js"], { cwd: cwd2, env: env2, stdio: "pipe" });
  560 + try {
  561 + await waitForServer(base2);
  562 + const loginRes = await fetch(`${base2}/api/admin/login`, {
  563 + method: "POST",
  564 + headers: { "Content-Type": "application/json" },
  565 + body: JSON.stringify({ password: ADMIN_PASSWORD }),
  566 + });
  567 + const cookie2 = (loginRes.headers.get("set-cookie") || "").split(";")[0];
  568 +
  569 + const created = await fetch(`${base2}/api/admin/dressgen`, {
  570 + method: "POST",
  571 + headers: { "Content-Type": "application/json", Cookie: cookie2 },
  572 + body: JSON.stringify({ bodyFront: "/uploads/restart-body.png", garmentImage: "/uploads/restart-garment.png" }),
  573 + }).then((r) => r.json());
  574 +
  575 + // Wait for the front view to actually reach "generating" (i.e. the
  576 + // hanging mock has been called) before killing — proves this is a
  577 + // genuinely in-flight task, not a "queued" one that never started.
  578 + const deadline = Date.now() + 5000;
  579 + let record;
  580 + do {
  581 + record = await fetch(`${base2}/api/admin/dressgen/${created.id}`, { headers: { Cookie: cookie2 } }).then((r) => r.json());
  582 + if (record.viewStatus.front === "generating") break;
  583 + await new Promise((r) => setTimeout(r, 20));
  584 + } while (Date.now() < deadline);
  585 + assert.equal(record.viewStatus.front, "generating", "front view must genuinely be in flight before the simulated crash");
  586 +
  587 + child2.kill("SIGKILL"); // simulate a crash — no graceful shutdown, no chance to mark anything failed on the way out
  588 + await new Promise((r) => setTimeout(r, 200));
  589 +
  590 + child2 = spawn(process.execPath, ["server.js"], { cwd: cwd2, env: env2, stdio: "pipe" });
  591 + await waitForServer(base2);
  592 + // adminSessions lives in memory — a fresh process means the old cookie
  593 + // is gone too, not just the in-flight generation.
  594 + const loginRes2 = await fetch(`${base2}/api/admin/login`, {
  595 + method: "POST",
  596 + headers: { "Content-Type": "application/json" },
  597 + body: JSON.stringify({ password: ADMIN_PASSWORD }),
  598 + });
  599 + const cookie3 = (loginRes2.headers.get("set-cookie") || "").split(";")[0];
  600 + const recovered = await fetch(`${base2}/api/admin/dressgen/${created.id}`, { headers: { Cookie: cookie3 } }).then((r) => r.json());
  601 + assert.equal(recovered.viewStatus.front, "failed");
  602 + assert.match(recovered.errors.front, /进程重启/);
  603 + assert.equal(recovered.status, "failed", "no view ever succeeded, so the whole task is 'failed'");
  604 + } finally {
  605 + child2.kill();
  606 + hangingMock.close();
  607 + fs.rmSync(cwd2, { recursive: true, force: true });
  608 + }
  609 +});
... ...
dressup-store.js
... ... @@ -23,4 +23,9 @@ const moderationLog = createStore(path.join(__dirname, &quot;moderation-log.json&quot;));
23 23 // the automated face-blacklist model (T32) or garment moderation (T26).
24 24 const blacklist = createStore(path.join(__dirname, "blacklist.json"));
25 25  
26   -module.exports = { characters, garments, danceRefs, generationJobs, moderationLog, blacklist };
  26 +// Standalone admin tool (separate from the dressup dance room pipeline
  27 +// above): one row per person-3-views + garment -> composite portrait call
  28 +// through the gpt-image-2 relay, see generateDressImage() in server.js.
  29 +const dressGenRecords = createStore(path.join(__dirname, "dress-gen.json"));
  30 +
  31 +module.exports = { characters, garments, danceRefs, generationJobs, moderationLog, blacklist, dressGenRecords };
... ...
generation-jobs.json
... ... @@ -149,9 +149,9 @@
149 149 "garmentId": "98a5fd6a-5db0-48d9-a21b-a3d800c5a4fd",
150 150 "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
151 151 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
152   - "status": "generating",
  152 + "status": "failed",
153 153 "h3JobId": "f068a14de195",
154   - "error": null,
  154 + "error": "用户手动取消",
155 155 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
156 156 },
157 157 {
... ... @@ -161,10 +161,11 @@
161 161 "garmentId": "91949fc3-0b5d-4b9b-b713-8f8b820d1624",
162 162 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
163 163 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
164   - "status": "generating",
  164 + "status": "done",
165 165 "h3JobId": "09fbde537cd0",
166 166 "error": null,
167   - "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
  167 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement.",
  168 + "generatedVideoId": "09fbde537cd0"
168 169 },
169 170 {
170 171 "id": "6d7d7dbd-3d96-4102-b0d3-1bb9661f7ab8",
... ... @@ -173,9 +174,9 @@
173 174 "garmentId": "d87033c9-1394-41c1-81b2-f3b78ef820f1",
174 175 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
175 176 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
176   - "status": "generating",
  177 + "status": "failed",
177 178 "h3JobId": "4d6a6b7cd9c3",
178   - "error": null,
  179 + "error": "用户手动取消",
179 180 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
180 181 },
181 182 {
... ... @@ -185,9 +186,9 @@
185 186 "garmentId": "707e8cb7-94ee-4d18-ad6c-196437de4a95",
186 187 "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
187 188 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
188   - "status": "generating",
  189 + "status": "failed",
189 190 "h3JobId": "6108079456e0",
190   - "error": null,
  191 + "error": "用户手动取消",
191 192 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
192 193 },
193 194 {
... ... @@ -197,9 +198,9 @@
197 198 "garmentId": "6b64ed0c-13aa-44f2-956b-535135b4b716",
198 199 "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
199 200 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
200   - "status": "generating",
  201 + "status": "failed",
201 202 "h3JobId": "3f16feeb77b4",
202   - "error": null,
  203 + "error": "用户手动取消",
203 204 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
204 205 },
205 206 {
... ... @@ -209,10 +210,11 @@
209 210 "garmentId": "be718f4f-5fbc-4e7c-b2b0-2f35b90ab844",
210 211 "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
211 212 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
212   - "status": "generating",
  213 + "status": "done",
213 214 "h3JobId": "20748be72660",
214 215 "error": null,
215   - "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
  216 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement.",
  217 + "generatedVideoId": "20748be72660"
216 218 },
217 219 {
218 220 "id": "203127c5-445f-4166-aac9-7bd39027aa24",
... ... @@ -221,9 +223,9 @@
221 223 "garmentId": "7bd749c2-5433-40ca-8252-f75230a93f9a",
222 224 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
223 225 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
224   - "status": "generating",
  226 + "status": "failed",
225 227 "h3JobId": "77fc8a752df6",
226   - "error": null,
  228 + "error": "用户手动取消",
227 229 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
228 230 },
229 231 {
... ... @@ -233,9 +235,9 @@
233 235 "garmentId": "47d0a350-9d21-49d8-9d8f-739380b06a8c",
234 236 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
235 237 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
236   - "status": "generating",
  238 + "status": "failed",
237 239 "h3JobId": "c197b9dc3496",
238   - "error": null,
  240 + "error": "用户手动取消",
239 241 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
240 242 },
241 243 {
... ... @@ -245,9 +247,9 @@
245 247 "garmentId": "cef049af-c9da-4842-8f7a-6f3e4991bf91",
246 248 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
247 249 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
248   - "status": "generating",
  250 + "status": "failed",
249 251 "h3JobId": "94ec7bf527a1",
250   - "error": null,
  252 + "error": "用户手动取消",
251 253 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
252 254 },
253 255 {
... ... @@ -257,9 +259,9 @@
257 259 "garmentId": "c0397572-5fd2-4905-8cf7-2b9f97851fc4",
258 260 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
259 261 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
260   - "status": "generating",
  262 + "status": "failed",
261 263 "h3JobId": "b889f56233cd",
262   - "error": null,
  264 + "error": "用户手动取消",
263 265 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
264 266 },
265 267 {
... ... @@ -269,9 +271,9 @@
269 271 "garmentId": "2015c633-29e3-4043-8315-ca29fd315d6d",
270 272 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
271 273 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
272   - "status": "generating",
  274 + "status": "failed",
273 275 "h3JobId": "7d96a42e1453",
274   - "error": null,
  276 + "error": "用户手动取消",
275 277 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
276 278 },
277 279 {
... ... @@ -281,9 +283,9 @@
281 283 "garmentId": "001055d3-fcf9-47c7-b619-be7ea91e65c7",
282 284 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
283 285 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
284   - "status": "generating",
  286 + "status": "failed",
285 287 "h3JobId": "c545aba0436c",
286   - "error": null,
  288 + "error": "用户手动取消",
287 289 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
288 290 },
289 291 {
... ... @@ -293,10 +295,11 @@
293 295 "garmentId": "650d9947-78b4-4abd-bf4a-a0c979c5357c",
294 296 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
295 297 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
296   - "status": "generating",
  298 + "status": "done",
297 299 "h3JobId": "056a7339da10",
298 300 "error": null,
299   - "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
  301 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement.",
  302 + "generatedVideoId": "056a7339da10"
300 303 },
301 304 {
302 305 "id": "3ec78ed1-e513-49e9-a049-51ebdd688a77",
... ... @@ -305,9 +308,9 @@
305 308 "garmentId": "f9c7bab2-be86-4f29-81bc-e81c1e2e6d58",
306 309 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
307 310 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
308   - "status": "generating",
  311 + "status": "failed",
309 312 "h3JobId": "5fbdacd89297",
310   - "error": null,
  313 + "error": "用户手动取消",
311 314 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
312 315 },
313 316 {
... ... @@ -317,9 +320,9 @@
317 320 "garmentId": "e0f3b73d-4e6b-4dcf-acae-1fef72f3d87e",
318 321 "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
319 322 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
320   - "status": "generating",
  323 + "status": "failed",
321 324 "h3JobId": "d01a648aed35",
322   - "error": null,
  325 + "error": "用户手动取消",
323 326 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
324 327 },
325 328 {
... ... @@ -329,9 +332,9 @@
329 332 "garmentId": "f3aa39fb-719c-4bf3-9e02-4db4cc182de5",
330 333 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
331 334 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
332   - "status": "generating",
  335 + "status": "failed",
333 336 "h3JobId": "1e23d35ca3ba",
334   - "error": null,
  337 + "error": "用户手动取消",
335 338 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
336 339 },
337 340 {
... ... @@ -341,9 +344,9 @@
341 344 "garmentId": "4e3515b0-610a-4ebe-8c7c-ef8a1ac0d71b",
342 345 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
343 346 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
344   - "status": "generating",
  347 + "status": "failed",
345 348 "h3JobId": "c81b28be540d",
346   - "error": null,
  349 + "error": "用户手动取消",
347 350 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
348 351 },
349 352 {
... ... @@ -353,9 +356,9 @@
353 356 "garmentId": "71d419a7-74d3-4b5c-980d-c7fbc2149e5d",
354 357 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
355 358 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
356   - "status": "generating",
  359 + "status": "failed",
357 360 "h3JobId": "1baeddc96f15",
358   - "error": null,
  361 + "error": "用户手动取消",
359 362 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
360 363 },
361 364 {
... ... @@ -365,9 +368,9 @@
365 368 "garmentId": "071c3876-a48a-40e9-a180-adb579bfafdc",
366 369 "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
367 370 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
368   - "status": "generating",
  371 + "status": "failed",
369 372 "h3JobId": "0b6eee0ddbb1",
370   - "error": null,
  373 + "error": "用户手动取消",
371 374 "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
372 375 },
373 376 {
... ... @@ -377,9 +380,42 @@
377 380 "garmentId": "680dc01a-159d-4247-8d6d-89c1669536e7",
378 381 "refDanceVideoId": "e5e56a4f-2318-4fec-91be-fd94cf5dcdfc",
379 382 "batchId": "2c01df77-d1fb-4c46-a6d9-5d1d63436f5b",
380   - "status": "generating",
  383 + "status": "done",
381 384 "h3JobId": "8be398082e49",
382 385 "error": null,
383   - "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
  386 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 年轻女性,长直黑发,鹅蛋脸,气质清冷. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 原地小幅度手势舞动,柔和克制的比心和摆手动作,节奏偏慢, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement.",
  387 + "generatedVideoId": "8be398082e49"
  388 + },
  389 + {
  390 + "id": "99738c70-3696-49ed-85b8-1380672309b0",
  391 + "createdAt": 1788360009192,
  392 + "characterId": "7f8d1cf1-3b98-4dc7-8c51-64967f3abc6b",
  393 + "garmentId": "7bd749c2-5433-40ca-8252-f75230a93f9a",
  394 + "refDanceVideoId": "7b8e13ce-c9e3-4a19-bccc-5f54e6aae98e",
  395 + "batchId": null,
  396 + "status": "failed",
  397 + "h3JobId": "bcfcb5054023",
  398 + "falRequestId": null,
  399 + "error": "missing character/garment/dance-ref record",
  400 + "backend": "h3_console",
  401 + "userId": "214e0b7f-2892-4d89-ad6e-62fb1db3f107",
  402 + "priority": "paid_boost",
  403 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 用户上传的人物形象. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 彩色旗袍变体,金色滚边,盘扣,侧开衩(占位素材,待替换), laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,双臂交替甩动,配合甩发动作,节奏感强的hip-hop风格, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
  404 + },
  405 + {
  406 + "id": "ed063d62-31c1-4917-bde3-b8c83d38d28b",
  407 + "createdAt": 1788360191860,
  408 + "characterId": "be0fc5e8-0ea4-417d-954b-05840a924502",
  409 + "garmentId": "2c4487a8-6ff7-402c-8e13-dfefcd8eda6f",
  410 + "refDanceVideoId": "f2c6304c-1155-4d82-afe9-17de85c9398a",
  411 + "batchId": null,
  412 + "status": "failed",
  413 + "h3JobId": "4a0dbbf26d7c",
  414 + "falRequestId": null,
  415 + "error": "missing character/garment/dance-ref record",
  416 + "backend": "h3_console",
  417 + "userId": "214e0b7f-2892-4d89-ad6e-62fb1db3f107",
  418 + "priority": "paid_boost",
  419 + "fullPrompt": "subject_definitions:\n<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: 用户上传的人物形象. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.\n<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.\n<Picture 2> is the garment reference image: 橙色旗袍,紫色滚边,盘扣,侧开衩, laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.\n<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.\n<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.\n\nsummary:\n[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing 宽马步,甩臂过顶,比心,hip-hop风格能量感 in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.\n\nretention_analysis:\n<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.\n<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.\n<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.\n<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.\n<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.\n\ndetailed_description:\nA vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts 宽马步,甩臂过顶,比心,hip-hop风格能量感, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.\n\noverall_soundscape:\nSoft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.\n\nnon_diegetic_music:\nAn upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement."
384 420 }
385 421 ]
386 422 \ No newline at end of file
... ...
server.js
... ... @@ -49,6 +49,323 @@ const FAL_REST_BASE = process.env.FAL_REST_BASE || &quot;https://rest.alpha.fal.ai&quot;;
49 49 const FAL_QUEUE_BASE = process.env.FAL_QUEUE_BASE || "https://queue.fal.run";
50 50 const FAL_MODEL_ID = "minimax/h3/reference-to-video";
51 51  
  52 +// Standalone "人衣图生成" admin tool (admin-dressgen.html) — person 3-views +
  53 +// garment -> composite portrait via gpt-image-2, hosted behind 万界方舟's
  54 +// maas-openapi relay. NOT the same pipeline as generateDanceVideoViaFal
  55 +// above (that's H3/fal video); this is a one-shot image edit call.
  56 +//
  57 +// Contract confirmed against a real curl example the user supplied for the
  58 +// sibling /images/generations endpoint on this same host, plus this
  59 +// environment's own OPTIONS/GET probing of /images/edits (2026-09-03):
  60 +// - host+path is maas-openapi.wanjiedata.com/api/v1/images/edits, NOT
  61 +// fangzhou.wanjiedata.com/v1/images/edits (that first guess 405'd —
  62 +// wrong host *and* wrong path; fangzhou is the docs/dashboard SPA, not
  63 +// the API gateway. GET on the real /images/edits path 405s with an
  64 +// explicit `Allow: POST` header from APISIX, confirming it's a real
  65 +// registered route, not the SPA's catch-all).
  66 +// - model id is the literal string "GPT Image 2" (with spaces) — the user
  67 +// was warned a typo here gets WAF-blocked with 410 or "model
  68 +// unauthorized", so don't "clean up" this string.
  69 +// - this host's TLS chain does not validate from this machine (confirmed:
  70 +// the user's own working curl needs -k) — generateDressImage() below
  71 +// sends the request via a raw https.request with rejectUnauthorized
  72 +// false scoped to just this one call, not a process-wide
  73 +// NODE_TLS_REJECT_UNAUTHORIZED=0 (that would silently weaken every
  74 +// other HTTPS call this server makes — Stripe, GLM, H3 Console, fal.ai).
  75 +// - the /generations sibling responds JSON {data:[{url}]} (a short-lived
  76 +// presigned URL, must be fetched immediately) and takes size/n as
  77 +// top-level fields, not response_format (explicitly unsupported — don't
  78 +// send it). /images/edits itself has NOT been call-tested (no example
  79 +// for it specifically was available, only /generations + this
  80 +// environment's unauthenticated OPTIONS/GET probing of the route's
  81 +// existence) — its multipart shape here (image[] repeated, prompt,
  82 +// model, size, n) mirrors OpenAI's own public /v1/images/edits
  83 +// contract, which the user confirmed this vendor keeps in lockstep
  84 +// with. First real authenticated call is still the actual proof.
  85 +// - a real edit call takes 60-180s; the timeout must allow for 300s+.
  86 +const IMAGE2_API_KEY = process.env.IMAGE2_API_KEY || "";
  87 +const IMAGE2_API_BASE = process.env.IMAGE2_API_BASE || "https://maas-openapi.wanjiedata.com";
  88 +const IMAGE2_EDIT_PATH = "/api/v1/images/edits";
  89 +const IMAGE2_MODEL = "GPT Image 2";
  90 +const IMAGE2_SIZE = "1024x1536"; // within the ~157万-pixel cap the user measured; anything larger is silently downscaled
  91 +const IMAGE2_TIMEOUT_MS = 300000;
  92 +const IMAGE2_MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp" };
  93 +
  94 +// Second dressgen provider (2026-09-03) — admin-picked per task
  95 +// (record.provider) via the UI, or picked automatically as a fallback (see
  96 +// below).
  97 +//
  98 +// UPDATE (2026-09-03, later same day): the paragraph above originally said
  99 +// this would never auto-fallback on an IMAGE2 failure, reasoning that it'd
  100 +// silently re-route content around whichever provider's own moderation
  101 +// rejected it first. Asked directly about that tradeoff — with the
  102 +// concrete case in hand (a real-person-photo + swimwear task whose IMAGE2
  103 +// 400 looked like a moderation rejection, not a technical fault) — the
  104 +// product call came back "any failure auto-switches", overriding that
  105 +// stance. See runDressGenViews' fallback branch below. Recorded here so
  106 +// the next session doesn't rediscover this as a bug.
  107 +//
  108 +// Contract confirmed 2026-09-03 via real calls (not guessed): upload each
  109 +// image with POST {base}/api/v3/media/upload/binary (multipart, field
  110 +// "file") -> {data.download_url}; submit with POST {base}/api/v3/{model}
  111 +// (JSON: {prompt, images:[url,...]}) -> {data.id, data.urls.get}; poll
  112 +// data.urls.get until status is "completed" (-> data.outputs[0] is the
  113 +// result URL) or "failed" (-> data.error). No TLS quirks here (unlike
  114 +// IMAGE2) — plain fetch works.
  115 +const WAVESPEED_API_KEY = process.env.WAVESPEED_API_KEY || "";
  116 +const WAVESPEED_API_BASE = process.env.WAVESPEED_API_BASE || "https://api.wavespeed.ai";
  117 +const WAVESPEED_MODEL = "bytedance/seedream-v4/edit";
  118 +const WAVESPEED_POLL_INTERVAL_MS = Number(process.env.WAVESPEED_POLL_INTERVAL_MS) || 3000;
  119 +const WAVESPEED_TIMEOUT_MS = 180000; // observed ~30s in practice; generous headroom over that
  120 +
  121 +const DRESSGEN_PROVIDERS = ["image2", "wavespeed"];
  122 +const DRESSGEN_DEFAULT_PROVIDER = "image2";
  123 +
  124 +function dressGenProviderKeyMissing(provider) {
  125 + if (provider === "wavespeed") return !WAVESPEED_API_KEY ? "WAVESPEED_API_KEY not configured" : null;
  126 + return !IMAGE2_API_KEY ? "IMAGE2_API_KEY not configured" : null;
  127 +}
  128 +
  129 +// Raw https.request wrapper (not fetch) so rejectUnauthorized:false can be
  130 +// scoped to exactly this one call — see the doc comment above. bodyBuffer
  131 +// may be omitted for a plain GET-style fetch (used for the presigned result
  132 +// URL, which may or may not share 万界's own broken cert chain — safer to
  133 +// go through the same relaxed path than to assume it doesn't). Dispatches
  134 +// on the URL's own protocol (not hardcoded to https) purely so tests can
  135 +// point IMAGE2_API_BASE at a plain-http local mock; the real vendor is
  136 +// always https.
  137 +function insecureHttpsRequest(urlString, { method = "GET", headers = {}, bodyBuffer } = {}) {
  138 + return new Promise((resolve, reject) => {
  139 + const url = new URL(urlString);
  140 + const transport = url.protocol === "http:" ? http : https;
  141 + const req = transport.request(
  142 + {
  143 + hostname: url.hostname,
  144 + port: url.port || (url.protocol === "http:" ? 80 : 443),
  145 + path: `${url.pathname}${url.search}`,
  146 + method,
  147 + rejectUnauthorized: false,
  148 + headers,
  149 + timeout: IMAGE2_TIMEOUT_MS,
  150 + },
  151 + (res) => {
  152 + const chunks = [];
  153 + res.on("data", (c) => chunks.push(c));
  154 + res.on("end", () => resolve({ status: res.statusCode, buffer: Buffer.concat(chunks) }));
  155 + }
  156 + );
  157 + req.on("timeout", () => req.destroy(new Error(`image2 request timed out after ${IMAGE2_TIMEOUT_MS}ms`)));
  158 + req.on("error", reject);
  159 + if (bodyBuffer) req.write(bodyBuffer);
  160 + req.end();
  161 + });
  162 +}
  163 +
  164 +// 人物参考图最多 6 张定妆照(全身/脸部 × 正/侧/背),但不强制全传——传几张
  165 +// 算几张(至少 1 张)。gpt-image-2 的 edit 端点没有"生成 3 张不同视角"的原生
  166 +// 能力(同一次调用的 n 个输出是同一编辑的独立采样,不是可寻址的不同机位,
  167 +// CODEX-IMAGEGEN.md §4.3 也确认 edit 没有 batch 子命令),所以三个目标视角
  168 +// 分三次独立调用;每次调用都把用户实际上传的全部人物参考图 + 新衣服图一起
  169 +// 喂进去(不按 slot 精确配对),靠 prompt 里的目标视角文字区分三次调用要出
  170 +// 哪个角度。三次调用按顺序(依次)发起,不并发。
  171 +const DRESSGEN_PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];
  172 +const DRESSGEN_VIEWS = ["front", "side", "back"];
  173 +const DRESSGEN_VIEW_LABEL = { front: "正面", side: "侧面", back: "背面" };
  174 +
  175 +function defaultDressgenPrompt(view) {
  176 + const label = DRESSGEN_VIEW_LABEL[view];
  177 + return (
  178 + "前面的图是同一位人物的参考图(可能包含全身正/侧/背、脸正/侧/背等不同角度和部位,具体张数以实际上传为准),最后一张图是新服装参考图。" +
  179 + `保持人物的脸部五官、发型、体型完全不变,仅将服装替换为新服装图所示服装,生成人物穿着新服装的${label}全身人物形象图。` +
  180 + "鞋子沿用人物原有款式,不要更换;背景干净,光线均匀,全身入镜。Size 1024x1536."
  181 + );
  182 +}
  183 +
  184 +// Runs (a subset of) DRESSGEN_VIEWS' edit calls in sequence against an
  185 +// already-created dressGenRecords row, writing progress back after every
  186 +// single view so admin-dressgen.html's poll (GET /api/admin/dressgen) can
  187 +// show "queued/generating/done/failed" per view while it's still running —
  188 +// not just a final result once the whole thing settles. Called
  189 +// fire-and-forget (`.catch(() => {})` at the call site) right after the
  190 +// HTTP response for POST /dressgen or POST /dressgen/:id/retry has already
  191 +// gone out; every failure mode here is recorded on the row itself, not
  192 +// thrown back to a caller that's no longer listening. `views` lets retry
  193 +// reprocess only the views that didn't succeed last time, without touching
  194 +// ones that already have an output.
  195 +async function runDressGenViews(recordId, imageAbsPaths, views) {
  196 + for (const view of views) {
  197 + const before = dressGenRecords.get(recordId);
  198 + if (!before) return; // row was deleted mid-flight
  199 + dressGenRecords.update(recordId, { viewStatus: { ...before.viewStatus, [view]: "generating" } });
  200 + const provider = before.provider || DRESSGEN_DEFAULT_PROVIDER;
  201 + let usedProvider = provider;
  202 + try {
  203 + let imageBuffer;
  204 + try {
  205 + imageBuffer = await generateDressImage({ imageAbsPaths, prompt: before.prompts[view], provider });
  206 + } catch (err) {
  207 + // Automatic fallback: default provider (image2) failed for any
  208 + // reason -> retry once via wavespeed before giving up on this view.
  209 + // See the note above generateDressImage's declaration for why.
  210 + if (provider === DRESSGEN_DEFAULT_PROVIDER && !dressGenProviderKeyMissing("wavespeed")) {
  211 + usedProvider = "wavespeed";
  212 + imageBuffer = await generateDressImage({ imageAbsPaths, prompt: before.prompts[view], provider: "wavespeed" });
  213 + } else {
  214 + throw err;
  215 + }
  216 + }
  217 + const { relPath } = placeInUploads(imageBuffer, "png");
  218 + const after = dressGenRecords.get(recordId);
  219 + if (!after) return;
  220 + dressGenRecords.update(recordId, {
  221 + outputImages: { ...after.outputImages, [view]: relPath },
  222 + errors: { ...after.errors, [view]: null },
  223 + viewStatus: { ...after.viewStatus, [view]: "done" },
  224 + viewProvider: { ...(after.viewProvider || {}), [view]: usedProvider },
  225 + });
  226 + } catch (err) {
  227 + const after = dressGenRecords.get(recordId);
  228 + if (!after) return;
  229 + dressGenRecords.update(recordId, {
  230 + errors: { ...after.errors, [view]: err.message },
  231 + viewStatus: { ...after.viewStatus, [view]: "failed" },
  232 + });
  233 + }
  234 + }
  235 + const final = dressGenRecords.get(recordId);
  236 + if (!final) return;
  237 + const succeeded = DRESSGEN_VIEWS.filter((v) => final.outputImages[v]).length;
  238 + const status = succeeded === DRESSGEN_VIEWS.length ? "done" : succeeded > 0 ? "partial" : "failed";
  239 + dressGenRecords.update(recordId, { status });
  240 +}
  241 +
  242 +// Startup recovery, same idea as resumeDressupGenerations() above: a
  243 +// record can be sitting in "generating" if the process died mid-view.
  244 +// Unlike generationJobs there's no in-flight request id to reconnect to
  245 +// (image2/wavespeed calls are plain awaited HTTP round-trips inside
  246 +// runDressGenViews, nothing survives the process exiting) — every
  247 +// "queued"/"generating" view is unrecoverable, so it's marked failed with
  248 +// a distinguishable error, same as a real generation failure would be, and
  249 +// is one click to retry from either list or detail page.
  250 +function resumeDressgenTasks() {
  251 + for (const record of dressGenRecords.list()) {
  252 + const staleViews = DRESSGEN_VIEWS.filter((v) => record.viewStatus[v] === "queued" || record.viewStatus[v] === "generating");
  253 + if (staleViews.length === 0) continue;
  254 + const viewStatus = { ...record.viewStatus };
  255 + const errors = { ...record.errors };
  256 + for (const v of staleViews) {
  257 + viewStatus[v] = "failed";
  258 + errors[v] = "进程重启,任务丢失,请点重试";
  259 + }
  260 + const succeeded = DRESSGEN_VIEWS.filter((v) => record.outputImages[v]).length;
  261 + const status = succeeded === DRESSGEN_VIEWS.length ? "done" : succeeded > 0 ? "partial" : "failed";
  262 + dressGenRecords.update(record.id, { viewStatus, errors, status });
  263 + }
  264 +}
  265 +
  266 +// imageAbsPaths for a dressGenRecords row, recomputed from its own stored
  267 +// fields — used both at creation time and by retry (which reuses the
  268 +// original upload, no re-selecting images).
  269 +function dressGenImageAbsPaths(record) {
  270 + const personPaths = DRESSGEN_PERSON_SLOTS.filter((f) => record[f]).map((f) => record[f]);
  271 + return [...personPaths, record.garmentImage].map((p) => path.join(__dirname, p));
  272 +}
  273 +
  274 +async function generateDressImage({ imageAbsPaths, prompt, provider }) {
  275 + return provider === "wavespeed" ? generateDressImageViaWavespeed({ imageAbsPaths, prompt }) : generateDressImageViaImage2({ imageAbsPaths, prompt });
  276 +}
  277 +
  278 +async function generateDressImageViaImage2({ imageAbsPaths, prompt }) {
  279 + if (!IMAGE2_API_KEY) throw new Error("IMAGE2_API_KEY not configured");
  280 + const form = new FormData();
  281 + form.set("model", IMAGE2_MODEL);
  282 + form.set("prompt", prompt);
  283 + form.set("n", "1");
  284 + form.set("size", IMAGE2_SIZE);
  285 + for (const absPath of imageAbsPaths) {
  286 + const ext = path.extname(absPath).slice(1).toLowerCase();
  287 + form.append("image[]", new Blob([fs.readFileSync(absPath)], { type: IMAGE2_MIME[ext] || "application/octet-stream" }), path.basename(absPath));
  288 + }
  289 + // Encode via the platform's own FormData/Response to get correct
  290 + // multipart boundary handling for free, then send over insecureHttpsRequest
  291 + // (not fetch) — see IMAGE2_API_KEY's doc comment for why.
  292 + const encoded = new Response(form);
  293 + const contentType = encoded.headers.get("content-type");
  294 + const bodyBuffer = Buffer.from(await encoded.arrayBuffer());
  295 +
  296 + const { status, buffer } = await insecureHttpsRequest(`${IMAGE2_API_BASE}${IMAGE2_EDIT_PATH}`, {
  297 + method: "POST",
  298 + headers: { Authorization: `Bearer ${IMAGE2_API_KEY}`, "Content-Type": contentType, "Content-Length": bodyBuffer.length },
  299 + bodyBuffer,
  300 + });
  301 + const text = buffer.toString("utf8");
  302 + if (status < 200 || status >= 300) throw new Error(`image2 edit failed: ${status} ${text.slice(0, 500)}`);
  303 + let json;
  304 + try {
  305 + json = JSON.parse(text);
  306 + } catch {
  307 + throw new Error(`image2 edit returned non-JSON response: ${text.slice(0, 500)}`);
  308 + }
  309 + const item = json.data && json.data[0];
  310 + if (!item) throw new Error("image2 edit response missing data[0]");
  311 + if (item.b64_json) return Buffer.from(item.b64_json, "base64");
  312 + if (item.url) {
  313 + const { status: imgStatus, buffer: imgBuffer } = await insecureHttpsRequest(item.url);
  314 + if (imgStatus < 200 || imgStatus >= 300) throw new Error(`failed to fetch generated image: ${imgStatus}`);
  315 + return imgBuffer;
  316 + }
  317 + throw new Error("image2 edit response missing b64_json/url");
  318 +}
  319 +
  320 +async function wavespeedUpload(absPath) {
  321 + const ext = path.extname(absPath).slice(1).toLowerCase();
  322 + const form = new FormData();
  323 + form.set("file", new Blob([fs.readFileSync(absPath)], { type: IMAGE2_MIME[ext] || "application/octet-stream" }), path.basename(absPath));
  324 + const res = await fetch(`${WAVESPEED_API_BASE}/api/v3/media/upload/binary`, {
  325 + method: "POST",
  326 + headers: { Authorization: `Bearer ${WAVESPEED_API_KEY}` },
  327 + body: form,
  328 + });
  329 + const json = await res.json();
  330 + if (!res.ok || !json.data || !json.data.download_url) throw new Error(`wavespeed upload failed: ${res.status} ${JSON.stringify(json).slice(0, 500)}`);
  331 + return json.data.download_url;
  332 +}
  333 +
  334 +async function generateDressImageViaWavespeed({ imageAbsPaths, prompt }) {
  335 + if (!WAVESPEED_API_KEY) throw new Error("WAVESPEED_API_KEY not configured");
  336 + const imageUrls = [];
  337 + for (const absPath of imageAbsPaths) imageUrls.push(await wavespeedUpload(absPath));
  338 +
  339 + const submitRes = await fetch(`${WAVESPEED_API_BASE}/api/v3/${WAVESPEED_MODEL}`, {
  340 + method: "POST",
  341 + headers: { Authorization: `Bearer ${WAVESPEED_API_KEY}`, "Content-Type": "application/json" },
  342 + body: JSON.stringify({ prompt, images: imageUrls }),
  343 + });
  344 + const submitJson = await submitRes.json();
  345 + if (!submitRes.ok || !submitJson.data || !submitJson.data.urls || !submitJson.data.urls.get) {
  346 + throw new Error(`wavespeed submit failed: ${submitRes.status} ${JSON.stringify(submitJson).slice(0, 500)}`);
  347 + }
  348 + const resultUrl = submitJson.data.urls.get;
  349 +
  350 + const deadline = Date.now() + WAVESPEED_TIMEOUT_MS;
  351 + while (Date.now() < deadline) {
  352 + await new Promise((r) => setTimeout(r, WAVESPEED_POLL_INTERVAL_MS));
  353 + const pollRes = await fetch(resultUrl, { headers: { Authorization: `Bearer ${WAVESPEED_API_KEY}` } });
  354 + const pollJson = await pollRes.json();
  355 + const status = pollJson.data && pollJson.data.status;
  356 + if (status === "completed") {
  357 + const outputUrl = pollJson.data.outputs && pollJson.data.outputs[0];
  358 + if (!outputUrl) throw new Error("wavespeed result completed but has no outputs[0]");
  359 + const imgRes = await fetch(outputUrl);
  360 + if (!imgRes.ok) throw new Error(`failed to fetch wavespeed output: ${imgRes.status}`);
  361 + return Buffer.from(await imgRes.arrayBuffer());
  362 + }
  363 + if (status === "failed") throw new Error(`wavespeed generation failed: ${pollJson.data.error || "unknown error"}`);
  364 + // otherwise still "created"/"processing" — keep polling
  365 + }
  366 + throw new Error(`wavespeed generation timed out after ${WAVESPEED_TIMEOUT_MS}ms`);
  367 +}
  368 +
52 369 const MIME = {
53 370 ".html": "text/html",
54 371 ".js": "text/javascript",
... ... @@ -71,10 +388,11 @@ const {
71 388 sniffVideoType,
72 389 stripMetadataAndSave,
73 390 saveVideoUpload,
  391 + placeInUploads,
74 392 forwardToH3Assets,
75 393 forwardToFalAssets,
76 394 } = require("./uploads.js");
77   -const { characters, garments, danceRefs, generationJobs, moderationLog, blacklist } = require("./dressup-store.js");
  395 +const { characters, garments, danceRefs, generationJobs, moderationLog, blacklist, dressGenRecords } = require("./dressup-store.js");
78 396 const { buildStage2Prompt } = require("./prompt-template.js");
79 397 const { users: userStore, sessions: userSessions, creditLedger } = require("./db.js");
80 398 const { pricing } = require("./pricing.js");
... ... @@ -1793,6 +2111,191 @@ http
1793 2111 if (garmentsCrud(pathname, req, res)) return;
1794 2112 if (danceRefsCrud(pathname, req, res)) return;
1795 2113  
  2114 + // Standalone "人衣图生成" admin tool (admin-dressgen.html +
  2115 + // admin-dressgen-task.html): up to 6 person reference photos
  2116 + // (full-body/face × front/side/back, however many are actually
  2117 + // uploaded) + 1 garment image (a) -> 3 new full-body images of that
  2118 + // person wearing the garment, one per view (c), generated by 3
  2119 + // sequential gpt-image-2 calls. This is a real background task, not a
  2120 + // blocking request: POST creates the row (status "queued") and returns
  2121 + // its id immediately, then runDressGenViews() runs fire-and-forget —
  2122 + // the 3 upstream calls take ~70s+ each (CODEX-IMAGEGEN.md), so blocking
  2123 + // the HTTP response on all 3 sequentially risked a front-end proxy
  2124 + // timing the request out (a real 502 was hit doing exactly this before
  2125 + // this was made async). The client polls GET /dressgen (list) or
  2126 + // GET /dressgen/:id (task detail) to watch it progress. See
  2127 + // generateDressImage()'s doc comment for the vendor contract this
  2128 + // assumes, and DRESSGEN_PERSON_SLOTS above for why one call gets every
  2129 + // uploaded person image rather than a fixed per-view pairing.
  2130 + if (pathname === "/api/admin/dressgen/prompts" && req.method === "GET") {
  2131 + const prompts = {};
  2132 + DRESSGEN_VIEWS.forEach((view) => (prompts[view] = defaultDressgenPrompt(view)));
  2133 + res.writeHead(200, { "Content-Type": "application/json" });
  2134 + res.end(JSON.stringify(prompts));
  2135 + return;
  2136 + }
  2137 + if (pathname === "/api/admin/dressgen" && req.method === "GET") {
  2138 + // Paginated (2026-09-03) — store.list()/dress-gen.json is a
  2139 + // full-file read (store.js's own doc comment), fine today at a
  2140 + // handful of records but not something to keep shipping whole to
  2141 + // the browser as it grows. Newest-first, same order the frontend
  2142 + // used to produce itself via .slice().reverse().
  2143 + const params = new URL(req.url, "http://x").searchParams;
  2144 + const page = Math.max(1, Number(params.get("page")) || 1);
  2145 + const pageSize = Math.min(100, Math.max(1, Number(params.get("pageSize")) || 20));
  2146 + const all = dressGenRecords.list().slice().reverse();
  2147 + const items = all.slice((page - 1) * pageSize, page * pageSize);
  2148 + res.writeHead(200, { "Content-Type": "application/json" });
  2149 + res.end(JSON.stringify({ items, total: all.length, page, pageSize }));
  2150 + return;
  2151 + }
  2152 + if (pathname === "/api/admin/dressgen" && req.method === "POST") {
  2153 + readJsonBody(req)
  2154 + .then((body) => {
  2155 + if (typeof body.garmentImage !== "string" || !body.garmentImage.startsWith("/uploads/")) {
  2156 + res.writeHead(400, { "Content-Type": "application/json" });
  2157 + res.end(JSON.stringify({ error: "garmentImage must be a /uploads/... path from POST /api/admin/upload" }));
  2158 + return;
  2159 + }
  2160 + const providedPersonSlots = DRESSGEN_PERSON_SLOTS.filter((f) => typeof body[f] === "string" && body[f]);
  2161 + const badSlot = providedPersonSlots.find((f) => !body[f].startsWith("/uploads/"));
  2162 + if (badSlot) {
  2163 + res.writeHead(400, { "Content-Type": "application/json" });
  2164 + res.end(JSON.stringify({ error: `${badSlot} must be a /uploads/... path from POST /api/admin/upload` }));
  2165 + return;
  2166 + }
  2167 + if (providedPersonSlots.length === 0) {
  2168 + res.writeHead(400, { "Content-Type": "application/json" });
  2169 + res.end(JSON.stringify({ error: "at least one person reference image is required (upload as many of the 6 as you have)" }));
  2170 + return;
  2171 + }
  2172 + const provider = DRESSGEN_PROVIDERS.includes(body.provider) ? body.provider : DRESSGEN_DEFAULT_PROVIDER;
  2173 + const missingKey = dressGenProviderKeyMissing(provider);
  2174 + if (missingKey) {
  2175 + res.writeHead(500, { "Content-Type": "application/json" });
  2176 + res.end(JSON.stringify({ error: missingKey }));
  2177 + return;
  2178 + }
  2179 +
  2180 + const prompts = {};
  2181 + for (const view of DRESSGEN_VIEWS) {
  2182 + const custom = body.prompts && typeof body.prompts[view] === "string" ? body.prompts[view].trim() : "";
  2183 + prompts[view] = custom || defaultDressgenPrompt(view);
  2184 + }
  2185 +
  2186 + const record = { garmentImage: body.garmentImage, prompts, provider };
  2187 + for (const slot of DRESSGEN_PERSON_SLOTS) record[slot] = body[slot] || null;
  2188 + record.status = "queued";
  2189 + record.viewStatus = { front: "queued", side: "queued", back: "queued" };
  2190 + record.outputImages = { front: null, side: null, back: null };
  2191 + record.errors = { front: null, side: null, back: null };
  2192 + record.viewProvider = { front: null, side: null, back: null };
  2193 +
  2194 + const entry = dressGenRecords.create(record);
  2195 + runDressGenViews(entry.id, dressGenImageAbsPaths(entry), DRESSGEN_VIEWS).catch(() => {});
  2196 + res.writeHead(200, { "Content-Type": "application/json" });
  2197 + res.end(JSON.stringify(entry));
  2198 + })
  2199 + .catch(() => {
  2200 + res.writeHead(400, { "Content-Type": "application/json" });
  2201 + res.end(JSON.stringify({ error: "invalid JSON body" }));
  2202 + });
  2203 + return;
  2204 + }
  2205 + // Retry: reuses the row's own stored images/prompts — no re-upload, no
  2206 + // re-typing a prompt (that was an explicit ask: a failed task should be
  2207 + // re-runnable with one click). Whole-task retry only reprocesses views
  2208 + // that don't already have an output (so a "partial" task doesn't redo
  2209 + // the view(s) that already succeeded); per-view retry
  2210 + // (POST .../:id/retry/:view) reprocesses exactly the one view clicked,
  2211 + // for admin-dressgen-task.html's "regenerate just this image" button.
  2212 + if (/^\/api\/admin\/dressgen\/[^/]+\/retry\/(front|side|back)$/.test(pathname) && req.method === "POST") {
  2213 + const [, id, view] = pathname.match(/^\/api\/admin\/dressgen\/([^/]+)\/retry\/(front|side|back)$/);
  2214 + const record = dressGenRecords.get(id);
  2215 + if (!record) {
  2216 + res.writeHead(404, { "Content-Type": "application/json" });
  2217 + res.end(JSON.stringify({ error: "not found" }));
  2218 + return;
  2219 + }
  2220 + readJsonBody(req)
  2221 + .catch(() => ({})) // an empty/absent body is fine — retry with the stored prompt/provider as-is
  2222 + .then((body) => {
  2223 + // admin-dressgen-task.html's per-image "edit prompt then
  2224 + // regenerate" flow: an optional prompt override for just this
  2225 + // view, persisted so the record keeps reflecting what was
  2226 + // actually sent. Same for an optional provider override — e.g.
  2227 + // switching a view that failed on image2 over to wavespeed
  2228 + // instead, an explicit admin choice per the note above
  2229 + // generateDressImage, not an automatic fallback.
  2230 + const promptOverride = typeof body.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : null;
  2231 + const provider = DRESSGEN_PROVIDERS.includes(body.provider) ? body.provider : record.provider || DRESSGEN_DEFAULT_PROVIDER;
  2232 + const missingKey = dressGenProviderKeyMissing(provider);
  2233 + if (missingKey) {
  2234 + res.writeHead(500, { "Content-Type": "application/json" });
  2235 + res.end(JSON.stringify({ error: missingKey }));
  2236 + return;
  2237 + }
  2238 + const updated = dressGenRecords.update(id, {
  2239 + status: "generating",
  2240 + provider,
  2241 + viewStatus: { ...record.viewStatus, [view]: "queued" },
  2242 + prompts: promptOverride ? { ...record.prompts, [view]: promptOverride } : record.prompts,
  2243 + });
  2244 + runDressGenViews(id, dressGenImageAbsPaths(updated), [view]).catch(() => {});
  2245 + res.writeHead(200, { "Content-Type": "application/json" });
  2246 + res.end(JSON.stringify(updated));
  2247 + });
  2248 + return;
  2249 + }
  2250 + if (/^\/api\/admin\/dressgen\/[^/]+\/retry$/.test(pathname) && req.method === "POST") {
  2251 + const id = pathname.slice("/api/admin/dressgen/".length, -"/retry".length);
  2252 + const record = dressGenRecords.get(id);
  2253 + if (!record) {
  2254 + res.writeHead(404, { "Content-Type": "application/json" });
  2255 + res.end(JSON.stringify({ error: "not found" }));
  2256 + return;
  2257 + }
  2258 + readJsonBody(req)
  2259 + .catch(() => ({})) // an empty/absent body is fine — retry with the stored provider as-is
  2260 + .then((body) => {
  2261 + // Optional provider override, same as per-view retry above — an
  2262 + // explicit admin choice (e.g. image2 failed, switch to
  2263 + // wavespeed), not an automatic fallback.
  2264 + const provider = DRESSGEN_PROVIDERS.includes(body.provider) ? body.provider : record.provider || DRESSGEN_DEFAULT_PROVIDER;
  2265 + const missingKey = dressGenProviderKeyMissing(provider);
  2266 + if (missingKey) {
  2267 + res.writeHead(500, { "Content-Type": "application/json" });
  2268 + res.end(JSON.stringify({ error: missingKey }));
  2269 + return;
  2270 + }
  2271 + const viewsToRetry = DRESSGEN_VIEWS.filter((v) => !record.outputImages[v]);
  2272 + if (viewsToRetry.length === 0) {
  2273 + res.writeHead(200, { "Content-Type": "application/json" });
  2274 + res.end(JSON.stringify(record)); // already fully done — nothing to retry
  2275 + return;
  2276 + }
  2277 + const viewStatus = { ...record.viewStatus };
  2278 + viewsToRetry.forEach((v) => (viewStatus[v] = "queued"));
  2279 + const updated = dressGenRecords.update(id, { status: "generating", provider, viewStatus });
  2280 + runDressGenViews(id, dressGenImageAbsPaths(updated), viewsToRetry).catch(() => {});
  2281 + res.writeHead(200, { "Content-Type": "application/json" });
  2282 + res.end(JSON.stringify(updated));
  2283 + });
  2284 + return;
  2285 + }
  2286 + if (pathname.startsWith("/api/admin/dressgen/") && req.method === "DELETE") {
  2287 + const removed = dressGenRecords.remove(pathname.slice("/api/admin/dressgen/".length));
  2288 + res.writeHead(removed ? 200 : 404, { "Content-Type": "application/json" });
  2289 + res.end(JSON.stringify({ deleted: removed }));
  2290 + return;
  2291 + }
  2292 + if (pathname.startsWith("/api/admin/dressgen/") && req.method === "GET") {
  2293 + const record = dressGenRecords.get(pathname.slice("/api/admin/dressgen/".length));
  2294 + res.writeHead(record ? 200 : 404, { "Content-Type": "application/json" });
  2295 + res.end(JSON.stringify(record || { error: "not found" }));
  2296 + return;
  2297 + }
  2298 +
1796 2299 // T30: manual balance correction, e.g. a support gesture ("sorry about
1797 2300 // the failed generation that didn't auto-refund, here's some credits")
1798 2301 // or clawing back an abused promo. Always attributable — see
... ... @@ -2922,6 +3425,7 @@ http
2922 3425  
2923 3426 resumeGenerations();
2924 3427 resumeDressupGenerations();
  3428 +resumeDressgenTasks();
2925 3429  
2926 3430 // Auto-sync the library from the H3 Console so content added there directly
2927 3431 // (not through our own /api/prompt) shows up without a manual `node ingest.js`.
... ...
uploads.js
... ... @@ -209,6 +209,7 @@ module.exports = {
209 209 sniffVideoType,
210 210 stripMetadataAndSave,
211 211 saveVideoUpload,
  212 + placeInUploads,
212 213 forwardToH3Assets,
213 214 forwardToFalAssets,
214 215 };
... ...