Files
AIGame/Tools/generate_ui_sfx.py
T
udxandClaude Opus 5.5 89e0fa246f 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>
2026-10-21 12:23:55 +08:00

172 lines
6.1 KiB
Python

#!/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()