呦呦有声技术揭秘:如何实现实现优雅的拼音标注功能

193 阅读7分钟

作为一款专注于有声内容创作的工具,「呦呦有声」 在画本编辑功能上一直追求更自然、更高效的创作体验。在有声书制作过程中,因为文本中多音字、偏僻字而导致CV读错是返音中的重灾区,所以,拼音标注功能的优化成为了我们近期迭代的重点。

传统画本工具的拼音标注方式(如「魑(chī)魅(mèi)魍(wǎng)魉(liǎng)」)不仅影响阅读流畅度,还会干扰字数统计,给内容制作和结算带来诸多麻烦。

今天就来分享 「呦呦有声」 是如何基于 Tiptap 实现拼音标注的优雅展示,以及我们在数据结构设计、交互体验上的技术思考。

传统拼音标注的痛点:为什么我们要重构?

在做 「呦呦有声」 的画本工具时,我们调研了市面上主流画本的拼音标注方式,发现大家几乎都采用 「文字 + 括号拼音」 的形式,例如:

魑(chī)魅(mèi)魍(wǎng)魉(liǎng)

虽然只有4个字,但明显已经影响阅读了,再让我们看一个更极端的案例:

纯文字版:

茕茕孑立 沆瀣一气  
踽踽独行 醍醐灌顶  
绵绵瓜瓞 奉为圭臬  
龙行龘龘 犄角旮旯

拼音版:

茕(qióng)茕(qióng)孑(jié)立(lì) 沆(hàng)瀣(xiè)一(yī)气(qì) 
踽(jǔ)踽(jǔ)独(dú)行(xíng) 醍(tí)醐(hú)灌(guàn)顶(dǐng)  
绵(mián)绵(mián)瓜(guā)瓞(dié) 奉(fèng)为(wéi)圭(guī)臬(niè)  
龙(lóng)行(xíng)龘(dá)龘(dá) 犄(jī)角(jiǎo)旮(gā)旯(lá)

在多组文字进行传统拼音标注后,已经完全丧失了可阅读性,原本的辅助功能反而成了CV配音的干扰项,并且这种方式还存在3个致命问题

  1. 阅读与配音障碍 括号和拼音会打断文本连贯性,尤其在 CV 配音时,配音演员需要跳过括号内容,严重影响朗读节奏;

  2. 字数统计不准确
    「呦呦有声」 的结算系统中,字数直接关系到制作成本。如果按照传统的拼音标注方式,则拼音会被计入总字数,导致内容创作者按字数结算时出现偏差(比如「魑(chī)」实际算 1 个汉字,却被统计为 6 个字符,即便不统计标点符号,也有4个字符);

  3. 破坏文本数据结构
    在传统模式中,拼音与正文混合存储,容易破坏段落结构,影响后台解析与二次加工。

为了解决这些问题,我们将目光投向了 Ruby 标签<ruby>)—— 这是一种 HTML 原生的注音格式,能将拼音显示在文字上方,既不干扰阅读,又能保持文本结构完整。

技术实现:基于 Tiptap 的 Ruby 标注方案

「呦呦有声」 的拼音标注功能基于 Tiptap(一款基于 ProseMirror 的富文本编辑框架)实现,核心是自定义 Ruby 标记(Mark)和配套的交互逻辑。

1. 自定义 Tiptap Mark:RubyMark 的设计

Tiptap 中,「标记(Mark)」用于描述文本的附加属性(如加粗、斜体)。我们通过自定义 RubyMark,让编辑器支持拼音标注的存储和渲染:

// RubyMark.js
import { Mark } from '@tiptap/core'

export const RubyMark = Mark.create({
  name: 'ruby', // 标记名称,用于后续调用

  // 定义属性:存储需要标注的文本(anchor)和对应的拼音(pinyin)
  addAttributes() {
    return {
      anchor: {
        default: null, // 存储标注的文本(如 ["魑", "魅"])
        parseHTML: element => {
          // 从 HTML 解析时,读取自定义属性
          return JSON.parse(element.getAttribute('data-anchor') || '[]')
        },
        renderHTML: attributes => {
          // 渲染时,将属性存入自定义数据属性
          return { 'data-anchor': JSON.stringify(attributes.anchor) }
        }
      },
      pinyin: {
        default: null, // 存储对应的拼音(如 ["chī", "mèi"])
        parseHTML: element => {
          return JSON.parse(element.getAttribute('data-pinyin') || '[]')
        },
        renderHTML: attributes => {
          return { 'data-pinyin': JSON.stringify(attributes.pinyin) }
        }
      }
    }
  },

  // 解析 HTML 时识别 <ruby> 标签
  parseHTML() {
    return [{ tag: 'ruby' }]
  },

  // 渲染时返回基础结构,实际样式由装饰器处理
  renderHTML({ mark, HTMLAttributes, children }) {
    return [
      'ruby',
      HTMLAttributes,
      // 文本内容
      children,
      // 拼音标签(<rt> 是 Ruby 标签中用于显示注音的部分)
      ['rt', null, mark.attrs.pinyin.join('、')]
    ]
  }
})

设计思路:

  • 通过 anchor 和 pinyin 两个属性,一对一存储「需要标注的文本」和「对应的拼音」,支持多组标注(如一段文本中多个词语需要标音)。
  • 渲染时使用原生 <ruby> 和 <rt> 标签,确保拼音显示在文字上方(需配合 CSS 调整样式)。

效果展示:

  1. 极端文本展示:

WechatIMG2912.jpg

  1. 常规文本展示:

WechatIMG2909.jpg

2. 拼音标注的交互逻辑:从选中到展示

为了让用户能直观地添加 / 删除拼音,我们在 「呦呦有声」 中封装了 useAiPinyinUtils 工具函数,处理选中文本、调用接口生成拼音、更新标记等逻辑,具体提供了以下3个核心功能:

  1. pinyinAnnotation:获取用户选中的文字,用于定位其在段落内的绝对位置。
  2. setPinyinMark:将拼音写入到 RubyMarkanchorpinyin 属性中,可多次追加。
  3. removeRubyMarkInSelection:删除选中部分的拼音标注,不影响其他标注。

部分核心代码:

const setPinyinMark = (params) => {
  if (!params || params.code !== 200 || !params.data) return

  const editor = getEditor()
  const { keyword, data: pinyinArray, selection } = params.data
  const { from, to } = selection

  editor.commands.setTextSelection({ from, to })

  const { $from } = editor.state.selection
  const paragraph = $from.node()
  const rubyMark = paragraph?.marks?.find(mark => mark.type.name === 'ruby')

  let existingAnchor = rubyMark?.attrs.anchor || []
  let existingPinyin = rubyMark?.attrs.pinyin || []

  if (existingAnchor.includes(keyword)) return

  existingAnchor.push(keyword)
  existingPinyin.push(pinyinArray)

  editor
    .chain()
    .focus()
    .setMark('ruby', { anchor: existingAnchor, pinyin: existingPinyin })
    .run()
}

3. 数据结构设计:为什么拼音不影响字数统计?

核心设计亮点在于:拼音信息存储在 Mark 的属性中,而非文本节点内

  • Tiptap 中,字数统计 基于文本节点的 textContent 计算,而 Mark 的属性属于元数据,不会被计入。
  • 即使删除或修改拼音,文本节点本身不会被破坏,避免了「标注后文本结构错乱」的问题。

例如,标注「魑魅」后,编辑器的文档结构如下(简化版):

{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "text": "魑魅", // 文本内容(字数统计只看这里)
      "marks": [
        {
          "type": "ruby",
          "attrs": {
            "anchor": ["魑", "魅"],
            "pinyin": ["chī", "mèi"] // 拼音存储在属性中,不影响字数
          }
        }
      ]
    }
  ]
}

实际效果:「呦呦有声」的拼音标注体验

拼音标注.gif

「呦呦有声」 的画本工具中,用户可以:

  1. 选中任意文本,一键添加拼音(自动调用 AI 生成准确拼音);
  2. 同一段文本可标注多个拼音;
  3. 选中带拼音的文本,一键删除指定拼音;
  4. 拼音显示在文字正上方,支持自定义样式(如字号、颜色)。

这种方式既解决了传统标注的阅读障碍,又保证了字数统计的准确性,让有声书内容的创作符合人类自然的阅读习惯。

总结:技术选型与产品思考

  1. 产品层面的取舍

    我们没有采用「拼音嵌入文本」的方案,而是通过 Ruby 标签和元数据存储,本质上是为了「分离内容与标注」—— 内容创作者只需要关注文本本身,标注信息作为附加属性存在,既不干扰创作,又便于后续处理(如配音、字数统计)。

  2. 未来规划

    下一步,我们计划在 「呦呦有声」 中接入 AI 批量智能拼音标注,进一步降低人工成本。之后也会陆续推出多语言标注、方言标注等功能,进一步提升CV配音的准确和高效性。

产品介绍

呦呦有声 是一款集有声书画本、审听、对轨和后期于一体的有声书在线制作工具,致力于为有声书创作者们提供高效、优雅的使用体验。本文介绍的拼音标注功能,已经在 呦呦有声 正式上线,我们将持续深耕有声书创作工具领域,为有声书创作者提供更多专业且易用的产品功能。

如果你也在做富文本编辑相关的产品,或者对 呦呦有声 的技术实现感兴趣,欢迎在评论区交流!也可以直接体验我们的产品,感受更高效的有声书创作工具~