Files
face_sdk/python/server/static/index.html
T
2026-05-05 22:37:49 +08:00

311 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#0f1115" />
<title>Face SDK Web · 测试</title>
<style>
:root {
--bg: #0f1115;
--card: #1a1d24;
--border: #2a2f3a;
--text: #e6e8eb;
--muted: #8a92a3;
--accent: #4f8cff;
--accent-hover: #3d78e8;
--error: #ff6b6b;
--ok: #4fd18b;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { overflow-x: hidden; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
padding: 24px;
padding-left: max(24px, env(safe-area-inset-left));
padding-right: max(24px, env(safe-area-inset-right));
padding-bottom: max(24px, env(safe-area-inset-bottom));
min-height: 100vh;
min-height: 100dvh;
-webkit-text-size-adjust: 100%;
}
h1 { margin: 0 0 8px; font-size: 22px; font-weight: 600; }
.subtitle { color: var(--muted); margin-bottom: 24px; font-size: 13px; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.row { display: flex; gap: 16px; flex-wrap: wrap; align-items: end; }
.field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 200px; min-width: 0; }
label { font-size: 12px; color: var(--muted); }
/* font-size: 16px 防止 iOS Safari 聚焦时自动放大页面 */
input[type="file"], select {
background: #0f1115;
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
font-size: 16px;
width: 100%;
min-height: 44px;
-webkit-appearance: none;
appearance: none;
}
select {
/* 自定义下拉箭头(去掉默认样式后补回来) */
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%238a92a3' d='M6 8L0 0h12z'/></svg>");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
button {
background: var(--accent);
color: white;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
min-height: 44px;
touch-action: manipulation;
}
button:hover:not(:disabled) { background: var(--accent-hover); }
button:active:not(:disabled) { transform: scale(0.98); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.status { margin-top: 12px; font-size: 13px; min-height: 18px; word-break: break-word; }
.status.error { color: var(--error); }
.status.ok { color: var(--ok); }
.preview {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.preview-cell {
background: #0a0c10;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
min-height: 200px;
display: flex;
flex-direction: column;
}
.preview-cell h3 {
margin: 0 0 10px;
font-size: 13px;
font-weight: 500;
color: var(--muted);
}
.preview-cell img {
max-width: 100%;
max-height: 70vh;
border-radius: 4px;
object-fit: contain;
align-self: center;
}
.placeholder {
color: var(--muted);
font-size: 13px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
}
.download {
margin-top: 10px;
font-size: 13px;
}
.download a {
color: var(--accent);
text-decoration: none;
display: inline-block;
padding: 6px 0;
}
.download a:hover { text-decoration: underline; }
/* 平板 / 小桌面 */
@media (max-width: 720px) {
body { padding: 16px; padding-left: max(16px, env(safe-area-inset-left)); padding-right: max(16px, env(safe-area-inset-right)); }
h1 { font-size: 20px; }
.subtitle { margin-bottom: 16px; }
.card { padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.row { gap: 12px; }
.field { flex: 1 1 100%; }
/* 移动端按钮整行,方便单手点 */
#submit-btn { width: 100%; flex: 1 1 100%; padding: 14px 20px; font-size: 16px; }
.preview { grid-template-columns: 1fr; gap: 12px; }
.preview-cell { padding: 10px; min-height: 160px; }
.preview-cell img { max-height: 60vh; max-height: 60dvh; }
}
/* 窄屏手机(375px 及以下) */
@media (max-width: 380px) {
body { padding: 12px; }
h1 { font-size: 18px; }
.card { padding: 14px; }
}
</style>
</head>
<body>
<h1>Face SDK Web · 测试页</h1>
<div class="subtitle">上传一张人脸图片,选择效果 ID,查看叠加渲染结果。</div>
<div class="card">
<div class="row">
<div class="field">
<label for="image-input">图片</label>
<input type="file" id="image-input" accept="image/*" />
</div>
<div class="field">
<label for="effect-select">效果 ID</label>
<select id="effect-select">
<option value="">加载中...</option>
</select>
</div>
<button id="submit-btn">渲染</button>
</div>
<div id="status" class="status"></div>
</div>
<div class="preview">
<div class="preview-cell">
<h3>原图</h3>
<div id="orig-wrap" class="placeholder">未选择图片</div>
</div>
<div class="preview-cell">
<h3>渲染结果</h3>
<div id="result-wrap" class="placeholder">等待渲染</div>
</div>
</div>
<script>
(() => {
const $ = (id) => document.getElementById(id);
const imageInput = $("image-input");
const effectSelect = $("effect-select");
const submitBtn = $("submit-btn");
const statusEl = $("status");
const origWrap = $("orig-wrap");
const resultWrap = $("result-wrap");
let lastResultUrl = null;
function setStatus(msg, kind) {
statusEl.textContent = msg || "";
statusEl.className = "status" + (kind ? " " + kind : "");
}
function showOriginal(file) {
origWrap.className = "";
origWrap.innerHTML = "";
const img = document.createElement("img");
img.src = URL.createObjectURL(file);
origWrap.appendChild(img);
}
function showResult(blob) {
if (lastResultUrl) URL.revokeObjectURL(lastResultUrl);
lastResultUrl = URL.createObjectURL(blob);
resultWrap.className = "";
resultWrap.innerHTML = "";
const img = document.createElement("img");
img.src = lastResultUrl;
resultWrap.appendChild(img);
const dl = document.createElement("div");
dl.className = "download";
const a = document.createElement("a");
a.href = lastResultUrl;
a.download = "result.png";
a.textContent = "下载 PNG";
dl.appendChild(a);
resultWrap.appendChild(dl);
}
async function loadEffects() {
try {
const res = await fetch("/effects");
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
const ids = data.effects || [];
effectSelect.innerHTML = "";
if (ids.length === 0) {
effectSelect.innerHTML = '<option value="">无可用效果</option>';
setStatus("server/effects/ 下没有 PNG 文件", "error");
return;
}
for (const id of ids) {
const opt = document.createElement("option");
opt.value = String(id);
opt.textContent = String(id);
effectSelect.appendChild(opt);
}
} catch (e) {
effectSelect.innerHTML = '<option value="">加载失败</option>';
setStatus("加载效果列表失败:" + e.message, "error");
}
}
imageInput.addEventListener("change", () => {
const f = imageInput.files && imageInput.files[0];
if (f) {
showOriginal(f);
setStatus("");
}
});
submitBtn.addEventListener("click", async () => {
const file = imageInput.files && imageInput.files[0];
const effectId = effectSelect.value;
if (!file) { setStatus("请先选择图片", "error"); return; }
if (!effectId) { setStatus("请选择效果 ID", "error"); return; }
submitBtn.disabled = true;
setStatus("渲染中...");
const t0 = performance.now();
try {
const fd = new FormData();
fd.append("image", file);
fd.append("effect_id", effectId);
const res = await fetch("/render", { method: "POST", body: fd });
if (!res.ok) {
let msg = "HTTP " + res.status;
try {
const j = await res.json();
if (j.error === "no_face") msg = "未检测到人脸";
else if (j.detail) msg = j.detail;
else if (j.error) msg = j.error;
} catch (_) { /* 非 JSON 响应 */ }
throw new Error(msg);
}
const blob = await res.blob();
showResult(blob);
const dt = ((performance.now() - t0) / 1000).toFixed(2);
setStatus(`完成(${dt}s`, "ok");
} catch (e) {
setStatus("失败:" + e.message, "error");
} finally {
submitBtn.disabled = false;
}
});
loadEffects();
})();
</script>
</body>
</html>