feat: add icon support for speech providers and update related configurations

This commit is contained in:
aki
2026-04-19 00:19:49 +09:00
parent f0a0706d75
commit 9e237e63d4
20 changed files with 131 additions and 6 deletions
@@ -35,6 +35,8 @@ import {
Lmstudio, Lmstudio,
Meta, Meta,
MetaColor, MetaColor,
Microsoft,
MicrosoftColor,
Minimax, Minimax,
MinimaxColor, MinimaxColor,
Mistral, Mistral,
@@ -105,6 +107,8 @@ export const iconMap: Record<string, Component> = {
'cohere-color': CohereColor, 'cohere-color': CohereColor,
'azure': Azure, 'azure': Azure,
'azure-color': AzureColor, 'azure-color': AzureColor,
'microsoft': Microsoft,
'microsoft-color': MicrosoftColor,
'nvidia': Nvidia, 'nvidia': Nvidia,
'nvidia-color': NvidiaColor, 'nvidia-color': NvidiaColor,
'fireworks': Fireworks, 'fireworks': Fireworks,
@@ -1,7 +1,19 @@
<template> <template>
<div class="p-4"> <div class="p-4">
<section class="flex items-center gap-3"> <section class="flex items-center gap-3">
<Volume2 class="size-5" /> <span class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
<ProviderIcon
v-if="curProvider?.icon"
:icon="curProvider.icon"
size="1.5em"
/>
<span
v-else
class="text-xs font-medium text-muted-foreground"
>
{{ getInitials(curProvider?.name) }}
</span>
</span>
<div class="min-w-0"> <div class="min-w-0">
<h2 class="text-sm font-semibold truncate"> <h2 class="text-sm font-semibold truncate">
{{ curProvider?.name }} {{ curProvider?.name }}
@@ -191,7 +203,7 @@ import {
Switch, Switch,
} from '@memohai/ui' } from '@memohai/ui'
import ModelConfigEditor from './model-config-editor.vue' import ModelConfigEditor from './model-config-editor.vue'
import { ChevronDown, ChevronUp, Eye, EyeOff, Volume2 } from 'lucide-vue-next' import { ChevronDown, ChevronUp, Eye, EyeOff } from 'lucide-vue-next'
import { computed, inject, reactive, ref, watch } from 'vue' import { computed, inject, reactive, ref, watch } from 'vue'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -199,6 +211,7 @@ import { useQuery, useQueryCache } from '@pinia/colada'
import { getSpeechModels, getSpeechProvidersMeta, putModelsById, putProvidersById } from '@memohai/sdk' import { getSpeechModels, getSpeechProvidersMeta, putModelsById, putProvidersById } from '@memohai/sdk'
import type { TtsSpeechModelResponse, TtsSpeechProviderResponse } from '@memohai/sdk' import type { TtsSpeechModelResponse, TtsSpeechProviderResponse } from '@memohai/sdk'
import LoadingButton from '@/components/loading-button/index.vue' import LoadingButton from '@/components/loading-button/index.vue'
import ProviderIcon from '@/components/provider-icon/index.vue'
interface SpeechFieldSchema { interface SpeechFieldSchema {
key: string key: string
@@ -233,6 +246,11 @@ interface SpeechProviderMeta {
models?: SpeechModelMeta[] models?: SpeechModelMeta[]
} }
function getInitials(name: string | undefined) {
const label = name?.trim() ?? ''
return label ? label.slice(0, 2).toUpperCase() : '?'
}
const { t } = useI18n() const { t } = useI18n()
const curProvider = inject('curTtsProvider', ref<TtsSpeechProviderResponse>()) const curProvider = inject('curTtsProvider', ref<TtsSpeechProviderResponse>())
const curProviderId = computed(() => curProvider.value?.id) const curProviderId = computed(() => curProvider.value?.id)
+16 -2
View File
@@ -18,6 +18,12 @@ import type { TtsSpeechProviderResponse } from '@memohai/sdk'
import ProviderSetting from './components/provider-setting.vue' import ProviderSetting from './components/provider-setting.vue'
import { Volume2 } from 'lucide-vue-next' import { Volume2 } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue' import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
import ProviderIcon from '@/components/provider-icon/index.vue'
function getInitials(name: string | undefined) {
const label = name?.trim() ?? ''
return label ? label.slice(0, 2).toUpperCase() : '?'
}
const { data: providerData } = useQuery({ const { data: providerData } = useQuery({
key: () => ['speech-providers'], key: () => ['speech-providers'],
@@ -79,9 +85,17 @@ watch(filteredProviders, (list) => {
> >
<span class="relative shrink-0"> <span class="relative shrink-0">
<span class="flex size-7 items-center justify-center rounded-full bg-muted"> <span class="flex size-7 items-center justify-center rounded-full bg-muted">
<Volume2 <ProviderIcon
class="size-3.5 text-muted-foreground" v-if="item.icon"
:icon="item.icon"
size="1.25em"
/> />
<span
v-else
class="text-xs font-medium text-muted-foreground"
>
{{ getInitials(item.name) }}
</span>
</span> </span>
<span <span
v-if="item.enable !== false" v-if="item.enable !== false"
+8
View File
@@ -0,0 +1,8 @@
name: Alibaba Cloud Speech
client_type: alibabacloud-speech
icon: bailian-color
models:
- model_id: cosyvoice-tts
name: CosyVoice TTS
type: speech
+7
View File
@@ -0,0 +1,7 @@
name: Deepgram Speech
client_type: deepgram-speech
models:
- model_id: deepgram-tts
name: Deepgram TTS
type: speech
+1 -1
View File
@@ -1,6 +1,6 @@
name: Edge name: Edge
client_type: edge-speech client_type: edge-speech
icon: edge icon: microsoft
models: models:
- model_id: edge-read-aloud - model_id: edge-read-aloud
+7
View File
@@ -0,0 +1,7 @@
name: ElevenLabs Speech
client_type: elevenlabs-speech
models:
- model_id: elevenlabs-tts
name: ElevenLabs TTS
type: speech
+8
View File
@@ -0,0 +1,8 @@
name: Microsoft Speech
client_type: microsoft-speech
icon: azure-color
models:
- model_id: microsoft-tts
name: Microsoft TTS
type: speech
+8
View File
@@ -0,0 +1,8 @@
name: MiniMax Speech
client_type: minimax-speech
icon: minimax-color
models:
- model_id: minimax-tts
name: MiniMax TTS
type: speech
+8
View File
@@ -0,0 +1,8 @@
name: OpenAI Speech
client_type: openai-speech
icon: openai
models:
- model_id: gpt-4o-mini-tts
name: GPT-4o Mini TTS
type: speech
+8
View File
@@ -0,0 +1,8 @@
name: OpenRouter Speech
client_type: openrouter-speech
icon: openrouter
models:
- model_id: openrouter-tts
name: OpenRouter TTS
type: speech
+8
View File
@@ -0,0 +1,8 @@
name: Volcengine Speech
client_type: volcengine-speech
icon: volcengine-color
models:
- model_id: sami-tts
name: SAMI TTS
type: speech
+5 -1
View File
@@ -17,11 +17,15 @@ func SyncRegistry(ctx context.Context, logger *slog.Logger, queries *sqlc.Querie
if err != nil { if err != nil {
return fmt.Errorf("marshal speech provider config: %w", err) return fmt.Errorf("marshal speech provider config: %w", err)
} }
var icon pgtype.Text
if def.Icon != "" {
icon = pgtype.Text{String: def.Icon, Valid: true}
}
provider, err := queries.UpsertRegistryProvider(ctx, sqlc.UpsertRegistryProviderParams{ provider, err := queries.UpsertRegistryProvider(ctx, sqlc.UpsertRegistryProviderParams{
Name: def.DisplayName, Name: def.DisplayName,
ClientType: string(def.ClientType), ClientType: string(def.ClientType),
Icon: pgtype.Text{}, Icon: icon,
Config: configJSON, Config: configJSON,
}) })
if err != nil { if err != nil {
+8
View File
@@ -25,6 +25,7 @@ type ProviderFactory func(config map[string]any) (sdk.SpeechProvider, error)
type ProviderDefinition struct { type ProviderDefinition struct {
ClientType models.ClientType ClientType models.ClientType
DisplayName string DisplayName string
Icon string
Description string Description string
ConfigSchema ConfigSchema ConfigSchema ConfigSchema
DefaultModel string DefaultModel string
@@ -123,6 +124,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeEdgeSpeech, ClientType: models.ClientTypeEdgeSpeech,
DisplayName: "Microsoft Edge", DisplayName: "Microsoft Edge",
Icon: "microsoft",
Description: "Free Edge Read Aloud TTS", Description: "Free Edge Read Aloud TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{stringField("base_url", "Base URL", "Override the Edge WebSocket endpoint", false, "", 10)}}, ConfigSchema: ConfigSchema{Fields: []FieldSchema{stringField("base_url", "Base URL", "Override the Edge WebSocket endpoint", false, "", 10)}},
DefaultModel: "edge-read-aloud", DefaultModel: "edge-read-aloud",
@@ -163,6 +165,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeOpenAISpeech, ClientType: models.ClientTypeOpenAISpeech,
DisplayName: "OpenAI Speech", DisplayName: "OpenAI Speech",
Icon: "openai",
Description: "OpenAI /audio/speech compatible TTS", Description: "OpenAI /audio/speech compatible TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("api_key", "API Key", "Bearer API key", true, 10), secretField("api_key", "API Key", "Bearer API key", true, 10),
@@ -204,6 +207,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeOpenRouterSpeech, ClientType: models.ClientTypeOpenRouterSpeech,
DisplayName: "OpenRouter Speech", DisplayName: "OpenRouter Speech",
Icon: "openrouter",
Description: "OpenRouter audio modality TTS", Description: "OpenRouter audio modality TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("api_key", "API Key", "OpenRouter API key", true, 10), secretField("api_key", "API Key", "OpenRouter API key", true, 10),
@@ -333,6 +337,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeMiniMaxSpeech, ClientType: models.ClientTypeMiniMaxSpeech,
DisplayName: "MiniMax Speech", DisplayName: "MiniMax Speech",
Icon: "minimax-color",
Description: "MiniMax TTS", Description: "MiniMax TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("api_key", "API Key", "MiniMax API key", true, 10), secretField("api_key", "API Key", "MiniMax API key", true, 10),
@@ -380,6 +385,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeVolcengineSpeech, ClientType: models.ClientTypeVolcengineSpeech,
DisplayName: "Volcengine Speech", DisplayName: "Volcengine Speech",
Icon: "volcengine-color",
Description: "Volcengine SAMI TTS", Description: "Volcengine SAMI TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("access_key", "Access Key", "Volcengine access key", true, 10), secretField("access_key", "Access Key", "Volcengine access key", true, 10),
@@ -431,6 +437,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeAlibabaSpeech, ClientType: models.ClientTypeAlibabaSpeech,
DisplayName: "Alibaba Cloud Speech", DisplayName: "Alibaba Cloud Speech",
Icon: "bailian-color",
Description: "DashScope CosyVoice TTS", Description: "DashScope CosyVoice TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("api_key", "API Key", "DashScope API key", true, 10), secretField("api_key", "API Key", "DashScope API key", true, 10),
@@ -478,6 +485,7 @@ func defaultProviderDefinitions() []ProviderDefinition {
{ {
ClientType: models.ClientTypeMicrosoftSpeech, ClientType: models.ClientTypeMicrosoftSpeech,
DisplayName: "Microsoft Speech", DisplayName: "Microsoft Speech",
Icon: "azure-color",
Description: "Azure Cognitive Services TTS", Description: "Azure Cognitive Services TTS",
ConfigSchema: ConfigSchema{Fields: []FieldSchema{ ConfigSchema: ConfigSchema{Fields: []FieldSchema{
secretField("api_key", "API Key", "Azure speech subscription key", true, 10), secretField("api_key", "API Key", "Azure speech subscription key", true, 10),
+5
View File
@@ -209,10 +209,15 @@ func mergeConfig(parts ...map[string]any) map[string]any {
} }
func toSpeechProviderResponse(row sqlc.Provider) SpeechProviderResponse { func toSpeechProviderResponse(row sqlc.Provider) SpeechProviderResponse {
icon := ""
if row.Icon.Valid {
icon = row.Icon.String
}
return SpeechProviderResponse{ return SpeechProviderResponse{
ID: row.ID.String(), ID: row.ID.String(),
Name: row.Name, Name: row.Name,
ClientType: row.ClientType, ClientType: row.ClientType,
Icon: icon,
Enable: row.Enable, Enable: row.Enable,
CreatedAt: row.CreatedAt.Time, CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time, UpdatedAt: row.UpdatedAt.Time,
+1
View File
@@ -17,6 +17,7 @@ type SpeechProviderResponse struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ClientType string `json:"client_type"` ClientType string `json:"client_type"`
Icon string `json:"icon,omitempty"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
+1
View File
@@ -1709,6 +1709,7 @@ export type TtsSpeechProviderResponse = {
client_type?: string; client_type?: string;
created_at?: string; created_at?: string;
enable?: boolean; enable?: boolean;
icon?: string;
id?: string; id?: string;
name?: string; name?: string;
updated_at?: string; updated_at?: string;
+3
View File
@@ -13111,6 +13111,9 @@ const docTemplate = `{
"enable": { "enable": {
"type": "boolean" "type": "boolean"
}, },
"icon": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
+3
View File
@@ -13102,6 +13102,9 @@
"enable": { "enable": {
"type": "boolean" "type": "boolean"
}, },
"icon": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
+2
View File
@@ -2880,6 +2880,8 @@ definitions:
type: string type: string
enable: enable:
type: boolean type: boolean
icon:
type: string
id: id:
type: string type: string
name: name: