当前位置: 首页 > news >正文

基于FreeSWITCH构建开源自动通话录音系统:从架构到实战

1. 项目概述:为什么我们需要一个自动化的通话录音方案?

在不少业务场景里,通话录音是一个刚需。比如,客服团队需要记录与客户的沟通细节,用于后续的质检和培训;自由职业者或小团队需要留存与客户的沟通凭证,避免后续的纠纷;甚至对于个人,有时也需要记录一些重要的电话会议或关键信息。然而,手动操作不仅繁琐,还容易遗漏。想象一下,你正在开车或者手头正忙,一个重要电话打进来,你既要接听,还要分心去找录音按钮,体验非常糟糕。

“自动接听 + 自动录音”这个组合,就是为了解决这个痛点而生。它意味着系统能够像一位不知疲倦的助理,在电话呼入时自动帮你接起来,并同时开启录音,全程无需你手动干预。等你有空了,再回头去听录音或者查看文字转写稿。这个需求听起来简单,但真要自己从头实现,涉及到电话线路、信令控制、媒体流处理、文件存储等一系列环节,门槛不低。

市面上当然有成熟的商业解决方案,但要么价格不菲,要么在数据隐私、功能定制上无法满足特定需求。因此,一个开源的、可自我掌控的实现方案,对于开发者、技术团队或是有定制化需求的企业来说,价值就凸显出来了。今天要聊的,就是如何利用开源技术栈,搭建一套属于你自己的自动化通话录音系统。我们将聚焦于一个经典且可行的技术组合,并深入每个环节的“为什么”和“怎么做”。

2. 核心架构与开源技术选型解析

要实现自动接听和录音,我们需要一个核心组件来处理电话信令(SIP协议)和媒体流(音频)。在开源世界里,FreeSWITCH是这方面当之无愧的王者。它是一个强大的软电话交换平台,功能极其丰富,我们只需要用到其冰山一角——作为一个SIP服务器(B2BUA)来接收来电,并执行我们设定的呼叫流程。

2.1 为什么是FreeSWITCH?

首先,它稳定且成熟,在全球范围内被广泛用于生产环境。其次,它的控制方式非常灵活,既可以通过内置的XML配置定义简单的IVR(交互式语音应答)菜单,也可以通过ESL(Event Socket Library)接口用外部程序(比如Python、Node.js)进行实时、精细化的控制。对于我们的“自动接听+录音”场景,初期用XML配置就能快速跑通,后期若需要复杂逻辑(比如根据来电号码决定是否接听),可以无缝升级到ESL控制。

除了FreeSWITCH,另一个常见选择是Asterisk。两者都是优秀的开源PBX。FreeSWITCH在某些方面,如高性能和并发处理上更有优势,且其模块化设计和API对开发者更友好。对于我们这个以“自动化控制”和“集成”为重点的项目,FreeSWITCH通常是更顺手的选择。

2.2 媒体流处理与录音生成

FreeSWITCH接听电话后,会建立音频媒体流。录音功能本质就是将这段双向的音频流(你说话的声音和对方说话的声音)混合后,保存为音频文件。FreeSWITCH内置了强大的mod_dptools: record_sessionmod_dptools: record应用,可以轻松实现单声道或立体声录音。

但光有录音文件还不够。我们可能还需要:

  1. 录音文件管理:自动按日期、主叫号码等规则命名和存储。
  2. 语音转文字(ASR):将录音内容转为文本,便于搜索和快速浏览。
  3. 事件监听与业务集成:录音开始、结束、生成文件时,需要通知我们的业务系统。

这就需要第二个核心组件:一个自定义的应用服务。我们可以用Python(Flask/Django)、Node.js、Go等任何你熟悉的语言来编写。这个服务通过ESL监听FreeSWITCH的事件,当有来电被接听时,触发录音指令;录音结束后,获取文件路径,并调用第三方ASR接口(如阿里云、腾讯云、或开源的Vosk)进行转写,最后将元数据(来电号码、时间、录音文件URL、转写文本)存入数据库或推送到消息队列。

2.3 整体数据流梳理

整个方案的数据流可以清晰地分为几个阶段:

  1. 呼入阶段:外部电话(PSTN或手机网络)通过运营商或SIP中继,将呼叫请求(INVITE)发送到你的FreeSWITCH服务器。
  2. 信令处理与自动接听阶段:FreeSWITCH根据预设的拨号计划(Dialplan),匹配到来电,并执行“answer”(应答)动作。这就是“自动接听”。
  3. 媒体建立与录音启动阶段:通话接通后,FreeSWITCH在拨号计划中继续执行“record”应用,开始将通话双方的音频写入指定路径的文件。
  4. 事件通知与后处理阶段:录音结束时,FreeSWITCH通过ESL向我们的应用服务发送包含文件路径的CHANNEL_EXECUTE_COMPLETE事件。应用服务捕获该事件,启动文件转码、ASR转写、元数据入库等异步任务。
  5. 存储与访问阶段:最终的录音文件(如WAV格式)和转写文本被存储起来(本地磁盘、对象存储如MinIO/S3),并通过一个简单的Web界面或API提供查询和播放功能。

这个架构清晰地将通信底层(FreeSWITCH)和业务逻辑(应用服务)解耦,使得两者可以独立扩展和维护。

3. FreeSWITCH核心配置详解

理论说完,我们进入实战。假设你已经在服务器上安装好了FreeSWITCH(安装过程略,官方文档很详细)。接下来,我们需要配置最关键的部分:拨号计划(Dialplan)。

3.1 拨号计划配置:实现自动接听与录音

FreeSWITCH的拨号计划决定了来电该如何被处理。我们需要编辑conf/dialplan/default.xml文件(或创建一个新的XML文件放在conf/dialplan/public/目录下)。

下面是一个最精简但功能完整的配置示例:

<context name="public"> <extension name="auto_answer_and_record"> <!-- 匹配所有来电 --> <condition field="destination_number" expression="^(\d+)$"> <!-- 1. 自动应答 --> <action application="answer"/> <!-- 等待100毫秒,确保媒体通道完全建立 --> <action application="sleep" data="100"/> <!-- 2. 播放提示音(可选),告知对方通话将被录音 --> <action application="playback" data="ivr/ivr-call_will_be_recorded.wav"/> <!-- 3. 开始录音 --> <!-- 关键参数解析: ${record_file}: 变量,用于存储录音文件路径。 /var/lib/freeswitch/recordings/: 录音文件存储根目录。 ${strftime(%Y-%m-%d-%H-%M-%S)}: 按年月日时分秒生成时间戳。 ${caller_id_number}: 主叫号码。 ${destination_number}: 被叫号码(即你的FS号码)。 .wav: 文件格式。也可以使用.mp3,但需要编译时包含mod_shout。 ${record_sample_rate=8000}: 设置采样率为8kHz,节省空间。 ${record_stereo=false}: 单声道录音,足够清晰且文件更小。 --> <action application="set" data="record_file=/var/lib/freeswitch/recordings/${strftime(%Y-%m-%d-%H-%M-%S)}_${caller_id_number}_to_${destination_number}.wav"/> <action application="set" data="record_sample_rate=8000"/> <action application="set" data="record_stereo=false"/> <action application="record_session" data="${record_file}"/> <!-- 4. 挂断后,执行一个自定义的脚本(用于通知外部服务) --> <!-- 这里使用`httapi`或通过ESL事件处理是更优解,本例为演示简单逻辑 --> <action application="log" data="INFO Recording saved: ${record_file}"/> </condition> </extension> </context>

配置要点与避坑指南:

  • 存储路径权限:确保FreeSWITCH运行用户(通常是freeswitchwww-data)对/var/lib/freeswitch/recordings/目录有读写权限。
  • 文件名规范:使用时间戳和号码组合命名,能有效避免重名,并便于后续检索。注意文件名中避免使用特殊字符。
  • 录音格式:WAV格式保真度最高,但文件大。MP3格式文件小,但需要额外的编码库(如lame)和支持模块(如mod_shout)。生产环境建议先录为WAV,再由后端服务统一转码为MP3以节省存储。
  • 提示音:播放录音提示音不仅是良好的用户体验,在某些地区也是法律要求。确保提示音文件(如.wav格式)已放置在FreeSWITCH的语音文件目录(如/usr/share/freeswitch/sounds/)下。

3.2 通过ESL实现更精细的控制

上述XML配置实现了基础功能,但不够灵活。比如,我们想只对特定号码录音,或者录音完成后立即将文件信息POST到我们的Webhook。这时就需要用到ESL。

我们可以写一个Python脚本,连接到FreeSWITCH的ESL端口(默认8021),订阅相关事件,并控制呼叫。

# record_agent.py import ESL import sys import datetime # 连接到FreeSWITCH ESL con = ESL.ESLconnection('localhost', '8021', 'ClueCon') if not con.connected(): print("无法连接到FreeSWITCH ESL") sys.exit(1) # 订阅所有事件,我们只过滤需要的 con.events('plain', 'all') while True: e = con.recvEvent() if e: event_name = e.getHeader('Event-Name') # 监听电话应答事件 if event_name == 'CHANNEL_ANSWER': uuid = e.getHeader('Unique-ID') caller_id = e.getHeader('Caller-Caller-ID-Number') dest_number = e.getHeader('Caller-Destination-Number') print(f"[{datetime.datetime.now()}] 来电接听: {caller_id} -> {dest_number}, UUID: {uuid}") # 构建录音文件名 record_file = f'/var/lib/freeswitch/recordings/{datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_{caller_id}_to_{dest_number}.wav' # 通过ESL命令启动录音 # `uuid_record` API 是针对特定通道的录音 api_cmd = f'uuid_record {uuid} start {record_file}' result = con.api(api_cmd) print(f" 启动录音: {api_cmd}, 结果: {result.getBody()}") # 将录音文件路径与通话UUID关联存储(例如存入Redis),供后续事件使用 # redis_client.set(f'record:{uuid}', record_file) # 监听通话挂断事件 elif event_name == 'CHANNEL_HANGUP_COMPLETE': uuid = e.getHeader('Unique-ID') # 从Redis获取录音文件路径 # record_file = redis_client.get(f'record:{uuid}') # if record_file: # print(f"通话 {uuid} 结束,录音文件: {record_file}") # # 这里可以触发后续处理:调用转写API、入库等 # # process_recording(record_file, uuid) print(f"[{datetime.datetime.now()}] 通话挂断: UUID: {uuid}")

这个脚本只是一个骨架。在生产环境中,你需要:

  • 使用python-esl库或socket直接连接ESL。
  • 加入更健壮的错误处理和日志。
  • 使用消息队列(如Redis List/RabbitMQ)将“录音完成”事件异步传递给后处理Worker,避免阻塞ESL连接。
  • 考虑并发连接的管理。

注意:直接使用mod_event_socket并编写ESL客户端,赋予了最大的灵活性,但同时也增加了复杂性。对于简单场景,拨号计划中的record_session配合httapi模块(可以通过HTTP回调通知你的服务)可能是更轻量的选择。

4. 录音后处理服务搭建实战

录音文件生成后,躺在FreeSWITCH的服务器上。我们需要一个常驻的后处理服务来完成“收尾”工作。

4.1 服务职责与设计

这个服务(我们称之为Recording-Processor)的核心职责如下:

  1. 监听事件:从消息队列(或直接监听ESL)获取“录音文件已就绪”事件。
  2. 文件转码:将高保真的WAV文件转换为更节省空间的MP3或OPUS格式。
  3. 语音转文字:调用ASR服务,生成录音文本。
  4. 元数据提取与存储:从文件名或事件信息中提取主叫号、被叫号、时间、时长等,连同文件存储路径、转写文本一起存入数据库。
  5. 文件管理:将最终的录音文件转移到更持久的对象存储中,并清理FreeSWITCH服务器上的原始文件。

4.2 技术栈示例(Python + FastAPI + Celery)

这是一个非常流行的异步任务处理组合。

  • FastAPI:提供轻量级的Webhook端点,接收来自FreeSWITCH(通过httapi)或我们自己ESL客户端推送的事件。
  • Celery:分布式任务队列,用于处理耗时的转码和ASR任务。
  • Redis:作为Celery的消息代理(Broker)和结果后端(Result Backend),同时也可以用作临时缓存。
  • PostgreSQL/MySQL:存储录音元数据。
  • MinIO/S3:对象存储,用于存放最终的录音文件。

核心代码结构示例:

# app/main.py (FastAPI 应用) from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel import httpx from celery_app import process_recording_task app = FastAPI() class RecordingEvent(BaseModel): file_path: str # FreeSWITCH服务器上的原始文件路径 caller_id: str callee_id: str start_time: str duration: int @app.post("/webhook/recording-complete") async def handle_recording_complete(event: RecordingEvent, background_tasks: BackgroundTasks): """接收录音完成事件的Webhook端点""" # 立即响应FreeSWITCH,避免超时 # 将耗时任务放入后台队列 background_tasks.add_task(process_recording_task.delay, event.dict()) return {"status": "accepted"} # celery_app.py (Celery 应用) from celery import Celery import subprocess import os from vosk import Model, KaldiRecognizer import json import wave # 配置Celery,使用Redis作为Broker celery_app = Celery('tasks', broker='redis://localhost:6379/0') @celery_app.task def process_recording_task(event_data): file_path = event_data['file_path'] # 1. 转码 WAV -> MP3 (使用ffmpeg) mp3_path = file_path.replace('.wav', '.mp3') cmd = ['ffmpeg', '-i', file_path, '-codec:a', 'libmp3lame', '-qscale:a', '2', mp3_path] subprocess.run(cmd, check=True, capture_output=True) # 2. 语音转文字 (使用开源Vosk模型,轻量级) model_path = "/path/to/vosk-model-small-en-us-0.15" if not os.path.exists(model_path): # 如果没装Vosk,可以跳过,或调用云API text = "[ASR not configured]" else: wf = wave.open(file_path, "rb") model = Model(model_path) rec = KaldiRecognizer(model, wf.getframerate()) text = "" while True: data = wf.readframes(4000) if len(data) == 0: break if rec.AcceptWaveform(data): result = json.loads(rec.Result()) text += result.get("text", "") + " " result = json.loads(rec.FinalResult()) text += result.get("text", "") wf.close() # 3. 上传到对象存储 (以MinIO为例) from minio import Minio client = Minio('minio.example.com', access_key='your-key', secret_key='your-secret', secure=False) bucket_name = 'call-recordings' object_name = os.path.basename(mp3_path) client.fput_object(bucket_name, object_name, mp3_path) file_url = f"http://minio.example.com/{bucket_name}/{object_name}" # 4. 元数据入库 (使用SQLAlchemy等ORM) # db.add(RecordingRecord(caller=..., file_url=..., transcript=text, ...)) # db.commit() # 5. 清理临时文件 os.remove(file_path) os.remove(mp3_path) print(f"处理完成: {object_name}, 转写长度: {len(text)}")

4.3 关键环节的注意事项

  • ffmpeg依赖:确保服务器上安装了ffmpeg,并且FreeSWITCH有执行系统命令的权限。
  • ASR选择:Vosk是优秀的离线开源ASR,支持多种语言,但准确率尤其是对中文电话录音的识别,可能不如商业云服务(如阿里云短语音识别)。如果对准确率要求高,建议使用云服务API,但需考虑成本和网络延迟。
  • 文件权限与隔离:处理服务运行用户必须有权限读取FreeSWITCH生成的录音文件,并在处理后删除它们。考虑使用同一用户组或设置适当的ACL。
  • 任务幂等性:网络可能重传事件,确保你的任务处理是幂等的(即同一文件处理多次结果相同),避免重复转码和入库。
  • 存储策略:原始WAV文件在转码为MP3后应立即删除,以释放磁盘空间。MP3文件上传至对象存储后,本地副本也可考虑删除。制定一个生命周期策略,例如30天后将对象存储中的文件归档到更便宜的存储层。

5. 系统部署、调优与问题排查

将各个组件组装起来并稳定运行,还需要最后一步。

5.1 部署架构建议

对于中小规模使用,一个简单的单体服务器部署即可:

  • 服务器:一台配置尚可的云服务器(如4核8G内存,50G SSD存储)。
  • 服务部署
    • FreeSWITCH:直接运行在主机上。
    • Redis、PostgreSQL、MinIO:可以使用Docker Compose一键部署,便于管理。
    • Recording-Processor(FastAPI + Celery Worker):使用Gunicorn运行FastAPI,使用Supervisor或Systemd管理Celery Worker进程。

对于更高可用性和扩展性的需求,可以考虑微服务化部署:

  • FreeSWITCH集群:多台FS服务器通过SIP注册和负载均衡对外。
  • 独立的ESL事件中继服务:专门负责连接所有FS实例的ESL,将事件统一发布到Kafka集群。
  • 多个Recording-ProcessorWorker:从Kafka消费事件,横向扩展处理能力。
  • 高可用数据库和对象存储。

5.2 性能调优与监控

  • FreeSWITCH调优
    • conf/autoload_configs/switch.conf.xml:调整max-sessions(最大并发会话数)、sessions-per-second等参数。
    • conf/autoload_configs/sofia.conf.xml:优化SIP Profile的rtp-timer-nameenable-100rel等设置,适应你的网络环境。
    • 录音使用record_session而非record,前者性能更好。
  • 网络与安全
    • 将FreeSWITCH的SIP端口(5060/5061)和RTP端口范围(16384-32768)在防火墙中打开。
    • 务必配置强密码的SIP认证,避免被恶意注册攻击。
    • 考虑将FreeSWITCH置于Nginx等反向代理之后,仅暴露必要的Web接口(如ESL需要严格限制访问IP)。
  • 监控
    • FreeSWITCH:使用fs_cli命令或通过ESL监控show callsshow channels、系统负载。
    • 服务健康:为FastAPI服务添加/health端点,使用Prometheus+Grafana监控Celery队列长度、任务处理耗时、系统资源。

5.3 常见问题排查实录

在实际部署和运行中,你几乎一定会遇到下面这些问题:

问题1:电话能接通,但没有录音文件生成。

  • 排查思路
    1. 检查拨号计划:确认record_session应用被正确执行。在fs_cli中运行show channels查看通话的当前状态和应用栈。
    2. 检查文件路径和权限:这是最常见的原因。手动在FreeSWITCH运行用户下,尝试在目标目录创建文件,看是否成功。使用ls -la /var/lib/freeswitch/recordings/检查。
    3. 检查磁盘空间df -h
    4. 查看FreeSWITCH日志tail -f /var/log/freeswitch/freeswitch.log,搜索record_session相关的错误信息。日志级别可以临时调整为DEBUG以获得更详细信息。

问题2:录音文件有杂音、回音或声音很小。

  • 排查思路
    1. 回音(AEC):FreeSWITCH默认会启用软件回音消除。确保conf/autoload_configs/echo.conf.xml被加载。如果问题严重,可能需要调整conf/autoload_configs/audio.conf.xml中的echo-cancel参数,或考虑使用硬件设备。
    2. 音量问题:在拨号计划中,可以在录音前使用set命令调整输入/输出增益。例如:<action application="set" data="input_volume=+2.0"/>
    3. 编码问题:确认通话双方协商的音频编码(如G.711, G.729, OPUS)。某些编码在丢包时音质会恶化。在SIP Profile中优先使用OPUSPCMU(G.711u)这类更抗丢包或保真度更高的编码。

问题3:ESL连接不稳定,经常断开。

  • 排查思路
    1. 网络与防火墙:确保ESL端口(默认8021)在本地环回或内部网络是通畅的,且没有被防火墙阻断。
    2. 心跳与超时:你的ESL客户端需要实现心跳机制,定期发送api命令(如status)以保持连接活跃。同时,在FreeSWITCH的conf/autoload_configs/event_socket.conf.xml中,可以调整apply-inbound-acl(访问控制)和socket-timeout等参数。
    3. 错误处理与重连:在客户端代码中必须包含完善的异常捕获和断线重连逻辑。连接断开后应等待几秒后尝试重新连接。

问题4:后处理服务处理速度慢,队列堆积。

  • 排查思路
    1. 定位瓶颈:使用tophtop查看是CPU(转码、ASR)还是I/O(文件读写、网络上传)成为瓶颈。
    2. 并行化:增加Celery Worker的数量。可以使用celery -A celery_app worker --concurrency=4 -l info启动多个工作进程。
    3. 优化任务:将转码和ASR这两个最耗时的步骤拆分成独立的子任务,并行执行。或者,如果使用云ASR API,检查其是否有并发限制或延迟过高。
    4. 升级硬件:如果转码是瓶颈,考虑使用支持硬件加速(如Intel QSV或NVIDIA NVENC)的ffmpeg编译版本。

这套开源方案从零搭建到稳定运行,需要你具备一定的Linux运维、网络和编程知识。它的优势在于完全自主可控、成本极低(主要是服务器和云ASR费用),并且可以根据你的业务需求进行无限定制。一旦跑通,它就是一个7x24小时在线的、可靠的电话录音助理,默默为你记录下每一通重要的对话。

http://www.jsqmd.com/news/864677/

相关文章:

  • NotebookLM显著性≠统计显著性!资深NLP工程师首曝5大语义显著性替代指标(含GitHub开源评估框架)
  • TranslucentTB:让Windows任务栏实现完美透明化的专业解决方案
  • 3步掌握AI智能分层:Layerdivider让复杂插画秒变可编辑PSD图层
  • RK3562开发板Linux系统镜像制作全流程:从分区到烧录
  • Zotero SciHub插件完整教程:5分钟实现文献PDF自动下载
  • 对抗性深度强化学习在自动驾驶安全测试中的应用与实现
  • RT-Thread Vector软件包:嵌入式C语言动态数组容器的设计与实战
  • Creality Print:如何用开源切片软件解决3D打印的三大核心挑战
  • 骁龙875深度解析:三星5nm工艺与Cortex-X1架构如何重塑旗舰芯片
  • Nexus Mods App:重新定义游戏模组管理的智能协调器
  • 移动应用安全测试实战:三维一体模型与核心场景解析
  • 抖音无水印批量下载技术深度解析:douyin-downloader架构设计与实战指南
  • 思源宋体如何彻底改变你的设计工作流:7种字重深度解析与实战应用
  • 通俗理解XGBoost:从决策树、梯度提升到核心参数调优实战
  • 在ubuntu20.04上首次使用taotoken的完整入门指引
  • 告别抢票焦虑:用Python脚本轻松锁定心仪演出门票
  • Windows 11 LTSC版安装Microsoft Store:3分钟解锁完整应用生态
  • 思源宋体完全指南:5分钟掌握开源中文字体的专业应用
  • 工业物联网数据采集系统设计:基于英飞凌MCU与传感器的实战指南
  • StarRC寄生参数抽取:签收精度、Open/Short调试与APR校准实战
  • 如何高效管理你的B站内容收藏库?BilibiliDown使用全攻略
  • 纯硅可编程振荡器:原理、选型与替换石英晶振的实战指南
  • Claude Code用户如何配置Taotoken解决封号与Token不足痛点
  • 专业级LLM数据标注解决方案:Autolabel高效标注指南
  • 树莓派+PIR传感器DIY智能感应灯:从硬件连接到Python编程全解析
  • 有哪些AI写作辅助平台是真的懂学术语言,而不是通用套壳?
  • 京东自动抢购工具实战指南:Python脚本实现秒杀自动化
  • 树莓派PIR运动传感器智能灯光控制:从硬件连接到Python编程实战
  • BsMax插件终极指南:3步让3ds Max用户快速掌握Blender的完整解决方案
  • TrollInstallerX终极指南:如何在iOS 14-16.6.1设备上3秒完成TrollStore安装?[特殊字符]