feat(speak): 实现语音播报功能
- 新增 speak-tts 依赖库 - 创建 speak.js 工具类,实现语音合成和播报功能 - 在 ChatInterface 组件中添加 TextToVoiceButton 组件,用于触发语音播报 - 在 VoiceInputSettings 组件中添加语音播报相关设置选项 - 更新 chat.js 中的逻辑,支持自动语音播报 - 新增 speakStore 管理语音播报相关状态
This commit is contained in:
parent
dbb45fb1bc
commit
a1b82d3cf8
|
@ -50,6 +50,7 @@ alwaysApply: false
|
||||||
- Props 必须定义类型和默认值
|
- Props 必须定义类型和默认值
|
||||||
- 使用 defineEmits 定义事件
|
- 使用 defineEmits 定义事件
|
||||||
- 复杂组件需要添加注释说明
|
- 复杂组件需要添加注释说明
|
||||||
|
- 高度组件化开发,细致到功能模块
|
||||||
|
|
||||||
4. 状态管理
|
4. 状态管理
|
||||||
- 使用 Pinia 进行状态管理
|
- 使用 Pinia 进行状态管理
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
### 技术栈
|
||||||
|
- Javascript
|
||||||
|
- Vue3
|
||||||
|
- vite
|
||||||
|
- axios
|
||||||
|
- element
|
||||||
|
- pinia
|
||||||
|
- mockjs
|
||||||
|
- sass
|
||||||
|
|
||||||
|
### 包管理器
|
||||||
|
- yarn
|
||||||
|
|
||||||
|
### 文件目录
|
||||||
|
|
||||||
|
- src/
|
||||||
|
- assets/ # 静态资源文件
|
||||||
|
- components/ # 公共组件
|
||||||
|
- views/ # 页面视图组件
|
||||||
|
- router/ # 路由配置
|
||||||
|
- store/ # Pinia状态管理
|
||||||
|
- api/ # API接口封装
|
||||||
|
- utils/ # 工具函数
|
||||||
|
- styles/ # 全局样式
|
||||||
|
- mock/ # Mock数据
|
||||||
|
- App.vue # 根组件
|
||||||
|
- main.js # 入口文件
|
||||||
|
|
||||||
|
### 开发规范
|
||||||
|
|
||||||
|
1. 命名规范
|
||||||
|
- 组件名:大驼峰命名法,如 `UserProfile.vue`
|
||||||
|
- 变量名:小驼峰命名法,如 `userInfo`
|
||||||
|
- 常量名:全大写,下划线分隔,如 `API_BASE_URL`
|
||||||
|
- 文件夹名:小写,中划线分隔,如 `user-center`
|
||||||
|
|
||||||
|
2. 代码风格
|
||||||
|
- 使用 ESLint + Prettier 进行代码格式化
|
||||||
|
- 组件内使用 `<script setup>` 语法
|
||||||
|
- 使用 TypeScript 进行类型检查
|
||||||
|
- 遵循 Vue3 组合式 API 规范
|
||||||
|
|
||||||
|
3. 组件开发
|
||||||
|
- 组件必须包含 name 属性
|
||||||
|
- Props 必须定义类型和默认值
|
||||||
|
- 使用 defineEmits 定义事件
|
||||||
|
- 复杂组件需要添加注释说明
|
||||||
|
- 高度组件化开发,细致到功能模块
|
||||||
|
|
||||||
|
4. 状态管理
|
||||||
|
- 使用 Pinia 进行状态管理
|
||||||
|
- Store 按功能模块划分
|
||||||
|
- 避免在组件中直接修改 store 状态
|
||||||
|
|
||||||
|
5. 路由规范
|
||||||
|
- 路由配置统一在 router 目录下管理
|
||||||
|
- 使用路由懒加载
|
||||||
|
- 路由命名遵循小驼峰命名法
|
||||||
|
|
||||||
|
6. API 规范
|
||||||
|
- API 请求统一在 api 目录下管理
|
||||||
|
- 使用 axios 进行请求封装
|
||||||
|
- 统一错误处理
|
||||||
|
- 请求参数和响应数据需要定义类型
|
||||||
|
|
||||||
|
7. 样式规范
|
||||||
|
- 使用 SCSS 预处理器
|
||||||
|
- 遵循 BEM 命名规范
|
||||||
|
- 全局样式统一在 styles 目录下管理
|
||||||
|
- 组件样式使用 scoped
|
||||||
|
|
||||||
|
8. 提交规范
|
||||||
|
- 遵循 Conventional Commits 规范
|
||||||
|
- 提交信息必须清晰描述改动内容
|
||||||
|
|
||||||
|
|
||||||
|
- 禁止提交大文件
|
|
@ -42,13 +42,12 @@
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"element-plus": "^2.9.7",
|
"element-plus": "^2.9.7",
|
||||||
"marked": "^15.0.8",
|
"marked": "^15.0.8",
|
||||||
"pcm": "^1.0.3",
|
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
"recorder-core": "^1.3.25011100",
|
"recorder-core": "^1.3.25011100",
|
||||||
|
"speak-tts": "^2.0.8",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0"
|
||||||
"wav": "^1.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
|
|
|
@ -4,34 +4,59 @@
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<span class="settings-label">语音转文字后直接发送</span>
|
<span class="settings-label">语音转文字后直接发送</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="voiceInputSettings.sendDirectly"
|
v-model="sendDirectly"
|
||||||
@change="updateSettings"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<span class="settings-label">自动语音播报</span>
|
<span class="settings-label">自动语音播报</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="voiceInputSettings.autoTTS"
|
v-model="autoTTS"
|
||||||
@change="updateSettings"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<span class="settings-label">语音播报音色</span>
|
||||||
|
<el-select v-model="ttsVoice" placeholder="默认" style="width: 180px">
|
||||||
|
<el-option
|
||||||
|
v-for="voice in zhVoices"
|
||||||
|
:key="voice.name"
|
||||||
|
:label="voice.name + (voice.lang ? ' (' + voice.lang + ')' : '')"
|
||||||
|
:value="voice.name"
|
||||||
|
/>
|
||||||
|
<el-option label="默认" value="" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { useSettingsStore } from '@/store/settings'
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
import { getTTSVoices } from '@/utils/speak'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const speakStore = useSpeakStore()
|
||||||
|
|
||||||
const voiceInputSettings = computed({
|
const sendDirectly = computed({
|
||||||
get: () => settingsStore.voiceInputSettings,
|
get: () => speakStore.sendDirectly,
|
||||||
set: (value) => settingsStore.updateVoiceInputSettings(value)
|
set: (val) => speakStore.setSendDirectly(val)
|
||||||
|
})
|
||||||
|
const autoTTS = computed({
|
||||||
|
get: () => speakStore.autoTTS,
|
||||||
|
set: (val) => speakStore.setAutoTTS(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateSettings = () => {
|
const ttsVoice = computed({
|
||||||
settingsStore.updateVoiceInputSettings(voiceInputSettings.value)
|
get: () => speakStore.ttsVoice,
|
||||||
}
|
set: (val) => speakStore.setTTSVoice(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const voices = ref([])
|
||||||
|
const zhVoices = computed(() =>
|
||||||
|
voices.value.filter(v => v.lang && v.lang.toLowerCase().startsWith('zh'))
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
voices.value = await getTTSVoices()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { sendMessage } from '@/api/chat'
|
import { sendMessage } from '@/api/chat'
|
||||||
|
import { useSettingsStore } from '@/store/settings'
|
||||||
|
import { speakText } from '@/utils/speak'
|
||||||
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
|
||||||
const chatModes = {
|
const chatModes = {
|
||||||
training: {
|
training: {
|
||||||
|
@ -72,6 +75,7 @@ const processTagContent = (answerCache, tagName, cachedMessage) => {
|
||||||
const content = answerCache.split(endTag)[0]
|
const content = answerCache.split(endTag)[0]
|
||||||
cachedMessage[tagName] += content
|
cachedMessage[tagName] += content
|
||||||
cachedMessage.respondingType = ''
|
cachedMessage.respondingType = ''
|
||||||
|
cachedMessage[`${tagName}Finished`] = true
|
||||||
return answerCache.split(endTag)[1] || ''
|
return answerCache.split(endTag)[1] || ''
|
||||||
} else {
|
} else {
|
||||||
// 继续累积内容
|
// 继续累积内容
|
||||||
|
@ -187,6 +191,7 @@ export const useChatStore = defineStore('chat', {
|
||||||
|
|
||||||
this.hasStartedResponse = false
|
this.hasStartedResponse = false
|
||||||
|
|
||||||
|
const spokenSet = new Set(); // 新增:记录已播报的 messageId
|
||||||
try {
|
try {
|
||||||
const messageStream = await sendMessage({
|
const messageStream = await sendMessage({
|
||||||
inputs: {},
|
inputs: {},
|
||||||
|
@ -219,6 +224,10 @@ export const useChatStore = defineStore('chat', {
|
||||||
pingfen: '',
|
pingfen: '',
|
||||||
zongjie: '',
|
zongjie: '',
|
||||||
dafen: '',
|
dafen: '',
|
||||||
|
kehuFinished: false,
|
||||||
|
pingfenFinished: false,
|
||||||
|
zongjieFinished: false,
|
||||||
|
dafenFinished: false,
|
||||||
}
|
}
|
||||||
this.conversationId = message.conversation_id
|
this.conversationId = message.conversation_id
|
||||||
this.messageCache[message.message_id] = currentMessage
|
this.messageCache[message.message_id] = currentMessage
|
||||||
|
@ -240,10 +249,20 @@ export const useChatStore = defineStore('chat', {
|
||||||
answerCache += message.answer
|
answerCache += message.answer
|
||||||
const cachedMessage = this.messageCache[message.message_id]
|
const cachedMessage = this.messageCache[message.message_id]
|
||||||
if (cachedMessage) {
|
if (cachedMessage) {
|
||||||
// 依次处理每个标签
|
|
||||||
for (const tag of tagList) {
|
for (const tag of tagList) {
|
||||||
answerCache = processTagContent(answerCache, tag, cachedMessage)
|
answerCache = processTagContent(answerCache, tag, cachedMessage)
|
||||||
}
|
}
|
||||||
|
// 只在kehuFinished为true且未播报过时播报
|
||||||
|
if (
|
||||||
|
cachedMessage.kehuFinished &&
|
||||||
|
!spokenSet.has(message.message_id)
|
||||||
|
) {
|
||||||
|
const speakStore = useSpeakStore()
|
||||||
|
if (speakStore.autoTTS) {
|
||||||
|
speakText(cachedMessage.kehu)
|
||||||
|
spokenSet.add(message.message_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import Speech from 'speak-tts'
|
||||||
|
|
||||||
|
export const useSpeakStore = defineStore('speak', {
|
||||||
|
state: () => ({
|
||||||
|
ttsStatus: 'idle', // 'idle' | 'playing'
|
||||||
|
ttsVoice: '', // 语音名称,空为默认
|
||||||
|
ttsVolume: 1, // 语音音量
|
||||||
|
ttsRate: 1.2, // 语音语速
|
||||||
|
ttsPitch: 1, // 语音音调
|
||||||
|
sendDirectly: true, // 语音转文字后直接发送
|
||||||
|
autoTTS: false // 自动文字语音播报
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setTTSStatus(status) {
|
||||||
|
this.ttsStatus = status
|
||||||
|
},
|
||||||
|
setTTSVoice(voice) {
|
||||||
|
this.ttsVoice = voice
|
||||||
|
},
|
||||||
|
setSendDirectly(val) {
|
||||||
|
this.sendDirectly = val
|
||||||
|
},
|
||||||
|
setAutoTTS(val) {
|
||||||
|
this.autoTTS = val
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: {
|
||||||
|
key: 'speakStore',
|
||||||
|
storage: localStorage,
|
||||||
|
paths: ['ttsVoice', 'sendDirectly', 'autoTTS']
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,62 @@
|
||||||
|
import Speech from 'speak-tts'
|
||||||
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
|
||||||
|
let speechInstance = null
|
||||||
|
let lastVoice = ''
|
||||||
|
|
||||||
|
export async function speakText(text) {
|
||||||
|
if (!text) return
|
||||||
|
const speakStore = useSpeakStore()
|
||||||
|
if (!speechInstance) {
|
||||||
|
speechInstance = new Speech()
|
||||||
|
if (!speechInstance.hasBrowserSupport()) {
|
||||||
|
console.warn('当前浏览器不支持语音合成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await speechInstance.init({
|
||||||
|
lang: 'zh-CN',
|
||||||
|
volume: speakStore.ttsVolume,
|
||||||
|
rate: speakStore.ttsRate,
|
||||||
|
pitch: speakStore.ttsPitch,
|
||||||
|
splitSentences: true,
|
||||||
|
voice: speakStore.ttsVoice || undefined
|
||||||
|
})
|
||||||
|
lastVoice = speakStore.ttsVoice || ''
|
||||||
|
}
|
||||||
|
// 切换voice或参数时重新init
|
||||||
|
if ((speakStore.ttsVoice || '') !== lastVoice
|
||||||
|
|| speechInstance._volume !== speakStore.ttsVolume
|
||||||
|
|| speechInstance._rate !== speakStore.ttsRate
|
||||||
|
|| speechInstance._pitch !== speakStore.ttsPitch) {
|
||||||
|
await speechInstance.init({
|
||||||
|
lang: 'zh-CN',
|
||||||
|
volume: speakStore.ttsVolume,
|
||||||
|
rate: speakStore.ttsRate,
|
||||||
|
pitch: speakStore.ttsPitch,
|
||||||
|
splitSentences: true,
|
||||||
|
voice: speakStore.ttsVoice || undefined
|
||||||
|
})
|
||||||
|
lastVoice = speakStore.ttsVoice || ''
|
||||||
|
}
|
||||||
|
speakStore.setTTSStatus('playing')
|
||||||
|
speechInstance.speak({
|
||||||
|
text,
|
||||||
|
onend: () => speakStore.setTTSStatus('idle'),
|
||||||
|
onerror: () => speakStore.setTTSStatus('idle')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTTSVoices() {
|
||||||
|
// 兼容speak-tts 2.x,使用浏览器原生API
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let voices = window.speechSynthesis.getVoices();
|
||||||
|
if (voices.length) {
|
||||||
|
resolve(voices);
|
||||||
|
} else {
|
||||||
|
window.speechSynthesis.onvoiceschanged = () => {
|
||||||
|
voices = window.speechSynthesis.getVoices();
|
||||||
|
resolve(voices);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -28,7 +28,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="message-actions">
|
<div class="message-actions">
|
||||||
<!-- 改进建议 -->
|
<!-- 改进建议 -->
|
||||||
<EvaluationButton
|
<EvaluationButton
|
||||||
|
class="message-action-button"
|
||||||
:show-evaluation="message.showEvaluation"
|
:show-evaluation="message.showEvaluation"
|
||||||
:responding-type="message.respondingType"
|
:responding-type="message.respondingType"
|
||||||
@toggle="chatStore.toggleEvaluation(message.messageId)"
|
@toggle="chatStore.toggleEvaluation(message.messageId)"
|
||||||
|
@ -64,7 +65,14 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-actions"></div>
|
<div class="message-actions">
|
||||||
|
<!-- 文字转语音 -->
|
||||||
|
<TextToVoiceButton
|
||||||
|
v-if="message.kehu && message.respondingType !== 'kehu'"
|
||||||
|
class="message-action-button"
|
||||||
|
:text="message.kehu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,7 +126,8 @@ import ChatHeader from '@/views/chat/components/ChatHeader.vue'
|
||||||
import ChatInput from '@/views/chat/components/ChatInput.vue'
|
import ChatInput from '@/views/chat/components/ChatInput.vue'
|
||||||
import VoiceInput from '@/views/chat/components/VoiceInput.vue'
|
import VoiceInput from '@/views/chat/components/VoiceInput.vue'
|
||||||
import EvaluationButton from '@/views/chat/components/EvaluationButton.vue'
|
import EvaluationButton from '@/views/chat/components/EvaluationButton.vue'
|
||||||
import { ElTag, ElButton, ElAvatar, ElIcon } from 'element-plus'
|
import TextToVoiceButton from '@/views/chat/components/TextToVoiceButton.vue'
|
||||||
|
import { ElTag, ElButton, ElAvatar } from 'element-plus'
|
||||||
import { Microphone, ChatDotSquare, Close } from '@element-plus/icons-vue'
|
import { Microphone, ChatDotSquare, Close } from '@element-plus/icons-vue'
|
||||||
import userAvatarUrl from '@/assets/user.png';
|
import userAvatarUrl from '@/assets/user.png';
|
||||||
import customAvatarUrl from '@/assets/custom.png';
|
import customAvatarUrl from '@/assets/custom.png';
|
||||||
|
@ -938,14 +947,14 @@ html.dark .background-section .background-content {
|
||||||
40% { transform: scale(1.2); }
|
40% { transform: scale(1.2); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-actions {
|
.message-actions .message-action-button {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content:hover .message-actions,
|
.message-wrapper:hover .message-actions .message-action-button,
|
||||||
.message-content:focus-within .message-actions {
|
.message-wrapper:focus-within .message-actions .message-action-button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<el-button type="primary" link :icon="Microphone" @click="handleTextToVoice">
|
||||||
|
语音
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Microphone } from '@element-plus/icons-vue'
|
||||||
|
import { speakText } from '@/utils/speak'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleTextToVoice() {
|
||||||
|
speakText(props.text)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -74,7 +74,7 @@ import 'recorder-core/src/engine/pcm'
|
||||||
import 'recorder-core/src/engine/wav'
|
import 'recorder-core/src/engine/wav'
|
||||||
import WebSocketClient from '@/utils/websocket'
|
import WebSocketClient from '@/utils/websocket'
|
||||||
import AudioWaveform from '@/components/AudioWaveform.vue'
|
import AudioWaveform from '@/components/AudioWaveform.vue'
|
||||||
import { useSettingsStore } from '@/store/settings'
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
|
||||||
// Props definition
|
// Props definition
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -88,7 +88,7 @@ const props = defineProps({
|
||||||
const emit = defineEmits(['sendText', 'fillInput'])
|
const emit = defineEmits(['sendText', 'fillInput'])
|
||||||
|
|
||||||
// 获取设置
|
// 获取设置
|
||||||
const settingsStore = useSettingsStore()
|
const speakStore = useSpeakStore()
|
||||||
|
|
||||||
// Refs related to voice input
|
// Refs related to voice input
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
|
@ -201,7 +201,7 @@ const confirmVoiceInput = async () => {
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
|
|
||||||
if (realTimeText.value) {
|
if (realTimeText.value) {
|
||||||
if (settingsStore.voiceInputSettings.sendDirectly) {
|
if (speakStore.sendDirectly) {
|
||||||
emit('sendText', realTimeText.value)
|
emit('sendText', realTimeText.value)
|
||||||
} else {
|
} else {
|
||||||
emit('fillInput', realTimeText.value)
|
emit('fillInput', realTimeText.value)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<span class="settings-label">语音转文字后直接发送</span>
|
<span class="settings-label">语音转文字后直接发送</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="voiceInputSettings.sendDirectly"
|
v-model="sendDirectly"
|
||||||
@change="updateVoiceInputSettings"
|
@change="updateVoiceInputSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,14 +20,17 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useSettingsStore } from '@/store/settings'
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const speakStore = useSpeakStore()
|
||||||
|
|
||||||
// 语音输入设置
|
const sendDirectly = computed({
|
||||||
const voiceInputSettings = computed({
|
get: () => speakStore.sendDirectly,
|
||||||
get: () => settingsStore.voiceInputSettings,
|
set: (val) => speakStore.setSendDirectly(val)
|
||||||
set: (value) => settingsStore.updateVoiceInputSettings(value)
|
})
|
||||||
|
const autoTTS = computed({
|
||||||
|
get: () => speakStore.autoTTS,
|
||||||
|
set: (val) => speakStore.setAutoTTS(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ... 其他代码 ...
|
// ... 其他代码 ...
|
||||||
|
|
93
yarn.lock
93
yarn.lock
|
@ -1342,34 +1342,11 @@ browserslist@^4.23.1, browserslist@^4.24.0, browserslist@^4.24.4:
|
||||||
node-releases "^2.0.19"
|
node-releases "^2.0.19"
|
||||||
update-browserslist-db "^1.1.1"
|
update-browserslist-db "^1.1.1"
|
||||||
|
|
||||||
buffer-alloc-unsafe@^1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
|
||||||
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
|
||||||
|
|
||||||
buffer-alloc@^1.1.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
|
||||||
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
|
|
||||||
dependencies:
|
|
||||||
buffer-alloc-unsafe "^1.1.0"
|
|
||||||
buffer-fill "^1.0.0"
|
|
||||||
|
|
||||||
buffer-builder@^0.2.0:
|
buffer-builder@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f"
|
resolved "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f"
|
||||||
integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==
|
integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==
|
||||||
|
|
||||||
buffer-fill@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
|
||||||
integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==
|
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
|
||||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
|
||||||
|
|
||||||
bundle-name@^4.1.0:
|
bundle-name@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
resolved "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
||||||
|
@ -1471,11 +1448,6 @@ copy-anything@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-what "^4.1.8"
|
is-what "^4.1.8"
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
|
||||||
|
|
||||||
cross-spawn@^7.0.3:
|
cross-spawn@^7.0.3:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||||
|
@ -1531,13 +1503,6 @@ de-indent@^1.0.2:
|
||||||
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||||
|
|
||||||
debug@2, debug@^2.2.0:
|
|
||||||
version "2.6.9"
|
|
||||||
resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
|
||||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
|
||||||
dependencies:
|
|
||||||
ms "2.0.0"
|
|
||||||
|
|
||||||
debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||||
|
@ -1965,11 +1930,6 @@ immutable@^5.0.2:
|
||||||
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354"
|
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354"
|
||||||
integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==
|
integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==
|
||||||
|
|
||||||
inherits@~2.0.1:
|
|
||||||
version "2.0.4"
|
|
||||||
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
|
||||||
|
|
||||||
is-core-module@^2.16.0:
|
is-core-module@^2.16.0:
|
||||||
version "2.16.1"
|
version "2.16.1"
|
||||||
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
||||||
|
@ -2033,11 +1993,6 @@ is-wsl@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container "^1.0.0"
|
is-inside-container "^1.0.0"
|
||||||
|
|
||||||
isarray@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
|
||||||
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
|
|
||||||
|
|
||||||
isexe@^2.0.0:
|
isexe@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||||
|
@ -2204,11 +2159,6 @@ mrmime@^2.0.0:
|
||||||
resolved "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
resolved "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
||||||
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
||||||
|
|
||||||
ms@2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
|
||||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
|
||||||
|
|
||||||
ms@^2.1.3:
|
ms@^2.1.3:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
|
@ -2323,11 +2273,6 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3:
|
||||||
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||||
|
|
||||||
pcm@^1.0.3:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.npmmirror.com/pcm/-/pcm-1.0.3.tgz#8e9508ab321f93ca61cd4ab44cc846530a185f24"
|
|
||||||
integrity sha512-N/qULxlCd++KuA4gAi9+wG9HpPmSOHowYj6KumrGsaoDVrcswut6K/GomqoFcbVXSnAnAf2SevVn1ehTQgFdHA==
|
|
||||||
|
|
||||||
perfect-debounce@^1.0.0:
|
perfect-debounce@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||||
|
@ -2713,16 +2658,6 @@ read-cache@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pify "^2.3.0"
|
pify "^2.3.0"
|
||||||
|
|
||||||
readable-stream@^1.1.14:
|
|
||||||
version "1.1.14"
|
|
||||||
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
|
||||||
integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==
|
|
||||||
dependencies:
|
|
||||||
core-util-is "~1.0.0"
|
|
||||||
inherits "~2.0.1"
|
|
||||||
isarray "0.0.1"
|
|
||||||
string_decoder "~0.10.x"
|
|
||||||
|
|
||||||
readdirp@^4.0.1:
|
readdirp@^4.0.1:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
||||||
|
@ -2997,6 +2932,11 @@ slash@^5.1.0:
|
||||||
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
|
speak-tts@^2.0.8:
|
||||||
|
version "2.0.8"
|
||||||
|
resolved "https://registry.npmmirror.com/speak-tts/-/speak-tts-2.0.8.tgz#9ed6660fcd710840fcc01144e73ba5ea35f99c1e"
|
||||||
|
integrity sha512-VY6Q6mRjdou6bF+x0LspvM7GJhBxHx8CLyGPTNQQ7jrztiGutyI4QNZn0cA17c4uk0FnFbA4PaMI3skeZ6PiFg==
|
||||||
|
|
||||||
speakingurl@^14.0.1:
|
speakingurl@^14.0.1:
|
||||||
version "14.0.1"
|
version "14.0.1"
|
||||||
resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
|
resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
|
||||||
|
@ -3007,18 +2947,6 @@ std-env@^3.8.1:
|
||||||
resolved "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
|
resolved "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
|
||||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||||
|
|
||||||
stream-parser@^0.3.1:
|
|
||||||
version "0.3.1"
|
|
||||||
resolved "https://registry.npmmirror.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
|
|
||||||
integrity sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==
|
|
||||||
dependencies:
|
|
||||||
debug "2"
|
|
||||||
|
|
||||||
string_decoder@~0.10.x:
|
|
||||||
version "0.10.31"
|
|
||||||
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
|
||||||
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
|
|
||||||
|
|
||||||
strip-final-newline@^4.0.0:
|
strip-final-newline@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
|
resolved "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
|
||||||
|
@ -3306,17 +3234,6 @@ vue@^3.5.13:
|
||||||
"@vue/server-renderer" "3.5.13"
|
"@vue/server-renderer" "3.5.13"
|
||||||
"@vue/shared" "3.5.13"
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
wav@^1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/wav/-/wav-1.0.2.tgz#bdbf3fa0d9b4519e9dfd2f603299ead0a2f22060"
|
|
||||||
integrity sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==
|
|
||||||
dependencies:
|
|
||||||
buffer-alloc "^1.1.0"
|
|
||||||
buffer-from "^1.0.0"
|
|
||||||
debug "^2.2.0"
|
|
||||||
readable-stream "^1.1.14"
|
|
||||||
stream-parser "^0.3.1"
|
|
||||||
|
|
||||||
webpack-virtual-modules@^0.6.2:
|
webpack-virtual-modules@^0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||||
|
|
Loading…
Reference in New Issue