用户脚本 iTest 音频转录器 调试与解决复盘

1. 初始问题描述

用户创建了一个 Tampermonkey 脚本,旨在 iTest 网站上,利用 vosk-browser 库对听力音频进行离线语音识别。脚本在运行时抛出 TypeError: Vosk.Recognizer is not a constructor 的错误,导致转录功能完全失败。

2. 问题分析与调试历程

整个调试过程可以分为几个关键阶段,每个阶段都基于上一个阶段的错误信息进行了新的假设和尝试。

阶段一:初步诊断 - 错误的 API 调用对象

  • 错误信息: TypeError: Vosk.Recognizer is not a constructor
  • 原始代码: new Vosk.Recognizer({ sampleRate: ... });
  • 分析与假设: 这个错误表明 Vosk.Recognizer 不是一个有效的构造函数。根据许多现代JS库的设计模式(例如 Chart.js),通常会从一个父实例(如模型实例)上创建子组件(如识别器)。因此,我们假设 Recognizer 构造函数应该在 model 实例上。
  • 尝试的解决方案: 将代码修改为 new model.Recognizer({ sampleRate: ... });
  • 结果: 失败。新的错误信息出现。

阶段二:二次诊断 - API 签名错误

  • 错误信息: TypeError: model.Recognizer is not a constructor
  • 分析与假设: 这个新错误否定了阶段一的假设。这说明 Recognizer 构造函数既不在全局的 Vosk 对象上,也不在 model 实例上。此时,我们重新审视了 vosk-browser 较新版本的文档,发现一个可能的正确用法:构造函数是 Vosk.Recognizer,但必须将 model 作为参数传入。
  • 尝试的解决方案: 将代码修改为 new Vosk.Recognizer({ model: model, sampleRate: ... });
  • 结果: 再次失败,并返回了与阶段一相同的错误 TypeError: Vosk.Recognizer is not a constructor

阶段三:关键转折 - API 版本与文档不匹配

  • 错误信息: TypeError: Vosk.Recognizer is not a constructor (反复出现)

  • 用户提供的关键信息: 用户提供了一份 vosk-browser 的旧版 (v0.0.5) 使用文档。

  • 分析与假设 (最终正解): 此时,我们面临一个矛盾:

    1. 脚本的 @require 指令明确指向 v0.0.8
    2. 所有基于 v0.0.8 API 的尝试都失败了。
    3. 错误的行为模式与用户提供的旧版文档 (v0.0.5) 描述的 API (new model.KaldiRecognizer(...) 和事件监听模式) 高度吻合。

    最终结论是:尽管 CDN URL 指向新版本,但在用户的实际运行环境(Tampermonkey沙箱 + 目标网站的JS环境)中,该库暴露出的 API 表现得像旧版本。 造成这种情况的原因可能很复杂(CDN缓存、库的打包方式、与页面其他脚本的冲突等),但我们无需深究其根源,只需接受这个事实并按照观察到的行为来编码。

阶段四:最终解决方案实施

基于第三阶段的结论,我们彻底推翻了之前的实现,完全按照旧版文档的 API 规范重写了核心的 transcribeAudio 函数:

  1. 实例化识别器:

    • 旧方法: new Vosk.Recognizer(...)new model.Recognizer(...)
    • 正确方法: recognizer = new model.KaldiRecognizer(audioBuffer.sampleRate);
  2. 获取转录结果:

    • 旧方法: 通过同步调用 recognizer.finalResult()
    • 正确方法: 采用事件驱动模式。
      • 使用 Promise 来封装这个异步过程。
      • 通过 recognizer.on("result", ...) 监听成功事件,在回调中 resolve Promise 并返回结果。
      • 通过 recognizer.on("error", ...) 监听错误事件,在回调中 reject Promise。
  3. 处理音频数据:

    • 旧方法: recognizer.acceptWaveform(audioBuffer.getChannelData(0));
    • 正确方法: recognizer.acceptWaveform(audioBuffer); (直接传入整个 AudioBuffer)
  4. 资源释放:

    • 旧方法: recognizer.free();
    • 正确方法: recognizer.remove(); 这个方法会彻底移除底层的 Web Worker,是事件驱动模型中推荐的清理方式。

将这些修正应用到脚本中,问题最终得到解决。

3. 核心收获与总结

  1. API 文档并非永远可靠: 尤其是在复杂的客户端环境(如油猴脚本)中,库的实际行为可能与文档描述不符。要相信控制台的错误日志,而不是固守文档
  2. 错误信息是最好的向导: 每一个 TypeError 都排除了一个可能性,逐步缩小了问题的范围。耐心、系统地根据错误信息进行迭代是解决问题的关键。
  3. 理解异步编程模式: 现代 JavaScript 库大量使用异步操作。本案例中的核心难点之一就是从同步的 finalResult() 模型切换到基于 Promise 封装的事件监听模型。
  4. 环境的重要性: 同一个库在不同环境(Node.js, 纯净的浏览器环境, 油猴脚本沙箱)下可能会有细微的行为差异。当标准方法行不通时,要考虑到环境因素并尝试其他可行的 API 路径。

这次调试是一个典型的“理论与实践脱节”的案例。通过一步步的试错和对错误日志的严谨分析,我们最终找到了符合实际运行环境的正确解决方案。