基于 DeepSeek API 的 ASR 文本纠错脚本实战:Python 多线程批量处理 JSONL 语音转写数据

在语音识别(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()

代码详解

代码的主要结构如下:

  1. 读取输入的 jsonl 文件。
  2. 逐条解析每条语音转写记录。
  3. 根据 record_uid 的前缀匹配店铺名称和商品类型。
  4. 构造系统提示词和用户提示词。
  5. 调用 DeepSeek 接口对 ASR 文本进行纠错。
  6. 将纠错后的文本写入 corrected_text 字段。
  7. 使用多线程提升整体处理速度。
  8. 最终把结果保存为新的 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() 函数是整个程序的入口,负责把所有流程串联起来。它大致分为四步。

  1. 读取输入文件

程序先逐行读取 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]

把每一行转成字典对象。

  1. 使用线程池并发处理

接着程序创建线程池:

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,就可以一边并发执行,一边实时显示处理进度。

  1. 用 results 字典保存结果

虽然任务是并发执行的,但结果返回顺序未必和输入顺序一致。为了保证最终输出文件顺序不乱,代码用了一个字典,这里的 idx 是原始记录的下标。这样无论线程完成顺序如何,最后都能按照索引重新排序输出。

  1. 按顺序写入输出文件

最后再通过循环按索引顺序写回:

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 调用
  • 多线程并发
  • 失败重试
  • 顺序写回结果

非常适合用于直播电商、语音转写清洗、字幕纠错、口播数据预处理等场景。如果你正在做语音识别后处理,或者希望利用大模型提升文本纠错质量,这种方案是非常值得尝试的。相比传统规则方法,它在复杂口语场景、品牌词纠错、上下文理解方面通常会更有优势。

相关推荐
泡泡鱼(敲代码中)2 小时前
C++-string学习笔记
c语言·开发语言·c++·笔记·学习·visualstudio
编程大师哥2 小时前
JAVA 动态代理
java·开发语言
ytttr8732 小时前
C# 读取数据库表结构工具设计与实现
开发语言·数据库·c#
Jinuss2 小时前
源码分析之React中的useImperativeHandle
开发语言·前端·javascript
csdn2015_2 小时前
HashSet 和 LinkedHashSet 区别
java·开发语言
优化控制仿真模型2 小时前
【26年英语四级】2015-2025年12月英语四级历年真题及答案PDF电子版(含听力音频)
经验分享·pdf
CoderCodingNo2 小时前
【GESP】C++五级练习题 luogu-P1102 A-B 数对
开发语言·c++·算法
Circ.2 小时前
文本相似性对比python代码
开发语言·python·相似度
2301_789015623 小时前
C++11新增特性:可变参数模板、lambda表达式、function包装器、bind绑定、defult和delete
c语言·开发语言·c++·算法·c++11·万能引用