Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the possibility to add voices to your favorites #677

Merged
merged 12 commits into from
Dec 26, 2023
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
<template>
<NvSelect :options="options" v-bind="$attrs" />
<NvSelect :options="options" v-bind="$attrs"/>
</template>
<script lang="ts" setup>
import { NvSelect } from '@packages/ui'
import { computed } from 'vue'
import { useSpeechEngineManager } from '@/modules/speech-engine-manager'
import { orderBy } from 'lodash'
import { groupOptions } from '@/utils/select'
import { capitalize } from '@/utils/text'

const { engines } = useSpeechEngineManager()
const options = computed(() =>
orderBy(
groupOptions(orderBy(
engines.value.map((engine) => {
const disabled = engine.hasCredentials ? !engine.hasCredentials() : false
return {
disabled,
label: engine.name,
value: engine.id,
category: capitalize(engine.category),
attrs: {
title: disabled ? 'Requires credentials' : '',
},
}
}),
['disabled', 'label'],
['asc', 'asc'],
),
), 'category'),
)
</script>
1 change: 1 addition & 0 deletions apps/app/src/modules/speech-engine-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Payload = { [key: string]: any }
export interface SpeechEngine {
id: string
name: string
category: 'cloud' | 'local' | 'other'
getVoiceName: (voice: any) => string
getSelectedVoice: () => any
getCredentials: () => Credentials
Expand Down
57 changes: 47 additions & 10 deletions apps/app/src/plugins/speech-engines/amazon-polly/NvVoiceSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,34 @@
...$attrs,
}"
valueKey="Id"
/>
>
<template #optionAfter="{ option, hover }">
<span v-show="(!option.children && hover) || favoriteVoiceIds.includes(option.id)">
<NvButton
:icon-name="favoriteVoiceIds.includes(option.id) ? 'times' : 'heart'"
:title="
favoriteVoiceIds.includes(option.id) ? 'Remove from favorites' : 'Add to favorites'
"
size="sm"
type="default"
@mousedown.prevent.stop="
setProperty('favoriteVoiceIds', xor(favoriteVoiceIds, [option.id]))
"
/>
</span>
</template>
</NvSelect>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue'
import { useQueryClient } from 'vue-query'
import { purify } from '@packages/toolbox'
import { orderBy } from 'lodash'
import { NvSelect } from '@packages/ui'
import { orderBy, xor } from 'lodash'
import { NvButton, NvSelect } from '@packages/ui'
import { useSpeechStore } from '@/features/speech/store'
import { groupOptions } from '@/utils/select'
import { useListVoicesQuery } from './hooks'
import { getVoiceName, LIST_VOICES_QUERY_KEY } from './shared'
import { getVoiceCategory, getVoiceId, getVoiceName, LIST_VOICES_QUERY_KEY } from './shared'
import { getProperty, setProperty } from './store'

const queryClient = useQueryClient()
Expand All @@ -40,12 +57,32 @@ const { data, isFetching } = useListVoicesQuery(computedParams, {
enabled: canFetch,
})
const voices = computed(() => orderBy(data.value || [], ['LanguageCode', 'Name']))
const options = computed(() =>
voices.value.map((voice) => ({
label: getVoiceName(voice),
value: voice,
})),
)
const getOptionFromVoice = (voice: any) => ({
id: getVoiceId(voice),
label: getVoiceName(voice),
value: voice,
category: getVoiceCategory(voice),
})

const options = computed(() => {
const localOptions = groupOptions(voices.value.map(getOptionFromVoice), 'category')
const favoriteVoiceIds = getProperty('favoriteVoiceIds')
if (favoriteVoiceIds) {
const favoriteVoices = voices.value.filter((voice: any) =>
favoriteVoiceIds.includes(getVoiceId(voice)),
)
if (favoriteVoices.length) {
localOptions.unshift({
label: 'Favorites',
children: favoriteVoices.map(getOptionFromVoice),
})
}
}
return localOptions
})

const favoriteVoiceIds = computed<string[]>(() => getProperty('favoriteVoiceIds'))

watch(
() => [getProperty('identityPoolId', true), getProperty('region')],
() => canFetch.value && queryClient.refetchQueries(LIST_VOICES_QUERY_KEY),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const getSelectedVoice = () => getProperty('selectedVoice')
registerEngine({
id: ENGINE_ID,
name: ENGINE_NAME,
category: 'cloud',
getSelectedVoice,
getVoiceName,
getCredentials,
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/plugins/speech-engines/amazon-polly/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const ENGINE_ID = 'aptts' as const
export const ENGINE_NAME = 'Amazon Polly' as const
export const LIST_VOICES_QUERY_KEY = 'aptts-list-voices' as const
export const getVoiceName = (voice: any) => `${voice.LanguageCode} ${voice.Name} - ${voice.Gender}`
export const getVoiceId = (voice: any) => voice.Id
export const getVoiceCategory = (voice: any) => voice.LanguageName
1 change: 1 addition & 0 deletions apps/app/src/plugins/speech-engines/amazon-polly/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const { setProperty, getProperty } = definePluginStore(ENGINE_ID, {
Name: 'Amy',
SupportedEngines: ['neural', 'standard'],
},
favoriteVoiceIds: [],
useLocalCredentials: false,
})
60 changes: 49 additions & 11 deletions apps/app/src/plugins/speech-engines/animalese/NvVoiceSelect.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,69 @@
<template>
<NvSelect
v-loading="isFetching"
:options="options"
v-bind="{
modelValue: getProperty('selectedVoice'),
'onUpdate:modelValue': (value) => setProperty('selectedVoice', value),
...$attrs,
}"
valueKey="name"
/>
>
<template #optionAfter="{ option, hover }">
<span v-show="(!option.children && hover) || favoriteVoiceIds.includes(option.id)">
<NvButton
:icon-name="favoriteVoiceIds.includes(option.id) ? 'times' : 'heart'"
:title="
favoriteVoiceIds.includes(option.id) ? 'Remove from favorites' : 'Add to favorites'
"
size="sm"
type="default"
@mousedown.prevent.stop="
setProperty('favoriteVoiceIds', xor(favoriteVoiceIds, [option.id]))
"
/>
</span>
</template>
</NvSelect>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { NvSelect } from '@packages/ui'
import { NvButton, NvSelect } from '@packages/ui'
import { xor } from 'lodash'
import { groupOptions } from '@/utils/select'
import { getProperty, setProperty } from './store'
import { defaultVoice } from './shared'
import { defaultVoice, getVoiceCategory, getVoiceId, getVoiceName } from './shared'

const options = computed(() => [
const voices = computed(() => [
{
label: 'Custom', value: {
name: 'Custom',
}
...defaultVoice,
},
{
label: 'Default', value: {
...defaultVoice
}
name: 'Custom',
},
])
const getOptionFromVoice = (voice: any) => ({
id: getVoiceId(voice),
label: getVoiceName(voice),
value: voice,
category: getVoiceCategory(voice),
})

const options = computed(() => {
const localOptions = groupOptions(voices.value.map(getOptionFromVoice), 'category')
const favoriteVoiceIds = getProperty('favoriteVoiceIds')
if (favoriteVoiceIds) {
const favoriteVoices = voices.value.filter((voice: any) =>
favoriteVoiceIds.includes(getVoiceId(voice)),
)
if (favoriteVoices.length) {
localOptions.unshift({
label: 'Favorites',
children: favoriteVoices.map(getOptionFromVoice),
})
}
}
return localOptions
})

const favoriteVoiceIds = computed<string[]>(() => getProperty('favoriteVoiceIds'))
</script>
1 change: 1 addition & 0 deletions apps/app/src/plugins/speech-engines/animalese/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const getSelectedVoice = () => {
registerEngine({
id: ENGINE_ID,
name: ENGINE_NAME,
category: 'local',
getSelectedVoice,
getVoiceName,
getCredentials() {
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/plugins/speech-engines/animalese/shared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export const ENGINE_ID = 'animalesetts' as const
export const ENGINE_NAME = 'Animalese' as const
export const getVoiceName = (voice: any) => voice.name
export const getVoiceId = (voice: any) => voice.name

export const getVoiceCategory = (voice: any) => voice.category || 'General'
export const defaultVoice = {
name: 'Default',
pitch: 1,
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/plugins/speech-engines/animalese/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const { setProperty, getProperty } = definePluginStore(ENGINE_ID, {
},
shortened: false,
pitch: 1,
favoriteVoiceIds: [],
})
56 changes: 47 additions & 9 deletions apps/app/src/plugins/speech-engines/custom/NvVoiceSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,34 @@
...$attrs,
}"
valueKey="name"
/>
>
<template #optionAfter="{ option, hover }">
<span v-show="(!option.children && hover) || favoriteVoiceIds.includes(option.id)">
<NvButton
:icon-name="favoriteVoiceIds.includes(option.id) ? 'times' : 'heart'"
:title="
favoriteVoiceIds.includes(option.id) ? 'Remove from favorites' : 'Add to favorites'
"
size="sm"
type="default"
@mousedown.prevent.stop="
setProperty('favoriteVoiceIds', xor(favoriteVoiceIds, [option.id]))
"
/>
</span>
</template>
</NvSelect>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue'
import { useQueryClient } from 'vue-query'
import { NvSelect } from '@packages/ui'
import { NvButton, NvSelect } from '@packages/ui'
import { groupOptions } from '@/utils/select'
import { xor } from 'lodash'
import { purify } from '@packages/toolbox'
import { getVoiceCategory, getVoiceId, getVoiceName, LIST_VOICES_QUERY_KEY } from './shared'
import { useListVoicesQuery } from './hooks'
import { getVoiceName, LIST_VOICES_QUERY_KEY } from './shared'

import { getProperty, setProperty } from './store'

const queryClient = useQueryClient()
Expand All @@ -31,12 +50,31 @@ const { data, isFetching } = useListVoicesQuery(computedParams, {
enabled: canFetch,
})
const voices = computed(() => data.value || [])
const options = computed(() => [
...voices.value.map((voice: any) => ({
label: getVoiceName(voice),
value: voice,
})),
])
const getOptionFromVoice = (voice: any) => ({
id: getVoiceId(voice),
label: getVoiceName(voice),
value: voice,
category: getVoiceCategory(voice),
})

const options = computed(() => {
const localOptions = groupOptions(voices.value.map(getOptionFromVoice), 'category')
const favoriteVoiceIds = getProperty('favoriteVoiceIds')
if (favoriteVoiceIds) {
const favoriteVoices = voices.value.filter((voice: any) =>
favoriteVoiceIds.includes(getVoiceId(voice)),
)
if (favoriteVoices.length) {
localOptions.unshift({
label: 'Favorites',
children: favoriteVoices.map(getOptionFromVoice),
})
}
}
return localOptions
})

const favoriteVoiceIds = computed<string[]>(() => getProperty('favoriteVoiceIds'))
watch(
() => [canFetch.value, computedParams.value.credentials, computedParams.value.endpoint],
() => canFetch.value && queryClient.refetchQueries(LIST_VOICES_QUERY_KEY),
Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/plugins/speech-engines/custom/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const getSelectedVoice = () => getProperty('selectedVoice')
registerEngine({
id: ENGINE_ID,
name: ENGINE_NAME,
category: 'other',
getSelectedVoice,
getVoiceName,
hasCredentials() {
Expand All @@ -33,7 +34,7 @@ registerEngine({
synthesizeSpeech({ payload, credentials }) {
const endpoint = getProperty('endpoint')
return axios.post<Blob>(
`${endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint}/synthesize-speech`,
`${ endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint }/synthesize-speech`,
{
credentials,
payload,
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/plugins/speech-engines/custom/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const ENGINE_ID = 'customtts' as const
export const ENGINE_NAME = 'Custom' as const
export const LIST_VOICES_QUERY_KEY = 'customtts-list-voices' as const
export const getVoiceName = (voice: any) => voice?.name || 'None'
export const getVoiceId = (voice: any) => voice.name
export const getVoiceCategory = (voice: any) => voice.category || 'General'
1 change: 1 addition & 0 deletions apps/app/src/plugins/speech-engines/custom/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const { setProperty, getProperty } = definePluginStore(ENGINE_ID, {
selectedVoice: null,
endpoint: '',
apiKey: '',
favoriteVoiceIds: [],
})
Loading
Loading