first commit
This commit is contained in:
48
engines/chattts.py
Normal file
48
engines/chattts.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
NovaAi – TTS-Engine-Hub
|
||||
engines/chattts.py
|
||||
Version: v0.0.1
|
||||
|
||||
Description:
|
||||
ChatTTS engine adapter.
|
||||
Implements TTSEngineBase interface for ChatTTS integration (dummy implementation).
|
||||
|
||||
Author: Abby (ChatGPT)
|
||||
Date: 2025-07-23
|
||||
Canvas: chattts.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from engines.engine_base import TTSEngineBase
|
||||
|
||||
class ChatTTSEngine(TTSEngineBase):
|
||||
async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "mp3"):
|
||||
# Dummy implementation: returns an empty string as it doesn't produce a file.
|
||||
print("Warning: ChatTTSEngine.synthesize is a dummy and does not produce audio.")
|
||||
return ""
|
||||
|
||||
def list_models(self):
|
||||
# Dummy implementation
|
||||
return ["chattts-v1", "chattts-v2"]
|
||||
|
||||
def list_voices(self, model: str = None):
|
||||
# Dummy implementation
|
||||
return ["default", "custom1", "custom2"]
|
||||
|
||||
def healthcheck(self):
|
||||
# Dummy implementation
|
||||
return {"status": "ok", "engine": "chattts"}
|
||||
|
||||
async def selftest(self):
|
||||
# Dummy implementation
|
||||
return {"selftest": True, "engine": "chattts"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def main():
|
||||
engine = ChatTTSEngine()
|
||||
print("Selftest:", await engine.selftest())
|
||||
print("Models:", engine.list_models())
|
||||
print("Voices:", engine.list_voices())
|
||||
print("Healthcheck:", engine.healthcheck())
|
||||
|
||||
asyncio.run(main())
|
||||
45
engines/engine_base.py
Normal file
45
engines/engine_base.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
NovaAi – TTS-Engine-Hub
|
||||
engine_base.py
|
||||
Version: v0.0.1
|
||||
|
||||
Description:
|
||||
Abstract base class for all TTS engine modules.
|
||||
Defines the required interface for engine adapters.
|
||||
|
||||
Author: Abby (ChatGPT)
|
||||
Date: 2025-07-23
|
||||
Canvas: engine_base.py
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
|
||||
class TTSEngineBase(ABC):
|
||||
@abstractmethod
|
||||
async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"):
|
||||
"""
|
||||
Asynchronously generate speech audio from text input.
|
||||
Returns path to audio file.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def list_models(self):
|
||||
"""Return a list of available models."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def list_voices(self, model: str = None):
|
||||
"""Return a list of available voices for a model."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def healthcheck(self):
|
||||
"""Return health/status info for this engine."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def selftest(self):
|
||||
"""Asynchronously run internal self-test (basic functionality check)."""
|
||||
raise NotImplementedError
|
||||
1
engines/f5-tts-voices/default.txt
Normal file
1
engines/f5-tts-voices/default.txt
Normal file
@ -0,0 +1 @@
|
||||
Some call me nature, others call me mother nature.
|
||||
143
engines/f5_tts.py
Normal file
143
engines/f5_tts.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
NovaAi – TTS-Engine-Hub
|
||||
f5_tts.py
|
||||
Version: v0.0.2
|
||||
|
||||
Description:
|
||||
F5-TTS engine module.
|
||||
Implements the TTSEngineBase for F5-TTS text-to-speech synthesis.
|
||||
Now with robust speaker handling.
|
||||
|
||||
Author: Your Name (or leave as generated)
|
||||
Date: 2025-12-03
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import torch
|
||||
import torchaudio
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import asyncio
|
||||
from .engine_base import TTSEngineBase
|
||||
from importlib.resources import files
|
||||
|
||||
try:
|
||||
from f5_tts.api import F5TTS
|
||||
except ImportError:
|
||||
print("Warning: F5TTS could not be imported. F5-TTS engine will not be available.")
|
||||
F5TTS = None
|
||||
|
||||
class F5TTSEngine(TTSEngineBase):
|
||||
def __init__(self):
|
||||
# Initialize F5-TTS specific resources, models, etc.
|
||||
print("F5-TTS Engine Initializing...")
|
||||
self.speakers = {}
|
||||
self.model = None
|
||||
|
||||
if F5TTS:
|
||||
try:
|
||||
self.model = F5TTS(model="F5TTS_v1_Base")
|
||||
print("F5-TTS Engine Initialized.")
|
||||
self._load_speakers()
|
||||
except Exception as e:
|
||||
print(f"Error initializing F5-TTS Engine: {e}")
|
||||
self.model = None
|
||||
else:
|
||||
print("F5-TTS Engine not initialized because F5TTS is not available.")
|
||||
|
||||
def _load_speakers(self):
|
||||
# Add the default speaker
|
||||
default_wav = str(files("f5_tts").joinpath("infer/examples/basic/basic_ref_en.wav"))
|
||||
default_txt = "engines/f5-tts-voices/default.txt"
|
||||
if os.path.exists(default_txt):
|
||||
self.speakers["default"] = {"wav": default_wav, "txt": default_txt}
|
||||
|
||||
# Scan for custom speakers
|
||||
voices_dir = "engines/f5-tts-voices"
|
||||
if not os.path.isdir(voices_dir):
|
||||
return
|
||||
for file in os.listdir(voices_dir):
|
||||
if file.endswith(".wav"):
|
||||
speaker_name = file.rsplit('.', 1)[0]
|
||||
wav_path = os.path.join(voices_dir, file)
|
||||
txt_path = os.path.join(voices_dir, f"{speaker_name}.txt")
|
||||
if os.path.exists(txt_path):
|
||||
self.speakers[speaker_name] = {"wav": wav_path, "txt": txt_path}
|
||||
print(f"Found custom speaker: {speaker_name}")
|
||||
|
||||
def _blocking_synthesize(self, text: str, speaker: str, fmt: str):
|
||||
"""The actual blocking synthesis logic."""
|
||||
speaker_data = self.speakers[speaker]
|
||||
ref_file = speaker_data["wav"]
|
||||
|
||||
with open(speaker_data["txt"], 'r') as f:
|
||||
ref_text = f.read()
|
||||
|
||||
print(f"F5-TTS: Synthesizing '{text}' with reference voice from '{ref_file}'.")
|
||||
|
||||
wav, sr, spec = self.model.infer(
|
||||
ref_file=ref_file,
|
||||
ref_text=ref_text,
|
||||
gen_text=text,
|
||||
)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{fmt}") as temp_file:
|
||||
if fmt == "wav":
|
||||
torchaudio.save(temp_file.name, torch.from_numpy(wav).unsqueeze(0), sr, format="wav")
|
||||
else:
|
||||
# Convert to float32 for soundfile
|
||||
wav_float = wav.astype(np.float32) / np.iinfo(wav.dtype).max
|
||||
sf.write(temp_file.name, wav_float, sr)
|
||||
return temp_file.name
|
||||
|
||||
async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"):
|
||||
"""
|
||||
Asynchronously generate speech audio from text input using F5-TTS.
|
||||
"""
|
||||
if not self.model:
|
||||
raise RuntimeError("F5-TTS Engine not initialized.")
|
||||
|
||||
speaker_to_use = speaker if speaker in self.speakers else "default"
|
||||
if speaker and speaker not in self.speakers:
|
||||
print(f"Warning: Speaker '{speaker}' not found. Falling back to default speaker.")
|
||||
|
||||
if speaker_to_use not in self.speakers:
|
||||
raise RuntimeError("No default speaker found for F5-TTS. Please add a 'default.wav' and 'default.txt' to the 'engines/f5-tts-voices' directory.")
|
||||
|
||||
try:
|
||||
# Run the blocking synthesis in a separate thread
|
||||
return await asyncio.to_thread(self._blocking_synthesize, text, speaker_to_use, fmt)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"F5-TTS synthesis failed: {e}")
|
||||
|
||||
def list_models(self):
|
||||
"""Return a list of available F5-TTS models."""
|
||||
if not self.model:
|
||||
return []
|
||||
return ["F5TTS_v1_Base"]
|
||||
|
||||
def list_voices(self, model: str = None):
|
||||
"""Return a list of available F5-TTS voices for a model."""
|
||||
return list(self.speakers.keys())
|
||||
|
||||
def healthcheck(self):
|
||||
"""Return health/status info for F5-TTS engine."""
|
||||
if self.model:
|
||||
return {"status": "ok", "message": "F5-TTS engine is ready"}
|
||||
else:
|
||||
return {"status": "error", "message": "F5-TTS engine failed to initialize"}
|
||||
|
||||
async def selftest(self):
|
||||
"""Run internal self-test for F5-TTS."""
|
||||
if not self.model:
|
||||
return {"status": "failed", "message": "F5-TTS Engine not initialized."}
|
||||
try:
|
||||
# Await the async synthesize method
|
||||
audio_file = await self.synthesize("this is a test.")
|
||||
selftest_passed = os.path.exists(audio_file) and os.path.getsize(audio_file) > 0
|
||||
if selftest_passed:
|
||||
os.remove(audio_file)
|
||||
return {"status": "passed" if selftest_passed else "failed", "message": "F5-TTS self-test successful"}
|
||||
except Exception as e:
|
||||
return {"status": "failed", "message": f"F5-TTS self-test failed: {e}"}
|
||||
163
engines/piper.py
Normal file
163
engines/piper.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""
|
||||
NovaAi – TTS-Engine-Hub
|
||||
engines/piper.py
|
||||
Version: v0.1.1
|
||||
|
||||
Description:
|
||||
Piper TTS engine adapter: real CLI invocation + output as WAV, OGG, or MP3.
|
||||
Synthesizes WAV via Piper, converts to OGG/MP3 via ffmpeg-python if needed.
|
||||
Uses dynamic model path: ./models/piper/[model]/model.onnx
|
||||
|
||||
Author: Abby (ChatGPT)
|
||||
Date: 2025-07-23
|
||||
Canvas: piper.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
from engines.engine_base import TTSEngineBase
|
||||
import ffmpeg
|
||||
|
||||
class PiperEngine(TTSEngineBase):
|
||||
def __init__(self):
|
||||
self.piper_executable = shutil.which("piper")
|
||||
self.ffmpeg_executable = shutil.which("ffmpeg")
|
||||
|
||||
def _load_config(self, model: str):
|
||||
"""Load the model config JSON file to get speaker mappings."""
|
||||
model_dir = f"./models/piper/{model}"
|
||||
config_file = os.path.join(model_dir, f"{model}.onnx.json")
|
||||
if os.path.isfile(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _get_speaker_id(self, speaker: str, model: str):
|
||||
"""Convert speaker name to speaker ID using the model's config."""
|
||||
if not speaker or speaker == "default":
|
||||
return None
|
||||
if speaker.isdigit():
|
||||
return speaker
|
||||
config = self._load_config(model)
|
||||
speaker_id_map = config.get('speaker_id_map', {})
|
||||
return str(speaker_id_map.get(speaker))
|
||||
|
||||
def _run_ffmpeg_blocking(self, input_path, output_path):
|
||||
"""Wrapper for the blocking ffmpeg call."""
|
||||
(
|
||||
ffmpeg
|
||||
.input(input_path)
|
||||
.output(output_path)
|
||||
.run(overwrite_output=True, quiet=True)
|
||||
)
|
||||
|
||||
async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"):
|
||||
if not self.piper_executable:
|
||||
raise RuntimeError("Piper executable not found. Please install it and ensure it's in your PATH.")
|
||||
if not model:
|
||||
raise ValueError("Model must be specified for Piper.")
|
||||
model_dir = f"./models/piper/{model}"
|
||||
model_file = os.path.join(model_dir, f"{model}.onnx")
|
||||
if not os.path.isfile(model_file):
|
||||
raise FileNotFoundError(f"Piper model not found: {model_file}")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", prefix="piper_", delete=False) as wav_file:
|
||||
output_wav_path = wav_file.name
|
||||
|
||||
cmd = [self.piper_executable, "--model", model_file, "--output_file", output_wav_path, "--stdin_text"]
|
||||
|
||||
if speaker:
|
||||
speaker_id = self._get_speaker_id(speaker, model)
|
||||
if speaker_id:
|
||||
cmd += ["--speaker", speaker_id]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate(input=text.encode('utf-8'))
|
||||
|
||||
if process.returncode != 0:
|
||||
os.remove(output_wav_path)
|
||||
raise RuntimeError(f"Piper synth failed: {stderr.decode()}")
|
||||
|
||||
fmt = (fmt or "ogg").lower()
|
||||
if fmt == "wav":
|
||||
return output_wav_path
|
||||
|
||||
if not self.ffmpeg_executable:
|
||||
os.remove(output_wav_path)
|
||||
raise RuntimeError("ffmpeg not found, cannot convert audio format.")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=f'.{fmt}', prefix="piper_conv_", delete=False) as converted_file:
|
||||
output_other_path = converted_file.name
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(self._run_ffmpeg_blocking, output_wav_path, output_other_path)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"ffmpeg conversion failed: {e}")
|
||||
finally:
|
||||
os.remove(output_wav_path)
|
||||
|
||||
return output_other_path
|
||||
|
||||
def list_models(self):
|
||||
models_dir = "./models/piper/"
|
||||
if not os.path.isdir(models_dir):
|
||||
return []
|
||||
return [name for name in os.listdir(models_dir)
|
||||
if os.path.isdir(os.path.join(models_dir, name))]
|
||||
|
||||
def list_voices(self, model: str = None):
|
||||
if not model:
|
||||
return ["default"]
|
||||
config = self._load_config(model)
|
||||
speaker_id_map = config.get('speaker_id_map', {})
|
||||
if speaker_id_map:
|
||||
return ["default"] + sorted(speaker_id_map.keys())
|
||||
return ["default"]
|
||||
|
||||
def healthcheck(self):
|
||||
status = "ok"
|
||||
if not self.piper_executable:
|
||||
status = "missing_piper_executable"
|
||||
return {"status": status, "engine": "piper"}
|
||||
|
||||
async def selftest(self):
|
||||
if not self.piper_executable:
|
||||
return {"selftest": False, "error": "Piper executable not found.", "engine": "piper"}
|
||||
try:
|
||||
models = self.list_models()
|
||||
if not models:
|
||||
return {"selftest": False, "error": "No Piper models found.", "engine": "piper"}
|
||||
|
||||
test_text = "This is a selftest."
|
||||
first_model = models[0]
|
||||
voices = self.list_voices(first_model)
|
||||
test_voice = voices[0] if voices else None
|
||||
|
||||
audio_file = await self.synthesize(test_text, speaker=test_voice, model=first_model, fmt="wav")
|
||||
|
||||
selftest_passed = os.path.exists(audio_file) and os.path.getsize(audio_file) > 0
|
||||
if selftest_passed:
|
||||
os.remove(audio_file)
|
||||
|
||||
return {"selftest": selftest_passed, "models": models, "engine": "piper"}
|
||||
except Exception as e:
|
||||
return {"selftest": False, "error": str(e), "engine": "piper"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def main():
|
||||
engine = PiperEngine()
|
||||
print("Selftest:", await engine.selftest())
|
||||
print("Models:", engine.list_models())
|
||||
print("Voices:", engine.list_voices(engine.list_models()[0]))
|
||||
print("Healthcheck:", engine.healthcheck())
|
||||
|
||||
asyncio.run(main())
|
||||
48
engines/styletts.py
Normal file
48
engines/styletts.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
NovaAi – TTS-Engine-Hub
|
||||
engines/styletts.py
|
||||
Version: v0.0.1
|
||||
|
||||
Description:
|
||||
StyleTTS engine adapter.
|
||||
Implements TTSEngineBase interface for StyleTTS integration (dummy implementation).
|
||||
|
||||
Author: Abby (ChatGPT)
|
||||
Date: 2025-07-23
|
||||
Canvas: styletts.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from engines.engine_base import TTSEngineBase
|
||||
|
||||
class StyleTTSEngine(TTSEngineBase):
|
||||
async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "mp3"):
|
||||
# Dummy implementation: returns an empty string as it doesn't produce a file.
|
||||
print("Warning: StyleTTSEngine.synthesize is a dummy and does not produce audio.")
|
||||
return ""
|
||||
|
||||
def list_models(self):
|
||||
# Dummy implementation
|
||||
return ["styletts_v2_de", "styletts_v2_en"]
|
||||
|
||||
def list_voices(self, model: str = None):
|
||||
# Dummy implementation
|
||||
return ["neutral", "emotional", "female"]
|
||||
|
||||
def healthcheck(self):
|
||||
# Dummy implementation
|
||||
return {"status": "ok", "engine": "styletts"}
|
||||
|
||||
async def selftest(self):
|
||||
# Dummy implementation
|
||||
return {"selftest": True, "engine": "styletts"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def main():
|
||||
engine = StyleTTSEngine()
|
||||
print("Selftest:", await engine.selftest())
|
||||
print("Models:", engine.list_models())
|
||||
print("Voices:", engine.list_voices())
|
||||
print("Healthcheck:", engine.healthcheck())
|
||||
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user