mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
add tts
This commit is contained in:
@@ -35,6 +35,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
</NSpace>
|
||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||
<NTimeline>
|
||||
<NTimelineItem type="success" title="功能添加" content="读弹幕" time="2023-12-17" />
|
||||
<NTimelineItem type="success" title="功能添加" content="直播记录" time="2023-12-3" />
|
||||
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
||||
<NTimelineItem type="success" title="功能添加" content="排队" time="2023-11-25" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NCard, NDivider, NGradientText, NSpace, NText, NIcon, NGrid, NGridItem,
|
||||
import vtb from '@/svgs/ic_vtuber.svg'
|
||||
import { AnalyticsSharp, Calendar, Chatbox, ListCircle, MusicalNote } from '@vicons/ionicons5'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { Lottery24Filled, MoneyOff24Filled, MoreHorizontal24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||
import { Lottery24Filled, MoneyOff24Filled, MoreHorizontal24Filled, TabletSpeaker24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
@@ -53,6 +53,11 @@ const functions = [
|
||||
desc: '通过发送弹幕和礼物加入队列, 允许设置多种条件',
|
||||
icon: ListCircle,
|
||||
},
|
||||
{
|
||||
name: '读弹幕',
|
||||
desc: '通过浏览器自带的tts服务念出弹幕 (此功能需要 Chrome, Edge 等现代浏览器!)',
|
||||
icon: TabletSpeaker24Filled,
|
||||
},
|
||||
{
|
||||
name: '视频征集',
|
||||
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选',
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, AnalyticsSharp } from '@vicons/ionicons5'
|
||||
import { CalendarClock24Filled, Chat24Filled, Info24Filled, Live24Filled, Lottery24Filled, PeopleQueue24Filled, PersonFeedback24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||
import { CalendarClock24Filled, Chat24Filled, Info24Filled, Live24Filled, Lottery24Filled, PeopleQueue24Filled, PersonFeedback24Filled, TabletSpeaker24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||
import { isLoadingAccount, useAccount } from '@/api/account'
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
@@ -280,6 +280,21 @@ const menuOptions = [
|
||||
icon: renderIcon(PeopleQueue24Filled),
|
||||
//disabled: accountInfo.value?.isEmailVerified == false,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'manage-speech',
|
||||
},
|
||||
},
|
||||
{ default: () => '读弹幕' }
|
||||
),
|
||||
key: 'manage-speech',
|
||||
icon: renderIcon(TabletSpeaker24Filled),
|
||||
//disabled: accountInfo.value?.isEmailVerified == false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { isDarkMode } from '@/Utils'
|
||||
import { OpenLiveInfo, ThemeType } from '@/api/api-models'
|
||||
import DanmakuClient, { AuthInfo } from '@/data/DanmakuClient'
|
||||
import { Lottery24Filled, PeopleQueue24Filled } from '@vicons/fluent'
|
||||
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent'
|
||||
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5'
|
||||
import { useElementSize, useStorage } from '@vueuse/core'
|
||||
import {
|
||||
@@ -74,7 +74,8 @@ const menuOptions = [
|
||||
),
|
||||
key: 'open-live-song-request',
|
||||
icon: renderIcon(MusicalNote),
|
||||
},{
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
@@ -89,6 +90,21 @@ const menuOptions = [
|
||||
key: 'open-live-queue',
|
||||
icon: renderIcon(PeopleQueue24Filled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'open-live-speech',
|
||||
query: route.query,
|
||||
},
|
||||
},
|
||||
{ default: () => '读弹幕' }
|
||||
),
|
||||
key: 'open-live-speech',
|
||||
icon: renderIcon(TabletSpeaker24Filled),
|
||||
},
|
||||
]
|
||||
|
||||
function renderIcon(icon: unknown) {
|
||||
|
||||
@@ -33,6 +33,13 @@ const accountInfo = useAccount()
|
||||
<NButton @click="$router.push({ name: 'open-live-queue', query: $route.query })" type="primary"> 前往使用 </NButton>
|
||||
</template>
|
||||
</NCard>
|
||||
|
||||
<NCard hoverable embedded size="small" title="读弹幕" style="width: 300px">
|
||||
通过浏览器自带的tts服务读弹幕 (此功能需要 Chrome, Edge 等现代浏览器!)
|
||||
<template #footer>
|
||||
<NButton @click="$router.push({ name: 'open-live-speech', query: $route.query })" type="primary"> 前往使用 </NButton>
|
||||
</template>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
<br />
|
||||
<NAlert v-if="accountInfo?.eventFetcherOnline != true" type="warning" title="可用性警告" style="max-width: 600px; margin: 0 auto">
|
||||
|
||||
@@ -8,29 +8,20 @@ import {
|
||||
QueueGiftFilterType,
|
||||
QueueSortType,
|
||||
Setting_Queue,
|
||||
Setting_SongRequest,
|
||||
SongFrom,
|
||||
QueueFrom,
|
||||
SongRequestInfo,
|
||||
QueueStatus,
|
||||
DanmakuUserInfo,
|
||||
SongsInfo,
|
||||
ResponseQueueModel,
|
||||
} from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
|
||||
import DanmakuClient, { AuthInfo, DanmakuInfo, GiftInfo, RoomAuthInfo, SCInfo } from '@/data/DanmakuClient'
|
||||
import { OPEN_LIVE_API_URL, SONG_API_URL, QUEUE_API_URL } from '@/data/constants'
|
||||
import { QUEUE_API_URL } from '@/data/constants'
|
||||
import {
|
||||
Check24Filled,
|
||||
Checkmark12Regular,
|
||||
ClipboardTextLtr24Filled,
|
||||
Delete24Filled,
|
||||
Dismiss12Filled,
|
||||
Dismiss16Filled,
|
||||
Info24Filled,
|
||||
Mic24Filled,
|
||||
PeopleQueue24Filled,
|
||||
Play24Filled,
|
||||
PresenceBlocked16Regular,
|
||||
} from '@vicons/fluent'
|
||||
import { ReloadCircleSharp } from '@vicons/ionicons5'
|
||||
@@ -47,7 +38,6 @@ import {
|
||||
NCollapseItem,
|
||||
NDataTable,
|
||||
NDivider,
|
||||
NEllipsis,
|
||||
NEmpty,
|
||||
NIcon,
|
||||
NInput,
|
||||
@@ -59,7 +49,6 @@ import {
|
||||
NListItem,
|
||||
NModal,
|
||||
NPopconfirm,
|
||||
NRadio,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
@@ -78,7 +67,6 @@ import {
|
||||
} from 'naive-ui'
|
||||
import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SongRequestOBS from '../obs/SongRequestOBS.vue'
|
||||
import QueueOBS from '../obs/QueueOBS.vue'
|
||||
|
||||
const defaultSettings = {
|
||||
|
||||
438
src/views/open_live/ReadDanmaku.vue
Normal file
438
src/views/open_live/ReadDanmaku.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import EasySpeech from 'easy-speech'
|
||||
import { NButton, NDivider, NIcon, NInput, NInputGroup, NInputGroupLabel, NPopconfirm, NSelect, NSlider, NSpace, NTag, NText, NTooltip, useMessage } from 'naive-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { Queue } from 'queue-typescript'
|
||||
import DanmakuClient, { DanmakuInfo, RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { EventDataTypes, EventModel } from '@/api/api-models'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { Mic24Filled } from '@vicons/fluent'
|
||||
import { copyToClipboard } from '@/Utils'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
client: DanmakuClient
|
||||
roomInfo: RoomAuthInfo
|
||||
code: string | undefined
|
||||
isOpenLive?: boolean
|
||||
}>()
|
||||
|
||||
type SpeechSettings = {
|
||||
speechInfo: SpeechInfo
|
||||
danmakuTemplate: string
|
||||
scTemplate: string
|
||||
guardTemplate: string
|
||||
giftTemplate: string
|
||||
}
|
||||
type SpeechInfo = {
|
||||
volume: number
|
||||
pitch: number
|
||||
rate: number
|
||||
voice: string
|
||||
}
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const settings = useStorage<SpeechSettings>('Setting.Speech.Settings', {
|
||||
speechInfo: {
|
||||
volume: 1,
|
||||
pitch: 1,
|
||||
rate: 1,
|
||||
voice: '',
|
||||
},
|
||||
danmakuTemplate: '{name} 说: {message}',
|
||||
scTemplate: '{name} 发送了醒目留言: {message}',
|
||||
guardTemplate: '感谢 {name} 的 {count} 个月 {guard_level}',
|
||||
giftTemplate: '感谢 {name} 赠送的 {count} 个 {gift_name}',
|
||||
})
|
||||
const speechSynthesisInfo = ref<{
|
||||
speechSynthesis: SpeechSynthesis | undefined
|
||||
speechSynthesisUtterance: SpeechSynthesisUtterance | undefined
|
||||
speechSynthesisVoice: SpeechSynthesisVoice | undefined
|
||||
speechSynthesisEvent: SpeechSynthesisEvent | undefined
|
||||
speechSynthesisErrorEvent: SpeechSynthesisErrorEvent | undefined
|
||||
onvoiceschanged: boolean
|
||||
onboundary: boolean
|
||||
onend: boolean
|
||||
onerror: boolean
|
||||
onmark: boolean
|
||||
onpause: boolean
|
||||
onresume: boolean
|
||||
onstart: boolean
|
||||
}>()
|
||||
const languageDisplayName = new Intl.DisplayNames(['zh'], { type: 'language' })
|
||||
const voiceOptions = computed(() => {
|
||||
return EasySpeech.voices().map((v) => {
|
||||
return {
|
||||
label: `[${languageDisplayName.of(v.lang)}] ${v.name}`,
|
||||
value: v.name,
|
||||
}
|
||||
})
|
||||
})
|
||||
const isSpeaking = ref(false)
|
||||
const speakQueue = new Queue<string>()
|
||||
|
||||
const canSpeech = ref(false)
|
||||
const readedDanmaku = ref(0)
|
||||
|
||||
const templateConstants = {
|
||||
name: {
|
||||
name: '用户名',
|
||||
words: '{name}',
|
||||
regex: /\{\s*name\s*\}/gi,
|
||||
},
|
||||
message: {
|
||||
name: '弹幕内容',
|
||||
words: '{message}',
|
||||
regex: /\{\s*message\s*\}/gi,
|
||||
},
|
||||
guard_level: {
|
||||
name: '舰长等级',
|
||||
words: '{guard_level}',
|
||||
regex: /\{\s*guard_level\s*\}/gi,
|
||||
},
|
||||
guard_num: {
|
||||
name: '上舰数量',
|
||||
words: '{guard_num}',
|
||||
regex: /\{\s*guard_num\s*\}/gi,
|
||||
},
|
||||
fans_medal_level: {
|
||||
name: '粉丝勋章等级',
|
||||
words: '{fans_medal_level}',
|
||||
regex: /\{\s*fans_medal_level\s*\}/gi,
|
||||
},
|
||||
price: {
|
||||
name: '价格',
|
||||
words: '{price}',
|
||||
regex: /\{\s*price\s*\}/gi,
|
||||
},
|
||||
count: {
|
||||
name: '数量',
|
||||
words: '{count}',
|
||||
regex: /\{\s*count\s*\}/gi,
|
||||
},
|
||||
gift_name: {
|
||||
name: '礼物名称',
|
||||
words: '{gift_name}',
|
||||
regex: /\{\s*gift_name\s*\}/gi,
|
||||
},
|
||||
}
|
||||
const speechCount = ref(0)
|
||||
async function speak() {
|
||||
if (isSpeaking.value) {
|
||||
return
|
||||
}
|
||||
const text = speakQueue.dequeue()
|
||||
if (text) {
|
||||
isSpeaking.value = true
|
||||
speechCount.value--
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
function onGetEvent(data: EventModel) {
|
||||
if (!canSpeech.value) {
|
||||
return
|
||||
}
|
||||
if (data.type == EventDataTypes.Message && (data.emoji || /^(?:\[\w+\])+$/.test(data.msg))) {
|
||||
// 不支持表情
|
||||
return
|
||||
}
|
||||
onGetEventInternal(data)
|
||||
}
|
||||
function onGetEventInternal(data: EventModel) {
|
||||
let text: string
|
||||
switch (data.type) {
|
||||
case EventDataTypes.Message:
|
||||
if (!settings.value.danmakuTemplate) {
|
||||
return
|
||||
}
|
||||
text = settings.value.danmakuTemplate
|
||||
break
|
||||
case EventDataTypes.SC:
|
||||
if (!settings.value.scTemplate) {
|
||||
return
|
||||
}
|
||||
text = settings.value.scTemplate
|
||||
break
|
||||
case EventDataTypes.Guard:
|
||||
if (!settings.value.guardTemplate) {
|
||||
return
|
||||
}
|
||||
text = settings.value.guardTemplate
|
||||
break
|
||||
case EventDataTypes.Gift:
|
||||
if (!settings.value.giftTemplate) {
|
||||
return
|
||||
}
|
||||
text = settings.value.giftTemplate
|
||||
break
|
||||
}
|
||||
text = text
|
||||
.replace(templateConstants.name.regex, data.name)
|
||||
.replace(templateConstants.count.regex, data.num.toString())
|
||||
.replace(templateConstants.price.regex, data.price.toString())
|
||||
.replace(templateConstants.message.regex, data.msg)
|
||||
.replace(templateConstants.guard_level.regex, data.guard_level == 1 ? '总督' : data.guard_level == 2 ? '提督' : data.guard_level == 3 ? '舰长' : '')
|
||||
.replace(templateConstants.fans_medal_level.regex, data.fans_medal_level.toString())
|
||||
|
||||
if (data.type === EventDataTypes.Message) {
|
||||
text = text.replace(/\[.*?\]/g, ' ') //删除表情
|
||||
} else if (data.type === EventDataTypes.Gift) {
|
||||
text = text.replace(templateConstants.gift_name.regex, data.msg)
|
||||
} else if (data.type === EventDataTypes.Guard) {
|
||||
text = text.replace(templateConstants.guard_num.regex, data.num.toString())
|
||||
}
|
||||
speakQueue.enqueue(text)
|
||||
speechCount.value++
|
||||
}
|
||||
function startSpeech() {
|
||||
canSpeech.value = true
|
||||
message.success('服务已启动')
|
||||
}
|
||||
function stopSpeech() {
|
||||
canSpeech.value = false
|
||||
message.success('已停止监听')
|
||||
}
|
||||
function cancelSpeech() {
|
||||
EasySpeech.cancel()
|
||||
}
|
||||
async function uploadConfig() {
|
||||
await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
||||
name: 'Speech',
|
||||
json: JSON.stringify(settings.value),
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已保存至服务器')
|
||||
} else {
|
||||
message.error('保存失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('保存失败')
|
||||
})
|
||||
}
|
||||
async function downloadConfig() {
|
||||
await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||
name: 'Speech',
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
settings.value = JSON.parse(data.data)
|
||||
message.success('已获取配置文件')
|
||||
} else if (data.code == 404) {
|
||||
message.error('未上传配置文件')
|
||||
} else {
|
||||
message.error('获取失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('获取失败')
|
||||
})
|
||||
}
|
||||
|
||||
function test(type: EventDataTypes) {
|
||||
switch (type) {
|
||||
case EventDataTypes.Message:
|
||||
onGetEventInternal({
|
||||
type: EventDataTypes.Message,
|
||||
name: accountInfo.value?.name ?? '未知用户',
|
||||
uid: accountInfo.value?.biliId ?? 0,
|
||||
msg: '测试弹幕',
|
||||
price: 0,
|
||||
num: 0,
|
||||
time: Date.now(),
|
||||
guard_level: 0,
|
||||
fans_medal_level: 1,
|
||||
fans_medal_name: '',
|
||||
fans_medal_wearing_status: false,
|
||||
emoji: undefined,
|
||||
avatar: '',
|
||||
})
|
||||
break
|
||||
case EventDataTypes.SC:
|
||||
onGetEventInternal({
|
||||
type: EventDataTypes.SC,
|
||||
name: accountInfo.value?.name ?? '未知用户',
|
||||
uid: accountInfo.value?.biliId ?? 0,
|
||||
msg: '测试sc',
|
||||
price: 30,
|
||||
num: 1,
|
||||
time: Date.now(),
|
||||
guard_level: 0,
|
||||
fans_medal_level: 1,
|
||||
fans_medal_name: '',
|
||||
fans_medal_wearing_status: false,
|
||||
emoji: undefined,
|
||||
avatar: '',
|
||||
})
|
||||
break
|
||||
case EventDataTypes.Guard:
|
||||
onGetEventInternal({
|
||||
type: EventDataTypes.Guard,
|
||||
name: accountInfo.value?.name ?? '未知用户',
|
||||
uid: accountInfo.value?.biliId ?? 0,
|
||||
msg: '舰长',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: Date.now(),
|
||||
guard_level: 3,
|
||||
fans_medal_level: 1,
|
||||
fans_medal_name: '',
|
||||
fans_medal_wearing_status: false,
|
||||
emoji: undefined,
|
||||
avatar: '',
|
||||
})
|
||||
break
|
||||
case EventDataTypes.Gift:
|
||||
onGetEventInternal({
|
||||
type: EventDataTypes.Gift,
|
||||
name: accountInfo.value?.name ?? '未知用户',
|
||||
uid: accountInfo.value?.biliId ?? 0,
|
||||
msg: '测试礼物',
|
||||
price: 5,
|
||||
num: 5,
|
||||
time: Date.now(),
|
||||
guard_level: 0,
|
||||
fans_medal_level: 1,
|
||||
fans_medal_name: '',
|
||||
fans_medal_wearing_status: false,
|
||||
emoji: undefined,
|
||||
avatar: '',
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let speechQueueTimer: any
|
||||
onMounted(() => {
|
||||
speechSynthesisInfo.value = EasySpeech.detect()
|
||||
speechQueueTimer = setInterval(() => {
|
||||
speak()
|
||||
}, 100)
|
||||
|
||||
props.client.onEvent('danmaku', onGetEvent)
|
||||
props.client.onEvent('sc', onGetEvent)
|
||||
props.client.onEvent('guard', onGetEvent)
|
||||
props.client.onEvent('gift', onGetEvent)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(speechQueueTimer)
|
||||
props.client.offEvent('danmaku', onGetEvent)
|
||||
props.client.offEvent('sc', onGetEvent)
|
||||
props.client.offEvent('guard', onGetEvent)
|
||||
props.client.offEvent('gift', onGetEvent)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace>
|
||||
<NButton @click="canSpeech ? stopSpeech() : startSpeech()" :type="canSpeech ? 'error' : 'primary'"> {{ canSpeech ? '停止监听' : '开始监听' }} </NButton>
|
||||
<NButton @click="uploadConfig" type="primary" secondary> 保存配置到服务器 </NButton>
|
||||
<NPopconfirm @positive-click="downloadConfig">
|
||||
<template #trigger>
|
||||
<NButton type="primary" secondary> 从服务器获取配置 </NButton>
|
||||
</template>
|
||||
这将覆盖当前设置, 确定?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
<template v-if="canSpeech">
|
||||
<NDivider> 状态 </NDivider>
|
||||
<NSpace vertical align="center">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton circle :disabled="!isSpeaking" @click="cancelSpeech" :style="`animation: ${isSpeaking ? 'animated-border 2.5s infinite;' : ''}`">
|
||||
<template #icon>
|
||||
<NIcon :component="Mic24Filled" :color="isSpeaking ? 'green' : 'gray'" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
{{ isSpeaking ? '取消朗读' : '未朗读' }}
|
||||
</NTooltip>
|
||||
<NText depth="3"> 队列: {{ speechCount }} <NDivider vertical /> 已读: {{readedDanmaku }} 条 </NText>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NDivider />
|
||||
<NSpace vertical>
|
||||
<NSelect v-model:value="settings.speechInfo.voice" :options="voiceOptions" :fallback-option="() => ({ label: '未选择, 将使用默认语音', value: '' })" />
|
||||
<span style="width: 100%">
|
||||
<NText> 音量 </NText>
|
||||
<NSlider style="min-width: 200px" v-model:value="settings.speechInfo.volume" :min="0" :max="1" :step="0.01" />
|
||||
</span>
|
||||
<span style="width: 100%">
|
||||
<NText> 音调 </NText>
|
||||
<NSlider style="min-width: 200px" v-model:value="settings.speechInfo.pitch" :min="0" :max="2" :step="0.01" />
|
||||
</span>
|
||||
<span style="width: 100%">
|
||||
<NText> 语速 </NText>
|
||||
<NSlider style="min-width: 200px" v-model:value="settings.speechInfo.rate" :min="0" :max="2" :step="0.01" />
|
||||
</span>
|
||||
</NSpace>
|
||||
<NDivider> 自定义内容 </NDivider>
|
||||
<NSpace vertical>
|
||||
<NSpace>
|
||||
支持的变量:
|
||||
<NButton size="tiny" secondary v-for="item in Object.values(templateConstants)" :key="item.name" @click="copyToClipboard(item.words)"> {{ item.words }} | {{ item.name }} </NButton>
|
||||
</NSpace>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 弹幕模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.danmakuTemplate" placeholder="弹幕消息" />
|
||||
<NButton @click="test(EventDataTypes.Message)" type="info"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 礼物模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.giftTemplate" placeholder="礼物消息" />
|
||||
<NButton @click="test(EventDataTypes.Gift)" type="info"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> SC模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.scTemplate" placeholder="SC消息" />
|
||||
<NButton @click="test(EventDataTypes.SC)" type="info"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 上舰模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.guardTemplate" placeholder="上舰消息" />
|
||||
<NButton @click="test(EventDataTypes.Guard)" type="info"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
<NDivider> 设置 </NDivider>
|
||||
<NText depth="3">
|
||||
没想好需要什么, 有建议的话可以和我说
|
||||
</NText>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user