呦呦有声原文对比功能:从「删除线乱象」到「智能 diff 可视化」的技术实践

108 阅读7分钟

在有声书制作流程中,文本修改是高频需求,创作者需要反复调整内容以适配配音节奏、优化听众理解。但我们调研发现,许多制作组在修改原文时,并不会直接删除内容,而是采用 删除线 来标记。这种做法的原因在于:

  • 希望保留历史修改痕迹,方便对比和追溯;
  • 担心改动影响原文剧情,需要回溯到原始文本。

但这种形式的「删除线对比」看似直观,却存在明显的问题。

删除线的局限性

image.png

在调研和实践中,我们发现删除线方式存在以下弊端:

  1. 阅读体验差
    删除线过多时,文本密集且凌乱,即使降低透明度,也会让句子断裂不连贯,严重影响审听和修改效率。

  2. 只记录删除,不记录新增和修改
    删除线能标记被删内容,但无法直观记录新增和修改的部分,信息维度不完整。

  3. 原文还原成本高
    想参考原文时,用户需要把带删除线的内容和改动文本结合起来「脑补」原文,不符合人的自然思考习惯。

可见,传统的删除线方式无法很好地满足有声书制作对精准对比和直观呈现的需求。

为此,「呦呦有声」 设计了一套完整的原文对比方案:基于 diff 算法精准识别文本增删改,通过 Tiptap 装饰器实现高亮可视化,并搭配左右分屏同步滚动功能,让修改痕迹一目了然。本文将详解这一功能的技术实现思路。

核心需求:让「修改痕迹」比「删除线」更懂创作者

我们希望原文对比功能解决三个核心问题:

  1. 精准识别差异:自动区分「原文删减」「当前新增」的内容;
  2. 直观展示差异:通过视觉区分(如颜色高亮)替代删除线,更精准高效的发现变动位置的同时还不干扰文本阅读;
  3. 高效参考原文:支持原文与当前文本并行查看,且能同步滚动定位相同段落。

我们的解决方案:基于 Diff 的原文对比

针对以上问题,「呦呦有声」 实现了一个全新的 「原文对比」 功能,核心思想是:

  • 使用 diff 算法 比较原文当前文本
  • 自动识别 删除修改新增 的差异点;
  • 通过 高亮展示分屏同步对比,让改动一目了然。

基于这些需求,我们设计了「diff 算法计算差异数据清洗去重Tiptap 装饰器渲染分屏同步滚动」的技术链路,核心代码已在文中给出,接下来将逐步解析实现细节。

最终效果:

  • 左侧:保留「原文」
  • 右侧:展示「当前修改后的文本」
  • 两侧支持 同步滚动,保证对比的上下文一致。

image.png

这样,用户不需要「猜」原文是什么,也能清晰地看到改动的来源与结果。

技术实现:从 diff 计算到可视化的全链路设计

1. 差异计算:用 diff-match-patch 破解文本比对难题

文本差异计算的核心是准确识别 「原文有而当前文本没有」(删减)「当前文本有而原文没有」(新增) 的内容。我们采用了成熟的diff-match-patch库作为基础工具,它能生成两个文本间的最小差异集。

const dmp = new diff_match_patch()
const diff = dmp.diff_main(originalContentText, updateContentText)
dmp.diff_cleanupSemantic(diff)

diff_main 会输出一个差异列表,其中:

  • -1 表示删除的内容;
  • 1 表示新增的内容;
  • 0 表示相同的内容。

我们在此基础上做了进一步优化。

2. 数据清洗:让差异结果更「懂上下文」

直接通过 diff 算法得到的差异可能包含大量碎片化内容(如单个标点的修改),反而影响可读性。我们设计了processDiffList方法对差异结果进行分组、去重和上下文补充。

核心逻辑

  1. 按类型分组:将差异分为「删减(state=-1)」和「新增(state=1)」两组,忽略无差异内容(state=0);
  2. 补充上下文:为每个差异片段添加前后文信息(如相邻的句子或标点),让用户理解修改的语境;
  3. 去重冗余内容:过滤掉「既被删减又被新增」的重复片段(如格式调整导致的临时修改),聚焦实质性变化。
// 处理差异列表,提取有效信息
const processDiffList = (diffList) => {
  if (!diffList?.length) return { delInfo: [], insInfo: [] }

  const result = { delInfo: [], insInfo: [] }
  let currentGroup = []
  let currentState = null

  // 步骤1:按状态(删减/新增)分组
  for (let i = 0; i < diffList.length; i++) {
    const [state, text] = diffList[i]
    if (state === 0) {
      // 遇到无差异内容时,处理当前分组并重置
      if (currentGroup.length > 0) {
        processGroup(currentGroup, result, text)
        currentGroup = []
        currentState = null
      }
      continue
    }
    // 状态变化时,处理上一分组
    if (currentState !== null && state !== currentState) {
      processGroup(currentGroup, result, '')
      currentGroup = []
    }
    currentGroup.push({ state, text })
    currentState = state
  }
  
  // 步骤2-5:补充上下文
  // ...
  
  return result
}

最终得到两个集合:

  • delInfo:删除的内容(原文中存在,但被去掉了)
  • insInfo:新增的内容(原文中不存在,但被加上了)

3. 可视化渲染:用 Tiptap 装饰器实现高亮展示

差异计算完成后,需要在编辑器中直观展示 —— 这一步我们借助了 Tiptap 的「装饰器(Decorations)」功能,无需修改文本本身,只需动态添加样式标记。

实现思路

  • 「原文删减内容」 添加红色背景高亮;
  • 「当前文本新增内容」 添加绿色背景高亮;
  • 高亮样式不影响文本结构,避免干扰字数统计或后续加工。

代码示例

// 在Tiptap扩展中定义装饰器
import { Decoration } from '@tiptap/pm/view'

export const DiffHighlight = Extension.create({
  name: 'diffHighlight',
  addProseMirrorPlugins() {
    return [
      new Plugin({
        props: {
          decorations: () => {
            const decorations = []
            // 为删减内容添加红色高亮
            originalDiffs.value.forEach(diff => {
              const { from, to } = findPosition(diff.keyword) // 定位文本位置
              decorations.push(
                Decoration.inline(from, to, { class: 'diff-deleted' })
              )
            })
            // 为新增内容添加绿色高亮
            currentDiffs.value.forEach(diff => {
              const { from, to } = findPosition(diff.keyword)
              decorations.push(
                Decoration.inline(from, to, { class: 'diff-added' })
              )
            })
            return Decoration.set(decorations)
          }
        }
      })
    ]
  }
})

4. 交互优化:左右分屏与同步滚动

为了让用户能同时查看原文和当前文本,我们设计了左右分屏布局,并实现了「滚动同步」功能 —— 当左侧原文滚动到某段落时,右侧当前文本会自动定位到对应位置,反之亦然。

同步滚动实现

  • 监听两侧编辑器的滚动事件,计算滚动比例;
  • 根据比例映射到另一侧的滚动位置,确保相同段落对齐。
// 同步滚动核心逻辑
const syncScroll = (source, scrollTop) => {
  const sourceHeight = source.scrollHeight - source.clientHeight
  const target = source === leftEditor ? rightEditor : leftEditor
  const targetHeight = target.scrollHeight - target.clientHeight
  const scrollRatio = scrollTop / sourceHeight
  target.scrollTop = scrollRatio * targetHeight
}

功能亮点:为什么比「删除线」更适合有声书创作?

  1. 无干扰阅读:用背景高亮替代删除线,文本保持连贯性,CV 配音时无需跳过删除线内容;
  2. 完整记录修改:同时展示删减和新增内容,支持追溯所有改动历史;
  3. 高效对比体验:左右分屏 + 同步滚动,避免用户在两个版本间反复切换查找;
  4. 不破坏文本结构:高亮通过装饰器实现,不影响原文的字数统计和后续加工(如导出、配音合成)。

结语:技术如何服务于创作场景?

「呦呦有声」 的原文对比功能,本质是通过技术手段解决创作中的「隐性效率损耗」—— 从用户用删除线标记修改的习惯中,我们发现了 「保留原文参考」 的核心需求,再用 diff 算法可视化渲染等技术将其转化为更高效的工具。

未来,我们计划进一步优化:

  • 版本历史回溯(支持多次改动追踪)

  • 差异合并建议(辅助用户快速修订)

  • AI 改动分析(识别是否存在逻辑或语义偏差)

原文对比功能已在 「呦呦有声」 官网正式上线,可前往体验完整功能,如果你在创作中也遇到过文本对比的痛点,或对技术实现有更多想法,欢迎在 「呦呦有声」 的产品反馈区与我们交流~

产品介绍

呦呦有声 是一款集有声书画本、审听、对轨和后期于一体的有声书在线制作工具,致力于为有声书创作者们提供高效、优雅的使用体验。本文介绍的原文对比功能,已经在 呦呦有声 正式上线,可前往体验完整功能。

如果你在创作中也遇到过文本对比的痛点,或对技术实现有更多想法,欢迎在 呦呦有声的产品反馈区与我们交流~