mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -04:00
feat(profile): configurable voice for pronunciation (defaulting to the first voice of the locale or gb)
This commit is contained in:
parent
3e49ba761c
commit
a504363cd6
@ -4,7 +4,6 @@ import type { Opinion } from '#shared/opinions.ts';
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
word: string;
|
word: string;
|
||||||
pronunciation?: string | null;
|
|
||||||
opinion: string;
|
opinion: string;
|
||||||
link?: unknown;
|
link?: unknown;
|
||||||
escape?: boolean;
|
escape?: boolean;
|
||||||
@ -42,7 +41,7 @@ const op = computed((): (Opinion & { description: string }) | null => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<nuxt-link v-if="link" :to="link" :class="`colour-${op.colour || 'default'}`"><Spelling :escape="escape" :text="word" /></nuxt-link>
|
<nuxt-link v-if="link" :to="link" :class="`colour-${op.colour || 'default'}`"><Spelling :escape="escape" :text="word" /></nuxt-link>
|
||||||
<Spelling v-else :escape="escape" :markdown="markdown" :text="word" class="d-inline-block" />
|
<Spelling v-else :escape="escape" :markdown="markdown" :text="word" class="d-inline-block" />
|
||||||
<Pronunciation v-if="pronunciation" :pronunciation="pronunciation" text />
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -186,10 +186,16 @@ const usedOpinions = computed(() => {
|
|||||||
:opinion="s.el.opinion"
|
:opinion="s.el.opinion"
|
||||||
:escape="false"
|
:escape="false"
|
||||||
:markdown="profile.markdown"
|
:markdown="profile.markdown"
|
||||||
:pronunciation="s.el.pronunciation"
|
|
||||||
:link="config.locale === 'tok' && config.pronouns.enabled ? `${config.pronouns.prefix}/${s.el.value}` : null"
|
:link="config.locale === 'tok' && config.pronouns.enabled ? `${config.pronouns.prefix}/${s.el.value}` : null"
|
||||||
:custom-opinions="profile.opinions"
|
:custom-opinions="profile.opinions"
|
||||||
/>
|
>
|
||||||
|
<Pronunciation
|
||||||
|
v-if="s.el.pronunciation"
|
||||||
|
:pronunciation="s.el.pronunciation"
|
||||||
|
text
|
||||||
|
:voices="s.el.voice !== null ? [s.el.voice] : []"
|
||||||
|
/>
|
||||||
|
</Opinion>
|
||||||
</template>
|
</template>
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,18 +2,19 @@
|
|||||||
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
import useConfig from '~/composables/useConfig.ts';
|
import useConfig from '~/composables/useConfig.ts';
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(defineProps<{
|
||||||
pronunciation: string;
|
pronunciation: string;
|
||||||
text?: boolean;
|
text?: boolean;
|
||||||
}>();
|
voices?: VoiceKey[];
|
||||||
|
}>(), {
|
||||||
|
voices: () => {
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
const config = useConfig();
|
if (!config.pronunciation?.enabled) {
|
||||||
|
return [];
|
||||||
const voices = computed((): VoiceKey[] => {
|
}
|
||||||
if (!config.pronunciation?.enabled) {
|
return config.pronunciation.voices;
|
||||||
return [];
|
},
|
||||||
}
|
|
||||||
return config.pronunciation.voices;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { escapePronunciationString, unescapePronunciationString } from '#shared/helpers.ts';
|
import { escapePronunciationString, unescapePronunciationString } from '#shared/helpers.ts';
|
||||||
|
import { voices } from '#shared/pronunciation/voices.ts';
|
||||||
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
import useConfig from '~/composables/useConfig.ts';
|
import useConfig from '~/composables/useConfig.ts';
|
||||||
|
|
||||||
const modelValue = defineModel<string | null>({ required: true });
|
const pronounciationModelValue = defineModel<string | null>('pronounciation', { required: true });
|
||||||
|
const voiceModelValue = defineModel<VoiceKey | null>('voice', { required: true });
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const rawPronunciation = computed({
|
const rawPronunciation = computed({
|
||||||
get(): string {
|
get(): string {
|
||||||
if (modelValue.value) {
|
if (pronounciationModelValue.value) {
|
||||||
const phonemes = modelValue.value.substring(1, modelValue.value.length - 1);
|
const phonemes = pronounciationModelValue.value.substring(1, pronounciationModelValue.value.length - 1);
|
||||||
return unescapePronunciationString(phonemes);
|
return unescapePronunciationString(phonemes);
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
@ -24,20 +26,33 @@ const rawPronunciation = computed({
|
|||||||
pronunciation = null;
|
pronunciation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelValue.value = pronunciation;
|
pronounciationModelValue.value = pronunciation;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const voices = computed((): VoiceKey[] => {
|
watch(pronounciationModelValue, (value, oldValue) => {
|
||||||
if (!config.pronunciation?.enabled) {
|
if (oldValue !== null) {
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
return config.pronunciation.voices;
|
// reset voice to default voice
|
||||||
|
if (!config.pronunciation?.enabled) {
|
||||||
|
voiceModelValue.value = 'gb';
|
||||||
|
} else {
|
||||||
|
voiceModelValue.value = config.pronunciation.voices[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedVoices = computed(() => {
|
||||||
|
const voicesOfLocale = config.pronunciation?.enabled ? config.pronunciation.voices : [];
|
||||||
|
|
||||||
|
return (Object.keys(voices) as VoiceKey[]).toSorted((a, b) => {
|
||||||
|
return (voicesOfLocale.includes(b) ? 1 : 0) - (voicesOfLocale.includes(a) ? 1 : 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="input-group input-group-sm w-auto">
|
<div class="input-group input-group-sm w-50">
|
||||||
<span class="input-group-text">/</span>
|
<span class="input-group-text">/</span>
|
||||||
<input
|
<input
|
||||||
v-model="rawPronunciation"
|
v-model="rawPronunciation"
|
||||||
@ -46,12 +61,28 @@ const voices = computed((): VoiceKey[] => {
|
|||||||
maxlength="255"
|
maxlength="255"
|
||||||
>
|
>
|
||||||
<span class="input-group-text">/</span>
|
<span class="input-group-text">/</span>
|
||||||
<PronunciationSpeaker
|
<template v-if="pronounciationModelValue !== null">
|
||||||
v-for="voice in voices"
|
<select
|
||||||
:key="voice"
|
v-model="voiceModelValue"
|
||||||
class="btn btn-sm rounded-start-0 btn-outline-secondary"
|
class="form-control"
|
||||||
:pronunciation="modelValue"
|
:class="voiceModelValue === null ? 'text-muted' : ''"
|
||||||
:voice="voice"
|
>
|
||||||
/>
|
<option :value="null">
|
||||||
|
<T>profile.pronunciation.voice.without</T>
|
||||||
|
</option>
|
||||||
|
<option v-for="voice of sortedVoices" :key="voice" :value="voice">
|
||||||
|
{{ voice.toUpperCase() }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<PronunciationSpeaker
|
||||||
|
v-if="voiceModelValue !== null"
|
||||||
|
class="btn btn-sm rounded-start-0 btn-outline-secondary"
|
||||||
|
:pronunciation="pronounciationModelValue"
|
||||||
|
:voice="voiceModelValue"
|
||||||
|
/>
|
||||||
|
<button v-else class="btn btn-sm rounded-start-0 btn-outline-secondary" type="button" disabled>
|
||||||
|
<Icon v="volume-slash" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -465,13 +465,16 @@ const propagateChanged = (field: string, checked: boolean): void => {
|
|||||||
</p>
|
</p>
|
||||||
<OpinionListInput
|
<OpinionListInput
|
||||||
v-model="formData.names"
|
v-model="formData.names"
|
||||||
:prototype="{ value: '', opinion: 'meh', pronunciation: null }"
|
:prototype="{ value: '', opinion: 'meh', pronunciation: null, voice: null }"
|
||||||
:custom-opinions="formData.opinions"
|
:custom-opinions="formData.opinions"
|
||||||
:maxitems="128"
|
:maxitems="128"
|
||||||
:maxlength="config.profile.longNames ? 255 : 32"
|
:maxlength="config.profile.longNames ? 255 : 32"
|
||||||
>
|
>
|
||||||
<template #additional="s">
|
<template #additional="s">
|
||||||
<PronunciationInput v-model="s.val.pronunciation" />
|
<PronunciationInput
|
||||||
|
v-model:pronounciation="s.val.pronunciation"
|
||||||
|
v-model:voice="s.val.voice"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</OpinionListInput>
|
</OpinionListInput>
|
||||||
<InlineMarkdownInstructions v-model="formData.markdown" />
|
<InlineMarkdownInstructions v-model="formData.markdown" />
|
||||||
|
@ -728,6 +728,8 @@ profile:
|
|||||||
names: 'Names'
|
names: 'Names'
|
||||||
pronunciation:
|
pronunciation:
|
||||||
ipa: 'Pronunciation using IPA'
|
ipa: 'Pronunciation using IPA'
|
||||||
|
voice:
|
||||||
|
without: '(without voice)'
|
||||||
pronouns: 'Pronouns'
|
pronouns: 'Pronouns'
|
||||||
pronounsInfo: >
|
pronounsInfo: >
|
||||||
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
||||||
|
@ -488,11 +488,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['de']
|
||||||
DE:
|
|
||||||
language: 'de-DE'
|
|
||||||
voice: 'Vicki'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -844,6 +844,8 @@ profile:
|
|||||||
names: 'Namen'
|
names: 'Namen'
|
||||||
pronunciation:
|
pronunciation:
|
||||||
ipa: 'Aussprache in IPA'
|
ipa: 'Aussprache in IPA'
|
||||||
|
voice:
|
||||||
|
without: '(ohne Sprachausgabe)'
|
||||||
pronouns: 'Pronomen'
|
pronouns: 'Pronomen'
|
||||||
pronounsInfo: >
|
pronounsInfo: >
|
||||||
Du kannst entweder ein <strong>Pronomen</strong> (z.B. „sier“ oder „sie/ihr“) oder einen <strong>Link</strong> (z.B. „https://pronomen.net/dey“)
|
Du kannst entweder ein <strong>Pronomen</strong> (z.B. „sier“ oder „sie/ihr“) oder einen <strong>Link</strong> (z.B. „https://pronomen.net/dey“)
|
||||||
|
@ -944,6 +944,8 @@ profile:
|
|||||||
names: 'Names'
|
names: 'Names'
|
||||||
pronunciation:
|
pronunciation:
|
||||||
ipa: 'Pronunciation using IPA'
|
ipa: 'Pronunciation using IPA'
|
||||||
|
voice:
|
||||||
|
without: '(without voice)'
|
||||||
pronouns: 'Pronouns'
|
pronouns: 'Pronouns'
|
||||||
pronounsInfo: >
|
pronounsInfo: >
|
||||||
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
||||||
|
20
migrations/095-profile-pronunciation-voice.sql
Normal file
20
migrations/095-profile-pronunciation-voice.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- Up
|
||||||
|
|
||||||
|
update profiles
|
||||||
|
set names = (select json_group_array(case
|
||||||
|
when json_type(names.value, '$.pronunciation') is 'text'
|
||||||
|
then json_set(names.value, '$.voice',
|
||||||
|
case
|
||||||
|
when locale in ('en', 'eo', 'fo', 'hu', 'tok', 'ua', 'vi')
|
||||||
|
then 'gb'
|
||||||
|
when locale = 'et' then 'fi'
|
||||||
|
when locale = 'lad' then 'es'
|
||||||
|
when locale = 'nn' then 'nb'
|
||||||
|
when locale = 'sv' then 'se'
|
||||||
|
when locale = 'zh' then 'cn'
|
||||||
|
else locale end)
|
||||||
|
else json(names.value) end)
|
||||||
|
from json_each(profiles.names) as names)
|
||||||
|
where 1 = 1;
|
||||||
|
|
||||||
|
-- Down
|
@ -348,7 +348,7 @@ const fetchProfiles = async (
|
|||||||
opinions: propv('opinions', () => JSON.parse(profile.opinions)),
|
opinions: propv('opinions', () => JSON.parse(profile.opinions)),
|
||||||
names: propv('names', () => {
|
names: propv('names', () => {
|
||||||
return JSON.parse(profile.names).map((name: ValueOpinion) => {
|
return JSON.parse(profile.names).map((name: ValueOpinion) => {
|
||||||
return { pronunciation: null, ...name };
|
return { pronunciation: null, voice: null, ...name };
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
pronouns: propv('pronouns', () => JSON.parse(profile.pronouns)),
|
pronouns: propv('pronouns', () => JSON.parse(profile.pronouns)),
|
||||||
|
@ -19,7 +19,6 @@ import grantOverridesRoute from './express/grantOverrides.ts';
|
|||||||
import imagesRoute from './express/images.ts';
|
import imagesRoute from './express/images.ts';
|
||||||
import mfaRoute from './express/mfa.ts';
|
import mfaRoute from './express/mfa.ts';
|
||||||
import profileRoute from './express/profile.ts';
|
import profileRoute from './express/profile.ts';
|
||||||
import pronounceRoute from './express/pronounce.ts';
|
|
||||||
import sentryRoute from './express/sentry.ts';
|
import sentryRoute from './express/sentry.ts';
|
||||||
import subscriptionRoute from './express/subscription.ts';
|
import subscriptionRoute from './express/subscription.ts';
|
||||||
import translationsRoute from './express/translations.ts';
|
import translationsRoute from './express/translations.ts';
|
||||||
@ -148,7 +147,6 @@ router.use(userRoute);
|
|||||||
router.use(profileRoute);
|
router.use(profileRoute);
|
||||||
router.use(adminRoute);
|
router.use(adminRoute);
|
||||||
router.use(mfaRoute);
|
router.use(mfaRoute);
|
||||||
router.use(pronounceRoute);
|
|
||||||
router.use(censusRoute);
|
router.use(censusRoute);
|
||||||
router.use(imagesRoute);
|
router.use(imagesRoute);
|
||||||
router.use(calendarRoute);
|
router.use(calendarRoute);
|
||||||
|
@ -2,6 +2,7 @@ import type { CustomEvent } from './calendar/helpers.ts';
|
|||||||
import type { Opinion } from './opinions.ts';
|
import type { Opinion } from './opinions.ts';
|
||||||
import type { User } from './user.ts';
|
import type { User } from './user.ts';
|
||||||
|
|
||||||
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
import type { LocaleCode } from '~~/locale/locales.ts';
|
import type { LocaleCode } from '~~/locale/locales.ts';
|
||||||
|
|
||||||
export interface UserWithProfiles {
|
export interface UserWithProfiles {
|
||||||
@ -63,6 +64,7 @@ export interface ValueOpinion {
|
|||||||
|
|
||||||
export interface NameOpinion extends ValueOpinion {
|
export interface NameOpinion extends ValueOpinion {
|
||||||
pronunciation: string;
|
pronunciation: string;
|
||||||
|
voice: VoiceKey | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Timezone {
|
export interface Timezone {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user