从零实现「拍照记单词」小应用(可复刻版)

0 阅读6分钟

从零实现「拍照记单词」小应用(可复刻版)

读完本文,你可以用浏览器单页 + 本地 Node 代理跑通:看图 → 输出单词卡片 JSON → 语音朗读

camera_word.gif

其实就是之前几个大模型的组成使用,请务必参考前面文章的文本模型、视觉模型、语音模型。

想直接体验,代码仓库,建立.env.local配置各种参数,然后运行node server.js,然后在浏览器打开index-capture-word.html就可以了。对,三个文件就可以了。

你会学到什么

  • 为什么不要把大模型与 TTS 的密钥写进前端,以及如何用同源代理解决 CORS 与保密。
  • 如何把本地图片变成 data URL,并塞给支持视觉的 OpenAI 兼容 Chat Completions
  • 如何用 Prompt 约定 JSON 形状,并在前端做一层代码围栏剥离容错解析。
  • 如何把卡片字段拼成朗读稿,调用火山豆包 HTTP 非流式 TTS,并把 base64 音频交给 <audio> 播放。

应用长什么样、用户怎样走完流程

  1. 用户选一张图(手机拍照后从相册选文件即可,本质仍是 <input type="file">)。
  2. 前端用 FileReader.readAsDataURL 得到 data:image/jpeg;base64,... 这类字符串,同时用于预览和后续 API。
  3. 点击「生成单词卡片」:把该 data URL 放进多模态消息里的 image_url.url,连同固定 VISION_PROMPT 发给 Moonshot(经本地代理),模型返回一段 JSON 文本
  4. 页面展示:单词拼写、读音标注、中文释义、英文例句、一段对话。
  5. 点击「朗读卡片」:把上述字段拼成一段适合朗读的文本,请求 TTS 代理,拿到音频后解码为 Blob URL,交给 <audio> 播放。

技术选型与本仓库一致:Moonshot(示例模型 moonshot-v1-8k-vision-preview)做多模态理解与结构化输出;火山豆包 TTSPOST /tts/api/v1/tts(由 server.js 转发到 openspeech)合成朗读音频。

整体架构(建议先理解再写代码)

浏览器只配置「代理根地址」,例如 http://127.0.0.1:3000,不直接接触 Moonshot / 火山公网域名,也不持有 sk- 密钥:

flowchart LR
  subgraph browser [浏览器]
    A[index-capture-word.html]
  end
  subgraph proxy [Node server.js]
    B["/moonshot/v1/*"]
    C["/tts/api/v1/tts"]
  end
  subgraph upstream [公网 API]
    M[Moonshot]
    V[火山 openspeech]
  end
  A --> B
  A --> C
  B --> M
  C --> V

先决条件

  • Node.js 22+(本仓库 server.js 使用内置 fetch,无需额外 npm install)。
  • 可用的 Moonshot API Key(支持视觉的模型)与 火山豆包语音 HTTP TTS 凭据(APP_IDACCESS_TOKENCLUSTER_ID 等,与 server.js 头注释一致)。
  • 仓库根目录的 server.js,已包含 /moonshot/tts 转发逻辑。

第一步:准备 .env.local

在项目根目录新建或编辑 .env.localserver.js 启动时会加载 .env.local.env)。至少包含:

用途变量(可与 server.js 中别名互换)
MoonshotMOONSHOT_API_KEY,或 VITE_API_KEY / API_KEY
火山 TTSVOLCENGINE_TTS_APP_IDVOLCENGINE_TTS_ACCESS_TOKENVOLCENGINE_TTS_CLUSTER_ID,或 VITE_APP_IDVITE_ACCESS_TOKENVITE_CLUSTER_ID

密钥不要写进前端仓库的公开配置;.env.local 应留在本机并加入 .gitignore

第二步:启动代理

在含 server.js 的目录执行:

node server.js

默认监听 http://127.0.0.1:3000(可通过环境变量 PORT 修改)。终端无报错即表示代理就绪。

第三步:静态打开单页并填写代理

import type="module" 的页面不要用 file:// 打开(易触发模块或跨域限制)。在仓库根目录用任意静态服务即可,例如:

npx serve .

浏览器打开 index-capture-word.html,在页面「代理」输入框填入 http://127.0.0.1:3000(与上一步一致;页面内会对末尾斜杠做规范化)。

核心实现拆解(与单页脚本逐段对应)

以下步骤你可完全用 Vue / React / 原生 JS 重写 UI,只要保留数据流与请求体结构,行为就与参考实现一致。

1. 选图 → data URL

  • 监听 <input type="file">change,取 e.target.files[0]
  • FileReader.readAsDataURL(file),在 onload 中把 reader.result 存为 imgDataUrl
  • 同一字符串绑定到 <img src> 做预览,并作为下一步视觉请求中 image_url.url 的值(Moonshot 接受 data URL 形式的图片)。

2. 生成卡片:POST {proxyBase}/moonshot/v1/chat/completions

  • 请求头:Content-Type: application/jsonAccept: application/json
  • Body:model 使用支持视觉的 id;stream: false,便于一次取全 content
  • messages:一条 user 消息,content数组:先 { type: 'image_url', image_url: { url: imgDataUrl } },再 { type: 'text', text: VISION_PROMPT }
  • 从响应读取 choices[0].message.content,再进入解析步骤。

3. Prompt 与 JSON 契约

VISION_PROMPT 中明确要求:只输出合法 JSON,并列出字段:wordphoneticmeaning_zhexampledialogue(均为字符串)。字段稳定后,你的卡片 UI 与 buildTtsText 才能长期不改。

4. 容错解析:parseVisionJson

模型有时仍会在 JSON 外包一层 markdown 代码围栏。建议逻辑为:若存在 ```json … ```,先取内部文本;再在字符串中定位第一个 { 与最后一个 }JSON.parse 得到对象。缺字段或 word 为空时,向用户展示可读错误及原文片段,便于调 Prompt。

5. 朗读:POST {proxyBase}/tts/api/v1/tts

  • buildTtsText()(或等价逻辑)把单词、音标、中文义、例句、对话拼成单段文本;注意厂商有长度上限,参考实现中对总长做了截断(如 1200 字符),避免合成失败。
  • Body 结构需与当前 server.js 转发及火山 HTTP 文档一致(单页中含 useraudiorequest 等字段,如 voice_typeencodingtextreqid)。
  • 响应中取出 base64 音频(参考实现使用字段 data),经 atobUint8ArrayBlob,再 URL.createObjectURL 赋给 <audio>
  • 再次合成前对旧 URL URL.revokeObjectURL;组件卸载时同样释放,避免泄漏。

6. 数据流与界面门闩(把逻辑钉死)

  • 串行依赖:先有 imgDataUrl 才能调用视觉接口;只有解析出 word(及你需要的其它字段)后,buildTtsText 与 TTS 才有稳定输入。不要跳过中间态发请求。
  • 按钮是否可点:与参考页一致时,可用 canAnalyze = !!imgDataUrl 控制「生成单词卡片」,hasCard = !!word.trim() 控制「朗读」。
  • 重新选图:新文件读入后应清空上一轮单词、例句、对话、错误提示,避免旧卡片与新照片叠在一起误导用户。

常见问题

  • CORS / 网络失败:确认页面来自 http(s) 静态源,且请求指向你填写的代理根地址,而不是 file://
  • 401 / 403:确认 .env.local 与运行 node server.js工作目录一致;改 env 后重启 server。
  • 视觉返回无法解析:收紧 Prompt、更换模型,或增强 parseVisionJson 与错误提示。
  • TTS 无数据:检查响应 JSON 是否返回错误信息、encoding 与解码后的 MIME 是否匹配(如 mp3audio/mpeg)。

复刻自检清单

  • .env.local 已同时配置 Moonshot 与火山 TTS。
  • node server.js 启动成功,端口与页面「代理」一致。
  • 通过静态服务打开 index-capture-word.html,非 file://
  • 选图后预览正常,「生成单词卡片」能填满五个字段。
  • 「朗读卡片」能播放合成语音(或截断后的片段)。

参考