"use client"; import { useState, useEffect } from "react"; import { Card } from "@/shared/components"; import { AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers"; import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders"; import { translate } from "@/i18n/runtime"; import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js"; import { GOOGLE_TTS_LANGUAGES } from "open-sse/config/googleTtsLanguages.js"; import { Row } from "./exampleShared"; const DEFAULT_TTS_RESPONSE_EXAMPLE = `// Audio will appear here after running. // Example JSON response (response_format=json): { "format": "mp3", "audio": "//NExAANaAIIAUAAANNNNNNNN..." // base64 encoded MP3 }`; export function TtsExampleCard({ providerId }) { const providerAlias = getProviderAlias(providerId); const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"]; // Voice state const [selectedVoice, setSelectedVoice] = useState(config.defaultVoiceId || ""); const [selectedVoiceName, setSelectedVoiceName] = useState(""); const [voiceId, setVoiceId] = useState(config.defaultVoiceId || ""); // editable voice id (elevenlabs/config providers) // Voices shown below Voice row after language selected const [countryVoices, setCountryVoices] = useState([]); const [selectedLang, setSelectedLang] = useState(""); const [selectedModel, setSelectedModel] = useState(() => { const cfgModels = AI_PROVIDERS[providerId]?.ttsConfig?.models; if (cfgModels?.length) return cfgModels[0].id; if (config.hasModelSelector && config.modelKey) { const models = getModelsByProviderId(config.modelKey); return models?.[0]?.id || ""; } return ""; }); // Form state const [input, setInput] = useState("Hello, this is a text to speech test."); const [style, setStyle] = useState(""); // style/voice instructions (e.g. MiMo voicedesign) const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json const [audioUrl, setAudioUrl] = useState(""); const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [latency, setLatency] = useState(null); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); // Country picker modal state const [modalOpen, setModalOpen] = useState(false); const [languages, setLanguages] = useState([]); const [modalLoading, setModalLoading] = useState(false); const [modalSearch, setModalSearch] = useState(""); const [modalError, setModalError] = useState(""); const [byLang, setByLang] = useState({}); // Language hint (e.g. Gemini/MiMo): guides the spoken language without affecting voice selection const [languageHint, setLanguageHint] = useState(""); // Number of stored provider connections (shown when no dashboard API key) const [connectionCount, setConnectionCount] = useState(0); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/providers", { cache: "no-store" }) .then((r) => r.json()) .then((d) => { setConnectionCount((d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false).length); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); // Pre-select default voice based on provider config if (config.voiceSource === "hardcoded") { const defaultModel = config.hasModelSelector && config.modelKey ? (getModelsByProviderId(config.modelKey)?.[0]?.id || "") : ""; // Use per-model voices if available, else flat list const voices = (config.voicesPerModel && defaultModel) ? (getTtsVoicesForModel(providerId, defaultModel) || []) : getModelsByProviderId(config.voiceKey || providerId).filter((m) => getModelKind(m) === "tts"); if (voices.length) { if (config.hasBrowseButton) { // Google TTS: pre-select "en" (English) as default, show as single voice chip const defaultVoice = voices.find((v) => v.id === "en") || voices[0]; setSelectedLang(defaultVoice.id); setSelectedVoice(defaultVoice.id); setSelectedVoiceName(defaultVoice.name); setCountryVoices([{ id: defaultVoice.id, name: defaultVoice.name }]); } else { // OpenAI/OpenRouter: set voice chips directly (no language picker) setCountryVoices(voices); setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } } } // api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language // config (nvidia, hyperbolic, deepgram, huggingface, cartesia, playht, coqui, tortoise, inworld, qwen): // use ttsConfig.models for model selector; voice is empty by default (backend uses provider default) }, [providerId]); // Update voices when model changes (voicesPerModel providers) useEffect(() => { if (!config.voicesPerModel || !selectedModel) return; const voices = getTtsVoicesForModel(providerId, selectedModel) || []; setCountryVoices(voices); if (voices.length) { setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } else { // Model has no preset voices (voicedesign/voiceclone) — drop stale voice setSelectedVoice(""); setSelectedVoiceName(""); } }, [selectedModel]); // Open modal — load language list const openModal = async () => { setModalOpen(true); setModalSearch(""); setModalError(""); if (languages.length) return; // already loaded setModalLoading(true); try { if (config.voiceSource === "hardcoded") { // Build languages/byLang from static providerModels data const voiceKey = config.voiceKey || providerId; const voices = getModelsByProviderId(voiceKey).filter((m) => getModelKind(m) === "tts"); const byLangMap = {}; for (const v of voices) { if (!byLangMap[v.id]) byLangMap[v.id] = { code: v.id, name: v.name, voices: [{ id: v.id, name: v.name }] }; } setByLang(byLangMap); setLanguages(Object.values(byLangMap).sort((a, b) => a.name.localeCompare(b.name))); } else { // Use provider-specific apiEndpoint if available, else default to edge-tts voices API const url = config.apiEndpoint ? config.apiEndpoint : `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`; const r = await fetch(url); const d = await r.json(); if (d.error) { setModalError(d.error); return; } setLanguages(d.languages || []); setByLang(d.byLang || {}); } } catch (e) { setModalError(e.message); } finally { setModalLoading(false); } }; // Click language → close modal → show voices below const handlePickLanguage = (lang) => { setModalOpen(false); setSelectedLang(lang.code); const voices = byLang[lang.code]?.voices || []; setCountryVoices(voices); // Auto-select first voice if (voices.length) { setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name); if (config.hasVoiceIdInput) setVoiceId(voices[0].id); } }; const filteredLanguages = modalSearch ? languages.filter((c) => c.name.toLowerCase().includes(modalSearch.toLowerCase()) || c.code.toLowerCase().includes(modalSearch.toLowerCase()) ) : languages; const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; // For ElevenLabs/config-driven: prefer manual voiceId (if any), else fall back to selectedVoice const activeVoiceId = config.hasVoiceIdInput ? (voiceId || selectedVoice) : selectedVoice; const modelFull = (() => { if (config.hasModelSelector && selectedModel && activeVoiceId) return `${providerAlias}/${selectedModel}/${activeVoiceId}`; if (config.hasModelSelector && selectedModel) return `${providerAlias}/${selectedModel}`; if (activeVoiceId) return `${providerAlias}/${activeVoiceId}`; return ""; })(); const ttsBody = (() => { const b = { model: modelFull, input }; if (config.hasLanguageHint && languageHint) b.language = languageHint; if (config.hasStyleInput && style.trim()) b.style = style.trim(); return b; })(); const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ -d '${JSON.stringify(ttsBody)}' \\ ${responseFormat === "json" ? "" : "--output speech.mp3"}`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setAudioUrl(""); setJsonResponse(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ ...ttsBody, input: input.trim() }), }); setLatency(Date.now() - start); if (!res.ok) { const d = await res.json().catch(() => ({})); setError(d?.error?.message || d?.error || `HTTP ${res.status}`); return; } if (responseFormat === "json") { const data = await res.json(); setJsonResponse(data); // Store full JSON response const format = data.format || "mp3"; const audioBlob = await fetch(`data:audio/${format};base64,${data.audio}`).then(r => r.blob()); setAudioUrl(URL.createObjectURL(audioBlob)); } else { const blob = await res.blob(); setAudioUrl(URL.createObjectURL(blob)); } } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; return ( <>

Example

{/* Endpoint + API Key as read-only text */}
{endpoint}/v1/audio/speech {tunnelEndpoint && ( )}
{apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : connectionCount > 0 ? Using stored key(s) · {connectionCount} connection{connectionCount > 1 ? "s" : ""} : No key configured} {/* Model selector — prefer PROVIDER_MODELS[kind=tts], else providerModels via modelKey */} {config.hasModelSelector && (config.modelKey || getModelsByProviderId(providerId).some(m => getModelKind(m) === "tts")) && ( )} {/* Language hint dropdown (Gemini, Xiaomi MiMo) — sends body.language to guide pronunciation */} {config.hasLanguageHint && ( )} {/* Language row + Browse button (edge-tts, local-device, elevenlabs) */} {config.hasBrowseButton && (
)} {/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs/MiMo) */} {countryVoices.length > 0 && (
{countryVoices.map((v) => ( ))}
)} {/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */} {config.hasVoiceIdInput && (
{ setVoiceId(e.target.value); setSelectedVoice(e.target.value); }} placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17" className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> {voiceId && ( )}
)} {/* Google TTS: Language dropdown */} {config.hasLanguageDropdown && ( )} {/* Input */}
setInput(e.target.value)} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Style / voice instructions (Xiaomi MiMo) */} {config.hasStyleInput && (