# Thorsten-Voice CosyVoice3 Space # Basiert auf FunAudioLLM/Fun-CosyVoice3-0.5B (Apache 2.0) import spaces import os import sys import random import gradio as gr import numpy as np import torch import pyloudnorm as pyln ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.join(ROOT_DIR, "third_party", "Matcha-TTS")) from huggingface_hub import snapshot_download, hf_hub_download # --------------------------------------------------------------------------- # Modell-Download beim Start # --------------------------------------------------------------------------- MODEL_DIR = "pretrained_models/CosyVoice3-0.5B" print("Lade Basis-Modell ...") snapshot_download("FunAudioLLM/Fun-CosyVoice3-0.5B-2512", local_dir=MODEL_DIR) print("Lade Thorsten-Voice Finetune-Gewichte ...") for filename in ["llm.pt", "flow.pt", "spk2info.pt"]: hf_hub_download(repo_id="Thorsten-Voice/CosyVoice3", filename=filename, local_dir=MODEL_DIR) from cosyvoice.cli.cosyvoice import CosyVoice3 from cosyvoice.utils.common import set_all_random_seed from german_transliterate.core import GermanTransliterate # --------------------------------------------------------------------------- # Konstanten # --------------------------------------------------------------------------- TARGET_SR = 24000 MAX_TEXT_LEN = 2000 SPEAKER = "thorsten" FIXED_SEED = 42 # Lautstärkenormalisierung (EBU R128) TRUE_PEAK_DB = -1.5 transliterator = GermanTransliterate(replace={';': ',', ':': ','}, sep_abbreviation=' -- ') meter = pyln.Meter(TARGET_SR) # EBU R128 Meter # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- def normalize_text(text: str) -> str: return transliterator.transliterate(text) def loudnorm(audio: np.ndarray, lufs_target: float) -> np.ndarray: """EBU R128 Lautstärkenormalisierung (True Peak -1.5 dBFS).""" loudness = meter.integrated_loudness(audio) # Nur normalisieren wenn Messung sinnvoll (nicht Stille) if np.isinf(loudness) or np.isnan(loudness): return audio normalized = pyln.normalize.loudness(audio, loudness, lufs_target) # True Peak begrenzen peak = np.max(np.abs(normalized)) tp_linear = 10 ** (TRUE_PEAK_DB / 20) if peak > tp_linear: normalized = normalized / peak * tp_linear return normalized # --------------------------------------------------------------------------- # Inferenz # --------------------------------------------------------------------------- @spaces.GPU def generate_audio(tts_text: str, use_transliterate: bool, use_loudnorm: bool, lufs_target: float, random_variance: bool): default_audio = (TARGET_SR, np.zeros(TARGET_SR, dtype=np.float32)) tts_text = tts_text.strip() if not tts_text: gr.Warning("Bitte gib einen Text ein.") return default_audio, "" if len(tts_text) > MAX_TEXT_LEN: gr.Warning(f"Der Text ist zu lang (max. {MAX_TEXT_LEN} Zeichen).") return default_audio, "" normalized = normalize_text(tts_text) if use_transliterate else tts_text seed = random.randint(1, 100_000_000) if random_variance else FIXED_SEED set_all_random_seed(seed) chunks = [] for chunk in cosyvoice.inference_sft(normalized, SPEAKER, stream=False, speed=1.0): chunks.append(chunk["tts_speech"]) audio = torch.concat(chunks, dim=1).numpy().flatten().astype(np.float64) if use_loudnorm: audio = loudnorm(audio, lufs_target) return (TARGET_SR, audio.astype(np.float32)), normalized # --------------------------------------------------------------------------- # CSS (Corporate Design) # --------------------------------------------------------------------------- CSS = """ :root { --tv-dark: #515f7f; --tv-light: #91a0bf; --tv-yellow:#ffc038; } body, .gradio-container { background-color: #f5f7fa !important; font-family: 'Segoe UI', Arial, sans-serif !important; } .tv-header { background: linear-gradient(135deg, var(--tv-dark) 0%, var(--tv-light) 100%); border-radius: 12px; padding: 24px 28px 18px 28px; margin-bottom: 8px; color: white !important; } .tv-header h1 { color: white !important; margin: 0 0 6px 0; font-size: 1.6rem; } .tv-header p { color: #dde3ef !important; margin: 0; font-size: 0.92rem; } .tv-header a { color: var(--tv-yellow) !important; text-decoration: none; } .tv-header a:hover { text-decoration: underline; } button.primary { background: var(--tv-yellow) !important; color: #1a1a1a !important; border: none !important; font-weight: 700 !important; border-radius: 8px !important; } button.primary:hover { background: #e6a800 !important; } label span { color: var(--tv-dark) !important; font-weight: 600; } .options-row { align-items: center; gap: 8px; } .normalized-box textarea { background: #eef1f7 !important; color: #3a4560 !important; font-size: 0.88rem !important; } .audio-out { border-top: 3px solid var(--tv-yellow) !important; border-radius: 8px; } """ # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- def build_ui(): with gr.Blocks(css=CSS, title="Thorsten-Voice · CosyVoice3") as demo: gr.HTML("""
Deutsches Text-to-Speech · Finetune von FunAudioLLM/Fun-CosyVoice3-0.5B auf dem Thorsten-Voice Datensatz · Modell: Thorsten-Voice/CosyVoice3