Parcourir la source

Force per-part boss regeneration

bang il y a 2 semaines
Parent
commit
59a2db04bb
4 fichiers modifiés avec 54 ajouts et 7 suppressions
  1. 8 2
      pipeline.py
  2. 4 2
      server.py
  3. 2 2
      slot_workflow.py
  4. 40 1
      web/index.html

+ 8 - 2
pipeline.py

@@ -214,7 +214,12 @@ def run(manifest, out_root, creds=None, log=print, merge_existing=False):
                 except Exception as e:
                     raise RuntimeError(f"完整 Boss 预览图生成失败:{e}")
                 sheet_cfg = c.get("spriteSheet") or {}
-                use_sheet = c.get("partGeneration") == "sprite_sheet" or sheet_cfg.get("enabled")
+                # Boss rigging sheets are too easy for image models to violate:
+                # one bad grid alignment corrupts every sliced part. Generate
+                # boss parts independently for usable rigging assets.
+                use_sheet = False
+                if (c.get("partGeneration") == "sprite_sheet" or sheet_cfg.get("enabled")):
+                    log(f"ℹ️ [{cid}] 已禁用 Boss 拆件表切图,改为逐部件生成,避免串格/半截拆件")
                 if use_sheet:
                     cols = int(sheet_cfg.get("cols") or 4)
                     rows = int(sheet_cfg.get("rows") or 4)
@@ -239,7 +244,8 @@ def run(manifest, out_root, creds=None, log=print, merge_existing=False):
                             style,
                             transparent_prompt(
                                 f"only the {part_id} rigging part from the boss character, isolated single part, "
-                                "not a full character, no complete body, no other body parts, centered"
+                                "not a full character, no complete body, no other body parts, no sprite sheet, "
+                                "fill most of the image with this one clean part, centered"
                             )
                         ] if x)
                         log(f"🎨 [{cid}/{part_id}] 生成 Boss 拆件…")

+ 4 - 2
server.py

@@ -555,7 +555,8 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
             consistency,
             _transparent_prompt(
                 f"only the {part_id} rigging part from the boss character, isolated single part, "
-                "not a full character, no complete body, no other body parts, centered"
+                "not a full character, no complete body, no other body parts, no sprite sheet, "
+                "fill most of the image with this one clean part, centered"
             ),
         ] if x)
         correction = ""
@@ -697,7 +698,8 @@ def _run_retry_boss_parts_from_preview_job(job_id, game, boss_id, creds):
                 consistency,
                 _transparent_prompt(
                     f"only the {pid} rigging part from the current boss preview, isolated single part, "
-                    "not a full character, no complete body, no other body parts, centered"
+                    "not a full character, no complete body, no other body parts, no sprite sheet, "
+                    "fill most of the image with this one clean part, centered"
                 ),
             ] if x)
             correction = ""

+ 2 - 2
slot_workflow.py

@@ -234,8 +234,8 @@ def build_boss_character(slot_config):
         "id": boss_id,
         "type": "spine_parts",
         "role": "boss",
-        "partGeneration": "sprite_sheet",
-        "spriteSheet": {"enabled": True, "cols": 4, "rows": 4, "size": "1024x1024"},
+        "partGeneration": "per_part",
+        "spriteSheet": {"enabled": False, "cols": 4, "rows": 4, "size": "1024x1024"},
         "animations": ["idle", "watch", "charge", "coin_throw", "taunt", "stomp", "attack", "hurt", "explode"],
         "prompt": (
             f"{profile['prompt']}, same visual style as {slot_config['theme']['world']}, "

+ 40 - 1
web/index.html

@@ -259,7 +259,10 @@
   <div class="modal-panel">
     <div class="modal-head">
       <h3 id="partsTitle">拆件预览</h3>
-      <button class="ghost" id="partsClose">关闭</button>
+      <div class="row">
+        <button class="ghost" id="partsRebuildAll">按主图重生全部拆件</button>
+        <button class="ghost" id="partsClose">关闭</button>
+      </div>
     </div>
     <div class="parts-grid">
       <div class="parts-box" style="grid-column:1/-1">
@@ -295,6 +298,7 @@ function markTasksRunning(items, running=true){
 
 function openPartsModal(c, t){
   CURRENT_PARTS = {asset:c, task:t};
+  $('#partsRebuildAll').dataset.boss = c.id || t.id;
   $('#partsTitle').textContent = `${t.chineseName} · 拆件检查`;
   $('#partsAtlas').src = assetUrl(c.png);
   const parts = c.parts || [];
@@ -654,6 +658,41 @@ $('#view').addEventListener('click', async e=>{
 });
 $('#partsClose').onclick=()=>$('#partsModal').classList.remove('open');
 $('#partsModal').onclick=e=>{ if(e.target.id==='partsModal') $('#partsModal').classList.remove('open'); };
+$('#partsRebuildAll').onclick=async()=>{
+  const game=$('#gameSel').value;
+  const bossId=$('#partsRebuildAll').dataset.boss;
+  if(!game||game==='(暂无)'||!bossId){ opMsg('没有可重生的关主拆件',false); return; }
+  const item={kind:'characters',id:bossId};
+  $('#partsRebuildAll').disabled=true;
+  $('#partsRebuildAll').textContent='重生中…';
+  markTasksRunning([item], true);
+  render();
+  opMsg(`正在按主图重生全部拆件 ${bossId}…`);
+  const log=$('#log'); log.style.display='block'; log.textContent=`按主图重生全部拆件 ${bossId} 任务创建中…`;
+  try{
+    const r=await fetch('/api/retry-boss-parts-from-preview',{method:'POST',headers:{'Content-Type':'application/json'},
+      body:JSON.stringify({
+        game, bossId,
+        provider:$('#provider').value, api_key:$('#apiKey').value,
+        base_url:$('#baseUrl').value, model:$('#model').value, size:$('#size').value })});
+    const d=await r.json();
+    if(!d.ok || !d.jobId){
+      log.textContent='❌ '+(d.error||'按主图重生拆件失败');
+      markTasksRunning([item], false); render();
+      $('#partsRebuildAll').disabled=false;
+      $('#partsRebuildAll').textContent='按主图重生全部拆件';
+      return;
+    }
+    await pollJob(d.jobId, log, $('#partsRebuildAll'), [item]);
+    const row=tasksFor('characters').find(t=>t.id===bossId);
+    if(row && row.asset) openPartsModal(row.asset, row);
+  }catch(err){
+    log.textContent='请求失败: '+err;
+    markTasksRunning([item], false); render();
+    $('#partsRebuildAll').disabled=false;
+    $('#partsRebuildAll').textContent='按主图重生全部拆件';
+  }
+};
 $('#partsList').addEventListener('click', async e=>{
   const btn=e.target.closest('.part-retry-btn');
   if(!btn) return;