Skip to content

Commit

Permalink
feat: different elevenlabs voices
Browse files Browse the repository at this point in the history
  • Loading branch information
nekomeowww committed Dec 28, 2024
1 parent 4564c67 commit ae504d0
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 33 deletions.
3 changes: 3 additions & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dictionaryDefinitions: []
dictionaries: []
words:
- acubismmotion
- Aerisita
- airi
- airi-vtuber
- Attributify
Expand Down Expand Up @@ -40,12 +41,14 @@ words:
- hyoban
- iconify
- intlify
- Kawaii
- kwaa
- live2dcubismcore
- live2dcubismframework
- Llmmarker
- Maru
- micvad
- Morioki
- Myriam
- Neko
- nekomeowww
Expand Down
2 changes: 2 additions & 0 deletions packages/stage/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ settings:
label: OpenAI API BaseURL
placeholder: Input your API base URL
placeholder_mobile: OpenAI API BaseURL
voices: Voice
stage:
message: Message
select-a-audio-input: Select a Audio Input
select-a-model: Select a model
select-a-voice: Choose a voice
waiting: Waiting
2 changes: 2 additions & 0 deletions packages/stage/locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ settings:
label: OpenAI API BaseURL
placeholder: 输入您的 API BaseURL
placeholder_mobile: OpenAI BaseURL
voices: 声线
stage:
message: 消息
select-a-audio-input: 选择一个音频输入设备
select-a-model: 选择一个模型
select-a-voice: 选择一个声线
waiting: 等待中
54 changes: 51 additions & 3 deletions packages/stage/src/components/Settings.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
<script setup lang="ts">
import type { Voice } from '../constants/elevenlabs'
import { useDark } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n'
import { voiceList } from '../constants/elevenlabs'
import { useLLM } from '../stores/llm'
import { useSettings } from '../stores/settings'
import TransitionVertical from './TransitionVertical.vue'
const { t } = useI18n()
const { t, locale } = useI18n()
const settings = useSettings()
const show = ref(false)
const dark = useDark({ disableTransition: false })
const supportedModels = ref<{ id: string, name?: string }[]>([])
const { models } = useLLM()
const { openAiModel, openAiApiBaseURL, openAiApiKey } = storeToRefs(settings)
const { openAiModel, openAiApiBaseURL, openAiApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(settings)
function handleModelChange(event: Event) {
const target = event.target as HTMLSelectElement
Expand All @@ -28,6 +30,26 @@ function handleModelChange(event: Event) {
openAiModel.value = found
}
function handleVoiceChange(event: Event) {
const value = (event.target as HTMLSelectElement).value as Voice
switch (locale.value) {
case 'en':
case 'en-US':
elevenlabsVoiceEnglish.value = value
break
case 'zh':
case 'zh-CN':
case 'zh-TW':
case 'zh-HK':
elevenlabsVoiceEnglish.value = value
break
case 'jp':
case 'jp-JP':
elevenlabsVoiceJapanese.value = value
break
}
}
watch([openAiApiBaseURL, openAiApiKey], async ([baseUrl, apiKey]) => {
if (!baseUrl || !apiKey) {
supportedModels.value = []
Expand Down Expand Up @@ -192,6 +214,32 @@ onMounted(async () => {
</option>
</select>
</div>
<div text-sm>
<span>{{ t('settings.voices') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<select
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono
outline-none
@change="handleVoiceChange"
>
<option disabled class="bg-white dark:bg-zinc-800">
{{ t('stage.select-a-voice') }}
</option>
<option v-if="['en', 'en-US'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
{{ elevenlabsVoiceEnglish }}
</option>
<option v-if="['zh', 'zh-CN', 'zh-TW', 'zh-HK'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
{{ elevenlabsVoiceEnglish }}
</option>
<option v-if="['jp', 'jp-JP'].indexOf(locale) !== -1 && elevenlabsVoiceJapanese" :value="elevenlabsVoiceJapanese">
{{ elevenlabsVoiceJapanese }}
</option>
<option v-for="(m, index) in voiceList[locale]" :key="index" :value="m">
{{ m }}
</option>
</select>
</div>
</div>
</TransitionVertical>
</div>
Expand Down
76 changes: 61 additions & 15 deletions packages/stage/src/components/Widgets/MobileSettings.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<script setup lang="ts">
import type { Voice } from '../../constants/elevenlabs'
import { useDark } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { voiceList } from '../../constants/elevenlabs'
import { useLLM } from '../../stores/llm'
import { useSettings } from '../../stores/settings'
const { t } = useI18n()
const { t, locale } = useI18n()
const settings = useSettings()
const dark = useDark({ disableTransition: false })
const supportedModels = ref<{ id: string, name?: string }[]>([])
const { models } = useLLM()
const { openAiModel, openAiApiBaseURL, openAiApiKey } = storeToRefs(settings)
const { openAiModel, openAiApiBaseURL, openAiApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(settings)
function handleModelChange(event: Event) {
const target = event.target as HTMLSelectElement
Expand All @@ -31,6 +33,26 @@ function handleViewChange(event: Event) {
settings.stageView = target.value
}
function handleVoiceChange(event: Event) {
const value = (event.target as HTMLSelectElement).value as Voice
switch (locale.value) {
case 'en':
case 'en-US':
elevenlabsVoiceEnglish.value = value
break
case 'zh':
case 'zh-CN':
case 'zh-TW':
case 'zh-HK':
elevenlabsVoiceEnglish.value = value
break
case 'jp':
case 'jp-JP':
elevenlabsVoiceJapanese.value = value
break
}
}
watch([openAiApiBaseURL, openAiApiKey], async ([baseUrl, apiKey]) => {
if (!baseUrl || !apiKey) {
supportedModels.value = []
Expand All @@ -53,7 +75,7 @@ onMounted(async () => {
<h2 text="slate-800/80 dark:slate-200/80 xl" font-bold>
Settings
</h2>
<div flex="~" gap-2>
<div>
<div
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
bg="[#fff6fc] dark:[#2c2529]" px-2 py-1 text="pink-400"
Expand All @@ -66,7 +88,7 @@ onMounted(async () => {
v-model="settings.openAiApiBaseURL"
type="text"
:placeholder="t('settings.openai-base-url.placeholder_mobile')"
w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
>
</div>
<div text="sm pink-500">
Expand All @@ -77,7 +99,7 @@ onMounted(async () => {
v-model="settings.openAiApiKey"
type="text"
:placeholder="t('settings.openai-api-key.placeholder_mobile')"
w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
>
</div>
<div text="sm pink-500">
Expand All @@ -88,7 +110,7 @@ onMounted(async () => {
v-model="settings.elevenLabsApiKey"
type="text"
:placeholder="t('settings.elevenlabs-api-key.placeholder_mobile')"
w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
>
</div>
<div text="sm pink-500">
Expand All @@ -97,7 +119,7 @@ onMounted(async () => {
<div flex="~ row" w-full text="sm">
<select
v-model="settings.language"
w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
>
<option value="en-US">
English
Expand All @@ -112,7 +134,7 @@ onMounted(async () => {
</div>
<div flex="~ row" w-full text="sm">
<select
w-full rounded-md bg-transparent px-2 py-1 font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
@change="handleModelChange"
>
<option disabled class="bg-white dark:bg-zinc-800">
Expand All @@ -126,6 +148,32 @@ onMounted(async () => {
</option>
</select>
</div>
<div text="sm pink-500">
<span>{{ t('settings.voices') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<select
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
@change="handleVoiceChange"
>
<option disabled class="bg-white dark:bg-zinc-800">
{{ t('stage.select-a-voice') }}
</option>
<option v-if="['en', 'en-US'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
{{ elevenlabsVoiceEnglish }}
</option>
<!-- TODO -->
<option v-if="['zh', 'zh-CN', 'zh-TW', 'zh-HK'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
{{ elevenlabsVoiceEnglish }}
</option>
<option v-if="['jp', 'jp-JP'].indexOf(locale) !== -1 && elevenlabsVoiceJapanese" :value="elevenlabsVoiceJapanese">
{{ elevenlabsVoiceJapanese }}
</option>
<option v-for="(m, index) in voiceList[locale]" :key="index" :value="m">
{{ m }}
</option>
</select>
</div>
</div>
</div>
<h2 text="slate-800/80 dark:slate-200/80 xl" font-bold>
Expand All @@ -140,7 +188,7 @@ onMounted(async () => {
<span>Viewer</span>
</div>
<select
w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
h-8 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
@change="handleViewChange"
>
<option value="2d">
Expand All @@ -153,9 +201,7 @@ onMounted(async () => {
<div text="sm pink-500">
<span>Theme</span>
</div>
<label
cursor-pointer px-2 py-1
>
<label h-8 flex cursor-pointer items-center justify-end>
<input
v-model="dark"
:checked="dark"
Expand All @@ -164,10 +210,10 @@ onMounted(async () => {
type="checkbox"
hidden appearance-none outline-none
>
<div flex select-none justify-end>
<div select-none>
<Transition name="slide-away" mode="out-in">
<div v-if="dark" i-solar:sun-fog-bold-duotone text="base hover:pink-600 dark:hover:pink-300" transition="all ease-in-out duration-250" />
<div v-else i-solar:moon-stars-bold-duotone text="base hover:pink-600 dark:hover:pink-300" transition="all ease-in-out duration-250" />
<div v-if="dark" i-solar:sun-fog-bold-duotone text="lg hover:pink-600 dark:hover:pink-300" transition="all ease-in-out duration-250" />
<div v-else i-solar:moon-stars-bold-duotone text="lg hover:pink-600 dark:hover:pink-300" transition="all ease-in-out duration-250" />
</Transition>
</div>
</label>
Expand Down
15 changes: 12 additions & 3 deletions packages/stage/src/components/Widgets/Stage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import type { Emotion } from '../../constants/emotions'
import { storeToRefs } from 'pinia'
import { onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMarkdown } from '../../composables/markdown'
import { useQueue } from '../../composables/queue'
import { useDelayMessageQueue, useEmotionsMessageQueue, useMessageContentQueue } from '../../composables/queues'
import { llmInferenceEndToken } from '../../constants'
import { Voice } from '../../constants/elevenlabs'
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
import { useChatStore } from '../../stores/chat'
import { useLLM } from '../../stores/llm'
import { useSettings } from '../../stores/settings'
import Live2DScene from '../Scenes/Live2D.vue'
Expand All @@ -19,12 +21,13 @@ import VRMScene from '../Scenes/VRM.vue'
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
const { stageView, elevenLabsApiKey } = storeToRefs(useSettings())
const { stageView, elevenLabsApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(useSettings())
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
const { audioContext, calculateVolume } = useAudioContext()
const { streamSpeech } = useLLM()
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd, streamingMessage } = useChatStore()
const { process } = useMarkdown()
const { locale } = useI18n()
const audioAnalyser = ref<AnalyserNode>()
const nowSpeaking = ref(false)
Expand Down Expand Up @@ -58,11 +61,17 @@ const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
const ttsQueue = useQueue<string>({
handlers: [
async (ctx) => {
let voice = Voice.Camilla_KM
if (locale.value === 'jp' || locale.value === 'jp-JP')
voice = elevenlabsVoiceJapanese.value
else
voice = elevenlabsVoiceEnglish.value
const now = Date.now()
const res = await streamSpeech('https://airi-api.ayaka.io', elevenLabsApiKey.value, ctx.data, {
// voice: 'ShanShan',
// Quite good for English
voice: 'Myriam',
voice,
// Beatrice is not 'childish' like the others
// voice: 'Beatrice',
model_id: 'eleven_multilingual_v2',
Expand Down
43 changes: 43 additions & 0 deletions packages/stage/src/constants/elevenlabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export enum Voice {
// English
Myriam = 'Myriam',
Beatrice = 'Beatrice',
Camilla_KM = 'Camilla_KM',
SallySunshine = 'Sally Sunshine',
Annie = 'Annie',
KawaiiAerisita = 'Kawaii Aerisita',
// Japanese
Morioki = 'Morioki',
}

export const voiceMap: Record<Voice, string> = {
// English
[Voice.Myriam]: 'lNxY9WuCBCZCISASyJ55',
[Voice.Beatrice]: 'KAsXoQDshjF6ehsWa1mF',
[Voice.Camilla_KM]: 'dLhSyo03JRp5WkGpUlz1',
[Voice.SallySunshine]: 'qswttdunP3b44zVZKMRB',
[Voice.Annie]: 'AfA1PA0ldViH0DA6pbml',
[Voice.KawaiiAerisita]: 'vGQNBgLaiM3EdZtxIiuY',
// Japanese
[Voice.Morioki]: '8EkOjt4xTPGMclNlh1pk',
}

export const enVoiceList = [
Voice.Myriam,
Voice.Beatrice,
Voice.Camilla_KM,
Voice.SallySunshine,
Voice.Annie,
Voice.KawaiiAerisita,
]

export const jaVoiceList = [
Voice.Morioki,
]

export const voiceList: Record<string, Voice[]> = {
'en': enVoiceList,
'en-US': enVoiceList,
'ja': jaVoiceList,
'ja-JP': jaVoiceList,
}
Loading

0 comments on commit ae504d0

Please sign in to comment.