voice-cloning-api / openvoice_transfer.py
rahulseerand's picture
Disable unused OpenVoice watermark loader
d1bf7f7 verified
Raw
History Blame Contribute Delete
3.45 kB
"""
OpenVoice v2 — tone-colour voice transfer.
Wraps MyShell's ToneColorConverter to stamp a parent's voice timbre
onto MMS-synthesised audio.
Usage pattern:
1. At profile upload time:
se = extract_se("parent_voice.wav")
torch.save(se, "profile.pt")
2. At audio generation time:
output_wav = transfer_voice(mms_wav_bytes, target_se_tensor)
"""
from __future__ import annotations
import os
import sys
import tempfile
import types
from typing import Optional
import torch
# Lazy-loaded singletons
_converter = None
def load_converter(ckpt_dir: str, device: str = "cpu") -> None:
"""
Load the OpenVoice v2 ToneColorConverter from *ckpt_dir*.
Expected files:
- <ckpt_dir>/config.json
- <ckpt_dir>/checkpoint.pth
"""
global _converter
from openvoice.api import ToneColorConverter
config_path = os.path.join(ckpt_dir, "config.json")
ckpt_path = os.path.join(ckpt_dir, "checkpoint.pth")
print(f"[OpenVoice] Loading converter from {ckpt_dir} …")
# OpenVoice imports wavmark inside the converter constructor even though
# watermarking is not needed for voice transfer. A no-op loader avoids an
# extra model and works around its broken enable_watermark keyword path.
wavmark_stub = types.ModuleType("wavmark")
class DisabledWatermark:
def to(self, _device):
return None
wavmark_stub.load_model = DisabledWatermark
sys.modules["wavmark"] = wavmark_stub
converter = ToneColorConverter(config_path, device=device)
converter.load_ckpt(ckpt_path)
_converter = converter
print("[OpenVoice] Converter ready.")
def _ensure_loaded() -> None:
if _converter is None:
raise RuntimeError(
"OpenVoice converter is not loaded. Call load_converter() at startup."
)
def extract_se(audio_path: str) -> torch.Tensor:
"""
Extract the speaker embedding (tone colour) from *audio_path*.
Returns a torch.Tensor that can be saved with torch.save().
"""
_ensure_loaded()
return _converter.extract_se(audio_path)
def transfer_voice(
source_wav_bytes: bytes,
target_se: torch.Tensor,
tau: float = 0.3,
) -> bytes:
"""
Stamp *target_se* (parent's voice) onto *source_wav_bytes* (MMS output).
*tau* controls transfer strength (0.0 = source unchanged, 1.0 = maximum
speaker similarity). Default 0.3 balances naturalness vs. speaker match.
Returns WAV bytes with the parent's voice characteristics applied.
"""
_ensure_loaded()
# Write MMS output to a temp file
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as src_f:
src_f.write(source_wav_bytes)
src_path = src_f.name
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as out_f:
out_path = out_f.name
try:
# Extract speaker embedding from MMS source audio
src_se = _converter.extract_se(src_path)
_converter.convert(
audio_src_path=src_path,
src_se=src_se,
tgt_se=target_se,
output_path=out_path,
tau=tau,
)
with open(out_path, "rb") as f:
return f.read()
finally:
for p in (src_path, out_path):
try:
os.unlink(p)
except OSError:
pass