初始提交: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 模板
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
stable-audio — command-line interface for Stable Audio 3.
|
||||
|
||||
Basic usage::
|
||||
|
||||
stable-audio --model small-music -p "lo-fi hip hop beat, 90 BPM" --duration 30 -o beat.wav
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import torch
|
||||
import torchaudio
|
||||
|
||||
from stable_audio_3 import StableAudioModel
|
||||
|
||||
|
||||
def _save_output(audio: torch.Tensor, sample_rate: int, output: str, batch_size: int):
|
||||
"""Save generated audio tensor(s) to disk."""
|
||||
base, ext = os.path.splitext(output)
|
||||
if not ext:
|
||||
ext = ".wav"
|
||||
for i in range(batch_size):
|
||||
path = f"{base}_{i}{ext}" if batch_size > 1 else f"{base}{ext}"
|
||||
torchaudio.save(path, audio[i].cpu(), sample_rate)
|
||||
print(f"Saved: {path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="stable-audio",
|
||||
description="Stable Audio 3 — CLI for text-to-audio, audio-to-audio, and inpainting",
|
||||
)
|
||||
|
||||
# Model
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="medium",
|
||||
choices=[
|
||||
"medium",
|
||||
"small-music",
|
||||
"small-sfx",
|
||||
"medium-base",
|
||||
"small-music-base",
|
||||
"small-sfx-base",
|
||||
],
|
||||
help="Model to load (default: medium)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device",
|
||||
default=None,
|
||||
help="Device: cuda / mps / cpu (auto-detected if omitted)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-half", action="store_true", help="Disable half-precision (fp16) on CUDA"
|
||||
)
|
||||
|
||||
# Generation
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--prompt",
|
||||
required=True,
|
||||
nargs="+",
|
||||
help="Text prompt(s). Pass multiple for per-batch prompts",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--negative-prompt", nargs="+", default=None, help="Negative prompt(s)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--duration",
|
||||
type=float,
|
||||
nargs="+",
|
||||
default=[120.0],
|
||||
help="Duration in seconds (default: 120). Pass multiple for per-batch durations",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--steps", type=int, default=8, help="Diffusion steps (default: 8)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cfg-scale",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="CFG scale (default: 1.0; try 7.0 for base models)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed", type=int, default=-1, help="Random seed (-1 = random, default: -1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Batch size (default: inferred from number of prompts, or 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
default="output.wav",
|
||||
help="Output file path (default: output.wav)",
|
||||
)
|
||||
|
||||
# Audio-to-Audio
|
||||
parser.add_argument(
|
||||
"--init-audio",
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help="Source audio file for audio-to-audio generation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-noise-level",
|
||||
type=float,
|
||||
default=0.9,
|
||||
help="Noise level for audio-to-audio (0.0–1.0, default: 0.9)",
|
||||
)
|
||||
|
||||
# Inpainting / Continuation
|
||||
parser.add_argument(
|
||||
"--inpaint-audio",
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help="Source audio file for inpainting or continuation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inpaint-start",
|
||||
type=float,
|
||||
action="append",
|
||||
dest="inpaint_starts",
|
||||
metavar="SECONDS",
|
||||
help="Start of inpaint region in seconds. Repeat for multiple regions.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inpaint-end",
|
||||
type=float,
|
||||
action="append",
|
||||
dest="inpaint_ends",
|
||||
metavar="SECONDS",
|
||||
help="End of inpaint region in seconds. Repeat for multiple regions.",
|
||||
)
|
||||
|
||||
# Chunked decode
|
||||
decode_group = parser.add_mutually_exclusive_group()
|
||||
decode_group.add_argument(
|
||||
"--chunked-decode",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="Force chunked decoding on",
|
||||
)
|
||||
decode_group.add_argument(
|
||||
"--no-chunked-decode",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="Force chunked decoding off",
|
||||
)
|
||||
|
||||
# LoRA
|
||||
parser.add_argument(
|
||||
"--lora-ckpt-path",
|
||||
action="append",
|
||||
dest="loras",
|
||||
metavar="PATH",
|
||||
help="LoRA checkpoint path. Repeat to stack multiple LoRAs.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lora-strength",
|
||||
type=float,
|
||||
default=None,
|
||||
help="LoRA strength (applied to all LoRAs)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lora-index",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Target a specific LoRA index when setting strength",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Validate inpaint args ---
|
||||
if (args.inpaint_starts is None) != (args.inpaint_ends is None):
|
||||
parser.error("--inpaint-start and --inpaint-end must both be provided together")
|
||||
if args.inpaint_starts and len(args.inpaint_starts) != len(args.inpaint_ends):
|
||||
parser.error(
|
||||
"--inpaint-start and --inpaint-end must be specified the same number of times"
|
||||
)
|
||||
if args.inpaint_starts and not args.inpaint_audio:
|
||||
parser.error("--inpaint-start/--inpaint-end require --inpaint-audio")
|
||||
if args.inpaint_audio and not args.inpaint_starts:
|
||||
parser.error("--inpaint-audio requires --inpaint-start and --inpaint-end")
|
||||
|
||||
# --- Resolve batch size ---
|
||||
n_prompts = len(args.prompt)
|
||||
if args.batch_size is None:
|
||||
batch_size = n_prompts
|
||||
elif n_prompts > 1 and args.batch_size != n_prompts:
|
||||
parser.error(
|
||||
f"--batch-size {args.batch_size} does not match the number of prompts "
|
||||
f"({n_prompts}); omit --batch-size to have it inferred automatically"
|
||||
)
|
||||
else:
|
||||
batch_size = args.batch_size
|
||||
|
||||
# --- Validate list-flag lengths against batch size ---
|
||||
if (
|
||||
args.negative_prompt
|
||||
and len(args.negative_prompt) > 1
|
||||
and len(args.negative_prompt) != batch_size
|
||||
):
|
||||
parser.error(
|
||||
f"Got {len(args.negative_prompt)} --negative-prompt values but batch size is {batch_size}"
|
||||
)
|
||||
if len(args.duration) > 1 and len(args.duration) != batch_size:
|
||||
parser.error(
|
||||
f"Got {len(args.duration)} --duration values but batch size is {batch_size}"
|
||||
)
|
||||
|
||||
# --- Build scalar / list args ---
|
||||
prompt = args.prompt[0] if len(args.prompt) == 1 else args.prompt
|
||||
negative_prompt = None
|
||||
if args.negative_prompt:
|
||||
negative_prompt = (
|
||||
args.negative_prompt[0]
|
||||
if len(args.negative_prompt) == 1
|
||||
else args.negative_prompt
|
||||
)
|
||||
duration = args.duration[0] if len(args.duration) == 1 else args.duration
|
||||
|
||||
# --- chunked_decode flag ---
|
||||
chunked_decode = None
|
||||
if args.chunked_decode:
|
||||
chunked_decode = True
|
||||
elif args.no_chunked_decode:
|
||||
chunked_decode = False
|
||||
|
||||
# --- Load model ---
|
||||
print(f"Loading model '{args.model}'…")
|
||||
model = StableAudioModel.from_pretrained(
|
||||
args.model, device=args.device, model_half=not args.no_half
|
||||
)
|
||||
|
||||
# --- LoRA ---
|
||||
if args.loras:
|
||||
print(f"Loading LoRA(s): {args.loras}")
|
||||
model.load_lora(args.loras)
|
||||
if args.lora_strength is not None:
|
||||
model.set_lora_strength(args.lora_strength, lora_index=args.lora_index)
|
||||
|
||||
# --- Load audio inputs ---
|
||||
# torchaudio.load returns (waveform, sample_rate); model.generate expects (sample_rate, waveform)
|
||||
init_audio = None
|
||||
if args.init_audio:
|
||||
waveform, sr = torchaudio.load(args.init_audio)
|
||||
init_audio = (sr, waveform)
|
||||
|
||||
inpaint_audio = None
|
||||
if args.inpaint_audio:
|
||||
waveform, sr = torchaudio.load(args.inpaint_audio)
|
||||
inpaint_audio = (sr, waveform)
|
||||
|
||||
inpaint_start = None
|
||||
inpaint_end = None
|
||||
if args.inpaint_starts:
|
||||
inpaint_start = (
|
||||
args.inpaint_starts[0]
|
||||
if len(args.inpaint_starts) == 1
|
||||
else args.inpaint_starts
|
||||
)
|
||||
inpaint_end = (
|
||||
args.inpaint_ends[0] if len(args.inpaint_ends) == 1 else args.inpaint_ends
|
||||
)
|
||||
|
||||
sample_size = model.model_config["sample_size"]
|
||||
# --- Generate ---
|
||||
print("Generating…")
|
||||
audio = model.generate(
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
duration=duration,
|
||||
steps=args.steps,
|
||||
cfg_scale=args.cfg_scale,
|
||||
seed=args.seed,
|
||||
batch_size=batch_size,
|
||||
sample_size=sample_size,
|
||||
init_audio=init_audio,
|
||||
init_noise_level=args.init_noise_level,
|
||||
inpaint_audio=inpaint_audio,
|
||||
inpaint_mask_start_seconds=inpaint_start,
|
||||
inpaint_mask_end_seconds=inpaint_end,
|
||||
chunked_decode=chunked_decode,
|
||||
)
|
||||
|
||||
_save_output(audio, model.model.sample_rate, args.output, batch_size)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user