Files
model-training/tts/find_voice.py
T
2026-07-19 23:52:25 +04:00

119 lines
4.4 KiB
Python

"""
maven voice finder
generates single samples via VoiceDesign until you find one you like.
saves the winner as maven_reference.wav + its text as maven_reference.txt
for use with Base model cloning.
requires: pip install qwen-tts soundfile torch
also needs: sudo apt install ffmpeg (for playback)
"""
import sys
import soundfile as sf
import subprocess
from pathlib import Path
import torch
from qwen_tts import Qwen3TTSModel
# ── config ────────────────────────────────────────────────────────────────────
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign"
REF_DIR = Path("ref")
TEXT_FILE = "maven_reference.txt"
AUDIO_FILE = "maven_reference.wav"
# neutral sentence — long enough to capture voice characteristics
SAMPLE_TEXT = (
"Ну, если честно, я не уверена, что это сработает именно так. "
"Но давай попробуем - хуже уже точно не будет."
"Хотя, возможно, стоит подумать о другом подходе."
"Ошибки нынче стоят дорого. "
)
VOICE_DESIGN = (
"Female voice, mid-20s. Natural and grounded, but lively. "
"Fast, slightly uneven pacing — thoughts often move ahead of speech. "
"Light, reactive expressiveness: small spikes of interest, brief amused or curious tones, "
"then returning to a calm baseline. "
"Occasional micro-pauses, restarts, or cut-off phrases, like thinking in real time. "
"Tone is clear and fairly light, but not high-pitched or cutesy. "
"Not performative — feels spontaneous rather than acted. "
"Subtle unpredictability in rhythm and delivery. "
"No anime-style, no exaggerated эмоции, no theatrical voice acting."
)
# ── playback ──────────────────────────────────────────────────────────────────
def play(path: Path):
"""play wav via ffplay (ffmpeg). silent if not available."""
try:
subprocess.run(
["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", str(path)],
check=True
)
except FileNotFoundError:
print(" [!] ffplay not found — open the wav manually")
except subprocess.CalledProcessError:
pass
# ── main ──────────────────────────────────────────────────────────────────────
def main():
print(f"[*] loading model: {MODEL}")
model = Qwen3TTSModel.from_pretrained(
MODEL,
device_map="cuda:0",
dtype=torch.float32,
)
attempt = 0
current_index = max([int(dir.stem) for dir in REF_DIR.iterdir() if dir.is_dir()])
next_index = current_index + 1
print(f"\ncurrent index is: {current_index}")
print(f"\nnext will be: {next_index}")
while True:
attempt += 1
print(f"\n[{attempt}] generating sample...")
wavs, sr = model.generate_voice_design(
text=SAMPLE_TEXT,
instruct=VOICE_DESIGN,
language="Russian",
)
audio = wavs[0] if isinstance(wavs, list) else wavs
tmp = Path(f"candidate_{attempt:03d}.wav")
sf.write(str(tmp), audio, sr)
print(f" saved → {tmp}")
play(tmp)
answer = input(" keep this voice? [y/n/q] ").strip().lower()
if answer == "y":
new_path = REF_DIR / str(next_index) / AUDIO_FILE
if not new_path.exists():
new_path.parent.mkdir()
tmp.rename(new_path)
text_path = REF_DIR / str(next_index) / TEXT_FILE
text_path.write_text(SAMPLE_TEXT, encoding="utf-8")
print(f"\n[✓] saved as {new_path}")
print(f"[✓] reference text → {text_path}")
print("\nnext step: update generate_dataset.py to use Base model + this reference.")
tmp.unlink(missing_ok=True)
break
elif answer == "q":
print("quitting.")
tmp.unlink(missing_ok=True)
sys.exit(0)
else:
print(" regenerating...")
tmp.unlink(missing_ok=True)
if __name__ == "__main__":
main()