Merge branch 'configurable-voice-p' into 'main'

configurable voices

See merge request PronounsPage/PronounsPage!652
This commit is contained in:
Valentyne Stigloher 2025-09-17 09:21:21 +00:00
commit d5656ef739
45 changed files with 461 additions and 360 deletions

View File

@ -4,7 +4,6 @@ import type { Opinion } from '#shared/opinions.ts';
const props = withDefaults(defineProps<{
word: string;
pronunciation?: string | null;
opinion: string;
link?: unknown;
escape?: boolean;
@ -42,7 +41,7 @@ const op = computed((): (Opinion & { description: string }) | null => {
</Tooltip>
<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" />
<Pronunciation v-if="pronunciation" :pronunciation="pronunciation" text />
<slot></slot>
</span>
</template>

View File

@ -186,10 +186,16 @@ const usedOpinions = computed(() => {
:opinion="s.el.opinion"
:escape="false"
:markdown="profile.markdown"
:pronunciation="s.el.pronunciation"
:link="config.locale === 'tok' && config.pronouns.enabled ? `${config.pronouns.prefix}/${s.el.value}` : null"
: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>
</ExpandableList>
</div>

View File

@ -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>
<span class="text-nowrap">
<span v-if="text" class="text-pronunciation">
<span v-if="text" class="text-secondary-emphasis fw-normal">
{{ pronunciation }}
</span>
<PronunciationSpeaker
v-for="voice in voices"
:key="voice"
:pronunciation="pronunciation"
:voice="voice"
:pronunciation
:voice
:show-voice-label="voices.length > 1"
class="btn btn-sm btn-link px-1 py-0"
/>
</span>
</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>

View File

@ -1,5 +1,58 @@
<script setup lang="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';
const pronounciationModelValue = defineModel<string | null>('pronounciation', { required: true });
const voiceModelValue = defineModel<VoiceKey | null>('voice', { required: true });
const config = useConfig();
const rawPronunciation = computed({
get(): string {
if (pronounciationModelValue.value) {
const phonemes = pronounciationModelValue.value.substring(1, pronounciationModelValue.value.length - 1);
return unescapePronunciationString(phonemes);
} else {
return '';
}
},
set(rawPronunciation: string) {
let pronunciation;
if (rawPronunciation) {
pronunciation = `/${escapePronunciationString(rawPronunciation)}/`;
} else {
pronunciation = null;
}
pronounciationModelValue.value = pronunciation;
},
});
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>
<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>
<input
v-model="rawPronunciation"
@ -8,62 +61,28 @@
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"
/>
<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>
<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';
export default defineComponent({
props: {
modelValue: { default: null, type: String as PropType<string | null> },
},
emits: ['update:modelValue'],
setup() {
return {
config: useConfig(),
};
},
computed: {
rawPronunciation: {
get(): string {
if (this.modelValue) {
const phonemes = this.modelValue.substring(1, this.modelValue.length - 1);
return unescapePronunciationString(phonemes);
} else {
return '';
}
},
set(rawPronunciation: string) {
let pronunciation;
if (rawPronunciation) {
pronunciation = `/${escapePronunciationString(rawPronunciation)}/`;
} else {
pronunciation = null;
}
this.$emit('update:modelValue', pronunciation);
},
},
voices(): string[] {
if (this.config.pronunciation?.enabled) {
return Object.keys(this.config.pronunciation.voices);
} else {
return [];
}
},
},
});
</script>

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
import { Base64 } from 'js-base64';
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
const props = withDefaults(defineProps<{
pronunciation?: string | null;
voice: string;
button?: boolean;
voice: VoiceKey;
showVoiceLabel?: boolean;
}>(), {
pronunciation: null,
});
@ -56,18 +58,21 @@ const icon = computed((): string => {
});
const config = useConfig();
const name = computed((): string | null => {
const voiceLabel = computed((): string | null => {
if (!props.showVoiceLabel) {
return null;
}
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]) {
// dont show voice name if it is considered the main voice for this locale
return null;
}
return props.voice;
return props.voice.toUpperCase();
});
</script>
@ -81,6 +86,6 @@ const name = computed((): string | null => {
target="_blank"
@click.prevent="pronounce"
>
<Icon :v="icon" /><sub v-if="name">{{ name }}</sub>
<Icon :v="icon" /><sub v-if="voiceLabel">{{ voiceLabel }}</sub>
</Tooltip>
</template>

View File

@ -460,18 +460,24 @@ const propagateChanged = (field: string, checked: boolean): void => {
<T>profile.names</T>
</template>
<template #names>
<p v-if="$te('profile.namesInfo')" class="small text-muted">
<T>profile.namesInfo</T>
</p>
<div class="alert alert-info">
<p class="small mb-0">
<Icon v="info-circle" />
<T>profile.namesInfo</T>
</p>
</div>
<OpinionListInput
v-model="formData.names"
:prototype="{ value: '', opinion: 'meh', pronunciation: null }"
:prototype="{ value: '', opinion: 'meh', pronunciation: null, voice: null }"
:custom-opinions="formData.opinions"
:maxitems="128"
:maxlength="config.profile.longNames ? 255 : 32"
>
<template #additional="s">
<PronunciationInput v-model="s.val.pronunciation" />
<PronunciationInput
v-model:pronounciation="s.val.pronunciation"
v-model:voice="s.val.voice"
/>
</template>
</OpinionListInput>
<InlineMarkdownInstructions v-model="formData.markdown" />

View File

@ -99,11 +99,7 @@ pronouns:
pronunciation:
enabled: true
voices:
GB:
language: 'en-GB'
voice: 'Emma'
engine: 'neural'
voices: ['gb']
sources:
enabled: true

View File

@ -726,8 +726,17 @@ user:
profile:
description: 'Description'
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:
ipa: 'Pronunciation using IPA'
voice:
without: '(without voice)'
pronouns: 'Pronouns'
pronounsInfo: >
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)

View File

@ -51,15 +51,7 @@ pronouns:
pronunciation:
enabled: true
voices:
AR:
language: 'arb'
voice: 'Zeina'
engine: 'standard'
AE:
language: 'ar-AE'
voice: 'Hala'
engine: 'neural'
voices: ['ar', 'ae']
sources:
enabled: false

View File

@ -1,7 +1,6 @@
import type { Engine, LanguageCode, VoiceId } from '@aws-sdk/client-polly';
import type { Category } from '#shared/classes.ts';
import type { GrammarTablesDefinition } from '#shared/language/grammarTables.ts';
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
import type { LocaleCode } from '~~/locale/locales.ts';
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
*/
ipa?: boolean;
voices: Record<string, PronunciationVoiceConfig>;
}
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;
voices: VoiceKey[];
}
interface SourcesConfig {

View File

@ -488,11 +488,7 @@ pronouns:
pronunciation:
enabled: true
voices:
DE:
language: 'de-DE'
voice: 'Vicki'
engine: 'standard'
voices: ['de']
sources:
enabled: true

View File

@ -842,12 +842,21 @@ user:
profile:
description: 'Beschreibung'
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:
ipa: 'Aussprache in IPA'
voice:
without: '(ohne Sprachausgabe)'
pronouns: 'Pronomen'
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“)
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.
pronounsNotFound: 'Nicht erkennbares Format. Bitte beachte die oben genannte Anweisung.'
words: 'Wörter'

View File

@ -149,11 +149,7 @@ pronouns:
pronunciation:
enabled: true
voices:
GB:
language: 'en-GB'
voice: 'Emma'
engine: 'neural'
voices: ['gb']
sources:
enabled: true

View File

@ -942,8 +942,17 @@ user:
profile:
description: 'Description'
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:
ipa: 'Pronunciation using IPA'
voice:
without: '(without voice)'
pronouns: 'Pronouns'
pronounsInfo: >
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)

View File

@ -97,7 +97,6 @@ pronouns:
pronunciation:
enabled: false
voices: {}
sources:
enabled: true

View File

@ -128,15 +128,7 @@ pronouns:
pronunciation:
enabled: true
voices:
ES:
language: 'es-ES'
voice: 'Lucia'
engine: 'standard'
MX:
language: 'es-MX'
voice: 'Mia'
engine: 'standard'
voices: ['es', 'mx']
sources:
enabled: true

View File

@ -37,11 +37,7 @@ pronouns:
pronunciation:
enabled: false
voices:
FI:
language: 'fi-FI'
voice: 'Suvi'
engine: 'neural'
voices: ['fi']
sources:
enabled: true

View File

@ -133,11 +133,6 @@ pronouns:
pronunciation:
enabled: false
voices:
GB:
language: 'en-GB'
voice: 'Emma'
engine: 'neural'
sources:
enabled: true

View File

@ -65,15 +65,7 @@ pronouns:
pronunciation:
enabled: true
voices:
FR:
language: 'fr-FR'
voice: 'Lea'
engine: 'standard'
CA:
language: 'fr-CA'
voice: 'Gabrielle'
engine: 'neural'
voices: ['fr', 'ca']
sources:
enabled: true

View File

@ -79,8 +79,7 @@ pronouns:
shortMorphemes: 3
pronunciation:
enabled: true
voices: {}
enabled: false
sources:
enabled: true

View File

@ -98,19 +98,7 @@ pronouns:
pronunciation:
enabled: true
voices:
BOS:
language: 'bos'
voice: 'suada'
provider: 'narakeet'
HRV:
language: 'hrv'
voice: 'jasna'
provider: 'narakeet'
SRP:
language: 'srp'
voice: 'milica-latin'
provider: 'narakeet'
voices: ['bos', 'hrv', 'srp']
sources:
enabled: true

View File

@ -44,12 +44,7 @@ pronouns:
others: 'Other pronouns'
pronunciation:
enabled: true
voices:
GB:
language: 'en-GB'
voice: 'Emma'
engine: 'neural'
enabled: false
sources:
enabled: true

View File

@ -324,11 +324,7 @@ pronouns:
pronunciation:
enabled: true
voices:
IT:
language: 'it-IT'
voice: 'Bianca'
engine: 'neural'
voices: ['it']
sources:
enabled: true

View File

@ -45,11 +45,7 @@ pronouns:
pronunciation:
enabled: true
voices:
JA:
language: 'ja-JP'
voice: 'Mizuki'
engine: 'standard'
voices: ['ja']
sources:
enabled: true

View File

@ -40,11 +40,7 @@ pronouns:
pronunciation:
enabled: true
voices:
KO:
language: 'ko-KR'
voice: 'Seoyeon'
engine: 'neural'
voices: ['ko']
sources:
enabled: true

View File

@ -120,11 +120,7 @@ pronouns:
pronunciation:
enabled: true
voices:
NO:
language: 'nb-NO'
voice: 'Liv'
engine: 'standard'
voices: ['no']
sources:
enabled: true

View File

@ -71,11 +71,7 @@ pronouns:
pronunciation:
enabled: true
voices:
GB:
language: 'nl-NL'
voice: 'Ruben'
engine: 'standard'
voices: ['nl']
sources:
enabled: true

View File

@ -121,11 +121,7 @@ pronouns:
pronunciation:
enabled: true
voices:
NO:
language: 'nb-NO'
voice: 'Liv'
engine: 'standard'
voices: ['no']
sources:
enabled: true

View File

@ -322,11 +322,7 @@ pronouns:
pronunciation:
enabled: true
voices:
PL:
language: 'pl-PL'
voice: 'Ewa'
engine: 'standard'
voices: ['pl']
sources:
enabled: true

View File

@ -81,15 +81,7 @@ pronouns:
pronunciation:
enabled: true
voices:
PT:
language: 'pt-PT'
voice: 'Cristiano'
engine: 'standard'
BR:
language: 'pt-BR'
voice: 'Vitoria'
engine: 'standard'
voices: ['pt', 'br']
sources:
enabled: true

View File

@ -113,11 +113,7 @@ pronouns:
pronunciation:
enabled: true
voices:
RO:
language: 'ro-RO'
voice: 'Carmen'
engine: 'standard'
voices: ['ro']
sources:
enabled: true

View File

@ -162,11 +162,7 @@ pronouns:
pronunciation:
enabled: true
voices:
GB:
language: 'ru-RU'
voice: 'Tatyana'
engine: 'standard'
voices: ['ru']
sources:
enabled: true

View File

@ -50,11 +50,7 @@ pronouns:
pronunciation:
enabled: true
voices:
SE:
language: 'sv-SE'
voice: 'Astrid'
engine: 'standard'
voices: ['se']
sources:
enabled: true

View File

@ -29,11 +29,7 @@ pronouns:
pronunciation:
enabled: false
ipa: true
voices:
TOK:
language: 'es-US'
voice: 'Lupe'
engine: 'standard'
voices: ['us']
sources:
enabled: false

View File

@ -11,11 +11,7 @@ pronouns:
pronunciation:
enabled: true
voices:
TR:
language: 'tr-TR'
voice: 'Filiz'
engine: 'standard'
voices: ['tr']
sources:
enabled: false

View File

@ -83,7 +83,6 @@ pronouns:
pronunciation:
enabled: false
voices: {}
sources:
enabled: true

View File

@ -35,7 +35,6 @@ pronouns:
pronunciation:
enabled: false
voices: {}
sources:
enabled: true

View File

@ -35,11 +35,7 @@ pronouns:
pronunciation:
enabled: true
voices:
CN:
language: 'cmn-CN'
voice: 'Zhiyu'
engine: 'standard'
voices: ['cn']
sources:
enabled: true

View 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

View File

@ -2,31 +2,21 @@ import { Polly } from '@aws-sdk/client-polly';
import { NoSuchKey } from '@aws-sdk/client-s3';
import type { S3 } from '@aws-sdk/client-s3';
import type { NodeJsClient } from '@smithy/types';
import { Router } from 'express';
import { getH3Event } from 'h3-express';
import { Base64 } from 'js-base64';
import sha1 from 'sha1';
import { s3, awsConfig, s3BucketParams } from '../cloudServices.ts';
import {
convertPronunciationStringToSsml,
convertPronunciationStringToNarakeetFormat,
handleErrorAsync,
sha256,
} from '#shared/helpers.ts';
import type {
PronunciationVoiceConfig,
AwsPollyPronunciationVoiceConfig,
NarakeetPronunciationVoiceConfig,
} from '~~/locale/config.ts';
import { getLocale, loadConfig, localesDict } from '~~/server/data.ts';
const router = Router();
import { voices } from '#shared/pronunciation/voices.ts';
import type { VoiceKey, AwsPollyVoice, NarakeetVoice, Voice } from '#shared/pronunciation/voices.ts';
import { s3, awsConfig, s3BucketParams } from '~~/server/cloudServices.ts';
type ProviderKey = 'aws_polly' | 'narakeet';
interface Provider {
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> = {
@ -34,7 +24,7 @@ const providers: Record<ProviderKey, Provider> = {
tokenised(text: string): string {
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 pollyResponse = await polly.synthesizeSpeech({
@ -56,7 +46,7 @@ const providers: Record<ProviderKey, Provider> = {
tokenised(text: string): string {
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 response = await fetch(url, {
@ -81,29 +71,40 @@ const providers: Record<ProviderKey, Provider> = {
},
};
router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res) => {
const text = Base64.decode(req.params.pronunciation);
export default defineEventHandler(async (event) => {
const text = Base64.decode(getRouterParam(event, 'pronunciation')!);
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 config = await loadConfig(locale);
const voice: PronunciationVoiceConfig | undefined = config.pronunciation?.voices?.[req.params.voice];
if (!voice) {
return res.status(404).json({ error: 'Not found' });
const voiceKey = getRouterParam(event, 'voice');
if (voiceKey === undefined || !(voiceKey in voices)) {
throw createError({
status: 404,
statusMessage: 'Not Found',
});
}
const voice = voices[voiceKey as VoiceKey];
const provider = providers[(voice.provider || 'aws_polly') as ProviderKey];
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 {
const s3Response = await (s3 as NodeJsClient<S3>).getObject({ Key: key, ...s3BucketParams });
res.set('content-type', s3Response.ContentType);
return s3Response.Body!.pipe(res);
setResponseHeader(event, 'content-type', s3Response.ContentType);
return s3Response.Body;
} catch (error) {
if (!(error instanceof NoSuchKey)) {
throw error;
@ -111,20 +112,15 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res)
const [buffer, contentType] = await provider.generate(tokenised, voice);
if (localesDict[locale]?.published) {
// don't populate the cache for locales still under construction
await s3.putObject({
Key: key,
Body: buffer,
ContentType: contentType,
...s3BucketParams,
});
}
// don't populate the cache for locales still under construction
await s3.putObject({
Key: key,
Body: buffer,
ContentType: contentType,
...s3BucketParams,
});
res.set('content-type', contentType);
res.write(buffer);
return res.end();
setResponseHeader(event, 'content-type', contentType);
return buffer;
}
}));
export default router;
});

View File

@ -5,12 +5,10 @@ import { buildCalendar } from '#shared/calendar/calendar.ts';
import type { Calendar } from '#shared/calendar/helpers.ts';
import { PronounLibrary } 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 { Translator } from '#shared/translator.ts';
import type { Config } from '~~/locale/config.ts';
import type { PronounData, PronounExamplesData } from '~~/locale/data.ts';
import locales from '~~/locale/locales.ts';
import type { LocaleCode } from '~~/locale/locales.ts';
import { loadSuml, loadTsv } from '~~/server/loader.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;
};
export const localesDict = buildDict(function* () {
for (const locale of locales) {
yield [locale.code, locale];
}
});
export const getLocale = (event: H3Event): LocaleCode => {
const query = getQuery(event);
if (typeof query.locale === 'string') {

View File

@ -348,7 +348,7 @@ const fetchProfiles = async (
opinions: propv('opinions', () => JSON.parse(profile.opinions)),
names: propv('names', () => {
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)),

View File

@ -19,7 +19,6 @@ import grantOverridesRoute from './express/grantOverrides.ts';
import imagesRoute from './express/images.ts';
import mfaRoute from './express/mfa.ts';
import profileRoute from './express/profile.ts';
import pronounceRoute from './express/pronounce.ts';
import sentryRoute from './express/sentry.ts';
import subscriptionRoute from './express/subscription.ts';
import translationsRoute from './express/translations.ts';
@ -148,7 +147,6 @@ router.use(userRoute);
router.use(profileRoute);
router.use(adminRoute);
router.use(mfaRoute);
router.use(pronounceRoute);
router.use(censusRoute);
router.use(imagesRoute);
router.use(calendarRoute);

View File

@ -2,6 +2,7 @@ import type { CustomEvent } from './calendar/helpers.ts';
import type { Opinion } from './opinions.ts';
import type { User } from './user.ts';
import type { VoiceKey } from '#shared/pronunciation/voices.ts';
import type { LocaleCode } from '~~/locale/locales.ts';
export interface UserWithProfiles {
@ -63,6 +64,7 @@ export interface ValueOpinion {
export interface NameOpinion extends ValueOpinion {
pronunciation: string;
voice: VoiceKey | null;
}
export interface Timezone {

View 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;