Commit 27de8a81d3cf692dba21dbf7d01b908984c99128
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
Showing
15 changed files
with
1998 additions
and
42 deletions
admin-batch-gen.html
admin-characters.html
admin-dance-refs.html
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
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, "moderation-log.json")); |
| 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 || "https://rest.alpha.fal.ai"; |
| 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`. | ... | ... |