+
{[
- { value: "browser", label: "Browser" },
- { value: "kokoro", label: "Kokoro" },
- ].map(({ value, label }) => (
+ { value: "edge", label: "Edge", desc: "Fast, natural" },
+ { value: "kokoro", label: "Kokoro", desc: "High quality" },
+ { value: "browser", label: "Browser", desc: "Built-in" },
+ ].map(({ value, label, desc }) => (
))}
@@ -267,6 +269,30 @@ export function SettingsPanel({
)}
+ {/* Edge TTS URL */}
+ {ttsSettings.engine === "edge" && (
+
+ )}
+
{/* Kokoro URL */}
{ttsSettings.engine === "kokoro" && (
@@ -286,7 +312,7 @@ export function SettingsPanel({
className="w-full p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)]"
/>
- URL of your Kokoro-FastAPI server
+ High quality but slower. Generates full audio before playing.
)}
diff --git a/src/hooks/useTTS.ts b/src/hooks/useTTS.ts
index 0fa6e22..b884d75 100644
--- a/src/hooks/useTTS.ts
+++ b/src/hooks/useTTS.ts
@@ -114,6 +114,75 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn {
speechSynthesis.speak(utterance);
}, [settings.speed, settings.voice, voices]);
+ const playEdge = useCallback(async () => {
+ setIsLoading(true);
+
+ try {
+ const response = await fetch(`${settings.edgeUrl}/v1/audio/speech`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ model: "tts-1",
+ input: textRef.current,
+ voice: settings.voice || "en-US-AvaNeural",
+ response_format: "mp3",
+ speed: settings.speed,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Edge TTS API error: ${response.status}`);
+ }
+
+ const blob = await response.blob();
+ const url = URL.createObjectURL(blob);
+
+ if (audioRef.current) {
+ audioRef.current.pause();
+ }
+
+ const audio = new Audio(url);
+
+ audio.onloadedmetadata = () => {
+ setDuration(audio.duration);
+ };
+
+ audio.ontimeupdate = () => {
+ setCurrentTime(audio.currentTime);
+ };
+
+ audio.onplay = () => {
+ setIsPlaying(true);
+ setIsPaused(false);
+ setIsLoading(false);
+ };
+
+ audio.onended = () => {
+ setIsPlaying(false);
+ setIsPaused(false);
+ setCurrentPosition(0);
+ setCurrentTime(0);
+ URL.revokeObjectURL(url);
+ };
+
+ audio.onerror = () => {
+ console.error("Audio playback error");
+ setIsPlaying(false);
+ setIsLoading(false);
+ URL.revokeObjectURL(url);
+ };
+
+ audioRef.current = audio;
+ await audio.play();
+ } catch (error) {
+ console.error("Edge TTS error:", error);
+ setIsLoading(false);
+ alert("Failed to connect to Edge TTS. Make sure it's running at " + settings.edgeUrl);
+ }
+ }, [settings.edgeUrl, settings.speed, settings.voice]);
+
const playKokoro = useCallback(async () => {
setIsLoading(true);
@@ -187,10 +256,12 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn {
const play = useCallback(() => {
if (settings.engine === "browser") {
playBrowser();
+ } else if (settings.engine === "edge") {
+ playEdge();
} else {
playKokoro();
}
- }, [settings.engine, playBrowser, playKokoro]);
+ }, [settings.engine, playBrowser, playEdge, playKokoro]);
const pause = useCallback(() => {
if (settings.engine === "browser") {
diff --git a/src/lib/types.ts b/src/lib/types.ts
index f1d179e..b0b982a 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -54,9 +54,10 @@ export interface ReaderSettings {
}
export interface TTSSettings {
- engine: "browser" | "kokoro";
+ engine: "browser" | "edge" | "kokoro";
speed: number; // 0.5-3.0
voice: string;
+ edgeUrl: string;
kokoroUrl: string;
}
@@ -89,8 +90,9 @@ export const defaultReaderSettings: ReaderSettings = {
};
export const defaultTTSSettings: TTSSettings = {
- engine: "browser",
+ engine: "edge",
speed: 1.0,
voice: "",
+ edgeUrl: "http://localhost:5050",
kokoroUrl: "http://localhost:8880",
};