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:
+1
-1
File diff suppressed because one or more lines are too long
+141
-141
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user