Fix Seed-VC integration: use SeedVCWrapper instead of non-existent load_model
- Replace broken import from inference import load_model with SeedVCWrapper - SeedVCWrapper properly loads all models (DiT, Whisper, BigVGAN, CAMPPlus, RMVPE) - Add support for F0 conditioning and voice conversion parameters - Implement proper voice conversion using wrapper.convert_voice() - Keep fallback to passthrough mode if wrapper fails to load Fixes: Could not import Seed-VC modules: cannot import name 'load_model' from 'inference'
This commit is contained in:
@@ -13,16 +13,16 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class SeedVCService:
|
class SeedVCService:
|
||||||
"""Service for voice conversion using Seed-VC."""
|
"""Service for voice conversion using Seed-VC."""
|
||||||
|
|
||||||
def __init__(self, model_name: str = "seed-uvit-whisper-small-wavenet", repo_path: str = "./Seed-VC"):
|
def __init__(self, model_name: str = "seed-uvit-whisper-small-wavenet", repo_path: str = "./Seed-VC"):
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
self.repo_path = repo_path
|
self.repo_path = repo_path
|
||||||
self.model = None
|
self.wrapper = None
|
||||||
self.device = None
|
self.device = None
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
|
|
||||||
logger.info(f"SeedVC Service initialized with model: {model_name}")
|
logger.info(f"SeedVC Service initialized with model: {model_name}")
|
||||||
|
|
||||||
def _ensure_repo_exists(self):
|
def _ensure_repo_exists(self):
|
||||||
"""Ensure Seed-VC repository is cloned."""
|
"""Ensure Seed-VC repository is cloned."""
|
||||||
if not os.path.exists(self.repo_path):
|
if not os.path.exists(self.repo_path):
|
||||||
@@ -32,171 +32,189 @@ class SeedVCService:
|
|||||||
"git", "clone", "https://github.com/Plachtaa/Seed-VC.git", self.repo_path
|
"git", "clone", "https://github.com/Plachtaa/Seed-VC.git", self.repo_path
|
||||||
], check=True)
|
], check=True)
|
||||||
logger.info("Seed-VC repository cloned")
|
logger.info("Seed-VC repository cloned")
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize the model (lazy loading)."""
|
"""Initialize the model (lazy loading)."""
|
||||||
if self.initialized:
|
if self.initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._ensure_repo_exists()
|
self._ensure_repo_exists()
|
||||||
|
|
||||||
# Add Seed-VC to path
|
# Add Seed-VC to path
|
||||||
if self.repo_path not in sys.path:
|
if self.repo_path not in sys.path:
|
||||||
sys.path.insert(0, self.repo_path)
|
sys.path.insert(0, self.repo_path)
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
logger.info(f"Using device: {self.device}")
|
logger.info(f"Using device: {self.device}")
|
||||||
|
|
||||||
# Import Seed-VC modules
|
# Import Seed-VC wrapper
|
||||||
try:
|
try:
|
||||||
from inference import load_model, inference
|
from seed_vc_wrapper import SeedVCWrapper
|
||||||
self.inference_fn = inference
|
self.wrapper = SeedVCWrapper(device=self.device)
|
||||||
self.model = load_model(self.model_name, self.device)
|
logger.info("Seed-VC wrapper loaded successfully")
|
||||||
logger.info(f"Model {self.model_name} loaded successfully")
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.warning(f"Could not import Seed-VC modules: {e}")
|
logger.warning(f"Could not import SeedVCWrapper: {e}")
|
||||||
logger.warning("Voice conversion will use passthrough mode")
|
logger.warning("Voice conversion will use passthrough mode")
|
||||||
self.model = None
|
self.wrapper = None
|
||||||
|
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initialize Seed-VC: {e}")
|
logger.error(f"Failed to initialize Seed-VC: {e}")
|
||||||
self.initialized = True # Mark as initialized to avoid retry
|
self.initialized = True # Mark as initialized to avoid retry
|
||||||
self.model = None
|
self.wrapper = None
|
||||||
|
|
||||||
def convert_voice(
|
def convert_voice(
|
||||||
self,
|
self,
|
||||||
source_audio_path: str,
|
source_audio_path: str,
|
||||||
reference_audio_path: str,
|
reference_audio_path: str,
|
||||||
output_path: str
|
output_path: str,
|
||||||
|
diffusion_steps: int = 10,
|
||||||
|
length_adjust: float = 1.0,
|
||||||
|
inference_cfg_rate: float = 0.7,
|
||||||
|
f0_condition: bool = False,
|
||||||
|
auto_f0_adjust: bool = True,
|
||||||
|
pitch_shift: int = 0
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Convert voice from source to match reference voice.
|
Convert voice from source to match reference voice.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_audio_path: Path to source audio (TTS output)
|
source_audio_path: Path to source audio (TTS output)
|
||||||
reference_audio_path: Path to reference voice audio
|
reference_audio_path: Path to reference voice audio
|
||||||
output_path: Path to save converted audio
|
output_path: Path to save converted audio
|
||||||
|
diffusion_steps: Number of diffusion steps (default: 10)
|
||||||
|
length_adjust: Length adjustment factor (default: 1.0)
|
||||||
|
inference_cfg_rate: Inference CFG rate (default: 0.7)
|
||||||
|
f0_condition: Whether to use F0 conditioning (default: False)
|
||||||
|
auto_f0_adjust: Whether to automatically adjust F0 (default: True)
|
||||||
|
pitch_shift: Pitch shift in semitones (default: 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to converted audio file
|
Path to converted audio file
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
logger.info(f"Converting voice: {source_audio_path} -> {output_path}")
|
logger.info(f"Converting voice: {source_audio_path} -> {output_path}")
|
||||||
logger.info(f"Using reference: {reference_audio_path}")
|
logger.info(f"Using reference: {reference_audio_path}")
|
||||||
|
|
||||||
if self.model is None:
|
if self.wrapper is None:
|
||||||
# Fallback: convert MP3 to WAV
|
# Fallback: convert MP3 to WAV
|
||||||
logger.warning("Seed-VC not available, using passthrough mode")
|
logger.warning("Seed-VC not available, using passthrough mode")
|
||||||
return self._passthrough_convert(source_audio_path, output_path)
|
return self._passthrough_convert(source_audio_path, output_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Load audio files
|
# Perform voice conversion using the wrapper
|
||||||
source_audio, sr = librosa.load(source_audio_path, sr=22050)
|
# Returns numpy array of audio samples
|
||||||
reference_audio, _ = librosa.load(reference_audio_path, sr=22050)
|
audio_array = self.wrapper.convert_voice(
|
||||||
|
source=source_audio_path,
|
||||||
# Perform voice conversion
|
target=reference_audio_path,
|
||||||
converted_audio = self.inference_fn(
|
diffusion_steps=diffusion_steps,
|
||||||
self.model,
|
length_adjust=length_adjust,
|
||||||
source_audio,
|
inference_cfg_rate=inference_cfg_rate,
|
||||||
reference_audio,
|
f0_condition=f0_condition,
|
||||||
sr=22050
|
auto_f0_adjust=auto_f0_adjust,
|
||||||
|
pitch_shift=pitch_shift,
|
||||||
|
stream_output=False # We want the full audio array, not streaming
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Determine sample rate based on f0_condition
|
||||||
|
sr = 44100 if f0_condition else 22050
|
||||||
|
|
||||||
# Save output
|
# Save output
|
||||||
sf.write(output_path, converted_audio, 22050)
|
sf.write(output_path, audio_array, sr)
|
||||||
logger.info(f"Voice conversion complete: {output_path}")
|
logger.info(f"Voice conversion complete: {output_path}")
|
||||||
|
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Voice conversion failed: {e}")
|
logger.error(f"Voice conversion failed: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
# Fallback: convert MP3 to WAV
|
# Fallback: convert MP3 to WAV
|
||||||
logger.warning("Fallback to MP3-to-WAV conversion")
|
logger.warning("Fallback to MP3-to-WAV conversion")
|
||||||
return self._passthrough_convert(source_audio_path, output_path)
|
return self._passthrough_convert(source_audio_path, output_path)
|
||||||
|
|
||||||
def _passthrough_convert(self, source_audio_path: str, output_path: str) -> str:
|
def _passthrough_convert(self, source_audio_path: str, output_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Convert MP3 to WAV as passthrough when Seed-VC is not available.
|
Convert MP3 to WAV as passthrough when Seed-VC is not available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_audio_path: Path to source MP3 audio
|
source_audio_path: Path to source MP3 audio
|
||||||
output_path: Path to save converted WAV audio
|
output_path: Path to save converted WAV audio
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to the output file (may differ from output_path if MP3 is used)
|
Path to the output file (may differ from output_path if MP3 is used)
|
||||||
"""
|
"""
|
||||||
# Use absolute paths
|
# Use absolute paths
|
||||||
source_audio_path = os.path.abspath(source_audio_path)
|
source_audio_path = os.path.abspath(source_audio_path)
|
||||||
output_path = os.path.abspath(output_path)
|
output_path = os.path.abspath(output_path)
|
||||||
|
|
||||||
logger.info(f"Passthrough convert: {source_audio_path} -> {output_path}")
|
logger.info(f"Passthrough convert: {source_audio_path} -> {output_path}")
|
||||||
|
|
||||||
# Verify source exists
|
# Verify source exists
|
||||||
if not os.path.exists(source_audio_path):
|
if not os.path.exists(source_audio_path):
|
||||||
raise FileNotFoundError(f"Source audio file not found: {source_audio_path}")
|
raise FileNotFoundError(f"Source audio file not found: {source_audio_path}")
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Ensure output directory exists
|
||||||
output_dir = os.path.dirname(output_path)
|
output_dir = os.path.dirname(output_path)
|
||||||
if output_dir:
|
if output_dir:
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
logger.debug(f"Ensured output directory exists: {output_dir}")
|
logger.debug(f"Ensured output directory exists: {output_dir}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
|
|
||||||
# Load MP3
|
# Load MP3
|
||||||
logger.debug(f"Loading MP3 from: {source_audio_path}")
|
logger.debug(f"Loading MP3 from: {source_audio_path}")
|
||||||
audio = AudioSegment.from_mp3(source_audio_path)
|
audio = AudioSegment.from_mp3(source_audio_path)
|
||||||
logger.debug(f"Audio loaded: {len(audio)}ms, {audio.channels} channels")
|
logger.debug(f"Audio loaded: {len(audio)}ms, {audio.channels} channels")
|
||||||
|
|
||||||
# Export as WAV
|
# Export as WAV
|
||||||
logger.debug(f"Exporting WAV to: {output_path}")
|
logger.debug(f"Exporting WAV to: {output_path}")
|
||||||
audio.export(output_path, format="wav")
|
audio.export(output_path, format="wav")
|
||||||
|
|
||||||
# Verify file was created
|
# Verify file was created
|
||||||
if not os.path.exists(output_path):
|
if not os.path.exists(output_path):
|
||||||
raise RuntimeError(f"WAV file was not created at {output_path}")
|
raise RuntimeError(f"WAV file was not created at {output_path}")
|
||||||
|
|
||||||
file_size = os.path.getsize(output_path)
|
file_size = os.path.getsize(output_path)
|
||||||
if file_size == 0:
|
if file_size == 0:
|
||||||
raise RuntimeError(f"WAV file is empty: {output_path}")
|
raise RuntimeError(f"WAV file is empty: {output_path}")
|
||||||
|
|
||||||
logger.info(f"MP3 to WAV conversion complete: {output_path} ({file_size} bytes)")
|
logger.info(f"MP3 to WAV conversion complete: {output_path} ({file_size} bytes)")
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"MP3 to WAV conversion failed: {e}")
|
logger.error(f"MP3 to WAV conversion failed: {e}")
|
||||||
|
|
||||||
# Fallback: return MP3 file directly (rename extension)
|
# Fallback: return MP3 file directly (rename extension)
|
||||||
mp3_output = output_path.replace('.wav', '.mp3')
|
mp3_output = output_path.replace('.wav', '.mp3')
|
||||||
if source_audio_path != mp3_output:
|
if source_audio_path != mp3_output:
|
||||||
import shutil
|
import shutil
|
||||||
logger.warning(f"Using MP3 fallback: copying to {mp3_output}")
|
logger.warning(f"Using MP3 fallback: copying to {mp3_output}")
|
||||||
shutil.copy2(source_audio_path, mp3_output)
|
shutil.copy2(source_audio_path, mp3_output)
|
||||||
|
|
||||||
if not os.path.exists(mp3_output):
|
if not os.path.exists(mp3_output):
|
||||||
raise RuntimeError(f"Failed to create fallback MP3 file: {mp3_output}")
|
raise RuntimeError(f"Failed to create fallback MP3 file: {mp3_output}")
|
||||||
|
|
||||||
logger.info(f"Fallback MP3 created: {mp3_output}")
|
logger.info(f"Fallback MP3 created: {mp3_output}")
|
||||||
return mp3_output
|
return mp3_output
|
||||||
else:
|
else:
|
||||||
# Source is already the target
|
# Source is already the target
|
||||||
return source_audio_path
|
return source_audio_path
|
||||||
|
|
||||||
def preprocess_audio(self, input_path: str, output_path: str, target_sr: int = 22050) -> str:
|
def preprocess_audio(self, input_path: str, output_path: str, target_sr: int = 22050) -> str:
|
||||||
"""
|
"""
|
||||||
Preprocess audio to match Seed-VC requirements.
|
Preprocess audio to match Seed-VC requirements.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_path: Input audio path
|
input_path: Input audio path
|
||||||
output_path: Output audio path
|
output_path: Output audio path
|
||||||
target_sr: Target sample rate
|
target_sr: Target sample rate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to preprocessed audio
|
Path to preprocessed audio
|
||||||
"""
|
"""
|
||||||
@@ -206,4 +224,4 @@ class SeedVCService:
|
|||||||
return output_path
|
return output_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Audio preprocessing failed: {e}")
|
logger.error(f"Audio preprocessing failed: {e}")
|
||||||
raise
|
raise
|
||||||
Reference in New Issue
Block a user