Aiden's blog Aiden's blog
首页
  • 前端文章

    • JavaScript
    • Vue
  • 学习笔记

    • JavaScript教程
    • JavaScript高级程序设计
    • ES6 教程
    • Vue
    • Vue3.0
    • React
    • TypeScript 从零实现 axios
    • Git
    • TypeScript
    • JS设计模式总结
    • 小程序
    • 小程序云开发
    • Echarts
    • 微前端
    • H5
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 搜索引擎
  • ES系列
  • 经典面试题
  • 知识点总结
  • uni-app
  • 算法
  • Vue3实战
  • 小程序chatgpt
  • 小程序配网流程
  • 程序WIFI配网
  • 小程序WebSocket
  • H5 WebSocket
  • H5 TTS
  • Vue3实现OSS存储
  • 大文件分片上传
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Aiden Wu

前端界的小学生
首页
  • 前端文章

    • JavaScript
    • Vue
  • 学习笔记

    • JavaScript教程
    • JavaScript高级程序设计
    • ES6 教程
    • Vue
    • Vue3.0
    • React
    • TypeScript 从零实现 axios
    • Git
    • TypeScript
    • JS设计模式总结
    • 小程序
    • 小程序云开发
    • Echarts
    • 微前端
    • H5
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 搜索引擎
  • ES系列
  • 经典面试题
  • 知识点总结
  • uni-app
  • 算法
  • Vue3实战
  • 小程序chatgpt
  • 小程序配网流程
  • 程序WIFI配网
  • 小程序WebSocket
  • H5 WebSocket
  • H5 TTS
  • Vue3实现OSS存储
  • 大文件分片上传
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • 搜索引擎

  • ES系列

  • 经典面试题

  • 知识点总结

  • uni-app

  • 算法

  • Vue3实战

  • 小程序chatgpt

  • 小程序WIFI配网

  • 小程序配网流程

  • 小程序WebSocket

  • H5 WebSocket

  • H5 TTS

    • H5 TTS
      • 播放音频
        • 方式一
        • 方式二
        • 方式三
      • 下载音频
      • 流式获取AI生成内容压入队列TTS播放
        • 获取对话内容
        • 流式获取内容
        • 队列TTS播放
  • Vue3实现OSS存储

  • 大文件分片上传

  • 技术
  • H5 TTS
wushengxin
2024-08-05
目录

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

# 方式二

// 将 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

# 方式三

// 获取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

# 下载音频

// 下载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

# 流式获取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

# 流式获取内容

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

# 队列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
编辑 (opens new window)
上次更新: 2024/08/05, 18:11:13
H5 WebSocket
Vue3实现OSS存储

← H5 WebSocket Vue3实现OSS存储→

最近更新
01
大文件分片上传
08-05
02
Vue3实现OSS存储
08-05
03
H5 WebSocket
08-05
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Aiden Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×