FreeSwitch通过Websocket(流式双向语音)对接AI实时语音大模型技术方案(mod_ppy_aduio_stream)

FreeSwitch通过WebSocket对接AI实时语音大模型插件技术方案

1. 方案概述

基于FreeSWITCH的实时通信能力,通过WebSocket协议桥接AI大模型服务,实现低延迟、高并发的智能语音交互系统。支持双向语音流处理、实时ASR/TTS转换和动态业务指令执行。

1753095153158#pic_center)

有这么方面项目需要的可联系。https://cwn1.x3322.net:7777/down/0UgRahEtbPEa.so

类似技术参考:https://www.ddrj.com/callcenter/largemodel.html

2. 架构设计

graph LR A[FreeSWITCH] -->SIP/RTP B(WebSocket网关/SFU) B -->双向WebSocket C(AI Gateway) C -->HTTP/GRPC Stream D(大模型服务) D -->文本/控制指令 C C -->TTS音频/指令 B B -->RTP音频 A

3. 核心组件

组件 技术选型 核心功能
媒体网关 FreeSWITCH 1.10+ 处理SIP呼叫、RTP音频流、DTMF事件管理
协议桥接层 mod_websocket (ESL+自定义模块) 音频转WebSocket二进制流(支持OPUS/PCM)
AI网关 Node.js/Python (Tornado) 双向WS通信、ASR/TTS调度、会话状态机管理
大模型接口 GRPC Stream/HTTP2 Server 流式对话处理&指令生成(200ms级响应)
ASR/TTS引擎 阿里云/讯飞/DeepSeek RTS 实时语音<=>文本转换(<300ms延迟)
模型推理层 DeepSeek-V2/GLM-4 API 流式对话生成,支持SSML控制指令

4. 关键流程

4.1 语音输入流 (User → AI)

FreeSWITCH --(RTP)--> mod_websocket --(WS Binary/OPUS)--> AI网关 --(ASR API)--> 大模型

  • 数据封装
    json
    {
    "call_id": "call-123456",
    "seq": 1024,
    "is_final": false,
    "timestamp": 1721541687000,
    "payload": "BASE64_OPUS"
    }

4.2 AI响应流 (AI → User)

大模型 --(SSML指令)--> AI网关 --(WS控制消息)--> TTS服务 --(RTP)--> FreeSWITCH

  • 中断响应机制
    • DTMF #键触发barge-in事件
    • TTS首包到达时间<100ms

4.3 控制指令示例

json

// ASR识别结果

{"event":"asr_result", "text":"查余额", "confidence":0.95}

// TTS响应指令

{"event":"ai_response", "type":"tts", "audio":"chunk_123.opus"}

// 业务转移指令

{"event":"action", "command":"transfer:6001"}

5. 性能优化

  • 音频分片处理:80ms/帧(160采样@16kHz)
  • 双缓冲ASR策略:预加载静音语音模型加速首字响应
  • 动态抖动缓冲:网络延迟>150ms时自动补偿
  • 会话热插拔:通话保持时维持AI对话上下文
  • 熔断机制:模型响应>2s时转人工服务

6. 异常处理机制

故障场景 解决方案
WebSocket断连 10秒自动重连+20秒音频缓存
ASR识别冲突 基于时间戳的序列仲裁
模型响应超时 播放「正在思考」提示音
DTMF中断事件 立即停止TTS并清空队列
编码格式不匹配 OPUS/PCM/G.711动态切换
lua 复制代码
local cjson = require "dkjson"
local pts   = require "ppytools"

local ws_addr = "ws://127.0.0.1:20000"
ws_addr = "wss://127.0.0.1:12345"
--ws_addr = "wss://ai.xxx.com:12345"

local records_base = "/workspace/records"

local script_path = debug.getinfo(1, "S").source:sub(2)
local script_name = script_path:match("([^/\\]+)$") or "unknown"

local fs_api = freeswitch.API()

function fslog(msg, log_level)
    log_level = (log_level ~= nil) and log_level or "info"  -- 严格判断nil
    freeswitch.consoleLog(log_level, "[".. script_name .. "] "..msg)
end

function main()
    local session_lega = session
    local session_lega_uuid = session_lega:get_uuid()
    fslog(string.format("[START][%s]\n", session_lega_uuid))
    session_lega:answer()
    
    local datetime_dir, records_dir = pts.create_compact_date_dir(records_base)
    local caller_id_number = session_lega:getVariable("caller_id_number")
    local destination_number = session_lega:getVariable("destination_number")

    fslog(string.format("session_lega_uuid: %s , caller_id_number: %s , destination_number: %s\n", 
        session_lega_uuid, 
        caller_id_number, 
        destination_number
        ))

    --后台通话录音
    if records_dir ~= nil then
        -- 启用双声道录音
        session_lega:setVariable("RECORD_STEREO", "true")  
        local records_str = string.format("bgapi uuid_record %s start %s/%s.wav 1000 0 0", 
            session_lega_uuid, 
            records_dir, 
            session_lega_uuid
            )
        fslog(records_str)
        fs_api:executeString(records_str) 
        --CDR自定义变量
        session_lega:setVariable(
            "record_file_uri_path", 
            string.format("%s/%s.wav", datetime_dir, session_lega_uuid)
            )
    end
    
    --缺省将用户语音数据通过二进制方式发送到AI服务器。
    --如果这个参数设置为true,则通过JSON格式发送。和AI服务器发给FS的JSON格式一致
    session_lega:setVariable("STREAM_MESSAGE_SENDJSON", "true")
    
    local con = freeswitch.EventConsumer()
    con:bind("CUSTOM", "mod_audio_stream::json")
    con:bind("CUSTOM", "mod_audio_stream::connect")
    con:bind("CUSTOM", "mod_audio_stream::disconnect")
    con:bind("CUSTOM", "mod_audio_stream::error")
    
    local start_time = os.date("%Y-%m-%d %H:%M:%S", os.time())
    local metadata_obj = {
        type = "init",
        sid  = session_lega_uuid,
        phone_number = caller_id_number,
        timestamp = start_time
    }
    local metadata = cjson.encode(metadata_obj)
    fslog("metadata:" .. metadata)

    local result, err = fs_api:execute("uuid_audio_stream", 
        string.format("%s start %s mono 8k %s", 
            session_lega_uuid, 
            ws_addr, 
            metadata)
        )
    if result then
        fslog(string.format("Function executed successfully: %s\n", result), "notice")
    else
        fslog(string.format("Error executing function: %s\n", err), "err")
    end

    while session_lega:ready() do
        local event = con:pop()
        if event then
            local event_uuid = event:getHeader("Unique-ID")
            if event_uuid == session_lega_uuid then
                local event_name = event:getHeader("Event-Name")
                local event_sub = event:getHeader("Event-Subclass")
                local body = event:getBody()
                fslog(string.format("JSON executing function, Event-Subclass: %s, body: %s\n", event_sub, body))
                if event_sub == "mod_audio_stream::connect" then
                    --
                elseif event_sub == "mod_audio_stream::disconnect" then
                    break
                elseif event_sub == "mod_audio_stream::json" then
                    local data = cjson.decode(body)
                    if data.type == "sentence" and data.status == "start" then
                        local metadata_obj = {
                            type = "sentence_callback",
                            sentence_id  = data.sentence_id,
                            status = "play",
                            timestamp = os.date("%Y-%m-%d %H:%M:%S", os.time())
                        }
                        local metadata = cjson.encode(metadata_obj)
                        fslog("[send_text]metadata:" .. metadata)
                        fs_api:execute("uuid_audio_stream", string.format("%s send_text %s", session_lega_uuid, metadata))
                    end

                    if data.type == "streamText" then
                        if data.assistant then
                            fslog(data.assistant)
                        end
                    end

                    if data.toHuman then
                        break
                    elseif data.stop then
                        fslog("data stop", "err")
                    elseif data.clear then
                        fslog("data clear", "err")
                    end
                elseif event_sub == "mod_audio_stream::error" then
                    break
                else
                    --
                end
            end
        else
            if session_lega then
                session_lega:sleep(20)
            else
                break
            end
        end
    end

    --fs_api:execute("uuid_record", string.format("%s stop", session_lega_uuid))
    fslog(string.format("[END][%s]\n", session_lega_uuid))
end

main()
相关推荐
静心问道2 分钟前
DeepSpeed-FastGen:通过 MII 和 DeepSpeed-Inference 实现大语言模型的高吞吐文本生成
人工智能·模型加速·ai技术应用
雪碧聊技术21 分钟前
CPU(中央处理器)和GPU(图形处理器)的区别
人工智能·cpu·gpu·中央处理器·图形处理器
IMER SIMPLE23 分钟前
人工智能-python-OpenCV图像处理核心技术:梯度计算、边缘检测与轮廓分析详解
人工智能·python·opencv
Luis Li 的猫猫23 分钟前
光纤多波长解释
网络·人工智能
被放养的研究生23 分钟前
MMAction2-1.2.0文档
人工智能
程序员NEO25 分钟前
我只说需求,AI 全程托管,代码自己长出来了!
人工智能·后端
盼小辉丶1 小时前
图机器学习(22)——图机器学习技术应用
人工智能·机器学习·图机器学习
连合机器人1 小时前
酷暑来袭,科技如何让城市清凉又洁净?
人工智能·ai·有鹿机器人·连合直租·智能清洁专家
之之为知知1 小时前
Chromadb 1.0.15 索引全解析:从原理到实战的向量检索优化指南
人工智能·深度学习·机器学习·大模型·索引·向量数据库·chromadb
Ronin-Lotus1 小时前
深度学习篇---图像数据采集
人工智能·opencv·计算机视觉