Feat : tts

This commit is contained in:
decolua
2026-04-10 10:17:53 +07:00
parent 39545cf4c8
commit 3c96e8d6d1
40 changed files with 1896 additions and 147 deletions
+33 -1
View File
@@ -8,11 +8,43 @@ import ProviderIcon from "@/shared/components/ProviderIcon";
import { ThemeToggle, LanguageSwitcher } from "@/shared/components";
import NineRemoteButton from "@/shared/components/NineRemoteButton";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS } from "@/shared/constants/providers";
import { translate } from "@/i18n/runtime";
const getPageInfo = (pathname) => {
if (!pathname) return { title: "", description: "", breadcrumbs: [] };
// Media provider detail: /dashboard/media-providers/[kind]/[id]
const mediaDetailMatch = pathname.match(/\/media-providers\/([^/]+)\/([^/]+)$/);
if (mediaDetailMatch) {
const kindId = mediaDetailMatch[1];
const providerId = mediaDetailMatch[2];
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kindId);
const provider = AI_PROVIDERS[providerId];
return {
title: provider?.name || providerId,
description: "",
breadcrumbs: [
{ label: "Media Providers", href: `/dashboard/media-providers/${kindId}` },
{ label: kindConfig?.label || kindId, href: `/dashboard/media-providers/${kindId}` },
{ label: provider?.name || providerId, image: `/providers/${providerId}.png` },
],
};
}
// Media provider kind: /dashboard/media-providers/[kind]
const mediaKindMatch = pathname.match(/\/media-providers\/([^/]+)$/);
if (mediaKindMatch) {
const kindId = mediaKindMatch[1];
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kindId);
return {
title: kindConfig?.label || kindId,
description: `Manage your ${kindConfig?.label || kindId} providers`,
icon: kindConfig?.icon || "perm_media",
breadcrumbs: [],
};
}
// Provider detail page: /dashboard/providers/[id]
const providerMatch = pathname.match(/\/providers\/([^/]+)$/);
if (providerMatch) {
@@ -34,7 +66,7 @@ const getPageInfo = (pathname) => {
}
}
if (pathname.includes("/providers"))
if (pathname.includes("/providers") && !pathname.includes("/media-providers"))
return {
title: "Providers",
description: "Manage your AI provider connections",
+40 -38
View File
@@ -10,6 +10,8 @@ import { MEDIA_PROVIDER_KINDS } from "@/shared/constants/providers";
import Button from "./Button";
import { ConfirmModal } from "./Modal";
const VISIBLE_MEDIA_KINDS = ["embedding", "tts"];
const navItems = [
{ href: "/dashboard/endpoint", label: "Endpoint", icon: "api" },
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
@@ -134,49 +136,49 @@ export default function Sidebar({ onClose }) {
</Link>
))}
{/* Media Providers accordion */}
{/* <button
onClick={() => setMediaOpen((v) => !v)}
className={cn(
"w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
pathname.startsWith("/dashboard/media-providers")
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]">perm_media</span>
<span className="text-sm font-medium flex-1 text-left">Media Providers</span>
<span className="material-symbols-outlined text-[14px] transition-transform" style={{ transform: mediaOpen ? "rotate(180deg)" : "rotate(0deg)" }}>
expand_more
</span>
</button> */}
{mediaOpen && (
<div className="pl-4">
{MEDIA_PROVIDER_KINDS.map((kind) => (
<Link
key={kind.id}
href={`/dashboard/media-providers/${kind.id}`}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-1.5 rounded-lg transition-all group",
pathname.startsWith(`/dashboard/media-providers/${kind.id}`)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[16px]">{kind.icon}</span>
<span className="text-sm">{kind.label}</span>
</Link>
))}
</div>
)}
{/* System section */}
<div className="pt-4 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
System
</p>
{/* Media Providers accordion */}
<button
onClick={() => setMediaOpen((v) => !v)}
className={cn(
"w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
pathname.startsWith("/dashboard/media-providers")
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]">perm_media</span>
<span className="text-sm font-medium flex-1 text-left">Media Providers</span>
<span className="material-symbols-outlined text-[14px] transition-transform" style={{ transform: mediaOpen ? "rotate(180deg)" : "rotate(0deg)" }}>
expand_more
</span>
</button>
{mediaOpen && (
<div className="pl-4">
{MEDIA_PROVIDER_KINDS.filter((k) => VISIBLE_MEDIA_KINDS.includes(k.id)).map((kind) => (
<Link
key={kind.id}
href={`/dashboard/media-providers/${kind.id}`}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-1.5 rounded-lg transition-all group",
pathname.startsWith(`/dashboard/media-providers/${kind.id}`)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[16px]">{kind.icon}</span>
<span className="text-sm">{kind.label}</span>
</Link>
))}
</div>
)}
{systemItems.map((item) => (
<Link
key={item.href}
+12 -6
View File
@@ -18,7 +18,7 @@ export const FREE_TIER_PROVIDERS = {
nvidia: { id: "nvidia", alias: "nvidia", name: "NVIDIA NIM", icon: "developer_board", color: "#76B900", textIcon: "NV", website: "https://developer.nvidia.com/nim", notice: { text: "Free access for NVIDIA Developer Program members (prototyping & testing).", apiKeyUrl: "https://build.nvidia.com/settings/api-keys" } },
ollama: { id: "ollama", alias: "ollama", name: "Ollama Cloud", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com", notice: { text: "Free tier: light usage, 1 cloud model at a time (limits reset every 5h & 7d). Pro $20/mo · Max $100/mo.", apiKeyUrl: "https://ollama.com/settings/keys" } },
vertex: { id: "vertex", alias: "vx", name: "Vertex AI", icon: "cloud", color: "#4285F4", textIcon: "VX", website: "https://cloud.google.com/vertex-ai", notice: { text: "New Google Cloud accounts get $300 free credits. Requires GCP project + Service Account with Vertex AI API enabled.", apiKeyUrl: "https://console.cloud.google.com/iam-admin/serviceaccounts" } },
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev" },
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev", serviceKinds: ["llm", "embedding"] },
};
// OAuth Providers
@@ -62,11 +62,14 @@ export const APIKEY_PROVIDERS = {
assemblyai: { id: "assemblyai", alias: "aai", name: "AssemblyAI", icon: "record_voice_over", color: "#0062FF", textIcon: "AA", website: "https://assemblyai.com", serviceKinds: ["stt"] },
nanobanana: { id: "nanobanana", alias: "nb", name: "NanoBanana", icon: "image", color: "#FFD700", textIcon: "NB", website: "https://nanobananaapi.ai", serviceKinds: ["image"] },
elevenlabs: { id: "elevenlabs", alias: "el", name: "ElevenLabs", icon: "record_voice_over", color: "#6C47FF", textIcon: "EL", website: "https://elevenlabs.io", serviceKinds: ["tts"] },
cartesia: { id: "cartesia", alias: "cartesia", name: "Cartesia", icon: "spatial_audio", color: "#FF4F8B", textIcon: "CA", website: "https://cartesia.ai", serviceKinds: ["tts"] },
playht: { id: "playht", alias: "playht", name: "PlayHT", icon: "play_circle", color: "#00B4D8", textIcon: "PH", website: "https://play.ht", serviceKinds: ["tts"] },
cartesia: { id: "cartesia", alias: "cartesia", name: "Cartesia", icon: "spatial_audio", color: "#FF4F8B", textIcon: "CA", website: "https://cartesia.ai", serviceKinds: ["tts"], hidden: true },
playht: { id: "playht", alias: "playht", name: "PlayHT", icon: "play_circle", color: "#00B4D8", textIcon: "PH", website: "https://play.ht", serviceKinds: ["tts"], hidden: true },
"local-device": { id: "local-device", alias: "local-device", name: "Local Device", icon: "speaker", color: "#64748B", textIcon: "LD", serviceKinds: ["tts"], noAuth: true },
"google-tts": { id: "google-tts", alias: "google-tts", name: "Google TTS", icon: "record_voice_over", color: "#4285F4", textIcon: "GT", serviceKinds: ["tts"], noAuth: true },
"edge-tts": { id: "edge-tts", alias: "edge-tts", name: "Edge TTS", icon: "record_voice_over", color: "#0078D4", textIcon: "ET", serviceKinds: ["tts"], noAuth: true },
sdwebui: { id: "sdwebui", alias: "sdwebui", name: "SD WebUI", icon: "brush", color: "#FF7043", textIcon: "SD", website: "https://github.com/AUTOMATIC1111/stable-diffusion-webui", serviceKinds: ["image"] },
comfyui: { id: "comfyui", alias: "comfyui", name: "ComfyUI", icon: "account_tree", color: "#4CAF50", textIcon: "CF", website: "https://github.com/comfyanonymous/ComfyUI", serviceKinds: ["image"] },
huggingface: { id: "huggingface", alias: "hf", name: "HuggingFace", icon: "face", color: "#FFD21E", textIcon: "HF", website: "https://huggingface.co", serviceKinds: ["embedding", "image", "tts"] },
huggingface: { id: "huggingface", alias: "hf", name: "HuggingFace", icon: "face", color: "#FFD21E", textIcon: "HF", website: "https://huggingface.co", serviceKinds: ["image", "tts"], hiddenKinds: ["tts"] },
chutes: { id: "chutes", alias: "ch", name: "Chutes AI", icon: "water_drop", color: "#ffffffff", textIcon: "CH", website: "https://chutes.ai" },
"ollama-local": { id: "ollama-local", alias: "ollama-local", name: "Ollama Local", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com" },
"vertex-partner": { id: "vertex-partner", alias: "vxp", name: "Vertex Partner", icon: "cloud", color: "#34A853", textIcon: "VP", website: "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models" },
@@ -76,7 +79,7 @@ export const APIKEY_PROVIDERS = {
export const MEDIA_PROVIDER_KINDS = [
{ id: "embedding", label: "Embedding", icon: "data_array", endpoint: { method: "POST", path: "/v1/embeddings" } },
{ id: "image", label: "Image", icon: "image", endpoint: { method: "POST", path: "/v1/images/generations" } },
{ id: "tts", label: "TTS", icon: "record_voice_over", endpoint: { method: "POST", path: "/v1/audio/speech" } },
{ id: "tts", label: "Text To Speech", icon: "record_voice_over", endpoint: { method: "POST", path: "/v1/audio/speech" } },
{ id: "stt", label: "STT", icon: "mic", endpoint: { method: "POST", path: "/v1/audio/transcriptions" } },
{ id: "video", label: "Video", icon: "movie", endpoint: { method: "POST", path: "/v1/video/generations" } },
{ id: "music", label: "Music", icon: "music_note", endpoint: { method: "POST", path: "/v1/audio/music" } },
@@ -141,7 +144,10 @@ export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
export function getProvidersByKind(kind) {
return Object.values(AI_PROVIDERS).filter((p) => {
const kinds = p.serviceKinds ?? ["llm"];
return kinds.includes(kind);
if (!kinds.includes(kind)) return false;
if (p.hidden) return false; // globally hidden
if (p.hiddenKinds?.includes(kind)) return false; // hidden for specific kind
return true;
});
}
+41
View File
@@ -0,0 +1,41 @@
/**
* TTS Provider Configuration
* Centralized config for TTS provider UI behavior
*/
export const TTS_PROVIDER_CONFIG = {
"google-tts": {
hasLanguageDropdown: true,
hasModelSelector: false,
hasBrowseButton: false,
voiceSource: "hardcoded", // from providerModels
},
"openai": {
hasLanguageDropdown: false,
hasModelSelector: true,
hasBrowseButton: false,
voiceSource: "hardcoded", // from providerModels
modelKey: "openai-tts-models",
voiceKey: "openai-tts-voices",
},
"elevenlabs": {
hasLanguageDropdown: false,
hasModelSelector: true,
hasBrowseButton: true,
hasVoiceIdInput: true, // allow manual voice id entry
voiceSource: "api-language", // grouped by language from backend
modelKey: "elevenlabs-tts-models",
apiEndpoint: "/api/media-providers/tts/elevenlabs/voices",
},
"edge-tts": {
hasLanguageDropdown: false,
hasModelSelector: false,
hasBrowseButton: true,
voiceSource: "api-language", // from API with language picker
},
"local-device": {
hasLanguageDropdown: false,
hasModelSelector: false,
hasBrowseButton: true,
voiceSource: "api-language", // from API with language picker
},
};