H5 TTS
# TTS语音播放
音频格式:wav
audioBase64: "AQAAAAAAAAAAAA"
# 播放音频
# 方式一
const audioRawString = await getAudioRaw(text)
const audioData = `data:audio/wav;base64,${audioRawString}` // audioRawString为实际的音频base64编码数据(AQAAAAAAAAAAAA)
const audio = new Audio(audioData)
audio.play()
1
2
3
4
2
3
4
# 方式二
// 将 base64 编码的字符串转换为 ArrayBuffer
const base64ToArrayBuffer = (base64) => {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
// 使用 Web Audio API 播放 Wav 格式的音频数据
function playWav(data) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const source = audioContext.createBufferSource()
audioContext.decodeAudioData(
data,
(buffer) => {
console.log('buffer', buffer)
source.buffer = buffer
source.connect(audioContext.destination)
source.start()
},
function (error) {
console.error('Failed to decode audio data:', error)
}
)
}
const audioRawString = await getAudioRaw(text)
const audioData = base64ToArrayBuffer(audioRawString);
playWav(audioData);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 方式三
// 获取tts加载动画
const loading = ref(false)
// 音频对象
const audio = ref(null)
// 获取auidoBase64数据
const getAudioRaw = async (text) => {
const params = {
voiceId: '181-xiyangyang',
audioFormat: 'WAV',
text,
// ssml: '',
tempo: 1.0,
pitch: 1.0,
vol: 1.0,
sampleRate: 16000,
enableSubtitle: true
}
const { audioBase64 } = await getTTSCenterApi(params)
return audioBase64
}
// 将 base64 编码的字符串转换为 ArrayBuffer
function base64toBlob(base64Data, contentType) {
contentType = contentType || ''
const sliceSize = 1024
const byteCharacters = atob(base64Data)
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
const blob = new Blob(byteArrays, { type: contentType })
return blob
}
// 停止播放音频
function stopAudio() {
if (audio.value != null) {
audio.value.pause()
audio.value.currentTime = 0
audio.value.load()
}
}
// 将 base64 编码的字符串转换为 ArrayBuffer
function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
}
// 播放tts
const playAudio = async (text) => {
try {
if (!text) {
showToast('内容不能为空')
return
}
if (audio.value == null) {
audio.value = new Audio()
}
stopAudio()
loading.value = true
const audioRawString = await getAudioRaw(text)
const blob = base64toBlob(audioRawString, 'audio/wav')
console.log('blob: ', blob)
loading.value = false
audio.value.src = URL.createObjectURL(blob)
audio.value.play()
} catch (error) {
console.log('error', error)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 下载音频
// 下载audio
const downloadAudio = async (text) => {
if (!text) {
showToast('请输入自定义的文本')
return
}
const audioRawString = await getAudioRaw(text)
const blob = base64toBlob(audioRawString, 'audio/wav')
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = text
link.click()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 流式获取AI生成内容压入队列TTS播放
# 获取对话内容
import { ref, onMounted } from 'vue'
const USER = import.meta.env.VITE_USER
const DIDY_URL = import.meta.env.VITE_AI_URL
const DIDY_HISTORY_URL = import.meta.env.VITE_AI_HISTORY_URL
const CONVERSATION_URL = import.meta.env.VITE_AI_CONVERSATION_URL
const API_KEY = import.meta.env.VITE_AI_API_KEY
const CONVERSATION_ID = import.meta.env.VITE_CONVERSATION_ID
const userName = ref(USER)
onMounted(() => {
getConversationListAPI()
})
const getConversationListAPI = async () => {
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`
}
}
const response = await fetch(`${CONVERSATION_URL}?user=${userName.value}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...config.headers
}
})
// 使用 .json() 方法解析JSON响应体
const { data } = await response.json() // 假设服务器返回的是JSON格式
console.log('response data', data)
// 假设返回的data是一个包含具体数据的对象,其中有一个数组
data.forEach(async (item) => {
const chat = await getPlayHistoryAPI(item.id)
// console.log('chat', chat)
stories.value.push({
prompts: chat.query,
content: chat.answer,
status: true,
startTime: '',
endTime: '',
timeDiff: '',
enquequeLength: ''
})
})
stories.value.reverse()
}
const getPlayHistoryAPI = async (sid) => {
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`
}
const response = await fetch(
`${DIDY_HISTORY_URL}?user=${userName.value}&conversation_id=${sid}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
...headers
}
}
)
const { data } = await response.json() // 假设服务器返回的是JSON格式
return data.map((item) => ({
answer: item.answer,
query: item.query
}))[0]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 流式获取内容
import { ref } from 'vue'
const prompts = ref(`# 孩子信息
- 昵称: 小星
- 性别: 女
- 生日: 2017年5月20日
- 主题偏好(故事标签):小星非常喜欢听关于历史上的伟人和英雄的故事,尤其是那些克服困难,实现梦想的故事。
主题:喜羊羊给我讲一个大蒜超人拯救小星的故事`)
// 生成故事按钮
const genStory = () => {
let index = stories.value.unshift({
prompts: prompts.value,
content: '',
status: false
})
console.log('index', index)
// 新增异步请求 dify 生成故事内容的
// 生成的故事内容通过index存入stories
fetchStory(prompts.value, stories.value[0])
}
const fetchStory = async (query, story) => {
story.status = false
const params = {
user: userName.value,
inputs: {},
response_mode: 'streaming',
conversation_id: '',
query
}
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`
},
responseType: 'stream'
}
try {
const response = await fetch(DIDY_URL, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json',
...config.headers
}
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
story.content = ''
reader.read().then(
function processText({ done, value }) {
if (done) {
console.log('Stream complete')
story.status = true
return
}
let res = decoder.decode(value, { stream: true })
parse2json(res).forEach((block) => {
if (block.event == 'message') {
block.answer = block.answer.replace('<context>', '')
story.content += block.answer
}
})
reader.read().then(processText.bind(this))
}.bind(this)
)
} catch (error) {
console.error('Error fetching story:', error)
story.status = false
}
}
function parse2json(blocks_str) {
const blocks = blocks_str.split('data: ').filter((part) => part.trim() !== '')
function parse(part) {
// console.log('原始部分:', part)
// 检查并移除所有 'ping' 消息
if (part.startsWith('ping')) {
console.log('忽略 ping 消息')
return null // 直接返回null,不尝试解析
}
part = part.replace(/\sevent: ping\s*/g, '')
part = part.trim()
try {
if (part) {
// 确保part不为空
return JSON.parse(part)
}
} catch (error) {
console.error('解析 JSON 时出错:', error, '问题部分:', part)
}
return null // 如果part为空或解析失败,返回null
}
return blocks.map((b) => parse(b)).filter(Boolean) // 过滤掉所有null值
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 队列TTS播放
import { getTTSCenterApi } from '@/api/xiaoice.js'
class PlayQueue {
constructor() {
this.queue = []
this.isPlaying = false
}
enqueue(blob) {
this.queue.push(blob)
}
async play(index) {
if (this.isPlaying) return
this.isPlaying = true
console.log('总段数:', stories.value[index].enquequeLength)
console.log('当前剩余段数:', this.queue.length)
if (stories.value[index].enquequeLength === this.queue.length) {
stories.value[index].startTime = new Date()
}
audio.value.onended = () => {
console.log(this.queue)
if (this.queue.length <= 0) {
stories.value[index].endTime = new Date()
getPlayTime(index)
return
}
this.queue.shift().then((blob) => playSounds(blob))
}
this.queue.shift().then((blob) => playSounds(blob))
this.isPlaying = false
}
}
const getPlayTime = (index) => {
const timeDiff = stories.value[index].endTime - stories.value[index].startTime
console.log('播放毫秒:', timeDiff)
// 将毫秒转换为时分秒
let hours = Math.floor(timeDiff / 3600000) // 1 Hour = 3600000 Milliseconds
let minutes = Math.floor((timeDiff % 3600000) / 60000) // 1 Minute = 60000 Milliseconds
let seconds = ((timeDiff % 3600000) % 60000) / 1000 // 1 Second = 1000 Milliseconds
console.log(`执行时间: ${hours}小时 ${minutes}分钟 ${seconds}秒`)
stories.value[
index
].timeDiff = `播放时长:${hours}小时 ${minutes}分钟 ${seconds}秒`
}
async function playSounds(blob) {
audio.value.src = blob
audio.value.play()
}
function splitStory(data) {
return data.split(/\n/).filter((line) => line.trim() !== '')
}
async function playStory(index) {
const story = stories.value[index]
// 将多行内容合并为单行
// 按照每20个字符分割合并后的内容
const parts = splitStory(story.content)
// 创建一个新的队列实例
const playQueue = new PlayQueue()
if (audio.value == null) {
audio.value = new Audio()
}
// 遍历列表,对每个部分请求TTS转换
parts.forEach((part) => {
const blob = getBlobAsync(part)
playQueue.enqueue(blob)
})
// console.log('index', index)
stories.value[index].enquequeLength = playQueue.queue.length
setTimeout(() => {
console.log(playQueue.queue)
playQueue.play(index)
}, 1000)
}
async function getBlobAsync(text) {
const audioRawString = await getAudioRaw(text)
const blob = base64toBlob(audioRawString, 'audio/wav')
// console.log('blob: ', blob)
return URL.createObjectURL(blob)
}
const getAudioRaw = async (text) => {
const params = {
voiceId: '181-xiyangyang',
audioFormat: 'WAV',
text,
// ssml: '',
tempo: 1.0,
pitch: 1.0,
vol: 1.0,
sampleRate: 16000,
enableSubtitle: true
}
const { audioBase64 } = await getTTSCenterApi(params)
return audioBase64
}
// 将 base64 编码的字符串转换为 ArrayBuffer
function base64toBlob(base64Data, contentType) {
contentType = contentType || ''
const sliceSize = 1024
const byteCharacters = atob(base64Data)
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
const blob = new Blob(byteArrays, { type: contentType })
return blob
}
// 停止播放音频
function stopAudio() {
if (audio.value != null) {
audio.value.pause()
audio.value.currentTime = 0
audio.value.load()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
编辑 (opens new window)