Paraformer-large输出结果导出:JSON/TXT格式化实战教程
1. 为什么需要导出识别结果?
你已经成功用Paraformer-large跑通了语音转文字流程,上传一段会议录音,几秒钟后屏幕上就跳出一整段带标点的中文文本——这很酷。但现实工作里,光在网页上“看看”远远不够。
比如:
- 你要把识别结果交给同事做纪要整理,对方只收TXT文件;
- 你需要把每句话的时间戳、置信度、分段信息一起导出,供后续做字幕或质检;
- 你正在搭建一个自动化语音处理流水线,下游系统只认标准JSON格式;
- 你想批量处理上百个音频,不能一个个点“复制粘贴”。
这时候,Gradio界面上那个漂亮的Textbox就只是个展示窗口了。真正能落地、能集成、能复用的,是结构化、可编程、可存储的输出结果。
本教程不讲怎么安装模型、不重复部署步骤,而是聚焦一个被很多人忽略但极其关键的环节:如何把Paraformer-large的识别结果,从“网页上看得见”,变成“代码里拿得到、磁盘里存得下、系统间传得走”。
全程基于你已有的app.py,零新增依赖,5分钟内完成升级。
2. 理解Paraformer-large的真实输出结构
先别急着改代码。我们得知道,model.generate()返回的到底是什么。
打开终端,进入Python交互环境,手动跑一次推理:
from funasr import AutoModel model = AutoModel(model="iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch", device="cuda:0") res = model.generate(input="test.wav") print(res)你会看到类似这样的输出(已简化):
[ { 'text': '今天我们要讨论Q3市场推广方案', 'timestamp': [[0, 1230], [1250, 2890], [2910, 4560]], 'seg_id': 0, 'confidence': 0.972 }, { 'text': '预算分配建议由张经理负责汇报', 'timestamp': [[4600, 6120], [6140, 7890]], 'seg_id': 1, 'confidence': 0.958 } ]注意三点:
- 它不是单个字符串,而是一个字典列表,每项代表一句话(segment);
- 每句都含
text(文字)、timestamp(毫秒级起止时间)、confidence(置信度); timestamp是二维列表,[start_ms, end_ms],不是秒,也不是帧数。
这个结构就是你的“金矿”。只要把它正确提取、组织、写入文件,你就拥有了工业级ASR输出能力。
3. 改造Gradio界面:支持一键导出TXT与JSON
现在回到你的app.py。我们不做大改,只在原有逻辑上加三处关键补丁:一个保存函数、两个按钮、一处结果处理逻辑。
3.1 新增文件保存工具函数
在asr_process函数上方,插入以下代码:
import json import os from datetime import datetime def save_as_txt(text_list, output_path): """将识别结果列表保存为纯文本(每句一行)""" with open(output_path, "w", encoding="utf-8") as f: for item in text_list: f.write(item['text'].strip() + "\n") return output_path def save_as_json(full_result, output_path): """将完整识别结果(含时间戳、置信度)保存为JSON""" # 添加元信息,便于追溯 export_data = { "export_time": datetime.now().isoformat(), "model_id": "iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch", "segments": full_result, "total_duration_ms": sum( seg['timestamp'][-1][1] - seg['timestamp'][0][0] for seg in full_result if seg.get('timestamp') ) if full_result else 0 } with open(output_path, "w", encoding="utf-8") as f: json.dump(export_data, f, ensure_ascii=False, indent=2) return output_path这两个函数完全独立,不依赖Gradio,你以后在脚本里也能直接调用。
3.2 修改asr_process:返回结构化结果而非字符串
原asr_process只返回res[0]['text'],太单薄。我们让它返回原始完整结果,再由UI层决定怎么展示和导出:
def asr_process(audio_path): if audio_path is None: return {"error": "请先上传音频文件"} res = model.generate( input=audio_path, batch_size_s=300, ) # 关键改动:不再拼接字符串,直接返回原始res if len(res) > 0: return res else: return {"error": "识别失败,请检查音频格式"}3.3 扩展Gradio界面:增加导出按钮与状态反馈
找到with gr.Blocks(...)内部,在text_output下方添加新组件:
# 原有输出框保持不变,用于实时预览 text_output = gr.Textbox(label="识别结果(预览)", lines=15) # 新增:导出控制区 with gr.Row(): txt_btn = gr.Button(" 导出为TXT(纯文字)", variant="secondary") json_btn = gr.Button("📦 导出为JSON(含时间戳)", variant="secondary") status_msg = gr.Textbox(label="导出状态", interactive=False, lines=2) # 新增:导出逻辑绑定 def export_to_txt(full_result): if not full_result or isinstance(full_result, dict) and "error" in full_result: return "❌ 无有效识别结果,无法导出" # 生成文件名:音频名 + 时间戳 base_name = os.path.basename(audio_input.value) if hasattr(audio_input, 'value') else "output" name_no_ext = os.path.splitext(base_name)[0] output_path = f"/root/workspace/{name_no_ext}_{int(datetime.now().timestamp())}.txt" try: save_as_txt(full_result, output_path) return f" 已保存:{output_path}" except Exception as e: return f"❌ 保存失败:{str(e)}" def export_to_json(full_result): if not full_result or isinstance(full_result, dict) and "error" in full_result: return "❌ 无有效识别结果,无法导出" base_name = os.path.basename(audio_input.value) if hasattr(audio_input, 'value') else "output" name_no_ext = os.path.splitext(base_name)[0] output_path = f"/root/workspace/{name_no_ext}_{int(datetime.now().timestamp())}.json" try: save_as_json(full_result, output_path) return f" 已保存:{output_path}" except Exception as e: return f"❌ 保存失败:{str(e)}" # 绑定按钮事件(注意:这里需在submit_btn.click之后定义) txt_btn.click( fn=export_to_txt, inputs=text_output, # 注意:这里输入是text_output,它现在接收的是full_result outputs=status_msg ) json_btn.click( fn=export_to_json, inputs=text_output, outputs=status_msg )重要提醒:Gradio中text_output现在承载的是Python列表(即full_result),不是字符串。所以它的type要改为object,否则会报错。修改这一行:
text_output = gr.Textbox(label="识别结果(预览)", lines=15, type="object") # ← 加上 type="object"同时,为了让预览更友好,我们加一个“格式化预览”函数,把结构化结果转成易读文本:
def format_preview(full_result): if not full_result or isinstance(full_result, dict) and "error" in full_result: return str(full_result) lines = [] for i, seg in enumerate(full_result): ts = seg.get('timestamp', []) start = ts[0][0] // 1000 if ts else 0 end = ts[-1][1] // 1000 if ts else 0 conf = seg.get('confidence', 0) lines.append(f"[{start}-{end}s] {seg['text']} (置信度: {conf:.3f})") return "\n".join(lines) # 在submit_btn.click中,同时更新text_output和预览 submit_btn.click( fn=asr_process, inputs=audio_input, outputs=text_output ).then( fn=format_preview, inputs=text_output, outputs=text_output )这样,界面上显示的是带时间戳和置信度的可读文本,而后台text_output值仍是原始结构体,供导出函数使用。
4. 实战验证:一次上传,三种输出
现在保存app.py,重启服务:
source /opt/miniconda3/bin/activate torch25 && cd /root/workspace && python app.py访问http://127.0.0.1:6006,上传一段30秒的测试音频(如meeting_sample.wav)。
点击【开始转写】后,你会看到:
- 预览框中显示:
[0-3s] 大家好欢迎参加本次项目启动会 [3-8s] 今天我们重点讨论三个核心模块 [8-15s] 第一是用户增长策略第二是技术架构升级
然后点击【 导出为TXT(纯文字)】:
- 状态栏显示:
已保存:/root/workspace/meeting_sample_1745678901.txt - 登录服务器,执行
cat /root/workspace/meeting_sample_1745678901.txt,内容为:大家好欢迎参加本次项目启动会 今天我们重点讨论三个核心模块 第一是用户增长策略第二是技术架构升级
再点击【📦 导出为JSON(含时间戳)】:
- 状态栏显示:
已保存:/root/workspace/meeting_sample_1745678901.json - 查看JSON内容(
jq . segments[0] /root/workspace/meeting_sample_1745678901.json):{ "text": "大家好欢迎参加本次项目启动会", "timestamp": [[0, 3250]], "seg_id": 0, "confidence": 0.982 }
你已获得:
- TXT:给行政、运营、非技术人员快速阅读;
- JSON:给开发、字幕系统、质检平台做二次处理;
- 原始结构:随时可扩展导出CSV、SRT、VTT等任意格式。
5. 进阶技巧:批量处理与自动化集成
上面是一次性操作。如果你要处理100个音频,总不能点100次。这里给你两个轻量级但极其实用的方案。
5.1 命令行批量导出脚本(无需GUI)
新建batch_export.py,放在/root/workspace/:
#!/usr/bin/env python3 import os import json from funasr import AutoModel from datetime import datetime model = AutoModel( model="iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch", device="cuda:0" ) def process_audio_file(audio_path): print(f"→ 正在处理:{audio_path}") res = model.generate(input=audio_path, batch_size_s=300) if not res: print(f" ❌ 识别失败") return # 生成同名JSON json_path = os.path.splitext(audio_path)[0] + ".json" with open(json_path, "w", encoding="utf-8") as f: json.dump({ "export_time": datetime.now().isoformat(), "segments": res }, f, ensure_ascii=False, indent=2) # 生成同名TXT txt_path = os.path.splitext(audio_path)[0] + ".txt" with open(txt_path, "w", encoding="utf-8") as f: for seg in res: f.write(seg['text'].strip() + "\n") print(f" 已生成:{json_path} 和 {txt_path}") # 批量处理当前目录所有wav/mp3 for file in os.listdir("."): if file.lower().endswith((".wav", ".mp3")): process_audio_file(file)运行它:
cd /root/workspace source /opt/miniconda3/bin/activate torch25 python batch_export.py10秒内,你目录下所有音频都生成了对应的.txt和.json——这才是生产环境该有的样子。
5.2 与现有工作流集成(示例:钉钉机器人自动推送)
假设你用钉钉机器人通知团队会议纪要已出。只需在导出JSON后加几行:
import requests import json def send_to_dingtalk(json_path, webhook_url): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # 提取前两句话作为摘要 summary = "|".join([seg['text'][:20] + "..." for seg in data["segments"][:2]]) payload = { "msgtype": "text", "text": { "content": f" 会议纪要已生成\n{summary}\n详情见附件" } } requests.post(webhook_url, json=payload)把这段塞进export_to_json函数末尾,再传入你的钉钉Webhook地址,识别完成那一刻,消息就推送到群里了。
技术的价值,从来不在“能不能跑”,而在“能不能连”。
6. 常见问题与避坑指南
实际用起来,你可能会遇到这几个高频问题。都是踩过坑后总结的真经验:
6.1 “导出按钮点了没反应?状态栏空白”
最常见原因:text_output的type没改成"object"。Gradio默认把它当字符串处理,传给导出函数时自动转成str(full_result),导致len(res)报错。
解决:确认gr.Textbox(..., type="object")已设置。
6.2 “JSON里timestamp是空的?”
Paraformer的VAD模块有时对静音长的音频不敏感,导致timestamp字段缺失。这不是bug,是模型行为。
解决:在save_as_json中加一层防御:
ts = seg.get('timestamp') or [[0, 0]]或者,强制启用VAD:在model.generate()中加参数vad=True。
6.3 “导出的TXT乱码,中文变问号”
Linux服务器默认编码常为latin-1,而Pythonopen()默认用系统编码。
解决:所有open()必须显式声明encoding="utf-8",本教程已全部加上,切勿省略。
6.4 “想导出SRT字幕,怎么改?”
SRT只需按序号、时间码、文本三段式组织。在save_as_json旁新增save_as_srt即可:
def save_as_srt(full_result, output_path): with open(output_path, "w", encoding="utf-8") as f: for i, seg in enumerate(full_result, 1): ts = seg.get('timestamp', [[0,0]]) start = ts[0][0] end = ts[-1][1] # 转换为 SRT 时间格式:HH:MM:SS,mmm def ms_to_srt(ms): s = int(ms / 1000) h, s = divmod(s, 3600) m, s = divmod(s, 60) ms_part = ms % 1000 return f"{h:02d}:{m:02d}:{s:02d},{ms_part:03d}" f.write(f"{i}\n") f.write(f"{ms_to_srt(start)} --> {ms_to_srt(end)}\n") f.write(f"{seg['text'].strip()}\n\n")加个按钮,5分钟搞定字幕导出。
7. 总结:让ASR结果真正流动起来
你刚刚完成的,不只是一个“导出功能”的添加。你打通了从语音到结构化数据的最后一公里。
回顾一下,我们做了什么:
- 看清本质:理解
model.generate()返回的是带时间戳、置信度的字典列表,不是字符串; - 最小改动:仅在
app.py中新增3个函数、修改2处配置、增加3个UI组件,零依赖、零风险; - 双格式覆盖:TXT满足人工阅读,JSON支撑系统集成,两者共用同一套结果源;
- 向下兼容:原有Gradio界面、预览逻辑、服务命令全部不受影响;
- 向上延展:批量脚本、钉钉推送、SRT导出……所有扩展都基于这个坚实的数据结构。
Paraformer-large的强大,不只在于它能把语音转成文字,更在于它输出的信息足够丰富、足够规范、足够“工程友好”。而你的任务,就是把这份友好,翻译成业务语言、交付语言、协作语言。
下次当你面对一段长达2小时的访谈录音,不用再手动拖进度条、不用再复制粘贴十几遍、不用再求人帮忙转格式——你点一下,它就生成;你写一行脚本,它就批量处理;你接一个API,它就自动推送。
这才是AI落地该有的样子:安静、可靠、不声张,但永远在你需要的时候,准备好了。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。