整体架构
AI Voice Platform 由 5 个相互独立的服务/模块 组成,各自职责单一、协议清晰,可以单机起,也可以分布式部署。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| ┌────────────────────────────┐ │ Caller / SIP Trunk │ └────────────┬───────────────┘ │ SIP / RTP ▼ ┌────────────────────────────────────────┐ │ FreeSWITCH │ │ ┌────────────────────────────────┐ │ │ │ dialplan: socket host:port ... │ │ │ ├────────────────────────────────┤ │ │ │ mod_audio_fork │ │ │ └─────┬──────────────────┬───────┘ │ └────────┼──────────────────┼────────────┘ │ outbound ESL │ WebSocket │ (TCP 9911) │ (PCM16 16k) ▼ ▼ ┌──────────────────┐ ┌─────────────────────────┐ │ callflow-esl │ │ sherpa-asr-online-server│ │ Bun + TS │ │ (C++ WebSocket /audio) │ │ ───────────── │ └─────────────┬───────────┘ │ SessionRunner │ │ │ ↓ │ ┌─────────────▼───────────┐ │ CallRuntime │ │ sherpa-onnx │ │ ↓ │ │ (ONNX Runtime, CPU) │ │ business fn │ │ ┌─────────────────┐ │ │ (speak/hear/ │ │ │ streaming │ │ │ originate) │ │ │ zipformer + │ │ └────────┬─────────┘ │ │ silero-vad │ │ │ │ └─────────────────┘ │ │ HTTP └─────────────────────────┘ │ POST /tts ▼ ┌─────────────────────────┐ │ sherpa-tts-server │ ┌───────────────┐ │ (C++ HTTP) │←──│ VITS │ │ POST /tts │ │ aishell3 │ │ GET /wav/<name>.wav │ └───────────────┘ └──────────┬──────────────┘ │ HTTP playback ▼ FreeSWITCH playback
|
组件职责矩阵
| 组件 |
角色 |
进程 |
主要协议 |
默认端口 |
| FreeSWITCH |
软交换、媒体处理、ESL |
第三方 |
SIP / RTP / ESL |
5060 / RTP / 8021 |
| mod_audio_fork |
把通话音频从 FreeSWITCH 桥接出去 |
FS 内 |
WebSocket(子协议 audio.drachtio.org) |
跟随上游 |
| callflow-esl |
业务编排:ESL 入站 + HTTP 外呼 API |
Bun |
ESL TCP / HTTP |
9911 / 9912 |
| sherpa-asr-online-server |
流式 ASR + VAD |
C++ |
WebSocket |
10096 |
| sherpa-tts-server |
离线 TTS + wav 缓存与下载 |
C++ |
HTTP |
9080 |
四条主数据流
1. 呼入流程(用户拨进来)
1 2 3 4 5 6 7 8 9
| SIP INVITE → FreeSWITCH dialplan → action socket host:9911 async full → outbound ESL 连接 callflow-esl → SessionRunner 解析 channel info → 路由业务(biz_id / 号码映射 → call_businesses) → business 函数被调用,拿到 CallContext → ctx.answer / ctx.speak / ctx.hear / ... → 业务完成 → ctx.hangup
|
2. 呼出流程(业务发起外呼)
1 2 3 4 5
| HTTP POST /outbound-calls (callflow-esl) → inbound ESL → FreeSWITCH originate → 接通后 FreeSWITCH 通过 dialplan 把 B leg 送回 callflow-esl → outbound ESL 复用同一套业务路由 → 业务函数被调用
|
3. 实时识别流程(hear / callHear)
1 2 3 4 5 6 7 8 9
| business: ctx.hear({...}) → CallRuntime 通过 ESL 下发 uuid_audio_fork start ws://asr:10096/audio → FreeSWITCH mod_audio_fork 建立 WebSocket → 先发 metadata 文本帧(含 channel uuid) → 持续推 16kHz/mono/PCM16 二进制帧 → sherpa-asr-online-server 推 partial / final 文本帧回 FS → FS CUSTOM event mod_audio_fork::transcription → callflow-esl 订阅事件 → 反序列化 → 触发 hear() Promise → 业务拿到识别结果
|
callHear 是 hear 的”长连接”版本——整通通话保持 audio_fork,业务通过 onResult / onFinal 回调驱动。
4. TTS 播报流程(speak)
1 2 3 4 5 6
| business: ctx.speak({ kind: "tts", text: "..." }) → CallRuntime → HTTP POST http://tts:9080/tts → sherpa-tts-server 合成 wav 落盘到 public/wav/tts-<hash>.wav → 返回 wavUrl → callflow-esl 通过 ESL 下发 playback shout://<wavUrl> → FreeSWITCH 从 TTS server 拉 wav 播给用户
|
文本+speakerId+speed 相同的合成会命中文件级缓存,第二次请求直接返回已有 wavUrl,零合成开销。
端口与协议矩阵
| 源 |
目标 |
协议 |
端口 |
用途 |
| SIP UAC |
FreeSWITCH |
SIP/UDP |
5060 |
呼入 |
| FreeSWITCH |
callflow-esl |
TCP |
9911 |
outbound ESL(dialplan socket) |
| callflow-esl |
FreeSWITCH |
TCP |
8021 |
inbound ESL(originate) |
| 调用方 |
callflow-esl |
HTTP |
9912 |
POST /outbound-calls |
| FreeSWITCH (mod_audio_fork) |
ASR Server |
WebSocket |
10096 |
/audio 路径,子协议 audio.drachtio.org |
| callflow-esl |
TTS Server |
HTTP |
9080 |
POST /tts |
| FreeSWITCH |
TTS Server |
HTTP |
9080 |
GET /wav/<file> 播放 |
| 任意 |
ASR Server |
HTTP |
10096 |
GET /health |
| 任意 |
callflow-esl |
TCP |
8021 |
业务订阅 ESL 事件(被动) |
ASR 与 TTS 服务没有鉴权。生产环境务必置于内网或加反代鉴权层,参见 部署。
业务编排分层
callflow-esl 内部分三层,自上而下:
1 2 3 4 5 6 7 8 9 10 11
| ┌───────────────────────────────────────────────┐ │ Business Layer ── 业务函数(一文件一业务) │ │ ctx.speak / hear / originate / conference │ ├───────────────────────────────────────────────┤ │ Runtime Layer ── CallRuntime / Controllers │ │ playback / recognition / dtmf / conference │ │ recording / channel-variable / originate │ ├───────────────────────────────────────────────┤ │ ESL Layer ── 协议层 + 入站/出站客户端 │ │ parser / commands / events / freeswitch IO │ └───────────────────────────────────────────────┘
|
业务层只看 CallContext,不直接碰 ESL 命令;Runtime 层负责把抽象命令翻译成 ESL execute / sendmsg / event 订阅;ESL 层负责协议序列化。
资源解耦
各组件之间无强耦合,可灵活替换:
| 想换什么 |
怎么做 |
| ASR 引擎 |
实现兼容 mod_audio_fork 子协议的 WebSocket 服务即可(参见 ASR 服务 协议章节) |
| TTS 引擎 |
实现 POST /tts 返回 { wavUrl } 的 HTTP 服务即可 |
| 业务编排 |
直接用裸 ESL 自行实现;或在 callflow-esl 里新增业务函数 |
| 模型 |
替换 models/ 下对应目录与配置中的路径,无需改代码 |
| 软交换 |
任何支持媒体桥接到 WebSocket 的 PBX;本项目以 FreeSWITCH 为最小验证集 |
下一步