mirror of
https://github.com/imsyy/home.git
synced 2025-05-22 22:20:14 +09:00
弃用依赖替代,逐字样式升级,调用AMLL歌词库默认使用加速镜像源(可在设置内关闭),语音提示添加延迟时间避免洪水
This commit is contained in:
parent
3aa28f6a9c
commit
f18146c54d
35
package.json
35
package.json
@ -16,16 +16,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@worstone/vue-aplayer": "^1.0.7",
|
"@worstone/vue-aplayer": "^1.0.7",
|
||||||
"aplayer": "^1.10.1",
|
"aplayer": "^1.10.1",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.9",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"element-plus": "^2.8.8",
|
"element-plus": "^2.9.1",
|
||||||
"fetch-jsonp": "^1.3.0",
|
"fetch-jsonp": "^1.3.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"pinia": "^2.2.7",
|
"pinia": "^2.3.0",
|
||||||
"pinia-plugin-persistedstate": "^4.1.3",
|
|
||||||
"pinia-plugin-persistedstate-2": "^2.0.27",
|
"pinia-plugin-persistedstate-2": "^2.0.27",
|
||||||
"swiper": "^11.1.15",
|
"swiper": "^11.1.15",
|
||||||
"three": "^0.170.0",
|
"three": "^0.171.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -37,15 +36,23 @@
|
|||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vicons/utils": "^0.1.4",
|
"@vicons/utils": "^0.1.4",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-vue": "^9.31.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.83.0",
|
||||||
"terser": "^5.36.0",
|
"terser": "^5.37.0",
|
||||||
"unplugin-auto-import": "^0.18.6",
|
"unplugin-auto-import": "^0.19.0",
|
||||||
"unplugin-vue-components": "^0.27.5",
|
"unplugin-vue-components": "^0.27.5",
|
||||||
"vite": "^5.4.11",
|
"vite": "^6.0.3",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression2": "^1.3.3",
|
||||||
"vite-plugin-pwa": "^0.20.5"
|
"vite-plugin-pwa": "^0.21.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"glob": "^9.0.1",
|
||||||
|
"@jridgewell/sourcemap-codec":"^1.5.0",
|
||||||
|
"magic-string":"^0.30.15",
|
||||||
|
"workbox-build":"^7.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1735
pnpm-lock.yaml
generated
1735
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -134,7 +134,7 @@ const siteUrl = computed(() => {
|
|||||||
|
|
||||||
&.fade-in-start {
|
&.fade-in-start {
|
||||||
text-shadow: 0px 0px 2px rgba(255, 240, 245, 1);
|
text-shadow: 0px 0px 2px rgba(255, 240, 245, 1);
|
||||||
opacity: 0.6;
|
opacity: 0.6; // 初始显示的透明度
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
transition:
|
transition:
|
||||||
color 0.5s linear,
|
color 0.5s linear,
|
||||||
@ -238,7 +238,7 @@ const siteUrl = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 240, 245, 1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
text-shadow: 3px 3px 7px rgba(255, 240, 245, 1),
|
text-shadow: 3px 3px 7px rgba(255, 240, 245, 1),
|
||||||
0px 0px 12px rgba(255, 182, 193, 1),
|
0px 0px 12px rgba(255, 182, 193, 1),
|
||||||
@ -259,7 +259,8 @@ const siteUrl = computed(() => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: auto;
|
width: auto;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
text-shadow: 0 0 6px rgba(255, 240, 245, 1),
|
color: rgba(255, 240, 245, 1);
|
||||||
|
text-shadow: 0 0 6px rgba(0, 191, 255, 1),
|
||||||
0px 0px 2px rgba(176, 224, 230, 1),
|
0px 0px 2px rgba(176, 224, 230, 1),
|
||||||
0px 0px 2px rgba(230, 230, 250, 1);
|
0px 0px 2px rgba(230, 230, 250, 1);
|
||||||
font-family: MiSans-Regular;
|
font-family: MiSans-Regular;
|
||||||
@ -278,7 +279,7 @@ const siteUrl = computed(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.8),
|
text-shadow: 0 0 6px rgba(255, 240, 245, 1),
|
||||||
0 0 2px rgba(255, 165, 0, 1),
|
0 0 2px rgba(255, 165, 0, 1),
|
||||||
0 0 2px rgba(255, 179, 71, 1);
|
0 0 2px rgba(255, 179, 71, 1);
|
||||||
font-family: MiSans-Regular;
|
font-family: MiSans-Regular;
|
||||||
@ -290,6 +291,7 @@ const siteUrl = computed(() => {
|
|||||||
// End
|
// End
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
<APlayer v-if="playList[0]" ref="player" :audio="playList" :autoplay="store.playerAutoplay" :theme="theme"
|
<APlayer v-if="playList[0]" ref="player" :audio="playList" :autoplay="store.playerAutoplay" :theme="theme"
|
||||||
:autoSwitch="false" :loop="store.playerLoop" :order="store.playerOrder" :volume="volume" :showLrc="true"
|
:autoSwitch="false" :loop="store.playerLoop" :order="store.playerOrder" :volume="volume" :showLrc="true"
|
||||||
:listFolded="listFolded" :listMaxHeight="listMaxHeight" :noticeSwitch="false" @play="onPlay" @pause="onPause"
|
:listFolded="listFolded" :listMaxHeight="listMaxHeight" :noticeSwitch="false" @play="onPlay" @pause="onPause"
|
||||||
@error="loadMusicError" />
|
@timeupdate="onTimeUp" @error="loadMusicError" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { MusicOne, PlayWrong } from "@icon-park/vue-next";
|
import { Float, MusicOne, PlayWrong } from "@icon-park/vue-next";
|
||||||
import { getPlayerList } from "@/api";
|
import { getPlayerList } from "@/api";
|
||||||
import { mainStore } from "@/store";
|
import { mainStore } from "@/store";
|
||||||
import APlayer from "@worstone/vue-aplayer";
|
import APlayer from "@worstone/vue-aplayer";
|
||||||
@ -14,7 +14,9 @@ import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
|||||||
import { decodeYrc } from "../utils/decodeYrc";
|
import { decodeYrc } from "../utils/decodeYrc";
|
||||||
|
|
||||||
const store = mainStore();
|
const store = mainStore();
|
||||||
|
let showYrcRunning = 0;
|
||||||
let lastTimestamp = Date.now();
|
let lastTimestamp = Date.now();
|
||||||
|
let nowLineStart = -1;
|
||||||
|
|
||||||
// 获取播放器 DOM
|
// 获取播放器 DOM
|
||||||
const player = ref(null);
|
const player = ref(null);
|
||||||
@ -173,10 +175,10 @@ const onPause = () => {
|
|||||||
store.setPlayerState(player.value.audioRef.paused);
|
store.setPlayerState(player.value.audioRef.paused);
|
||||||
};
|
};
|
||||||
|
|
||||||
let nowLineStart = -1
|
|
||||||
// 音频时间更新事件
|
// 音频时间更新事件
|
||||||
function showYrc() {
|
function showYrc() {
|
||||||
// 至于为什么所有源的逐字都叫 YRC 呢...因为逐字功能本来是打算写网易云音乐独占的,但好像有些偏心了(bushi)...看了一下 qrc 和 yrc 没什么大区别,就顺带捏一起了。但是就懒得改变量名了!
|
// 至于为什么所有源的逐字都叫 YRC 呢...因为逐字功能本来是打算写网易云音乐独占的,但好像有些偏心了(bushi)...看了一下 qrc 和 yrc 没什么大区别,就顺带捏一起了。但是就懒得改变量名了!
|
||||||
|
showYrcRunning = 1;
|
||||||
try {
|
try {
|
||||||
// 至于为什么要 try 呢?问得好!因为网易云接口时不时会返回一些令人费解的东西,比如没有时间轴、时间轴为负数([20720,-4200])、时间轴乱码,这些东西会造成模块直接卡死,除非刷新页面。暂时没有那么多纠错逻辑,为了防止模块死掉,就先加个 try 在这里复活自己。咕咕咕!
|
// 至于为什么要 try 呢?问得好!因为网易云接口时不时会返回一些令人费解的东西,比如没有时间轴、时间轴为负数([20720,-4200])、时间轴乱码,这些东西会造成模块直接卡死,除非刷新页面。暂时没有那么多纠错逻辑,为了防止模块死掉,就先加个 try 在这里复活自己。咕咕咕!
|
||||||
if (player.value == null) {
|
if (player.value == null) {
|
||||||
@ -221,10 +223,15 @@ function showYrc() {
|
|||||||
if (!songId) {
|
if (!songId) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const songUrlInfUrl = {
|
const songUrlInfUrl = store.playerYrcATDBF
|
||||||
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
|
? {
|
||||||
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
|
'netease': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
|
||||||
};
|
'tencent': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
|
||||||
|
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
|
||||||
|
};
|
||||||
if (!['netease', 'tencent'].includes(songServer)) {
|
if (!['netease', 'tencent'].includes(songServer)) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -263,10 +270,15 @@ function showYrc() {
|
|||||||
const songIdlrc = songUrlInfw.get('id')
|
const songIdlrc = songUrlInfw.get('id')
|
||||||
const songServerlrc = songUrlInfw.get("server");
|
const songServerlrc = songUrlInfw.get("server");
|
||||||
if (songIdlrc) {
|
if (songIdlrc) {
|
||||||
const songUrlInfwurl = {
|
const songUrlInfwurl = store.playerYrcATDBF
|
||||||
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songIdlrc}.lrc`,
|
? {
|
||||||
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songIdlrc}.lrc`
|
'netease': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.lrc`,
|
||||||
};
|
'tencent': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.lrc`
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.lrc`,
|
||||||
|
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.lrc`
|
||||||
|
};
|
||||||
if (!['netease', 'tencent'].includes(songServerlrc)) {
|
if (!['netease', 'tencent'].includes(songServerlrc)) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -365,13 +377,18 @@ function showYrc() {
|
|||||||
};
|
};
|
||||||
nowLineStart = yrcFiltered.slice(-1)[0][0];
|
nowLineStart = yrcFiltered.slice(-1)[0][0];
|
||||||
};
|
};
|
||||||
requestAnimationFrame(showYrc);
|
return requestAnimationFrame(showYrc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
return requestAnimationFrame(showYrc);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeUp = () => {
|
||||||
|
if (showYrcRunning == 0) {
|
||||||
requestAnimationFrame(showYrc);
|
requestAnimationFrame(showYrc);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
requestAnimationFrame(showYrc);
|
|
||||||
|
|
||||||
// 切换播放暂停事件
|
// 切换播放暂停事件
|
||||||
const playToggle = () => {
|
const playToggle = () => {
|
||||||
@ -479,6 +496,7 @@ defineExpose({ playToggle, changeVolume, changeSong, toggleList });
|
|||||||
#fff 85%,
|
#fff 85%,
|
||||||
hsla(0deg, 0%, 100%, 0.6) 90%,
|
hsla(0deg, 0%, 100%, 0.6) 90%,
|
||||||
hsla(0deg, 0%, 100%, 0));
|
hsla(0deg, 0%, 100%, 0));
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -54,6 +54,10 @@
|
|||||||
不稳定的网络中可能导致歌词载入速度变慢)</span>
|
不稳定的网络中可能导致歌词载入速度变慢)</span>
|
||||||
<el-switch v-model="playerYrcATDB" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
<el-switch v-model="playerYrcATDB" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="playerLrcShow && playerYrcATDB" class="item">
|
||||||
|
<span class="text" white-space="pre">调用 AMLL TTML Database 时使用镜像加速</span>
|
||||||
|
<el-switch v-model="playerYrcATDBF" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||||
|
</div>
|
||||||
<div v-if="playerLrcShow" class="item">
|
<div v-if="playerLrcShow" class="item">
|
||||||
<span class="text">逐字歌词解析总开关</span>
|
<span class="text">逐字歌词解析总开关</span>
|
||||||
<el-switch v-model="playerYrcShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
<el-switch v-model="playerYrcShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||||
@ -101,6 +105,7 @@ const {
|
|||||||
playerYrcShow,
|
playerYrcShow,
|
||||||
playerYrcShowPro,
|
playerYrcShowPro,
|
||||||
playerYrcATDB,
|
playerYrcATDB,
|
||||||
|
playerYrcATDBF,
|
||||||
} = storeToRefs(store);
|
} = storeToRefs(store);
|
||||||
|
|
||||||
// 默认选中项
|
// 默认选中项
|
||||||
|
@ -29,6 +29,7 @@ export const mainStore = defineStore("main", {
|
|||||||
playerYrcShow: true, // 逐字歌词解析总开关
|
playerYrcShow: true, // 逐字歌词解析总开关
|
||||||
playerYrcShowPro: false, // 逐字效果增强开关
|
playerYrcShowPro: false, // 逐字效果增强开关
|
||||||
playerYrcATDB: true, // 允许接入 AMLL TTML Database
|
playerYrcATDB: true, // 允许接入 AMLL TTML Database
|
||||||
|
playerYrcATDBF: true, // 接入 AMLL TTML Database 时使用镜像加速
|
||||||
yrcIndex: -1, // 逐字歌词进度存储
|
yrcIndex: -1, // 逐字歌词进度存储
|
||||||
yrcTemp: [], // 逐字歌词缓存
|
yrcTemp: [], // 逐字歌词缓存
|
||||||
yrcEnable: true,
|
yrcEnable: true,
|
||||||
@ -100,6 +101,7 @@ export const mainStore = defineStore("main", {
|
|||||||
"playerYrcShow",
|
"playerYrcShow",
|
||||||
"playerYrcShowPro",
|
"playerYrcShowPro",
|
||||||
"playerYrcATDB",
|
"playerYrcATDB",
|
||||||
|
"playerYrcATDBF",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
let currentAudio = null;
|
let currentAudio = null;
|
||||||
let audioQueue = [];
|
let audioQueue = [];
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
let controller = null; // 用于取消请求
|
let controller = null;
|
||||||
|
let timeoutId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speech
|
* Speech
|
||||||
* Made by NanoRocky
|
* Made by NanoRocky
|
||||||
* 使用指定参数生成语音并播放音频。
|
* 使用指定参数生成语音并播放音频。
|
||||||
* 该功能原为 Azure 设计,理应兼容大部分使用 post 传参的 api 。请自行根据要求修改!如果也使用 Azure ,您可直接使用 https://github.com/NanoRocky/AzureSpeechAPI-by-PHP 完成 API 部署
|
* 该功能原为 Azure 设计,理应兼容大部分使用 post 传参的 api 。请自行根据要求修改!
|
||||||
* https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/speech-synthesis-markup-voice
|
* https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/speech-synthesis-markup-voice
|
||||||
*
|
*
|
||||||
* @param {string} text - 朗读的文本
|
* @param {string} text - 朗读的文本
|
||||||
@ -16,6 +17,7 @@ let controller = null; // 用于取消请求
|
|||||||
* @param {string} [role="Boy"] - 讲话角色扮演(默认为“Boy”)
|
* @param {string} [role="Boy"] - 讲话角色扮演(默认为“Boy”)
|
||||||
* @param {string} [rate="1"] - 语速(默认为“1”)
|
* @param {string} [rate="1"] - 语速(默认为“1”)
|
||||||
* @param {string} [volume="100"] - 音量(默认为“100”)
|
* @param {string} [volume="100"] - 音量(默认为“100”)
|
||||||
|
* @param {number} [delay=300] - 等待时间【毫秒】后发出请求,防止频繁点击产生请求洪水(默认为等待300毫秒)
|
||||||
* @returns {Promise<void>} - 一个 Promise,在语音播放完成时解析或出现错误时拒绝
|
* @returns {Promise<void>} - 一个 Promise,在语音播放完成时解析或出现错误时拒绝
|
||||||
*/
|
*/
|
||||||
export function Speech(
|
export function Speech(
|
||||||
@ -25,15 +27,24 @@ export function Speech(
|
|||||||
role = "Boy",
|
role = "Boy",
|
||||||
rate = "1",
|
rate = "1",
|
||||||
volume = "100",
|
volume = "100",
|
||||||
|
delay = 300,
|
||||||
) {
|
) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// 如果有现有的等待,取消之前的 timeout
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
};
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
};
|
||||||
// 创建新的 AbortController 实例,并中断旧请求
|
// 创建新的 AbortController 实例,并中断旧请求
|
||||||
if (controller) {
|
if (controller) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
};
|
||||||
controller = new AbortController();
|
controller = new AbortController();
|
||||||
const { signal } = controller;
|
const { signal } = controller;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("text", text);
|
formData.append("text", text);
|
||||||
formData.append("voice", voice);
|
formData.append("voice", voice);
|
||||||
@ -41,26 +52,136 @@ export function Speech(
|
|||||||
formData.append("role", role);
|
formData.append("role", role);
|
||||||
formData.append("rate", rate);
|
formData.append("rate", rate);
|
||||||
formData.append("volume", volume);
|
formData.append("volume", volume);
|
||||||
|
// 在指定的 delay 后开始请求
|
||||||
|
timeoutId = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const speechapi = import.meta.env.VITE_TTS_API;
|
||||||
|
const response = await fetch(speechapi, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
signal, // 传递 AbortSignal
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
if (!response.ok) {
|
||||||
const speechapi = import.meta.env.VITE_TTS_API;
|
const errorData = await response.json();
|
||||||
const response = await fetch(speechapi, {
|
throw new Error(errorData.error);
|
||||||
method: "POST",
|
};
|
||||||
body: formData,
|
|
||||||
signal, // 传递 AbortSignal
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const blob = await response.blob();
|
||||||
const errorData = await response.json();
|
const audioUrl = URL.createObjectURL(blob);
|
||||||
throw new Error(errorData.error);
|
|
||||||
|
// 将新的音频对象添加到队列
|
||||||
|
audioQueue.push(audioUrl);
|
||||||
|
|
||||||
|
if (!isPlaying) {
|
||||||
|
playNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
function playNext() {
|
||||||
|
if (audioQueue.length === 0) {
|
||||||
|
isPlaying = false;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
isPlaying = true;
|
||||||
|
|
||||||
|
const nextAudioUrl = audioQueue.shift();
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const audio = new Audio();
|
||||||
|
audio.src = nextAudioUrl;
|
||||||
|
audio.play();
|
||||||
|
|
||||||
|
// 在音频播放结束时解析 Promise
|
||||||
|
audio.onended = () => {
|
||||||
|
resolve();
|
||||||
|
playNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果发生错误,拒绝 Promise
|
||||||
|
audio.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
playNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将当前播放的语音赋值给全局变量
|
||||||
|
currentAudio = audio;
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === "AbortError") {
|
||||||
|
console.log("Request canceled");
|
||||||
|
} else {
|
||||||
|
console.error("Error:", error.message);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止当前播放的语音,并清空播放队列。
|
||||||
|
*/
|
||||||
|
export function stopSpeech() {
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
};
|
||||||
|
audioQueue = [];
|
||||||
|
isPlaying = false;
|
||||||
|
if (controller) {
|
||||||
|
controller.abort();
|
||||||
|
controller = null;
|
||||||
|
};
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpeechLocal
|
||||||
|
* Made by NanoRocky
|
||||||
|
* 播放本地预生成的语音音频。
|
||||||
|
* 考虑到生成延迟,所以加了这个,仅必要模块调用 api 实时生成,其它模块使用预先生成好的音频。记得根据需求更换自己的音频文件哇!
|
||||||
|
*
|
||||||
|
* @param {string} fileName - 音频文件名 + 文件拓展名(请将文件放在指定路径)
|
||||||
|
* @param {number} [delay=0] - 等待时间【毫秒】后发出请求,防止频繁点击产生请求洪水(默认提前生成的不等待)
|
||||||
|
* @returns {Promise<void>} - 一个 Promise,在语音播放完成时解析或出现错误时拒绝
|
||||||
|
*/
|
||||||
|
export function SpeechLocal(fileName, delay = 0) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!fileName) {
|
||||||
|
reject(new Error("No file name provided"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioUrl = `https://file.nanorocky.top/home/speechlocal/${fileName}`;
|
||||||
|
|
||||||
|
// 如果有现有的等待,取消之前的 timeout
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
};
|
||||||
|
// 清除之前的音频
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(async () => {
|
||||||
|
// 停止当前正在播放的语音
|
||||||
|
audioQueue = [];
|
||||||
|
isPlaying = false;
|
||||||
|
if (controller) {
|
||||||
|
controller.abort();
|
||||||
|
controller = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.blob();
|
// 添加新音频到队列并播放
|
||||||
const audioUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// 将新的音频对象添加到队列
|
|
||||||
audioQueue.push(audioUrl);
|
audioQueue.push(audioUrl);
|
||||||
|
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
playNext();
|
playNext();
|
||||||
}
|
}
|
||||||
@ -74,14 +195,14 @@ export function Speech(
|
|||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
|
|
||||||
const nextAudioUrl = audioQueue.shift();
|
const nextAudioUrl = audioQueue.shift();
|
||||||
if (currentAudio) {
|
|
||||||
currentAudio.pause();
|
|
||||||
currentAudio = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
audio.src = nextAudioUrl;
|
audio.src = nextAudioUrl;
|
||||||
audio.play();
|
|
||||||
|
// 确保新的音频对象没有被中途替换
|
||||||
|
audio.oncanplaythrough = () => {
|
||||||
|
currentAudio = audio;
|
||||||
|
currentAudio.play();
|
||||||
|
};
|
||||||
|
|
||||||
// 在音频播放结束时解析 Promise
|
// 在音频播放结束时解析 Promise
|
||||||
audio.onended = () => {
|
audio.onended = () => {
|
||||||
@ -94,104 +215,7 @@ export function Speech(
|
|||||||
reject(error);
|
reject(error);
|
||||||
playNext();
|
playNext();
|
||||||
};
|
};
|
||||||
|
};
|
||||||
// 将当前播放的语音赋值给全局变量
|
}, delay);
|
||||||
currentAudio = audio;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === "AbortError") {
|
|
||||||
console.log("Request canceled");
|
|
||||||
} else {
|
|
||||||
console.error("Error:", error.message);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止当前播放的语音,并清空播放队列。
|
|
||||||
*/
|
|
||||||
export function stopSpeech() {
|
|
||||||
if (currentAudio) {
|
|
||||||
currentAudio.pause();
|
|
||||||
currentAudio = null;
|
|
||||||
}
|
|
||||||
audioQueue = [];
|
|
||||||
isPlaying = false;
|
|
||||||
if (controller) {
|
|
||||||
controller.abort();
|
|
||||||
controller = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SpeechLocal
|
|
||||||
* Made by NanoRocky
|
|
||||||
* 播放本地预生成的语音音频。
|
|
||||||
* 考虑到生成延迟,所以加了这个,仅必要模块调用 api 实时生成,其它模块使用预先生成好的音频。记得根据需求更换自己的音频文件哇!
|
|
||||||
*
|
|
||||||
* @param {string} fileName - 音频文件名 + 文件拓展名(请将文件放在指定路径)
|
|
||||||
* @returns {Promise<void>} - 一个 Promise,在语音播放完成时解析或出现错误时拒绝
|
|
||||||
*/
|
|
||||||
export function SpeechLocal(fileName) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!fileName) {
|
|
||||||
reject(new Error("No file name provided"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioUrl = `/speechlocal/${fileName}`;
|
|
||||||
|
|
||||||
// 清除之前的音频
|
|
||||||
if (currentAudio) {
|
|
||||||
currentAudio.pause();
|
|
||||||
currentAudio = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止当前正在播放的语音
|
|
||||||
audioQueue = [];
|
|
||||||
isPlaying = false;
|
|
||||||
if (controller) {
|
|
||||||
controller.abort();
|
|
||||||
controller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新音频到队列并播放
|
|
||||||
audioQueue.push(audioUrl);
|
|
||||||
if (!isPlaying) {
|
|
||||||
playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
function playNext() {
|
|
||||||
if (audioQueue.length === 0) {
|
|
||||||
isPlaying = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaying = true;
|
|
||||||
|
|
||||||
const nextAudioUrl = audioQueue.shift();
|
|
||||||
const audio = new Audio();
|
|
||||||
audio.src = nextAudioUrl;
|
|
||||||
|
|
||||||
// 确保新的音频对象没有被中途替换
|
|
||||||
audio.oncanplaythrough = () => {
|
|
||||||
currentAudio = audio;
|
|
||||||
currentAudio.play();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 在音频播放结束时解析 Promise
|
|
||||||
audio.onended = () => {
|
|
||||||
resolve();
|
|
||||||
playNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果发生错误,拒绝 Promise
|
|
||||||
audio.onerror = (error) => {
|
|
||||||
reject(error);
|
|
||||||
playNext();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,7 @@ import { VitePWA } from "vite-plugin-pwa";
|
|||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import AutoImport from "unplugin-auto-import/vite";
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
import Components from "unplugin-vue-components/vite";
|
import Components from "unplugin-vue-components/vite";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression2";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default ({ mode }) =>
|
export default ({ mode }) =>
|
||||||
@ -107,7 +107,7 @@ export default ({ mode }) =>
|
|||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
charset: false,
|
charset: false,
|
||||||
additionalData: `@use "./src/style/global.scss" as global;`,
|
additionalData: `@use "@/style/global.scss" as global;`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user