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()