Files
aiTTS/main.py

267 lines
11 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 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--- 🎙 Запись нового сэмпла ---")
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 (Voice Activity Detection)
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🛑 Тишина обнаружена. Остановка записи.")
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)}")
# sd.wait() # Раскомментировать, если нужно блокировать консоль до конца воспроизведения
except Exception as e:
print(f"❌ Ошибка воспроизведения: {e}")
# --- Главное меню ---
def main():
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" + "="*40)
print(" QWEN3-TTS CONSOLE (Full Version)")
print("="*40)
print("1. 🎙 Управление сэмплами")
print("2. 🗣 Синтез речи")
print("3. 📁 История (Прослушивание/Чтение)")
print("4. ⚙️ Настройки")
print("0. Выход")
choice = input(">> ").strip()
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("Сброшено. Будет использован голос по умолчанию.")
elif choice == '2':
text = input("\nВведите текст: ").strip()
if not text: continue
print("\nРежим синтеза:")
print(f"1. Клонирование (Сэмпл: {'Да' if current_sample_path else 'Нет'})")
print("2. Описание голоса (Voice Design)")
print("3. Стандартный голос")
mode = input(">> ").strip()
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 = "Neutral voice"
wavs, sr = engine.generate_with_description(text, desc)
elif mode == '3':
wavs, sr = engine.generate_standard(text)
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}")
# Вопрос о воспроизведении
ans = input("Воспроизвести сейчас? (y/n): ").strip().lower()
if ans == 'y':
play_audio(saved_path, output_device)
except Exception as e:
print(f"❌ Ошибка генерации: {e}")
elif choice == '3':
history = engine.get_history()
if not history:
print("\n📂 Папка out пуста.")
continue
print(f"\n--- 📂 История ({len(history)} файлов) ---")
# Выводим последние 10
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
# Ищем в полном списке, но отображаем 10
# Для простоты берем индекс из отображаемого списка (0-9)
# Но правильнее из полного списка history
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)
elif choice == '4':
output_device = select_audio_device()
if output_device:
print(f"Установлено устройство вывода: {sd.query_devices(output_device)['name']}")
else:
print("Используется системное устройство по умолчанию.")
elif choice == '0':
print("Выход...")
break
if __name__ == "__main__":
main()