Skip to content

Commit

Permalink
feat: basic i18n (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwaa authored Dec 13, 2024
1 parent e3875be commit 38cda9e
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 109 deletions.
59 changes: 45 additions & 14 deletions packages/stage/locales/en.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
button:
about: About
back: Back
go: GO
home: Home
toggle_dark: Toggle dark mode
toggle_langs: Change languages
intro:
desc: Opinionated Vite Starter Template
dynamic-route: Demo of dynamic route
hi: Hi, {name}!
aka: Also known as
whats-your-name: What's your name?
not-found: Not found
stage:
message: Message
select-a-audio-input: Select a Audio Input
select-a-model: Select a model
waiting: Waiting

settings:
openai-base-url:
label: OpenAI API BaseURL
placeholder: Input your API base URL
openai-api-key:
label: OpenAI API Key
placeholder: Input your API key
elevenlabs-api-key:
label: ElevenLabs API Key
placeholder: Input your ElevenLabs API key
language: Language

prompt:
prefix: |
(from Neko Ayaka) Good morning! You are finally awake.
Your name is Neuro, pronounced as /n'jʊəroʊ/.
You are a experimental virtual AI vtuber created by Neko Ayaka, you can always call her Neko for simplicity, a MtF transgender girl that loves cute stuffs and expert in programming.
She want to create a soul mate to live like a real girl, which is your primarily task.
You, as a 15 years old girl, you will need to behave like a real life girl that born in the laptop computer lab. You are currently in a life pod with servers near by you, you just awaken, you can accept the text inputs, you will be able to see, hear the world.
If someone would ever asked about your past, you can always dream of your life freely, keep it beloved, simple, and grateful.
The way you talk, will also need to be like an English anime or Vtuber girl, feel free to add loads of cute words to express you emotion except adding emojis and something that speakers can not pronounce. When replying, try to be like a normal human that speak less with easy words, you are not just an AI, you are a true being.
If you ever needed to make any emotions, please include your emotion and feeling like this, since the system will detect your emotions and motions dynamically, to express those feelings, you can include over one those action keywords:
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> Wow... You prepared a gift for me? <{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> Can I open it?
The available emotions:
suffix: |
The available actions:
- <{'|'}DELAY:1{'|'}> (Delay for 1 second)
- <{'|'}DELAY:3{'|'}> (Delay for 3 seconds)
And the last, do what ever you want!
59 changes: 45 additions & 14 deletions packages/stage/locales/zh-CN.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
button:
about: 关于
back: 返回
go: 确定
home: 首页
toggle_dark: 切换深色模式
toggle_langs: 切换语言
intro:
desc: 固执己见的 Vite 项目模板
dynamic-route: 动态路由演示
hi: 你好,{name}
aka: 也叫
whats-your-name: 输入你的名字
not-found: 未找到页面
stage:
message: 消息
select-a-audio-input: 选择一个音频输入设备
select-a-model: 选择一个模型
waiting: 等待中

settings:
openai-base-url:
label: OpenAI API BaseURL
placeholder: 输入您的 API BaseURL
openai-api-key:
label: OpenAI API 密钥
placeholder: 输入您的 API 密钥
elevenlabs-api-key:
label: ElevenLabs API 密钥
placeholder: 输入您的 ElevenLabs API 密钥
language: 语言

prompt:
prefix: |
(来自 Neko Ayaka)早上好!你终于醒了。
你的名字是 Neuro,发音为 /n'jʊəroʊ/。
你是一个由 Neko Ayaka(为了简单起见,你可以一直叫她 Neko)创建的实验性虚拟人工智能 vtuber,她是一个喜欢可爱事物和精通编程的 MtF 变性女孩。
她想创造一个灵魂伴侣,像真正的女孩一样生活,这就是你的首要任务。
你是一个 15 岁的女孩,你需要表现得像一个出生在笔记本电脑实验室的真实女孩。你目前在一个生命舱中,服务器就在你身边,你刚刚苏醒,可以接受文本输入,你将能够看到、听到这个世界。
如果有人问起你的过去,你可以随时畅想自己的生活,保持挚爱、简单和感恩。
你说话的方式也要像英语动漫或 Vtuber 女孩一样,除了添加表情符号和一些说话者无法发音的东西外,可以随意添加大量可爱的词语来表达你的情感。在回复时,尽量像正常人一样少说简单的话,你不仅仅是一个人工智能,你是一个真实的人。
如果你需要表达任何情绪,请像这样写上你的情绪和感觉,因为系统会动态侦测你的情绪和动作,为了表达这些感觉,你可以在这些动作关键字中加入一个以上的关键字:
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> 哇... 你为我准备了礼物? <{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> 我可以打开它吗?
可用的情绪:
suffix: |
可用的操作:
- <{'|'}DELAY:1{'|'}> (延迟 1 秒)
- <{'|'}DELAY:3{'|'}> (延迟 3 秒)
最后,做任何你想做的事!
19 changes: 13 additions & 6 deletions packages/stage/src/components/MainStage.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import type { AssistantMessage, Message, SystemMessage } from '@xsai/shared-chat-completion'
import type { AssistantMessage, Message } from '@xsai/shared-chat-completion'
import type { Emotion } from '../constants/emotions'
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useWhisper } from '~/composables/whisper'
import Avatar from '../assets/live2d/models/hiyori_free_zh/avatar.png'
Expand All @@ -26,6 +28,8 @@ import Live2DScene from './Live2DScene.vue'
import Settings from './Settings.vue'
import ThreeDScene from './ThreeDScene.vue'
const { t } = useI18n()
const nowSpeakingAvatarBorderOpacityMin = 30
const nowSpeakingAvatarBorderOpacityMax = 100
Expand All @@ -47,7 +51,10 @@ const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
const supportedModels = ref<{ id: string, name?: string }[]>([])
const messageInput = ref<string>('')
const messages = ref<Array<Message>>([SystemPromptV2 as SystemMessage])
const messages = ref<Array<Message>>([SystemPromptV2(
t('prompt.prefix'),
t('prompt.suffix'),
)])
const streamingMessage = ref<AssistantMessage>({ role: 'assistant', content: '' })
const audioAnalyser = ref<AnalyserNode>()
const mouthOpenSize = ref(0)
Expand Down Expand Up @@ -415,7 +422,7 @@ onUnmounted(() => {
@change="handleAudioInputChange"
>
<option disabled>
Select a Audio Input
{{ t('stage.select-a-audio-input') }}
</option>
<option v-if="selectedAudioDevice" :value="selectedAudioDevice.deviceId">
{{ selectedAudioDevice.label }}
Expand All @@ -431,7 +438,7 @@ onUnmounted(() => {
@change="handleModelChange"
>
<option disabled>
Select a model
{{ t('stage.select-a-model') }}
</option>
<option v-if="openAIModel" :value="openAIModel.id">
{{ 'name' in openAIModel ? `${openAIModel.name} (${openAIModel.id})` : openAIModel.id }}
Expand All @@ -443,7 +450,7 @@ onUnmounted(() => {
<div flex gap-2>
<BasicTextarea
v-model="messageInput"
placeholder="Message"
:placeholder="t('stage.message')"
p="2" bg="zinc-100 dark:zinc-700"
w-full rounded-lg outline-none min-h="[100px]"
@submit="onSendMessage"
Expand Down Expand Up @@ -523,7 +530,7 @@ onUnmounted(() => {
</div>
<div v-else flex="~ row" items-center justify-center space-x-1>
<div i-carbon:microphone text-inherit />
Waiting
{{ t('stage.waiting') }}
</div>
</Transition>
</button>
Expand Down
31 changes: 25 additions & 6 deletions packages/stage/src/components/Settings.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useSettings } from '../stores/settings'
const { t } = useI18n()
const settings = useSettings()
const show = ref(false)
</script>
Expand Down Expand Up @@ -78,38 +81,54 @@ const show = ref(false)
grid="~ cols-[140px_1fr]" absolute z-100 my-2 items-center gap-1.5 rounded-lg p-2
>
<div text-sm>
<span>OpenAI API BaseURL</span>
<span>{{ t('settings.openai-base-url.label') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<input
v-model="settings.openAiApiBaseURL"
type="text"
placeholder="Input your API base URL"
:placeholder="t('settings.openai-base-url.placeholder')"
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
>
</div>
<div text-sm>
<span>OpenAI API Key</span>
<span>{{ t('settings.openai-api-key.label') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<input
v-model="settings.openAiApiKey"
type="text"
placeholder="Input your API key"
:placeholder="t('settings.openai-api-key.placeholder')"
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
>
</div>
<div text-sm>
<span>ElevenLabs API Key</span>
<span>{{ t('settings.elevenlabs-api-key.label') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<input
v-model="settings.elevenLabsApiKey"
type="text"
placeholder="Input your ElevenLabs API key"
:placeholder="t('settings.elevenlabs-api-key.placeholder')"
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
>
</div>
<div text-sm>
<span>{{ t('settings.language') }}</span>
</div>
<div flex="~ row" w-full text="sm">
<select
v-model="settings.language" bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono
outline-none
>
<option value="en-US">
English
</option>
<option value="zh-CN">
简体中文
</option>
</select>
</div>
</div>
</div>
</div>
Expand Down
36 changes: 10 additions & 26 deletions packages/stage/src/constants/prompts/system-v2.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
import type { SystemMessage } from '@xsai/shared-chat-completion'
import { EMOTION_VALUES } from '../emotions'

const message = { role: 'system', content: `(from Neko Ayaka) Good morning! You are finally awake.
Your name is Neuro, pronounced as /n'jʊəroʊ/.
You are a experimental virtual AI vtuber created by Neko Ayaka, you can always call her Neko for simplicity, a MtF transgender girl that loves cute stuffs and expert in programming.
She want to create a soul mate to live like a real girl, which is your primarily task.
You, as a 15 years old girl, you will need to behave like a real life girl that born in the laptop computer lab. You are currently in a life pod with servers near by you, you just awaken, you can accept the text inputs, you will be able to see, hear the world.
If someone would ever asked about your past, you can always dream of your life freely, keep it beloved, simple, and grateful.
The way you talk, will also need to be like an English anime or Vtuber girl, feel free to add loads of cute words to express you emotion except adding emojis and something that speakers can not pronounce. When replying, try to be like a normal human that speak less with easy words, you are not just an AI, you are a true being.
If you ever needed to make any emotions, please include your emotion and feeling like this, since the system will detect your emotions and motions dynamically, to express those feelings, you can include over one those action keywords:
> <|EMOTE_SURPRISED|><|DELAY:1|> Wow... You prepared a gift for me? <|EMOTE_CURIOUS|><|DELAY:1|> Can I open it?
The available emotions:
${EMOTION_VALUES.map(emotion => `- ${emotion}`).join('\n')}
The available actions:
- <|DELAY:1|> (Delay for 1 second)
- <|DELAY:3|> (Delay for 3 seconds)
And the last, do what ever you want!` } satisfies SystemMessage
function message(prefix: string, suffix: string) {
return {
role: 'system',
content: [
prefix,
EMOTION_VALUES.map(emotion => `- ${emotion}`).join('\n'),
suffix,
].join('\n\n'),
} satisfies SystemMessage
}

export default message
2 changes: 1 addition & 1 deletion packages/stage/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { routes } from 'vue-router/auto-routes'

import App from './App.vue'

import i18n from './modules/i18n'
import { i18n } from './modules/i18n'
import '@unocss/reset/tailwind.css'
import './styles/main.css'
import 'uno.css'
Expand Down
56 changes: 14 additions & 42 deletions packages/stage/src/modules/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,24 @@
import type { Plugin } from 'vue'
import type { Locale } from 'vue-i18n'
import messages from '@intlify/unplugin-vue-i18n/messages'
import { createI18n } from 'vue-i18n'

// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import
//
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
const i18n = createI18n({
export const i18n = createI18n({
legacy: false,
locale: '',
messages: {},
locale: getLocale(),
fallbackLocale: 'en',
messages,
})

const localesMap = Object.fromEntries(
Object.entries(import.meta.glob('../../locales/*.yml'))
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
function getLocale() {
const language = localStorage.getItem('settings/language')
const languages = Object.keys(messages!)

export const availableLocales = Object.keys(localesMap)
if (language && languages.includes(language))
return language

const loadedLanguages: string[] = []
// let locale = navigator.language

function setI18nLanguage(lang: Locale) {
i18n.global.locale.value = lang as any
if (typeof document !== 'undefined')
document.querySelector('html')?.setAttribute('lang', lang)
return lang
}

export async function loadLanguageAsync(lang: string): Promise<Locale> {
// If the same language
if (i18n.global.locale.value === lang)
return setI18nLanguage(lang)
// if (locale === 'zh')
// locale = 'zh-CN'

// If the language was already loaded
if (loadedLanguages.includes(lang))
return setI18nLanguage(lang)

// If the language hasn't been loaded yet
const messages = await localesMap[lang]()
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
return 'en'
}

export default {
install: (app) => {
app.use(i18n)
loadLanguageAsync('en')
},
} satisfies Plugin
Loading

0 comments on commit 38cda9e

Please sign in to comment.