- 共享引擎(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 模板
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
"""Convert a Stable Audio 3 *-base torch checkpoint → the MLX training npz.
|
|
|
|
Torch-free (safetensors.numpy). Produces `dit_<model>-base_f16.npz` — the DiT
|
|
weights + baked conditioner that `lora_train_mlx.py --dit-weights` expects.
|
|
LoRA training uses the BASE checkpoint (not the shipped ARC weights inference
|
|
uses); these npz are auto-downloaded by weights.py, or reproduced here from the
|
|
HF `stabilityai/stable-audio-3-*-base` repos.
|
|
|
|
python scripts/export_base_npz.py \
|
|
--base-dir <sa3-*-base> # dir with model.safetensors + model_config.json
|
|
--output models/mlx/dit_<model>-base_f16.npz
|
|
[--validate-against <existing npz>] # assert bit-parity with a known-good npz
|
|
|
|
Conversion (identical to the shipped inference-npz layout): strip the DiT
|
|
wrapper prefix `model.model.`, transpose the two Conv1d weights
|
|
[out,in,k]→[out,k,in], rename RMSNorm `.gamma`→`.weight`, remap
|
|
`.to_local_embed.{0,2}`→`.seq.{0,2}`, and fold the 3 conditioner tensors under
|
|
the `cond.` prefix. Same for small (sm-music/sm-sfx) and medium.
|
|
"""
|
|
import argparse
|
|
|
|
import mlx.core as mx
|
|
import numpy as np
|
|
from safetensors.numpy import load_file
|
|
|
|
_DIT_PREFIX = "model.model."
|
|
_CONV_WEIGHTS = ("preprocess_conv.weight", "postprocess_conv.weight")
|
|
_COND_MAP = {
|
|
"conditioner.conditioners.prompt.padding_embedding": "cond.padding_embedding",
|
|
"conditioner.conditioners.seconds_total.embedder.embedding.1.weight": "cond.seconds_total_weight",
|
|
"conditioner.conditioners.seconds_total.embedder.embedding.1.bias": "cond.seconds_total_bias",
|
|
}
|
|
|
|
|
|
def convert(base_dir):
|
|
sd = load_file(f"{base_dir}/model.safetensors")
|
|
out = {}
|
|
for k, arr in sd.items():
|
|
if k in _COND_MAP:
|
|
out[_COND_MAP[k]] = mx.array(arr.astype(np.float32))
|
|
continue
|
|
if not k.startswith(_DIT_PREFIX):
|
|
continue # pretransform / T5 backbone — not part of the DiT npz
|
|
sk = k[len(_DIT_PREFIX):]
|
|
a = arr.astype(np.float32)
|
|
if sk in _CONV_WEIGHTS: # Conv1d [out,in,k] -> MLX [out,k,in]
|
|
out[sk] = mx.array(a.transpose(0, 2, 1))
|
|
elif sk.endswith(".gamma"): # RMSNorm .gamma -> .weight
|
|
out[sk[: -len(".gamma")] + ".weight"] = mx.array(a)
|
|
else:
|
|
sk = sk.replace(".to_local_embed.0.", ".to_local_embed.seq.0.")
|
|
sk = sk.replace(".to_local_embed.2.", ".to_local_embed.seq.2.")
|
|
out[sk] = mx.array(a)
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--base-dir", required=True)
|
|
ap.add_argument("--output", required=True)
|
|
ap.add_argument("--validate-against", default=None,
|
|
help="Assert key-set + value parity with a known-good npz.")
|
|
args = ap.parse_args()
|
|
|
|
wd = convert(args.base_dir)
|
|
print(f"converted {len(wd)} keys ({sum(1 for k in wd if k.startswith('cond'))} conditioner)")
|
|
|
|
if args.validate_against:
|
|
ref = dict(mx.load(args.validate_against))
|
|
extra, missing = set(wd) - set(ref), set(ref) - set(wd)
|
|
assert not extra and not missing, f"key mismatch: extra={extra}, missing={missing}"
|
|
worst = max(
|
|
float(mx.max(mx.abs(wd[k].astype(mx.float32) - ref[k].astype(mx.float32))))
|
|
for k in ref
|
|
)
|
|
print(f"validate: {len(ref)} keys match, max|Δ| = {worst:.2e}")
|
|
# Tolerance is fp16-scale: the source ckpt is fp32 and both npz are fp16,
|
|
# so a handful of the specially-transformed keys (conv transpose, the
|
|
# to_local_embed rename) can differ by ~1 fp16 ULP (~4e-3) depending on
|
|
# the rounding path. A real structural bug (wrong transpose/rename) gives
|
|
# an O(0.1-1) diff or a shape/key mismatch — both caught well below this.
|
|
assert worst < 1e-2, "values diverge from the reference npz (structural bug)"
|
|
|
|
mx.savez(args.output, **{k: v.astype(mx.float16) for k, v in wd.items()})
|
|
print(f"wrote {args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|