语音合成与视觉模型api接入实现
读完这篇,你应能按步骤复现本仓库里的两条能力:火山豆包语音 TTS(文本 → 音频)与 Moonshot 视觉理解(图 + 文 → 描述)。代码已在仓库中落地,本文侧重为什么要这样拆、先写哪一层、再拼哪一层,方便你搬到自己的项目里。
多模态业务里,「语音」和「看图说话」常来自不同厂商、不同鉴权方式;浏览器又不能随便塞 Secret。做法是:单页只负责表单与展示,本机 server.js 当 BFF 读 .env.local、带齐 Header、转发到官方域名。下面按推荐实现顺序写。
你将得到什么
| 能力 | 页面 | 代理路径 | 上游 |
|---|---|---|---|
| 语音合成 | index-volc-tts.html | POST /tts/api/v1/tts | openspeech.bytedance.com/api/v1/tts |
| 视觉理解 | index-moonshot-vision.html | POST /moonshot/v1/chat/completions | api.moonshot.cn/v1/chat/completions |
效果图:
第 0 步:先画清楚「三层」
无论做哪一条链路,都可以抽象成同一副骨架:
- 浏览器:只认识
http://127.0.0.1:3000(或你的代理地址),用fetch发 JSON,不出现厂商密钥。 server.js:读.env.local,补鉴权(Header 或 body 里的app),fetch到真实上游,把状态码和 body 原样或略加工返回给浏览器。- 厂商 API:校验通过后返回 JSON(TTS 里是 base64 音频;Chat 里是
choices[0].message.content)。
flowchart LR
subgraph browser["浏览器单页"]
A[表单 / 选图]
B[fetch 本地路径]
end
subgraph bff["server.js"]
C[读环境变量]
D[拼上游 URL + Header]
end
subgraph upstream["厂商 HTTPS"]
E[(openspeech / Moonshot)]
end
A --> B --> C --> D --> E
E --> D --> B
实现顺序建议:先能用 curl 或最小脚本从本机打到上游(验证密钥与路径),再写 BFF 路由,最后写 Vue 单页把体验补齐。本仓库把后两步都写好了,你可以对照 server.js 里的 proxyVolcengineTts 与 proxyMoonshotRequest 逆序读回去。
第 1 步:准备账号与密钥(两条线各自一次)
火山(语音)
去 火山引擎语音活动/实名 与 控制台创建应用,拿到 AppID、Access Token(测试 Token 常有有效期,过期要重新复制)。HTTP 一次性合成文档见 豆包语音 · HTTP 非流式。请求体里的 app.cluster 在常见在线 TTS 场景下为 volcano_tts(若你开通的是其它产品线,以控制台绑定的文档为准)。
Moonshot(视觉)
去 Moonshot 开放平台 注册,在 API Keys 创建 sk-...。视觉走 OpenAI 兼容的 /v1/chat/completions,messages 里 content 可为数组:type: image_url(url 可用 Data URL)+ type: text。model 须选支持视觉的型号(如 moonshot-v1-8k-vision-preview),以 官方 Chat 文档 为准。
第 2 步:在 server.js 接通路(先后端,再前端)
2.1 为什么要单独路由,而不是让浏览器直连?
- 密钥:火山 Token、Moonshot API Key 不能进前端仓库与打包产物。
- Header 细节:火山 TTS 要求
Authorization: Bearer;token(分号);Moonshot 是常见的Bearer <空格>token。写进 BFF 可避免前端写错格式。 - 路径前缀:本仓库用
/tts/...、/moonshot/v1/...作为「入口命名空间」,与仓库里可灵/kling并列,便于在createServer里分支维护。
2.2 火山:proxyVolcengineTts 在做什么(实现要点)
- 只处理
POST /tts/api/v1/tts(与前端约定死,避免误打到别的服务)。 JSON.parse请求体后,强制写入parsed.app = { appid, token, cluster }(来自环境变量),这样即使浏览器带了假app也会被覆盖。- 若缺
request.reqid,服务端用crypto.randomUUID()补上,满足「每次合成唯一」。 - 向上游
POST https://openspeech.bytedance.com/api/v1/tts(可用VOLCENGINE_TTS_ORIGIN覆盖域名),带上Bearer;头,把上游响应 原样写回(状态码 +Content-Type+ body)。
环境变量示例(.env.local,勿提交):
VOLCENGINE_TTS_APP_ID=你的AppId
VOLCENGINE_TTS_ACCESS_TOKEN=你的AccessToken
VOLCENGINE_TTS_CLUSTER_ID=volcano_tts
也支持 VITE_APP_ID / VITE_ACCESS_TOKEN / VITE_CLUSTER_ID。启动后看控制台 [Volc TTS] 已配置…。
2.3 Moonshot:proxyMoonshotRequest 在做什么
- 匹配
/moonshot前缀,剥掉后剩余路径必须以/v1/开头且无..,防止开放代理被滥用。 - 拼
MOONSHOT_API_ORIGIN + restPath,默认https://api.moonshot.cn。 - 请求头
Authorization: Bearer ${MOONSHOT_API_KEY},body 透传(密钥不在 body 里)。
MOONSHOT_API_KEY=sk-你的密钥
也可用 VITE_API_KEY / API_KEY。日志里 [Moonshot] 已配置… 表示就绪。
第 3 步:写 index-volc-tts.html(从文本到能播的音频)
目标:用户输入文案 → 点按钮 → 听到合成声。
- 技术栈:一个 HTML 里
<script type="module">,从 unpkg 引入 Vue 3 ESM;需要npx serve .这类静态服务,避免file://下模块加载失败。 - 代理根
proxyBase:所有请求都是base + '/tts/api/v1/tts',与第 2 步里服务端路由一致;成功请求前写入localStorage,下次打开少输一次地址。 - 请求体:只组
user/audio/request,不要在浏览器写app(交给服务端合并)。 reqid:在浏览器生成 UUID,减少与服务端补全的竞态,也符合文档习惯。- 解析返回:JSON 里
data是纯 base64;用atob→Uint8Array→Blob,MIME 用mimeForEncoding(encoding)(mp3↔audio/mpeg,ogg_opus↔audio/ogg),否则容易「有数据但播不出」。 - 播放与内存:
URL.createObjectURL赋给<audio>;每次合成前revokeObjectURL旧地址,组件卸载时再清一次,避免泄漏。
跑通检查清单:node server.js → npx serve . → 打开 index-volc-tts.html → 代理填 http://127.0.0.1:3000 → 点 Generate & Play → 听到声音。更多字段以火山文档为准;仓库交叉索引:README.md。
第 4 步:写 index-moonshot-vision.html(从本地图片到模型回复)
目标:选一张图 + 一句问法 → 看到模型对图的描述。
- 读文件:
<input type="file">+FileReader.readAsDataURL,得到data:image/...;base64,...,同时用于<img :src>预览 和 请求体(一份数据两处用,避免双份状态)。 isValid:用computed判断是否已选图,禁用「提交」,减少空请求。- 请求 URL:
base + '/moonshot/v1/chat/completions',对应第 2.3 步的代理前缀。 - 请求体:
stream: false;messages[0].content为数组,先图后文(与多模态习惯一致);model可做成输入框便于换型号线。 - 解析:取
choices[0].message.content;HTTP 错误或结构不对时,把截断的 JSON 塞进Error文案,便于对照上游error字段排错。 - 大图:Data URL 会线性撑大 POST body,易触发超时或网关限制——产品上要引导压图或走对象存储 URL,本示例先文档化约束。
跑通检查清单:.env.local 配好 Moonshot Key → node server.js → 打开 index-moonshot-vision.html → 选图 → 提交 → 看到文字回复。
第 5 步:单页内部的共通套路(和 server.js 怎么分工)
| 环节 | 单页负责 | server.js 负责 |
|---|---|---|
| 密钥 | 不出现 | 读 .env.local,向上游带正确 Header / 火山 app |
| URL | proxyBase + 固定相对路径 | 拼官方 origin + /api/v1/tts 或 /v1/... |
| 业务 JSON | 文案、音色、encoding、图片 Data URL、model 等 | 火山:覆盖 app;Moonshot:透传 body |
| 错误展示 | try/catch、HTTP 状态、关键字段校验 | 上游非 2xx 时透传 status + body |
一句话:单页只做 「表单 → JSON → fetch → 解析 → UI」;BFF 只做 「鉴权 + 合法路径 + 转发」。
index-volc-tts.html 再抠两点
status/error:把「进行中 / 成功 / 失败」从控制台搬到页面上,demo 才像产品。await audio.play():若浏览器拦截自动播放,用户仍可通过controls手动点播放。
index-moonshot-vision.html 再抠两点
- OpenAI 兼容:
content为数组是多模态与纯文本混排的关键形状,顺序影响模型「先看到什么」。 - 与火山的 Header 差异:火山是
Bearer;,Moonshot 是Bearer——写博客或接其它厂商时务必按文档逐字核对,不要想当然混用。
排错与扩展
- 火山 401 / 鉴权失败:核对 Token 是否过期、
Authorization是否为Bearer;、cluster 是否为volcano_tts(或文档要求值)。 - Moonshot 4xx:Key 是否有效、模型是否支持视觉、图片是否过大。
- 改
.env.local后:务必重启node server.js。 - 扩展:TTS 若改流式要换协议;视觉若改流式要解析 SSE / chunk,单页复杂度会明显高于本文的非流式示例。
相关文件(按阅读顺序)
server.js— 搜proxyVolcengineTts、proxyMoonshotRequestindex-volc-tts.html、index-moonshot-vision.htmlREADME.md— 环境变量与接口总表
若你还想对照「文生图 + 异步轮询」的另一条 BFF 链路,可继续读 image-model.md 里的 index-keling.html 思路(与本篇的「一次 POST 即返回」形态不同)。