Files
aiTTS/main.py

380 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import os
import time
import sounddevice as sd
import soundfile as sf
import numpy as np
import yaml
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from tts_engine import TTSEngine
# --- Функции записи и воспроизведения ---
def get_rms(data):
"""Вычисление среднеквадратичного уровня громкости"""
return np.sqrt(np.mean(data**2))
def record_sample_interactive(engine):
cfg = engine.config['recording']
sr = cfg['sample_rate']
channels = cfg['channels']
print("\n--- [REC] Запись нового сэмпла ---")
name = input("Имя сэмпла (латиница, без пробелов, Enter для auto): ").strip()
if not name: name = f"speaker_{int(time.time())}"
# Очистка имени
name = "".join(c for c in name if c.isalnum() or c in ('_', '-')).strip()
text_prompt = input("Фраза для чтения (сохранится как текст сэмпла): ").strip()
if not text_prompt:
print("[!] Текст сэмпла обязателен для качественного клонирования.")
return None
save_dir = os.path.join(engine.config['storage']['sample_dir'], name)
if os.path.exists(save_dir):
print(f"[!] Сэмпл с именем '{name}' уже существует. Перезапись.")
print("\n>>> НАЧАЛО ЗАПИСИ. Говорите сейчас!")
print(f">>> Запись остановится автоматически после паузы в {cfg['silence_duration']} сек.")
frames = []
silence_counter = 0
start_time = time.time()
try:
with sd.InputStream(samplerate=sr, channels=channels, dtype='float32') as stream:
while True:
data, overflowed = stream.read(1024)
if overflowed:
print("[!] Buffer overflow")
frames.append(data.copy())
# Логика VAD
rms = get_rms(data)
if rms < cfg['silence_threshold']:
silence_counter += len(data) / sr
else:
silence_counter = 0
current_duration = time.time() - start_time
if silence_counter >= cfg['silence_duration'] and current_duration > cfg['min_duration']:
print("\n[STOP] Тишина обнаружена. Остановка записи.")
break
except KeyboardInterrupt:
print("\n[!] Запись прервана вручную.")
# Обработка и сохранение
recording = np.concatenate(frames, axis=0)
os.makedirs(save_dir, exist_ok=True)
audio_path = os.path.join(save_dir, "audio.wav")
sf.write(audio_path, recording, sr)
prompt_path = os.path.join(save_dir, "prompt.txt")
with open(prompt_path, 'w', encoding='utf-8') as f:
f.write(text_prompt)
print(f"[+] Сэмпл сохранен: {save_dir}")
return save_dir
def select_sample_ui(engine):
samples = engine.get_available_samples()
if not samples:
print("[!] Нет сохраненных сэмплов. Сначала запишите один.")
return None
print("\n--- Выберите сэмпл ---")
for i, s in enumerate(samples):
txt_preview = s['prompt'][:40] + "..." if len(s['prompt']) > 40 else s['prompt']
print(f"[{i+1}] {s['name']} : \"{txt_preview}\"")
try:
idx = int(input("Номер: ")) - 1
if 0 <= idx < len(samples):
return samples[idx]['path']
except ValueError:
pass
print("[!] Неверный выбор.")
return None
def select_audio_device():
print("\n--- Выбор устройства вывода ---")
devices = sd.query_devices()
output_devices = []
for i, dev in enumerate(devices):
if dev['max_output_channels'] > 0:
output_devices.append((i, dev['name']))
for idx, name in output_devices:
default_marker = " (DEFAULT)" if idx == sd.default.device[1] else ""
print(f"[{idx}] {name}{default_marker}")
print("\nВведите ID устройства или Enter для использования по умолчанию.")
choice = input(">> ").strip()
if choice.isdigit():
return int(choice)
return None
def play_audio(filepath, device_id=None):
try:
data, sr = sf.read(filepath, dtype='float32')
sd.play(data, sr, device=device_id)
print(f">>> Воспроизведение: {os.path.basename(filepath)}")
except Exception as e:
print(f"[!] Ошибка воспроизведения: {e}")
def get_test_phrase(engine):
"""Получает тестовую фразу из конфига или возвращает дефолт"""
return engine.config.get('voice_design', {}).get('test_phrase',
'Привет! Это тестовая фраза для проверки нового голоса.')
# --- Главное меню ---
def main():
# Попытка установить UTF-8 для консоли Windows
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8')
except:
pass
print("Инициализация движка...")
try:
engine = TTSEngine("config.yaml")
except Exception as e:
print(f"Критическая ошибка инициализации: {e}")
return
current_sample_path = None
output_device = None
while True:
print("\n" + "="*50)
print(" QWEN3-TTS CONSOLE")
print("="*50)
print("1. Управление сэмплами (Запись/Выбор)")
print("2. Синтез речи")
print("3. История (Прослушивание/Чтение)")
print("4. Список стандартных голосов (CustomVoice)")
print("5. Загрузить/Проверить модели (кэширование)")
print("6. Настройки (Устройство вывода)")
print("0. Выход")
choice = input(">> ").strip()
# 1. Управление сэмплами
if choice == '1':
print("\n--- Управление сэмплами ---")
print("1. Записать новый сэмпл")
print("2. Выбрать существующий")
print("3. Сбросить текущий выбор")
sub = input(">> ").strip()
if sub == '1':
path = record_sample_interactive(engine)
if path: current_sample_path = path
elif sub == '2':
path = select_sample_ui(engine)
if path:
current_sample_path = path
print(f"[+] Выбран сэмпл: {path}")
elif sub == '3':
current_sample_path = None
print("[i] Сброшено. Будет использован голос по умолчанию.")
# 2. Синтез речи
elif choice == '2':
print("\nРежим синтеза:")
print(f"1. Клонирование (Сэмпл: {'Да' if current_sample_path else 'Нет'})")
print("2. Voice Design (описание + клонирование)")
print("3. Предпрослушка VoiceDesign (только генерация)")
print("4. Стандартный голос")
mode = input(">> ").strip()
# Для режимов 1, 2, 4 нужен ввод текста
# Для режима 3 используем тестовую фразу из конфига
text = None
if mode in ['1', '2', '4']:
text = input("\nВведите текст: ").strip()
if not text:
continue
elif mode == '3':
test_phrase = get_test_phrase(engine)
print(f"\nТестовая фраза из конфига: \"{test_phrase}\"")
use_custom = input("Использовать свой текст? (y/n): ").strip().lower()
if use_custom == 'y':
text = input("Введите текст: ").strip()
if not text:
continue
else:
text = test_phrase
else:
continue
try:
start_t = time.time()
wavs, sr = None, None
if mode == '1':
if not current_sample_path:
print("[!] Ошибка: Сэмпл не выбран!")
continue
wavs, sr = engine.generate_with_sample(text, current_sample_path)
elif mode == '2':
desc = input("Описание голоса (ТОЛЬКО АНГЛИЙСКИЙ!): ").strip()
if not desc:
desc = "A neutral clear voice with moderate pace" # дефолт на английском
if not desc: desc = "Neutral voice"
wavs, sr = engine.generate_with_description(text, desc)
elif mode == '3':
# Предпрослушка VoiceDesign
desc = input("Описание голоса (напр. 'Злой робот'): ").strip()
if not desc: desc = "Neutral voice"
print(f"\n🎨 Генерация VoiceDesign: '{desc}'")
wavs, sr = engine.generate_voice_design_only(text, desc)
# Автовоспроизведение
print("🎵 Воспроизведение...")
temp_path = os.path.join(engine.config['storage']['output_dir'], "_temp_preview.wav")
sf.write(temp_path, wavs[0], sr)
play_audio(temp_path, output_device)
# Меню после предпрослушки
print("\n--- Что дальше? ---")
print("1. Сгенерировать полный текст этим голосом (Design + Clone)")
print("2. Попробовать другое описание")
print("3. Сохранить результат")
print("4. Вернуться в меню")
next_action = input(">> ").strip()
if next_action == '1':
full_text = input("\nВведите полный текст: ").strip()
if full_text:
print(f"\n🔄 Генерация полного текста...")
start_t = time.time()
wavs, sr = engine.generate_with_description(full_text, desc)
else:
continue
elif next_action == '2':
continue
elif next_action == '3':
# Сохраняем текущий результат (тестовую фразу)
pass # wavs уже содержит аудио, сохранится ниже
else:
continue
elif mode == '4':
# Получаем список доступных спикеров
print("\nЗагрузка списка стандартных голосов...")
speakers = engine.get_custom_speakers_list()
selected_speaker = None
if speakers:
print(f"\n--- Доступные голоса ({len(speakers)}) ---")
for i, spk in enumerate(speakers):
marker = " (DEFAULT)" if spk == engine.config['generation']['default_speaker'] else ""
print(f"[{i+1}] {spk}{marker}")
print(f"\nВведите номер голоса (1-{len(speakers)}) или Enter для default:")
spk_choice = input(">> ").strip()
if spk_choice.isdigit():
spk_idx = int(spk_choice) - 1
if 0 <= spk_idx < len(speakers):
selected_speaker = speakers[spk_idx]
print(f"[+] Выбран голос: {selected_speaker}")
else:
print("[!] Неверный номер, используется default.")
else:
print("[i] Используется голос по умолчанию.")
else:
print("[!] Не удалось получить список голосов, используется default.")
wavs, sr = engine.generate_standard(text, speaker=selected_speaker)
if wavs is not None:
# Сохранение
saved_path = engine.save_result(text, wavs, sr)
elapsed = time.time() - start_t
print(f"\n[+] Успешно за {elapsed:.2f} сек.")
print(f"[+] Файл: {saved_path}")
# Для режима 3 (предпрослушка) уже воспроизвели, спрашиваем повторно только для других
if mode != '3':
ans = input("Воспроизвести сейчас? (y/n): ").strip().lower()
if ans == 'y':
play_audio(saved_path, output_device)
except Exception as e:
print(f"[!] Ошибка генерации: {e}")
# 3. История
elif choice == '3':
history = engine.get_history()
if not history:
print("\n[i] Папка out пуста.")
continue
print(f"\n--- История ({len(history)} файлов) ---")
for i, item in enumerate(history[:10]):
print(f"[{i+1}] {item['filename']}")
print(f" Текст: {item['text'][:50]}...")
if len(history) > 10:
print("... (показаны последние 10)")
print("\nВведите номер для прослушивания/чтения или Enter.")
sel = input(">> ").strip()
if sel.isdigit():
idx = int(sel) - 1
if 0 <= idx < len(history):
item = history[idx]
print(f"\n>>> Файл: {item['filename']}")
print(f"Текст:\n{item['text']}")
print("-" * 30)
play_audio(item['wav_path'], output_device)
# 4. Список стандартных голосов
elif choice == '4':
print("\nЗагрузка списка голосов из CustomVoice модели...")
try:
speakers = engine.get_custom_speakers_list()
if speakers:
print("\n--- Доступные голоса ---")
for spk in speakers:
print(f"- {spk}")
print("\n(Один из них используется в режиме 'Стандартный голос')")
else:
print("[!] Список пуст или модель не загружена.")
except Exception as e:
print(f"[!] Ошибка: {e}")
# 5. Принудительное скачивание моделей
elif choice == '5':
engine.download_all_models()
# 6. Настройки
elif choice == '6':
output_device = select_audio_device()
if output_device:
print(f"[+] Установлено устройство вывода: {sd.query_devices(output_device)['name']}")
else:
print("[i] Используется системное устройство по умолчанию.")
elif choice == '0':
print("Выход...")
break
if __name__ == "__main__":
main()