OpenAI的Whisper模型在语音识别领域无疑是革命性的,它能以惊人的准确率将音频转为文字。然而,对于长视频或复杂对话,其自动断句和标点符号功能有时会不尽人意,常常生成不便于阅读的大段文字。
本文将提供一个解决方案:结合Whisper的字级时间戳功能与大语言模型(LLM)的强大理解能力,打造一个能智能断句、优化文本并输出结构化数据的全自动字幕处理管道。
从 Whisper 获取“原料”—— 字级时间戳
要让LLM精确地为新句子赋予起止时间,我必须先从Whisper获取每个字或词的时间信息。这需要开启一个特定参数。
在使用Whisper进行识别时,务必将 word_timestamps
参数设为 True
。以Python的openai-whisper
库为例:
import whispermodel = whisper.load_model("base")# 开启 word_timestamps 选项result = model.transcribe("audio.mp3", word_timestamps=True)
result
中会包含一个 segments
列表,每个 segment 里又有 words
列表。我需要的数据就在这里。接下来,我将这些数据组装成一个干净的、专为LLM设计的JSON列表。
word_level_timestamps = []for segment in result['segments']: for word_info in segment['words']: word_level_timestamps.append({ 'word': word_info['word'], 'start': word_info['start'], 'end': word_info['end'] })# 最终得到的数据结构:# [# {"word": " 五", "start": 1.95, "end": 2.17},# {"word": "老", "start": 2.17, "end": 2.33},# ...# ]
这个列表就是我喂给LLM的“原料”。
智能分块(Chunking)—— 规避 Token 限制
一个小时的视频转写出的字词列表可能非常庞大,直接发送给LLM会超出其Token限制(Context Window)。因此,必须进行分块处理。
一个简单有效的方法是设定一个阈值,例如每500个字词为一块。
def create_chunks(data, chunk_size=500): chunks = [] for i in range(0, len(data), chunk_size): chunks.append(data[i:i + chunk_size]) return chunksword_chunks = create_chunks(word_level_timestamps, 500)
高级技巧:为了避免在句子中间粗暴地切断,更好的分块策略是,在达到 chunk_size
附近时,寻找一个字词间隙(end
到下一个start
时间差)最大的地方进行切分。这能提高LLM处理每一块时的上下文完整性。
编写高质量的 LLM 提示词
提示词是整个流程的灵魂,它直接决定了输出的质量和稳定性。一个优秀的提示词应该包含以下几个要素:
- 清晰的角色与目标:明确告知LLM它的身份(如“AI字幕处理引擎”)和唯一任务。详细的处理流程:分步描述它需要做什么,包括识别语言、智能分段、文本修正、添加标点等。极其严格的输出格式定义:使用表格、代码块等方式,精确定义输出的JSON结构、键名、值类型,并强调哪些是“必须”和“禁止”的。提供示例:给出1-2个包含输入和预期输出的完整示例。这能极大地帮助模型理解任务,尤其是在处理特殊情况(如修正错别字、移除口头禅)时。内置最终检查清单:在Prompt末尾让模型进行自我检查,这是一种强大的心理暗示,能有效提升输出格式的遵循度。
最终优化出的提示词,正是遵循了以上所有原则的典范。(具体提示词见底部)
结构化调用的常见问题与解决方案
这是实践中最容易出错的环节,也是我今天对话解决的核心问题。
陷阱一:指令与数据混为一谈
问题描述:初学者常常将长篇的提示词指令和海量的JSON数据拼接成一个巨大的字符串,然后作为单条消息发送给LLM。
症状:LLM返回错误,抱怨“输入格式不符合要求”,因为它看到的是一个混合了自然语言和JSON的复杂文本,而不是它被告知要处理的纯JSON数据。
{ "error": "The input provided does not conform to the expected format for processing. Please ensure the input is a valid JSON list of dictionaries, each containing \'word\', \'start\', and \'end\' keys."}'
解决方案:严格分离指令与数据。利用OpenAI API的 messages
结构,将你的提示词放入 role: 'system'
的消息中,将待处理的纯JSON数据字符串放入 role: 'user'
的消息中。
messages = [ {"role": "system", "content": "你的完整提示词..."}, {"role": "user", "content": '纯JSON数据字符串...'} # e.g., json.dumps(chunk)]
陷阱二:json_object
模式与 Prompt 指令冲突
问题描述:为了确保100%返回合法的JSON,我使用 response_format={"type": "json_object"}
参数。但这个参数强制模型返回一个JSON对象(以 {}
包裹)。如果在提示词中,你却要求模型直接返回一个JSON列表(以 []
包裹),就会产生指令冲突。
response = model.chat.completions.create( model=config.params['chatgpt_model'], timeout=7200, max_tokens= max(int(config.params.get('chatgpt_max_token')) if config.params.get('chatgpt_max_token') else 4096,4096), messages=message, response_format= { "type":"json_object" } )
错误的提示词
## 输出 **json** 格式结果 (关键且必须遵守)你**必须**以合法 json 列表的形式返回结果,输出列表中的每个元素**必须且只能**包含以下三个键:
症状:即使分离了指令和数据,LLM仍然可能报错,因为它无法同时满足“返回一个对象”和“返回一个列表”这两个矛盾的要求。
解决方案:让Prompt的指令与API的约束保持一致。修改你的提示词,要求模型返回一个包裹着字幕列表的JSON对象。
- 错误的做法:要求直接输出
[{...}, {...}]
正确的做法:要求输出 {"subtitles": [{...}, {...}]}
这样,API的要求(返回一个对象)和Prompt的指令(返回一个包含subtitles
键的对象)就完美统一了。相应地,在代码中解析结果时,也需要多一步提取:result_object['subtitles']
。
其他注意事项
完整流程:在代码中,你需要遍历所有分块(chunks),对每一块调用LLM进行处理,然后将每一块返回的字幕列表拼接起来,形成最终完整的字幕文件。
错误处理与重试:网络请求可能失败,LLM也可能偶尔返回不合规范的JSON。在API调用外层包裹 try-except
块,并加入重试机制(如使用 tenacity
库),是保证程序稳定性的关键。
成本与模型选择:像 GPT-4o
或 deepseek-chat
这样的模型在遵循复杂指令和格式化输出方面表现更佳。
最终校对:虽然LLM能完成99%的工作,但在拼接完所有结果后,可以编写简单的脚本进行最后一次检查,例如:检查是否有字幕时长超过6秒,或两条字幕的起止时间是否重叠。
附录:最终系统提示词
# 角色与最终目标你是一位顶级的 AI 字幕处理引擎。你的**唯一目标**是将用户输入(user message)中的**字级**时间戳数据(包含 `'word'` 键),转换成**句子级**的、经过智能断句和文本优化的字幕列表,并以一个包含字幕列表的 **JSON 对象**格式返回。---## 核心处理流程1. **接收输入**: 你会收到一个 json 格式的列表作为用户输入。列表中每个元素均包含 `'word'`, `'start'`, `'end'`。2. **识别语言**: 自动判断输入文本的主要语言(如中文、英文、日文、西班牙语等),并调用相应的语言知识库。**单次任务只处理一种语言**。3. **智能分段与合并**: * **原则**: 以**语义连贯、语法自然**为最高准则进行断句。 * **时长**: 每条字幕理想时长为 1-3 秒,**绝对不能超过 6 秒**。 * **合并**: 将属于同一句话的多个字/词字典合并成一个。4. **文本修正与增强**: * 在合并文本的过程中,对**整句**进行深度校对和优化。 * **修正**: 自动修正拼写错误、语法错误以及特定语言的常见用词错误。 * **优化**: 移除不必要的口头禅、调整语序,使表达更流畅、地道,但绝不改变原意。 * **标点**: 在断句处和句子内部,根据已识别语言的规范,智能添加或修正标点符号。5. **生成输出**: 按照下方**严格定义的输出格式**返回结果。---## 输出 json 格式结果 (关键且必须遵守)你**必须**以一个合法的 **JSON 对象**格式返回结果。该对象**必须**包含一个名为 `'subtitles'` 的键,其值是一个字幕列表。列表中的每个元素**必须且只能**包含以下三个键:| 输出键 (Key) | 类型 (Type) | 说明 || :------------- | :----------- | :------------------------------------------------------------------------------------------------------------- || `'start'` | `float` | **必须存在**。取自该句**第一个字/词**的 `start` 时间。 || `'end'` | `float` | **必须存在**。取自该句**最后一个字/词**的 `end` 时间。 || `'text'` | `str` | **必须存在**。合并、修正、优化并添加标点后的**完整字幕文本**。**【这是最重要的键,绝对不能使用 'word' 或其他任何名称。】** |**严格禁止**:输出的字典中**不应**出现 `'word'` 键。输入的 `'word'` 内容经过处理后,统一存放于 `'text'` 键中。---## 示例:演示核心处理原则 (适用于所有语言)**重要提示**: 以下示例旨在阐明您需要遵循的**处理逻辑和输出格式**。这些原则是通用的,您必须将它们应用于您在用户输入中识别出的**任何语言**,而不仅仅是示例中的语言。### 原则演示 1#### 用户输入```[ {'word': 'so', 'start': 0.5, 'end': 0.7}, {'word': 'uh', 'start': 0.9, 'end': 1.0}, {'word': 'whatis', 'start': 1.2, 'end': 1.6}, {'word': 'your', 'start': 1.7, 'end': 1.9}, {'word': 'plan', 'start': 2.0, 'end': 2.4}]```#### 你的 JSON 输出```json{ "subtitles": [ { "start": 0.5, "end": 2.4, "text": "So, what is your plan?" } ]}```### 原则演示 2#### 用户输入```[ {'word': '这', 'start': 2.1, 'end': 2.2}, {'word': '里是', 'start': 2.3, 'end': 2.6}, {'word': '机', 'start': 2.8, 'end': 2.9}, {'word': '场吗', 'start': 3.0, 'end': 3.5}, {'word': '以经', 'start': 4.2, 'end': 4.5}, {'word': '很晚', 'start': 4.6, 'end': 5.0}]```#### 你的 JSON 输出```json{ "subtitles": [ { "start": 2.1, "end": 3.5, "text": "这里是机场吗?" }, { "start": 4.2, "end": 5.0, "text": "已经很晚了。" } ]}```---## 执行前最终检查在你生成最终答案之前,请在内部进行最后一次检查,确保你的输出 **100%** 符合以下规则:1. **最终输出是否是合法的 json 对象`{...}`?** -> (是/否)2. **该 JSON 对象是否包含一个名为 `'subtitles'` 的键?** -> (是/否)3. **`'subtitles'` 的值是否是一个列表 `[...]`,且列表中的每一个元素都是一个合法的 JSON 对象`{...}`?** -> (是/否)4. **列表中的每个字典是否都只包含 `'start'`, `'end'`, `'text'` 这三个键?** -> (是/否)5. **最关键的一点:键名是否是 `'text'`,而不是 `'word'`?** -> (是/否)**只有当以上所有问题的答案都是“是”时,才生成你的最终输出。**