FSMN VAD JSON结果解析:语音片段时间戳提取代码示例
1. 什么是FSMN VAD?一句话说清它的用处
FSMN VAD是阿里达摩院FunASR项目中开源的语音活动检测(Voice Activity Detection)模型,核心任务就一个:从一段音频里准确找出“哪里有说话”,并标出每段语音的起止时间。
它不是语音识别(ASR),不转文字;也不是声纹识别,不认人。它只做一件事——当音频流进来时,像一个专注的监听员,实时判断“此刻是不是人在说话”。输出结果就是一组带毫秒级精度的时间戳,比如{"start": 70, "end": 2340},告诉你:第0.07秒开始有人说话,到第2.34秒结束。
这个能力看似简单,却是语音处理流水线的“守门人”:会议录音切分、电话质检、语音唤醒前的静音过滤、甚至AI配音前的语句对齐,都依赖它给出干净、准确的语音片段边界。而科哥基于FunASR构建的WebUI,把这项能力变成了点点鼠标就能用的工具,让非工程师也能轻松拿到结构化的时间戳数据。
2. 为什么需要解析JSON结果?直接看不行吗?
WebUI界面上显示的JSON结果确实一目了然:
[ {"start": 70, "end": 2340, "confidence": 1.0}, {"start": 2590, "end": 5180, "confidence": 1.0} ]但“看得懂”不等于“能用”。真实工作场景中,你往往需要:
- 把这些时间戳喂给下一个模型(比如ASR),就得转换成它要求的格式(如秒单位、或FFmpeg可读的
-ss 0.07 -t 2.27参数); - 统计总语音时长、平均片段长度、静音间隙分布,用于质量评估;
- 导出为SRT字幕文件,让视频自动配上“语音发生时段”的标记;
- 和原始音频对齐,在Audacity里批量剪掉静音段;
- 写进数据库,和业务系统打通,比如“通话时长>30秒才计入有效服务”。
这些操作,靠肉眼复制粘贴JSON是不现实的。你需要一段稳定、可复用、带错误处理的解析代码,把原始JSON变成你程序里能直接调用的Python列表、Pandas DataFrame,或者导出为CSV/SRT等标准格式。下面我们就从最基础的解析开始,一步步给出实用代码。
3. 基础解析:从JSON字符串到Python对象
FSMN VAD WebUI返回的是标准JSON数组,每个元素是一个字典。解析它只需一行核心代码,但加上健壮性检查后,推荐这样写:
3.1 安全加载JSON(处理空结果、格式错误)
import json from typing import List, Dict, Optional def parse_vad_json(json_str: str) -> List[Dict[str, float]]: """ 安全解析FSMN VAD的JSON输出 Args: json_str: 从WebUI复制的JSON字符串,或API返回的响应体 Returns: 包含字典的列表,每个字典含 'start', 'end', 'confidence' 键 时间单位为毫秒(ms) Raises: json.JSONDecodeError: 当输入不是合法JSON时 ValueError: 当JSON结构不符合预期(如缺少必要字段) """ try: data = json.loads(json_str.strip()) except json.JSONDecodeError as e: raise ValueError(f"JSON解析失败:输入不是合法JSON。错误详情:{e}") if not isinstance(data, list): raise ValueError(f"JSON根节点必须是数组,当前类型为:{type(data).__name__}") # 遍历每个片段,做字段校验 parsed_segments = [] for i, seg in enumerate(data): if not isinstance(seg, dict): raise ValueError(f"第{i+1}个片段不是字典类型,实际类型:{type(seg).__name__}") # 检查必需字段 for field in ["start", "end"]: if field not in seg: raise ValueError(f"第{i+1}个片段缺少必需字段:'{field}'") if not isinstance(seg[field], (int, float)): raise ValueError(f"第{i+1}个片段的'{field}'字段必须是数字,当前值:{seg[field]}") # confidence是可选字段,提供默认值1.0 confidence = seg.get("confidence", 1.0) if not isinstance(confidence, (int, float)) or not (0.0 <= confidence <= 1.0): raise ValueError(f"第{i+1}个片段的'confidence'应在0.0-1.0间,当前值:{confidence}") parsed_segments.append({ "start": float(seg["start"]), "end": float(seg["end"]), "confidence": float(confidence) }) return parsed_segments # 使用示例 raw_json = '''[ {"start": 70, "end": 2340, "confidence": 1.0}, {"start": 2590, "end": 5180, "confidence": 0.98} ]''' try: segments = parse_vad_json(raw_json) print(f"成功解析 {len(segments)} 个语音片段") for i, seg in enumerate(segments, 1): print(f"片段 {i}: {seg['start']:.0f}ms → {seg['end']:.0f}ms (置信度 {seg['confidence']:.2f})") except ValueError as e: print(f"解析出错:{e}")这段代码的关键在于防御性编程:它不假设输入永远正确。当遇到空字符串、格式错乱、字段缺失或类型错误时,会抛出清晰的错误信息,而不是让程序在后续步骤崩溃。这是工程落地的第一道防线。
4. 实用转换:毫秒→秒、计算时长、生成FFmpeg命令
拿到segments列表后,下一步通常是做单位转换和衍生计算。以下是几个高频需求的代码封装:
4.1 毫秒转秒 + 计算持续时间
def convert_to_seconds(segments: List[Dict[str, float]]) -> List[Dict[str, float]]: """将所有时间戳从毫秒转换为秒,并添加duration字段""" result = [] for seg in segments: start_sec = seg["start"] / 1000.0 end_sec = seg["end"] / 1000.0 duration_sec = end_sec - start_sec result.append({ "start_sec": round(start_sec, 3), "end_sec": round(end_sec, 3), "duration_sec": round(duration_sec, 3), "confidence": seg["confidence"] }) return result # 示例使用 seconds_segments = convert_to_seconds(segments) print("\n转换为秒单位:") for seg in seconds_segments: print(f"开始:{seg['start_sec']}s | 结束:{seg['end_sec']}s | 时长:{seg['duration_sec']}s")4.2 生成FFmpeg剪辑命令(批量提取语音片段)
如果你要把每个语音片段单独保存为.wav文件,FFmpeg是最常用工具。以下函数自动生成命令行:
def generate_ffmpeg_commands( audio_path: str, segments: List[Dict[str, float]], output_prefix: str = "speech_" ) -> List[str]: """ 为每个语音片段生成FFmpeg剪辑命令 Args: audio_path: 原始音频文件路径(如 "meeting.wav") segments: 解析后的片段列表 output_prefix: 输出文件名前缀 Returns: FFmpeg命令字符串列表 """ commands = [] for i, seg in enumerate(segments, 1): start_ms = seg["start"] duration_ms = seg["end"] - seg["start"] # FFmpeg要求秒单位,且支持小数 start_sec = start_ms / 1000.0 duration_sec = duration_ms / 1000.0 output_file = f"{output_prefix}{i:03d}.wav" cmd = f'ffmpeg -y -i "{audio_path}" -ss {start_sec:.3f} -t {duration_sec:.3f} -c:a copy "{output_file}"' commands.append(cmd) return commands # 示例:为meeting.wav生成剪辑命令 ffmpeg_cmds = generate_ffmpeg_commands("meeting.wav", segments) print("\nFFmpeg剪辑命令:") for cmd in ffmpeg_cmds: print(cmd)运行后你会得到类似这样的命令:
ffmpeg -y -i "meeting.wav" -ss 0.070 -t 2.270 -c:a copy "speech_001.wav" ffmpeg -y -i "meeting.wav" -ss 2.590 -t 2.590 -c:a copy "speech_002.wav"复制粘贴到终端即可批量执行,无需手动计算。
5. 进阶应用:导出为SRT字幕格式(兼容视频编辑)
SRT是通用字幕格式,很多视频编辑软件(Premiere、Final Cut)和播放器都支持。我们可以把语音片段当作“字幕出现时段”,生成一个只有时间轴、没有文字的SRT文件,用于视觉标记:
def segments_to_srt(segments: List[Dict[str, float]], output_path: str): """ 将语音片段导出为SRT格式文件(仅时间轴,无文字) SRT格式说明: 序号 开始时间 --> 结束时间 (空行) 时间格式:HH:MM:SS,mmm(毫秒用逗号分隔) """ def ms_to_srt_time(ms: float) -> str: total_seconds = int(ms // 1000) milliseconds = int(ms % 1000) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" with open(output_path, "w", encoding="utf-8") as f: for i, seg in enumerate(segments, 1): start_time = ms_to_srt_time(seg["start"]) end_time = ms_to_srt_time(seg["end"]) f.write(f"{i}\n") f.write(f"{start_time} --> {end_time}\n") f.write("\n") # SRT要求空行分隔 print(f"SRT文件已保存至:{output_path}") # 示例:导出为 speech_segments.srt segments_to_srt(segments, "speech_segments.srt")生成的speech_segments.srt内容如下:
1 00:00:00,070 --> 00:00:02,340 2 00:00:02,590 --> 00:00:05,180导入视频编辑软件后,就能看到一条条绿色时间条精准覆盖在语音发生的区域,极大提升人工审核效率。
6. 工程化建议:如何集成到你的项目中
以上代码都是独立函数,要真正融入生产环境,还需考虑三点:
6.1 错误处理与日志记录
不要让一次解析失败导致整个流程中断。在关键调用处加try/except,并记录日志:
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def safe_parse_and_process(json_str: str, audio_path: str): try: segments = parse_vad_json(json_str) logger.info(f"成功解析 {len(segments)} 个片段") # 后续处理... return segments except ValueError as e: logger.error(f"VAD JSON解析失败:{e}") return []6.2 批量处理封装
如果要处理上百个音频,别写循环调用WebUI。直接调用其后端API(通常为Gradio的/run接口),用requests批量提交:
import requests import time def batch_process_audio(file_paths: List[str], api_url: str = "http://localhost:7860/run"): results = {} for path in file_paths: with open(path, "rb") as f: files = {"file": f} # 模拟WebUI表单提交(具体字段名需查看Gradio API文档) data = {"fn_index": 0, "data": [None, 800, 0.6]} response = requests.post(api_url, files=files, data=data) if response.status_code == 200: # 解析response.json()中的result字段 results[path] = response.json().get("result", []) else: results[path] = {"error": f"HTTP {response.status_code}"} time.sleep(0.1) # 避免请求过密 return results6.3 参数配置化
把尾部静音阈值、语音-噪声阈值等参数从硬编码中解耦,放在配置文件(config.yaml)里:
vad_params: max_end_silence_time: 1000 # 单位:毫秒 speech_noise_thres: 0.6 # 置信度阈值 audio_format: "wav" # 推荐格式代码中用PyYAML加载,方便不同场景(会议/电话/安静录音)快速切换。
7. 总结:你现在已经掌握了VAD结果处理的核心链路
回顾一下,我们从一个简单的JSON字符串出发,完成了整条数据处理链路:
- 解析层:用健壮的
parse_vad_json()把原始JSON变成可信的Python对象; - 转换层:毫秒转秒、计算时长、生成FFmpeg命令,让数据立刻可用;
- 导出层:SRT格式输出,无缝对接视频工作流;
- 工程层:错误处理、批量调用、配置管理,确保代码能在真实项目中长期运行。
FSMN VAD本身是一个轻量但精准的模型,而它的价值,恰恰体现在下游这些“不起眼”的解析和转换工作中。当你能稳定、高效地把{"start": 70, "end": 2340}变成一段可剪辑、可分析、可可视化的数据时,语音处理的自动化大门才算真正打开。
下一步,你可以尝试把这些函数封装成命令行工具(用argparse),或者做成Jupyter Notebook模板,分享给团队里的其他成员。技术的价值,永远在于它被多少人用起来、解决多少实际问题。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。