Skip to content

Commit

Permalink
feat: 语音增加预览功能 (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
liou666 committed Apr 20, 2023
1 parent 09c66c1 commit a663a6b
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 31 deletions.
46 changes: 26 additions & 20 deletions src/hooks/useSpeechService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {
SpeechRecognizer,
SpeechSynthesizer,
} from 'microsoft-cognitiveservices-speech-sdk'

export const useSpeechService = (langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-CN', 'zh-HK', 'ko-KR', 'de-DE']) => {
interface Config {
langs?: readonly['fr-FR', 'ja-JP', 'en-US', 'zh-CN', 'zh-HK', 'ko-KR', 'de-DE']
isFetchAllVoice?: boolean
}
export const useSpeechService = ({ langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-CN', 'zh-HK', 'ko-KR', 'de-DE'], isFetchAllVoice = true }: Config = {}) => {
const { azureKey, azureRegion } = useGlobalSetting()
const languages = ref(langs)
const language = ref<typeof langs[number]>(langs[0])
Expand Down Expand Up @@ -118,16 +121,17 @@ export const useSpeechService = (langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-
})
}

const ssmlToSpeak = async (text: string) => {
const ssmlToSpeak = async (text: string, { voice, voiceRate, lang }: { voice?: string; voiceRate?: number; lang?: string } = {}) => {
isSynthesizing.value = true

const lang = speechConfig.value.speechSynthesisLanguage
const voice = speechConfig.value.speechSynthesisVoiceName
const targetLang = lang || speechConfig.value.speechSynthesisLanguage
const targetVoice = voice || speechConfig.value.speechSynthesisVoiceName
const targetRate = voiceRate || rate.value

const ssml = `
<speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="${lang}">
<voice name="${voice}">
<prosody rate="${rate.value}">
<speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="${targetLang}">
<voice name="${targetVoice}">
<prosody rate="${targetRate}">
${text}
</prosody>
</voice>
Expand Down Expand Up @@ -158,18 +162,20 @@ export const useSpeechService = (langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-
}

onMounted(async () => {
try {
isFetchAllVoices.value = true
allVoices.value = await getVoices()
// fr-FR 法语 ja-JP 日语 en-US 英语 zh-CN 中文 zh-HK 粤语 ko-KR 韩语 de-DE 德语
for (const lang of languages.value)
languageMap.value[lang] = allVoices.value.filter(x => lang === x.locale)
console.log(languageMap)
isFetchAllVoices.value = false
}
catch (error) {
isFetchAllVoices.value = false
allVoices.value = []
if (isFetchAllVoice) {
try {
isFetchAllVoices.value = true
allVoices.value = await getVoices()
// fr-FR 法语 ja-JP 日语 en-US 英语 zh-CN 中文 zh-HK 粤语 ko-KR 韩语 de-DE 德语
for (const lang of languages.value)
languageMap.value[lang] = allVoices.value.filter(x => lang === x.locale)
console.log(languageMap)
isFetchAllVoices.value = false
}
catch (error) {
isFetchAllVoices.value = false
allVoices.value = []
}
}
})

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Home/components/Content.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
stopRecognizeSpeech,
ssmlToSpeak,
isSynthesizing,
} = useSpeechService(store.allLanguage as any)
} = useSpeechService({ langs: store.allLanguage as any })
// states
const message = ref('') // input message
Expand Down
50 changes: 40 additions & 10 deletions src/pages/Home/components/NewChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ const avatarList = ref<string[]>(Object.keys(modules).map(path => path.replace('
const currentAvatarIndex = ref(Math.random() * avatarList.value.length | 0)
const store = useConversationStore()
const { ssmlToSpeak, isSynthesizing } = useSpeechService({ isFetchAllVoice: false })
const allLanguages = computed(() => [...new Set(allVoices.map(v => v.locale))].filter(l => Object.keys(supportLanguageMap).includes(l)))
const selectLanguage = ref('')
const filterVoices = ref<VoiceInfo[]>([])
const selectVoiceName = ref('')
const desc = ref('')
const name = ref('')
const rate = ref('1.0')
const previewText = ref('hello wrold')
const canAdd = computed(() => !!(selectLanguage.value && selectVoiceName.value && desc.value && name.value))
Expand Down Expand Up @@ -58,6 +59,9 @@ const addChat = (event: any) => {
const changeAvatar = () => {
currentAvatarIndex.value = avatarList.value.length - 1 === currentAvatarIndex.value ? 0 : currentAvatarIndex.value + 1
}
const previewSpeech = () => {
ssmlToSpeak(previewText.value, { voice: selectVoiceName.value, lang: selectLanguage.value, voiceRate: +rate })
}
</script>

<script>
Expand All @@ -74,31 +78,45 @@ const changeAvatar = () => {
<Avatar v-model:image-url="imageUrl" />
</div>
<div flex>
<label center-y justify-end mr-2 for="">姓名</label>
<input v-model="name" w-50 p-2 type="text">
<label for="">姓名</label>
<input v-model="name" type="text">
</div>
<div flex>
<label center-y justify-end mr-2 for="">备注</label>
<input v-model="desc" w-50 p-2 type="text">
<label for="">备注</label>
<input v-model="desc" type="text">
</div>
<div flex>
<label center-y justify-end mr-2 for="">语言</label>
<select v-model="selectLanguage" w-55 select-settings>
<label for="">语言</label>
<select v-model="selectLanguage">
<option v-for="item in allLanguages" :key="item" :value="item">
{{ supportLanguageMap[item] }}
</option>
</select>
</div>
<div flex>
<label center-y justify-end mr-2 for="">音色</label>
<select v-model="selectVoiceName" w-55 select-settings>
<label for="">音色</label>
<select v-model="selectVoiceName">
<option v-for="item in filterVoices" :key="item.shortName" :value="item.shortName">
{{ `${item.locale} / ${item.gender === 1 ? 'Female' : 'Male'} / ${item.localName}` }}
</option>
</select>
</div>
<div relative center-y>
<div flex>
<label for="">语音预览</label>
<input v-model="previewText" placeholder="输入文字预览语音" type="text">
</div>
<div absolute left-77>
<button v-if="!isSynthesizing" :disabled="!previewText" center-y ml-2 @click="previewSpeech()">
<i icon-btn i-ic:baseline-volume-up />
</button>
<button v-else center-y ml-2>
<i icon-btn i-eos-icons:bubble-loading />
</button>
</div>
</div>
<div flex>
<label center-y justify-end mr-2 for="">语速</label>
<label for="">语速</label>
<div w-55 flex>
<input v-model="rate" flex-1 type="range" step="0.1" min="0.1" max="2.0">
<span w-4 ml-1>{{ Number(rate).toFixed(1) }}</span>
Expand All @@ -119,3 +137,15 @@ const changeAvatar = () => {
</button>
</div>
</template>

<style scoped>
label{
@apply center-y justify-end mr-2 w-20
}
input{
@apply w-50 p-2
}
select{
@apply w-55 select-settings
}
</style>

0 comments on commit a663a6b

Please sign in to comment.