跳到正文

Java/TS 开发者导读与心智模型手册 (funasr-asr-server) ​

本文档专为熟悉 Java 或 TypeScript / Node.js 的工程师编写,旨在打破语言范式隔阂,帮助你深刻理解 onnx-platform/funasr-asr-server 的设计哲学、内存所有权结构与并发调度机制。


1. Java / TS 与 Rust 核心概念映射表 ​

概念分类Java 经典对应TypeScript / Node.js 对应Rust (当前项目实践)设计原由与底层差异
共享单例Spring @Singleton 依赖注入模块导出单例对象 export const engine = ...Arc<FunAsrEngine>跨线程/跨协程共享只读对象,由原子引用计数管理生命周期,无需 GC
会话上下文class Session 实例对象new Session() 会话状态对象TwoPassSession 结构体每个 WebSocket 连接独占一份,由连接协程独占可变借用(&mut self),零锁争用
多线程安全synchronized / ReentrantLock单线程(Worker 线程传消息)Arc<Mutex<T>> 或只读 Arc<T>模型推理是只读的,直接通过 Arc<T> 并发调用,避免锁带来的上下文切换
异步调度Netty Worker 线程池 / Virtual Threadlibuv 事件循环 + Promise 任务Tokio 异步运行时 + Task 协程语法像 TS 的 async/await,但底层是由多线程工作窃取(Work-Stealing)池并发驱动
通道通信LinkedBlockingQueue / DisruptorEventEmitter / RxJS Subjecttokio::sync::watch / mpsc线程与协程间通信的通道,通过所有权转移杜绝共享内存隐患
空指针防护Optional<T>(但容易忘记处理)T | null | undefined 可选链Option<T>(Some / None)编译器强制穷尽匹配,Rust 语言层面不存在 null 或 NPE 异常
异常与返回throw new Exception() + try/catchthrow new Error() + try/catchResult<T, anyhow::Error> + ?错误是显式返回值,? 语法糖遇到错误立即提前 return,堆栈清晰明了
定点/浮点转换ByteBuffer.getShort() 转 floatDataView.getInt16(i, true) 转浮点i16::from_le_bytes 转 f32零开销低层内存字节重排,直接对接 PCM16 电话语音格式

2. 核心架构深度剖析(面向 Java/TS 视角) ​

2.1 全局单例与零锁并发推理 (Arc<FunAsrEngine>) ​

在 Java 中,高并发场景下调用模型推理常常面临两难:

  • 给推理加 synchronized 互斥锁:多连接排队,CPU 多核利用率极低;
  • 为每个连接 new 一个模型:内存直接 OOM 崩溃。

Rust 的解决方式: onnxruntime 底层的 OrtSession 是 C++ 编写的,只要每次推理时传入的输入张量(Input Tensor)是线程独立的,ONNX Runtime 的前向推理函数就是原生线程安全的! 因此,funasr-asr-server 将引擎包装为不可变的 Arc<FunAsrEngine>:

  • 不需要加任何互斥锁(Mutex);
  • 几十路 WebSocket 连接和 HTTP 请求可以同时并发调用 engine.paraformer.recognize_samples(...);
  • 由操作系统多核 CPU 并发执行矩阵运算,既零锁等待,又只占一份模型内存!

2.2 会话隔离与独占借用 (TwoPassSession) ​

在 Node.js 中处理流式识别时,通常在 ws.on('connection') 时创建一个 session 闭包对象。 在 Rust 中亦是如此,但 Rust 编译器通过所有权与生命周期规则提供了更强的安全保障:

rust
// server/ws_streaming.rs
let mut session = TwoPassSession::new(...);

while let Some(msg) = ws_stream.next().await {
    // 独占可变借用(&mut session),保证同一时刻绝无其他线程修改内部缓存
    let result = session.push_pcm16(&bytes, ...)?;
}

TwoPassSession 内部持有:

  1. vad_caches: [Array4<f32>; 4]:FSMN 神经网络的 4 层历史时域记忆块;
  2. online_ctx: OnlineSessionContext:CIF 点火器累积的声学权重与 Partial 文本;
  3. utterance_pcm: Vec<f32>:当前句子从字头到字尾的完整音频采样。

由于会话被当前 WebSocket 协程严格独占(&mut self),所有状态更新完全在栈/单协程中完成,无任何锁开销。当 WebSocket 断开连接时,session 离开作用域自动析构回收,零内存泄漏。

2.3 优雅停机信号广播 (watch::channel) ​

类似于 Java Spring 容器关闭时的 @PreDestroy 钩子或 Node.js 的 process.on('SIGINT'):

rust
// 1. 创建 watch 通道(单写者,多读者)
let (shutdown_tx, mut shutdown_rx_ws) = tokio::sync::watch::channel(());
let mut shutdown_rx_http = shutdown_tx.subscribe();

// 2. 两个独立 Listener 分别监听停机信号
axum::serve(ws_listener, ws_router)
    .with_graceful_shutdown(async move {
        let _ = shutdown_rx_ws.changed().await;
    });

// 3. 主协程监听操作系统 Ctrl+C 信号并广播
tokio::select! {
    _ = tokio::signal::ctrl_c() => {
        info!("Received Ctrl+C, broadcasting shutdown...");
        let _ = shutdown_tx.send(());
    }
}

一旦收到操作系统的终止信号,两个 HTTP/WS 服务停止接收新的网络连接,并平滑等待正在处理中的语音识别完成,最后安全退出。


3. 源码阅读路线图 ​

如果你准备通读或参与贡献 funasr-asr-server 的代码,建议遵循以下清晰的路线:

  1. 入口与服务装配:
    • src/main.rs:双 Listener 绑定与优雅停机编排;
    • src/config.rs:配置结构体与模型 Profile 机制;
  2. 2-Pass 流水线中枢:
    • src/engine/session.rs(最重要!):仔细阅读 push_pcm16 函数,观察音频如何依次经过 VAD -> Online CIF -> Offline Final;
  3. 前端特征提取:
    • src/engine/frontend.rs:加窗、FFT、Mel 滤波、LFR 拼帧与 CMVN 归一化;
  4. 模型与算法核心:
    • src/engine/online.rs:CifSearcher 积分点火器与流式 Decoder;
    • src/engine/vad.rs:FSMN-VAD 状态机与滑动窗判定;
    • src/engine/paraformer.rs:离线大模型 Greedy Search 解码;
    • src/engine/punc.rs:CT-Transformer 标点纠错与 jieba-rs 分词结合;
  5. 协议与网络接口:
    • src/server/ws_streaming.rs:WebSocket 流式协议与 Drachtio 子协议处理;
    • src/server/http_offline.rs:HTTP /asr 与 OpenAI Whisper 规范转写接口。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。