From 82a167d9cc6238218f63b0c746bff3bae1848c4a Mon Sep 17 00:00:00 2001 From: Megghy Date: Tue, 26 Dec 2023 12:15:18 +0800 Subject: [PATCH] support use tts from api --- src/views/open_live/ReadDanmaku.vue | 242 +++++++++++++++++++++------- tsconfig.json | 1 + 2 files changed, 188 insertions(+), 55 deletions(-) diff --git a/src/views/open_live/ReadDanmaku.vue b/src/views/open_live/ReadDanmaku.vue index a1e7eaf..10fa733 100644 --- a/src/views/open_live/ReadDanmaku.vue +++ b/src/views/open_live/ReadDanmaku.vue @@ -4,10 +4,11 @@ import { useAccount } from '@/api/account' import { EventDataTypes, EventModel } from '@/api/api-models' import { QueryGetAPI, QueryPostAPI } from '@/api/query' import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient' -import { VTSURU_API_URL } from '@/data/constants' +import { FETCH_API, VTSURU_API_URL } from '@/data/constants' import { Info24Filled, Mic24Filled } from '@vicons/fluent' import { useStorage } from '@vueuse/core' import EasySpeech from 'easy-speech' +import GraphemeSplitter from 'grapheme-splitter' import { List } from 'linqts' import { NAlert, @@ -22,18 +23,23 @@ import { NInputGroup, NInputGroupLabel, NInputNumber, + NLi, NList, NListItem, NPopconfirm, + NRadioButton, + NRadioGroup, NSelect, NSlider, NSpace, + NSpin, NTag, NText, NTooltip, + NUl, useMessage, } from 'naive-ui' -import { computed, onMounted, onUnmounted, ref } from 'vue' +import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' import { useRoute } from 'vue-router' import { clearInterval, setInterval } from 'worker-timers' @@ -50,6 +56,9 @@ type SpeechSettings = { scTemplate: string guardTemplate: string giftTemplate: string + voiceType: 'local' | 'api' + voiceAPISchemeType: 'http' | 'https' + voiceAPI?: string combineGiftDelay?: number } @@ -74,6 +83,9 @@ const settings = useStorage('Setting.Speech', { scTemplate: '{name} 发送了醒目留言: {message}', guardTemplate: '感谢 {name} 的 {count} 个月 {guard_level}', giftTemplate: '感谢 {name} 赠送的 {count} 个 {gift_name}', + voiceType: 'local', + voiceAPISchemeType: 'https', + voiceAPI: 'voice.vtsuru.live/voice/bert-vits2?text={{text}}&id=1', combineGiftDelay: 2, }) @@ -106,6 +118,9 @@ const voiceOptions = computed(() => { }) const isSpeaking = ref(false) const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([]) +const isVtsuruVoiceAPI = computed(() => { + return settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live') +}) const canSpeech = ref(false) const readedDanmaku = ref(0) @@ -177,29 +192,21 @@ async function speak() { isSpeaking.value = true readedDanmaku.value++ console.log(`[TTS] 正在朗读: ${text}`) - /*await EasySpeech.speak({ - text: text, - volume: settings.value.speechInfo.volume, - pitch: settings.value.speechInfo.pitch, - rate: settings.value.speechInfo.rate, - voice: EasySpeech.voices().find((v) => v.name == settings.value.speechInfo.voice) ?? undefined, - }) - .then(() => {}) - .catch((error) => { - if (error.error == 'interrupted') { - //被中断 - return - } - console.log(error) - message.error('无法播放语音: ' + error.error) - }) - .finally(() => { - isSpeaking.value = false - })*/ - speakDirect(text) + if (checkTimer) { + clearInterval(checkTimer) + } + checkTimer = setInterval(() => { + message.error('语音播放超时') + cancelSpeech() + }, 30000) + if (settings.value.voiceType == 'local') { + speakDirect(text) + } else { + speakFromAPI(text) + } } } -let checkTimer: number +let checkTimer: number | undefined function speakDirect(text: string) { try { const synth = window.speechSynthesis @@ -213,18 +220,11 @@ function speakDirect(text: string) { let voices = synth.getVoices() const voice = voices.find((v) => v.name === settings.value.speechInfo.voice) if (voice) { - if (checkTimer) { - clearInterval(checkTimer) - } u.voice = voice u.volume = settings.value.speechInfo.volume u.rate = settings.value.speechInfo.rate u.pitch = settings.value.speechInfo.pitch synth.speak(u) - checkTimer = setInterval(() => { - message.error('语音播放超时') - cancelSpeech() - }, 30000) u.onend = () => { cancelSpeech() } @@ -241,6 +241,51 @@ function speakDirect(text: string) { console.log(err) } } +const apiAudio = ref() +const isApiAudioLoading = ref(false) +const apiAudioSrc = ref('') +const splitter = new GraphemeSplitter() +function speakFromAPI(text: string) { + if (!settings.value.voiceAPI) { + message.error('未设置语音API') + return + } + isSpeaking.value = true + isApiAudioLoading.value = true + const url = `${settings.value.voiceAPISchemeType == 'https' ? 'https' : FETCH_API + 'http'}://${settings.value.voiceAPI + .trim() + .replace(/^(?:https?:\/\/)/, '') + .replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}` + const tempURL = new URL(url) + if (isVtsuruVoiceAPI.value && splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 50) { + message.error('本站提供的测试接口字数不允许超过 50 字. 内容: [' + tempURL.searchParams.get('text') + ']') + cancelSpeech() + return + } + apiAudioSrc.value = url + nextTick(() => { + //apiAudio.value?.load() + apiAudio.value?.play().catch((err) => { + console.log(err) + message.error('无法播放语音:' + err) + cancelSpeech() + }) + }) +} +function onAPIError(e: Event) { + if (!apiAudioSrc.value) return + cancelSpeech() +} +function cancelSpeech() { + isSpeaking.value = false + if (checkTimer) { + clearInterval(checkTimer) + checkTimer = undefined + } + isApiAudioLoading.value = false + apiAudio.value?.pause() + EasySpeech.cancel() +} function onGetEvent(data: EventModel) { if (!canSpeech.value) { return @@ -326,13 +371,6 @@ function stopSpeech() { canSpeech.value = false message.success('已停止监听') } -function cancelSpeech() { - EasySpeech.cancel() - isSpeaking.value = false - if (checkTimer) { - clearInterval(checkTimer) - } -} async function uploadConfig() { await QueryPostAPI(VTSURU_API_URL + 'set-config', { name: 'Speech', @@ -439,6 +477,9 @@ function test(type: EventDataTypes) { break } } +function testAPI() { + speakFromAPI('这是一条测试弹幕') +} let speechQueueTimer: number onMounted(() => { @@ -473,7 +514,8 @@ onUnmounted(() => { 例如 Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland), 各种营销号就用的这些配音 - 系列语音, 效果要好很多 + 系列语音, 效果要好 + 很多很多 当在后台运行时请关闭浏览器的 页面休眠/内存节省功能. Chrome: @@ -515,7 +557,17 @@ onUnmounted(() => {