语音合成与视觉模型api接入实现

0 阅读6分钟

语音合成与视觉模型api接入实现

读完这篇,你应能按步骤复现本仓库里的两条能力:火山豆包语音 TTS(文本 → 音频)与 Moonshot 视觉理解(图 + 文 → 描述)。代码已在仓库中落地,本文侧重为什么要这样拆、先写哪一层、再拼哪一层,方便你搬到自己的项目里。

多模态业务里,「语音」和「看图说话」常来自不同厂商、不同鉴权方式;浏览器又不能随便塞 Secret。做法是:单页只负责表单与展示本机 server.js 当 BFF.env.local、带齐 Header、转发到官方域名。下面按推荐实现顺序写。


你将得到什么

能力页面代理路径上游
语音合成index-volc-tts.htmlPOST /tts/api/v1/ttsopenspeech.bytedance.com/api/v1/tts
视觉理解index-moonshot-vision.htmlPOST /moonshot/v1/chat/completionsapi.moonshot.cn/v1/chat/completions

效果图:

volc-tts.gif

moon.gif


第 0 步:先画清楚「三层」

无论做哪一条链路,都可以抽象成同一副骨架:

  1. 浏览器:只认识 http://127.0.0.1:3000(或你的代理地址),用 fetch 发 JSON,不出现厂商密钥
  2. server.js:读 .env.local,补鉴权(Header 或 body 里的 app),fetch 到真实上游,把状态码和 body 原样或略加工返回给浏览器。
  3. 厂商 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 里的 proxyVolcengineTtsproxyMoonshotRequest 逆序读回去。


第 1 步:准备账号与密钥(两条线各自一次)

火山(语音)

火山引擎语音活动/实名控制台创建应用,拿到 AppIDAccess Token(测试 Token 常有有效期,过期要重新复制)。HTTP 一次性合成文档见 豆包语音 · HTTP 非流式。请求体里的 app.cluster 在常见在线 TTS 场景下为 volcano_tts(若你开通的是其它产品线,以控制台绑定的文档为准)。

Moonshot(视觉)

Moonshot 开放平台 注册,在 API Keys 创建 sk-...。视觉走 OpenAI 兼容的 /v1/chat/completionsmessagescontent 可为数组:type: image_urlurl 可用 Data URL)+ type: textmodel 须选支持视觉的型号(如 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 在做什么(实现要点)

  1. 只处理 POST /tts/api/v1/tts(与前端约定死,避免误打到别的服务)。
  2. JSON.parse 请求体后,强制写入 parsed.app = { appid, token, cluster }(来自环境变量),这样即使浏览器带了假 app 也会被覆盖。
  3. 若缺 request.reqid,服务端用 crypto.randomUUID() 补上,满足「每次合成唯一」。
  4. 向上游 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 在做什么

  1. 匹配 /moonshot 前缀,剥掉后剩余路径必须以 /v1/ 开头且无 ..,防止开放代理被滥用。
  2. MOONSHOT_API_ORIGIN + restPath,默认 https://api.moonshot.cn
  3. 请求头 Authorization: Bearer ${MOONSHOT_API_KEY},body 透传(密钥不在 body 里)。
MOONSHOT_API_KEY=sk-你的密钥

也可用 VITE_API_KEY / API_KEY。日志里 [Moonshot] 已配置… 表示就绪。


第 3 步:写 index-volc-tts.html(从文本到能播的音频)

目标:用户输入文案 → 点按钮 → 听到合成声。

  1. 技术栈:一个 HTML 里 <script type="module">,从 unpkg 引入 Vue 3 ESM;需要 npx serve . 这类静态服务,避免 file:// 下模块加载失败。
  2. 代理根 proxyBase:所有请求都是 base + '/tts/api/v1/tts',与第 2 步里服务端路由一致;成功请求前写入 localStorage,下次打开少输一次地址。
  3. 请求体:只组 user / audio / request不要在浏览器写 app(交给服务端合并)。
  4. reqid:在浏览器生成 UUID,减少与服务端补全的竞态,也符合文档习惯。
  5. 解析返回:JSON 里 data 是纯 base64;用 atobUint8ArrayBlob,MIME 用 mimeForEncoding(encoding)mp3audio/mpegogg_opusaudio/ogg),否则容易「有数据但播不出」。
  6. 播放与内存URL.createObjectURL 赋给 <audio>;每次合成前 revokeObjectURL 旧地址,组件卸载时再清一次,避免泄漏。

跑通检查清单:node server.jsnpx serve . → 打开 index-volc-tts.html → 代理填 http://127.0.0.1:3000 → 点 Generate & Play → 听到声音。更多字段以火山文档为准;仓库交叉索引:README.md


第 4 步:写 index-moonshot-vision.html(从本地图片到模型回复)

目标:选一张图 + 一句问法 → 看到模型对图的描述。

  1. 读文件<input type="file"> + FileReader.readAsDataURL,得到 data:image/...;base64,...,同时用于 <img :src> 预览请求体(一份数据两处用,避免双份状态)。
  2. isValid:用 computed 判断是否已选图,禁用「提交」,减少空请求。
  3. 请求 URLbase + '/moonshot/v1/chat/completions',对应第 2.3 步的代理前缀。
  4. 请求体stream: falsemessages[0].content 为数组,先图后文(与多模态习惯一致);model 可做成输入框便于换型号线。
  5. 解析:取 choices[0].message.content;HTTP 错误或结构不对时,把截断的 JSON 塞进 Error 文案,便于对照上游 error 字段排错。
  6. 大图: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
URLproxyBase + 固定相对路径拼官方 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,单页复杂度会明显高于本文的非流式示例。

相关文件(按阅读顺序)

  1. server.js — 搜 proxyVolcengineTtsproxyMoonshotRequest
  2. index-volc-tts.htmlindex-moonshot-vision.html
  3. README.md — 环境变量与接口总表

若你还想对照「文生图 + 异步轮询」的另一条 BFF 链路,可继续读 image-model.md 里的 index-keling.html 思路(与本篇的「一次 POST 即返回」形态不同)。