基于 Vue.js 的有声书编辑器开发实践(简单版)

169 阅读2分钟

在这篇文章中,我们将介绍如何使用 Vue.js 和 SpeechSynthesis API 开发一个基础的有声书编辑器。这个编辑器允许用户在段落中插入语音角色、设置语气(如开心、伤心等),并支持段落试听功能。以下是功能的详细实现步骤。

image.png

功能概述

  1. 段落编辑:支持用户动态添加和编辑段落内容。
  2. 语音角色选择:通过 SpeechSynthesis API 动态加载系统可用的语音角色。
  3. 语气设置:为每个段落设置语气(如开心、平静、伤心等)。
  4. 段落试听:支持段落内容的语音播放,语音参数(语速、音调、音量)根据用户选择的语气动态调整。
  5. 快捷键交互:通过双击回车键快速添加新段落。

代码解析

基础结构

HTML 部分使用 v-for 遍历渲染段落列表,每个段落包含语音角色、语气选择、可编辑的内容区域,以及试听按钮。

<template>
  <div class="book-editor-wrap">
    <div v-for="(item, paragraphIndex) in paragraphList" :key="item.id" class="paragraph-wrap">
      <div class="paragraph-info">
        <!-- 语音角色选择 -->
        <a-popover title="添加角色" placement="bottom" overlayClassName="role-list" trigger="click">
          <template #content>
            <p v-for="roleItem in roleList" :key="roleItem.voiceURI" class="role-item" :class="roleItem.voiceURI === item.roleId && 'active'" @click="changeParmTone(paragraphIndex, roleItem, 'roleId')">
              {{ roleItem.voiceURI }}
            </p>
          </template>
          <div class="role-dom">{{ item.roleId.voiceURI || '暂无角色' }}</div>
        </a-popover>
        
        <!-- 语气选择 -->
        <a-popover title="添加语气" placement="bottom" overlayClassName="tone-list" trigger="click">
          <template #content>
            <p v-for="toneItem in toneList" :key="toneItem.id" class="tone-item" :class="toneItem.id === item.toneId && 'active'" @click="changeParmTone(paragraphIndex, toneItem, 'toneId')">
              {{ toneItem.tone }}
            </p>
          </template>
          <div class="tone-dom">
            <img src="https://p5.ssl.qhimg.com/t110b9a93016ab227550b4aa226.png"/>
            {{ item.toneId.tone || '心情' }}
          </div>
        </a-popover>

        <!-- 可编辑内容 -->
        <div contenteditable @keydown="handleKeyDown" :ref="`editor_${item.id}`" class="editor-dom" v-html="item.content"></div>
      </div>
      
      <!-- 段落试听 -->
      <div v-if="auditionID === item.id" class="audition-play" @click="onAudition(item, false)">
        正在试听...<img src="https://p3.ssl.qhimg.com/t110b9a9301808402d167204131.png"/>
      </div>
      <div v-else class="audition-play" @click="onAudition(item, true)">
        段落试听<img src="https://p4.ssl.qhimg.com/t110b9a9301293dc726c49e7a0f.png"/>
      </div>
    </div>
  </div>
</template>

数据结构设计

段落数据结构

每个段落保存内容、角色 ID 和语气 ID:

const defaultParagraph = {
  content: '暂无段落',
  roleId: '',
  toneId: '',
  id: 1
};

语气列表

定义不同语气的参数(语速、音调、音量):

const toneList = [
  { id: 'happy', tone: '开心', rate: 1.5, pitch: 1.8, volume: 1 },
  { id: 'sad', tone: '伤心', rate: 0.8, pitch: 0.8, volume: 0.6 },
  { id: 'angry', tone: '生气', rate: 1.2, pitch: 1.2, volume: 1 },
  { id: 'calm', tone: '平静', rate: 1, pitch: 1, volume: 0.8 },
  { id: 'default', tone: '默认', rate: 1, pitch: 1, volume: 1 }
];

核心方法

加载语音角色

通过 SpeechSynthesis API 获取系统支持的语音角色:

methods: {
  getRoleList() {
    const loadVoices = () => {
      const voices = window.speechSynthesis.getVoices();
      if (voices.length > 0) {
        this.roleList = voices;
      }
    };

    if ('speechSynthesis' in window) {
      window.speechSynthesis.onvoiceschanged = loadVoices;
      if (window.speechSynthesis.getVoices().length > 0) {
        loadVoices();
      }
    } else {
      console.error('当前浏览器不支持 SpeechSynthesis API。');
    }
  }
}

试听功能

根据段落内容、角色和语气动态调整语音播放参数:

onAudition(item, isPlay) {
  if (isPlay) {
    this.auditionID = item.id
  } else {
    this.auditionID = undefined
    return
  }
  let content = this.$refs[`editor_${item.id}`][0].innerHTML
  item.content = content
  window.speechSynthesis.cancel();
  
  const utterance = new SpeechSynthesisUtterance();
  utterance.voice = item.roleId || this.roleList[0];

  const toneInfo = item.toneId || this.toneList.find(t => t.id === 'default');
  utterance.pitch = toneInfo.pitch;
  utterance.rate = toneInfo.rate;
  utterance.volume = toneInfo.volume;

  window.speechSynthesis.speak(utterance);
}

段落快捷添加

通过双击回车键快速添加新段落:

handleKeyDown(event) {
  if (event.key === 'Enter') {
    this.enterCount++;
    clearTimeout(this.timer);
    if (this.enterCount === 1) {
      this.timer = setTimeout(() => this.enterCount = 0, 300);
    } else if (this.enterCount === 2) {
      this.enterCount = 0;
      this.paragraphList.push({ ...defaultParagraph, id: this.paragraphList.length + 1 });
    }
  }
}

总结与优化方向

通过本文的实现,我们完成了一个基础的有声书编辑器,它能动态设置段落内容、语气和语音角色,并支持段落试听。未来的优化方向包括:

  1. 语音播放进度:支持显示播放进度条。
  2. 批量试听:一次性播放多个段落。
  3. 多设备兼容性:解决不同浏览器对 SpeechSynthesis API 的支持差异。