OpenAI的Whisper模型在语音识别领域无疑是革命性的,它能以惊人的准确率将音频转为文字。然而,对于长视频或复杂对话,其自动断句和标点符号功能有时会不尽人意,常常生成不便于阅读的大段文字。
本文将提供一个解决方案:结合Whisper的字级时间戳功能与大语言模型(LLM)的强大理解能力,打造一个能智能断句、优化文本并输出结构化数据的全自动字幕处理管道。
从 Whisper 获取"原料"------ 字级时间戳
要让LLM精确地为新句子赋予起止时间,我必须先从Whisper获取每个字或词的时间信息。这需要开启一个特定参数。
在使用Whisper进行识别时,务必将 word_timestamps
参数设为 True
。以Python的openai-whisper
库为例:
python
import whisper
model = whisper.load_model("base")
# 开启 word_timestamps 选项
result = model.transcribe("audio.mp3", word_timestamps=True)
result
中会包含一个 segments
列表,每个 segment 里又有 words
列表。我需要的数据就在这里。接下来,我将这些数据组装成一个干净的、专为LLM设计的JSON列表。
python
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个字词为一块。
python
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 chunks
word_chunks = create_chunks(word_level_timestamps, 500)
高级技巧 :为了避免在句子中间粗暴地切断,更好的分块策略是,在达到 chunk_size
附近时,寻找一个字词间隙(end
到下一个start
时间差)最大的地方进行切分。这能提高LLM处理每一块时的上下文完整性。
编写高质量的 LLM 提示词
提示词是整个流程的灵魂,它直接决定了输出的质量和稳定性。一个优秀的提示词应该包含以下几个要素:
- 清晰的角色与目标:明确告知LLM它的身份(如"AI字幕处理引擎")和唯一任务。
- 详细的处理流程:分步描述它需要做什么,包括识别语言、智能分段、文本修正、添加标点等。
- 极其严格的输出格式定义:使用表格、代码块等方式,精确定义输出的JSON结构、键名、值类型,并强调哪些是"必须"和"禁止"的。
- 提供示例:给出1-2个包含输入和预期输出的完整示例。这能极大地帮助模型理解任务,尤其是在处理特殊情况(如修正错别字、移除口头禅)时。
- 内置最终检查清单:在Prompt末尾让模型进行自我检查,这是一种强大的心理暗示,能有效提升输出格式的遵循度。
最终优化出的提示词,正是遵循了以上所有原则的典范。(具体提示词见底部)
结构化调用的常见问题与解决方案
陷阱一:指令与数据混为一谈
问题描述:初学者常常将长篇的提示词指令和海量的JSON数据拼接成一个巨大的字符串,然后作为单条消息发送给LLM。
症状:LLM返回错误,抱怨"输入格式不符合要求",因为它看到的是一个混合了自然语言和JSON的复杂文本,而不是它被告知要处理的纯JSON数据。
swift
{ "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'
的消息中。
python
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列表 (以 []
包裹),就会产生指令冲突。
ini
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" }
)
错误的提示词
markdown
## 输出 **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秒,或两条字幕的起止时间是否重叠。
附录:最终系统提示词
markdown
# 角色与最终目标
你是一位顶级的 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'`?** -> (是/否)
**只有当以上所有问题的答案都是"是"时,才生成你的最终输出。**