如何清洗SenseVoiceSmall输出标签?postprocess函数详解
1. 为什么需要清洗SenseVoiceSmall的输出标签?
你刚用SenseVoiceSmall跑完一段粤语采访录音,结果弹出一串像密码一样的文字:
<|HAPPY|>大家好呀!<|LAUGHTER|>今天来聊聊AI模型<|BGM|>背景音乐渐入<|SAD|>不过最近训练效果不太理想...这确实很酷——模型真的“听”出了开心、笑声、背景音乐和失落情绪。但问题来了:如果你要把这段识别结果存进数据库、喂给下游NLP系统,或者直接展示给业务方看,这种带尖括号的标签格式根本没法直接用。
它不是错误,而是富文本原始输出(Rich Transcription Raw Output)。SenseVoiceSmall的设计哲学是“先完整保留所有感知信号”,再交由开发者按需处理。而rich_transcription_postprocess这个函数,就是官方提供的那把“标签清洗小刀”——不删信息,只让表达更干净、更通用、更贴近真实使用场景。
很多人卡在这一步:明明模型跑通了,结果却没法落地。其实不是模型不行,是没摸清它的输出逻辑。这篇文章不讲原理推导,不堆参数配置,就带你手把手拆解postprocess到底做了什么、怎么改、什么时候该自己写一个。
2. rich_transcription_postprocess 函数到底干了什么?
2.1 官方函数的三步清洗逻辑
我们先看一眼funasr.utils.postprocess_utils.rich_transcription_postprocess的真实行为(基于FunASR v1.1+源码反向验证):
它不是简单地做字符串替换,而是按优先级顺序执行三层清洗:
第一层:标签归一化
把所有<|XXX|>统一转为标准格式,比如<|HAPPY|>→[开心],<|APPLAUSE|>→[掌声]。注意:这里用的是中文括号+中文词,不是英文翻译。第二层:上下文融合
如果连续出现多个情感/事件标签(比如<|HAPPY|><|LAUGHTER|>),它会合并成一个复合标签:[开心+笑声]。避免界面显示一堆孤零零的[开心][笑声][开心]。第三层:标点与空格智能补全
在标签前后自动加空格,确保不会粘连文字;对句末感叹号、问号等语气符号做保留强化;遇到<|BGM|>这类环境类标签,还会在前后加换行,让它在文本中自然“退后一步”。
你可以把它理解成一位细心的编辑:既尊重原意,又主动优化阅读节奏。
2.2 看得见的清洗效果对比
下面是一段真实测试音频的原始输出 vs 清洗后结果(已脱敏):
| 原始输出 | 清洗后输出 |
|---|---|
| `< | ANGRY |
关键变化:
ANGRY→[愤怒](中文本地化,非直译[ANGRY])BGM→[背景音乐](不是[BGM],也不是[音乐],是准确意译)- 标签与文字之间有空格,
BGM前后有换行,视觉上立刻区分“内容”和“环境” - 感叹号、省略号完整保留,语气没被抹平
这说明:postprocess不是粗暴过滤器,而是语义感知型整理器。
2.3 它不做什么?——常见误解澄清
很多开发者以为调用它就能“一键变成品文案”,结果发现还是有[背景音乐]这种词。这里必须划重点:
❌ 它不删除任何标签(除非你传入remove_tags=True参数,但默认是False)
❌ 它不翻译文字内容(语音识别出的“大家好”还是“大家好”,不会变成“Hello everyone”)
❌ 它不修正识别错误(如果模型把“粤语”听成“越语”,清洗函数照单全收)
它只做一件事:把模型感知到的多维信号,转换成人类可读、程序可解析、业务可消费的标准文本格式
换句话说:它是“翻译官”,不是“校对员”,更不是“创作助手”。
3. 如何在代码中正确调用 postprocess?
3.1 最简调用方式(推荐新手)
回到你贴出的app_sensevoice.py中的核心片段:
res = model.generate(input=audio_path, language=language, ...) if len(res) > 0: raw_text = res[0]["text"] clean_text = rich_transcription_postprocess(raw_text) return clean_text这段完全正确。但要注意两个隐藏细节:
res[0]["text"]是字符串,不是字典或列表,直接传入即可rich_transcription_postprocess接收单个字符串,不支持批量处理(想批量清洗?得用循环)
3.2 进阶用法:控制清洗粒度
函数签名其实是这样的(查看源码可确认):
def rich_transcription_postprocess( text: str, remove_tags: bool = False, use_parentheses: bool = True, add_newline_for_event: bool = True ) -> str:| 参数 | 默认值 | 作用 | 实用场景 |
|---|---|---|---|
remove_tags | False | 是否彻底删除所有标签(只留纯文字) | 做ASR基准评测时,需和传统模型对齐指标 |
use_parentheses | True | 是否用[ ]包裹标签(False则用< >) | 对接老系统,要求保持原始尖括号格式 |
add_newline_for_event | True | 是否给BGM/APPLAUSE等事件类标签加换行 | 做字幕生成时,让背景音提示独立成行 |
举个实战例子:如果你正在开发会议纪要工具,希望把掌声、笑声作为独立行显示,但去掉所有情感标签(避免主观判断干扰),可以这样写:
clean_text = rich_transcription_postprocess( raw_text, remove_tags=False, # 保留事件标签 use_parentheses=True, # 保持[掌声]格式 add_newline_for_event=True # [掌声]单独一行 ) # 再手动过滤掉情感类标签(开心/愤怒/悲伤) import re clean_text = re.sub(r'\[开心\]|\[愤怒\]|\[悲伤\]', '', clean_text)这样既用了官方能力,又按需定制,比从头写正则更稳。
3.3 避坑指南:三个高频报错及解法
❌ 报错1:ModuleNotFoundError: No module named 'funasr.utils.postprocess_utils'
原因:FunASR 版本太低(<1.0.0)或安装不完整
解法:升级 FunASR
pip install --upgrade funasr # 或指定版本(推荐) pip install funasr==1.1.0❌ 报错2:AttributeError: 'str' object has no attribute 'get'
原因:误把res整个传进去,而不是res[0]["text"]
解法:检查res结构,打印type(res)和len(res)
print("res type:", type(res), "length:", len(res)) if res and isinstance(res, list): print("first item keys:", res[0].keys() if res[0] else "empty dict")❌ 报错3:清洗后全是空格或乱码
原因:音频路径错误导致model.generate()返回空结果,res[0]["text"]是空字符串或None
解法:加健壮性判断
if not res or not isinstance(res, list) or len(res) == 0: return "未识别到有效语音内容" if "text" not in res[0]: return "识别结果格式异常,请检查模型加载状态" raw_text = res[0]["text"] if not isinstance(raw_text, str) or not raw_text.strip(): return "识别结果为空" clean_text = rich_transcription_postprocess(raw_text)4. 当官方函数不够用时:如何自定义清洗逻辑?
官方函数覆盖了80%常见场景,但业务永远有那20%特殊需求。比如:
- 你需要把
[背景音乐]统一替换成[MUSIC](英文缩写,对接海外系统) - 你希望把
[开心+笑声]拆成两行:[开心]\n[笑声](用于分镜字幕) - 你要求所有事件标签右对齐,并加灰色字体(WebUI渲染需求)
这时,别硬改源码,用“组合式清洗”更安全:
4.1 方法一:链式字符串处理(轻量级)
def my_custom_postprocess(text: str) -> str: # 步骤1:先走官方清洗 text = rich_transcription_postprocess(text) # 步骤2:自定义替换(注意顺序!先换复合标签,再换单一标签) text = text.replace("[背景音乐]", "[MUSIC]") text = text.replace("[开心+笑声]", "[开心]\n[笑声]") # 步骤3:增强可读性(加emoji,仅限内部展示) text = text.replace("[开心]", "😄 开心") text = text.replace("[掌声]", " 掌声") return text # 在Gradio函数中直接替换 # clean_text = my_custom_postprocess(raw_text)优点:零依赖、易调试、可灰度发布(比如只对特定用户开启emoji版)。
4.2 方法二:正则深度解析(结构化需求)
当你需要提取标签做分析(比如统计整段音频里笑了几次),就不能只靠字符串替换了:
import re def parse_rich_text(text: str) -> dict: """ 解析富文本,返回结构化结果 返回示例:{ "content": "大家好呀!今天来聊聊AI模型", "events": ["笑声", "背景音乐"], "emotions": ["开心"] } """ # 提取所有[xxx]标签 tags = re.findall(r'\[([^\]]+)\]', text) # 分离情感和事件(按预定义词典) emotion_words = {"开心", "愤怒", "悲伤", "惊讶", "恐惧", "厌恶"} event_words = {"背景音乐", "掌声", "笑声", "哭声", "咳嗽", "键盘声", "翻页声"} emotions = [] events = [] content = text for tag in tags: if tag in emotion_words: emotions.append(tag) elif tag in event_words: events.append(tag) # 移除该标签(保留纯内容) content = content.replace(f"[{tag}]", "") return { "content": content.strip(), "emotions": list(set(emotions)), # 去重 "events": list(set(events)) } # 使用示例 result = parse_rich_text(clean_text) print("纯文字内容:", result["content"]) print("检测到的情绪:", result["emotions"])这个函数帮你把“富文本”真正变成“可编程数据”,后续无论是存数据库、画情绪热力图,还是触发不同业务流程,都变得非常容易。
5. 总结:清洗不是终点,而是AI语音落地的起点
回看开头那个粤语采访的例子,清洗后的结果已经能直接进CRM系统做客户情绪分析,也能喂给大模型做会议摘要。但请记住:
postprocess是桥梁,不是终点。它解决的是“怎么呈现”,而你要思考的是“呈现给谁、用来做什么”。- 不要迷信“全自动”。真正的工程落地,往往是在官方函数基础上,加1-2行业务逻辑,就解决了90%问题。
- 标签本身是金矿。
[背景音乐]不只是提示音效,它可能意味着“对方在边听音乐边开会”,这是传统ASR永远丢失的上下文。
最后送你一句实操口诀:
“先跑通,再清洗;先保真,后美化;先结构,再应用。”
——模型输出永远比你想的更丰富,缺的只是一把用对的“小刀”。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。