• 文件 >
  • 使用 Emformer RNN-T 的裝置端 ASR >
  • 舊版本 (穩定)
快捷方式

使用 Emformer RNN-T 進行裝置端 ASR

作者: Moto Hira, Jeff Hwang.

本教程展示如何使用 Emformer RNN-T 和流式 API 在流式裝置輸入(例如筆記型電腦上的麥克風)上執行語音識別。

注意

本教程需要 FFmpeg 庫。詳細資訊請參閱FFmpeg 依賴

注意

本教程已在裝有 Windows 10 的 MacBook Pro 和 Dynabook 上測試。

本教程無法在 Google Colab 上執行,因為執行此教程的伺服器沒有您可以對著說話的麥克風。

1. 概述

我們使用流式 API 從音訊裝置(麥克風)逐塊獲取音訊,然後使用 Emformer RNN-T 執行推理。

關於流式 API 和 Emformer RNN-T 的基本用法,請參閱StreamReader 基本用法使用 Emformer RNN-T 進行線上 ASR

2. 檢查支援的裝置

首先,我們需要檢查流式 API 可以訪問的裝置,並找出我們需要傳遞給 StreamReader() 類的引數(srcformat)。

我們為此使用 ffmpeg 命令。ffmpeg 抽象了底層硬體實現之間的差異,但 format 的預期值因作業系統而異,並且每個 format 定義了不同的 src 語法。

有關支援的 format 值和 src 語法的詳細資訊,請參閱 https://ffmpeg.org/ffmpeg-devices.html

對於 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

我們將為流式 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}"

在上述情況下,可以使用以下值從麥克風流式傳輸。

StreamReader(
    src = "audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{BF2B8AE1-10B8-4CA4-A0DC-D02E18A56177}",
    format = "dshow",
)

3. 資料採集

從麥克風輸入流式傳輸音訊需要正確計時的資料採集。否則可能會在資料流中引入不連續性。

因此,我們將在子程序中執行資料採集。

首先,我們建立一個幫助函式,封裝在子程序中執行的整個過程。

此函式初始化流式 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)

與非裝置流式傳輸的顯著區別在於,我們為 stream 方法提供了 timeoutbackoff 引數。

採集資料時,如果採集請求的速率高於硬體準備資料的速率,則底層實現會報告特殊錯誤程式碼,並期望客戶端程式碼重試。

精確的時序是流暢流式傳輸的關鍵。在重試之前,將此錯誤從低層實現一直報告回 Python 層會增加不必要的開銷。因此,重試行為是在 C++ 層實現的,而 timeoutbackoff 引數允許客戶端程式碼控制此行為。

有關 timeoutbackoff 引數的詳細資訊,請參閱 stream() 方法的文件。

注意

backoff 的適當值取決於系統配置。判斷 backoff 值是否合適的一種方法是將採集到的連續音訊塊儲存下來並收聽。如果 backoff 值過大,則資料流不連續。結果音訊聽起來會加速。如果 backoff 值過小或為零,則音訊流正常,但資料採集過程會進入忙等待狀態,從而增加 CPU 消耗。

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.hypotheses = None

    def infer(self, segment: torch.Tensor) -> str:
        """Perform streaming inference"""
        features, length = self.feature_extractor(segment)
        self.hypotheses, self.state = self.decoder.infer(
            features, length, self.beam_width, state=self.state, hypothesis=self.hypotheses
        )
        transcript = self.token_processor(self.hypotheses[0][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. 主程序

主程序的執行流程如下:

  1. 初始化推理流水線。

  2. 啟動資料採集子程序。

  3. 執行推理。

  4. 清理

注意

由於資料採集子程序將使用 “spawn” 方法啟動,因此全域性範圍內的所有程式碼也將在此子程序上執行。

我們只想在主程序中例項化流水線,因此我們將它們放在一個函式中,並在 __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="\r", 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

Tag: torchaudio.io

指令碼總執行時間: ( 0 分 0.000 秒)

由 Sphinx-Gallery 生成的相簿

文件

訪問全面的 PyTorch 開發者文件

檢視文件

教程

獲取針對初學者和高階開發者的深度教程

檢視教程

資源

查詢開發資源並獲得問題解答

檢視資源