整体架构

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) │←──│ Kokoro │
│ POST /tts │ │ (Matcha/VITS)│
│ 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
→ 路由业务(business_code / 号码映射 → 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
→ 业务拿到识别结果

callHearhear 的”长连接”版本——整通通话保持 audio_fork,业务通过 onResult / onFinal 回调驱动。

4. TTS 播报流程(speak)

apps/callflow-esl/config.jsontts.streamingEnabled 决定走哪条链路:

普通链路(streamingEnabled=false

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 <wavUrl>
→ FreeSWITCH 从 TTS server 拉 wav 播给用户

流式链路(streamingEnabled=true

1
2
3
4
5
6
7
8
business: ctx.speak({ kind: "tts", text: "..." })
→ CallRuntime → HTTP POST http://tts:9080/tts-stream
→ sherpa-tts-server 立即返回 streamUrl(不等合成完成)
→ callflow-esl 拼上 streamPlaybackPrefix(默认 shout://)
→ 通过 ESL 下发 playback shout://tts:9080/tts-stream/<id>.mp3
→ FreeSWITCH mod_shout 边拉 MP3 边播
→ sherpa-tts-server 用 ffmpeg 把增量 PCM 实时编码成 MP3 写回响应
→ 整段完成后 wav 仍落盘到 public/wav/ 供下次缓存命中

文本+speakerId+speed 相同的合成会命中文件级缓存,第二次请求直接返回已有 wavUrl,零合成开销。

管理面与外呼面

除上面的媒体主链路外,仓库还提供两个面向运营、与通话媒体路径解耦的子系统:

组件 角色 进程 协议 默认端口
callflow-server 管理台 API:运行时配置 / FreeSWITCH 数据视图 / TTS 试听代理 Bun HTTP 9913
callflow-webpage 管理控制台前端(看板 / 表浏览 / 配置 / 音色试听) Quasar HTTP(dev) 19913
callout-server 外呼任务 API:活动 / 名单 / 批次调度 / 角色鉴权 / 坐席 / 转人工会议 / 策略 / 回铃音 + 嘟声检测 / 线路号码池 Bun HTTP + inbound ESL 9920
callout-webpage 外呼管理前端 Quasar HTTP(dev) 19920
emotion-analysis-server 通话后情绪分析(文本 BERT + 音频 wav2vec2),由 callout 后台调用 C++ HTTP 9090
  • 管理控制台(callflow-server + callflow-webpage)把运行时 TTS / 录音配置写进
    callflow-esl 同一套 callflow schema,再通过 POST /runtime-configs/reload 通知
    callflow-esl 热重载;同时只读浏览 FreeSWITCH 数据库、管理号码 → 业务路由、试听 TTS
    音色。详见 管理控制台
  • 智能外呼(callout-server + callout-webpage)直接连 FreeSWITCH inbound ESL 批量
    originate,接通后复用同一套 dialplan socket 把通话送回 callflow-esl 执行业务:
1
2
3
4
callout-webpage → callout-server
→ inbound ESL → FreeSWITCH originate {business_code=...} &socket(...)
→ 接通后 B leg 经 dialplan socket 回到 callflow-esl(与呼入复用同一路由)
→ 通话结束 → POST /api/call-results 回写结果

调度、并发、工作时段、重试、回铃音 + 嘟声检测、角色鉴权、坐席工作台、转人工会议
(盲转 / 协商转 / 主管监听·插话)、外呼策略与 DNC,以及通话后情绪分析(后台 worker
调用 情绪分析服务POST /analyze)详见 智能外呼

端口与协议矩阵

目标 协议 端口 用途
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 PostgreSQL TCP 5432 业务路由与运行时配置
浏览器 callflow-server HTTP 9913 管理控制台 API
callflow-server PostgreSQL / FreeSWITCH DB TCP 5432 运行时配置 + FS 数据视图
浏览器 callout-server HTTP 9920 外呼管理 API
callout-server FreeSWITCH TCP 8021 inbound ESL(批量 originate)
callout-server emotion-analysis-server HTTP 9090 通话后情绪分析 POST /analyze(可选)

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 为最小验证集

下一步