Whisper 音频转录和说话人分离(说话人识别)
如何使用 OpenAI 的 Whisper 转录和说话人分离音频文件
什么是 Whisper?
Whisper 是 OpenAI 提供的最先进的语音识别系统,训练了 680,000 小时的多语言和多任务监督数据,这些数据从网上收集而来。这个大而多样的数据集提高了对口音、背景噪音和技术语言的鲁棒性。此外,它还能进行多种语言的转录,以及从这些语言翻译成英文。OpenAI 发布了这些模型和代码,以作为构建有用的语音识别应用程序的基础。
Whisper 的一个很大的缺点是它不能告诉你对话中谁在说话。这在分析对话时是个问题。这就是说话人分离的作用所在。说话人分离是识别对话中谁在说话的过程。
在本教程中,你将学习如何识别说话人,然后将它们与 Whisper 的转录匹配起来。我们将使用 pyannote-audio
来完成这一任务。让我们开始吧!
准备音频文件
首先,我们需要准备音频文件。我们将使用 Lex Fridman 与 Yann 的播客的前 20 分钟内容来做示例。为了下载视频并提取音频,我们将使用 yt-dlp
包。
!pip install -U yt-dlp
我们还需要安装 ffmpeg
!wget -O - -q https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz | xz -qdc | tar -x
现在我们可以通过命令行进行实际的下载和音频提取。
!yt-dlp -xv --ffmpeg-location ffmpeg-master-latest-linux64-gpl/bin --audio-format wav -o download.wav -- https://youtu.be/SGzMElJ11Cc
现在我们有了 download.wav
文件在工作目录中。让我们裁剪音频的前 20 分钟。我们可以使用 pydub 包做几行代码来实现这一点。
!pip install pydub
from pydub import AudioSegment
t1 = 0 * 1000 # 以毫秒为单位
t2 = 20 * 60 * 1000
newAudio = AudioSegment.from_wav("download.wav")
a = newAudio[t1:t2]
a.export("audio.wav", format="wav")
audio.wav
现在就是音频文件的前 20 分钟。
Pyannote 的说话人分离
pyannote.audio
是一个用 Python 编写的开源工具包,用于说话人分离。基于 PyTorch 机器学习框架,它提供了一套可训练的端到端神经网络构建块,可以组合和联合优化以构建说话人分离流水线。pyannote.audio
还附带了预训练模型和流水线,涵盖了对语音活动检测、说话人分段、重叠语音检测、说话人嵌入的广泛应用,达到了大多数情况下的最先进性能。
安装 Pyannote 并在视频音频上运行以生成说话人分离数据。
!pip install pyannote.audio
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained('pyannote/speaker-diarization')
DEMO_FILE = {'uri': 'blabal', 'audio': 'audio.wav'}
dz = pipeline(DEMO_FILE)
with open("diarization.txt", "w") as text_file:
text_file.write(str(dz))
让我们打印出来看看效果。
print(*list(dz.itertracks(yield_label=True))[:10], sep="\n")
输出结果:
(<Segment(2.03344, 36.8128)>, 0, 'SPEAKER_00')
(<Segment(38.1122, 51.3759)>, 0, 'SPEAKER_00')
(<Segment(51.8653, 90.2053)>, 1, 'SPEAKER_01')
(<Segment(91.2853, 92.9391)>, 1, 'SPEAKER_01')
(<Segment(94.8628, 116.497)>, 0, 'SPEAKER_00')
(<Segment(116.497, 124.124)>, 1, 'SPEAKER_01')
(<Segment(124.192, 151.597)>, 1, 'SPEAKER_01')
(<Segment(152.018, 179.12)>, 1, 'SPEAKER_01')
(<Segment(180.318, 194.037)>, 1, 'SPEAKER_01')
(<Segment(195.016, 207.385)>, 0, 'SPEAKER_00')
这看起来已经不错了,但让我们再简单处理一下数据:
def millisec(timeStr):
spl = timeStr.split(":")
s = (int)((int(spl[0]) * 60 * 60 + int(spl[1]) * 60 + float(spl[2]) )* 1000)
return s
import re
dz = open('diarization.txt').read().splitlines()
dzList = []
for l in dz:
start, end = tuple(re.findall('[0-9]+:[0-9]+:[0-9]+\.[0-9]+', string=l))
start = millisec(start) - spacermilli
end = millisec(end) - spacermilli
lex = not re.findall('SPEAKER_01', string=l)
dzList.append([start, end, lex])
print(*dzList[:10], sep='\n')
[33, 34812, True]
[36112, 49375, True]
[49865, 88205, False]
[89285, 90939, False]
[92862, 114496, True]
[114496, 122124, False]
[122191, 149596, False]
[150018, 177119, False]
[178317, 192037, False]
[193015, 205385, True]
现在我们已经用一个列表表示了说话人分离数据。前两个数字是说话者片段的开始和结束时间(以毫秒为单位)。第三个布尔值表示是否为 Lex 说话。
根据说话人分离准备音频文件
接下来,我们根据说话人分离数据附加音频片段,以空白作为分隔符。
from pydub import AudioSegment
import re
sounds = spacer
segments = []
dz = open('diarization.txt').read().splitlines()
for l in dz:
start, end = tuple(re.findall('[0-9]+:[0-9]+:[0-9]+\.[0-9]+', string=l))
start = int(millisec(start)) # 毫秒
end = int(millisec(end)) # 毫秒
segments.append(len(sounds))
sounds = sounds.append(audio[start:end], crossfade=0)
sounds = sounds.append(spacer, crossfade=0)
sounds.export("dz.wav", format="wav") # 导出为当前路径下的 wav 文件。
print(segments[:8])
[2000, 38779, 54042, 94382, 98036, 121670, 131297, 160702]
使用 Whisper 进行转录
接下来,我们将使用 Whisper 转录音频文件的不同片段。重要提示:pyannote.audio
存在版本冲突,导致错误。我们的解决方法是先运行 Pyannote,然后再运行 Whisper。可以安全地忽略该错误。
安装 OpenAI Whisper。
!pip install git+https://github.com/openai/whisper.git
在准备好的音频文件上运行 OpenAI Whisper。它将转录结果写入文件。你可以根据需要调整模型大小。你可以在 Github 的模型卡片上找到所有模型。
!whisper dz.wav --language en --model base
以下是中文翻译:
[00:00.000 --> 00:04.720] 以下是与Yann LeCun的对话,
[00:04.720 --> 00:06.560] 这是他第二次参加播客。
[00:06.560 --> 00:11.160] 他是Meta(前身为Facebook)的首席AI科学家,
[00:11.160 --> 00:15.040] 纽约大学教授,图灵奖得主,
[00:15.040 --> 00:17.600] 机器学习和人工智能历史上
[00:17.600 --> 00:20.460] 重要的人物之一,
...
为了处理.vtt文件,我们需要安装webvtt-py库。
!pip install -U webvtt-py
我们来看一下数据:
import webvtt
captions = [[(int)(millisec(caption.start)), (int)(millisec(caption.end)), caption.text] for caption in webvtt.read('dz.wav.vtt')]
print(*captions[:8], sep='\n')
[0, 4720, '以下是与Yann LeCun的对话,']
[4720, 6560, '这是他第二次参加播客。']
[6560, 11160, '他是Meta(前身为Facebook)的首席AI科学家,']
[11160, 15040, '纽约大学教授,图灵奖得主,']
[15040, 17600, '机器学习和人工智能历史上']
[17600, 20460, '重要的人物之一,']
[20460, 23940, '他才华横溢且见解独到']
[23940, 25400, '以最好的方式,']
...
匹配转录和分段
接下来,我们将匹配每行转录到一些分段,并通过生成HTML文件来显示所有内容。为了获得正确的时间,我们应注意原始 音频中不在任何分段部分的部分。我们为音频中的每个段落附加一个新的div.
# 我们需要这个为我们的HTML文件(主要只是一些样式)
preS = '<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <meta http-equiv="X-UA-Compatible" content="ie=edge">\n <title>Lexicap</title>\n <style>\n body {\n font-family: sans-serif;\n font-size: 18px;\n color: #111;\n padding: 0 0 1em 0;\n }\n .l {\n color: #050;\n }\n .s {\n display: inline-block;\n }\n .e {\n display: inline-block;\n }\n .t {\n display: inline-block;\n }\n #player {\n\t\tposition: sticky;\n\t\ttop: 20px;\n\t\tfloat: right;\n\t}\n </style>\n </head>\n <body>\n <h2>Yann LeCun: Dark Matter of Intelligence and Self-Supervised Learning | Lex Fridman Podcast #258</h2>\n <div id="player"></div>\n <script>\n var tag = document.createElement(\'script\');\n tag.src = "https://www.youtube.com/iframe_api";\n var firstScriptTag = document.getElementsByTagName(\'script\')[0];\n firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);\n var player;\n function onYouTubeIframeAPIReady() {\n player = new YT.Player(\'player\', {\n height: \'210\',\n width: \'340\',\n videoId: \'SGzMElJ11Cc\',\n });\n }\n function setCurrentTime(timepoint) {\n player.seekTo(timepoint);\n player.playVideo();\n }\n </script><br>\n'
postS = '\t</body>\n</html>'
from datetime import timedelta
html = list(preS)
for i in range(len(segments)):
idx = 0
for idx in range(len(captions)):
if captions[idx][0] >= (segments[i] - spacermilli):
break;
while (idx < (len(captions))) and ((i == len(segments) - 1) or (captions[idx][1] < segments[i+1])):
c = captions[idx]
start = dzList[i][0] + (c[0] -segments[i])
if start < 0:
start = 0
idx += 1
start = start / 1000.0
startStr = '{0:02d}:{1:02d}:{2:02.2f}'.format((int)(start // 3600),
(int)(start % 3600 // 60),
start % 60)
html.append('\t\t\t<div class="c">\n')
html.append(f'\t\t\t\t<a class="l" href="#{startStr}" id="{startStr}">link</a> |\n')
html.append(f'\t\t\t\t<div class="s"><a href="javascript:void(0);" onclick=setCurrentTime({int(start)})>{startStr}</a></div>\n')
html.append(f'\t\t\t\t<div class="t">{"[Lex]" if dzList[i][2] else "[Yann]"} {c[2]}</div>\n')
html.append('\t\t\t</div>\n\n')
html.append(postS)
s = "".join(html)
with open("lexicap.html", "w") as text_file:
text_file.write(s)
print(s)
加入LabLab Discord
在lablab discord,我们讨论这个代码库和许多其他与人工智能相关的主题!查看即将举行的人工智能黑客马拉松活动