Эх сурвалжийг харах

Mark invalid boss assets in library

bang 2 долоо хоног өмнө
parent
commit
ab199dce07
2 өөрчлөгдсөн 59 нэмэгдсэн , 10 устгасан
  1. 38 1
      server.py
  2. 21 9
      web/index.html

+ 38 - 1
server.py

@@ -322,6 +322,38 @@ def _task_prompt(kind, item):
     return item.get("prompt") or item.get("template") or item.get("preset") or ""
 
 
+def _asset_quality_for_task(game, kind, item, asset):
+    if not asset:
+        return {"ok": False, "errors": [], "warnings": []}
+    if kind != "characters" or asset.get("type") != "spine_parts":
+        return {"ok": True, "errors": [], "warnings": []}
+
+    base = os.path.join(OUT_ROOT, game)
+    errors = []
+    preview = asset.get("preview")
+    if preview:
+        ppath = os.path.join(base, preview)
+        if os.path.isfile(ppath):
+            ok, reason, _ = asset_quality.boss_preview_quality(ppath)
+            if not ok:
+                errors.append(f"完整预览不可用:{reason}")
+        else:
+            errors.append("完整预览文件缺失")
+    else:
+        errors.append("完整预览缺失")
+
+    for part in asset.get("parts") or []:
+        pfile = part.get("file", "")
+        ppath = os.path.join(base, pfile)
+        if not pfile or not os.path.isfile(ppath):
+            errors.append(f"拆件缺失:{part.get('id', '')}")
+            continue
+        ok, reason, _ = asset_quality.boss_part_quality(part.get("id", ""), ppath)
+        if not ok:
+            errors.append(f"{part.get('id', '')}:{reason}")
+    return {"ok": not errors, "errors": errors, "warnings": []}
+
+
 def _build_tasks(lib):
     manifest = _manifest_from_library(lib)
     slot_config = manifest.get("slot_config", {})
@@ -336,6 +368,10 @@ def _build_tasks(lib):
         for item in manifest.get(kind, []):
             item_id = item.get("id")
             asset = existing[kind].get(item_id)
+            quality = _asset_quality_for_task(lib.get("game", ""), kind, item, asset)
+            status = "done" if asset else "missing"
+            if asset and not quality.get("ok", True):
+                status = "invalid"
             tasks[kind].append({
                 "kind": kind,
                 "id": item_id,
@@ -343,8 +379,9 @@ def _build_tasks(lib):
                 "chineseName": _zh_name(kind, item, slot_config),
                 "use": _task_use(kind, item),
                 "prompt": _task_prompt(kind, item),
-                "status": "done" if asset else "missing",
+                "status": status,
                 "asset": asset,
+                "quality": quality,
                 "assetType": item.get("type") or kind,
                 "animations": item.get("animations", []),
                 "transparent": item.get("transparent"),

+ 21 - 9
web/index.html

@@ -58,7 +58,10 @@
   .task-status{border-radius:20px;padding:2px 9px;font-size:11px;white-space:nowrap;border:1px solid var(--line)}
   .task-status.done{color:#96ffce;background:#10291f}
   .task-status.missing{color:#ffd0df;background:#3d1830}
+  .task-status.invalid{color:#ffd87a;background:#3c2810}
   .task-status.running{color:#7ee8ff;background:#112d3a}
+  .qa-list{background:#241426;border:1px solid #6f3c56;color:#ffd6e6;border-radius:8px;
+        padding:8px;font-size:11px;line-height:1.45}
   .task-prompt{max-height:94px;overflow:auto;background:#160f29;border:1px solid var(--line);
         border-radius:8px;padding:8px;color:#cfc4ec;font-size:11px;line-height:1.45;white-space:pre-wrap}
   .placeholder{width:82%;height:82%;border:1px dashed #6b5c92;border-radius:12px;display:flex;
@@ -69,6 +72,9 @@
   .stage.clickable{cursor:pointer}
   .stage img{max-height:88%;max-width:88%;transform-origin:50% 100%;will-change:transform;
         filter:drop-shadow(0 6px 10px rgba(0,0,0,.35))}
+  .stage.invalid-preview img{opacity:.46;filter:grayscale(.25) drop-shadow(0 6px 10px rgba(0,0,0,.35))}
+  .stage-badge{position:absolute;left:10px;top:10px;background:#3c2810;color:#ffd87a;
+        border:1px solid #80622d;border-radius:999px;padding:4px 9px;font-size:11px;font-weight:700}
   .stage canvas{position:absolute;inset:0;width:100%;height:100%}
   .name{font-weight:700;font-size:15px}
   .meta{color:var(--muted);font-size:12px;line-height:1.5}
@@ -380,30 +386,36 @@ function renderChars(v){
   list.forEach(t=>{
     const c=t.asset||{};
     const done=t.status==='done' && c.png;
+    const usable=(t.status==='done'||t.status==='invalid') && c.png;
     const running=ACTIVE_TASKS.has(taskKey('characters',t.id));
     const animMap=c.animations||{};
     const anims=Object.keys(animMap);
     const displayImg = c.preview || c.png;
     const isParts = c.type==='spine_parts' || t.assetType==='spine_parts';
     const partCount = (c.parts || []).length;
-    const card=document.createElement('div'); card.className='card '+(done?'':'missing')+(running?' running':'');
+    const qaErrors=(t.quality&&t.quality.errors)||[];
+    const statusClass=running?'running':(t.status==='invalid'?'invalid':(done?'done':'missing'));
+    const statusText=running?'生成中':(t.status==='invalid'?'需修复':(done?'已生成':'缺失'));
+    const card=document.createElement('div'); card.className='card '+(usable?'':'missing')+(running?' running':'');
+    const stageClass = `stage ${usable&&isParts?'clickable':''} ${t.status==='invalid'?'invalid-preview':''}`;
     card.innerHTML=`
-      <div class="stage ${done&&isParts?'clickable':''}">${done?`<img src="${assetUrl(displayImg)}" alt="${esc(t.id)}">`:`<div class="placeholder">${running?'生成中…':'待生成'}<br>${esc(t.chineseName)}</div>`}</div>
+      <div class="${stageClass}">${usable?`<img src="${assetUrl(displayImg)}" alt="${esc(t.id)}">${t.status==='invalid'?'<div class="stage-badge">预览未通过</div>':''}`:`<div class="placeholder">${running?'生成中…':'待生成'}<br>${esc(t.chineseName)}</div>`}</div>
       <div class="task-head"><div><div class="name">${esc(t.chineseName)}</div><div class="meta">${esc(t.englishName)}</div></div>
-        <span class="task-status ${done?'done':(running?'running':'missing')}">${done?'已生成':(running?'生成中':'缺失')}</span></div>
-      ${done&&anims.length?`<div class="row"><select>${anims.map(a=>`<option>${esc(a)}</option>`).join('')}</select></div>`:''}
+        <span class="task-status ${statusClass}">${statusText}</span></div>
+      ${usable&&!isParts&&anims.length?`<div class="row"><select>${anims.map(a=>`<option>${esc(a)}</option>`).join('')}</select></div>`:''}
       <div class="meta"><span class="pill">${esc(t.assetType||'spine')}</span> ${isParts?`<span class="pill">拆件 ${partCount}</span> `:''}${esc(t.use)}<br>
-        动作: ${esc((done?anims:t.animations||[]).join(', ')||'idle')}</div>
-      ${done&&isParts?'<button class="ghost parts-btn">查看拆件图</button>':''}
+        ${isParts?'Cocos 中播放真实骨骼动作;此处只做静态资源检查。':'动作: '+esc((done?anims:t.animations||[]).join(', ')||'idle')}</div>
+      ${qaErrors.length?`<div class="qa-list">${qaErrors.slice(0,3).map(x=>`<div>需修复:${esc(x)}</div>`).join('')}</div>`:''}
+      ${usable&&isParts?'<button class="ghost parts-btn">查看拆件图</button>':''}
       <div class="task-prompt">${esc(t.prompt||'')}</div>
-      <button class="ghost retry-btn" data-kind="characters" data-id="${esc(t.id)}" ${running?'disabled':''}>${running?'正在生成…':(done?'重新生成':'补生成 / 重试')}</button>`;
-    if(done&&anims.length){
+      <button class="ghost retry-btn" data-kind="characters" data-id="${esc(t.id)}" ${running?'disabled':''}>${running?'正在生成…':(usable?'重新生成':'补生成 / 重试')}</button>`;
+    if(usable&&!isParts&&anims.length){
       const img=card.querySelector('img'), selA=card.querySelector('select');
       const tgt={el:img, anim:animMap[anims[0]], start:performance.now()};
       animTargets.push(tgt);
       selA.onchange=()=>{ tgt.anim=animMap[selA.value]; tgt.start=performance.now(); };
     }
-    if(done&&isParts){
+    if(usable&&isParts){
       card.querySelector('.stage').onclick=()=>openPartsModal(c,t);
       card.querySelector('.parts-btn').onclick=()=>openPartsModal(c,t);
     }