在语音识别(ASR)场景中,原始转写文本经常会出现同音字错误、漏字、重复字、断句不自然等问题。尤其是在直播电商场景下,口播内容节奏快、语气随意、商品名称复杂,传统规则方法往往很难保证纠错效果。
这篇文章分享一段我自己使用的 Python 脚本,核心功能是:调用 DeepSeek 大模型接口,对 JSONL 格式的 ASR 转写文本进行批量纠错,并将结果写回输出文件 。同时,代码还加入了多线程并发、失败重试、进度条显示、按原顺序写回结果等实用功能,适合处理大批量语音文本数据。
全部代码
python
import json
import time
import os
from openai import OpenAI
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, as_completed
# ==================== 配置区域 ====================
API_KEY = os.getenv("DEMO_API_KEY", "your_api_key_here") # 这里的API_KEY我使用环境变量代替比较安全,当然你也可以直接作为字符串复制一下
BASE_URL = "https://api.deepseek.com"
MODEL = "deepseek-chat"
INPUT_FILE = "/path/to/input_demo.jsonl" # 输入文件路径
OUTPUT_FILE = "/path/to/output_demo.jsonl" # 输出文件路径
MAX_WORKERS = 20 # 并发线程数(根据API限制调整)
# =================================================
# 映射表 这里只给一个示例
PREFIX_MAP = {
"A1": ("某美妆旗舰店", "美妆"),
}
# 每个线程需独立客户端实例(避免并发下共享客户端带来的问题)
def create_client():
return OpenAI(api_key=API_KEY, base_url=BASE_URL)
def call_api(system_prompt, user_prompt, retries=3):
"""在子线程中调用,每次创建新客户端"""
client = create_client()
for attempt in range(retries):
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.0,
max_tokens=512,
stream=False
)
return response.choices[0].message.content.strip()
except Exception as e:
if attempt < retries - 1:
time.sleep(2 ** attempt)
else:
raise e
def build_prompts(shop_name, product_type, raw_text):
system = (
"你是直播电商语境下的中文语音转写文本纠错助手。"
"你的任务是只纠正ASR造成的错别字、漏字、重复字、"
"明显的断句/标点错误和同音替换错误。"
"严格保持原意、原语气、原表达风格,不得扩写、改写、润色、总结。"
)
user = (
f"【商家】{shop_name}【商品类型】{product_type}\n"
"下面是一段直播口播的ASR转写文本,请你进行纠错:"
"- 如果没有错误:原样输出,不要添加任何说明。"
"- 如果有错误:只做最小必要修改以纠正识别错误;"
"不得改变任何词句的意思;不得添加新内容。"
"如果无内容输出空白即可,仅输出最终纠错后的文本"
"(不要输出解释、不要加引号、不要加标签)。\n"
f"【待纠错文本】{raw_text}"
)
return system, user
def process_item(idx, record):
"""处理单条记录,返回 (idx, 更新后的数据)"""
record_uid = record.get("record_uid", "")
raw_text = record.get("text", "")
prefix = record_uid[:2] if len(record_uid) >= 2 else None
if prefix not in PREFIX_MAP:
record["corrected_text"] = ""
return idx, record
shop_name, product_type = PREFIX_MAP[prefix]
system_prompt, user_prompt = build_prompts(shop_name, product_type, raw_text)
try:
corrected = call_api(system_prompt, user_prompt)
record["corrected_text"] = corrected
except Exception as e:
print(f"处理失败 idx {idx}, record_uid: {record_uid}, 错误: {e}")
record["corrected_text"] = ""
return idx, record
def main():
# 读取所有行到内存(带索引),以便并发处理后按序写入
with open(INPUT_FILE, 'r', encoding='utf-8') as f:
lines = [line.strip() for line in f if line.strip()]
records = [json.loads(line) for line in lines]
total = len(records)
print(f"共加载 {total} 条记录")
# 使用字典保存结果:idx -> updated_record
results = {}
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_idx = {executor.submit(process_item, i, records[i]): i for i in range(total)}
for future in tqdm(as_completed(future_to_idx), total=total, desc="纠错处理中"):
idx = future_to_idx[future]
try:
idx, updated = future.result()
results[idx] = updated
except Exception as e:
print(f"任务 {idx} 发生未知异常: {e}")
records[idx]["corrected_text"] = ""
results[idx] = records[idx]
# 按原顺序写入输出文件
with open(OUTPUT_FILE, 'w', encoding='utf-8') as outfile:
for i in range(total):
outfile.write(json.dumps(results[i], ensure_ascii=False) + '\n')
print(f"处理完成!结果已保存至 {OUTPUT_FILE}")
if __name__ == "__main__":
main()
代码详解
代码的主要结构如下:
- 读取输入的 jsonl 文件。
- 逐条解析每条语音转写记录。
- 根据 record_uid 的前缀匹配店铺名称和商品类型。
- 构造系统提示词和用户提示词。
- 调用 DeepSeek 接口对 ASR 文本进行纠错。
- 将纠错后的文本写入 corrected_text 字段。
- 使用多线程提升整体处理速度。
- 最终把结果保存为新的 jsonl 文件。
依赖模块
python
import json
import time
from openai import OpenAI
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, as_completed
- json:用于读取和写入 JSON 数据。由于输入文件是 jsonl 格式,所以每一行都需要通过 json.loads() 转成 Python 字典,输出时再用 json.dumps() 写回。
- time:主要用于失败重试时的延迟等待,例如第一次失败后等待 1 秒,第二次失败后等待 2 秒,第三次失败后再抛出异常。
- OpenAI:虽然这里连接的是 DeepSeek API,但它兼容 OpenAI 风格接口,所以可以直接通过 OpenAI 客户端发起请求。
- tqdm:用于显示处理进度条,方便观察批量任务执行进度。
- ThreadPoolExecutor 和 as_completed:用于实现多线程并发调用接口,大幅提升批量处理效率。
配置参数
python
# ==================== 配置区域 ====================
API_KEY = os.getenv("DEMO_API_KEY", "your_api_key_here") # 这里的API_KEY我使用环境变量代替比较安全,当然你也可以直接作为字符串复制一下
BASE_URL = "https://api.deepseek.com"
MODEL = "deepseek-chat"
INPUT_FILE = "/path/to/input_demo.jsonl" # 输入文件路径
OUTPUT_FILE = "/path/to/output_demo.jsonl" # 输出文件路径
MAX_WORKERS = 20 # 并发线程数(根据API限制调整)
# =================================================
# 映射表 这里只给一个示例
PREFIX_MAP = {
"A1": ("某美妆旗舰店", "美妆"),
}
- API_KEY:用于接口鉴权。这里建议不要把真实密钥直接写进代码中,而是改成环境变量读取,这样更安全。
- BASE_URL:这里配置的是 https://api.deepseek.com,说明代码请求的是 DeepSeek 的接口地址。
- MODEL:指定使用的模型,比如 deepseek-chat。
- INPUT_FILE 和 OUTPUT_FILE:分别表示输入文件和输出文件路径。输入文件中保存原始语音转写文本,输出文件中保存纠错后的结果。
- MAX_WORKERS:表示并发线程数。设置为 20,意味着最多同时发起 20 个请求。这个数值不能一味调大,还要结合 API 限流策略和机器性能来调整。
- PREFIX_MAP:根据 record_uid 前缀,映射出对应的店铺名称和商品类型。这样设计的意义在于:给大模型提供更准确的上下文信息 。因为同样一句 ASR 文本,如果是在美妆场景下,模型更容易纠正品牌名、产品名;如果是在数码场景下,则更容易纠正手机、耳机、显示器等专业词汇。也就是说,这一层映射本质上是在给模型"补充行业上下文",从而提高纠错质量。
当然注意一件事情,PREFIX_MAP的设计要根据自身任务的数据构成方式来做,我这里是这样设计的,你那里就不一定了。
create_client 函数
python
def create_client():
return OpenAI(api_key=API_KEY, base_url=BASE_URL)
每次调用接口时重新创建客户端实例。这样做的原因是:OpenAI 客户端对象并不一定是线程安全的。在多线程环境下,如果多个线程共享同一个客户端对象,可能会导致请求冲突、状态异常,甚至返回结果错乱。因此,这里采用的策略是:
- 每个线程单独创建自己的客户端实例
- 避免多个线程共用一个连接对象
- 提高并发调用时的稳定性
这是一种很典型、也很实用的线程安全写法。当然如果不放心的话,你甚至可以多创建几个API-KEY,每个API-KEY一个连接,但是注意控制连接数量,不要引发风控和限制。
call_api 函数
python
def call_api(system_prompt, user_prompt, retries=3):
"""在子线程中调用,每次创建新客户端"""
client = create_client()
for attempt in range(retries):
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.0,
max_tokens=512,
stream=False
)
return response.choices[0].message.content.strip()
except Exception as e:
if attempt < retries - 1:
time.sleep(2 ** attempt)
else:
raise e
call_api() 是整段代码的核心函数之一。
它主要负责:
- 创建客户端
- 发起对话请求
- 返回模型纠错结果
- 处理异常并进行重试
代码中使用了如下参数:
- temperature=0.0:表示尽量让模型输出更稳定、更确定的结果。因为这里的目标是"纠错",不是"创作",所以低随机性更合适。
- max_tokens=512:限制返回文本的最大长度,避免输出过长。
- stream=False:表示采用一次性返回结果的方式,而不是流式输出。
另外,这个函数还实现了一个非常实用的机制:失败重试。
如果请求失败,程序不会立刻终止,而是:第一次失败后等待,第二次失败后继续等待更长时间,
最终超过重试次数后再抛出异常。这种写法能有效应对网络抖动、临时限流、接口不稳定等问题。
build_prompts 函数
python
def build_prompts(shop_name, product_type, raw_text):
system = (
"你是直播电商语境下的中文语音转写文本纠错助手。"
"你的任务是只纠正ASR造成的错别字、漏字、重复字、"
"明显的断句/标点错误和同音替换错误。"
"严格保持原意、原语气、原表达风格,不得扩写、改写、润色、总结。"
)
user = (
f"【商家】{shop_name}【商品类型】{product_type}\n"
"下面是一段直播口播的ASR转写文本,请你进行纠错:"
"- 如果没有错误:原样输出,不要添加任何说明。"
"- 如果有错误:只做最小必要修改以纠正识别错误;"
"不得改变任何词句的意思;不得添加新内容。"
"如果无内容输出空白即可,仅输出最终纠错后的文本"
"(不要输出解释、不要加引号、不要加标签)。\n"
f"【待纠错文本】{raw_text}"
)
return system, user
build_prompts() 函数负责构造系统提示词和用户提示词。这一部分非常关键,因为大模型纠错效果好不好,很大程度上取决于提示词是否清晰。
系统提示词(system)主要限定模型的身份和任务边界,例如:
- 你是中文语音转写纠错助手
- 只纠正常见的识别错误
- 不允许扩写
- 不允许改写
- 不允许润色
- 必须保持原意和原语气
这样做的目的是防止模型"过度发挥"。因为在语音纠错场景下,我们需要的是"最小修改",而不是"二次创作"。
用户提示词(user)中加入了:
- 店铺名称
- 商品类型
- 原始 ASR 文本
- 明确的输出要求
例如要求模型:
- 如果没有错误,原样输出
- 如果有错误,只做最小必要修改
- 不要添加解释
- 不要加引号
- 不要输出标签
这类约束非常重要,它能让返回结果更适合程序自动处理,而不用再做额外清洗。
process_item 函数
python
def process_item(idx, record):
"""处理单条记录,返回 (idx, 更新后的数据)"""
record_uid = record.get("record_uid", "")
raw_text = record.get("text", "")
prefix = record_uid[:2] if len(record_uid) >= 2 else None
if prefix not in PREFIX_MAP:
record["corrected_text"] = ""
return idx, record
shop_name, product_type = PREFIX_MAP[prefix]
system_prompt, user_prompt = build_prompts(shop_name, product_type, raw_text)
try:
corrected = call_api(system_prompt, user_prompt)
record["corrected_text"] = corrected
except Exception as e:
print(f"处理失败 idx {idx}, record_uid: {record_uid}, 错误: {e}")
record["corrected_text"] = ""
return idx, record
process_item() 的作用是处理一条输入记录。
它的执行流程可以概括为:
- 获取 record_uid
- 获取原始文本 text
- 截取 record_uid 前两位作为前缀
- 根据前缀去 PREFIX_MAP 中查找店铺和商品类型
- 构造提示词
- 调用 API 获取纠错结果
- 把结果写入 corrected_text
如果某条记录的前缀不在映射表中,代码不会报错,而是直接给 corrected_text 赋空字符串并返回原记录。考虑到了数据不完整或前缀不匹配的情况,保证了程序的健壮性。另外,如果接口调用失败,函数会捕获异常并打印错误信息,同时将当前记录的 corrected_text 置空,这样即使个别记录失败,也不会影响整个批处理任务继续执行。
main 函数
main() 函数是整个程序的入口,负责把所有流程串联起来。它大致分为四步。
- 读取输入文件
程序先逐行读取 jsonl 文件,并过滤掉空行:
python
with open(INPUT_FILE, 'r', encoding='utf-8') as f:
lines = [line.strip() for line in f if line.strip()]
然后通过:
python
records = [json.loads(line) for line in lines]
把每一行转成字典对象。
- 使用线程池并发处理
接着程序创建线程池:
python
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_idx = {executor.submit(process_item, i, records[i]): i for i in range(total)}
for future in tqdm(as_completed(future_to_idx), total=total, desc="纠错处理中"):
idx = future_to_idx[future]
try:
idx, updated = future.result()
results[idx] = updated
except Exception as e:
print(f"任务 {idx} 发生未知异常: {e}")
records[idx]["corrected_text"] = ""
results[idx] = records[idx]
然后把每一条记录都提交为一个异步任务。
这一步的作用比较大:
- 单线程调用接口速度慢
- 批量数据量大时耗时长
- 多线程可以同时发起多个 API 请求
- 明显提升整体吞吐量
再结合 tqdm,就可以一边并发执行,一边实时显示处理进度。
- 用 results 字典保存结果
虽然任务是并发执行的,但结果返回顺序未必和输入顺序一致。为了保证最终输出文件顺序不乱,代码用了一个字典,这里的 idx 是原始记录的下标。这样无论线程完成顺序如何,最后都能按照索引重新排序输出。
- 按顺序写入输出文件
最后再通过循环按索引顺序写回:
python
# 按原顺序写入输出文件
with open(OUTPUT_FILE, 'w', encoding='utf-8') as outfile:
for i in range(total):
outfile.write(json.dumps(results[i], ensure_ascii=False) + '\n')
print(f"处理完成!结果已保存至 {OUTPUT_FILE}")
这里使用 ensure_ascii=False,可以保证中文正常写入,不会被转成 Unicode 转义字符。最终生成的新文件中,每一条记录都会新增一个 corrected_text 字段,用来保存纠错后的文本。
总结
这段 Python 脚本本质上是一个大模型驱动的 ASR 文本纠错批处理工具。它结合了:
- JSONL 数据读取
- Prompt 工程
- DeepSeek API 调用
- 多线程并发
- 失败重试
- 顺序写回结果
非常适合用于直播电商、语音转写清洗、字幕纠错、口播数据预处理等场景。如果你正在做语音识别后处理,或者希望利用大模型提升文本纠错质量,这种方案是非常值得尝试的。相比传统规则方法,它在复杂口语场景、品牌词纠错、上下文理解方面通常会更有优势。