Files
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

123 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Mureka instrumental generator: submit -> poll -> download.
Usage:
python3 tools/mureka_gen.py --prompt "..." --out /path/to/out_dir \
[--model mureka-7.6] [--task-id <existing id>] [--format flac|mp3]
Requires env: MUREKA_API_KEY, MUREKA_API_BASE (default https://api.mureka.ai).
With --task-id, skips submission and just polls that task. Downloads all
choices into <out>/<task_id>_<choice_index>.<format> and prints a JSON summary.
"""
import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
POLL_INTERVAL = 10
POLL_TIMEOUT = 15 * 60
def api(method, path, body=None):
base = os.environ.get("MUREKA_API_BASE", "https://api.mureka.ai").rstrip("/")
key = os.environ.get("MUREKA_API_KEY")
if not key:
sys.exit("MUREKA_API_KEY not set")
req = urllib.request.Request(
base + path,
method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode())
def submit(prompt, model):
resp = api("POST", "/v1/instrumental/generate", {"prompt": prompt, "model": model})
tid = resp.get("id")
if not tid:
sys.exit("submit failed, no id in response: %s" % resp)
return tid, resp
def query(tid):
try:
return api("GET", "/v1/instrumental/query/%s" % tid)
except urllib.error.HTTPError:
# fall back to the song endpoint
return api("GET", "/v1/song/query/%s" % tid)
def poll(tid):
deadline = time.time() + POLL_TIMEOUT
while True:
resp = query(tid)
status = resp.get("status")
print("poll %s: status=%s" % (tid, status), flush=True)
if status == "succeeded":
return resp
if status in ("failed", "cancelled"):
sys.exit("task %s ended with status=%s: %s" % (tid, status, resp))
if time.time() > deadline:
sys.exit("task %s timed out after %ds" % (tid, POLL_TIMEOUT))
time.sleep(POLL_INTERVAL)
def download(url, path):
with urllib.request.urlopen(url, timeout=120) as r, open(path, "wb") as f:
while True:
chunk = r.read(1 << 16)
if not chunk:
break
f.write(chunk)
return path
def main():
p = argparse.ArgumentParser()
p.add_argument("--prompt")
p.add_argument("--out", required=True, help="output directory")
p.add_argument("--model", default="mureka-7.6")
p.add_argument("--task-id", help="poll existing task instead of submitting")
p.add_argument("--format", choices=["flac", "mp3"], default="flac")
args = p.parse_args()
os.makedirs(args.out, exist_ok=True)
if args.task_id:
tid = args.task_id
else:
if not args.prompt:
sys.exit("--prompt required when submitting")
tid, resp = submit(args.prompt, args.model)
print("submitted task id=%s status=%s" % (tid, resp.get("status")), flush=True)
result = poll(tid)
summary = {"task_id": tid, "files": []}
for i, ch in enumerate(result.get("choices", [])):
url = ch.get("flac_url") if args.format == "flac" else ch.get("url")
if not url:
url = ch.get("url")
ext = "flac" if "flac" in (url or "") else "mp3"
path = os.path.join(args.out, "%s_%d.%s" % (tid, i, ext))
download(url, path)
summary["files"].append(
{"path": path, "duration_ms": ch.get("duration"), "choice_id": ch.get("id")}
)
print("downloaded %s (%sms)" % (path, ch.get("duration")), flush=True)
print("SUMMARY " + json.dumps(summary))
if __name__ == "__main__":
main()