注意
单击此处下载完整的示例代码
使用 Emformer RNN-T 的设备 ASR¶
作者:Moto Hira、Jeff Hwang。
本教程介绍如何使用 Emformer RNN-T 和流式处理 API 在流式处理设备 input(即 Microphone)上执行语音识别 在笔记本电脑上。
注意
本教程需要 FFmpeg 库。 请参考 FFmpeg 依赖 细节。
注意
本教程已在装有 Windows 10 的 MacBook Pro 和 Dynabook 上进行了测试。
本教程在 Google Colab 上不起作用,因为运行 本教程没有您可以与之交谈的麦克风。
1. 概述¶
我们使用流式处理 API 从音频设备(麦克风)获取音频 逐个块,然后使用 Emformer RNN-T 运行推理。
对于流式处理 API 和 Emformer RNN-T 的基本用法 请参考 StreamReader 基本使用和使用 Emformer RNN-T 的在线 ASR。
2. 检查支持的设备¶
首先,我们需要检查 Streaming API 可以访问的设备,
并找出我们需要传递的参数 ( 和 )
到类。
src
format
我们使用 command 来实现此目的。 抽象掉
底层硬件实现的差异,但预期的
的值因操作系统而异,每个定义
的不同语法。ffmpeg
ffmpeg
format
format
src
支持的值和语法的详细信息可以
在 https://ffmpeg.org/ffmpeg-devices.html 中找到。format
src
对于 macOS,以下命令将列出可用设备。
$ ffmpeg -f avfoundation -list_devices true -i dummy
...
[AVFoundation indev @ 0x126e049d0] AVFoundation video devices:
[AVFoundation indev @ 0x126e049d0] [0] FaceTime HD Camera
[AVFoundation indev @ 0x126e049d0] [1] Capture screen 0
[AVFoundation indev @ 0x126e049d0] AVFoundation audio devices:
[AVFoundation indev @ 0x126e049d0] [0] ZoomAudioDevice
[AVFoundation indev @ 0x126e049d0] [1] MacBook Pro Microphone
我们将对 Streaming API 使用以下值。
StreamReader(
src = ":1", # no video, audio from device 1, "MacBook Pro Microphone"
format = "avfoundation",
)
对于 Windows,设备应该可以正常工作。dshow
> ffmpeg -f dshow -list_devices true -i dummy
...
[dshow @ 000001adcabb02c0] DirectShow video devices (some may be both video and audio devices)
[dshow @ 000001adcabb02c0] "TOSHIBA Web Camera - FHD"
[dshow @ 000001adcabb02c0] Alternative name "@device_pnp_\\?\usb#vid_10f1&pid_1a42&mi_00#7&27d916e6&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"
[dshow @ 000001adcabb02c0] DirectShow audio devices
[dshow @ 000001adcabb02c0] "... (Realtek High Definition Audio)"
[dshow @ 000001adcabb02c0] Alternative name "@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{BF2B8AE1-10B8-4CA4-A0DC-D02E18A56177}"
在上述情况下,以下值可用于从 microphone 流式传输。
StreamReader(
src = "audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{BF2B8AE1-10B8-4CA4-A0DC-D02E18A56177}",
format = "dshow",
)
3. 数据采集¶
从麦克风输入流式传输音频需要正确计时数据 收购。如果不这样做,可能会在 数据流。
因此,我们将在子流程中运行数据采集。
首先,我们创建一个 helper 函数,它将整个 进程。
此函数初始化流式处理 API,然后获取数据 将其放入主进程正在监视的队列中。
import torch
import torchaudio
# The data acquisition process will stop after this number of steps.
# This eliminates the need of process synchronization and makes this
# tutorial simple.
NUM_ITER = 100
def stream(q, format, src, segment_length, sample_rate):
from torchaudio.io import StreamReader
print("Building StreamReader...")
streamer = StreamReader(src, format=format)
streamer.add_basic_audio_stream(frames_per_chunk=segment_length, sample_rate=sample_rate)
print(streamer.get_src_stream_info(0))
print(streamer.get_out_stream_info(0))
print("Streaming...")
print()
stream_iterator = streamer.stream(timeout=-1, backoff=1.0)
for _ in range(NUM_ITER):
(chunk,) = next(stream_iterator)
q.put(chunk)
与非设备流式处理的显著区别在于,
我们提供 和 parameters 给 method.timeout
backoff
stream
获取数据时,如果获取请求率较高 比硬件可以准备数据的时间长,则 底层实现会报告特殊的错误代码,并期望 client 代码重试。
精确的计时是平滑流式处理的关键。报告此错误
从低级实现一直回到 Python 层,
before retry 会增加不需要的开销。
因此,重试行为在 C++ 层中实现,并且参数允许客户端代码控制
行为。timeout
backoff
详细参数请参考
到 method 的文档。
timeout
backoff
注意
的正确值取决于系统配置。
查看值是否合适的一种方法是将
系列获取的 chunk 作为连续音频并收听它。
如果 value 太大,则数据流是不连续的。
由此产生的音频声音加快了。
如果 value 太小或为零,则音频流正常。
但是 Data Acquisition 过程进入 busy-waiting 状态,并且
这会增加 CPU 消耗。backoff
backoff
backoff
backoff
4. 构建推理管道¶
下一步是创建推理所需的组件。
这与使用 Emformer RNN-T 的在线 ASR 的过程相同。
class Pipeline:
"""Build inference pipeline from RNNTBundle.
Args:
bundle (torchaudio.pipelines.RNNTBundle): Bundle object
beam_width (int): Beam size of beam search decoder.
"""
def __init__(self, bundle: torchaudio.pipelines.RNNTBundle, beam_width: int = 10):
self.bundle = bundle
self.feature_extractor = bundle.get_streaming_feature_extractor()
self.decoder = bundle.get_decoder()
self.token_processor = bundle.get_token_processor()
self.beam_width = beam_width
self.state = None
self.hypothesis = None
def infer(self, segment: torch.Tensor) -> str:
"""Perform streaming inference"""
features, length = self.feature_extractor(segment)
hypos, self.state = self.decoder.infer(
features, length, self.beam_width, state=self.state, hypothesis=self.hypothesis
)
self.hypothesis = hypos[0]
transcript = self.token_processor(self.hypothesis[0], lstrip=False)
return transcript
class ContextCacher:
"""Cache the end of input data and prepend the next input data with it.
Args:
segment_length (int): The size of main segment.
If the incoming segment is shorter, then the segment is padded.
context_length (int): The size of the context, cached and appended.
"""
def __init__(self, segment_length: int, context_length: int):
self.segment_length = segment_length
self.context_length = context_length
self.context = torch.zeros([context_length])
def __call__(self, chunk: torch.Tensor):
if chunk.size(0) < self.segment_length:
chunk = torch.nn.functional.pad(chunk, (0, self.segment_length - chunk.size(0)))
chunk_with_context = torch.cat((self.context, chunk))
self.context = chunk[-self.context_length :]
return chunk_with_context
5. 主要过程¶
主进程的执行流程如下:
初始化推理管道。
启动 Data acquisition 子流程。
运行推理。
收拾
注意
由于数据采集子流程将使用 “spawn” 方法启动,因此全局范围内的所有代码都在子流程上执行 也。
我们只想在主进程中实例化 pipeline, 所以我们把它们放在一个函数中,并在 __name__ == “__main__” 守卫中调用它。
def main(device, src, bundle):
print(torch.__version__)
print(torchaudio.__version__)
print("Building pipeline...")
pipeline = Pipeline(bundle)
sample_rate = bundle.sample_rate
segment_length = bundle.segment_length * bundle.hop_length
context_length = bundle.right_context_length * bundle.hop_length
print(f"Sample rate: {sample_rate}")
print(f"Main segment: {segment_length} frames ({segment_length / sample_rate} seconds)")
print(f"Right context: {context_length} frames ({context_length / sample_rate} seconds)")
cacher = ContextCacher(segment_length, context_length)
@torch.inference_mode()
def infer():
for _ in range(NUM_ITER):
chunk = q.get()
segment = cacher(chunk[:, 0])
transcript = pipeline.infer(segment)
print(transcript, end="", flush=True)
import torch.multiprocessing as mp
ctx = mp.get_context("spawn")
q = ctx.Queue()
p = ctx.Process(target=stream, args=(q, device, src, segment_length, sample_rate))
p.start()
infer()
p.join()
if __name__ == "__main__":
main(
device="avfoundation",
src=":1",
bundle=torchaudio.pipelines.EMFORMER_RNNT_BASE_LIBRISPEECH,
)
Building pipeline...
Sample rate: 16000
Main segment: 2560 frames (0.16 seconds)
Right context: 640 frames (0.04 seconds)
Building StreamReader...
SourceAudioStream(media_type='audio', codec='pcm_f32le', codec_long_name='PCM 32-bit floating point little-endian', format='flt', bit_rate=1536000, sample_rate=48000.0, num_channels=1)
OutputStream(source_index=0, filter_description='aresample=16000,aformat=sample_fmts=fltp')
Streaming...
hello world
脚本总运行时间:(0 分 0.000 秒)