← 返回全部文章

从视频里的一个生词,到一个真正能用的本地优先应用:Context Vocabulary Notebook 开发复盘

从视频里的一个生词,到一个真正能用的本地优先应用:Context Vocabulary Notebook 开发复盘

我经常在看外语视频时遇到这样的情况:一句台词里有一个生词,当时结合人物的表情、语气和画面,我几乎立刻就能理解它。暂停视频,查一下词典,觉得自己已经记住了;但过几天再次见到这个词,脑子里只剩下一条干巴巴的中文释义,原来的场景早已消失。

问题可能不在于我没有记下那个单词,而在于传统单词本丢掉了最有价值的部分——我第一次理解它时的语境。

于是,我开始做 Context Vocabulary Notebook,中文名叫“语境单词本”。我想保存的不只是一个单词,而是我理解它的那个瞬间:原句、当前语境中的含义、视频片段、画面、声音,以及之后在其他地方再次遇见它时的新语境。

Context Vocabulary Notebook 项目预览

Context Vocabulary Notebook:围绕真实语境积累和复习词汇。

这篇文章不是一份功能说明书,也不是简单罗列 React、Express 和 SQLite 的技术栈介绍。我更想复盘的是:一个来自个人学习痛点的想法,怎样被收敛成产品模型,怎样经历取舍、回滚和跨平台问题,最后又怎样从“我的机器上能运行”,慢慢走到其他 GitHub 用户也能安装和理解。

一、真正的核心不是“单词”,而是“词义”

项目最重要的决定并不是选择什么框架,而是确定系统里最核心的对象是什么。

最初很容易想到的结构是:

word = charge
meaning = 收费、指控、充电……

但这种结构仍然在把“单词”当作一个稳定、单一的知识点。实际学习时,我记住的往往是某个单词在某句话中的一个具体含义。于是项目把核心对象定义成了 Word Sense Card(词义卡片)

一张词义卡片 = 一个目标单词 + 一个具体语义

例如:

charge = 收费
charge = 指控
charge = 充电

它们是三张不同的卡片。另一方面,如果我后来又在三段视频中遇到了“charge = 收费”,就不应该重复创建三张卡,而应该把三个原句和媒体片段追加到同一张词义卡片下面。

因此,数据模型自然分成了两层:

Word Sense Card
├── Context Example A
│   ├── 原句
│   ├── 备注
│   └── 视频 / 图片 / 音频
├── Context Example B
└── Context Example C

每张卡可以有多个语境实例,其中一个是主语境,用于列表和复习时的默认展示。如果主语境被删除,系统会从剩余语境中选择新的主语境;调整主语境时,则通过数据库事务保证同一张卡不会同时出现两个主语境。

这个模型还改变了制卡流程。输入目标词后,系统会查找已有词义:如果“目标单词+当前语境释义”完全相同,就把新原句和媒体追加到已有卡片;如果只是单词相同、含义不同,则仍然允许创建新的词义卡片。

回头看,这是整个项目最值得保留的产品判断。框架可以替换,界面可以重做,识别方案也会升级,但只要“一个具体词义可以不断积累多个真实语境”这个模型成立,产品的灵魂就还在。

二、第一版最重要的工作,是知道不做什么

最早的需求文档列出过一组非常明确的限制:

  • 不内置词库;
  • 不接入传统词典;
  • 不做音标系统;
  • 不先做云同步;
  • 不把 AI 自动制卡作为必需能力;
  • 不单独建立复杂的来源表,用标签承担分类和来源标记;
  • 不为了“功能完整”阻塞最基本的手动制卡。

这些限制并不是因为相关功能没有价值,而是因为第一版必须先回答一个问题:即使没有 OCR、语音识别和 AI,这个产品是否仍然值得使用?

如果答案是否定的,那么再多自动化能力也只是给一个不成立的流程增加装饰。最后,我把最短学习闭环收敛成了:

遇到生词
→ 保存单词、具体含义和原句
→ 添加可选媒体
→ 到期复习
→ 再次遇见时补充新语境

手动创建一张卡片就能进入这个闭环。OCR、STT 和 AI 后来都可以提高效率,但都不能成为它的前置条件。

这也让我重新理解了 MVP。MVP 不是把十个功能各做 20%,而是选择一条可以独立成立的路径,把它从头到尾做通。对于语境单词本,这条路径不是“自动从视频中生成漂亮卡片”,而是“用户愿意长期保存并复习自己真正遇到的词义”。

新建语境卡片页面

即使不配置识别或 AI,也可以手动完成一张语境卡片。

三、让算法负责复杂,让用户界面保持简单

复习调度使用了 FSRS。它会维护卡片的稳定度、难度、复习次数、遗忘次数和下次到期时间。但我没有把这些参数全部暴露给用户,复习页只保留了两个选择:

  • Again:这次没有想起来;
  • Good:这次想起来了。

用户只需要表达最直接的学习结果,系统负责把评分转换成 FSRS 状态。提交评分时,服务端会在同一个事务里完成两件事:更新卡片的 FSRS 状态,并写入一条永久复习日志。日志保存评分、复习时间以及调整前后的到期时间,因此统计页面不需要从当前状态反推历史。

Again 还有一个产品层面的补充:卡片会被标记为当天可以再次出现。标准调度负责长期记忆,而“今天没想起来,稍后再见一次”满足的是短期学习体验。直到用户给出 Good,这个当天重试标记才会被清除。

项目还记录每日复习进度、正确与重试次数、下一个到期时间,并允许按学习语言缩小复习范围。复习页没有要求用户理解这些实现细节,它只需要在正确的时间展示正确的下一张卡。

这部分给我的经验是:算法的价值不在于让界面看起来更专业,而在于替用户承担本不应该由他处理的复杂度。一个学习者不需要知道稳定度是多少,也不应该手动计算三天后还是七天后复习;他只需要诚实地告诉系统:“记住了”或者“没记住”。

语境复习页面

复习时先面对单词和语境,再用 Again / Good 给出最小反馈。

四、本地优先不是一句宣传语

我把项目定位为本地优先、自托管的 Web 应用。它通过浏览器访问,但默认数据都留在安装目录:

data/context-vocabulary-notebook.sqlite
uploads/
.env

SQLite 保存卡片、语境、标签、FSRS 状态、复习日志和设置,媒体文件则保存在 uploads/。这个选择让部署足够轻,也让用户能够明确知道自己的数据在哪里。

不过,本地优先不等于“什么都不用设计”。一旦卡片和媒体分开保存,迁移就必须同时处理数据库关系和真实文件;一旦用户可以导入别人的卡片,就必须面对 ID、重复词义、标签和语境冲突;一旦提供 AI 配置,就必须区分哪些内容能进入备份,哪些秘密绝对不能导出。

因此,项目提供 ZIP 导入导出:完整备份可以迁移个人数据和媒体,纯卡片导出则适合分享。导入前会扫描内容,报告重复词义和缺失媒体,再由用户决定如何处理;真正写入时使用事务,避免导入一半后留下不一致状态。API Key 虽然保存在本地数据库中,但接口只返回“是否已配置”,不会把明文密钥回传给界面,也不会把密钥放进 ZIP 导出。

本地优先还需要诚实说明网络边界:

  • 手动制卡不会发送数据;
  • Tesseract OCR 和 whisper.cpp STT 在本机运行;
  • 配置 OpenAI-compatible 服务后,AI 建议会发送相应文本;
  • 使用云端转写时,音频会发送给配置的服务商;
  • 只有显式开启云端回退,本地片段识别失败后才可能发送帧图或音频;
  • 默认还会阻止回环和私网 AI 地址,只有用户明确确认可信环境后才允许放开。

隐私不是 README 上的一枚徽章,而是一组可以被代码和默认配置验证的边界。对本地优先项目来说,“默认不上传”远远不够,还必须让用户知道什么情况下会联网、发送什么,以及如何彻底关闭。

五、视频识别的难点,不只是调用三个工具

随着手动闭环稳定下来,我加入了本地视频片段批量识别。它处理的是多个本地 MP4 片段,而不是视频网站链接。大致流程是:

本地 MP4
→ ffmpeg 提取字幕区域帧和音频
→ Tesseract 识别画面文字
→ whisper.cpp 识别语音
→ 合并候选原句与目标词
→ 用户逐条确认、修改
→ 可选 AI 补充释义、翻译和词形
→ 保存为卡片或追加到已有词义

这里有一个很重要的原则:识别结果只是候选,不是事实。

字幕可能太小,画面可能模糊,语音可能有背景音乐,词形还原也可能改变原意。系统不会静默地批量写入,而是把每个片段的结果放进确认流程,让用户修改目标词、原句、释义、语言和标签后再保存。AI 可以给建议,但决定什么才是“当前语境中的含义”的人仍然是用户。

本地视频片段批量识别页面

批量识别的目标不是取消确认,而是减少机械输入。

真正耗时的部分也不止识别算法,而是让依赖在不同系统上可靠运行:

  • ffmpeg 是否存在,路径是否正确;
  • whisper.cpp 的可执行文件和模型是否都可读;
  • Tesseract 是否安装了目标语言数据;
  • Windows 安装程序是否卡住;
  • 已安装工具能否被复用;
  • 下载失败究竟是文件错误、代理超时还是网络暂时中断;
  • 更新 .env 后,正在运行的服务是否重新加载了识别路径。

Whisper 模型有数百 MB,因此核心应用安装和识别工具安装被刻意拆成两步。用户可以先用手动制卡和复习功能,确实需要批量识别时再下载 ffmpeg、Tesseract、whisper.cpp 和模型。Windows 安装器会把项目专用工具放进 tools/,模型放进 models/,对下载内容做校验,并在再次运行时复用通过校验的文件。

这段经历让我意识到:在开发机上成功调用一次命令,只证明了功能原型可行。要让陌生用户在 Windows、Linux、macOS 或 WSL 上真正装起来,需要处理的是另一类工程问题。功能代码可能只占前 80%,最后那 20% 的安装、路径、超时、重试、诊断和文档,往往更昂贵。

六、UI 不是一次设计出来的

语境单词本的界面经历过多次方向调整。项目曾做过一轮非首页页面的视觉改造,但结果破坏了原本更有辨识度的“蓝色魔法笔记本”气质,于是我选择回滚,再重新建立统一的视觉方向。后来形成的 Crystal Archive 风格使用书本、水晶、羽毛笔、沙漏和页面专属头图,但装饰必须服从内容。

有几次问题非常典型:

第一,批量导入功能已经存在,却在视觉调整后失去了足够明显的入口。从工程角度看它没有消失,从用户角度看却等同于不存在,最后又专门恢复了可见入口。

第二,复习页加入了漂浮气泡。最初它们只是背景装饰,但气泡互相穿过、覆盖文字时会显得廉价,于是后来增加了碰撞处理、边界约束和动画状态维护。一个看似纯 CSS 的效果,最终变成了需要测试的小型物理系统。

第三,媒体是语境的一部分,不能因为追求版式而被挤到首屏之外。复习页后来专门调整布局,让视频、图片和音频在常见屏幕高度下保持可见。

第四,页面请求媒体时,浏览器切换或中止播放可能取消连接。服务端如果把这个正常中止当作未处理异常,装饰和布局之外的整个体验都会被一次请求拖垮,因此又补上了对 aborted media stream 的处理。

这些问题让我逐渐明确了一条 UI 原则:视觉风格负责建立记忆点,信息层级负责完成任务。前者不能遮住入口,后者也不意味着所有页面都必须长得像后台管理系统。

学习统计页面

最终界面保留了鲜明主题,同时让复习数量、正确率和趋势保持可读。

七、我如何使用 GPT-5.6 Sol

这个项目的开发过程中,我使用了 GPT-5.6 Sol。OpenAI 将它定位为 GPT-5.6 系列中面向复杂专业工作的旗舰模型;相比这句官方描述,我自己的感受更具体:它真正强悍的地方,不是偶尔生成一段漂亮代码,而是能够进入一个真实、混乱、持续变化的工程上下文,并长时间保持对目标的理解。GPT-5.6 Sol 官方模型说明

我没有把它当成“输入需求、吐出整个项目”的黑盒,而是把它放进一个完整循环:

说明目标和边界
→ 阅读需求、代码与历史
→ 拆解问题并提出实现方案
→ 修改多个关联文件
→ 运行测试和构建
→ 根据失败继续定位
→ 检查界面或公开结果
→ 整理文档和交付说明

GPT-5.6 Sol 给我印象最深的能力主要有五点。

第一,跨文件理解能力。 一个看似简单的功能,经常同时涉及数据库结构、服务端领域逻辑、路由、共享类型、客户端状态和测试。它能够沿着数据流找出真正相关的文件,而不是只修改我最先指出的组件。

第二,长链路任务保持。 安装器问题尤其考验上下文:PowerShell、WSL、路径、下载校验、环境变量和启动进程互相影响。它能在多轮检查与修复之后仍然记得最初的用户目标,而不是只对最后一条错误信息做局部反应。

第三,错误定位和回归意识。 它不只是给出“可能是某某原因”,而是会读取实现、测试和日志,把假设缩小到可验证的范围;修改后继续运行相关检查,发现副作用再迭代。这种从诊断到验证的闭环,比单次代码生成有价值得多。

第四,补全测试的能力。 当需求涉及主语境替换、导入冲突、API Key 脱敏、识别就绪检查或媒体请求中止时,它能够把边界条件转化成测试场景。很多真正难发现的问题,不在成功路径,而在“只完成了一半”“用户取消了请求”“工具存在但语言包缺失”这些状态里。

第五,从代码延伸到产品交付。 它可以在同一个任务中理解 README、安装脚本、用户手册、截图和 GitHub 可见结果。对公开项目而言,这一点非常关键,因为用户看到的产品不只有网页,也包括第一条安装命令和第一次失败时的报错。

但模型能力强,不意味着人可以退出决策。哪些功能值得做、一个词义应该怎样建模、AI 是否应当拥有最终写入权、一次 UI 改版是否真的更好,这些仍然需要我决定。测试通过也不代表产品体验正确,尤其是安装流程和视觉效果,最终必须由真实环境和人的判断验收。

我更愿意把这种关系理解为:GPT-5.6 Sol 大幅扩大了我一次能够处理的问题范围,而不是替我承担产品责任。它可以非常强悍地探索、实现、审查和验证,但方向、边界和“什么才算完成”必须由我说清楚。

八、从“能运行”到“别人能使用”

项目进入公开阶段之后,工作的重心发生了变化。

最初我关注的是 API 是否正确、页面是否能完成操作;后来我开始关心另一组问题:用户应该在哪个目录执行安装命令?Windows PowerShell 和 WSL 的行为是否一致?更新时会不会套出第二层项目目录?下载超时和脚本失败能否区分?README 顶部能不能在一分钟内说明产品是什么、不能做什么?

现在,核心应用提供 Linux、macOS、WSL 和 Windows PowerShell 安装方式,本地识别工具则有独立安装器。用户手册覆盖更新、备份、识别依赖、环境变量和故障排查。README 不只提供中文和英文,而是有 11 个语言版本;应用界面则支持简体中文、英语、日语、韩语、法语、德语、西班牙语和俄语 8 种语言。

多语言文档并不是复制十一份文件就结束。项目增加了一致性检查,确保不同 README 都保留关键能力、限制、隐私边界和正确安装方式;链接也有独立检查。安装脚本还有冒烟测试,客户端、服务端、共享逻辑和端到端流程则分别覆盖。

截至 2026 年 7 月 14 日,主分支包含 228 个提交,tests/ 目录共有 48 个测试及辅助文件,公开开发记录推进到了 PR #74。数字本身并不代表质量,但它们能说明一件事:从五月底的早期需求到七月中旬的公开版本,真正占据大量提交的,不只是新增功能,还有回归修复、安装改进、文档校正和公开页面整理。

我越来越认同一句话:开源项目的用户界面,不只存在于浏览器里。

README 是用户看到的第一个页面,安装脚本是第一个交互流程,错误信息是第一个客服,备份文档则决定了用户敢不敢把真实数据交给这个应用。代码能运行只是起点,别人能够理解、安装、出错后自救,项目才开始具有公共产品的形状。

九、几次真实的错误

回顾开发过程,最有价值的部分不是证明每次决定都正确,而是看到错误怎样改变了之后的原则。

1. 一次 UI 改造被完整回滚

新的样式在局部看起来更现代,却削弱了产品原有的气质和页面一致性。与其在错误方向上继续打补丁,我选择回滚,再从已经认可的视觉语言重新推进。

这让我确认:沉没成本不是保留错误设计的理由。视觉改版必须同时通过截图比较、实际交互和多页面一致性检查。

2. Windows 安装方案修复后又撤回

原生依赖安装曾经尝试增加重试,但新的处理方式并不可靠,因此很快被撤回。之后的改进不再只追求“再试一次”,而是区分工具缺失、现有安装、下载失败、代理超时和原生构建失败。

这让我确认:安装器的错误分类比笼统重试更重要。用户只有知道失败发生在哪一层,才有可能自己解决。

3. 功能存在,但入口不见了

批量导入页面和路由都还在,视觉调整却让入口变得不明显。测试如果只验证 URL 可以打开,就无法发现这种问题。

这让我确认:可达性不仅是路由存在,还包括用户能否从合理位置发现入口。关键功能需要页面级测试,也需要真实截图和人工巡检。

4. 正常取消的媒体请求拖累服务端

浏览器停止播放或切换页面时,中止媒体流是正常行为。如果服务端把连接关闭继续当作异常写入响应,就可能出现崩溃或噪声日志。

这让我确认:系统不仅要处理成功和失败,还要处理“用户已经不需要结果了”。取消是现代客户端的正常状态,不是罕见异常。

5. 报错是真的,但没有帮助

“安装失败”可能意味着脚本语法问题,也可能只是代理连接超时。如果错误信息把两者混为一谈,用户会误以为项目本身不可用。

这让我确认:好的错误信息应该回答三件事——哪里失败、数据有没有被破坏、下一步可以做什么。

十、我最终学到的几件事

如果把这段开发经历压缩成几条可以带到下一个项目里的原则,我会留下这些:

  1. 先找到正确的核心对象。 对这个项目而言,不是“单词”,而是“具体词义及其多个语境”。
  2. 先做成不依赖自动化的闭环。 AI 和识别可以提高效率,但不能掩盖基础流程是否成立。
  3. 本地优先是一组工程责任。 隐私之外,还包括迁移、备份、冲突处理、密钥边界和清晰的联网说明。
  4. 算法应该隐藏复杂度,而不是展示复杂度。 FSRS 可以很复杂,用户的反馈仍然可以只有 Again 和 Good。
  5. 安装、文档和错误提示都是产品。 “在我的机器上能跑”与“陌生用户能用”之间隔着大量真实工作。
  6. 允许自己回滚。 不合适的 UI 和安装方案应该尽早撤回,而不是因为已经投入时间就继续维护。
  7. AI 能扩大个人开发者的能力半径,但不能代替责任。 GPT-5.6 Sol 可以承担大量探索、实现和验证工作,最终的产品判断仍然属于开发者。

Context Vocabulary Notebook 目前仍是早期预发布项目,版本为 0.2.0-alpha,接口和数据处理方式仍可能变化。但它已经完成了我最初想要的那条闭环:把真实遇见的词连同语境保存下来,在合适的时间重新遇见它,并让同一个词义随着新的经历不断变得丰富。

最开始,我只是觉得查过的词总会忘。

现在回头看,我真正想解决的并不是“怎样记住更多释义”,而是“怎样不丢失理解发生时的上下文”。这也是我仍然愿意继续做这个项目的原因。

如果你也在视频、播客或阅读中积累外语词汇,欢迎查看项目、试用当前版本,或者带着自己的学习习惯提出建议:

也许一个单词真正被记住的时刻,不是它进入词表的时候,而是某段声音、某个画面和某句话再次把它叫醒的时候。

END

感谢你读到这里。

LEAVE A NOTE

留下一句话

使用 GitHub 登录后即可回应