沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 必看导言

    • 就业的核心问题

      • 课件
    • 表现力的训练

      • 课件
  • 简历制作

    • 课件
    • 简历准备课堂笔记
    • 个人信息课堂笔记
    • 求职意向课堂笔记
    • 技术栈课堂笔记
    • 教育经历课堂笔记
    • 个人优势与评价课堂笔记
    • 工作经历课堂笔记
    • 项目
    • 项目亮难点问题

    • 项目经历

  • 项目准备

    • 课件
    • 课件
  • 技术重点

    • 划重点

    • 非技术环节
  • 简历投递和面试准备

    • 简历投递
    • 面试准备
  • 笔面试环节知识讲解
  • 项目准备
  • 难点攻关
  • TTS性能优化
  • 课件资料
luzhichang
2024-09-18
目录

03-技术讲解

# TTS性能优化技术讲解

# 什么问题

TTS,英语全称 Text-to-Speech,中文就是“文字转语音”。

  • 阅读器应用
  • 导航系统
  • 在线教育平台
  • ....

TTS的实现直接使用 WebAPI 就能够实现。

  1. 不同的操作系统转换出来的音效不同
  2. 无法定制化语音效果
  3. 有一定的兼容性问题

因为上面罗列出来的种种原因,这里不考虑 WebAPI.

这里考虑使用第三方平台,整体架构如下:

image-20240618154956651

这种架构下,仍然存在一些待解决的问题:

  1. 大文本的转换非常耗时
  2. 大文本转换出来的音频也很大,也存在传输耗时的问题
  3. 如何给文本添加情绪

# 解决思路

  1. 将大文本进行分割,拆解成小文本。
  2. 添加情绪需要用到一些自然语言处理库
  3. 对转换过了的文本进行缓存

# 解决细节

1. 文本断句

文本断句的时候需要考虑到文本切割的一个粒度。

  1. 以标点符号来断句(推荐使用)

    function splitTextByPunctuation(text) {
      // 使用正则表达式匹配句子结束的标点符号
      return text.match(/[^。!?]*[。!?]/g) || [];
    }
    
    // 示例文本
    const longText = "这是第一句话。这里是第二句话,包含更多的内容。还有一些问句吗?当然有!这是最后一句。";
    
    // 使用标点符号进行断句
    const sentences = splitTextByPunctuation(longText);
    console.log(sentences);
    
    [
      "这是第一句话。",
      "这里是第二句话,包含更多的内容。",
      "还有一些问句吗?",
      "当然有!",
      "这是最后一句。"
    ]
    
  2. 固定长度来切割

2. 情绪标记

张三:“你在干嘛呀?我真的要被你气爆炸了”
const dialogues = [
  { speaker: '张三', text: '你在干嘛呀?我真的要被你气爆炸了', emotion: 'angry' },
  // 更多对话...
];

这里需要借助一些自然语言处理库:

  • NLTK (Natural Language Toolkit):这是一个强大的 Python 库,用于各种自然语言处理任务。NLTK有许多预训练的情绪分析模型,可以用于情绪分类。
  • TextBlob:这是一个简单易用的 Python 库,基于 NLTK 和 Pattern。它提供了情绪分析功能,可以快速对文本进行情绪分类。
  • Google Cloud Natural Language API:Google 的自然语言处理 API 提供了强大的情绪分析功能,可以处理大规模文本并返回情绪分类结果。
  • ....

这里我们可以在后端自己启用一个 python 服务,架构如下:

image-20240619100456958

整体流程如下:

  1. 客户端发送请求:客户端将文本数据发送到前端服务器。

  2. 前端服务器处理请求

    • 接收客户端请求,将文本数据通过 WebSocket 转发给后端服务器。

    • 前端服务器无需关心后端的具体处理过程,只需等待返回结果。

  3. 后端服务器进行情绪分析

    • 使用自然语言处理库(如NLTK、TextBlob)对文本进行情绪分析和标记。

    • 标记后的文本包含情绪信息,可以用于不同的语音合成。

  4. 调用第三方 TTS 平台

    • 后端服务器通过 WebSocket 与第三方 TTS 平台建立连接。

    • 将标记后的文本发送到第三方平台,进行语音合成。

    • 接收第三方平台返回的音频字节数据。

  5. 返回处理结果

    • 将音频字节数据通过 WebSocket 传回前端服务器。

    • 前端服务器将音频字节数据转换为 base64 格式,返回给客户端。

3. 并发的控制

为什么需要并发?因为大文本被拆成了小文本,文本内容减少了,但是文本的量增多了。

但是并发也需要对一次性并发的量进行控制,防止服务器过载。

  1. 限制并发的数量

    const maxConcurrency = 5; // 最大并发请求数
    
    async function fetchTTS(segment) {
      // 模拟一个TTS请求
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(`TTS result for: ${segment}`);
        }, Math.random() * 2000); // 随机延迟模拟请求时间
      });
    }
    
    async function processSegments(textSegments) {
      const results = [];
    	
      for (let i = 0; i < textSegments.length; i += maxConcurrency) {
        const segmentBatch = textSegments.slice(i, i + maxConcurrency); // 分批处理
        const segmentResults = await Promise.all(segmentBatch.map(segment => fetchTTS(segment)));
        results.push(...segmentResults); // 合并结果
      }
    
      return results;
    }
    
    // 示例文本分段
    const textSegments = ["Segment 1", "Segment 2", "Segment 3", "Segment 4", "Segment 5", "Segment 6", "Segment 7"];
    
    // 处理文本分段
    processSegments(textSegments).then(results => {
      console.log(results); // 输出处理结果
    });
    
  2. 使用队列来管理请求

    const maxConcurrency = 5; // 最大并发请求数
    let activeRequests = 0; // 当前活动请求数
    const queue = []; // 请求队列
    
    function enqueueRequest(request) {
      return new Promise((resolve, reject) => {
        queue.push({ request, resolve, reject }); // 将请求加入队列
        processQueue(); // 处理队列中的请求
      });
    }
    
    function processQueue() {
      if (activeRequests >= maxConcurrency || queue.length === 0) {
        return; // 如果达到最大并发请求数或队列为空,则返回
      }
    
      const { request, resolve, reject } = queue.shift(); // 从队列中取出一个请求
      activeRequests++; // 增加活动请求数
    
      request()
        .then(resolve)
        .catch(reject)
        .finally(() => {
          activeRequests--; // 请求完成后减少活动请求数
          processQueue(); // 继续处理队列中的下一个请求
        });
    }
    
    async function fetchTTS(segment) {
      // 模拟一个TTS请求
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(`TTS result for: ${segment}`);
        }, Math.random() * 2000); // 随机延迟模拟请求时间
      });
    }
    
    async function processSegments(textSegments) {
      const results = [];
    	
      for (let i = 0; i < textSegments.length; i += maxConcurrency) {
        const segmentBatch = textSegments.slice(i, i + maxConcurrency); // 分批处理
        const segmentResults = await Promise.all(
          segmentBatch.map(segment => enqueueRequest(() => fetchTTS(segment)))
        );
      }
    
      results.push(...segmentResults); // 合并结果
      return results;
    }
    
    // 示例文本分段
    const textSegments = ["Segment 1", "Segment 2", "Segment 3", "Segment 4", "Segment 5", "Segment 6", "Segment 7"];
    
    // 处理文本分段
    processSegments(textSegments).then(results => {
      console.log(results); // 输出处理结果
    });
    
  3. 动态的去调整并发的数量

    let maxConcurrency = 5; // 初始最大并发请求数
    
    async function adjustConcurrencyBasedOnLoad() {
      const serverLoad = await getServerLoad(); // 获取服务器负载
    
      if (serverLoad > 80) {
        maxConcurrency = Math.max(1, maxConcurrency - 1); // 如果负载超过80%,减少并发请求数
      } else if (serverLoad < 50) {
        maxConcurrency++; // 如果负载低于50%,增加并发请求数
      }
    
      setTimeout(adjustConcurrencyBasedOnLoad, 10000); // 每10秒调整一次并发请求数
    }
    
    function getServerLoad() {
      // 模拟获取服务器负载
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(Math.random() * 100); // 返回0到100之间的随机负载
        }, 1000);
      });
    }
    
    adjustConcurrencyBasedOnLoad(); // 启动并发调整
    
    // 继续使用上面的队列管理示例代码
    

4. 请求的取消

用户快速hover了第一段和第二段,那么第一段的响应变得毫无用处,因此这里涉及到一个请求取消的操作。

image-20240620152454467

AbortController,这是浏览器提供的一个 API.

结合之前的并发控制,示例代码如下:

const maxConcurrency = 5; // 最大并发请求数
let activeRequests = 0; // 当前活动请求数
const queue = []; // 请求队列
let currentController = null; // 当前的 AbortController

function enqueueRequest(request, controller) {
  return new Promise((resolve, reject) => {
    queue.push({ request, resolve, reject, controller }); // 将请求加入队列
    processQueue(); // 处理队列中的请求
  });
}

function processQueue() {
  if (activeRequests >= maxConcurrency || queue.length === 0) {
    return; // 如果达到最大并发请求数或队列为空,则返回
  }

  const { request, resolve, reject, controller } = queue.shift(); // 从队列中取出一个请求
  activeRequests++; // 增加活动请求数

  request(controller)
    .then(resolve)
    .catch(reject)
    .finally(() => {
      activeRequests--; // 请求完成后减少活动请求数
      processQueue(); // 继续处理队列中的下一个请求
    });
}

async function fetchTTS(segment, controller) {
  // 模拟一个TTS请求
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      resolve(`TTS result for: ${segment}`);
    }, Math.random() * 2000); // 随机延迟模拟请求时间

    // 监听取消信号
    controller.signal.addEventListener('abort', () => {
      clearTimeout(timeout);
      reject(new Error('Request was aborted'));
    });
  });
}

async function processSegments(textSegments) {
  const results = await Promise.all(
    textSegments.map(segment => {
      const controller = new AbortController();
      return enqueueRequest(() => fetchTTS(segment, controller), controller);
    })
  );
  return results;
}

// 示例文本分段
const textSegments = [
  "Segment 1",
  "Segment 2",
  "Segment 3",
  "Segment 4",
  "Segment 5",
  "Segment 6",
  "Segment 7",
];

// 处理文本分段
processSegments(textSegments).then(results => {
  console.log(results); // 输出处理结果
});

5. 转换base64

前端服务器拿到 bytes 数据后,可以进行一层转换,将其转换为 base64 格式的数据,这样客户端的页面上使用的时候就会更加方便,可以给生成的 audio 元素的 src 属性直接赋值这个 base64

示例代码如下:

// 前端服务器
const express = require('express');
const axios = require('axios');
const app = express();
const port = 3000;

// 示例后端服务器URL
const backendUrl = 'http://backend-server-url/get-tts-audio';

// 获取后端音频数据并转换为Base64
app.get('/get-audio', async (req, res) => {
  try {
    // 从后端服务器获取音频字节数据
    const response = await axios.get(backendUrl, { responseType: 'arraybuffer' });
    const audioBytes = response.data;

    // 将音频字节数据转换为Base64
    const base64Audio = Buffer.from(audioBytes).toString('base64');

    // 将Base64音频数据发送给客户端
    res.send(base64Audio);
  } catch (error) {
    console.error('Error fetching audio from backend:', error);
    res.status(500).send('Error fetching audio from backend');
  }
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Audio Example</title>
</head>
<body>
  <audio id="audioPlayer" controls></audio>

  <script>
    // 获取 base64 音频数据
    fetch('http://localhost:3000/get-audio')
      .then(response => response.text())
      .then(base64Audio => {
        const audioPlayer = document.getElementById('audioPlayer');
        // 将 base64 数据设置为 audio 元素的 src 属性
        audioPlayer.src = `data:audio/wav;base64,${base64Audio}`;
        audioPlayer.play();
      })
      .catch(error => console.error('Error fetching audio:', error));
  </script>
</body>
</html>

6. 进行缓存

  1. 缓存在客户端
    • 之前你转换过的文本能够再次复用
    • 速度相比缓存在服务器会更快一些
  2. 缓存在服务器端
    • 别人转换过的音频也能得到复用

如果要缓存在服务器端,那么就需要加一个数据库

image-20240619151359475

整体的流程:

  • 如果缓存中存在该文本的转换结果,前端服务器直接返回 Base64 格式的音频数据给客户端。
  • 如果缓存中不存在该文本的转换结果,前端服务器将请求转发给后端服务器,之后前端服务器将转换结果存入缓存数据库中,以便下次请求时可以直接使用。

-EOF-

Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式