mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 07:55:33 -06:00
Release NVIDIA WakeWord Trainer v22
This commit is contained in:
151
cli/tts_qwen_worker.py
Executable file
151
cli/tts_qwen_worker.py
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Persistent-process Qwen3-TTS worker used by the sample orchestrator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from qwen_tts import Qwen3TTSModel
|
||||
|
||||
|
||||
VOICE_DESIGN_MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign"
|
||||
VOICE_CLONE_MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
|
||||
|
||||
def read_jsonl(path: Path) -> list[dict]:
|
||||
entries = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
entries.append(json.loads(line))
|
||||
return entries
|
||||
|
||||
|
||||
def chunks(values: list, size: int):
|
||||
for index in range(0, len(values), size):
|
||||
yield values[index : index + size]
|
||||
|
||||
|
||||
def runtime() -> tuple[str, torch.dtype]:
|
||||
if torch.cuda.is_available():
|
||||
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
||||
return "cuda:0", dtype
|
||||
return "cpu", torch.float32
|
||||
|
||||
|
||||
def load_model(model_id: str) -> Qwen3TTSModel:
|
||||
device, dtype = runtime()
|
||||
return Qwen3TTSModel.from_pretrained(
|
||||
model_id,
|
||||
device_map=device,
|
||||
dtype=dtype,
|
||||
attn_implementation="sdpa",
|
||||
)
|
||||
|
||||
|
||||
def build_bank(entries: list[dict], output_dir: Path, batch_size: int) -> None:
|
||||
model = load_model(VOICE_DESIGN_MODEL)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
for batch in chunks(entries, max(1, batch_size)):
|
||||
seed = int(batch[0].get("seed", 0))
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
wavs, sample_rate = model.generate_voice_design(
|
||||
text=[str(item["text"]) for item in batch],
|
||||
language=[str(item["language_name"]) for item in batch],
|
||||
instruct=[str(item["instruct"]) for item in batch],
|
||||
)
|
||||
for item, wav in zip(batch, wavs):
|
||||
sf.write(output_dir / f"{item['id']}.wav", wav, sample_rate)
|
||||
|
||||
|
||||
def generate_direct(entries: list[dict], output_dir: Path, batch_size: int) -> None:
|
||||
"""Create every final corpus candidate with a fresh voice design."""
|
||||
|
||||
model = load_model(VOICE_DESIGN_MODEL)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
completed = 0
|
||||
for batch in chunks(entries, max(1, batch_size)):
|
||||
seed = int(batch[0].get("seed", completed + 1))
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
wavs, sample_rate = model.generate_voice_design(
|
||||
text=[str(item["text"]) for item in batch],
|
||||
language=[str(item["language_name"]) for item in batch],
|
||||
instruct=[str(item["instruct"]) for item in batch],
|
||||
# Qwen emits 12 acoustic frames per second. Four seconds is a hard
|
||||
# wake-phrase ceiling and prevents decoder rambling.
|
||||
max_new_tokens=48,
|
||||
temperature=0.8,
|
||||
top_k=50,
|
||||
top_p=0.9,
|
||||
repetition_penalty=1.12,
|
||||
)
|
||||
for item, wav in zip(batch, wavs):
|
||||
sf.write(output_dir / f"{item['id']}.wav", wav, sample_rate)
|
||||
completed += 1
|
||||
if completed % 25 == 0 or completed == len(entries):
|
||||
print(f"Qwen direct generation created {completed}/{len(entries)}", flush=True)
|
||||
|
||||
|
||||
def generate(entries: list[dict], output_dir: Path, batch_size: int) -> None:
|
||||
model = load_model(VOICE_CLONE_MODEL)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
grouped: dict[tuple[str, str, str], list[dict]] = defaultdict(list)
|
||||
for item in entries:
|
||||
key = (
|
||||
str(item["ref_audio"]),
|
||||
str(item["ref_text"]),
|
||||
str(item["language_name"]),
|
||||
)
|
||||
grouped[key].append(item)
|
||||
|
||||
for (ref_audio, ref_text, language_name), group in grouped.items():
|
||||
prompt = model.create_voice_clone_prompt(
|
||||
ref_audio=ref_audio,
|
||||
ref_text=ref_text,
|
||||
x_vector_only_mode=False,
|
||||
)
|
||||
for batch in chunks(group, max(1, batch_size)):
|
||||
seed = int(batch[0].get("seed", 0))
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
wavs, sample_rate = model.generate_voice_clone(
|
||||
text=[str(item["text"]) for item in batch],
|
||||
language=[language_name] * len(batch),
|
||||
voice_clone_prompt=prompt,
|
||||
)
|
||||
for item, wav in zip(batch, wavs):
|
||||
sf.write(output_dir / f"{item['id']}.wav", wav, sample_rate)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--mode", choices=("bank", "direct", "generate"), required=True)
|
||||
parser.add_argument("--input-jsonl", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument("--batch-size", type=int, default=4)
|
||||
args = parser.parse_args()
|
||||
|
||||
entries = read_jsonl(args.input_jsonl)
|
||||
if not entries:
|
||||
return 0
|
||||
if args.mode == "bank":
|
||||
build_bank(entries, args.output_dir, args.batch_size)
|
||||
elif args.mode == "direct":
|
||||
generate_direct(entries, args.output_dir, args.batch_size)
|
||||
else:
|
||||
generate(entries, args.output_dir, args.batch_size)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user