feat: initial Flask REST API implementation for TTS + Voice Conversion

This commit is contained in:
root
2026-04-08 01:35:34 +02:00
commit 879f770927
17 changed files with 837 additions and 0 deletions

5
app/services/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""Services package."""
from app.services.edge_tts_service import EdgeTTSService
from app.services.seed_vc_service import SeedVCService
__all__ = ['EdgeTTSService', 'SeedVCService']

View File

@@ -0,0 +1,67 @@
"""Edge-TTS service for text-to-speech conversion."""
import os
import asyncio
import logging
from typing import Optional
import edge_tts
logger = logging.getLogger(__name__)
class EdgeTTSService:
"""Service for converting text to speech using Edge-TTS."""
def __init__(self, voice: str = "ru-RU-SvetlanaNeural"):
self.voice = voice
logger.info(f"EdgeTTS Service initialized with voice: {voice}")
async def _generate_speech_async(self, text: str, output_path: str, speed: float = 1.0) -> str:
"""Generate speech asynchronously."""
# Adjust rate based on speed
if speed != 1.0:
rate_percent = int((speed - 1.0) * 100)
rate = f"{rate_percent:+d}%"
else:
rate = "+0%"
communicate = edge_tts.Communicate(text, self.voice, rate=rate)
await communicate.save(output_path)
return output_path
def text_to_speech(self, text: str, output_path: str, speed: float = 1.0) -> str:
"""
Convert text to speech and save to file.
Args:
text: Text to convert
output_path: Path to save audio file
speed: Speech speed multiplier (0.5 - 2.0)
Returns:
Path to generated audio file
"""
logger.info(f"Generating TTS for text: {text[:50]}...")
# Run async function in sync context
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
self._generate_speech_async(text, output_path, speed)
)
finally:
loop.close()
logger.info(f"TTS generated: {result}")
return result
def get_supported_voices(self) -> list:
"""Get list of supported voices."""
voices = [
"ru-RU-SvetlanaNeural",
"ru-RU-DmitryNeural",
"en-US-AriaNeural",
"en-US-GuyNeural",
]
return voices

View File

@@ -0,0 +1,143 @@
"""Seed-VC service for voice conversion."""
import os
import sys
import logging
import tempfile
from typing import Optional
import soundfile as sf
import librosa
import numpy as np
logger = logging.getLogger(__name__)
class SeedVCService:
"""Service for voice conversion using Seed-VC."""
def __init__(self, model_name: str = "seed-uvit-whisper-small-wavenet", repo_path: str = "./Seed-VC"):
self.model_name = model_name
self.repo_path = repo_path
self.model = None
self.device = None
self.initialized = False
logger.info(f"SeedVC Service initialized with model: {model_name}")
def _ensure_repo_exists(self):
"""Ensure Seed-VC repository is cloned."""
if not os.path.exists(self.repo_path):
logger.info("Cloning Seed-VC repository...")
import subprocess
subprocess.run([
"git", "clone", "https://github.com/Plachtaa/Seed-VC.git", self.repo_path
], check=True)
logger.info("Seed-VC repository cloned")
def initialize(self):
"""Initialize the model (lazy loading)."""
if self.initialized:
return
try:
self._ensure_repo_exists()
# Add Seed-VC to path
if self.repo_path not in sys.path:
sys.path.insert(0, self.repo_path)
import torch
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
logger.info(f"Using device: {self.device}")
# Import Seed-VC modules
try:
from inference import load_model, inference
self.inference_fn = inference
self.model = load_model(self.model_name, self.device)
logger.info(f"Model {self.model_name} loaded successfully")
except ImportError as e:
logger.warning(f"Could not import Seed-VC modules: {e}")
logger.warning("Voice conversion will use passthrough mode")
self.model = None
self.initialized = True
except Exception as e:
logger.error(f"Failed to initialize Seed-VC: {e}")
self.initialized = True # Mark as initialized to avoid retry
self.model = None
def convert_voice(
self,
source_audio_path: str,
reference_audio_path: str,
output_path: str
) -> str:
"""
Convert voice from source to match reference voice.
Args:
source_audio_path: Path to source audio (TTS output)
reference_audio_path: Path to reference voice audio
output_path: Path to save converted audio
Returns:
Path to converted audio file
"""
self.initialize()
logger.info(f"Converting voice: {source_audio_path} -> {output_path}")
logger.info(f"Using reference: {reference_audio_path}")
if self.model is None:
# Fallback: copy source audio
logger.warning("Seed-VC not available, using passthrough mode")
import shutil
shutil.copy(source_audio_path, output_path)
return output_path
try:
# Load audio files
source_audio, sr = librosa.load(source_audio_path, sr=22050)
reference_audio, _ = librosa.load(reference_audio_path, sr=22050)
# Perform voice conversion
converted_audio = self.inference_fn(
self.model,
source_audio,
reference_audio,
sr=22050
)
# Save output
sf.write(output_path, converted_audio, 22050)
logger.info(f"Voice conversion complete: {output_path}")
return output_path
except Exception as e:
logger.error(f"Voice conversion failed: {e}")
# Fallback: copy source audio
import shutil
shutil.copy(source_audio_path, output_path)
return output_path
def preprocess_audio(self, input_path: str, output_path: str, target_sr: int = 22050) -> str:
"""
Preprocess audio to match Seed-VC requirements.
Args:
input_path: Input audio path
output_path: Output audio path
target_sr: Target sample rate
Returns:
Path to preprocessed audio
"""
try:
audio, sr = librosa.load(input_path, sr=target_sr)
sf.write(output_path, audio, target_sr)
return output_path
except Exception as e:
logger.error(f"Audio preprocessing failed: {e}")
raise