使用 Vue.js 实现 TTS(Text-to-Speech)编辑器:开发经验分享

1,710 阅读7分钟

在现代网页应用中,TTS(Text-to-Speech,文本转语音)功能被广泛应用于无障碍浏览、语音助手、自动化播报等场景。为了更好地管理和编辑文本中的语音属性,开发一个 TTS 编辑器显得尤为重要。本文将分享我在开发 TTS 编辑器过程中遇到的技术问题及解决方案,帮助读者更好地理解如何从零开始构建一个功能完备的 TTS 编辑器。

一、项目背景与需求

kBnPmMHRy35YKcr.jpg

TTS 编辑器的核心需求包括:

  1. 文本编辑功能:用户可以自由输入或粘贴文本。
  2. 语音标记:用户可以对文本中的某些部分添加语音属性标签,比如设置语速、语调等。
  3. 多音字处理:支持处理多音字,让用户选择正确的发音。
  4. 预览与导出:用户可以在编辑器中预览TTS效果,并将编辑的内容导出为标准化的格式(如 SSML)。

二、技术选型与架构设计

  1. 前端技术选型
  • Vue.js:主流的前端框架,提供了强大的双向绑定、组件化开发能力,能够帮助我们高效构建交互复杂的应用。
  • Web Speech API:原生浏览器提供的 API,用于实现 TTS 功能。如果需要支持更复杂的场景,可以集成第三方 TTS 服务。
  • Ant Design Vue:用于构建编辑器的 UI 组件库,提供了丰富的交互组件(如弹窗、表单等),提升开发效率。
  1. 项目架构设计

    整个 TTS 编辑器的架构可以简单分为以下几部分:

    • 文本编辑器组件:核心的文本输入与编辑功能。
    • 语音标记组件:用于添加语音标签、调整语速、语调等。
    • 预览与导出模块:用户可以在编辑器中实时预览文本的语音效果,并导出为标准化格式(如 SSML)。
    • 多音字处理模块:处理中文中的多音字问题,提供交互选项让用户选择正确发音。

三、关键功能实现

  1. 文本编辑功能

文本编辑器是整个项目的核心部分,负责处理用户输入和文本标记。在 Vue 中,我们使用 contenteditable 属性来实现一个可编辑的文本区域。通过监听用户的输入和选择,我们可以对文本进行操作。

<div
  class="tts-editor-content"
  contenteditable="true"
  @input="handleInput"
  @mouseup="handleSelection"
></div>
  • handleInput 方法用来监听文本内容的变化,并根据需求实时更新数据。
  • handleSelection 用来捕获用户选择的文本部分,这在后续添加语音标记时非常重要。
  1. 语音标记功能

为了让用户能够自由标记文本的语音属性(如语速、语调等),我们需要实现一个标记功能,允许用户选择一段文本并插入标签。通过 JavaScript 的 Range API,我们可以精确地获取用户选择的文本位置,并在此处插入带有特定样式的 span 标签。

handleSelection() {
  const selection = window.getSelection();
  if (!selection.rangeCount) return;
  
  const range = selection.getRangeAt(0);
  const selectedText = range.toString();

  if (selectedText) {
    // 创建包含语音标记的 span 标签
    const span = document.createElement("span");
    span.textContent = selectedText;
    span.classList.add("tts-tag"); // 自定义样式

    // 插入 span 到 DOM
    range.deleteContents();
    range.insertNode(span);
  }
}

通过这种方式,用户可以对文本中任意部分进行语音属性标记。

  1. 多音字处理

中文中的多音字是一个常见的问题,当一个多音字在文本中多次出现时,用户需要对每个出现位置的字做不同的选择。为了实现这个功能,我们不仅要记录多音字的选择,还要确保每个相同字符能够独立选择正确的发音。

实现步骤

  1. 捕获多音字及其位置:在用户点击多音字时,我们要确保获取该多音字在文本中的索引位置,避免覆盖其他相同的多音字。这里我们使用 getSelection API 结合自定义属性来标记多音字的位置。

  2. 弹窗选择多音字发音:为每个多音字弹出一个选择框,用户可以从中选择合适的拼音。同时,为每个多音字记录其唯一的索引,确保处理不同位置的多音字。

  3. 记录每个多音字的发音:使用一个数据结构(如数组或对象)来记录每个多音字及其对应的拼音选择,保证相同字的不同位置可以保存不同的发音。

代码实现

  1. 更新多音字点击事件

在点击多音字时,我们不仅要记录被点击的字,还需要获取该字在文本中的位置。通过 getSelection,我们可以计算出当前点击的多音字在原始文本中的索引。

handlePinyinClick(event) {
  const selection = window.getSelection();
  const range = selection.getRangeAt(0);
  const clickedText = range.startContainer.textContent;
  
  const highlightedText = event.target.textContent; // 获取点击的多音字

  // 计算当前点击的多音字在原始文本中的位置
  const indexInOriginalText = this.getOriginalTextIndex(clickedText, highlightedText, range.startOffset);

  // 弹出拼音选择弹窗,并传入该字的索引位置
  this.showPinyinSelectionPopup(highlightedText, indexInOriginalText);
}

2. 计算多音字在原始文本中的位置

为了准确处理多个相同字符,我们需要根据用户点击的位置找到当前多音字在原文本中的实际索引。

getOriginalTextIndex(clickedText, highlightedText, startOffset) {
  const allIndices = [];
  let index = clickedText.indexOf(highlightedText);

  // 收集所有出现该多音字的索引
  while (index !== -1) {
    allIndices.push(index);
    index = clickedText.indexOf(highlightedText, index + 1);
  }

  // 根据 startOffset 找到实际点击的那个多音字的索引
  let actualIndex = 0;
  for (let i = 0; i < allIndices.length; i++) {
    if (startOffset >= allIndices[i]) {
      actualIndex = allIndices[i];
    } else {
      break;
    }
  }

  return actualIndex;
}

3. 拼音选择弹窗

当用户点击多音字时,会弹出一个选择拼音的弹窗。我们需要记录该字的索引和用户的选择。可以用数组来记录每个多音字的选择,确保相同的字在不同位置能有独立的发音选择。

<a-popover
  v-if="isPolyphonicCharacter"
  placement="bottom"
  title="选择正确发音"
  :content="generatePinyinOptions"
>
  <span @click="handlePinyinClick">{{ highlightedText }}</span>
</a-popover>

拼音选择处理:

showPinyinSelectionPopup(text, indexInOriginalText) {
  // 弹窗显示拼音选项,并记录用户选择的拼音
  const pinyinOptions = this.getPinyinOptions(text);
  
  this.$modal.confirm({
    title: '选择拼音',
    content: (
      <div>
        {pinyinOptions.map(pinyin => (
          <a onClick={() => this.savePinyinSelection(indexInOriginalText, pinyin)}>
            {pinyin}
          </a>
        ))}
      </div>
    ),
    onOk() {
      // 保存用户选择
    },
  });
}

savePinyinSelection(indexInOriginalText, selectedPinyin) {
  // 记录每个多音字的发音选择,基于其在原文本中的索引位置
  this.clickRecords[indexInOriginalText] = selectedPinyin;
}

4. 预览与导出

预览功能通过 Web Speech API 实现。用户可以实时预览编辑后的文本转语音效果。

const synth = window.speechSynthesis;
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.0; // 设置语速
utterance.pitch = 1.0; // 设置语调
synth.speak(utterance);

导出功能主要是将标记的文本转化为 SSML 格式,以供其他系统使用。SSML 是一种用于控制 TTS 系统发音的 XML 格式。我们通过遍历 DOM,提取语音标签并生成 SSML 内容。

convertToSSML() {
  let ssml = `<speak>`;
  this.$refs.editor.childNodes.forEach(node => {
    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains("tts-tag")) {
      ssml += `<prosody rate="1.2">${node.textContent}</prosody>`;
    } else {
      ssml += node.textContent;
    }
  });
  ssml += `</speak>`;
  return ssml;
}

四、npm发布

为了方便开发者集成 TTS 编辑器的多音字处理和其他相关功能,我将所有功能模块封装成了一个 npm 包,便于在其他项目中快速使用。这个 npm 包已经包含了所有 TTS 编辑器的核心功能,包括多音字处理、文本转换为 SSML、拼音选择等。

你可以通过以下方式安装并使用该 npm 包:

npm install @lucifinil/tts-editor 

这个包已经集成了包括多音字处理、文本转 SSML、拼音选择等完整的功能,用户只需要简单配置,即可快速集成到自己的项目中。

五、结语

通过 Vue.js 和 Web Speech API,我们能够快速开发一个具备语音标记、多音字处理、实时预览与导出功能的 TTS 编辑器。在实际开发中,文本操作、DOM 操作与语音标记的交互设计是比较有挑战的部分,但通过合理的封装和模块化设计,我们可以构建出一个功能完善、易于扩展的 TTS 编辑器。

TTS 编辑器的开发从一开始处理文本到现在完整的功能封装,一步步实现了多音字处理、SSML 转换、拼音选择等复杂功能。在开发的过程中,遇到的问题也都一一解决,并通过封装成 npm 包的形式简化了用户的使用体验。如果你对这些功能感兴趣,欢迎下载和使用这个 npm 包,也期待你的反馈和建议!

如果你在开发过程中遇到了类似的挑战或有其他问题,欢迎在评论区讨论与交流!