
接到“音频分割”这个需求的时候大多数人第一反应是“不就是按时间切一刀嘛”。真做起来会发现几个硬问题固定时长切十刀有八刀切在句子中间语音识别出来全是半句话按静音切参数设置不对时把停顿和呼吸声全当成了内容批量处理时文件一多格式、采样率、声道不统一脚本直接跑挂。这篇文章把长语音音频分割为短语音音频的 Python 实现讲清楚包括方案选型、静音检测的参数计算、完整代码实现、以及我踩过的坑适合做语音转写、语料库整理、音频数据集清洗的开发者参考。1. 方案设计先想清楚怎么切再写代码1.1 长语音分割到底在解决什么问题长语音分割不是单纯为了“把文件变小一点”它背后通常跟着三类真实需求。第一类是语音识别前的预处理。现在很多开源 ASR 模型对输入长度有上限比如 Whisper 默认按 30 秒一段做特征提取虽然它能处理长音频但会把一整段塞进模型里做分块一旦音频里有很长的静音段识别结果里会多出一堆无意义的标点或重复识别。提前把长语音切成干净短句识别准确率能明显提升后面做字幕对齐也更省事。第二类是数据集的清洗和标注。做 TTS 音色克隆、做说话人识别、做情感语音数据集标注人员需要的不是“第 3 分钟到第 60 分钟”而是“一句话一个文件”。我在整理电话客服语料的时候拿到过一批单条 40 分钟的录音人工切录了整整两天后来换成脚本十分钟全部搞定标注人员只需要对着几十个短文件打标签效率完全不一样。第三类是业务系统里的“短音频需求”。很多第三方语音评测、情绪识别 API 只接受 15 秒或 60 秒以内的音频平台侧又不能要求用户自己切这时候后台就需要一个自动分割模块把用户上传的长语音拆成合规的短语音。这个标题里提到的“长语音音频分割为短语音音频”本质上就是在解决以上问题。实现路径可以拆成四步读取音频、检测有效语音段、确定切割边界、导出新文件。难点在第二步和第三步但踩坑最多的地方往往是第一步和第四步后面代码部分我会逐个说。1.2 工具链选型为什么我用 Python pydub webrtcvad拿到这个需求可选的方案其实不少。FFmpeg 命令行可以直接按静音分割但它适合“一锤子买卖”参数写起来很痛苦要做批量、要动态调整阈值、要对接后续逻辑shell 脚本写多了就像在糊墙。C 或者 Rust 写分割性能最高但开发周期长对大多数业务场景来说是杀鸡用牛刀。Python 生态里最常用的几个库我列个对比库擅长场景注意点pydub快速处理常见音频格式静音分割开箱即用底层依赖 ffmpeg大文件内存占用高librosa音频分析、特征提取适合做能量/过零率计算只适合做科学计算导出音频不如 pydub 方便webrtcvad实时/近实时语音活动检测对噪声鲁棒性较好只接受 16kHz 单声道 16bit PCM原生 wave audioop轻量、无第三方依赖适合结构化简单 wav只支持 wav麻烦事要自己造轮子我的建议是如果是 wav 文件且对性能有要求优先用“wave 自定义能量检测”的组合因为 wav 文件头部信息简单PCM 数据可以直接读进内存做数值计算速度快还不用担心 ffmpeg 没装导致 pydub 启动报错。如果是 mp3、m4a、flac 这类压缩格式直接用 pydub它能把格式转换的活全部包了但前提是系统里装好 ffmpeg。如果音频里有明显的背景噪声、音乐底噪或者需要做人声和静音的精细区分那就必须上 webrtcvad。我做的电话录音分割里固定阈值能量检测根本压不住线路底噪和店员说话声换成 WebRTC 的 VAD 之后误切率直接降了一半以上。后面 3.2 会给完整代码。1.3 三种常见的分割策略在写代码之前得先定分割策略。我总结下来主流方案就三种。固定时长切分最简单也最粗暴。按 30 秒或 60 秒一刀切代码只要三行但缺点非常明显一句话会被切断语音识别的语义完整性完全没法保证。它只适用于“平台限定时长内容完整性不重要”的场景比如临时文件拆分。静音检测切分是目前应用最广的方案。原理是语音和语音之间总有停顿识别出这些停顿的位置在停顿处切割就能得到“一句一文件”的效果。普通话的自然语速下句间停顿通常有 300ms 到 800ms如果一段静音超过 500ms基本可以认定是一句话的结束。这个方案的难点在于阈值设置设置太小会把“嗯”“啊”等语气词也切出来设置太大又会把两句连读的话合成一段。模型辅助切分是近几年才普及的进阶方案。先用语音识别模型比如 Whisper把整段音频转写成带时间戳的文本再根据句号、问号等标点对应的起止时间来做切分。这个方案切出来的片段语义最完整因为它是“按含义切”不是“按能量切”。代价是耗时高、需要 GPU而且如果识别文本有错切割点也会跟着错。这一套我在 4.4 节里会讲到一个轻量替代方案。这三种策略不是互斥的。我实际项目里通常先用静音检测快速切成粗片段再对过长的粗片段用能量曲线或识别时间戳做二次切分分段准确率能做到 95% 以上性能也不会太差。2. 核心原理静音检测的参数与计算逻辑2.1 静音检测是怎么工作的静音检测说白了就是能量检测。一段 PCM 音频在电脑里是一个采样点序列每个点代表某个瞬间的声音振幅。没有人说话的时候振幅基本接近零有人说话的时候振幅会明显往上冲。通过滑动窗口计算这一小段音频的能量再和预设阈值比较就能区分“有声音”和“没有声音”。能量计算有几种方式最常用的是 RMS均方根。公式不复杂RMS sqrt( sum(x[i]^2) / N )其中 x[i] 是窗口内第 i 个采样点的振幅N 是窗口内采样点的个数。pydub 内部也是拿这个来做静音判断的只不过它帮你把窗口的滑过来了而已。但要注意能量阈值不是绝对值而是相对于满量程的分贝值。满量程是 0 dBFS也就是采样点振幅达到最大值的情况正常录音的语音能量一般在 -30 dBFS 到 -20 dBFS 之间。静音段也不是绝对的“无声”它会有一个底噪比如房间里空调声、电脑风扇声通常是 -50 dBFS 到 -40 dBFS。所以设置阈值时要留出余量比如设成 -45 dBFS这样既能跨过底噪又不会把正常语音切掉。2.2 四个关键参数的选择思路使用 pydub 的 split_on_silence 时核心参数就四个min_silence_len、silence_thresh、keep_silence、seek_step。这四个参数每一个都有讲究。min_silence_len 是最小静音长度单位是毫秒。它的含义是“当检测到至少这么久没有声音才认为这里是一个切割点”。设置 300ms 会切得非常碎因为语气词和短暂停顿都会被当作断点设置 800ms 又会把两句话粘在一起。我处理普通话对话录音时常用 500ms如果是语速慢的访谈、播客建议 600ms 到 700ms。silence_thresh 是静音阈值的分贝值。常见范围是 -50 到 -35 dBFS。录音环境安静、底噪低阈值可以收紧到 -50环境嘈杂、底噪高阈值要放松到 -35 甚至 -30不然会把底噪当成语音永远找不到静音点最后只得到一个“一整个文件”。keep_silence 是保留静音时长。很多人不理解为什么要保留因为切割点如果刚好在说话声音落下的瞬间片段的开头和结尾会非常“秃”听起来有突兀感转写模型也容易认为音频没录完。保留 200ms 到 300ms 的静音听感会自然很多。数值不宜过大过大又会让前后片段混入大量静音降低有效信息密度。seek_step 是检测精度默认值是 10ms意思是每 10 毫秒做一次静音判断。这个值设得越小边界检测越精细但计算量成倍上涨。一般不需要改只有做实时处理、性能吃紧的时候才会调到 50ms 以上。下面这张表是我常用的参数组合可以直接抄场景min_silence_lensilence_threshkeep_silence安静环境普通话录音500-45200电话客服录音700-38250播客/访谈有BGM800-30300新闻发布会600-402002.3 为什么不能只靠静音阈值一条路走到黑静音阈值法最大的问题是它对“静音”的定义太宽泛。呼吸声、叹气、键盘敲击声、翻纸声这些声音在能量上远低于正常语音但又不完全等于数字静音。阈值设得紧它们会被当成语音保留导致切割点滞后阈值设得松它们会把一段连续语音从中间截断。更麻烦的是一个场景很常见的现象两个人对话一人说话时另一人在安静听这时候静音段检测还算正常但两人同时开口、或者有环境噪声持续存在整个音频就会变成一条几乎没有静音的能量带静音阈值法会直接失效。处理这类问题常规做法是先做降噪预处理比如用 noise reduce 库做频谱减除或者用 RNNoise 模型降噪。降噪后再做静音检测准确率会高很多。但降噪本身也有副作用轻度语音可能会被降掉听起来像“闷在罐子里”所以要不要降噪得根据实际音频质量权衡。另外还有一个小技巧不要用固定的 RMS 阈值而是用“自适应阈值”。先把整个音频的 RMS 曲线算出来找出最高的 20% 和最低的 20%取它们的中值作为阈值。这样做的好处是算法在不同响度环境下都能自动调整不需要开发人员手工调参。这段代码实现不难我会在进阶版里给出一个简化写法。3. Python 实现从基础版到可上线版本3.1 基础版用 pydub 快速完成分割先上个最直观的版本如果你的音频是安静环境录的普通话语音这个版本基本能直接跑。from pydub import AudioSegment from pydub.silence import split_on_silence # 读取音频文件支持 mp3/wav/m4a/flac 等需要系统安装 ffmpeg audio AudioSegment.from_file(long_audio.mp3, formatmp3) # 静音分割 segments split_on_silence( audio, min_silence_len500, # 静音达到 500ms 才切割 silence_thresh-45, # 低于 -45dBFS 视为静音 keep_silence200, # 切割后保留 200ms 静音 seek_step10 # 每 10ms 检测一次 ) print(f共切出 {len(segments)} 段) # 导出 for i, seg in enumerate(segments): if len(seg) 300: # 过滤掉小于 300ms 的噪声片段 continue seg.export(foutput_{i:03d}.wav, formatwav)这里有两个逻辑需要解释。第一过滤小于 300ms 的片段因为咳嗽声、点击声、短暂的爆音往往也被切割成一个极短片段它既不是完整语义也没有转写价值直接丢掉可以省不少事。第二文件名用:03d补零是为了避免后面排序的时候出现 “output_10” 排在 “output_2” 前面的问题。这段代码跑出来的片段切割边界基本是准的但有两个隐患一是读长文件时内存占用很大一段 1 小时 44.1kHz 的立体声 wavpcm 数据大概 600MB普通电脑勉强能撑住但再做切片复制操作就很容易内存溢出二是它对噪声敏感环境稍有嘈杂就会切出很多废片段。3.2 进阶版用 webrtcvad 处理复杂现实音频为了解决噪声环境下的切割准确率问题我后来改用了 WebRTC 的 VAD 方案。webrtcvad 是 Google WebRTC 里语音活动检测模块的 Python 封装它内部用高斯混合模型和语音/噪声分类器比单纯看能量阈值聪明得多。但它有一个硬性要求输入必须是 16kHz、单声道、16bit 的 PCM 数据。所以整个链路变成先用 pydub 做格式转换再喂给 webrtcvad 判断每帧是不是语音最后根据“连续语音帧”和“连续非语音帧”来切分。完整代码如下import collections import contextlib import wave import webrtcvad from pydub import AudioSegment def read_wave_from_pydub(audio, target_sample_rate16000): # 转成单声道 16bit PCM if audio.channels 1: audio audio.set_channels(1) if audio.frame_rate ! target_sample_rate: # 重采样到 16kwebrtcvad 只认这个采样率 audio audio.set_frame_rate(target_sample_rate) pcm_data audio.raw_data # 已经是 16bit little-endian return pcm_data, target_sample_rate def frame_generator(frame_duration_ms, audio, sample_rate): n int(sample_rate * (frame_duration_ms / 1000.0) * 2) # 2 bytes per sample offset 0 while offset n len(audio): yield audio[offset:offset n] offset n def vad_segmenter(pcm_data, sample_rate, frame_duration_ms30, padding_duration_ms300, threshold_ratio0.6): vad webrtcvad.Vad(2) # 聚合模式 0~3数字越大越敏感 frames list(frame_generator(frame_duration_ms, pcm_data, sample_rate)) num_padding_frames int(padding_duration_ms / frame_duration_ms) ring_buffer collections.deque(maxlennum_padding_frames) triggered False voiced_frames [] segments [] for frame in frames: is_speech vad.is_speech(frame, sample_rate) if not triggered: # 还没进入有声状态先积累缓冲 ring_buffer.append((frame, is_speech)) num_voiced len([f for f, speech in ring_buffer if speech]) if num_voiced threshold_ratio * ring_buffer.maxlen: # 缓冲里超过一半帧都是有声认为语音开始 triggered True for f, speech in ring_buffer: if speech: voiced_frames.append(f) ring_buffer.clear() else: # 已经进入有声状态继续累积语音帧 voiced_frames.append(frame) ring_buffer.append((frame, is_speech)) num_unvoiced len([f for f, speech in ring_buffer if not speech]) if num_unvoiced threshold_ratio * ring_buffer.maxlen: # 连续静音超过阈值认为这一段语音结束 triggered False segments.append(b.join(voiced_frames)) ring_buffer.clear() voiced_frames [] if voiced_frames: segments.append(b.join(voiced_frames)) return segments audio AudioSegment.from_file(noisy_recording.mp3) pcm_data, sample_rate read_wave_from_pydub(audio) raw_segments vad_segmenter(pcm_data, sample_rate) # 把字节流转回 pydub 可导出的 AudioSegment from pydub import AudioSegment as AS out_idx 0 for raw in raw_segments: seg AS( dataraw, sample_width2, # 16bit frame_rate16000, channels1 ) if len(seg) 300: continue seg.export(fvad_output_{out_idx:03d}.wav, formatwav) out_idx 1这个代码里有几个点值得展开讲。webrtcvad.Vad(2)的聚合模式0 是最不敏感只把非常明显的人声当语音3 最敏感会把轻微说话声也识别出来。电话录音我一般用 1 或 2麦克风近距离录音可以用 2远场录音或会议麦克风建议 3否则很多轻微语音会被漏掉。padding_duration_ms 300的作用是避免“话还没说完就被切断”。因为 VAD 是逐帧判断的如果一句话中间有个 100ms 的停顿可能被判成静音从而中断当前片段。缓冲区机制看连续 300ms 都是非语音才真正断句这样中间小停顿就不会导致误切。这个版本在电话客服录音上的表现我实测下来误切率能控制在 10% 以内而纯能量阈值法通常会到 30% 以上。代价是处理速度稍微慢一些但一小时的音频也就是几十秒的事完全可以接受。3.3 批量处理与文件输出规范项目最终要落地不能只切一个文件。批量处理时有两个细节特别容易踩坑。第一个是文件命名必须带“上下文信息”。我一开始用的命名是segment_001.wav后来客户反馈说“根本不知道这段是谁说的、在第几分钟”。改成20240601_lecture_seg_001_time_0032_0045.wav这种带来源和时间戳的名字后后续追溯、检索、对齐元数据都非常方便。时间戳可以从片段在原始音频里的偏移量算出来偏移量在切割过程中是很容易拿到的别偷懒不记录。第二个是目录结构要做好分层。建议按“原始文件目录 / 分割结果目录 / 日志目录”分开分割结果里每个源文件单独建子目录。我见过有人把所有切割文件全部丢进同一个文件夹切完 100 个文件直接多出 3000 个文件再想找到某个源文件对应的片段全靠猜。批量处理的骨架可以这么写import os from pathlib import Path from pydub import AudioSegment from pydub.silence import split_on_silence INPUT_DIR Path(./raw_audio) OUTPUT_DIR Path(./segments) LOG_FILE Path(./split_log.txt) for audio_path in sorted(INPUT_DIR.glob(*.mp3)): audio AudioSegment.from_file(audio_path) segments split_on_silence(audio, min_silence_len500, silence_thresh-45, keep_silence200) # 源文件对应的子目录 sub_dir OUTPUT_DIR / audio_path.stem sub_dir.mkdir(parentsTrue, exist_okTrue) time_offset 0 for idx, seg in enumerate(segments): start_ms time_offset end_ms time_offset len(seg) fname f{audio_path.stem}_seg_{idx:03d}_{start_ms//1000}_{end_ms//1000}.wav seg.export(sub_dir / fname, formatwav) with open(LOG_FILE, a, encodingutf-8) as f: f.write(f{audio_path.name}\t{fname}\t{start_ms}\t{end_ms}\n) time_offset end_mstime_offset的更新逻辑要小心pydub 的 split_on_silence 返回的片段是“裁剪后”的本身不包含它在原文件中的位置信息所以需要通过累积 len(seg) 来推算偏移量。如果你的原始音频是变长切割比如中间有删除或拼接这个逻辑就需要再调整但对我们这种“从头到尾顺序切”的场景是够用的。4. 实操中的坑与排查技巧4.1 格式、采样率、声道数80% 的报错都出在这音频处理是个格式地狱。我接手的很多录音文件表面上是 wav实际封装格式五花八门有的 wav 是 32bit float有的 mp3 的码率是 32kbps有的 m4a 带有多个音轨。这些差异几乎都能在 pydub 加载阶段引发一次报错或者静默异常。最常见的一个坑是webrtcvad 只支持 16bit、16kHz、单声道如果你直接从 pydub 拿raw_data却不重采样VAD 会一直返回 False不管怎么调聚合模式都没效果最后所有音频被切成零段。这不是 bug是输入格式不满足要求。遇到这种情况我建议在读取音频后立刻打印音频信息print(audio.frame_rate, audio.channels, audio.sample_width)先确认格式再往下走能省下半小时排查时间。第二个坑是缺失 ffmpeg。pydub 读取 mp3、m4a 等压缩格式时底层走的是 ffmpeg 命令如果系统没装 ffmpeg程序会报FileNotFoundError。Windows 上还要注意 ffmpeg 是否在 PATH 环境变量里。Linux 服务器可以用apt install ffmpegmacOS 用brew install ffmpegWindows 用户建议把 ffmpeg 解压目录直接添加进 PATH。第三个坑是“输出格式不匹配输入格式”。比如你输入的是 44.1kHz 立体声 wav切的片段默认也是 44.1kHz 立体声但很多语音相关 API 只接受 16kHz 单声道。分割的同时顺便做一次统一转换比之后逐个转格式要高效得多。这个转换在导出前加一句即可seg seg.set_frame_rate(16000).set_channels(1)4.2 静音阈值调参的实战经验阈值怎么调是这个项目里最玄学也最核心的部分。给一个粗暴但好上手的流程先看波形。如果你用 Audacity 或 Praat 打开音频能看到波形图上语音段和静音段的界限其实非常明显。把鼠标放到静音段看软件左下角显示的 RMS 值是多少那个值的 dB 数就是你要设置的阈值的直接参考。假设显示 -52 dB那把 silence_thresh 设成 -45 或 -42 就合适因为要留出一点余量防止底噪变化。如果没法用软件看波形也有个经验法先把阈值设成 -50跑一遍看输出片段数量。如果片段数量特别多、很多是一个词一个词的碎片说明阈值太松把静音检测到了呼气声调成 -42。如果片段数量特别少、长段落都粘连在一起说明阈值太紧把正常语音都当成静音了调成 -55。来回调两三次基本能找到合适的点。这里还有一个细节min_silence_len 的大小会对最终片段的有效性产生很大的影响。一次线上事故我设置了 min_silence_len300结果语音片段被切成了平均 2 秒的碎片转写模型把每段都当成一句“你好”或“嗯”后来改成 600ms碎片明显减少平均句长变成 6 秒转写质量大幅提升。所以建议 min_silence_len 起步就用 500ms再根据实际语速微调。4.3 大文件的内存与性能优化处理 1 小时以上的音频时pydub 的整文件加载方式可能会撑爆内存。我的经验是不要怕“切分”本身会慢最怕的是内存被打满后系统触发 swap整个进程卡死。解决办法有两个。一个是“分块读取”用 pydub 的audio[start_ms:end_ms]切片方式虽然也是读整个文件但配合 ffmpeg 的-ss参数可以直接从文件指定位置读取避免载入全部数据。这个做法需要自己维护切割点的循环代码复杂度会上升。另一个思路是处理泛音频时用librosa或soundfile按帧流式读取。我刚做过一个 3 小时会议音频的分割用 soundfile 的 blocks 接口每次读 5 秒配合能量检测判断切割点内存占用不超过 200MB。这个方案适合 wav/flac 等非压缩格式mp3 的话还需要先解码成 pcm 再流式读取比较绕但效果是肉眼可见的提升。性能调优还有一个容易忽略的点seek_step。pydub 默认是 10ms如果你觉得检测速度慢可以把它调成 30ms切割边界精度会差一点点但处理速度能提高两倍。对大多数不是做“帧级精准切割”的场景来说30ms 完全够用。4.4 常见问题速查表问题可能原因解决方案分割结果为空VAD 输入采样率非 16k / 非单声道检查 frame_rate、channels先重采样转换片段数量爆炸silence_thresh 设置过松阈值太高调低阈值比如从 -40 调到 -50长段落切割失败静音太短或环境噪声大增大 min_silence_len或先做降噪再切割片段开头/结尾有爆音切割点落在波形高峰处增加 keep_silence把切割点移到静音区内输出文件顺序混乱文件名没有补零使用{idx:04d}格式命名报错 FileNotFoundError缺少 ffmpeg安装 ffmpeg 并加入 PATH内存溢出pydub 整文件加载改用分块读取或流式处理mp3 切完内容对不上时长码率固定/可变帧造成时间轴漂移统一转成 wav 后再处理这里最后单独提醒一句如果你做的是“语义完整切割”也就是希望每个片段尽可能是一句完整的话我强烈建议先用 Whisper 或 FunASR 这类带时间戳的识别模型过一遍直接把每句话的起止时间导出来再按时间戳裁剪。这个方案我在生产环境里实践过后确实比纯 VAD 更精准尤其是遇到大段连续口语、没有明显停顿的语音时静音检测永远切不好但模型能根据语义判断句子边界这是静音检测做不到的事。不过模型方案也不是万能的。识别时间戳如果偏移了几百毫秒裁出来的片段会把上一个字的尾音吃掉听起来像结巴。针对这个问题我后来加了一个“边界回退”逻辑把时间戳前后各 200ms 的范围作为候选区在候选区内找能量最低的帧作为实际切割点这样既能保留模型语义边界又能避开音频波形上的高峰听感和识别率都均衡了不少。最后再分享一个小技巧。无论用哪种方式分割上传到业务系统前建议都做一个“时长分布统计”。把切出来的所有片段按秒数分成 0-3 秒、3-10 秒、10 秒以上三档看看比例如果 0-3 秒的片段占比特别高说明参数设置太激进或者音频本身噪声太多这时候不要急着改代码先把原始音频试听几段往往比调参更高效。音频分割这个需求代码只是工具耳朵才是最终评审。