讯飞同传Demo实战:Node.js零依赖实现实时语音转写与翻译

发布时间:2026/10/10 10:46:46
讯飞同传Demo实战:Node.js零依赖实现实时语音转写与翻译 简介这份资源是面向Node.js开发者的科大讯飞同声传译接口调用演示项目适合希望快速集成实时语音转写与多语言翻译能力的后端开发人员及语音技术初学者。项目免去繁琐依赖安装只需配置APPID和密钥即可运行降低了接入门槛可用于同声传译、会议记录、语音交互等场景的原型验证。压缩包共9个文件约180KB包含js主程序、json配置、md与txt说明文档、pcm音频示例及docx附赠资料覆盖代码、配置与测试素材便于直接运行和二次修改。目前已有72人学习下载。通过该演示读者可掌握讯飞API的调用流程、音频数据实时转写与翻译结果输出的实现思路并借助示例音频快速验证效果为后续集成到自有应用提供可复用的参考代码与排错依据。1. 一个不用 npm install 就能跑的讯飞同传 Demo到底省掉了哪些事拿到一个 Node.js 项目第一反应往往是npm install然后等依赖、修 node-gyp、处理版本冲突。这个基于 Node.js 的科大讯飞同声传译接口调用演示项目反其道而行——它把依赖压到最低配置好 APPID 和密钥就能直接运行核心目标是演示实时语音转写与多语言翻译的完整调用链路。适合两类人一是想快速验证讯飞同传接口能不能满足自己业务场景的开发者二是不想在环境配置上耗时间、只想看接口怎么调、参数怎么传的工程师。它解决的不是“生产级部署”问题而是“我能不能在半小时内看到语音转文字加翻译跑通”的问题。下面按实际拆包顺序从配置、调用、参数到踩坑逐层展开。2. 拆开这个 Demo文件结构与讯飞同传接口的调用链路2.1 目录里有什么每个文件负责哪一段这个压缩包解压后不会出现node_modules目录这是它最直观的特征。常见做法是只保留一个入口文件比如index.js或demo.js、一个配置文件config.js或直接写在入口里的常量区以及可选的音频样本文件。没有package.json里那一长串依赖意味着它大概率用的是 Node.js 内置的crypto、https、fs模块外加讯飞 WebSocket 接口所需的原生ws或直接走 HTTPS 轮询。我一般会先看三个位置入口文件顶部的require语句、配置区里APPID/API_KEY/API_SECRET三个占位符、以及音频文件的读取方式。如果require里只有crypto、https、fs、path那基本可以确认它是纯原生实现不需要任何第三方包。如果出现了ws那说明它走的是 WebSocket 长连接这时候才需要npm install ws但很多演示项目会把 WebSocket 部分用 HTTPS 分片上传替代进一步降低依赖。文件结构通常长这样xfyun-demo/ ├── index.js # 入口包含鉴权、音频读取、接口调用 ├── config.js # APPID、API_KEY、API_SECRET、音频路径 ├── test.pcm # 16k 采样率、16bit 位深的裸音频 └── README.md # 配置说明和运行命令test.pcm这个文件值得单独说。讯飞实时转写接口对音频格式有硬性要求采样率 16000Hz、位深 16bit、单声道、PCM 裸数据。如果你拿一个 MP3 或 WAV 直接丢进去接口会返回错误码而不是转写结果。Demo 里自带test.pcm就是为了让你第一次运行就能看到输出不用自己先转音频。2.2 鉴权怎么算HMAC-SHA256 与 Base64 的拼接顺序讯飞接口的鉴权不是简单传一个 token而是要在每次请求时动态生成一个带签名的 URL。核心逻辑是把API_KEY、API_SECRET、时间戳、请求路径拼成一个字符串用API_SECRET做 HMAC-SHA256 签名再 Base64 编码最后把authorization参数拼到 URL 上。常见实现如下const crypto require(crypto); function buildAuthUrl({ apiKey, apiSecret, host, path, date }) { // date 格式必须是 RFC1123例如 Mon, 01 Jan 2024 00:00:00 GMT const signatureOrigin host: ${host}\ndate: ${date}\nGET ${path} HTTP/1.1; const signature crypto .createHmac(sha256, apiSecret) .update(signatureOrigin) .digest(base64); const authorizationOrigin api_key${apiKey}, algorithmhmac-sha256, headershost date request-line, signature${signature}; const authorization Buffer.from(authorizationOrigin).toString(base64); return https://${host}${path}?authorization${authorization}date${encodeURIComponent(date)}host${host}; }这段代码里最容易翻车的是signatureOrigin的换行和空格。host:后面有一个空格date:后面有一个空格GET前面是\nHTTP/1.1前面也有一个空格。少一个空格或者把\n写成\r\n签名就会对不上接口返回 401。date必须是 GMT 格式且和服务器时间偏差不能超过 5 分钟否则同样鉴权失败。authorizationOrigin里的headers字段必须严格写成host date request-line顺序不能换。这是讯飞接口的硬性约定不是可选项。最后把整个字符串 Base64 编码后作为authorization查询参数拼到 URL 上同时把date和host也作为查询参数带上。2.3 音频怎么送分片上传与帧长控制鉴权通过后音频数据不是一次性 POST 上去而是按帧分片发送。讯飞实时转写接口要求每帧 40ms对应 16k 采样率、16bit 位深的 PCM 数据就是 1280 字节。Demo 里通常会写一个循环每次从test.pcm读取 1280 字节构造一个 JSON 帧通过 WebSocket 或 HTTPS 分片上传发出去。const fs require(fs); const FRAME_SIZE 1280; // 40ms * 16000Hz * 2字节 / 1000 function sendAudioFrames(filePath, sendFn) { const buffer fs.readFileSync(filePath); let offset 0; let seq 1; while (offset buffer.length) { const chunk buffer.slice(offset, offset FRAME_SIZE); const frame { data: { status: offset FRAME_SIZE buffer.length ? 2 : 1, // 1中间帧, 2最后一帧 format: audio/L16;rate16000, encoding: raw, audio: chunk.toString(base64) } }; sendFn(JSON.stringify(frame)); offset FRAME_SIZE; seq; } }status字段是关键第一帧和中间帧传1最后一帧传2。如果所有帧都传1接口会一直等后续数据不会返回最终结果。如果第一帧就传2接口会认为音频只有一帧转写结果可能不完整。format必须写成audio/L16;rate16000encoding固定raw。audio字段是 Base64 编码后的 PCM 数据不是原始二进制。发送频率也要注意。如果一次性把几百帧全推过去接口可能来不及处理导致丢帧如果每帧之间间隔太久又会触发超时。常见做法是每发送一帧后等待 40ms 左右或者用setInterval按 40ms 节奏推。Demo 里如果用了setTimeout递归基本就是这个思路。3. 配置与运行从填 APPID 到看到第一行转写结果3.1 三个密钥从哪里拿填在哪个位置打开讯飞开放平台进入控制台创建一个“语音听写”或“实时语音转写”应用就能拿到APPID、API_KEY、API_SECRET。这三个值分别对应应用标识、接口调用密钥和签名密钥。注意API_KEY和API_SECRET不是同一个东西前者用于标识调用方后者用于签名计算不要填反。Demo 里通常会在config.js或入口文件顶部留出占位符// config.js module.exports { APPID: your_appid_here, API_KEY: your_api_key_here, API_SECRET: your_api_secret_here, AUDIO_PATH: ./test.pcm, HOST: iat-api.xfyun.cn, // 实时转写用这个 PATH: /v2/api/asr, TRANSLATE_HOST: itrans.xfyun.cn, // 翻译接口用这个 TRANSLATE_PATH: /v2/api/its };把三个占位符替换成控制台里的真实值即可。HOST和PATH不要改除非讯飞官方文档明确说接口版本升级。AUDIO_PATH指向test.pcm如果想测自己的音频把路径改掉但格式必须符合 16k/16bit/单声道/PCM。提示API_SECRET只在签名计算时用不会出现在请求 URL 的明文参数里。如果你在浏览器地址栏或日志里看到API_SECRET原文说明代码写错了。3.2 运行命令与首次输出解读配置完成后在项目根目录执行node index.js如果一切正常终端会先打印鉴权 URL 和 WebSocket 连接状态然后逐帧发送音频最后输出类似下面的 JSON{ code: 0, message: success, sid: iat0007a1b2dx1a2b3c4d5e6f7g8h, data: { status: 2, result: { ws: [ { cw: [ { w: 你好 } ] }, { cw: [ { w: 世界 } ] } ] } } }code: 0表示成功sid是本次请求的唯一标识排查问题时需要提供。data.result.ws是分词结果每个cw里w就是识别出的文字。如果开了翻译功能还会多一个translate_result字段里面是目标语言的译文。如果输出code非 0先看message。常见的有invalid authorization签名错、audio format error音频格式不对、appid not foundAPPID 填错或应用未开通对应服务。sid一定要留着找讯飞技术支持时提供这个值能省很多沟通成本。3.3 翻译功能怎么开多语言参数与返回结构同声传译的“传译”部分依赖翻译接口。Demo 里通常会在转写结果返回后把识别出的文本再调一次翻译接口或者直接在转写请求里带上翻译参数。常见做法是转写和翻译分两步先拿到result.ws里的文本拼接成完整句子再调用itrans.xfyun.cn的翻译接口。翻译请求的核心参数是from和to。from填源语言to填目标语言比如cn到en。返回结构里data.result.trans_result是一个数组每个元素包含src和dst分别对应原文和译文。async function translateText(text, from cn, to en) { const url buildAuthUrl({ apiKey: config.API_KEY, apiSecret: config.API_SECRET, host: config.TRANSLATE_HOST, path: config.TRANSLATE_PATH, date: new Date().toUTCString() }); const body JSON.stringify({ common: { app_id: config.APPID }, business: { from, to }, data: { text: Buffer.from(text, utf8).toString(base64) } }); // 实际发送用 https.request 或 fetch这里省略发送细节 // 返回后解析 data.result.trans_result }text需要 Base64 编码后再传不是直接传明文。from和to的语言代码要查讯飞文档常见的有cn中文、en英文、jp日文、kr韩文。如果from填错翻译结果可能为空或者返回原文。4. 避坑排查签名 401、音频格式报错与依赖缺失4.1 现象接口返回 401 或invalid authorization原因几乎总是签名计算错误。最常见的是date格式不对——用了new Date().toISOString()而不是toUTCString()前者带T和Z后者是 RFC1123 格式。另一个高频错误是signatureOrigin里的换行和空格数量不对尤其是host:和date:后面的空格以及GET前面的\n。解决方法是把signatureOrigin打印出来逐字符对照讯飞文档里的示例。host值不要带https://只写域名比如iat-api.xfyun.cn。date必须和请求 URL 里的date参数完全一致不能一个用 GMT 一个用本地时间。4.2 现象audio format error或转写结果为空原因通常是音频格式不符合要求。讯飞实时转写只认 16k 采样率、16bit 位深、单声道、PCM 裸数据。如果你用手机录了一段 m4a或者用 Audacity 导出了 WAV直接改后缀名成.pcm是没用的文件头还在接口解析不了。解决方法是先用 ffmpeg 转成标准 PCMffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le -acodec pcm_s16le output.pcm-ar 16000指定采样率-ac 1指定单声道-f s16le指定 16bit 小端 PCM-acodec pcm_s16le确保编码格式正确。转完后用ffprobe output.pcm确认参数或者直接看文件大小每秒音频应该是 32000 字节16000 采样点 × 2 字节。4.3 现象npm : 无法加载文件 ... npm.ps1因为在此系统上禁止运行脚本这是 Windows PowerShell 的执行策略限制不是项目本身的问题。如果你在 PowerShell 里运行npm install或node命令时看到这个报错说明 PowerShell 禁止运行脚本文件。Node.js 本身已经装好了只是 PowerShell 不让执行.ps1脚本。解决方法有两种一是改用 CMD 或 Git Bash 运行命令二是以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入Y确认。改完后npm和node都能正常用。如果只是跑这个 Demo其实不需要npm install直接用node index.js就行但如果你后续要装ws或其他包这个策略问题必须先解决。4.4 现象WebSocket 连接建立后立刻断开原因可能是host或path写错或者请求头里缺少必要字段。讯飞 WebSocket 接口在握手阶段会校验Host、Date、Authorization三个头少一个都会导致连接被拒。如果你用的是ws库需要在new WebSocket(url, { headers: {...} })里把这三个头带上。另一个可能是时间偏差。服务器时间和本地时间差超过 5 分钟签名里的date就会失效。解决方法是在代码里用new Date().toUTCString()获取当前 GMT 时间不要手动拼字符串。如果本地时间本身不准先同步系统时间。4.5 现象翻译结果返回code: 0但trans_result为空原因通常是from或to语言代码填错或者text没有做 Base64 编码。讯飞翻译接口要求data.text是 Base64 编码后的字符串直接传明文会返回空结果。另外from和to不能相同相同语言对不会触发翻译。解决方法是先确认语言代码在讯飞文档的支持列表里然后把text用Buffer.from(text, utf8).toString(base64)编码后再传。如果还是空检查business字段里是否漏了app_id有些接口版本要求app_id放在common里有些放在business里以 Demo 里的写法为准。5. 进阶技巧把 Demo 改成可复用的转写翻译模块5.1 把鉴权逻辑抽成独立函数Demo 里的鉴权代码通常和主流程混在一起想复用到其他项目就得复制粘贴。我一般会把buildAuthUrl抽成一个独立模块接收apiKey、apiSecret、host、path四个参数返回完整 URL。这样换接口时只需要改host和path签名逻辑不用动。// auth.js const crypto require(crypto); function buildAuthUrl({ apiKey, apiSecret, host, path }) { const date new Date().toUTCString(); const signatureOrigin host: ${host}\ndate: ${date}\nGET ${path} HTTP/1.1; const signature crypto.createHmac(sha256, apiSecret).update(signatureOrigin).digest(base64); const authorizationOrigin api_key${apiKey}, algorithmhmac-sha256, headershost date request-line, signature${signature}; const authorization Buffer.from(authorizationOrigin).toString(base64); return https://${host}${path}?authorization${authorization}date${encodeURIComponent(date)}host${host}; } module.exports { buildAuthUrl };抽出来后转写和翻译两个接口可以共用同一个函数只是传入不同的host和path。这样代码量减少维护也方便。5.2 用流式读取替代一次性读文件Demo 里通常用fs.readFileSync一次性把整个 PCM 读进内存然后切片发送。测试音频小的时候没问题但如果要转写一个几十分钟的录音内存占用会很高。常见改进是用fs.createReadStream按帧读取每读 1280 字节就发一帧。const fs require(fs); const FRAME_SIZE 1280; function streamAudio(filePath, onFrame) { const stream fs.createReadStream(filePath, { highWaterMark: FRAME_SIZE }); let seq 1; stream.on(data, (chunk) { onFrame({ data: { status: chunk.length FRAME_SIZE ? 2 : 1, format: audio/L16;rate16000, encoding: raw, audio: chunk.toString(base64) } }); seq; }); stream.on(end, () { // 如果最后一帧刚好是整帧需要补一个空帧 status2 }); }注意highWaterMark设为 1280 后每次data事件拿到的 chunk 大小不一定正好是 1280可能是 1280 的整数倍。稳妥做法是在data回调里再按 1280 切片确保每帧大小一致。另外如果文件大小正好是 1280 的整数倍最后一帧的status不会被设为 2需要在end事件里补发一个空帧或者手动标记。5.3 转写与翻译的时序控制如果先转写再翻译要等转写结果完整返回后才能调翻译接口。但实时转写是流式的中间帧也会返回部分结果。常见做法是只取status: 2的那一帧结果作为最终文本或者把中间结果拼接起来。如果要做“边说边译”就需要每收到一个中间结果就调一次翻译但这样翻译接口调用频率会很高适合对延迟敏感的场景。我一般会在status: 2时触发翻译这样文本完整翻译质量更稳定。如果业务要求低延迟可以在status: 1时也触发翻译但需要做去重和拼接避免重复翻译同一段话。5.4 错误重试与日志记录接口调用失败时不要直接抛异常退出。常见做法是记录sid、code、message和当前帧序号然后根据错误码决定是否重试。code: 10105表示签名过期需要重新生成 URLcode: 10106表示音频格式错误重试也没用直接报错退出。function handleError(err, context) { const log { time: new Date().toISOString(), sid: context.sid, code: err.code, message: err.message, frameSeq: context.seq }; console.error(JSON.stringify(log)); if (err.code 10105) { // 签名过期重建连接 return retry; } return abort; }日志里带上sid和帧序号排查时能快速定位是鉴权问题还是音频问题。如果sid为空说明连接都没建立成功优先检查host和path。5.5 一个我踩过的坑date参数编码buildAuthUrl返回的 URL 里date参数必须用encodeURIComponent编码。toUTCString()返回的字符串里有空格和逗号不编码直接拼进 URL 会导致参数截断接口收到的date不完整签名校验失败。这个坑很隐蔽因为浏览器或https模块可能自动处理了一部分编码但ws库不会。从那以后我每次拼鉴权 URL 都强制走一遍encodeURIComponent不管参数看起来多安全。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询