做语音转文字接入
要接入语音转文字功能,方言和实时性要求让选型很纠结,怕接错服务识别效果差还返工。
有人这样说「我得做个语音输入框,要能听懂东北话,还得实时出字,急着用。」
能替你选的都选好了。不想动,直接点下面「直接生成语音转文字接入」,点一下就开始做;想改,就点一下要的那个。
已替你选好想改就点一下
只有你知道的不填就先空着,成品里留【待填】
还想怎么改可以不选
高级选项一般不用动 · 这件事通常 1 分钟左右做好
示例成品全文按上面这组示例选项真跑出来的
直接上能跑的方案:前端录音 PCM 16k/16bit → 后端 WebSocket 代理 → 火山流式 ASR → 实时回传文字。
东北话先按“普通话大模型 + 热词表”落地,最稳;如果火山控制台有“东北话”方言选项,就把语言参数改成对应值。
1. 先到火山控制台拿 4 个东西
- 开通:火山引擎 → 语音技术 / 豆包语音识别
- 创建应用,选“流式语音识别”
- 拿到:
App IDAccess TokenCluster
- 热词表:创建东北话热词表,绑定这个应用
热词表至少塞这些常见东北口语:
咋地
咋整
干啥
嘎哈
唠嗑
得劲
磨叽
磕碜
老铁
稀罕
拉倒
可劲造
忽悠
波棱盖儿
埋汰
别介
嗯呐
扒拉
秃噜反仗
吭哧瘪肚这些词能明显减少同音字识别错。
2. 代码目录
dongbei-asr/
├── package.json
├── server.js
└── public/
├── index.html
└── pcm-worklet.js3. 后端:Node.js 代理
package.json
{
"name": "dongbei-asr",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"ws": "^8.16.0"
}
}server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { WebSocketServer, WebSocket } = require('ws');
const PORT = process.env.PORT || 8000;
// 火山控制台配置
const ASR_APPID = process.env.ASR_APPID;
const ASR_TOKEN = process.env.ASR_TOKEN;
const ASR_CLUSTER = process.env.ASR_CLUSTER || 'volcengine_streaming_common';
const ASR_LANGUAGE = process.env.ASR_LANGUAGE || 'zh-CN';
// 老版流式 ASR 接口。如果你开的是豆包方舟新版,只改这里和认证头即可。
const ASR_WS_BASE =
process.env.ASR_WS_BASE || 'wss://openspeech.bytedance.com/api/v2/asr';
function buildUpstreamUrl(uid) {
const q = new URLSearchParams({
appid: ASR_APPID,
token: ASR_TOKEN,
cluster: ASR_CLUSTER,
uid,
workflow: 'audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate',
language: ASR_LANGUAGE,
use_itn: 'true',
nlu_punctuate: 'true',
format: 'pcm',
sample_rate: '16000',
bits: '16',
channel: '1'
});
return `${ASR_WS_BASE}?${q.toString()}`;
}
function extractText(msg) {
const r = msg.result || msg.payload_msg || {};
if (typeof r.text === 'string' && r.text.trim()) return r.text.trim();
const u = (r.utterances || msg.utterances || [])[0];
if (u && typeof u.text === 'string' && u.text.trim()) return u.text.trim();
if (typeof msg.text === 'string' && msg.text.trim()) return msg.text.trim();
return '';
}
function extractIsFinal(msg) {
const r = msg.result || msg.payload_msg || {};
for (const key of ['is_final', 'definite', 'final', 'end']) {
if (typeof r[key] === 'boolean') return r[key];
}
const u = (r.utterances || msg.utterances || [])[0];
if (u) {
for (const key of ['is_final', 'definite', 'final', 'end']) {
if (typeof u[key] === 'boolean') return u[key];
}
}
return false;
}
const server = http.createServer((req, res) => {
const urlPath = (req.url || '/').split('?')[0];
const publicDir = path.join(__dirname, 'public');
let file = null;
if (urlPath === '/' || urlPath === '/index.html') {
file = path.join(publicDir, 'index.html');
} else if (urlPath === '/pcm-worklet.js') {
file = path.join(publicDir, 'pcm-worklet.js');
}
if (!file) {
res.writeHead(404);
res.end('not found');
return;
}
fs.stat(file, (err) => {
if (err) {
res.writeHead(404);
res.end('file not found');
return;
}
const ext = path.extname(file);
const mime = ext === '.html'
? 'text/html; charset=utf-8'
: 'application/javascript; charset=utf-8';
res.writeHead(200, { 'Content-Type': mime });
fs.createReadStream(file).pipe(res);
});
});
const wss = new WebSocketServer({ server, path: '/asr' });
wss.on('connection', (browserW