119 lines
4.4 KiB
Python
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() |