Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate UI SFX via local TangoFluxService (see D:/UD/AI/XModel/TangoFlux_AI调用指南.md)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_MANIFEST = ROOT / "Doc/Design/Sound/UiSfx/manifest.json"
|
||||
|
||||
|
||||
def wait_ready(base: str, timeout: float = 1800) -> dict:
|
||||
deadline = time.time() + timeout
|
||||
last = {}
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
last = requests.get(f"{base}/api/status", timeout=30).json()
|
||||
if last.get("ready"):
|
||||
return last
|
||||
if not last.get("loaded") and not last.get("busy"):
|
||||
try:
|
||||
requests.post(f"{base}/api/warmup", timeout=10)
|
||||
except requests.Timeout:
|
||||
pass
|
||||
except requests.RequestException as exc:
|
||||
last = {"error": str(exc)}
|
||||
time.sleep(2)
|
||||
raise TimeoutError(f"TangoFlux not ready: {last}")
|
||||
|
||||
|
||||
def submit(base: str, item: dict, defaults: dict) -> dict:
|
||||
body = {
|
||||
"prompt": item["prompt"],
|
||||
"duration": float(item.get("duration", defaults.get("duration", 2))),
|
||||
"steps": int(item.get("steps", defaults.get("steps", 50))),
|
||||
"guidance_scale": float(item.get("guidance_scale", defaults.get("guidance_scale", 4.5))),
|
||||
"seed": int(item.get("seed", 0)),
|
||||
"sync": False,
|
||||
}
|
||||
while True:
|
||||
r = requests.post(f"{base}/api/generate", json=body, timeout=60)
|
||||
if r.status_code == 429:
|
||||
time.sleep(3)
|
||||
continue
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def wait_job(base: str, job_id: str, timeout: float = 900) -> dict:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
j = requests.get(f"{base}/api/jobs/{job_id}", timeout=30).json()
|
||||
status = j.get("status")
|
||||
if status == "succeeded":
|
||||
return j
|
||||
if status == "failed":
|
||||
raise RuntimeError(j.get("error") or f"job failed: {job_id}")
|
||||
time.sleep(0.8)
|
||||
raise TimeoutError(job_id)
|
||||
|
||||
|
||||
def download(base: str, job: dict, out_path: Path) -> Path:
|
||||
url = job.get("url") or job.get("download_url") or f"/api/jobs/{job['id']}/audio"
|
||||
if url.startswith("/"):
|
||||
url = base + url
|
||||
audio = requests.get(url, timeout=120)
|
||||
audio.raise_for_status()
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(audio.content)
|
||||
return out_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
|
||||
ap.add_argument("--base", default="")
|
||||
ap.add_argument("--force", action="store_true", help="Regenerate even if WAV exists")
|
||||
ap.add_argument("--max-inflight", type=int, default=3)
|
||||
args = ap.parse_args()
|
||||
|
||||
manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
|
||||
base = (args.base or manifest.get("service") or "http://127.0.0.1:8012").rstrip("/")
|
||||
out_dir = ROOT / manifest["outputDir"]
|
||||
design_dir = ROOT / manifest["designDir"]
|
||||
bundle_dir = ROOT / manifest["bundleDir"]
|
||||
defaults = manifest.get("defaults") or {}
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
design_dir.mkdir(parents=True, exist_ok=True)
|
||||
bundle_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
status = wait_ready(base)
|
||||
print(json.dumps({"ready": True, "status": status}, ensure_ascii=False))
|
||||
|
||||
results = []
|
||||
pending: list[tuple[dict, dict]] = []
|
||||
|
||||
def flush_one() -> None:
|
||||
item, job = pending.pop(0)
|
||||
done = wait_job(base, job["id"])
|
||||
wav = out_dir / f"{item['id']}.wav"
|
||||
download(base, done, wav)
|
||||
dest = bundle_dir / f"{item['id']}.wav"
|
||||
shutil.copy2(wav, dest)
|
||||
meta = {
|
||||
"id": item["id"],
|
||||
"jobId": done["id"],
|
||||
"prompt": item["prompt"],
|
||||
"duration": item.get("duration", defaults.get("duration")),
|
||||
"steps": item.get("steps", defaults.get("steps")),
|
||||
"guidance_scale": item.get("guidance_scale", defaults.get("guidance_scale")),
|
||||
"seed": item.get("seed", 0),
|
||||
"bytes": wav.stat().st_size,
|
||||
"outputWav": str(wav.relative_to(ROOT)).replace("\\", "/"),
|
||||
"bundleWav": str(dest.relative_to(ROOT)).replace("\\", "/"),
|
||||
"status": done.get("status"),
|
||||
}
|
||||
(out_dir / f"{item['id']}.json").write_text(
|
||||
json.dumps({**meta, "serverJob": done}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
results.append(meta)
|
||||
print(json.dumps({"done": item["id"], "bytes": meta["bytes"]}, ensure_ascii=False))
|
||||
|
||||
for item in manifest["items"]:
|
||||
wav = out_dir / f"{item['id']}.wav"
|
||||
if wav.exists() and not args.force:
|
||||
dest = bundle_dir / f"{item['id']}.wav"
|
||||
if not dest.exists() or args.force:
|
||||
shutil.copy2(wav, dest)
|
||||
results.append({
|
||||
"id": item["id"],
|
||||
"skipped": True,
|
||||
"outputWav": str(wav.relative_to(ROOT)).replace("\\", "/"),
|
||||
"bundleWav": str(dest.relative_to(ROOT)).replace("\\", "/"),
|
||||
"bytes": wav.stat().st_size,
|
||||
})
|
||||
print(json.dumps({"skip": item["id"]}, ensure_ascii=False))
|
||||
continue
|
||||
|
||||
while len(pending) >= args.max_inflight:
|
||||
flush_one()
|
||||
job = submit(base, item, defaults)
|
||||
pending.append((item, job))
|
||||
print(json.dumps({"queued": item["id"], "jobId": job["id"]}, ensure_ascii=False))
|
||||
|
||||
while pending:
|
||||
flush_one()
|
||||
|
||||
report = {
|
||||
"batchId": manifest.get("batchId"),
|
||||
"base": base,
|
||||
"generatedUtc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"count": len(results),
|
||||
"items": results,
|
||||
}
|
||||
(out_dir / "batch_report.json").write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
(design_dir / "generation.json").write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(json.dumps({"batchComplete": True, "count": len(results)}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user