1010 * 符合项目"可选增强"设计原则。
1111 * @ai -context: 音频输入要求:16kHz 单声道 Float32 PCM。
1212 * 渲染进程发来的音频块已满足此格式(process-audio native addon 输出)。
13+ * @ai -context: sherpa-onnx-node API 兼容:旧版(<=1.12)导出工厂函数
14+ * createOfflineRecognizer/createOnlineRecognizer;新版(1.13+,如 1.13.4)
15+ * 改为导出类 OfflineRecognizer/OnlineRecognizer(new 构造,config 形状不变)。
16+ * 下方工厂包装双路径兼容,否则升级依赖后 TypeError: ... is not a function
17+ * (2026-08 真流式启动失败根因,离线路径同样受影响)。
1318 */
1419
1520import * as path from 'path' ;
@@ -26,9 +31,16 @@ import {
2631// sherpa-onnx 动态加载(可选依赖,加载失败不崩溃)
2732// ================================================================
2833
34+ /** 识别器构造器签名(新版 API:类导出) */
35+ type RecognizerCtor < T > = new ( config : Record < string , unknown > ) => T ;
36+
2937interface SherpaOnnx {
30- createOfflineRecognizer ( config : Record < string , unknown > ) : OfflineRecognizer ;
31- createOnlineRecognizer ( config : Record < string , unknown > ) : OnlineRecognizer ;
38+ /** 旧版 API(<=1.12):工厂函数 */
39+ createOfflineRecognizer ?: ( config : Record < string , unknown > ) => OfflineRecognizer ;
40+ createOnlineRecognizer ?: ( config : Record < string , unknown > ) => OnlineRecognizer ;
41+ /** 新版 API(1.13+):类导出 */
42+ OfflineRecognizer ?: RecognizerCtor < OfflineRecognizer > ;
43+ OnlineRecognizer ?: RecognizerCtor < OnlineRecognizer > ;
3244}
3345
3446interface OfflineRecognizer {
@@ -38,9 +50,12 @@ interface OfflineRecognizer {
3850}
3951
4052interface OfflineStream {
53+ /** 旧版:(sampleRate, samples);新版 1.13+:({ samples, sampleRate }),统一走 feedWaveform */
4154 acceptWaveform ( sampleRate : number , samples : Float32Array ) : void ;
55+ acceptWaveform ( waveform : { samples : Float32Array ; sampleRate : number } ) : void ;
4256 inputFinished ( ) : void ;
43- free ( ) : void ;
57+ /** 新版 OnlineStream/OfflineStream 未提供 free(句柄由 GC 回收),故为可选 */
58+ free ?( ) : void ;
4459}
4560
4661export interface OnlineRecognizer {
@@ -53,14 +68,56 @@ export interface OnlineRecognizer {
5368}
5469
5570export interface OnlineStream {
71+ /** 旧版:(sampleRate, samples);新版 1.13+:({ samples, sampleRate }),统一走 feedWaveform */
5672 acceptWaveform ( sampleRate : number , samples : Float32Array ) : void ;
73+ acceptWaveform ( waveform : { samples : Float32Array ; sampleRate : number } ) : void ;
5774 inputFinished ( ) : void ;
58- free ( ) : void ;
75+ free ?( ) : void ;
76+ }
77+
78+ /**
79+ * 向流喂入音频:自动适配新旧 API 签名差异。
80+ * 旧版(<=1.12)acceptWaveform(sampleRate, samples) 双参;
81+ * 新版(1.13+)acceptWaveform({ samples, sampleRate }) 单对象。
82+ * 按函数形参数(length)判别,调用方无需感知版本。
83+ */
84+ export function feedWaveform (
85+ stream : OnlineStream | OfflineStream ,
86+ sampleRate : number ,
87+ samples : Float32Array ,
88+ ) : void {
89+ if ( stream . acceptWaveform . length <= 1 ) {
90+ stream . acceptWaveform ( { samples, sampleRate } ) ;
91+ } else {
92+ stream . acceptWaveform ( sampleRate , samples ) ;
93+ }
5994}
6095
6196let _sherpa : SherpaOnnx | null = null ;
6297let _loadAttempted = false ;
6398
99+ /** 创建离线识别器:优先旧版工厂函数,缺失时用新版类构造 */
100+ function instantiateOffline ( sherpa : SherpaOnnx , config : Record < string , unknown > ) : OfflineRecognizer {
101+ if ( typeof sherpa . createOfflineRecognizer === 'function' ) {
102+ return sherpa . createOfflineRecognizer ( config ) ;
103+ }
104+ if ( typeof sherpa . OfflineRecognizer === 'function' ) {
105+ return new sherpa . OfflineRecognizer ( config ) ;
106+ }
107+ throw new Error ( 'sherpa-onnx-node 既无 createOfflineRecognizer 工厂也无 OfflineRecognizer 类,请核对依赖版本' ) ;
108+ }
109+
110+ /** 创建在线识别器:优先旧版工厂函数,缺失时用新版类构造 */
111+ function instantiateOnline ( sherpa : SherpaOnnx , config : Record < string , unknown > ) : OnlineRecognizer {
112+ if ( typeof sherpa . createOnlineRecognizer === 'function' ) {
113+ return sherpa . createOnlineRecognizer ( config ) ;
114+ }
115+ if ( typeof sherpa . OnlineRecognizer === 'function' ) {
116+ return new sherpa . OnlineRecognizer ( config ) ;
117+ }
118+ throw new Error ( 'sherpa-onnx-node 既无 createOnlineRecognizer 工厂也无 OnlineRecognizer 类,请核对依赖版本' ) ;
119+ }
120+
64121/** 尝试加载 sherpa-onnx-node(仅尝试一次) */
65122function loadSherpa ( ) : SherpaOnnx | null {
66123 if ( _loadAttempted ) return _sherpa ;
@@ -97,7 +154,7 @@ function getOfflineRecognizer(): OfflineRecognizer | null {
97154 const threads = config . threads > 0 ? config . threads : Math . max ( 1 , os . cpus ( ) . length - 1 ) ;
98155
99156 try {
100- _offlineRecognizer = sherpa . createOfflineRecognizer ( {
157+ _offlineRecognizer = instantiateOffline ( sherpa , {
101158 featConfig : { sampleRate : 16000 , featureDim : 80 } ,
102159 modelConfig : {
103160 senseVoice : {
@@ -131,7 +188,7 @@ export function getOnlineRecognizer(): OnlineRecognizer | null {
131188 const threads = config . threads > 0 ? config . threads : Math . max ( 1 , os . cpus ( ) . length - 1 ) ;
132189
133190 try {
134- _onlineRecognizer = sherpa . createOnlineRecognizer ( {
191+ _onlineRecognizer = instantiateOnline ( sherpa , {
135192 featConfig : { sampleRate : 16000 , featureDim : 80 } ,
136193 modelConfig : {
137194 paraformer : {
@@ -219,7 +276,7 @@ export async function transcribeOffline(
219276
220277 const stream = recognizer . createStream ( ) ;
221278 try {
222- stream . acceptWaveform ( 16000 , pcmData ) ;
279+ feedWaveform ( stream , 16000 , pcmData ) ;
223280 stream . inputFinished ( ) ;
224281 recognizer . decode ( stream ) ;
225282 const result = recognizer . getResult ( stream ) ;
@@ -229,7 +286,7 @@ export async function transcribeOffline(
229286 logger . debug ( `[LocalASR] Offline transcribe: ${ text . length } chars, ${ durationMs } ms` ) ;
230287 return { text, engine : 'offline' , durationMs } ;
231288 } finally {
232- stream . free ( ) ;
289+ stream . free ?. ( ) ;
233290 }
234291}
235292
@@ -258,7 +315,7 @@ export async function transcribeStreaming(
258315 const chunkSize = 1600 ;
259316 for ( let offset = 0 ; offset < pcmData . length ; offset += chunkSize ) {
260317 const chunk = pcmData . subarray ( offset , Math . min ( offset + chunkSize , pcmData . length ) ) ;
261- stream . acceptWaveform ( 16000 , chunk ) ;
318+ feedWaveform ( stream , 16000 , chunk ) ;
262319 while ( recognizer . isReady ( stream ) ) {
263320 recognizer . decode ( stream ) ;
264321 }
@@ -277,7 +334,7 @@ export async function transcribeStreaming(
277334 logger . debug ( `[LocalASR] Streaming transcribe: ${ text . length } chars, ${ durationMs } ms` ) ;
278335 return { text, engine : 'streaming' , durationMs } ;
279336 } finally {
280- stream . free ( ) ;
337+ stream . free ?. ( ) ;
281338 }
282339}
283340
0 commit comments