基于Websocket的AI流媒体协议
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Seven Du c75eb456b8 Merge pull request 'add graphviz & msc graph' (#1) from add-graphviz-picture into master 1 month ago
mock add test html 9 months ago
LICENSE Initial commit 9 months ago
README.md add graphviz & msc graph 1 month ago

README.md

aistream

基于WebSocket的AI流媒体通信协议。本协议目前处于Beta版,欢迎大家参与。

背景

随着AI时代到来,出现了越来越多的智能业务场景和玩家。很多场景都需要与PSTN(传统电话网络)通信,然而,会AI的人不精通PSTN,会PSTN的人又不会AI,这就造成在AI与PSTN对接时出会现各种纠结和失配,基于两者进行业务逻辑开发的集成商和工程师也各种头痛。

本文旨在“发明”一种简单的通信协议,并把这里面的业务逻辑讲清楚,使工作在各个层面、各个环节的工程师都能愉快地工作,减少对接摩擦,让互联互通更顺滑。

现有协议分析

不同业务,不同厂商的产品想要对接,大家首先想到的首先就是一个“标准”的协议。在AI领域,这个协议就是MRCP。

MRCP的全称是Media Resource Control Protocol,即对媒体资源进行管理。它有两个版本:在信令层,v1使用RTSP、v2使用SIP,而媒体层都使用RTP。

现代语音相关的AI主要有三个功能:ASR、TTS和NLP,翻译成人话就是语音识别(语音转文本)、语音合成(文本转语音)和自然语音处理。

大体架构如下:

digraph G {
    node[shape=box]
	rankdir=LR

    phone[label="电话"]
    XSwitch[label="XSwitch"]
    server[label="MRCP Server(ASR/TTS)"]

    phone -> XSwitch
    XSwitch ->server
}

其中XSwitch是小樱桃科技基于FreeSWITCH开发的电话服务器,主要用于对接PSTN。

  • ASR:XSwitch将收到的音频流发给MRCP Server,后者识别到语音后,返回文本
  • TTS:XSwitch想对电话放音,它把一段文字发给MRCP Server,后者发回语音流,XSwitch转发给电话
  • NLP:NLP通常单独配置,它需要对文字进行理解,可以直接对接XSwitch,也可以对接MRCP Server(因为两者上面都有文字)

MRCP是比较老的协议,支持MRCP的服务通常需要私有化部署且非常贵。而现代的AI几乎都靠大数据支撑,因而放到互联网上更合适。但MRCP创建之初其实是没有考虑到互联网的,因而不适合用在互联网上。这就造成了做互联网、做AI的厂商实际上不想做MRCP,它不仅复杂,前景也没那么好。

但对接还是要做,因而,有了以下架构:

digraph G {
    node[shape=box]
	rankdir=LR

    phone[label="电话"]
    XSwitch[label="XSwitch"]
    server[label="MRCP Server"]
    ai[label="AI:ASR/TTS"]

    phone -> XSwitch
    XSwitch -> server
    server -> ai [dir="both",style=dotted,label="WebSocket"]
}

其中,AI平台对外提供WebSocket接口(WebSocket基于HTTP,它更像互联网),中间有一个服务负责做协议转换。

XSwitch中有一个模块(mod_unimrcp)与MRCP Server可以对接。

但MRCP大家都不懂(或不想懂),只是拿来用。但业务是千变万化的,因此很多人需要从XSwitch到AI平台传输一些私有的消息,典型地如主、被叫号码等。但大家都不会改。

事实上,MRCP中间件本身就是多余的。XSwitch本身就是模块化的结构,因此,小樱桃直接在XSwitch内部写了一个模块,直通:

digraph G {
    node[shape=box]
	rankdir=LR

    phone[label="电话"]
    XSwitch[label="XSwitch"]
    ai[label="AI:ASR/TTS"]

    phone -> XSwitch
    XSwitch -> ai [dir="both",style=dotted,label="WebSocket"]
}

在WebSocket上想传什么传什么。真香。但香是有代价的,那就是:它变得不标准了(各AI厂家都有自己的定义)。

在参考了各AI的协议后,本文试图定义一个标准,一个最简单的标准。

协议

每路通话都使用一个独立的WebSocket连接。

服务地址

服务在URL中指定,如:

wss://ip:port/asr
wss://ip:port/tts

认证鉴权

认证鉴权通常使用其它手段获取,如通过HTTP获取Token。

实际的鉴权信息有的在HTTP头域中传,有的在消息中传,本协议支持两种。

Authorization: Basic <credentials> Authorization: Bearer <token>

这个不是必须的,也可以在连接消息中传,见下面的初始消息中的usernamepasswordtoken部分。

具体的Token获取和刷新流程跟本协议无关。具体的验证过程也跟本协议无关,由厂商决定。

消息约定

控制消息使用文本(TEXT)传输,媒体消息使用二进制(BINARY)传输。

控制消息使用JSON RPC描述。简单来讲,JSON RPC消息分为请求和响应消息。而请求消息又分为两种:有id的和没有id的,后者不需要响应,它实际上是一个事件。参考链接:

http://wiki.geekdream.com/Specification/json-rpc_2.0.html

返回值放到resulterror对象中,标准的返回值如下:

{
    "jsonrpc": "2.0",
    "id": 0,
    "result": {
        "code": 200,
        "message": "OK"
    }
}

通用code返回值说明:

  • 100:临时响应,实际的响应消息将在后续以异步的方式返回。
  • 200:成功。
  • 202:请求已成功收到,但尚不知道结果,后续的结果将以事件(NOTIFY)的形式发出。
  • 206:成功,但是数据不全,如发生在放音过程中通过API暂停的情况。
  • 400:客户端错误,多发生在参数不全或不合法的情况。
  • 500:内部错误。
  • 6xx:系统错误,如发生在关机或即将关机的情况下,拒绝呼叫。

此外:

  • 所有文本消息都使用UTF-8编码
  • method区分大小写
  • 本文档描述之外的字段均为可选字段,应该忽略
  • JSON字段多单词使用下划线分割,如:caller_id_number,而不是:callerIdNumber
  • 对于所有请求(id不为空),都需要等待对方返回结果以后才可以进行下一步
  • 收到的所有事件(id为空)均可以忽略,这主要是为了不同具体实现的兼容(如在不期望ASR回复的场景下ASR发了文本消息,则客户端可以忽略)

初始消息

初始消息是一个JSON结构:

{
    "jsonrpc": "2.0",
    "id": 0,
    "method": "start",
    "params": {
        "version": "1",
        "username": "username",
        "password": "password",
        "token": "",
        "client": "XSwitch 3.0",
        "uuid": "当前通话uuid",
        "codec": "L16",
        "rate": 8000,
        "ms": 100,
        "heartbeat": 10,
        "audio": "sendrecv",
        "caller_id_number": "主叫号码",
        "destination_number": "被叫号码",
        "...": "other ..."
    }
}

当客户端连接成功后发送初始消息。初始消息包含当前通话信息,媒体流信息,协议版本信息。

  • version:版本号,目前固定为"1",字符串。
  • username:用户名,字符串,可选
  • password:密码,密码,可选
  • token:鉴权Token,可选
  • client:客户端类型,标志等,可选
  • uuid:当前通话的UUID,可选
  • codec:编码,如L16、PCMA、PCMU、WAV等,可选,默认为L16(PCM 16bit)
  • rate:采样率,可选,默认为8000
  • channels:整数,声道数,可选,默认为1,左右声道以interleaved方式排列,服务器可以不支持多声道
  • ms:多少毫秒的数据打一个包,可选,默认为100。实际发送的采样点数最大值为rate / 1000 * ms,采样点数也可能少于该值,接收方应该以实际收到的采样定点为准。
  • heartbeat:心跳,如果超时收不到任何包又收不到心跳由可以断开连接。可选,默认为10秒,如果不使用心跳,则可以传一个很大的值或负数。
  • audio:媒体流方向:sendrecvsendonlyrecvonlyinactive。服务端如果不回应,则默认为inactive,即服务端不发语音流。
  • caller_id_number:主叫号码
  • destination_number:被叫叫号码
  • 其它:厂商可自定义其它参数,比如多声道的场景需要有声道相关的描述,如座席还是客户

服务端回复:

{
    "id": 0,
    "result": {
        "code": 200,
        "message": "OK",
        "audio": "sendrecv",
        "heartbeat": 100
    }
}
  • audio:可选,语音方向。如果ASR服务端想返回语音流,则应该返回sendrecv
  • heartbeat:可选,默认为10秒,如果不使用心跳,可以使用一个很大的值或负数。上、下行心跳可以不对称。

服务端可以回复其它错误消息或事件并主动断开连接(延迟一点断开以便客户端能收全错误消息),也可以等客户端主动断开。

媒体信息

客户端在收到200后即开始发送媒体。

媒体按前面约定的格式发送,如果有多个声道则声道以interleaved方式排列。

本协议版本不支持视频,所有二进制消息全认为是音频。

音频数据均为实时数据,不带时间戳。

心跳

如果一方不发媒体或任何数据,应该周期性地发心跳,具体心跳间隔由双方协商(默认为10秒)。

{
    "jsonrpc": "2.0",
    "method": "ping"
}

心跳是一个事件,不需要响应。任何一方在超时没有收到任何消息后,可以主动断开。

ASR返回消息

在连续识别中,如果ASR识别到结果,直接以事件形式返回(不带id)。

{
    "jsonrpc": "2.0",
    "method": "text",
    "params": {
        "confidence": 0.99,
        "text": "你好"
    }
}

如果ASR端有VAD,可以返回如下事件:

{
    "jsonrpc": "2.0",
    "method": "start_speaking",
    "params": {
    }
}
{
    "jsonrpc": "2.0",
    "method": "stop_speaking",
    "params": {
    }
}

这些事件通知仅用于“通知”目的,不影响主体逻辑。也就是说让对方了解现在的状态和进展,但如果这些消息缺失,不应该影响总体识别逻辑。

结束

两侧在结束前都可以发送{method: 'stop'}消息通知对方将要断开连接,也可以不发直接断开。

对话场景

如果ASR同时返回语音流,则XSwitch会向电话侧播放。一旦连接成功并返回200 OK后,就可以发语音流。语音流格式必须与上行格式相同。

如果ASR侧中断发语音流,则XSwitch会发静音流给话机。

ASR也可以通过JSON消息发送静音包,这样会节省一些流量:

{
    "jsonrpc": "2.0",
    "method": "silence",
    "params": {
        "ms": 200,
        "level": 400
    }
}
  • ms:静音包时长
  • level:整数,可选,0 ~ 100,舒适噪声音量,越大噪声越大。

ASR

ASR不返回识别结果

ASR不返回识别结果,识别结果返回给其它系统,或直接返回语音流。

如果服务端返回语音流,则audio需为sendrecv,否则,返回recvonly。如果不返回,则默认为inactive,客户端无法发送语音流,会导致连接断开。

ASR返回结果,连续识别

ASR如果返回结果,可以以事件形式返回,不影响当前媒体流收发。

ASR返回结果,断句识别

断句识别需要更多交互,可以参考文末阿里的协议实现。

ASR识别结果中带:{status: 'break'},表示识别结束了,客户端应该停止发语音,如果继续发将会被丢弃。

客户端再次发语音前,应该发送{method: 'resume'}事件(通知,不需要回复),并可以立即发送音频(这主要是为了减少交互延迟)。

如果客户端有VAD,则可以主动发暂停消息:{method: 'break'},并停止发语音。

TTS

TTS连接后的第一个start消息跟上面描述的相同,音频参数代表期望收到的媒体流格式。

TTS文本消息:

"params": {
    "text": "你好",
    "voice": "小红",
    "audio": "recvonly",
    "...": "其它参数"
}

其它具体参数由厂商定义。

在TTS场景中,服务端发送200 OK后,可以立即发送音频。

NLP

本协议也支持与NLP对接。start消息中可以没有音频参数。也可以携带任意其它参数。

  • uuid:本轮对话唯一ID
  • text:输入文本消息。

NLP对话支持在一个Socket上同时传多个Session,在这种场景下,请求和影响消息中,uuid都必须带,以区分是哪个Session。

中间的消息都以{method: 'text'}发送,对话结束后发{method: 'stop'}以便让服务端释放资源。

所有对话都是请求响应式,即所有请求消息都必须有id,参考以下对话流程:

id: 0, method: start, text: 你好...
id: 1, method: text, text: 你好...
id: 2, method: text, text: 你好...
id: 3, method: text, text: 你好...
id: 4, method: stop

返回消息必须包含text字段,其它字段由厂商定义:

{
    "jsonrpc": "2.0",
    "id": 0,
    "result": {
        "text": "你也好",
        "...": "其它参数"
    }
}

使用场景

AI对话机器人

AI直接参与对话,XSwitch和AI间直接双向传输语音流。

digraph G {
    node[shape=box]
	rankdir=LR

    phone[label="电话"]
    XSwitch[label="XSwitch"]
    ai[label="AI"]

    phone -> XSwitch
    XSwitch -> ai [dir="both",style=dotted,label="WebSocket"]
}

第三方对话机器人

msc {
    phone [label="电话"],
    XSwitch [label="XSwitch"],
    ASR [label="ASR"],
    NLP [label="NLP"],
    TTS [label="TTS"];

    phone => XSwitch [label = "语音:你好"];
    XSwitch => ASR [label = "WebSocket(语音:你好)"];
    ASR >> XSwitch [label = "TEXT:你好"];
    XSwitch >> NLP [label = "TEXT:你好"];
    NLP >> XSwitch [label = "TEXT:你也好"];
    XSwitch >> TTS [label = "TEXT:你也好"];
    TTS => XSwitch [label = "语音:你也好"];
    XSwitch => phone [label = "语音:你也好"];
}

第三方对话机器人需要跟XSwitch对接使用Lua脚本或XSwitch API控制业务逻辑。

另一种场景是有Controller参与(用户自行实现业务逻辑),Controller可以直接对NLP:

msc {
    phone [label="电话"],
    XSwitch [label="XSwitch"],
    ASR [label="ASR"],
    ctrl [label="Controller"],
    NLP [label="NLP"],
    TTS [label="TTS"];

    phone => XSwitch [label = "语音:你好"];
    XSwitch => ASR [label = "WebSocket(语音:你好)"];
    ASR >> XSwitch [label = "TEXT:你好"];
    XSwitch >> ctrl [label = "TEXT:你好"];
    ctrl >> NLP [label = "TEXT:你好"];
    NLP >> ctrl [label = "TEXT:你也好"];
    ctrl :> XSwitch [label = "指令:播放(你也好)"];
    XSwitch >> TTS [label = "TEXT:你也好"];
    TTS => XSwitch [label = "语音:你也好"];
    XSwitch => phone [label = "语音:你也好"];
}

电话质检

只需要向ASR发送单向的语音流。

digraph G {
    node[shape=box]
	rankdir=LR

    phone[label="电话"]
    XSwitch[label="XSwitch"]
    ASR[label="ASR"]
    ext[label="分机"]
    act[label="写到数据库"]

    phone -> XSwitch
    XSwitch -> ext [style=dotted]
    XSwitch -> ASR [style=dotted, splines=polyline]
    ASR -> act [style=dotted]
}

这种场景在XSwtich侧可以使用录音方式实现,可以发送单双向语音流而不影响当前通话。

参考

本仓库mock/目录中有一个Node.js的实现参考。

其它

以上协议主要是描述单向交互的场景,如果复杂的交互场景建议直接使用阿里的协议:

https://help.aliyun.com/document_detail/324262.html