Commit 47cf8fbd153698cd513a9caf9c6214b5f1ee50c0

Authored by yibo
1 parent 83626817

h3-console: 上传素材保留原始文件名,预览缩略图下加文件名 caption

之前上传后只显示 uuid 文件名,传多个参考图/视频时分不清哪个是哪个。server.py 把原始
文件名塞进存盘文件名(<aid>__<原名>.ext),前端从 URL 反解出来显示在缩略图下方,旧任务
(改动前上传的,URL里没有__分段)会退化成显示uuid,不算错误只是没有原名可用。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
h3-console/server.py
... ... @@ -129,7 +129,10 @@ async def upload_asset(file: UploadFile = File(...)):
129 129 if len(body) > max_bytes:
130 130 raise HTTPException(413, f"文件超过 {max_bytes // 1024 // 1024}MB 上限")
131 131 aid = uuid.uuid4().hex[:16]
132   - path = ASSETS / f"{aid}{ext}"
  132 + # 原始文件名塞进存盘文件名里(aid__原名.ext),前端从 URL 就能反解出来显示——
  133 + # 不用另外给 job req 加字段存文件名,省了一次 schema 改动。
  134 + stem = re.sub(r"[^\w.-]+", "_", Path(file.filename or "").stem)[:60] or "file"
  135 + path = ASSETS / f"{aid}__{stem}{ext}"
133 136 path.write_bytes(body)
134 137 if kind == "image":
135 138 try:
... ... @@ -137,7 +140,7 @@ async def upload_asset(file: UploadFile = File(...)):
137 140 except Exception:
138 141 path.unlink(missing_ok=True)
139 142 raise HTTPException(400, "文件不是有效图片")
140   - return {"id": aid, "url": f"{_public_base()}/assets/{aid}{ext}", "kind": kind}
  143 + return {"id": aid, "url": f"{_public_base()}/assets/{aid}__{stem}{ext}", "kind": kind}
141 144  
142 145  
143 146 def _public_base() -> str:
... ...
h3-console/static/index.html
... ... @@ -48,14 +48,19 @@
48 48 button.submit { margin-top: 12px; padding: 10px 20px; background: #4a7; color: white; border: none; border-radius: 6px; cursor: pointer; }
49 49 button.submit:disabled { opacity: .5; cursor: default; }
50 50 .hint { font-size: 12px; opacity: .65; margin: 6px 0 4px; line-height: 1.5; }
51   - .thumbrow { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; align-items: flex-start; }
52   - .thumbrow img { height: 60px; border-radius: 4px; }
53   - .thumbrow video { height: 220px; max-width: 100%; border-radius: 4px; }
54   - .thumbrow audio { height: 32px; width: 220px; }
55   - .thumb-wrap { position: relative; display: inline-block; }
56   - .thumb-del { position: absolute; top: -6px; right: -6px; width: 20px; height: 20px; padding: 0;
57   - border-radius: 50%; border: none; background: #c33; color: #fff; font-size: 13px; line-height: 1;
58   - cursor: pointer; }
  51 + .thumbrow { display: flex; gap: 20px; flex-wrap: wrap; margin-top: 12px; align-items: flex-start; }
  52 + .thumbrow img { height: 160px; width: 160px; object-fit: cover; border-radius: 8px;
  53 + border: 1px solid rgba(128,128,128,.3); display: block; }
  54 + .thumbrow video { height: 200px; width: 340px; max-width: 100%; object-fit: cover;
  55 + border-radius: 8px; border: 1px solid rgba(128,128,128,.3); display: block; background: #000; }
  56 + .thumbrow audio { height: 40px; width: 340px; max-width: 100%; display: block; }
  57 + .thumb-wrap { display: inline-flex; flex-direction: column; gap: 6px; max-width: 340px; }
  58 + .thumb-media { position: relative; display: inline-block; line-height: 0; }
  59 + .thumb-name { font-size: 12px; opacity: .7; max-width: 100%; overflow: hidden;
  60 + text-overflow: ellipsis; white-space: nowrap; }
  61 + .thumb-del { position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; padding: 0;
  62 + border-radius: 50%; border: none; background: #c33; color: #fff; font-size: 15px; line-height: 1;
  63 + cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,.35); }
59 64 .thumb-del:hover { background: #a22; }
60 65 .status-done { color: #2a7; } .status-failed { color: #c33; } .status-running { color: #a70; } .status-queued { color: #777; } .status-cancelled { color: #999; }
61 66 .err { color: #c33; font-size: 13px; margin-top: 8px; white-space: pre-wrap; font-family: ui-monospace, Menlo, Consolas, monospace;
... ... @@ -331,15 +336,35 @@ async function uploadFile(file) {
331 336 return await r.json();
332 337 }
333 338  
334   -// 每个预览项都包一层 .thumb-wrap 挂一个 × 删除按钮——上传上去选错了/传多了,
335   -// 之前只能整个表单清空重来,2026-08-24 用户要求能单个删。
336   -function wrapWithDelete(mediaEl, onDelete) {
  339 +// server.py 把原始文件名塞进了存盘文件名里(<aid>__<原名>.ext),这里直接从 URL 反解出来
  340 +// 显示——不用单独维护一份 filename 状态,旧任务(改动前上传的,URL 里没有 __ 分段)会
  341 +// 退化成显示 uuid 文件名,不算错误,只是没有原名可用。
  342 +function filenameFromUrl(u) {
  343 + try {
  344 + const base = decodeURIComponent(u.split('/').pop().split('?')[0]);
  345 + const m = base.match(/^[0-9a-f]{16}__(.+)$/);
  346 + return m ? m[1] : base;
  347 + } catch (e) { return u; }
  348 +}
  349 +
  350 +// 每个预览项都包一层 .thumb-wrap,媒体本体再包一层 .thumb-media 挂 × 删除按钮,
  351 +// 下面接一行文件名 caption——上传上去选错了/传多了,之前只能整个表单清空重来,
  352 +// 2026-08-24 用户要求能单个删;文件名是 2026-08-24 补的,之前只有个位置标签看不出传的是哪个文件。
  353 +function wrapWithDelete(mediaEl, onDelete, caption) {
337 354 const wrap = document.createElement('span');
338 355 wrap.className = 'thumb-wrap';
  356 + const mediaWrap = document.createElement('span');
  357 + mediaWrap.className = 'thumb-media';
339 358 const del = document.createElement('button');
340 359 del.type = 'button'; del.className = 'thumb-del'; del.textContent = '×'; del.title = '删除';
341 360 del.onclick = onDelete;
342   - wrap.appendChild(mediaEl); wrap.appendChild(del);
  361 + mediaWrap.appendChild(mediaEl); mediaWrap.appendChild(del);
  362 + wrap.appendChild(mediaWrap);
  363 + if (caption) {
  364 + const cap = document.createElement('span');
  365 + cap.className = 'thumb-name'; cap.textContent = caption; cap.title = caption;
  366 + wrap.appendChild(cap);
  367 + }
343 368 return wrap;
344 369 }
345 370  
... ... @@ -353,7 +378,7 @@ function renderFl2vaThumbs() {
353 378 uploaded[key] = null;
354 379 document.getElementById(key).value = ''; // 清掉file input,不然重选同一个文件不会触发change
355 380 renderFl2vaThumbs();
356   - }));
  381 + }, `${label} · ${filenameFromUrl(u)}`));
357 382 });
358 383 }
359 384  
... ... @@ -371,10 +396,11 @@ function renderRefThumbs() {
371 396 const box = document.getElementById('ref2va-thumbs'); box.innerHTML = '';
372 397 uploaded['ref2va-refs'].forEach((u, i) => {
373 398 const img = document.createElement('img'); img.src = u;
  399 + const label = `参考图${i+1}`; img.title = label;
374 400 box.appendChild(wrapWithDelete(img, () => {
375 401 uploaded['ref2va-refs'].splice(i, 1);
376 402 renderRefThumbs();
377   - }));
  403 + }, `${label} · ${filenameFromUrl(u)}`));
378 404 });
379 405 }
380 406  
... ... @@ -397,10 +423,12 @@ function renderTags(elId, stateKey) {
397 423 uploaded[stateKey].forEach((u, i) => {
398 424 const el = document.createElement(isVideo ? 'video' : 'audio');
399 425 el.src = u; el.controls = true;
  426 + const label = (isVideo ? '参考视频' : '参考音频') + (i + 1);
  427 + el.title = label;
400 428 box.appendChild(wrapWithDelete(el, () => {
401 429 uploaded[stateKey].splice(i, 1);
402 430 renderTags(elId, stateKey);
403   - }));
  431 + }, `${label} · ${filenameFromUrl(u)}`));
404 432 });
405 433 }
406 434  
... ... @@ -799,10 +827,17 @@ async function refreshDetail() {
799 827 if (req.first_frame_url) imgs.push(['首帧', req.first_frame_url]);
800 828 if (req.last_frame_url) imgs.push(['尾帧', req.last_frame_url]);
801 829 (req.reference_image_urls || []).forEach((u, i) => imgs.push(['参考图'+(i+1), u]));
802   - // 图片点开看大图,参考视频/音频跟生成结果的视频一样用原生播放器直接预览,不再只显示文件名。
803   - const imgTags = imgs.map(([label, u]) => `<a href="${u}" target="_blank" title="${label},点击查看大图"><img src="${u}" title="${label}"></a>`).join('');
804   - const videoTags = (req.reference_video_urls || []).map((u, i) => `<video controls src="${u}" title="参考视频${i+1}"></video>`).join('');
805   - const audioTags = (req.reference_audio_urls || []).map((u, i) => `<audio controls src="${u}" title="参考音频${i+1}"></audio>`).join('');
  830 + // 图片点开看大图,参考视频/音频跟生成结果的视频一样用原生播放器直接预览;每个媒体下面
  831 + // 再带一行文件名(从上传时 server.py 塞进 URL 里的原名反解出来,见 filenameFromUrl)。
  832 + const thumbCap = (label, u) => `${label} · ${filenameFromUrl(u)}`.replace(/</g, '&lt;');
  833 + const viewThumb = (mediaHtml, label, u) =>
  834 + `<span class="thumb-wrap"><span class="thumb-media">${mediaHtml}</span><span class="thumb-name" title="${thumbCap(label,u)}">${thumbCap(label,u)}</span></span>`;
  835 + const imgTags = imgs.map(([label, u]) =>
  836 + viewThumb(`<a href="${u}" target="_blank" title="${label},点击查看大图"><img src="${u}" title="${label}"></a>`, label, u)).join('');
  837 + const videoTags = (req.reference_video_urls || []).map((u, i) =>
  838 + viewThumb(`<video controls src="${u}" title="参考视频${i+1}"></video>`, '参考视频'+(i+1), u)).join('');
  839 + const audioTags = (req.reference_audio_urls || []).map((u, i) =>
  840 + viewThumb(`<audio controls src="${u}" title="参考音频${i+1}"></audio>`, '参考音频'+(i+1), u)).join('');
806 841 if (imgTags || videoTags) refThumbs += `<div class="thumbrow">${imgTags}${videoTags}</div>`;
807 842 if (audioTags) refThumbs += `<div class="thumbrow">${audioTags}</div>`;
808 843  
... ...