mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 12:43:48 -04:00
Merge branch 'configurable-voice-p' into 'main'
configurable voices See merge request PronounsPage/PronounsPage!652
This commit is contained in:
commit
d5656ef739
@ -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>
|
||||||
|
@ -1,50 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
|
import useConfig from '~/composables/useConfig.ts';
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
pronunciation: string;
|
||||||
|
text?: boolean;
|
||||||
|
voices?: VoiceKey[];
|
||||||
|
}>(), {
|
||||||
|
voices: () => {
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
if (!config.pronunciation?.enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return config.pronunciation.voices;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="text-nowrap">
|
<span class="text-nowrap">
|
||||||
<span v-if="text" class="text-pronunciation">
|
<span v-if="text" class="text-secondary-emphasis fw-normal">
|
||||||
{{ pronunciation }}
|
{{ pronunciation }}
|
||||||
</span>
|
</span>
|
||||||
<PronunciationSpeaker
|
<PronunciationSpeaker
|
||||||
v-for="voice in voices"
|
v-for="voice in voices"
|
||||||
:key="voice"
|
:key="voice"
|
||||||
:pronunciation="pronunciation"
|
:pronunciation
|
||||||
:voice="voice"
|
:voice
|
||||||
|
:show-voice-label="voices.length > 1"
|
||||||
class="btn btn-sm btn-link px-1 py-0"
|
class="btn btn-sm btn-link px-1 py-0"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
import useConfig from '../composables/useConfig.ts';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
pronunciation: { required: true, type: String },
|
|
||||||
text: { default: false, type: Boolean },
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
config: useConfig(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
voices(): string[] {
|
|
||||||
if (this.config.pronunciation?.enabled) {
|
|
||||||
return Object.keys(this.config.pronunciation.voices);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "~/assets/variables";
|
|
||||||
|
|
||||||
.text-pronunciation {
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--#{$prefix}secondary-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,46 +1,18 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<div class="input-group input-group-sm w-auto">
|
|
||||||
<span class="input-group-text">/</span>
|
|
||||||
<input
|
|
||||||
v-model="rawPronunciation"
|
|
||||||
class="form-control mw-input"
|
|
||||||
:placeholder="$t('profile.pronunciation.ipa')"
|
|
||||||
maxlength="255"
|
|
||||||
>
|
|
||||||
<span class="input-group-text">/</span>
|
|
||||||
<PronunciationSpeaker
|
|
||||||
v-for="voice in voices"
|
|
||||||
:key="voice"
|
|
||||||
class="btn btn-sm rounded-start-0 btn-outline-secondary"
|
|
||||||
:pronunciation="modelValue"
|
|
||||||
:voice="voice"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
|
|
||||||
import useConfig from '../composables/useConfig.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 useConfig from '~/composables/useConfig.ts';
|
||||||
|
|
||||||
export default defineComponent({
|
const pronounciationModelValue = defineModel<string | null>('pronounciation', { required: true });
|
||||||
props: {
|
const voiceModelValue = defineModel<VoiceKey | null>('voice', { required: true });
|
||||||
modelValue: { default: null, type: String as PropType<string | null> },
|
|
||||||
},
|
const config = useConfig();
|
||||||
emits: ['update:modelValue'],
|
|
||||||
setup() {
|
const rawPronunciation = computed({
|
||||||
return {
|
|
||||||
config: useConfig(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
rawPronunciation: {
|
|
||||||
get(): string {
|
get(): string {
|
||||||
if (this.modelValue) {
|
if (pronounciationModelValue.value) {
|
||||||
const phonemes = this.modelValue.substring(1, this.modelValue.length - 1);
|
const phonemes = pronounciationModelValue.value.substring(1, pronounciationModelValue.value.length - 1);
|
||||||
return unescapePronunciationString(phonemes);
|
return unescapePronunciationString(phonemes);
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
@ -54,16 +26,63 @@ export default defineComponent({
|
|||||||
pronunciation = null;
|
pronunciation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('update:modelValue', pronunciation);
|
pronounciationModelValue.value = pronunciation;
|
||||||
},
|
|
||||||
},
|
|
||||||
voices(): string[] {
|
|
||||||
if (this.config.pronunciation?.enabled) {
|
|
||||||
return Object.keys(this.config.pronunciation.voices);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(pronounciationModelValue, (value, oldValue) => {
|
||||||
|
if (oldValue !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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 entriesWithKeys(voices).toSorted((a, b) => {
|
||||||
|
return (voicesOfLocale.includes(b.key as VoiceKey) ? 1 : 0) - (voicesOfLocale.includes(a.key as VoiceKey) ? 1 : 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="input-group input-group-sm w-50">
|
||||||
|
<span class="input-group-text">/</span>
|
||||||
|
<input
|
||||||
|
v-model="rawPronunciation"
|
||||||
|
class="form-control mw-input"
|
||||||
|
:placeholder="$t('profile.pronunciation.ipa')"
|
||||||
|
maxlength="255"
|
||||||
|
>
|
||||||
|
<span class="input-group-text">/</span>
|
||||||
|
<template v-if="pronounciationModelValue !== null">
|
||||||
|
<select
|
||||||
|
v-model="voiceModelValue"
|
||||||
|
class="form-control"
|
||||||
|
:class="voiceModelValue === null ? 'text-muted' : ''"
|
||||||
|
>
|
||||||
|
<option :value="null">
|
||||||
|
<T>profile.pronunciation.voice.without</T>
|
||||||
|
</option>
|
||||||
|
<option v-for="voice of sortedVoices" :key="voice.key" :value="voice.key">
|
||||||
|
{{ voice.key.toUpperCase() }} – {{ voice.name }}
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
|
|
||||||
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pronunciation?: string | null;
|
pronunciation?: string | null;
|
||||||
voice: string;
|
voice: VoiceKey;
|
||||||
button?: boolean;
|
showVoiceLabel?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
pronunciation: null,
|
pronunciation: null,
|
||||||
});
|
});
|
||||||
@ -56,18 +58,21 @@ const icon = computed((): string => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const name = computed((): string | null => {
|
const voiceLabel = computed((): string | null => {
|
||||||
|
if (!props.showVoiceLabel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!config.pronunciation?.enabled) {
|
if (!config.pronunciation?.enabled) {
|
||||||
return props.voice;
|
return props.voice.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const voices = Object.keys(config.pronunciation.voices);
|
const voices = config.pronunciation.voices;
|
||||||
if (voices.length === 1 && props.voice === voices[0]) {
|
if (voices.length === 1 && props.voice === voices[0]) {
|
||||||
// don’t show voice name if it is considered the main voice for this locale
|
// don’t show voice name if it is considered the main voice for this locale
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.voice;
|
return props.voice.toUpperCase();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -81,6 +86,6 @@ const name = computed((): string | null => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
@click.prevent="pronounce"
|
@click.prevent="pronounce"
|
||||||
>
|
>
|
||||||
<Icon :v="icon" /><sub v-if="name">{{ name }}</sub>
|
<Icon :v="icon" /><sub v-if="voiceLabel">{{ voiceLabel }}</sub>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
@ -460,18 +460,24 @@ const propagateChanged = (field: string, checked: boolean): void => {
|
|||||||
<T>profile.names</T>
|
<T>profile.names</T>
|
||||||
</template>
|
</template>
|
||||||
<template #names>
|
<template #names>
|
||||||
<p v-if="$te('profile.namesInfo')" class="small text-muted">
|
<div class="alert alert-info">
|
||||||
|
<p class="small mb-0">
|
||||||
|
<Icon v="info-circle" />
|
||||||
<T>profile.namesInfo</T>
|
<T>profile.namesInfo</T>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<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" />
|
||||||
|
@ -99,11 +99,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['gb']
|
||||||
GB:
|
|
||||||
language: 'en-GB'
|
|
||||||
voice: 'Emma'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -726,8 +726,17 @@ user:
|
|||||||
profile:
|
profile:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
names: 'Names'
|
names: 'Names'
|
||||||
|
namesInfo: >
|
||||||
|
You can enter your <strong>names</strong> or <strong>initials</strong> with the spelling
|
||||||
|
and optionally with their pronunciation.
|
||||||
|
Keep in mind that you need to specify pronunciation in IPA (International Phonetic Alphabet)
|
||||||
|
so that it is in an universally recognized format.
|
||||||
|
If the used voice synthesizers does not produce the correct pronunciation,
|
||||||
|
you can try switching to a different one or disabling the voice entirely if it is too far off.
|
||||||
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”)
|
||||||
|
@ -51,15 +51,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['ar', 'ae']
|
||||||
AR:
|
|
||||||
language: 'arb'
|
|
||||||
voice: 'Zeina'
|
|
||||||
engine: 'standard'
|
|
||||||
AE:
|
|
||||||
language: 'ar-AE'
|
|
||||||
voice: 'Hala'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { Engine, LanguageCode, VoiceId } from '@aws-sdk/client-polly';
|
|
||||||
|
|
||||||
import type { Category } from '#shared/classes.ts';
|
import type { Category } from '#shared/classes.ts';
|
||||||
import type { GrammarTablesDefinition } from '#shared/language/grammarTables.ts';
|
import type { GrammarTablesDefinition } from '#shared/language/grammarTables.ts';
|
||||||
|
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
|
||||||
import type { LocaleCode } from '~~/locale/locales.ts';
|
import type { LocaleCode } from '~~/locale/locales.ts';
|
||||||
|
|
||||||
export type Toggable<T> = ({ enabled: true } & T) | { enabled: false } & Partial<T>;
|
export type Toggable<T> = ({ enabled: true } & T) | { enabled: false } & Partial<T>;
|
||||||
@ -404,45 +403,7 @@ interface PronunciationConfig {
|
|||||||
* whether to treat letter graphemes as phonemes. useful when the configured locale has no matching voice
|
* whether to treat letter graphemes as phonemes. useful when the configured locale has no matching voice
|
||||||
*/
|
*/
|
||||||
ipa?: boolean;
|
ipa?: boolean;
|
||||||
voices: Record<string, PronunciationVoiceConfig>;
|
voices: VoiceKey[];
|
||||||
}
|
|
||||||
|
|
||||||
export type PronunciationVoiceConfig = AwsPollyPronunciationVoiceConfig | NarakeetPronunciationVoiceConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
|
|
||||||
*/
|
|
||||||
export interface AwsPollyPronunciationVoiceConfig {
|
|
||||||
/**
|
|
||||||
* text-to-speech provider (aws_polly is the default, if not specified)
|
|
||||||
*/
|
|
||||||
provider?: 'aws_polly';
|
|
||||||
/**
|
|
||||||
* language code
|
|
||||||
*/
|
|
||||||
language: LanguageCode;
|
|
||||||
/**
|
|
||||||
* voice name
|
|
||||||
*/
|
|
||||||
voice: VoiceId;
|
|
||||||
/**
|
|
||||||
* voice engine (default: 'standard')
|
|
||||||
*/
|
|
||||||
engine?: Engine;
|
|
||||||
}
|
|
||||||
export interface NarakeetPronunciationVoiceConfig {
|
|
||||||
/**
|
|
||||||
* text-to-speech provider
|
|
||||||
*/
|
|
||||||
provider: 'narakeet';
|
|
||||||
/**
|
|
||||||
* language code
|
|
||||||
*/
|
|
||||||
language: string;
|
|
||||||
/**
|
|
||||||
* voice name
|
|
||||||
*/
|
|
||||||
voice: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourcesConfig {
|
interface SourcesConfig {
|
||||||
|
@ -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
|
||||||
|
@ -842,12 +842,21 @@ user:
|
|||||||
profile:
|
profile:
|
||||||
description: 'Beschreibung'
|
description: 'Beschreibung'
|
||||||
names: 'Namen'
|
names: 'Namen'
|
||||||
|
namesInfo: >
|
||||||
|
Du kannst deine <strong>Namen</strong> oder <strong>Initialen</strong> mit ihrer Schreibung
|
||||||
|
und optional ihrer Aussprache eingeben.
|
||||||
|
Beachte bitte, dass die Aussprache in IPA (Internationales Phonetisches Alphabet) eingegeben werden muss,
|
||||||
|
damit diese in einem universellen Format vorliegt.
|
||||||
|
Wenn die Sprachausgabe nicht die korrekte Aussprache erzeugt,
|
||||||
|
kannst du eine andere Sprachausgabe versuchen oder die Sprachausgabe komplett abschalten.
|
||||||
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“)
|
||||||
oder <strong>vier benutzerdefinierten Formen</strong> (z.B. „xier/xies/xiem/xien“).
|
oder <strong>vier benutzerdefinierten Formen</strong> (z.B. „xier/xies/xiem/xien“) eingeben.
|
||||||
Du kannst auch den {/pronomen#generator=<strong>Generator</strong>} verwenden, um die Sätze mit benutzerdefinierten Formen auszufüllen.
|
Du kannst auch den {/pronomen#generator=<strong>Generator</strong>} verwenden, um die Sätze mit benutzerdefinierten Formen auszufüllen.
|
||||||
pronounsNotFound: 'Nicht erkennbares Format. Bitte beachte die oben genannte Anweisung.'
|
pronounsNotFound: 'Nicht erkennbares Format. Bitte beachte die oben genannte Anweisung.'
|
||||||
words: 'Wörter'
|
words: 'Wörter'
|
||||||
|
@ -149,11 +149,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['gb']
|
||||||
GB:
|
|
||||||
language: 'en-GB'
|
|
||||||
voice: 'Emma'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -942,8 +942,17 @@ user:
|
|||||||
profile:
|
profile:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
names: 'Names'
|
names: 'Names'
|
||||||
|
namesInfo: >
|
||||||
|
You can enter your <strong>names</strong> or <strong>initials</strong> with the spelling
|
||||||
|
and optionally with their pronunciation.
|
||||||
|
Keep in mind that you need to specify pronunciation in IPA (International Phonetic Alphabet)
|
||||||
|
so that it is in an universally recognized format.
|
||||||
|
If the used voice synthesizers does not produce the correct pronunciation,
|
||||||
|
you can try switching to a different one or disabling the voice entirely if it is too far off.
|
||||||
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”)
|
||||||
|
@ -97,7 +97,6 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
voices: {}
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -128,15 +128,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['es', 'mx']
|
||||||
ES:
|
|
||||||
language: 'es-ES'
|
|
||||||
voice: 'Lucia'
|
|
||||||
engine: 'standard'
|
|
||||||
MX:
|
|
||||||
language: 'es-MX'
|
|
||||||
voice: 'Mia'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -37,11 +37,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
voices:
|
voices: ['fi']
|
||||||
FI:
|
|
||||||
language: 'fi-FI'
|
|
||||||
voice: 'Suvi'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -133,11 +133,6 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
voices:
|
|
||||||
GB:
|
|
||||||
language: 'en-GB'
|
|
||||||
voice: 'Emma'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -65,15 +65,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['fr', 'ca']
|
||||||
FR:
|
|
||||||
language: 'fr-FR'
|
|
||||||
voice: 'Lea'
|
|
||||||
engine: 'standard'
|
|
||||||
CA:
|
|
||||||
language: 'fr-CA'
|
|
||||||
voice: 'Gabrielle'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -79,8 +79,7 @@ pronouns:
|
|||||||
shortMorphemes: 3
|
shortMorphemes: 3
|
||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: false
|
||||||
voices: {}
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -98,19 +98,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['bos', 'hrv', 'srp']
|
||||||
BOS:
|
|
||||||
language: 'bos'
|
|
||||||
voice: 'suada'
|
|
||||||
provider: 'narakeet'
|
|
||||||
HRV:
|
|
||||||
language: 'hrv'
|
|
||||||
voice: 'jasna'
|
|
||||||
provider: 'narakeet'
|
|
||||||
SRP:
|
|
||||||
language: 'srp'
|
|
||||||
voice: 'milica-latin'
|
|
||||||
provider: 'narakeet'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -44,12 +44,7 @@ pronouns:
|
|||||||
others: 'Other pronouns'
|
others: 'Other pronouns'
|
||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: false
|
||||||
voices:
|
|
||||||
GB:
|
|
||||||
language: 'en-GB'
|
|
||||||
voice: 'Emma'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -324,11 +324,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['it']
|
||||||
IT:
|
|
||||||
language: 'it-IT'
|
|
||||||
voice: 'Bianca'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -45,11 +45,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['ja']
|
||||||
JA:
|
|
||||||
language: 'ja-JP'
|
|
||||||
voice: 'Mizuki'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -40,11 +40,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['ko']
|
||||||
KO:
|
|
||||||
language: 'ko-KR'
|
|
||||||
voice: 'Seoyeon'
|
|
||||||
engine: 'neural'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -120,11 +120,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['no']
|
||||||
NO:
|
|
||||||
language: 'nb-NO'
|
|
||||||
voice: 'Liv'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -71,11 +71,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['nl']
|
||||||
GB:
|
|
||||||
language: 'nl-NL'
|
|
||||||
voice: 'Ruben'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -121,11 +121,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['no']
|
||||||
NO:
|
|
||||||
language: 'nb-NO'
|
|
||||||
voice: 'Liv'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -322,11 +322,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['pl']
|
||||||
PL:
|
|
||||||
language: 'pl-PL'
|
|
||||||
voice: 'Ewa'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -81,15 +81,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['pt', 'br']
|
||||||
PT:
|
|
||||||
language: 'pt-PT'
|
|
||||||
voice: 'Cristiano'
|
|
||||||
engine: 'standard'
|
|
||||||
BR:
|
|
||||||
language: 'pt-BR'
|
|
||||||
voice: 'Vitoria'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -113,11 +113,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['ro']
|
||||||
RO:
|
|
||||||
language: 'ro-RO'
|
|
||||||
voice: 'Carmen'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -162,11 +162,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['ru']
|
||||||
GB:
|
|
||||||
language: 'ru-RU'
|
|
||||||
voice: 'Tatyana'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -50,11 +50,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['se']
|
||||||
SE:
|
|
||||||
language: 'sv-SE'
|
|
||||||
voice: 'Astrid'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -29,11 +29,7 @@ pronouns:
|
|||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
ipa: true
|
ipa: true
|
||||||
voices:
|
voices: ['us']
|
||||||
TOK:
|
|
||||||
language: 'es-US'
|
|
||||||
voice: 'Lupe'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -11,11 +11,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['tr']
|
||||||
TR:
|
|
||||||
language: 'tr-TR'
|
|
||||||
voice: 'Filiz'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -83,7 +83,6 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
voices: {}
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -35,7 +35,6 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: false
|
enabled: false
|
||||||
voices: {}
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -35,11 +35,7 @@ pronouns:
|
|||||||
|
|
||||||
pronunciation:
|
pronunciation:
|
||||||
enabled: true
|
enabled: true
|
||||||
voices:
|
voices: ['cn']
|
||||||
CN:
|
|
||||||
language: 'cmn-CN'
|
|
||||||
voice: 'Zhiyu'
|
|
||||||
engine: 'standard'
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
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
|
@ -2,31 +2,21 @@ import { Polly } from '@aws-sdk/client-polly';
|
|||||||
import { NoSuchKey } from '@aws-sdk/client-s3';
|
import { NoSuchKey } from '@aws-sdk/client-s3';
|
||||||
import type { S3 } from '@aws-sdk/client-s3';
|
import type { S3 } from '@aws-sdk/client-s3';
|
||||||
import type { NodeJsClient } from '@smithy/types';
|
import type { NodeJsClient } from '@smithy/types';
|
||||||
import { Router } from 'express';
|
|
||||||
import { getH3Event } from 'h3-express';
|
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import sha1 from 'sha1';
|
|
||||||
|
|
||||||
import { s3, awsConfig, s3BucketParams } from '../cloudServices.ts';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
convertPronunciationStringToSsml,
|
convertPronunciationStringToSsml,
|
||||||
convertPronunciationStringToNarakeetFormat,
|
convertPronunciationStringToNarakeetFormat,
|
||||||
handleErrorAsync,
|
sha256,
|
||||||
} from '#shared/helpers.ts';
|
} from '#shared/helpers.ts';
|
||||||
import type {
|
import { voices } from '#shared/pronunciation/voices.ts';
|
||||||
PronunciationVoiceConfig,
|
import type { VoiceKey, AwsPollyVoice, NarakeetVoice, Voice } from '#shared/pronunciation/voices.ts';
|
||||||
AwsPollyPronunciationVoiceConfig,
|
import { s3, awsConfig, s3BucketParams } from '~~/server/cloudServices.ts';
|
||||||
NarakeetPronunciationVoiceConfig,
|
|
||||||
} from '~~/locale/config.ts';
|
|
||||||
import { getLocale, loadConfig, localesDict } from '~~/server/data.ts';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
type ProviderKey = 'aws_polly' | 'narakeet';
|
type ProviderKey = 'aws_polly' | 'narakeet';
|
||||||
interface Provider {
|
interface Provider {
|
||||||
tokenised(text: string): string;
|
tokenised(text: string): string;
|
||||||
generate(textTokenised: string, voice: PronunciationVoiceConfig): Promise<[Uint8Array, string]>;
|
generate(textTokenised: string, voice: Voice): Promise<[Uint8Array, string]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers: Record<ProviderKey, Provider> = {
|
const providers: Record<ProviderKey, Provider> = {
|
||||||
@ -34,7 +24,7 @@ const providers: Record<ProviderKey, Provider> = {
|
|||||||
tokenised(text: string): string {
|
tokenised(text: string): string {
|
||||||
return convertPronunciationStringToSsml(text);
|
return convertPronunciationStringToSsml(text);
|
||||||
},
|
},
|
||||||
async generate(textTokenised: string, voice: AwsPollyPronunciationVoiceConfig): Promise<[Uint8Array, string]> {
|
async generate(textTokenised: string, voice: AwsPollyVoice): Promise<[Uint8Array, string]> {
|
||||||
const polly = new Polly(awsConfig) as NodeJsClient<Polly>;
|
const polly = new Polly(awsConfig) as NodeJsClient<Polly>;
|
||||||
|
|
||||||
const pollyResponse = await polly.synthesizeSpeech({
|
const pollyResponse = await polly.synthesizeSpeech({
|
||||||
@ -56,7 +46,7 @@ const providers: Record<ProviderKey, Provider> = {
|
|||||||
tokenised(text: string): string {
|
tokenised(text: string): string {
|
||||||
return convertPronunciationStringToNarakeetFormat(text);
|
return convertPronunciationStringToNarakeetFormat(text);
|
||||||
},
|
},
|
||||||
async generate(textTokenised: string, voice: NarakeetPronunciationVoiceConfig): Promise<[Uint8Array, string]> {
|
async generate(textTokenised: string, voice: NarakeetVoice): Promise<[Uint8Array, string]> {
|
||||||
const url = `https://api.narakeet.com/text-to-speech/mp3?voice=${voice.voice}&language=${voice.language}`;
|
const url = `https://api.narakeet.com/text-to-speech/mp3?voice=${voice.voice}&language=${voice.language}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@ -81,29 +71,40 @@ const providers: Record<ProviderKey, Provider> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const text = Base64.decode(req.params.pronunciation);
|
const text = Base64.decode(getRouterParam(event, 'pronunciation')!);
|
||||||
|
|
||||||
if (!text || text.length > 256) {
|
if (!text || text.length > 256) {
|
||||||
return res.status(404).json({ error: 'Not found' });
|
throw createError({
|
||||||
|
status: 404,
|
||||||
|
statusMessage: 'Not Found',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = getLocale(getH3Event(req));
|
const voiceKey = getRouterParam(event, 'voice');
|
||||||
const config = await loadConfig(locale);
|
if (voiceKey === undefined || !(voiceKey in voices)) {
|
||||||
|
throw createError({
|
||||||
const voice: PronunciationVoiceConfig | undefined = config.pronunciation?.voices?.[req.params.voice];
|
status: 404,
|
||||||
if (!voice) {
|
statusMessage: 'Not Found',
|
||||||
return res.status(404).json({ error: 'Not found' });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const voice = voices[voiceKey as VoiceKey];
|
||||||
const provider = providers[(voice.provider || 'aws_polly') as ProviderKey];
|
const provider = providers[(voice.provider || 'aws_polly') as ProviderKey];
|
||||||
const tokenised = provider.tokenised(text);
|
const tokenised = provider.tokenised(text);
|
||||||
const key = `pronunciation/${config.locale}-${req.params.voice}/${sha1(tokenised)}.mp3`;
|
const key = `pronunciation/${voiceKey}/${await sha256(tokenised)}.mp3`;
|
||||||
|
|
||||||
|
// bypass cache in development
|
||||||
|
if (import.meta.dev) {
|
||||||
|
const [buffer, contentType] = await provider.generate(tokenised, voice);
|
||||||
|
setResponseHeader(event, 'content-type', contentType);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const s3Response = await (s3 as NodeJsClient<S3>).getObject({ Key: key, ...s3BucketParams });
|
const s3Response = await (s3 as NodeJsClient<S3>).getObject({ Key: key, ...s3BucketParams });
|
||||||
res.set('content-type', s3Response.ContentType);
|
setResponseHeader(event, 'content-type', s3Response.ContentType);
|
||||||
return s3Response.Body!.pipe(res);
|
return s3Response.Body;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof NoSuchKey)) {
|
if (!(error instanceof NoSuchKey)) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -111,7 +112,6 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res)
|
|||||||
|
|
||||||
const [buffer, contentType] = await provider.generate(tokenised, voice);
|
const [buffer, contentType] = await provider.generate(tokenised, voice);
|
||||||
|
|
||||||
if (localesDict[locale]?.published) {
|
|
||||||
// don't populate the cache for locales still under construction
|
// don't populate the cache for locales still under construction
|
||||||
await s3.putObject({
|
await s3.putObject({
|
||||||
Key: key,
|
Key: key,
|
||||||
@ -119,12 +119,8 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res)
|
|||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
...s3BucketParams,
|
...s3BucketParams,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
res.set('content-type', contentType);
|
setResponseHeader(event, 'content-type', contentType);
|
||||||
res.write(buffer);
|
return buffer;
|
||||||
return res.end();
|
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
|
||||||
export default router;
|
|
@ -5,12 +5,10 @@ import { buildCalendar } from '#shared/calendar/calendar.ts';
|
|||||||
import type { Calendar } from '#shared/calendar/helpers.ts';
|
import type { Calendar } from '#shared/calendar/helpers.ts';
|
||||||
import { PronounLibrary } from '#shared/classes.ts';
|
import { PronounLibrary } from '#shared/classes.ts';
|
||||||
import type { Pronoun, PronounGroup } from '#shared/classes.ts';
|
import type { Pronoun, PronounGroup } from '#shared/classes.ts';
|
||||||
import { buildDict } from '#shared/helpers';
|
|
||||||
import type { NounsData } from '#shared/nouns.ts';
|
import type { NounsData } from '#shared/nouns.ts';
|
||||||
import { Translator } from '#shared/translator.ts';
|
import { Translator } from '#shared/translator.ts';
|
||||||
import type { Config } from '~~/locale/config.ts';
|
import type { Config } from '~~/locale/config.ts';
|
||||||
import type { PronounData, PronounExamplesData } from '~~/locale/data.ts';
|
import type { PronounData, PronounExamplesData } from '~~/locale/data.ts';
|
||||||
import locales from '~~/locale/locales.ts';
|
|
||||||
import type { LocaleCode } from '~~/locale/locales.ts';
|
import type { LocaleCode } from '~~/locale/locales.ts';
|
||||||
import { loadSuml, loadTsv } from '~~/server/loader.ts';
|
import { loadSuml, loadTsv } from '~~/server/loader.ts';
|
||||||
import { getLocaleForUrl, getUrlForLocale } from '~~/server/src/domain.ts';
|
import { getLocaleForUrl, getUrlForLocale } from '~~/server/src/domain.ts';
|
||||||
@ -25,12 +23,6 @@ const setDefault = async <K, V>(map: Map<K, V>, key: K, supplier: () => Promise<
|
|||||||
return computedValue;
|
return computedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const localesDict = buildDict(function* () {
|
|
||||||
for (const locale of locales) {
|
|
||||||
yield [locale.code, locale];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getLocale = (event: H3Event): LocaleCode => {
|
export const getLocale = (event: H3Event): LocaleCode => {
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
if (typeof query.locale === 'string') {
|
if (typeof query.locale === 'string') {
|
||||||
|
@ -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 {
|
||||||
|
211
shared/pronunciation/voices.ts
Normal file
211
shared/pronunciation/voices.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import type { Engine, LanguageCode, VoiceId } from '@aws-sdk/client-polly';
|
||||||
|
|
||||||
|
export type Voice = AwsPollyVoice | NarakeetVoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
|
||||||
|
*/
|
||||||
|
export interface AwsPollyVoice {
|
||||||
|
/**
|
||||||
|
* human-readable name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* text-to-speech provider (aws_polly is the default, if not specified)
|
||||||
|
*/
|
||||||
|
provider?: 'aws_polly';
|
||||||
|
/**
|
||||||
|
* language code
|
||||||
|
*/
|
||||||
|
language: LanguageCode;
|
||||||
|
/**
|
||||||
|
* voice name
|
||||||
|
*/
|
||||||
|
voice: VoiceId;
|
||||||
|
/**
|
||||||
|
* voice engine (default: 'standard')
|
||||||
|
*/
|
||||||
|
engine?: Engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NarakeetVoice {
|
||||||
|
/**
|
||||||
|
* human-readable name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* text-to-speech provider
|
||||||
|
*/
|
||||||
|
provider: 'narakeet';
|
||||||
|
/**
|
||||||
|
* language code
|
||||||
|
*/
|
||||||
|
language: string;
|
||||||
|
/**
|
||||||
|
* voice name
|
||||||
|
*/
|
||||||
|
voice: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineVoices = <T extends Record<string, Voice>>(voices: T) => voices as Readonly<Record<keyof T, Voice>>;
|
||||||
|
|
||||||
|
export const voices = defineVoices({
|
||||||
|
ar: {
|
||||||
|
name: 'العربية',
|
||||||
|
language: 'arb',
|
||||||
|
voice: 'Zeina',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
ae: {
|
||||||
|
name: '(الخليجية) العربية',
|
||||||
|
language: 'ar-AE',
|
||||||
|
voice: 'Hala',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
name: 'Deutsch',
|
||||||
|
language: 'de-DE',
|
||||||
|
voice: 'Vicki',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
gb: {
|
||||||
|
name: 'English (British)',
|
||||||
|
language: 'en-GB',
|
||||||
|
voice: 'Emma',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
us: {
|
||||||
|
name: 'English (American)',
|
||||||
|
language: 'es-US',
|
||||||
|
voice: 'Lupe',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
name: 'Español (España)',
|
||||||
|
language: 'es-ES',
|
||||||
|
voice: 'Lucia',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
mx: {
|
||||||
|
name: 'Español (México)',
|
||||||
|
language: 'es-MX',
|
||||||
|
voice: 'Mia',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
name: 'Suomi',
|
||||||
|
language: 'fi-FI',
|
||||||
|
voice: 'Suvi',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
name: 'Français (France)',
|
||||||
|
language: 'fr-FR',
|
||||||
|
voice: 'Lea',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
ca: {
|
||||||
|
name: 'Français (Canada)',
|
||||||
|
language: 'fr-CA',
|
||||||
|
voice: 'Gabrielle',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
bos: {
|
||||||
|
name: 'Босански',
|
||||||
|
language: 'bos',
|
||||||
|
voice: 'suada',
|
||||||
|
provider: 'narakeet',
|
||||||
|
},
|
||||||
|
hrv: {
|
||||||
|
name: 'Hrvatski',
|
||||||
|
language: 'hrv',
|
||||||
|
voice: 'jasna',
|
||||||
|
provider: 'narakeet',
|
||||||
|
},
|
||||||
|
srp: {
|
||||||
|
name: 'Српски',
|
||||||
|
language: 'srp',
|
||||||
|
voice: 'milica-latin',
|
||||||
|
provider: 'narakeet',
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
name: 'Italiano',
|
||||||
|
language: 'it-IT',
|
||||||
|
voice: 'Bianca',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
name: '日本語',
|
||||||
|
language: 'ja-JP',
|
||||||
|
voice: 'Mizuki',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
name: '한국어',
|
||||||
|
language: 'ko-KR',
|
||||||
|
voice: 'Seoyeon',
|
||||||
|
engine: 'neural',
|
||||||
|
},
|
||||||
|
nb: {
|
||||||
|
name: 'Norsk',
|
||||||
|
language: 'nb-NO',
|
||||||
|
voice: 'Liv',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
name: 'Nederlands',
|
||||||
|
language: 'nl-NL',
|
||||||
|
voice: 'Ruben',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
name: 'Polski',
|
||||||
|
language: 'pl-PL',
|
||||||
|
voice: 'Ewa',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
name: 'Português (europeu)',
|
||||||
|
language: 'pt-PT',
|
||||||
|
voice: 'Cristiano',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
br: {
|
||||||
|
name: 'Português (brasileiro)',
|
||||||
|
language: 'pt-BR',
|
||||||
|
voice: 'Vitoria',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
ro: {
|
||||||
|
name: 'Română',
|
||||||
|
language: 'ro-RO',
|
||||||
|
voice: 'Carmen',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
name: 'Русский',
|
||||||
|
language: 'ru-RU',
|
||||||
|
voice: 'Tatyana',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
se: {
|
||||||
|
name: 'Svenska',
|
||||||
|
language: 'sv-SE',
|
||||||
|
voice: 'Astrid',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
name: 'Türkçe',
|
||||||
|
language: 'tr-TR',
|
||||||
|
voice: 'Filiz',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
cn: {
|
||||||
|
name: '中文(普通话)',
|
||||||
|
language: 'cmn-CN',
|
||||||
|
voice: 'Zhiyu',
|
||||||
|
engine: 'standard',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type VoiceKey = keyof typeof voices;
|
Loading…
x
Reference in New Issue
Block a user