|
|
@@ -36,6 +36,15 @@ const GATEWAY = process.env.NLS_GATEWAY || 'wss://nls-gateway-cn-shanghai.aliyun
|
|
|
const audioArg = process.argv.find((a) => a.startsWith('--audio='));
|
|
|
const sayArg = process.argv.find((a) => a.startsWith('--say='));
|
|
|
const waitMs = Number((process.argv.find((a) => a.startsWith('--wait=')) || '').slice(7)) || 4000;
|
|
|
+/** 端点静音断句阈值(0 = 用协议层默认 800)。延迟高先怀疑它 */
|
|
|
+const silenceArg = process.argv.find((a) => a.startsWith('--silence='));
|
|
|
+const maxSentenceSilence = silenceArg ? Number(silenceArg.slice('--silence='.length)) : 0;
|
|
|
+/** 语义断句(试它是否能提高中间结果频率) */
|
|
|
+const semanticDetection = process.argv.includes('--semantic');
|
|
|
+/** 模拟握手期间已攒下的音频(毫秒) */
|
|
|
+const prebufferMs = Number((process.argv.find((a) => a.startsWith('--prebuffer=')) || '').slice(12)) || 0;
|
|
|
+/** 每个 tick 最多发几帧(100ms 一帧):1 = 1× 实时,4 = 4× 实时 */
|
|
|
+const drainFrames = Number((process.argv.find((a) => a.startsWith('--drain=')) || '').slice(8)) || 1;
|
|
|
|
|
|
const stamp = () => new Date().toISOString().slice(11, 23);
|
|
|
const log = (...args) => console.log(`[${stamp()}]`, ...args);
|
|
|
@@ -69,6 +78,8 @@ const newId = () => randomUUID().replace(/-/g, ''); // 文档要求 32 位十六
|
|
|
const startMessageId = newId();
|
|
|
const taskId = newId();
|
|
|
let stopSent = false;
|
|
|
+let audioStartAt = 0; // 第一帧音频发出的时刻
|
|
|
+let sentBytes = 0; // 已推给网关的音频字节数
|
|
|
|
|
|
const send = (payload) => {
|
|
|
const text = JSON.stringify(payload);
|
|
|
@@ -121,16 +132,36 @@ ws.onopen = () => {
|
|
|
enable_punctuation_prediction: true,
|
|
|
enable_inverse_text_normalization: true,
|
|
|
enable_voice_detection: true,
|
|
|
- max_sentence_silence: 800,
|
|
|
+ ...(maxSentenceSilence ? { max_sentence_silence: maxSentenceSilence } : {}),
|
|
|
+ ...(semanticDetection ? { enable_semantic_sentence_detection: true } : {}),
|
|
|
},
|
|
|
});
|
|
|
+ if (audioPcm) audioStartAt = Date.now();
|
|
|
|
|
|
if (audioPcm) {
|
|
|
- // 按 100ms 一帧推流(16k/16bit/mono → 3200 字节 = 1600 采样)
|
|
|
+ // 模拟真实采集:**可用音频 = 握手积压 + 已过时间**(1× 实时到达),
|
|
|
+ // 每个 tick 最多排空 drainFrames 帧 —— 这正是前端 nls.ts 的排空策略。
|
|
|
+ // `--drain=1` 复现"只按 1× 排空"的旧行为(积压永远排不掉);
|
|
|
+ // `--drain=4` 是现在前端的策略(积压时最多 4× 追)。
|
|
|
const pcm = audioPcm;
|
|
|
- const frame = 3200;
|
|
|
+ const frame = 3200; // 100ms
|
|
|
+ const bytesPerMs = 32; // 16k/16bit/mono → 32 字节/ms
|
|
|
+ const startedAt = Date.now();
|
|
|
let offset = 0;
|
|
|
const pump = setInterval(() => {
|
|
|
+ const elapsedMs = Date.now() - startedAt;
|
|
|
+ const available = Math.min(pcm.length, Math.round((prebufferMs + elapsedMs) * bytesPerMs));
|
|
|
+ let sentThisTick = 0;
|
|
|
+ while (offset < available && sentThisTick < drainFrames) {
|
|
|
+ const end = Math.min(offset + frame, pcm.length, available);
|
|
|
+ const slice = pcm.subarray(offset, end);
|
|
|
+ offset = end;
|
|
|
+ if (ws.readyState === WebSocket.OPEN) {
|
|
|
+ ws.send(slice);
|
|
|
+ sentBytes += slice.length;
|
|
|
+ }
|
|
|
+ sentThisTick += 1;
|
|
|
+ }
|
|
|
if (offset >= pcm.length) {
|
|
|
clearInterval(pump);
|
|
|
log(`音频推完(${pcm.length} 字节)`);
|
|
|
@@ -138,11 +169,7 @@ ws.onopen = () => {
|
|
|
stopSent = true;
|
|
|
sendCommand({ name: 'StopTranscription', task: 'stop' });
|
|
|
}
|
|
|
- return;
|
|
|
}
|
|
|
- const slice = pcm.subarray(offset, Math.min(offset + frame, pcm.length));
|
|
|
- offset += slice.length;
|
|
|
- if (ws.readyState === WebSocket.OPEN) ws.send(slice);
|
|
|
}, 100);
|
|
|
} else {
|
|
|
setTimeout(() => {
|
|
|
@@ -160,7 +187,16 @@ ws.onmessage = (event) => {
|
|
|
try {
|
|
|
const body = JSON.parse(text);
|
|
|
const name = body?.header?.name || '(无 header.name)';
|
|
|
- log(` 事件=${name} status=${body?.header?.status ?? body?.payload?.status ?? ''} 结果=${JSON.stringify(body?.payload?.result ?? '')}`);
|
|
|
+ const gwMs = Number(body?.payload?.time ?? NaN);
|
|
|
+ const sentMs = Math.round((sentBytes / 32000) * 1000);
|
|
|
+ // 感知延迟 = 「麦克风已经说了多少」−「网关处理到第几毫秒」。
|
|
|
+ // 用采集量(积压 + 已过时间)而不是发送量来算 —— 用户感受到的是前者。
|
|
|
+ const capturedMs = audioStartAt ? prebufferMs + (Date.now() - audioStartAt) : sentMs;
|
|
|
+ const lag = Number.isFinite(gwMs) ? capturedMs - gwMs : null;
|
|
|
+ log(
|
|
|
+ ` 事件=${name} 网关已处理=${Number.isFinite(gwMs) ? gwMs + 'ms' : '-'} 已推=${sentMs}ms ` +
|
|
|
+ `${lag === null ? '' : `**感知延迟=${lag}ms** `}结果=${JSON.stringify(body?.payload?.result ?? '')}`
|
|
|
+ );
|
|
|
if (name === 'TranscriptionCompleted') finish(0);
|
|
|
if (name === 'TaskFailed') finish(3);
|
|
|
} catch {
|