CosyVoice / app.py
Thorsten-Voice's picture
Update app.py
0f2a762 verified
Raw
History Blame Contribute Delete
9.77 kB
# 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("""
<div class="tv-header">
<h1>🎙️ Thorsten-Voice &nbsp;·&nbsp; CosyVoice3</h1>
<p>
Deutsches Text-to-Speech &nbsp;·&nbsp;
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>
&nbsp;·&nbsp;
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()