- 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>
172 lines
6.1 KiB
Python
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()
|