Разбиение на отдельные файлы и запуск модели

#31
by tomasusername - opened

Если запускать с параметром --gen_text - создаёт множество маленьких аудиофайлов, но при каждом запуске нужно ожидать значительное время, пока загрузится модель.

При генерации из файла с параметром -f - создаёт один большой аудиофайл, и загрузка происходит только один раз при запуске.

Можно ли запустить так, чтобы создавались множество коротких файлов, но загрузка произошла единожды?

Буквально:

  • запуск, загружается модель, анализирует референс
  • получает перечень фраз и создаёт множество аудиофайлов, вместо одного большого файла

Как это можно сделать, подскажите пожалуйста? Спасибо!

Большую часть времени происходит не генерация, а загрузка модели и образца.

Можно ли сделать так, чтобы загруженная модель с образцом оставались доступными для многократной генерации?

Сделал. Теперь работает максимально быстро.

У меня GeForce RTX 4060 c 16 ГБ ВОЗУ - генерирует речь раза в 2-3 быстрее, чем произносится. Например, если в результате получается аудио на 30 секунд - он генерирует его 10 секунд.

При этом, главное - генерация идёт непрерывно, без перезагрузки модели.

У меня коллекция (ref_audi, ref_text) в yaml-файле на сервере. Клиент передаёт ключ к голосу. Переделайте по вашему усмотрению, если вам так не удобно.

#!   (путь к локальному венву, т.к. запускаю из другого сервера, как службу) /f5tts2/venv/bin/python3

from flask import Flask, request, jsonify
import logging
import os
import yaml
from pathlib import Path
import torch
import soundfile as sf
import numpy as np
import sys
import socket
from threading import Thread
import time 
# Импорты для низкоуровневой загрузки модели, как в infer_cli.py
from omegaconf import OmegaConf
from hydra.utils import get_class
from importlib.resources import files

# Убедитесь, что utils_infer.py находится в PYTHONPATH или импортируйте явно
try:
    from f5_tts.infer.utils_infer import (
        device,
        mel_spec_type,
        target_rms,
        cross_fade_duration,
        nfe_step,
        cfg_strength,
        sway_sampling_coef,
        speed,
        fix_duration,
        infer_process,
        load_model,
        load_vocoder,
        preprocess_ref_audio_text,
    )
except ImportError as e:
    # Запасной вариант, если utils_infer.py лежит рядом
    if 'f5_tts.infer.utils_infer' in str(e):
        sys.path.append(os.path.join(os.path.dirname(__file__), '..')) # Пример
        from f5_tts.infer.utils_infer import (
            device,
            mel_spec_type,
            target_rms,
            cross_fade_duration,
            nfe_step,
            cfg_strength,
            sway_sampling_coef,
            speed,
            fix_duration,
            infer_process,
            load_model,
            load_vocoder,
            preprocess_ref_audio_text,
        )
    else:
        raise e


# ------------------------------------------------------------
# Настройки и предзагрузка модели
# ------------------------------------------------------------
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__)

OUTPUT_FOLDER = 'output'
os.makedirs(OUTPUT_FOLDER, exist_ok=True)

# Глобальные переменные для модели и вокодера
ema_model = None
vocoder = None



def is_port_free(port):
    """Проверяет, свободен ли указанный порт, пытаясь привязать к нему сокет."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            # Пытаемся привязаться ко всем интерфейсам
            s.bind(('0.0.0.0', port))
            return True
        except OSError as e:
            # EADDRINUSE - это наиболее распространенная ошибка, указывающая на занятость порта
            if 'Address already in use' in str(e):
                 return False
            # Для других ошибок (например, permission denied)
            raise


def initialize_model():
    """Загружаем модель и вокодер один раз при запуске, используя только локальные пути."""
    global ema_model
    global vocoder
    
    
    # Настройки TTS модели
    model_name = "F5TTS_v1_Base"
    ckpt_path = 'f5-tts-russian/F5TTS_v1_Base_v2/model_last_inference.safetensors'
    vocab_file = 'ckpts/ru_f5tts/F5TTS_v1_Base/vocab.txt'
    model_cfg_path = 'ckpts/ru_f5tts/F5TTS_v1_Base_v2/F5TTS_v1_Base.yaml'

    # Настройки Vocoder (как в CLI)
    vocoder_name = "vocos"
    vocoder_local_path = "ckpts/vocos-mel-24khz/"
    # --------------------------------------------------

    try:
        logger.info("Загрузка моделей (ПОЛНОСТЬЮ ОФЛАЙН)...")
        current_device = "cuda" if torch.cuda.is_available() else "cpu"
        logger.info(f"Используется устройство: {current_device}")
        
        # 1. Загрузка Vocoder (КЛЮЧЕВОЙ МОМЕНТ - is_local=True)
        logger.info(f"1. Загрузка Vocoder '{vocoder_name}' (локально)...")
        vocoder = load_vocoder(
            vocoder_name=vocoder_name, 
            is_local=True, # <--- ЭТО УБИРАЕТ ЗАПРОС В ИНТЕРНЕТ
            local_path=vocoder_local_path, 
            device=current_device
        )
        logger.info(f"Vocoder загружен из: {vocoder_local_path}")
        
        # 2. Загрузка TTS модели (логика из infer_cli.py)
        logger.info(f"2. Загрузка TTS модели '{model_name}'...")
        
        # NOTE: Если model_cfg_path не найден, вам нужно будет указать
        # правильный путь к файлу configs/F5TTS_v1_Base.yaml
        model_cfg = OmegaConf.load(model_cfg_path)
        model_cls = get_class(f"f5_tts.model.{model_cfg.model.backbone}")
        model_arc = model_cfg.model.arch

        # В infer_cli.py используется vocoder_name как mel_spec_type
        ema_model = load_model(
            model_cls, 
            model_arc, 
            ckpt_path, 
            mel_spec_type=vocoder_name, 
            vocab_file=vocab_file, 
            device=current_device
        )
        
        logger.info(f"Модель TTS успешно загружена: {ckpt_path}")
        return True
        
    except Exception as e:
        logger.error(f"Ошибка загрузки модели. Проверьте пути к файлам и наличие папок:\n{e}")
        return False



@app
	.route('/voice', methods=['POST'])
def synthesize_speech():
    """
    Генерация речи с референсным голосом, используя infer_process.
    """
    global ema_model
    global vocoder
    
    if ema_model is None or vocoder is None:
        return jsonify({'error': 'Model or Vocoder not initialized'}), 500
    
    try:
        # -------------------------------------------------------------
        # 1. Получаем параметры и конфиги
        # -------------------------------------------------------------
        data = request.form
        gen_text = data.get('text', '').strip()
        out_wav = data.get('out', '').strip()
        sample = data.get('sample', '').strip()

        if not all([gen_text, out_wav, sample]):
            return jsonify({'error': 'Parameters text, out and sample are required'}), 400

        # Загружаем конфигурацию samples
        with open('samples/samples.yaml', 'r', encoding='utf-8') as f:
             samples_config = yaml.safe_load(f)
        
        if sample not in samples_config:
            return jsonify({'error': f'Sample {sample} not found'}), 400
            
        sample_data = samples_config[sample]
        ref_audio_orig = sample_data.get('ref_audio')
        ref_text_orig = sample_data.get('ref_text')

        if not ref_audio_orig or not ref_text_orig:
            return jsonify({'error': 'Invalid sample configuration'}), 400

        # -------------------------------------------------------------
        # 2. Препроцессинг референсного аудио/текста (как в CLI)
        # -------------------------------------------------------------
        logger.info(f"Синтез: '{gen_text}' с референсом {sample} ({ref_audio_orig})")
        
        ref_audio, ref_text = preprocess_ref_audio_text(
            ref_audio_orig, 
            ref_text_orig, 
            show_info=logger.info
        )
        
        # Добавляем пробелы как в оригинальной логике
        formatted_text = f'{gen_text}   .               .'
        
        output_path = os.path.join(OUTPUT_FOLDER, out_wav)
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        # -------------------------------------------------------------
        # 3. Генерируем речь с помощью infer_process
        # -------------------------------------------------------------
        audio_segment, final_sample_rate, spectragram = infer_process(
            ref_audio,
            ref_text,
            formatted_text,
            ema_model,
            vocoder,
            mel_spec_type=mel_spec_type,
            target_rms=target_rms,
            cross_fade_duration=cross_fade_duration,
            nfe_step=nfe_step,
            cfg_strength=cfg_strength,
            sway_sampling_coef=sway_sampling_coef,
            speed=speed,
            fix_duration=fix_duration,
            device=device,
        )

        final_wave = np.concatenate([audio_segment]) # infer_process возвращает сегмент, но в CLI это список
        
        # 4. Сохранение и ответ
        sf.write(output_path, final_wave, final_sample_rate)
        absolute_path = os.path.abspath(output_path)
        
        logger.info(f"Генерация завершена успешно. Sample rate: {final_sample_rate}")
        logger.info(f"Синтез завершен: {absolute_path}")
        
        return jsonify({
            'success': True,
            'output_file': absolute_path,
            'filename': out_wav,
            'message': 'Synthesis completed',
            'sample_rate': final_sample_rate
        })

    except Exception as e:
        logger.exception("Ошибка при синтезе речи")
        return jsonify({'error': str(e)}), 500

# Тестовый endpoint и Health Check можно оставить без изменений


@app
	.route('/health', methods=['GET'])
def health_check():
    status = 'ok' if ema_model is not None and vocoder is not None else 'model_not_loaded'
    device_name = str(device) if ema_model else 'none'
    return jsonify({
        'status': status, 
        'service': 'F5-TTS Server',
        'device': device_name,
        # 'sample_rate': target_sample_rate # Используем значение из utils_infer.py
    })



@app
	.route('/test', methods=['GET'])
def test_synthesis():
    """Тестовый endpoint с теми же параметрами что и в CLI"""
    global ema_model
    global vocoder

    if ema_model is None or vocoder is None:
        return jsonify({'error': 'Model or Vocoder not initialized'}), 500
    
    try:
        # Загружаем конфигурацию samples
        with open('samples/samples.yaml', 'r', encoding='utf-8') as f:
             samples_config = yaml.safe_load(f)

        if not samples_config:
            return jsonify({'error': 'No samples configured'}), 500
            
        first_sample = list(samples_config.keys())[0]
        sample_data = samples_config[first_sample]
        
        ref_audio_orig = sample_data['ref_audio']
        ref_text_orig = sample_data['ref_text']

        if not os.path.exists(ref_audio_orig):
            return jsonify({'error': f'Reference audio file not found: {ref_audio_orig}'}), 400
        
        # Препроцессинг
        ref_audio, ref_text = preprocess_ref_audio_text(
            ref_audio_orig, 
            ref_text_orig, 
            show_info=logger.info
        )

        test_text = "Привет, это тест синтеза речи."
        test_output = "test_output.wav"
        output_path = os.path.join(OUTPUT_FOLDER, test_output)
        
        # Генерируем с помощью infer_process
        audio_segment, final_sample_rate, spectragram = infer_process(
            ref_audio,
            ref_text,
            f'   {test_text}   ', # С пробелами как в CLI
            ema_model,
            vocoder,
            mel_spec_type=mel_spec_type,
            target_rms=target_rms,
            cross_fade_duration=cross_fade_duration,
            nfe_step=nfe_step,
            cfg_strength=cfg_strength,
            sway_sampling_coef=sway_sampling_coef,
            speed=speed,
            fix_duration=fix_duration,
            device=device,
        )
        
        final_wave = np.concatenate([audio_segment]) 
        sf.write(output_path, final_wave, final_sample_rate)

        return jsonify({
            'success': True,
            'message': 'Test synthesis completed',
            'output_file': os.path.abspath(output_path),
            'sample_used': first_sample,
            'sample_rate': final_sample_rate
        })
        
    except Exception as e:
        logger.error(f"Тестовый синтез failed: {e}")
        return jsonify({'error': str(e)}), 500




@app
	.route('/shutdown', methods=['POST'])
def shutdown():
    """Отключает сервер, используя механизм Werkzeug для Graceful Shutdown."""
    logger.info("Получена команда на отключение сервера...")
    # Получаем функцию отключения, предоставляемую сервером Werkzeug
    func = request.environ.get('werkzeug.server.shutdown')
    if func:
        # Выполняем отключение в отдельном потоке, чтобы запрос /shutdown мог завершиться
        # Используем небольшую задержку, чтобы HTTP-ответ успел уйти клиенту
        Thread(target=lambda: (logger.info("Отключение сервера через 1 секунду..."), time.sleep(1), func()), daemon=True).start()
        logger.warning("Сервер получил команду на отключение и завершит работу через несколько секунд.")
        return jsonify({'message': 'Сервер получил команду на отключение и завершит работу.'}), 200
    else:
        logger.warning("Не удалось найти функцию Werkzeug для отключения. Сервер запущен в продакшен режиме?")
        return jsonify({'error': 'Не удалось выполнить корректное отключение сервера.'}), 500




if __name__ == '__main__':
    # Инициализируем модель при запуске
    logger.info("Запуск инициализации модели...")
    
    port = 3000 # Используем запрошенный порт 3000

    # 1. Проверка занятости порта
    if not is_port_free(port):
        logger.error(f"🚨 Порт {port} уже используется. Сервер не будет запущен. Завершение процесса.")
        sys.exit(1)
        
    # 2. Инициализация модели
    if initialize_model():
        logger.info(f"🚀 Запуск F5-TTS сервера на localhost:{port}")
        app.run(host='0.0.0.0', port=port, debug=False)
    else:
        logger.error("Не удалось загрузить модель. Сервер не запущен.")

И, да... это локальная версия. Я добился стабильной работы оффлайн.

Sign up or log in to comment