示例成品 · 平台演示,按左边这组点选真跑出来的
直接上能跑的方案:**前端录音 PCM 16k/16bit → 后端 WebSocket 代理 → 火山流式 ASR → 实时回传文字**。
东北话先按“普通话大模型 + 热词表”落地,最稳;如果火山控制台有“东北话”方言选项,就把语言参数改成对应值。
---
## 1. 先到火山控制台拿 4 个东西
1. 开通:火山引擎 → 语音技术 / 豆包语音识别
2. 创建应用,选“流式语音识别”
3. 拿到:
- `App ID`
- `Access Token`
- `Cluster`
4. 热词表:创建东北话热词表,绑定这个应用
热词表至少塞这些常见东北口语:
```text
咋地
咋整
干啥
嘎哈
唠嗑
得劲
磨叽
磕碜
老铁
稀罕
拉倒
可劲造
忽悠
波棱盖儿
埋汰
别介
嗯呐
扒拉
秃噜反仗
吭哧瘪肚
```
这些词能明显减少同音字识别错。
---
## 2. 代码目录
```text
dongbei-asr/
├── package.json
├── server.js
└── public/
├── index.html
└── pcm-worklet.js
```
---
## 3. 后端:Node.js 代理
### package.json
```json
{
"name": "dongbei-asr",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"ws": "^8.16.0"
}
}
```
### server.js
```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
点左边「开工 · 直接出成品」,出一份你自己的版本(文字免费)