2026-09-13 15:51:48 +00:00
|
|
|
"""
|
|
|
|
|
Generic MOSS-TTS zero-shot cloning CLI: clone the voice in --reference and
|
|
|
|
|
say --text in it. Used directly, and as the final stage of webui/app.py.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import importlib.util
|
|
|
|
|
|
|
|
|
|
import soundfile as sf
|
|
|
|
|
import torch
|
|
|
|
|
from transformers import AutoModel, AutoProcessor
|
|
|
|
|
|
|
|
|
|
PRETRAINED = "OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_attn_implementation(device: str, dtype: torch.dtype) -> str:
|
|
|
|
|
if (
|
|
|
|
|
device == "cuda"
|
|
|
|
|
and importlib.util.find_spec("flash_attn") is not None
|
|
|
|
|
and dtype in {torch.float16, torch.bfloat16}
|
|
|
|
|
):
|
|
|
|
|
major, _ = torch.cuda.get_device_capability()
|
|
|
|
|
if major >= 8:
|
|
|
|
|
return "flash_attention_2"
|
|
|
|
|
return "sdpa" if device == "cuda" else "eager"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
parser = argparse.ArgumentParser(description="MOSS-TTS zero-shot voice cloning")
|
|
|
|
|
parser.add_argument("--text", required=True, help="Text to synthesize")
|
|
|
|
|
parser.add_argument("--reference", required=True, help="Reference audio path")
|
|
|
|
|
parser.add_argument("--output", required=True, help="Output wav path")
|
|
|
|
|
parser.add_argument("--language", default="English", help="Language tag")
|
2026-09-15 12:21:10 +00:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--instruction", default=None,
|
|
|
|
|
help='Style/delivery instruction (e.g. "Change the emotion to excited."). '
|
|
|
|
|
"Undocumented MOSS-TTS field for this checkpoint - confirmed to work for "
|
|
|
|
|
"emotion in testing, passed through as-is.",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--seed", type=int, default=None,
|
|
|
|
|
help="Random seed for reproducibility. Unset = nondeterministic.",
|
|
|
|
|
)
|
2026-09-13 15:51:48 +00:00
|
|
|
parser.add_argument("--audio_temperature", type=float, default=1.7)
|
|
|
|
|
parser.add_argument("--audio_top_p", type=float, default=0.8)
|
|
|
|
|
parser.add_argument("--audio_top_k", type=int, default=25)
|
|
|
|
|
parser.add_argument("--audio_repetition_penalty", type=float, default=1.0)
|
|
|
|
|
parser.add_argument("--max_new_tokens", type=int, default=4096)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
|
dtype = torch.bfloat16 if device == "cuda" else torch.float32
|
|
|
|
|
|
2026-09-15 12:21:10 +00:00
|
|
|
if args.seed is not None:
|
|
|
|
|
torch.manual_seed(args.seed)
|
|
|
|
|
if device == "cuda":
|
|
|
|
|
torch.cuda.manual_seed_all(args.seed)
|
|
|
|
|
|
2026-09-13 15:51:48 +00:00
|
|
|
torch.backends.cuda.enable_cudnn_sdp(False)
|
|
|
|
|
torch.backends.cuda.enable_flash_sdp(True)
|
|
|
|
|
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
|
|
|
|
torch.backends.cuda.enable_math_sdp(True)
|
|
|
|
|
|
|
|
|
|
attn_implementation = resolve_attn_implementation(device, dtype)
|
|
|
|
|
print(f"[INFO] attn_implementation={attn_implementation}")
|
|
|
|
|
|
|
|
|
|
processor = AutoProcessor.from_pretrained(PRETRAINED, trust_remote_code=True)
|
|
|
|
|
processor.audio_tokenizer = processor.audio_tokenizer.to(device)
|
|
|
|
|
|
|
|
|
|
model = AutoModel.from_pretrained(
|
|
|
|
|
PRETRAINED,
|
|
|
|
|
trust_remote_code=True,
|
|
|
|
|
attn_implementation=attn_implementation,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
).to(device)
|
|
|
|
|
model.eval()
|
|
|
|
|
|
|
|
|
|
conversation = [
|
|
|
|
|
processor.build_user_message(
|
|
|
|
|
text=args.text,
|
|
|
|
|
language=args.language,
|
|
|
|
|
reference=[args.reference],
|
2026-09-15 12:21:10 +00:00
|
|
|
instruction=args.instruction,
|
2026-09-13 15:51:48 +00:00
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
batch = processor([conversation], mode="generation")
|
|
|
|
|
outputs = model.generate(
|
|
|
|
|
input_ids=batch["input_ids"].to(device),
|
|
|
|
|
attention_mask=batch["attention_mask"].to(device),
|
|
|
|
|
max_new_tokens=args.max_new_tokens,
|
|
|
|
|
do_sample=True,
|
|
|
|
|
audio_temperature=args.audio_temperature,
|
|
|
|
|
audio_top_p=args.audio_top_p,
|
|
|
|
|
audio_top_k=args.audio_top_k,
|
|
|
|
|
audio_repetition_penalty=args.audio_repetition_penalty,
|
|
|
|
|
)
|
|
|
|
|
message = processor.decode(outputs)[0]
|
|
|
|
|
audio = message.audio_codes_list[0]
|
|
|
|
|
data = audio.detach().cpu().to(torch.float32).numpy()
|
|
|
|
|
if data.ndim == 2:
|
|
|
|
|
data = data.T
|
|
|
|
|
sf.write(args.output, data, processor.model_config.sampling_rate)
|
|
|
|
|
print(f"[INFO] Saved {args.output}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|