作品分享:我做的语音情绪可视化小工具
1. 这个小工具到底能做什么?
你有没有过这样的体验:听一段客户投诉录音,光靠文字转录根本抓不住对方语气里的火药味;或者剪辑短视频时,想快速定位哪段有笑声、哪段有背景音乐,却得反复拖进度条?我之前也这样——直到把 SenseVoiceSmall 模型和 Gradio WebUI 搭在一起,做了一个真正“看得见声音情绪”的小工具。
它不是简单的语音转文字。上传一段音频,它会立刻告诉你:
- 这句话是笑着说的(
<|HAPPY|>),还是咬着牙说的(<|ANGRY|>) - 中间突然插进来的“啪啪啪”是掌声(
<|APPLAUSE|>),不是敲桌子 - 背景里若隐若现的旋律是 BGM(
<|BGM|>),不是人声哼唱 - 甚至能标出哪句是粤语、哪句切换成了英文,不用手动切分
最让我惊喜的是,它不挑语言——中文会议录音、日语客服对话、韩语播客、粤语访谈,扔进去就能识别,连情绪标签都原样保留。这不是在听声音,是在读一段带表情包的富文本。
下面我会从零开始,带你看看这个小工具是怎么搭出来的,重点不是代码堆砌,而是怎么让模型的能力真正“长”在界面上,变成你能一眼看懂、马上用上的东西。
2. 为什么选 SenseVoiceSmall?它和普通语音识别有什么不一样?
2.1 不是“听清了就行”,而是“听懂了情绪”
传统语音识别(ASR)的目标很明确:把声音变成准确的文字。但现实中的语音远比文字复杂。一句“好的”,用平缓语调说可能是敷衍,用上扬语调说可能是惊喜,用压低声音说可能藏着不满。SenseVoiceSmall 的核心突破,就是把这种“弦外之音”变成了可识别、可标注的结构化信息。
它输出的不是纯文本,而是一段富文本(Rich Transcription)。比如原始识别结果可能是:
<|zh|>今天项目上线了<|HAPPY|>,<|APPLAUSE|>大家辛苦了<|BGM|>经过后处理,就变成更易读的:
【中文】今天项目上线了(开心)!【掌声】大家辛苦了【背景音乐】
你看,语言、情绪、事件三类信息被清晰区分开,而且全部来自一次推理——不需要额外训练情感分类器,也不用单独跑一个事件检测模型。这是 SenseVoiceSmall 的“非自回归”架构带来的天然优势:所有任务在一个前向过程中同步完成。
2.2 多语言不是“加个词表”,而是真能混着说
很多多语种模型要求你提前指定语言,一旦说话人中途切语种,识别就容易崩。SenseVoiceSmall 的language="auto"模式,能在一句话内动态判断语种。我试过一段真实录音:开头是中文提问,中间夹杂英文术语(如“API response”),结尾用粤语补充说明。它不仅全识别出来了,还在对应位置打上了<|zh|>、<|en|>、<|yue|>标签。
这背后是它在数十万小时多语种混合数据上训练的结果。不是简单拼接几个单语模型,而是让模型真正理解“不同语言的声音特征如何共存”。
2.3 小身材,大速度:为什么它适合做交互工具?
名字里带“Small”,不是功能缩水,而是工程优化。它采用轻量级非自回归解码,在 RTX 4090D 上处理 30 秒音频仅需 1.2 秒左右。这意味着:
- 你上传一个 2 分钟的会议录音,5 秒内就能看到带情绪标签的全文
- 界面不会卡顿,用户不用盯着加载动画发呆
- 即使部署在入门级 GPU 上,也能保持秒级响应
这对一个需要频繁上传、反复调试的可视化工具来说,是决定体验上限的关键。
3. 工具界面怎么设计?让情绪“看得见”才是重点
光有模型能力还不够,关键是怎么把它呈现出来。我最初的版本只是把原始富文本丢进一个文本框,结果发现:用户根本懒得看那些<|HAPPY|>标签。他们需要的是直觉反馈。
所以我重构了界面逻辑,核心思路就一条:把抽象标签,变成视觉信号。
3.1 原始输出 → 可读文本:后处理不是可选项
SenseVoiceSmall 输出的富文本包含大量控制标签,直接展示对用户不友好。官方提供了rich_transcription_postprocess函数,但它默认只做基础清洗。我做了两处增强:
from funasr.utils.postprocess_utils import rich_transcription_postprocess def enhance_postprocess(raw_text): # 第一步:用官方函数做基础清洗 clean_text = rich_transcription_postprocess(raw_text) # 第二步:把情绪/事件标签转成带颜色的 Markdown # 替换 <|HAPPY|> -> <span style="color:green">[开心]</span> replacements = { "<|HAPPY|>": '<span style="color:#28a745;font-weight:bold">[开心]</span>', "<|ANGRY|>": '<span style="color:#dc3545;font-weight:bold">[愤怒]</span>', "<|SAD|>": '<span style="color:#007bff;font-weight:bold">[悲伤]</span>', "<|APPLAUSE|>": '<span style="color:#6f42c1;font-weight:bold">[掌声]</span>', "<|LAUGHTER|>": '<span style="color:#fd7e14;font-weight:bold">[笑声]</span>', "<|BGM|>": '<span style="color:#17a2b8;font-weight:bold">[背景音乐]</span>', } for tag, html in replacements.items(): clean_text = clean_text.replace(tag, html) return clean_text这样,输出就不再是冷冰冰的标签,而是带颜色、加粗的提示词,一眼就能扫出情绪分布。
3.2 文本框 → 情绪热力图:让节奏感浮现出来
但光改颜色还不够。我想让用户感受到“这段话的情绪起伏”。于是我在 Gradio 界面里加了一个隐藏功能:当鼠标悬停在某段文字上时,自动高亮同一情绪的所有出现位置。
实现原理很简单——给每个情绪标签加上唯一 class 名,再用前端 JS 绑定 hover 事件:
# 在 Gradio Textbox 的 value 参数中传入 HTML 字符串 text_output = gr.Textbox( label="识别结果(含情绪与事件可视化)", lines=15, interactive=False, elem_id="emotion-output" )然后在页面底部注入一小段 JS(通过 Gradio 的head参数):
<script> document.addEventListener('DOMContentLoaded', () => { const output = document.getElementById('emotion-output'); if (output) { output.addEventListener('mouseover', (e) => { if (e.target.classList.contains('emotion-happy')) { document.querySelectorAll('.emotion-happy').forEach(el => el.style.backgroundColor = 'rgba(40,167,69,0.2)'); } // 其他情绪同理... }); output.addEventListener('mouseout', () => { document.querySelectorAll('[class^="emotion-"]').forEach(el => el.style.backgroundColor = ''); }); } }); </script>效果是:当你把鼠标移到第一个[开心]上,页面里所有[开心]都会泛起一层浅绿色底纹。情绪的密度和节奏,瞬间变得可感知。
3.3 加个“情绪统计栏”:用数字回答“整体氛围如何”
最后,我加了一个极简的统计栏,放在文本框下方:
with gr.Row(): gr.Markdown("### 情绪分布概览") emotion_stats = gr.Label(label="情绪统计", num_top_classes=5)后端计算逻辑也很直接:
def get_emotion_stats(clean_text): emotions = ["HAPPY", "ANGRY", "SAD", "APPLAUSE", "LAUGHTER", "BGM"] counts = {} for emo in emotions: counts[emo] = clean_text.count(f"<|{emo}|>") # 转成 Gradio Label 接受的格式 return {emo: cnt for emo, cnt in counts.items() if cnt > 0}它不追求复杂分析,就干一件事:告诉你这段音频里,开心出现了 3 次,掌声 1 次,背景音乐 2 次。没有算法黑箱,全是可验证的计数。用户一眼就知道:“哦,这是一段偏积极的对话”。
4. 实际用起来效果怎么样?三个真实场景测试
理论说得再好,不如实测。我用这个工具跑了三类真实音频,记录下它的表现和我的调整过程。
4.1 场景一:客服通话质检(中文+粤语混合)
- 音频特点:普通话提问 + 粤语解答,背景有轻微空调噪音,语速较快
- 原始识别:
<|zh|>请问订单号是多少<|yue|>啊...<|ANGRY|>呢個單我跟咗好耐啦<|APPLAUSE|> - 问题发现:
<|yue|>标签后缺少空格,导致“啊...呢個單”连在一起,影响阅读 - 我的调整:在后处理函数里加了一行正则修复:
clean_text = re.sub(r'(<\|[a-z]+\|>)([^\s])', r'\1 \2', clean_text) - 最终效果:
【中文】请问订单号是多少 【粤语】啊……【愤怒】呢個單我跟咗好耐啦【掌声】
——标签之间有了呼吸感,情绪断点更自然。
4.2 场景二:产品发布会视频(中英混杂 + BGM)
- 音频特点:主持人中文介绍,PPT翻页时插入英文产品名(如“SenseVoiceSmall”),全程有低音量背景音乐
- 原始识别:
<|zh|>我们发布了全新语音模型<|en|>SenseVoiceSmall<|BGM|> - 问题发现:BGM 标签覆盖了整段,但实际音乐只在 PPT 切换时出现
- 我的调整:启用
merge_vad=False参数,关闭 VAD 合并,让模型更细粒度地切分:res = model.generate( input=audio_path, language="auto", merge_vad=False, # 关键!让事件检测更精准 ... ) - 最终效果:BGM 标签只出现在音乐实际响起的 2 秒片段,不再“污染”整段文字。
4.3 场景三:团队晨会录音(多人发言 + 笑声穿插)
- 音频特点:5 人轮流发言,中间有自发笑声、拍桌声、纸张翻页声
- 原始识别:
<|zh|>这个需求我来跟进<|LAUGHTER|><|zh|>那接口文档什么时候给?<|APPLAUSE|> - 问题发现:
<|LAUGHTER|>和<|APPLAUSE|>紧挨着文字,用户误以为是发言者本人在笑/鼓掌 - 我的调整:在后处理时,为所有事件标签添加前置空格和括号,并统一用斜体:
"<|LAUGHTER|>": '<i>[笑声]</i>', "<|APPLAUSE|>": '<i>[掌声]</i>', - 最终效果:
【中文】这个需求我来跟进 <i>[笑声]</i> 【中文】那接口文档什么时候给? <i>[掌声]</i>
——事件和发言内容有了明确视觉隔离。
这三次测试让我明白:模型能力是基础,但真正的工具价值,藏在那些微小的、面向人的细节里——空格、颜色、间距、标签样式。它们不改变技术本质,却决定了用户愿不愿意多用一次。
5. 你也能快速搭一个:精简版部署指南
不想从头写代码?我为你准备了一个最小可行版本,5 分钟就能跑起来。
5.1 三步启动(无需改代码)
确保环境已就绪:你的镜像已预装 Python 3.11、PyTorch 2.5、Gradio、ffmpeg
创建启动脚本(复制粘贴即可):
# 创建 app_simple.py cat > app_simple.py << 'EOF' import gradio as gr from funasr import AutoModel from funasr.utils.postprocess_utils import rich_transcription_postprocess # 初始化模型(自动下载,首次运行稍慢) model = AutoModel( model="iic/SenseVoiceSmall", trust_remote_code=True, vad_model="fsmn-vad", device="cuda:0" ) def process_audio(audio_path, lang): if not audio_path: return "请上传音频文件" res = model.generate(input=audio_path, language=lang) if res and len(res) > 0: raw = res[0]["text"] # 简化后处理:只做基础清洗 + 情绪高亮 clean = rich_transcription_postprocess(raw) for k, v in {"HAPPY": "green", "ANGRY": "red", "SAD": "blue"}.items(): clean = clean.replace(f"<|{k}|>", f'<span style="color:{v}">[{k}]</span>') return clean return "识别失败" demo = gr.Interface( fn=process_audio, inputs=[ gr.Audio(type="filepath", label="上传音频"), gr.Dropdown(["auto", "zh", "en", "yue", "ja", "ko"], value="auto", label="语言") ], outputs=gr.Textbox(label="识别结果(情绪已高亮)", lines=10), title="🎙 语音情绪可视化小工具(精简版)" ) demo.launch(server_name="0.0.0.0", server_port=6006) EOF一键运行:
python app_simple.py
访问http://127.0.0.1:6006,上传任意音频,立刻看到带颜色的情绪标签。
5.2 两个必调参数,解决 80% 的识别问题
merge_vad=False:当音频里有短促事件(笑声、掌声)时,关闭 VAD 合并,让事件定位更准batch_size_s=15:对于语速快、停顿少的音频,把批处理时长从默认 60 秒降到 15 秒,减少长句截断
这两个参数不写死在代码里,而是做成界面选项,用户按需切换——这才是工具该有的样子。
6. 总结:一个小工具,如何让语音理解真正“落地”
回看这个小工具的诞生过程,它没有发明新模型,也没有突破算法瓶颈。它只是做了一件事:把 SenseVoiceSmall 模型里那些沉睡的标签,唤醒、翻译、包装,变成人眼能立刻捕捉的信息。
- 它证明了:富文本识别的价值,不在后台日志里,而在前端界面上。当
<|HAPPY|>变成绿色高亮,当掌声统计变成一个数字,技术才真正完成了从“能做”到“好用”的跨越。 - 它提醒我:最好的工具设计,往往藏在“不做什么”的克制里。我没有加情感强度评分、没有做情绪趋势图、没有连通数据库——因为第一个用户要的,只是看清那一句“你真的这么想吗?”里,藏着几分无奈。
- 它也让我确认:开源模型的生命力,取决于有多少人愿意为它写“最后一公里”的代码。SenseVoiceSmall 再强大,也需要一个
app.py把它推到用户面前;FunASR 再完善,也需要一行gr.Textbox(...)让它开口说话。
如果你也想试试,现在就可以打开镜像,跑起那个app_simple.py。别担心识别不准——多试几段音频,观察标签在哪出错,然后微调一行正则、改一个参数。工具的进化,从来不是等来的,而是一次次“咦,这里好像不太对”的好奇心堆出来的。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。