Feat: Edge-TTS-Engine (native dt. Azure-Stimmen, kein Akzent) + Probe-hören-Knopf

Edge-TTS als 4. Engine (gratis, kein Key, KEIN Cloning → natives Deutsch ohne Akzent — die einzige
Lösung gegen das Akzent-Problem aller Cloning-Engines). /voices listet dt. Edge-Stimmen (weiblich
zuerst, inkl. Gisela). Picker: Engine 'Edge (natürlich · gratis)' + Probe-hören-Knopf (festen Satz je
Engine/Stimme abspielen, ohne reinsprechen). Default bleibt Piper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 00:44:01 +02:00
parent aa62c98247
commit 11f0066b47
7 changed files with 257 additions and 154 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DzmNpOtE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DueAT_Qc.css">
<script type="module" crossorigin src="/assets/index-DesgNIBq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-jNLzkOWh.css">
</head>
<body>
<div id="root"></div>
+48 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react"
import { Upload, Link2, ExternalLink, Check, Sparkles } from "lucide-react"
import { Upload, Link2, ExternalLink, Check, Sparkles, Volume2, Loader2 } from "lucide-react"
import { saveUploadedVrm, loadUploadedVrm } from "@/lib/voice/vrmStore"
// Avatar selbst aussuchen: kuratierte Galerie (öffentliche, CORS-freie VRMs) + eigenes .vrm
@@ -26,7 +26,36 @@ export function AvatarPicker({ avatarUrl, onAvatarChange }: {
const [voices, setVoices] = useState<Voice[]>([])
const [engine, setEngine] = useState(localStorage.getItem("mc_voice_engine") || "piper")
const [voice, setVoice] = useState(localStorage.getItem("mc_voice_voice") || "")
const [previewing, setPreviewing] = useState(false)
const fileRef = useRef<HTMLInputElement>(null)
const previewAudio = useRef<HTMLAudioElement | null>(null)
// „Probe hören": synthetisiert einen festen Satz mit der aktuellen Engine+Stimme und spielt ihn ab.
const playPreview = async () => {
if (previewing) return
setPreviewing(true)
try {
const r = await fetch("/api/voice/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: "Hallo! Ich bin deine Assistentin. So klingt diese Stimme auf Deutsch.",
engine, voice,
}),
})
if (!r.ok) throw new Error(`TTS ${r.status}`)
const url = URL.createObjectURL(await r.blob())
previewAudio.current?.pause()
const a = new Audio(url)
previewAudio.current = a
a.onended = () => URL.revokeObjectURL(url)
await a.play()
} catch (e) {
console.error("Probe fehlgeschlagen:", e)
} finally {
setPreviewing(false)
}
}
// Hochgeladenes VRM nach Reload wiederherstellen.
useEffect(() => {
@@ -70,8 +99,9 @@ export function AvatarPicker({ avatarUrl, onAvatarChange }: {
localStorage.setItem("mc_voice_voice", v)
}
// Feste Reihenfolge der bekannten Engines (ElevenLabs auch ohne Key sichtbar → „Key fehlt"-Hinweis).
const enginesAvail = ["piper", "chatterbox", "elevenlabs"]
// Feste Reihenfolge der bekannten Engines (Edge zuerst = natürlich+gratis; ElevenLabs auch ohne Key
// sichtbar → „Key fehlt"-Hinweis).
const enginesAvail = ["edge", "piper", "chatterbox", "elevenlabs"]
const voicesForEngine = voices.filter((v) => v.engine === engine)
return (
@@ -158,7 +188,7 @@ export function AvatarPicker({ avatarUrl, onAvatarChange }: {
engine === eng ? "border-primary/50 bg-primary/10 text-primary" : "border-border/40 hover:bg-accent"
}`}
>
{eng === "piper" ? "Piper (schnell)" : eng === "chatterbox" ? "Chatterbox (lokal)" : eng === "elevenlabs" ? "ElevenLabs (premium)" : eng}
{eng === "edge" ? "Edge (natürlich · gratis)" : eng === "piper" ? "Piper (lokal)" : eng === "chatterbox" ? "Chatterbox (lokal)" : eng === "elevenlabs" ? "ElevenLabs (premium)" : eng}
</button>
))}
</div>
@@ -172,6 +202,20 @@ export function AvatarPicker({ avatarUrl, onAvatarChange }: {
<option key={v.id} value={v.id}>{v.label}{v.clonable ? " · klonbar" : ""}</option>
))}
</select>
<button
onClick={playPreview}
disabled={previewing}
className="flex w-full items-center justify-center gap-2 rounded-md border border-primary/40 bg-primary/10 px-2.5 py-2 text-xs text-primary hover:bg-primary/20 transition-colors disabled:opacity-60"
>
{previewing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Volume2 className="h-3.5 w-3.5" />}
{previewing ? "Spielt …" : "Probe hören"}
</button>
{engine === "edge" && (
<p className="text-[11px] text-muted-foreground">
Edge-TTS: native deutsche Azure-Stimmen sehr natürlich, kein Akzent, gratis & ohne Key.
Jüngste Stimme: <code>Gisela</code>. Cloud (Text Microsoft).
</p>
)}
{engine === "chatterbox" && (
<p className="text-[11px] text-muted-foreground">
Chatterbox läuft auf CPU erste Antwort kann ein paar Sekunden dauern. Lokal + Voice-Cloning (dt. mit Akzent).