admin-dressgen.html
7.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>新建人衣图任务</title>
<link rel="stylesheet" href="style.css" />
<style>
.admin-wrap { padding: 18px 24px; max-width: 1400px; }
.upload-section-label { font-weight: 600; margin: 14px 0 6px; font-size: 13px; }
.upload-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 800px; }
.upload-grid.garment { grid-template-columns: 1fr; max-width: 260px; }
.upload-slot { border: 1px dashed var(--border); border-radius: 6px; padding: 8px; text-align: center; }
.upload-slot label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 12px; }
.upload-slot img { width: 100%; height: 140px; object-fit: cover; border-radius: 4px; margin-top: 6px; display: none; }
.hint { color: var(--muted); font-size: 12px; }
.prompt-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-top: 8px; }
.prompt-grid .field label { font-weight: 600; font-size: 12px; }
.prompt-grid textarea { min-height: 130px; font-size: 12px; }
</style>
</head>
<body>
<header class="topbar">
<h1>新建人衣图任务</h1>
<div class="spacer"></div>
<a href="admin-dressgenlist.html">人衣图任务列表</a>
<a href="admin.html">视频库</a>
<a href="admin-characters.html">人物库</a>
<a href="admin-garments.html">衣服库</a>
<a href="admin-batch-gen.html">批量生成</a>
<a href="admin-dance-refs.html">参考跳舞视频库</a>
<a href="admin-dance-library.html">跳舞直播间片库</a>
<a href="admin-dance-qc.html">质检</a>
<a href="live.html">← 返回直播频道</a>
</header>
<main class="admin-wrap">
<div class="card">
<div class="field"><label>人物定妆照(有几张传几张,至少 1 张)+ 新衣服 → 依次生成穿新衣服的全身正/侧/背 3 张图</label></div>
<div class="upload-section-label">人物 · 全身(可选)</div>
<div class="upload-grid">
<div class="upload-slot"><label>全身正</label><input type="file" data-slot="bodyFront" accept="image/*" /><img data-preview="bodyFront" /></div>
<div class="upload-slot"><label>全身侧</label><input type="file" data-slot="bodySide" accept="image/*" /><img data-preview="bodySide" /></div>
<div class="upload-slot"><label>全身背</label><input type="file" data-slot="bodyBack" accept="image/*" /><img data-preview="bodyBack" /></div>
</div>
<div class="upload-section-label">人物 · 脸部(可选)</div>
<div class="upload-grid">
<div class="upload-slot"><label>脸正</label><input type="file" data-slot="faceFront" accept="image/*" /><img data-preview="faceFront" /></div>
<div class="upload-slot"><label>脸侧</label><input type="file" data-slot="faceSide" accept="image/*" /><img data-preview="faceSide" /></div>
<div class="upload-slot"><label>脸背</label><input type="file" data-slot="faceBack" accept="image/*" /><img data-preview="faceBack" /></div>
</div>
<div class="hint">全身/脸部 6 张不用都传,传几张算几张,至少上传 1 张人物参考图。</div>
<div class="upload-section-label">新衣服(必传)</div>
<div class="upload-grid garment">
<div class="upload-slot"><label>衣服图</label><input type="file" data-slot="garmentImage" accept="image/*" /><img data-preview="garmentImage" /></div>
</div>
<div class="upload-section-label">生成 prompt(每个视角各一条,已填默认值,改不改都能直接提交)</div>
<div class="prompt-grid">
<div class="field"><label>正面 prompt</label><textarea id="promptFront"></textarea></div>
<div class="field"><label>侧面 prompt</label><textarea id="promptSide"></textarea></div>
<div class="field"><label>背面 prompt</label><textarea id="promptBack"></textarea></div>
</div>
<div class="field" style="margin-top:12px;max-width:320px;">
<label>生成供应商</label>
<select id="providerSelect">
<option value="image2">GPT Image 2(万界方舟,默认,60-180秒/张;失败自动改用 Seedream v4)</option>
<option value="wavespeed">Seedream v4(wavespeed.ai,约30秒/张,更便宜)</option>
</select>
</div>
<button id="submitBtn" style="margin-top:12px;">生成人衣图(提交后台任务)</button>
<span id="submitHint" class="hint"></span>
<div id="submitError" class="error-box" hidden></div>
</div>
</main>
<script>
const _fetch = window.fetch;
window.fetch = async (...args) => {
const res = await _fetch(...args);
if (res.status === 401 && String(args[0]).startsWith("/api/admin/") && !String(args[0]).includes("/login")) {
location.href = "/admin-login.html?next=" + encodeURIComponent(location.pathname);
}
return res;
};
const PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];
document.querySelectorAll("[data-slot]").forEach((input) => {
input.addEventListener("change", () => {
const preview = document.querySelector(`[data-preview="${input.dataset.slot}"]`);
const file = input.files[0];
if (!file) {
preview.style.display = "none";
return;
}
preview.src = URL.createObjectURL(file);
preview.style.display = "block";
});
});
async function loadDefaultPrompts() {
const res = await fetch("/api/admin/dressgen/prompts");
const prompts = await res.json();
document.getElementById("promptFront").value = prompts.front;
document.getElementById("promptSide").value = prompts.side;
document.getElementById("promptBack").value = prompts.back;
}
async function uploadImage(file) {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/admin/upload", { method: "POST", body: fd });
if (!res.ok) throw new Error((await res.json()).error || "上传失败");
return (await res.json()).url;
}
document.getElementById("submitBtn").addEventListener("click", async () => {
const errorBox = document.getElementById("submitError");
const submitBtn = document.getElementById("submitBtn");
const submitHint = document.getElementById("submitHint");
errorBox.hidden = true;
const garmentFile = document.querySelector('[data-slot="garmentImage"]').files[0];
if (!garmentFile) {
errorBox.hidden = false;
errorBox.textContent = "衣服图必传";
return;
}
const personFiles = PERSON_SLOTS.filter((slot) => document.querySelector(`[data-slot="${slot}"]`).files[0]);
if (personFiles.length === 0) {
errorBox.hidden = false;
errorBox.textContent = "至少上传 1 张人物参考图(全身/脸部任意角度)";
return;
}
submitBtn.disabled = true;
try {
submitHint.textContent = "上传中...";
const urls = { garmentImage: await uploadImage(garmentFile) };
for (const slot of personFiles) urls[slot] = await uploadImage(document.querySelector(`[data-slot="${slot}"]`).files[0]);
submitHint.textContent = "已提交后台任务...";
const res = await fetch("/api/admin/dressgen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...urls,
provider: document.getElementById("providerSelect").value,
prompts: {
front: document.getElementById("promptFront").value,
side: document.getElementById("promptSide").value,
back: document.getElementById("promptBack").value,
},
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "提交失败");
location.href = `admin-dressgendetail.html?id=${data.id}`;
} catch (e) {
errorBox.hidden = false;
errorBox.textContent = e.message;
} finally {
submitBtn.disabled = false;
}
});
loadDefaultPrompts();
</script>
</body>
</html>