Spaces:
Running on Zero
Running on Zero
| # 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 | |
| # --------------------------------------------------------------------------- | |
| 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(""" | |
| <div class="tv-header"> | |
| <h1>🎙️ Thorsten-Voice · CosyVoice3</h1> | |
| <p> | |
| Deutsches Text-to-Speech · | |
| Finetune von <a href="https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B-2512" target="_blank">FunAudioLLM/Fun-CosyVoice3-0.5B</a> | |
| auf dem <a href="https://huggingface.co/thorsten-voice" target="_blank">Thorsten-Voice Datensatz</a> | |
| · | |
| Modell: <a href="https://huggingface.co/Thorsten-Voice/CosyVoice3" target="_blank">Thorsten-Voice/CosyVoice3</a> | |
| </p> | |
| </div> | |
| """) | |
| tts_text = gr.Textbox( | |
| label=f"Text eingeben (max. {MAX_TEXT_LEN} Zeichen)", | |
| placeholder="Hallo, ich bin Thorsten. Schön, dass du da bist.", | |
| lines=5, | |
| value="Hallo, ich bin Thorsten. Schön, dass du da bist.", | |
| max_lines=12, | |
| ) | |
| with gr.Row(elem_classes=["options-row"]): | |
| use_transliterate = gr.Checkbox( | |
| label="Text normalisieren", | |
| value=False, | |
| info="Wandelt Zahlen, Abkürzungen und Sonderzeichen in ausgeschriebene Wörter um (z. B. '3 km' → 'drei Kilometer'). Empfohlen bei Texten mit Zahlen oder Fachbegriffen. Kann sonst bei Zahlen zu englischer Aussprache kommen.", | |
| ) | |
| use_loudnorm = gr.Checkbox( | |
| label="Lautstärke normalisieren", | |
| value=True, | |
| info="Gleicht die Lautstärke auf einen einheitlichen Pegel an (EBU R128). Verhindert übersteuerte oder zu leise Ausgaben.", | |
| ) | |
| random_variance = gr.Checkbox( | |
| label="Zufällige Varianz", | |
| value=False, | |
| info="Verwendet bei jeder Generierung einen anderen Zufallswert. Die Aussprache variiert dadurch leicht – hilfreich wenn eine Ausgabe nicht gefällt.", | |
| ) | |
| lufs_slider = gr.Slider( | |
| minimum=-30, | |
| maximum=-10, | |
| value=-23, | |
| step=1, | |
| label="Ziellautstärke (−10 dB = sehr laut · −23 dB = Broadcast-Standard · −30 dB = leise)", | |
| visible=True, | |
| ) | |
| generate_button = gr.Button("🔊 Audio generieren", variant="primary") | |
| audio_output = gr.Audio( | |
| label="Synthetisiertes Audio", | |
| autoplay=True, | |
| streaming=False, | |
| elem_classes=["audio-out"], | |
| ) | |
| normalized_text = gr.Textbox( | |
| label="Normalisierter Text (nach german_transliterate)", | |
| interactive=False, | |
| lines=3, | |
| elem_classes=["normalized-box"], | |
| visible=False, | |
| ) | |
| # Slider nur anzeigen wenn Loudnorm aktiv | |
| use_loudnorm.change( | |
| fn=lambda x: gr.update(visible=x), | |
| inputs=[use_loudnorm], | |
| outputs=[lufs_slider], | |
| ) | |
| # Normalisierter Text nur anzeigen wenn Transliterate aktiv | |
| use_transliterate.change( | |
| fn=lambda x: gr.update(visible=x), | |
| inputs=[use_transliterate], | |
| outputs=[normalized_text], | |
| ) | |
| generate_button.click( | |
| fn=generate_audio, | |
| inputs=[tts_text, use_transliterate, use_loudnorm, lufs_slider, random_variance], | |
| outputs=[audio_output, normalized_text], | |
| ) | |
| return demo | |
| # --------------------------------------------------------------------------- | |
| # Hauptprogramm | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| print("Initialisiere CosyVoice3 ...") | |
| cosyvoice = CosyVoice3(MODEL_DIR) | |
| # Instruct-Prompt Patch — verhindert Halluzinationen bei SFT-Inferenz | |
| original_frontend_sft = cosyvoice.frontend.frontend_sft | |
| def patched_frontend_sft(tts_text, spk_id): | |
| model_input = original_frontend_sft(tts_text, spk_id) | |
| instruct = 'You are a helpful assistant.<|endofprompt|>' | |
| prompt_token, prompt_token_len = cosyvoice.frontend._extract_text_token(instruct) | |
| model_input['prompt_text'] = prompt_token | |
| model_input['prompt_text_len'] = prompt_token_len | |
| return model_input | |
| cosyvoice.frontend.frontend_sft = patched_frontend_sft | |
| demo = build_ui() | |
| demo.queue(default_concurrency_limit=2).launch() | |