ChristophSchuhmann's picture
Release merged NotebookLM full FT + rank-64 final 652
9771537 verified
Raw History Blame Contribute Delete
7.46 kB
"""Humaneness Voice Large 1.3 Alpha: portable full-model inference.
Use a downloaded model directory, or a Hugging Face model ID. This file has no
JUPITER-specific runtime dependency. Twelve MOSS-v2 codebooks,48kHz mono.
"""
import argparse
import json
from pathlib import Path
LANGUAGES = {'en': 'English', 'de': 'German', 'nl': 'Dutch', 'fr': 'French', 'es': 'Spanish'}
def load_model(directory, device='cuda:0'):
import torch
from transformers import AutoModel, AutoTokenizer
from transformers.dynamic_module_utils import get_class_from_dynamic_module
model = AutoModel.from_pretrained(str(directory), trust_remote_code=True,
dtype=torch.bfloat16, attn_implementation='sdpa').to(device).eval()
Processor = get_class_from_dynamic_module('processing_moss_tts.MossTTSLocalProcessor', str(directory))
tokenizer = AutoTokenizer.from_pretrained(str(directory), trust_remote_code=True)
proc = Processor(tokenizer=tokenizer, audio_tokenizer=None, model_config=model.config)
return model, proc
def load_codec(proc, path, device='cuda:0'):
from transformers import AutoModel
codec = AutoModel.from_pretrained(str(path), trust_remote_code=True, codec_weight_dtype='fp32')
if hasattr(codec, 'set_compute_dtype'):
codec.set_compute_dtype('bf16')
proc.audio_tokenizer = codec.to(device).eval()
return proc
def build_user(proc, *, text, prompt, frames, language, reference=None, alias=None):
if not text.strip() or not prompt.strip() or language not in LANGUAGES:
raise ValueError('Nonempty literal text, English directions and a supported language required')
if reference is not None and (reference.ndim != 2 or reference.shape[1] != 12
or not 63 <= len(reference) <= 187):
raise ValueError('Reference codes must contain12codebooks over5.04–14.96seconds')
if alias is not None:
if reference is not None:
raise ValueError('Choose either a reference recording or a registered voice name, not both')
catalog = json.loads(Path(__file__).with_name('voices.json').read_text())
names = {v['speaker_alias'] for v in catalog['voices']}
if alias not in names:
raise ValueError('Unknown voice name; see VOICE_ALIASES.md and voices.json')
user = proc.build_user_message(text=text, instruction=prompt, tokens=int(frames),
language=LANGUAGES[language], reference=[reference] if reference is not None else None)
if alias is not None:
# Exact alias representation used by training/legacy/acting_data.py.
# It is not an invented prefix in the spoken text or a new special token.
marker = '- Reference(s):\nNone\n'
if user['content'].count(marker) != 1 or user.get('audio_codes_list'):
raise RuntimeError('Processor wrapper no longer matches the trained voice-name format')
user['content'] = user['content'].replace(marker, f'- Reference(s):\nSpeaker: {alias}\n', 1)
return user
def generate(model, proc, user, *, seed=777, max_frames=625, device='cuda:0'):
import torch
batch = proc([[user]], mode='generation')
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
with torch.inference_mode(), torch.autocast('cuda', dtype=torch.bfloat16):
output = model.generate(input_ids=batch['input_ids'].to(device),
attention_mask=batch['attention_mask'].to(device), max_new_frames=max_frames,
do_sample=True, audio_temperature=1., audio_top_p=.95, audio_top_k=50,
audio_repetition_penalty=1., use_kv_cache=True)
prefix, sequence = output[0]
if prefix != 0 or sequence.ndim != 2 or sequence.shape[1] != 13:
raise RuntimeError('Unexpected MOSS frame layout')
codes = sequence[1:, 1:]
codes = codes[(codes[:, 0] >= 0) & (codes[:, 0] != model.config.audio_pad_token_id)]
if len(codes) < 2 or int(codes.min()) < 0 or int(codes.max()) > 1023:
raise RuntimeError('Too few frames or corrupt codec indices')
return codes.cpu()
def decode(proc, codes, device='cuda:0'):
import numpy as np
import torch
with torch.inference_mode():
wave = proc.decode_audio_codes([codes.to(device)], return_stereo=False)[0]
wave = wave.float().cpu().numpy().reshape(-1)
rate = int(proc.model_config.sampling_rate)
if not np.isfinite(wave).all() or abs(len(wave) / len(codes) - rate / 12.5) > .01 * rate / 12.5:
raise RuntimeError('Codec frame-rate/sample-rate canary failed')
return wave, rate
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--model', default='laion/Humaneness-Voice-Large-1.3-Alpha')
p.add_argument('--codec', default='OpenMOSS-Team/MOSS-Audio-Tokenizer-v2')
p.add_argument('--text', required=True, help='Literal spoken words, without directing tags')
p.add_argument('--prompt', help='GENERAL/SCRIPT directions; literal \\n accepted')
p.add_argument('--prompt-file', type=Path)
p.add_argument('--language', choices=LANGUAGES, default='en')
voice = p.add_mutually_exclusive_group()
voice.add_argument('--reference-wav', type=Path)
voice.add_argument('--voice-name', help='Registered fictional voice alias; see VOICE_ALIASES.md')
p.add_argument('--seconds', type=float, default=8.)
p.add_argument('--seed', type=int, default=777)
p.add_argument('--output', type=Path, required=True)
args = p.parse_args()
if bool(args.prompt) == bool(args.prompt_file):
p.error('Supply exactly one of --prompt or --prompt-file')
if not 1 <= args.seconds <= 60:
p.error('Target seconds must be between1and60')
import numpy as np
import soundfile as sf
import torch
if not torch.cuda.is_available():
raise RuntimeError('The tested path requires a CUDA GPU')
device = 'cuda:0'
model, proc = load_model(args.model, device)
load_codec(proc, args.codec, device)
reference = None
if args.reference_wav:
wave, sr = sf.read(args.reference_wav, dtype='float32', always_2d=True)
if not np.isfinite(wave).all() or len(wave) / sr < 5.04:
raise ValueError('Use at least5.04seconds of clean single-speaker reference speech')
# Never reinstate37frames/2.96s. Crop waveform BEFORE encoding.
wave = wave[:int(sr * 14.96)]
with torch.inference_mode():
reference = proc.encode_audios_from_wav([torch.from_numpy(wave.T.copy())], int(sr), n_vq=12)[0][:187]
prompt = args.prompt_file.read_text() if args.prompt_file else args.prompt.replace('\\n', '\n')
frames = round(args.seconds * 12.5)
user = build_user(proc, text=args.text, prompt=prompt, frames=frames,
language=args.language, reference=reference, alias=args.voice_name)
codes = generate(model, proc, user, seed=args.seed, max_frames=min(1000, frames + 190))
wave, sr = decode(proc, codes)
args.output.parent.mkdir(parents=True, exist_ok=True)
if args.output.exists():
raise FileExistsError('Refusing to overwrite ' + str(args.output))
sf.write(args.output, wave, sr, subtype='PCM_16')
print(json.dumps({'output': str(args.output), 'duration_s': len(wave) / sr,
'requested_s': args.seconds, 'frames': len(codes), 'language': args.language,
'reference_frames': len(reference) if reference is not None else 0,
'voice_name': args.voice_name, 'seed': args.seed}))
if __name__ == '__main__':
main()