mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 12:42:11 -03:00
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
import os
|
|
import requests
|
|
import random
|
|
|
|
class Everything(str):
|
|
def __ne__(self, __value: object) -> bool:
|
|
return False
|
|
|
|
VOICE_OPTIONS = {
|
|
"af_bella": "Bella (American Female) - af_bella",
|
|
"af_nicole": "Nicole (American Female) - af_nicole",
|
|
"af_sarah": "Sarah (American Female) - af_sarah",
|
|
"af_sky": "Sky (American Female) - af_sky",
|
|
"af": "Default (American Female) - af",
|
|
"am_adam": "Adam (American Male) - am_adam",
|
|
"am_michael": "Michael (American Male) - am_michael",
|
|
"bf_emma": "Emma (British Female) - bf_emma",
|
|
"bf_isabella": "Isabella (British Female) - bf_isabella",
|
|
"bm_george": "George (British Male) - bm_george",
|
|
"bm_lewis": "Lewis (British Male) - bm_lewis"
|
|
}
|
|
|
|
# Create a reversed mapping for display to value
|
|
VOICE_DISPLAY_TO_VALUE = {v: k for k, v in VOICE_OPTIONS.items()}
|
|
|
|
LANGUAGE_OPTIONS = {
|
|
"en-us": "English (US)",
|
|
"en-gb": "English (UK)",
|
|
"fr-fr": "French",
|
|
"ja": "Japanese",
|
|
"ko": "Korean",
|
|
"cmn": "Chinese (Mandarin)"
|
|
}
|
|
|
|
def download_if_not_exists(url, dest_path):
|
|
"""Download a file from a URL if it doesn't already exist."""
|
|
if not os.path.exists(dest_path):
|
|
print(f"Downloading {os.path.basename(dest_path)}...")
|
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
response = requests.get(url, stream=True)
|
|
response.raise_for_status()
|
|
with open(dest_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
print(f"Downloaded {os.path.basename(dest_path)}")
|
|
|
|
class KokoroTTS:
|
|
BASE_DIR = "Bjornulf/Kokoro"
|
|
MODEL_FILE = os.path.join(BASE_DIR, "kokoro-v0_19.onnx")
|
|
VOICES_FILE = os.path.join(BASE_DIR, "voices.bin")
|
|
|
|
VOICE_LANGUAGES = {
|
|
'af': 'en-us', 'am': 'en-us', 'bf': 'en-gb', 'bm': 'en-gb'
|
|
}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {"multiline": True}),
|
|
"voice": (list(VOICE_OPTIONS.values()), {"default": "Default (American Female) - af"}),
|
|
"language": (list(LANGUAGE_OPTIONS.keys()), {"default": "en-us"}),
|
|
"speed": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.1}),
|
|
"autoplay": ("BOOLEAN", {"default": True}),
|
|
"save_audio": ("BOOLEAN", {"default": True}),
|
|
"overwrite": ("BOOLEAN", {"default": False}),
|
|
"seed": ("INT", {"default": 0}),
|
|
},
|
|
"optional": {
|
|
"connect_to_workflow": (Everything("*"), {"forceInput": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("AUDIO", "STRING", "STRING", "FLOAT")
|
|
RETURN_NAMES = ("AUDIO", "audio_path", "audio_full_path", "audio_duration")
|
|
FUNCTION = "generate_audio"
|
|
CATEGORY = "Bjornulf/Kokoro"
|
|
|
|
def generate_audio(self, text: str, voice: str, language: str, speed: float,
|
|
autoplay: bool, save_audio: bool,
|
|
overwrite: bool, seed: int, connect_to_workflow: any = None):
|
|
random.seed(seed)
|
|
|
|
config = {
|
|
"model_path": self.MODEL_FILE,
|
|
"voices_path": self.VOICES_FILE,
|
|
"speed": speed,
|
|
"language": language
|
|
}
|
|
|
|
download_if_not_exists(
|
|
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/kokoro-v0_19.onnx",
|
|
config["model_path"]
|
|
)
|
|
download_if_not_exists(
|
|
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/voices.bin",
|
|
config["voices_path"]
|
|
)
|
|
|
|
try:
|
|
from kokoro_onnx import Kokoro
|
|
import soundfile as sf
|
|
import torch
|
|
import numpy as np
|
|
from pydub import AudioSegment
|
|
from pydub.playback import play
|
|
|
|
voice_id = VOICE_DISPLAY_TO_VALUE[voice]
|
|
kokoro = Kokoro(config["model_path"], config["voices_path"])
|
|
|
|
# Check if file exists and overwrite is False
|
|
sanitized_text = ''.join(c if c.isalnum() else '_' for c in text[:50])
|
|
save_path = os.path.join("Bjornulf_TTS_Kokoro", voice_id, f"{sanitized_text}.wav")
|
|
full_path = os.path.abspath(save_path)
|
|
|
|
if os.path.exists(full_path) and not overwrite:
|
|
print(f"File exists: {full_path}. Loading existing audio.")
|
|
samples, sample_rate = sf.read(full_path)
|
|
if autoplay:
|
|
audio_segment = AudioSegment.from_file(full_path)
|
|
play(audio_segment)
|
|
else:
|
|
# Generate new audio
|
|
samples, sample_rate = kokoro.create(
|
|
text,
|
|
voice=voice_id,
|
|
speed=config["speed"],
|
|
lang=language
|
|
)
|
|
|
|
if save_audio:
|
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
sf.write(full_path, samples, sample_rate)
|
|
|
|
if autoplay:
|
|
try:
|
|
audio_segment = AudioSegment(
|
|
samples.tobytes(),
|
|
frame_rate=sample_rate,
|
|
sample_width=samples.dtype.itemsize,
|
|
channels=1
|
|
)
|
|
play(audio_segment)
|
|
except Exception as e:
|
|
print(f"Autoplay error: {e}")
|
|
|
|
audio_tensor = torch.from_numpy(samples).unsqueeze(0)
|
|
audio_output = {"waveform": audio_tensor.unsqueeze(0), "sample_rate": sample_rate}
|
|
return (audio_output, save_path, full_path, len(samples) / sample_rate)
|
|
|
|
except Exception as e:
|
|
print(f"Error in Kokoro TTS: {e}")
|
|
return ({"waveform": torch.zeros(1, 1, 1), "sample_rate": 22050}, "", "", 0.0)
|