From 26aca70b96c2c2f9ac5b26025c5a7473160921e4 Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Wed, 17 Nov 2021 10:05:53 -0500 Subject: [PATCH 0001/1144] fbshipit-source-id: b6de6faa2f66e50ab47a2fba3c46674a828d6d0a From 501cbd42ac66f46f9ea64a08d72906f66a4d41c1 Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Wed, 17 Nov 2021 10:29:19 -0500 Subject: [PATCH 0002/1144] fbshipit-source-id: 4f215e22c8fea8225bab37bac26bde44ae66e8a0 From 1f423f294b6c284c7c94f75a24888a0de258e550 Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Wed, 17 Nov 2021 10:33:05 -0500 Subject: [PATCH 0003/1144] fbshipit-source-id: da75a22e9a21a07c40738c9ae097b04d6440ff4f From 8af01f0af3a2fb2ed6d5748fb107ea730bb307d7 Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Wed, 17 Nov 2021 10:42:55 -0500 Subject: [PATCH 0004/1144] fbshipit-source-id: 0bf4d4a2ef7a2f6bf9eae87ed39a1b4dceb0bf65 From 2a5fe5ff332848034e80ef44b95dbc8f9738032b Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Wed, 17 Nov 2021 11:44:36 -0800 Subject: [PATCH 0005/1144] Remove facebook folder in wav2vec unittests (#2015) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2015 as titled Reviewed By: hwangjeff, mthrok Differential Revision: D32495691 fbshipit-source-id: 60d8a2337585e3147f24ca9f0b6518e30cd9134a --- .../wav2vec2-base-10k-voxpopuli.json | 68 ++++++++++++++++ .../huggingface/wav2vec2-base-960h.json | 68 ++++++++++++++++ .../wav2vec2/huggingface/wav2vec2-base.json | 77 +++++++++++++++++++ .../wav2vec2-large-960h-lv60-self.json | 68 ++++++++++++++++ .../huggingface/wav2vec2-large-960h-lv60.json | 68 ++++++++++++++++ .../huggingface/wav2vec2-large-960h.json | 68 ++++++++++++++++ .../huggingface/wav2vec2-large-lv60.json | 68 ++++++++++++++++ .../wav2vec2-large-xlsr-53-german.json | 68 ++++++++++++++++ .../huggingface/wav2vec2-large-xlsr-53.json | 75 ++++++++++++++++++ .../wav2vec2/huggingface/wav2vec2-large.json | 68 ++++++++++++++++ .../wav2vec2/huggingface_intergration_test.py | 20 ++--- 11 files changed, 706 insertions(+), 10 deletions(-) create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-10k-voxpopuli.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-960h.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60-self.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-lv60.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53-german.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53.json create mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large.json diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-10k-voxpopuli.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-10k-voxpopuli.json new file mode 100644 index 0000000000..927428cb10 --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-10k-voxpopuli.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2Model" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": false, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": false, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "group", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 768, + "initializer_range": 0.02, + "intermediate_size": 3072, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 12, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 12, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-960h.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-960h.json new file mode 100644 index 0000000000..3a1c7f1a4b --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base-960h.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2ForCTC" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": false, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": false, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "group", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 768, + "initializer_range": 0.02, + "intermediate_size": 3072, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 12, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 12, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base.json new file mode 100644 index 0000000000..7927c2e4a9 --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-base.json @@ -0,0 +1,77 @@ +{ + "activation_dropout": 0.0, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2Model" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": false, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": false, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_norm": "group", + "feat_proj_dropout": 0.1, + "final_dropout": 0.0, + "freeze_feat_extract_train": true, + "gradient_checkpointing": true, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_size": 768, + "initializer_range": 0.02, + "intermediate_size": 3072, + "layer_norm_eps": 1e-05, + "layerdrop": 0.05, + "mask_channel_length": 10, + "mask_channel_min_space": 1, + "mask_channel_other": 0.0, + "mask_channel_prob": 0.0, + "mask_channel_selection": "static", + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_min_space": 1, + "mask_time_other": 0.0, + "mask_time_prob": 0.05, + "mask_time_selection": "static", + "model_type": "wav2vec2", + "no_mask_channel_overlap": false, + "no_mask_time_overlap": false, + "num_attention_heads": 12, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 12, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60-self.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60-self.json new file mode 100644 index 0000000000..e9d79893ac --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60-self.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2ForCTC" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": true, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": true, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "layer", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60.json new file mode 100644 index 0000000000..e9d79893ac --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h-lv60.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2ForCTC" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": true, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": true, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "layer", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h.json new file mode 100644 index 0000000000..e50233c00e --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-960h.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2ForCTC" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": false, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": false, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "group", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-lv60.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-lv60.json new file mode 100644 index 0000000000..ba3d2c2e21 --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-lv60.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2Model" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": true, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": true, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "layer", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53-german.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53-german.json new file mode 100644 index 0000000000..120f142b86 --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53-german.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2ForCTC" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": true, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": true, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "layer", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 36 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53.json new file mode 100644 index 0000000000..80f0f61e8e --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large-xlsr-53.json @@ -0,0 +1,75 @@ +{ + "activation_dropout": 0.0, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2Model" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": true, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": true, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "layer", + "feat_proj_dropout": 0.1, + "final_dropout": 0.0, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_channel_length": 10, + "mask_channel_min_space": 1, + "mask_channel_other": 0.0, + "mask_channel_prob": 0.0, + "mask_channel_selection": "static", + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_min_space": 1, + "mask_time_other": 0.0, + "mask_time_prob": 0.075, + "mask_time_selection": "static", + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large.json new file mode 100644 index 0000000000..db1635eb0c --- /dev/null +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/wav2vec2-large.json @@ -0,0 +1,68 @@ +{ + "activation_dropout": 0.1, + "apply_spec_augment": true, + "architectures": [ + "Wav2Vec2Model" + ], + "attention_dropout": 0.1, + "bos_token_id": 1, + "conv_bias": false, + "conv_dim": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "conv_kernel": [ + 10, + 3, + 3, + 3, + 3, + 2, + 2 + ], + "conv_stride": [ + 5, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "ctc_loss_reduction": "sum", + "ctc_zero_infinity": false, + "do_stable_layer_norm": false, + "eos_token_id": 2, + "feat_extract_activation": "gelu", + "feat_extract_dropout": 0.0, + "feat_extract_norm": "group", + "feat_proj_dropout": 0.1, + "final_dropout": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout": 0.1, + "hidden_dropout_prob": 0.1, + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "layerdrop": 0.1, + "mask_feature_length": 10, + "mask_feature_prob": 0.0, + "mask_time_length": 10, + "mask_time_prob": 0.05, + "model_type": "wav2vec2", + "num_attention_heads": 16, + "num_conv_pos_embedding_groups": 16, + "num_conv_pos_embeddings": 128, + "num_feat_extract_layers": 7, + "num_hidden_layers": 24, + "pad_token_id": 0, + "transformers_version": "4.5.1", + "vocab_size": 32 +} diff --git a/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py b/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py index ff177b7cfa..49a7a88c21 100644 --- a/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py +++ b/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py @@ -26,17 +26,17 @@ def _name_func(testcase_func, i, param): # Pretrained -HF_BASE = _load_config('facebook', 'wav2vec2-base') -HF_LARGE = _load_config('facebook', 'wav2vec2-large') -HF_LARGE_LV60 = _load_config('facebook', 'wav2vec2-large-lv60') -HF_LARGE_XLSR_53 = _load_config('facebook', 'wav2vec2-large-xlsr-53') -HF_BASE_10K_VOXPOPULI = _load_config('facebook', 'wav2vec2-base-10k-voxpopuli') +HF_BASE = _load_config('wav2vec2-base') +HF_LARGE = _load_config('wav2vec2-large') +HF_LARGE_LV60 = _load_config('wav2vec2-large-lv60') +HF_LARGE_XLSR_53 = _load_config('wav2vec2-large-xlsr-53') +HF_BASE_10K_VOXPOPULI = _load_config('wav2vec2-base-10k-voxpopuli') # Finetuned -HF_BASE_960H = _load_config('facebook', 'wav2vec2-base-960h') -HF_LARGE_960H = _load_config('facebook', 'wav2vec2-large-960h') -HF_LARGE_LV60_960H = _load_config('facebook', 'wav2vec2-large-960h-lv60') -HF_LARGE_LV60_SELF_960H = _load_config('facebook', 'wav2vec2-large-960h-lv60-self') -HF_LARGE_XLSR_DE = _load_config('facebook', 'wav2vec2-large-xlsr-53-german') +HF_BASE_960H = _load_config('wav2vec2-base-960h') +HF_LARGE_960H = _load_config('wav2vec2-large-960h') +HF_LARGE_LV60_960H = _load_config('wav2vec2-large-960h-lv60') +HF_LARGE_LV60_SELF_960H = _load_config('wav2vec2-large-960h-lv60-self') +HF_LARGE_XLSR_DE = _load_config('wav2vec2-large-xlsr-53-german') # Config and corresponding factory functions PRETRAIN_CONFIGS = parameterized.expand([ From 7cc6745699d24f0deea9da2d2e76d5e7020ed3ae Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Thu, 18 Nov 2021 13:28:39 -0500 Subject: [PATCH 0006/1144] fbshipit-source-id: 8fd4e205d6dd6403e559ea6b3aa846b0bd732a00 From b4184dc6dcc80be70fd902b21f22d9bb430774ce Mon Sep 17 00:00:00 2001 From: Facebook Community Bot Date: Thu, 18 Nov 2021 13:42:12 -0500 Subject: [PATCH 0007/1144] Re-sync with internal repository (#2017) Co-authored-by: Facebook Community Bot <6422482+facebook-github-bot@users.noreply.github.com> --- .../facebook/wav2vec2-base-10k-voxpopuli.json | 68 ---------------- .../facebook/wav2vec2-base-960h.json | 68 ---------------- .../huggingface/facebook/wav2vec2-base.json | 77 ------------------- .../wav2vec2-large-960h-lv60-self.json | 68 ---------------- .../facebook/wav2vec2-large-960h-lv60.json | 68 ---------------- .../facebook/wav2vec2-large-960h.json | 68 ---------------- .../facebook/wav2vec2-large-lv60.json | 68 ---------------- .../wav2vec2-large-xlsr-53-german.json | 68 ---------------- .../facebook/wav2vec2-large-xlsr-53.json | 75 ------------------ .../huggingface/facebook/wav2vec2-large.json | 68 ---------------- tools/release_notes/classify_prs.py | 4 +- 11 files changed, 2 insertions(+), 698 deletions(-) delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-10k-voxpopuli.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-960h.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60-self.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-lv60.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53-german.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53.json delete mode 100644 test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large.json diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-10k-voxpopuli.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-10k-voxpopuli.json deleted file mode 100644 index 927428cb10..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-10k-voxpopuli.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2Model" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": false, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": false, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "group", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 768, - "initializer_range": 0.02, - "intermediate_size": 3072, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 12, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 12, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-960h.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-960h.json deleted file mode 100644 index 3a1c7f1a4b..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base-960h.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2ForCTC" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": false, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": false, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "group", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 768, - "initializer_range": 0.02, - "intermediate_size": 3072, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 12, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 12, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base.json deleted file mode 100644 index 7927c2e4a9..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-base.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "activation_dropout": 0.0, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2Model" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": false, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": false, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_norm": "group", - "feat_proj_dropout": 0.1, - "final_dropout": 0.0, - "freeze_feat_extract_train": true, - "gradient_checkpointing": true, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_size": 768, - "initializer_range": 0.02, - "intermediate_size": 3072, - "layer_norm_eps": 1e-05, - "layerdrop": 0.05, - "mask_channel_length": 10, - "mask_channel_min_space": 1, - "mask_channel_other": 0.0, - "mask_channel_prob": 0.0, - "mask_channel_selection": "static", - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_min_space": 1, - "mask_time_other": 0.0, - "mask_time_prob": 0.05, - "mask_time_selection": "static", - "model_type": "wav2vec2", - "no_mask_channel_overlap": false, - "no_mask_time_overlap": false, - "num_attention_heads": 12, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 12, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60-self.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60-self.json deleted file mode 100644 index e9d79893ac..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60-self.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2ForCTC" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": true, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": true, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "layer", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60.json deleted file mode 100644 index e9d79893ac..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h-lv60.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2ForCTC" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": true, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": true, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "layer", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h.json deleted file mode 100644 index e50233c00e..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-960h.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2ForCTC" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": false, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": false, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "group", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-lv60.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-lv60.json deleted file mode 100644 index ba3d2c2e21..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-lv60.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2Model" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": true, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": true, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "layer", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53-german.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53-german.json deleted file mode 100644 index 120f142b86..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53-german.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2ForCTC" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": true, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": true, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "layer", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 36 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53.json deleted file mode 100644 index 80f0f61e8e..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large-xlsr-53.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "activation_dropout": 0.0, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2Model" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": true, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": true, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "layer", - "feat_proj_dropout": 0.1, - "final_dropout": 0.0, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_channel_length": 10, - "mask_channel_min_space": 1, - "mask_channel_other": 0.0, - "mask_channel_prob": 0.0, - "mask_channel_selection": "static", - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_min_space": 1, - "mask_time_other": 0.0, - "mask_time_prob": 0.075, - "mask_time_selection": "static", - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large.json b/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large.json deleted file mode 100644 index db1635eb0c..0000000000 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/facebook/wav2vec2-large.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "activation_dropout": 0.1, - "apply_spec_augment": true, - "architectures": [ - "Wav2Vec2Model" - ], - "attention_dropout": 0.1, - "bos_token_id": 1, - "conv_bias": false, - "conv_dim": [ - 512, - 512, - 512, - 512, - 512, - 512, - 512 - ], - "conv_kernel": [ - 10, - 3, - 3, - 3, - 3, - 2, - 2 - ], - "conv_stride": [ - 5, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "ctc_loss_reduction": "sum", - "ctc_zero_infinity": false, - "do_stable_layer_norm": false, - "eos_token_id": 2, - "feat_extract_activation": "gelu", - "feat_extract_dropout": 0.0, - "feat_extract_norm": "group", - "feat_proj_dropout": 0.1, - "final_dropout": 0.1, - "gradient_checkpointing": false, - "hidden_act": "gelu", - "hidden_dropout": 0.1, - "hidden_dropout_prob": 0.1, - "hidden_size": 1024, - "initializer_range": 0.02, - "intermediate_size": 4096, - "layer_norm_eps": 1e-05, - "layerdrop": 0.1, - "mask_feature_length": 10, - "mask_feature_prob": 0.0, - "mask_time_length": 10, - "mask_time_prob": 0.05, - "model_type": "wav2vec2", - "num_attention_heads": 16, - "num_conv_pos_embedding_groups": 16, - "num_conv_pos_embeddings": 128, - "num_feat_extract_layers": 7, - "num_hidden_layers": 24, - "pad_token_id": 0, - "transformers_version": "4.5.1", - "vocab_size": 32 -} diff --git a/tools/release_notes/classify_prs.py b/tools/release_notes/classify_prs.py index f9bc9e04ef..2790c5fd6b 100644 --- a/tools/release_notes/classify_prs.py +++ b/tools/release_notes/classify_prs.py @@ -48,7 +48,7 @@ def get_labels(col_name, labels): df[col_name] = [[] for _ in range(len(df))] - for i, row in df.iterrows(): + for _, row in df.iterrows(): row[col_name] = "None" for label in labels: if label in row["labels"]: @@ -77,7 +77,7 @@ def get_labels(col_name, labels): if secondary_df.empty: continue print(f"### {secondary_labels_mapping[secondary_label]}") - for idx, row in secondary_df.iterrows(): + for _, row in secondary_df.iterrows(): print(f"- {row['title']}") print() print() From 78ce7010e1512d21efeee72be084964cf36ff7b7 Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Thu, 18 Nov 2021 12:12:04 -0800 Subject: [PATCH 0008/1144] Add Emformer RNN-T model (#2003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Adds streaming-capable recurrent neural network transducer (RNN-T) model that uses Emformer for its transcription network. Includes two factory functions — one that allows for building a custom model, and one that builds a preconfigured base model. Pull Request resolved: https://github.com/pytorch/audio/pull/2003 Reviewed By: nateanl Differential Revision: D32440879 Pulled By: hwangjeff fbshipit-source-id: 601cb1de368427f25e3b7d120e185960595d2360 --- docs/source/prototype.rst | 43 +- .../prototype/rnnt_cpu_test.py | 13 + .../prototype/rnnt_gpu_test.py | 15 + .../prototype/rnnt_test_impl.py | 331 ++++++++ torchaudio/prototype/__init__.py | 3 +- torchaudio/prototype/rnnt.py | 789 ++++++++++++++++++ 6 files changed, 1185 insertions(+), 9 deletions(-) create mode 100644 test/torchaudio_unittest/prototype/rnnt_cpu_test.py create mode 100644 test/torchaudio_unittest/prototype/rnnt_gpu_test.py create mode 100644 test/torchaudio_unittest/prototype/rnnt_test_impl.py create mode 100644 torchaudio/prototype/rnnt.py diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index 60e11aa863..82cc97be2e 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -1,17 +1,17 @@ .. role:: hidden :class: hidden-section -torchaudio.prototype.emformer -============================= +torchaudio.prototype +==================== -.. py:module:: torchaudio.prototype.emformer +.. py:module:: torchaudio.prototype -.. currentmodule:: torchaudio.prototype.emformer +.. currentmodule:: torchaudio.prototype -Emformer is a prototype feature; see `here `_ -for more information on prototype features. -It is available only within nightly builds and must be imported -explicitly, e.g. via ``from torchaudio.prototype.emformer import Emformer``. +``torchaudio.prototype`` provides prototype features; +see `here `_ for more information on prototype features. +The module is available only within nightly builds and must be imported +explicitly, e.g. ``import torchaudio.prototype``. Emformer ~~~~~~~~ @@ -22,6 +22,33 @@ Emformer .. automethod:: infer + +RNNT +~~~~ + +.. autoclass:: RNNT + + .. automethod:: forward + + .. automethod:: transcribe_streaming + + .. automethod:: transcribe + + .. automethod:: predict + + .. automethod:: join + +emformer_rnnt_base +~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_base + +emformer_rnnt_model +~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_model + + References ~~~~~~~~~~ diff --git a/test/torchaudio_unittest/prototype/rnnt_cpu_test.py b/test/torchaudio_unittest/prototype/rnnt_cpu_test.py new file mode 100644 index 0000000000..93d28bacaa --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_cpu_test.py @@ -0,0 +1,13 @@ +import torch +from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl +from torchaudio_unittest.common_utils import PytorchTestCase + + +class RNNTFloat32CPUTest(RNNTTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cpu") + + +class RNNTFloat64CPUTest(RNNTTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/prototype/rnnt_gpu_test.py b/test/torchaudio_unittest/prototype/rnnt_gpu_test.py new file mode 100644 index 0000000000..04f476e6ee --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_gpu_test.py @@ -0,0 +1,15 @@ +import torch +from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl +from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + + +@skipIfNoCuda +class RNNTFloat32GPUTest(RNNTTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cuda") + + +@skipIfNoCuda +class RNNTFloat64GPUTest(RNNTTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/prototype/rnnt_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_test_impl.py new file mode 100644 index 0000000000..ba68aa7b03 --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_test_impl.py @@ -0,0 +1,331 @@ +import torch +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script +from torchaudio.prototype.rnnt import emformer_rnnt_model + + +class RNNTTestImpl(TestBaseMixin): + def _get_input_config(self): + model_config = self._get_model_config() + return { + "batch_size": 8, + "max_input_length": 61, + "num_symbols": model_config["num_symbols"], + "max_target_length": 23, + "input_dim": model_config["input_dim"], + "right_context_length": model_config["right_context_length"], + "encoding_dim": model_config["encoding_dim"], + "joiner_max_input_length": 61 // model_config["time_reduction_stride"], + "segment_length": model_config["segment_length"], + "time_reduction_stride": model_config["time_reduction_stride"], + } + + def _get_model_config(self): + return { + "input_dim": 80, + "encoding_dim": 128, + "num_symbols": 256, + "segment_length": 16, + "right_context_length": 4, + "time_reduction_input_dim": 128, + "time_reduction_stride": 4, + "transformer_num_heads": 4, + "transformer_ffn_dim": 64, + "transformer_num_layers": 3, + "transformer_dropout": 0.0, + "transformer_activation": "relu", + "transformer_left_context_length": 30, + "transformer_max_memory_size": 0, + "transformer_weight_init_scale_strategy": "depthwise", + "transformer_tanh_on_mem": True, + "symbol_embedding_dim": 64, + "num_lstm_layers": 2, + "lstm_layer_norm": True, + "lstm_layer_norm_epsilon": 1e-3, + "lstm_dropout": 0.0, + } + + def _get_model(self): + return ( + emformer_rnnt_model(**self._get_model_config()) + .to(device=self.device, dtype=self.dtype) + .eval() + ) + + def _get_transcriber_input(self): + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + max_input_length = input_config["max_input_length"] + input_dim = input_config["input_dim"] + right_context_length = input_config["right_context_length"] + + torch.random.manual_seed(31) + input = torch.rand( + batch_size, max_input_length + right_context_length, input_dim + ).to(device=self.device, dtype=self.dtype) + lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) + return input, lengths + + def _get_transcriber_streaming_input(self): + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + segment_length = input_config["segment_length"] + input_dim = input_config["input_dim"] + right_context_length = input_config["right_context_length"] + + torch.random.manual_seed(31) + input = torch.rand( + batch_size, segment_length + right_context_length, input_dim + ).to(device=self.device, dtype=self.dtype) + lengths = torch.randint( + 1, segment_length + right_context_length + 1, (batch_size,) + ).to(device=self.device, dtype=torch.int32) + return input, lengths + + def _get_predictor_input(self): + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + num_symbols = input_config["num_symbols"] + max_target_length = input_config["max_target_length"] + + torch.random.manual_seed(31) + input = torch.randint(0, num_symbols, (batch_size, max_target_length)).to( + device=self.device, dtype=torch.int32 + ) + lengths = torch.randint(1, max_target_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) + return input, lengths + + def _get_joiner_input(self): + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + joiner_max_input_length = input_config["joiner_max_input_length"] + max_target_length = input_config["max_target_length"] + input_dim = input_config["encoding_dim"] + + torch.random.manual_seed(31) + utterance_encodings = torch.rand( + batch_size, joiner_max_input_length, input_dim + ).to(device=self.device, dtype=self.dtype) + utterance_lengths = torch.randint( + 0, joiner_max_input_length + 1, (batch_size,) + ).to(device=self.device, dtype=torch.int32) + target_encodings = torch.rand(batch_size, max_target_length, input_dim).to( + device=self.device, dtype=self.dtype + ) + target_lengths = torch.randint(0, max_target_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) + return utterance_encodings, utterance_lengths, target_encodings, target_lengths + + def test_torchscript_consistency_forward(self): + r"""Verify that scripting RNNT does not change the behavior of method `forward`.""" + inputs, input_lengths = self._get_transcriber_input() + targets, target_lengths = self._get_predictor_input() + + rnnt = self._get_model() + scripted = torch_script(rnnt).eval() + + ref_state, scripted_state = None, None + for _ in range(2): + ref_out, ref_input_lengths, ref_target_lengths, ref_state = rnnt( + inputs, input_lengths, targets, target_lengths, ref_state + ) + ( + scripted_out, + scripted_input_lengths, + scripted_target_lengths, + scripted_state, + ) = scripted(inputs, input_lengths, targets, target_lengths, scripted_state) + + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_input_lengths, scripted_input_lengths) + self.assertEqual(ref_target_lengths, scripted_target_lengths) + self.assertEqual(ref_state, scripted_state) + + def test_torchscript_consistency_transcribe(self): + r"""Verify that scripting RNNT does not change the behavior of method `transcribe`.""" + input, lengths = self._get_transcriber_input() + + rnnt = self._get_model() + scripted = torch_script(rnnt) + + ref_out, ref_lengths = rnnt.transcribe(input, lengths) + scripted_out, scripted_lengths = scripted.transcribe(input, lengths) + + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_lengths, scripted_lengths) + + def test_torchscript_consistency_transcribe_streaming(self): + r"""Verify that scripting RNNT does not change the behavior of method `transcribe_streaming`.""" + input, lengths = self._get_transcriber_streaming_input() + + rnnt = self._get_model() + scripted = torch_script(rnnt) + + ref_state, scripted_state = None, None + for _ in range(2): + ref_out, ref_lengths, ref_state = rnnt.transcribe_streaming( + input, lengths, ref_state + ) + ( + scripted_out, + scripted_lengths, + scripted_state, + ) = scripted.transcribe_streaming(input, lengths, scripted_state) + + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_lengths, scripted_lengths) + self.assertEqual(ref_state, scripted_state) + + def test_torchscript_consistency_predict(self): + r"""Verify that scripting RNNT does not change the behavior of method `predict`.""" + input, lengths = self._get_predictor_input() + + rnnt = self._get_model() + scripted = torch_script(rnnt) + + ref_state, scripted_state = None, None + for _ in range(2): + ref_out, ref_lengths, ref_state = rnnt.predict(input, lengths, ref_state) + scripted_out, scripted_lengths, scripted_state = scripted.predict( + input, lengths, scripted_state + ) + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_lengths, scripted_lengths) + self.assertEqual(ref_state, scripted_state) + + def test_torchscript_consistency_join(self): + r"""Verify that scripting RNNT does not change the behavior of method `join`.""" + ( + utterance_encodings, + utterance_lengths, + target_encodings, + target_lengths, + ) = self._get_joiner_input() + + rnnt = self._get_model() + scripted = torch_script(rnnt) + + ref_out, ref_src_lengths, ref_tgt_lengths = rnnt.join( + utterance_encodings, utterance_lengths, target_encodings, target_lengths + ) + scripted_out, scripted_src_lengths, scripted_tgt_lengths = scripted.join( + utterance_encodings, utterance_lengths, target_encodings, target_lengths + ) + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_src_lengths, scripted_src_lengths) + self.assertEqual(ref_tgt_lengths, scripted_tgt_lengths) + + def test_output_shape_forward(self): + r"""Check that method `forward` produces correctly-shaped outputs.""" + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + joiner_max_input_length = input_config["joiner_max_input_length"] + max_target_length = input_config["max_target_length"] + num_symbols = input_config["num_symbols"] + + inputs, input_lengths = self._get_transcriber_input() + targets, target_lengths = self._get_predictor_input() + + rnnt = self._get_model() + + state = None + for _ in range(2): + out, out_lengths, target_lengths, state = rnnt( + inputs, input_lengths, targets, target_lengths, state + ) + self.assertEqual( + (batch_size, joiner_max_input_length, max_target_length, num_symbols), + out.shape, + ) + self.assertEqual((batch_size,), out_lengths.shape) + self.assertEqual((batch_size,), target_lengths.shape) + + def test_output_shape_transcribe(self): + r"""Check that method `transcribe` produces correctly-shaped outputs.""" + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + max_input_length = input_config["max_input_length"] + + input, lengths = self._get_transcriber_input() + + model_config = self._get_model_config() + encoding_dim = model_config["encoding_dim"] + time_reduction_stride = model_config["time_reduction_stride"] + rnnt = self._get_model() + + out, out_lengths = rnnt.transcribe(input, lengths) + self.assertEqual( + (batch_size, max_input_length // time_reduction_stride, encoding_dim), + out.shape, + ) + self.assertEqual((batch_size,), out_lengths.shape) + + def test_output_shape_transcribe_streaming(self): + r"""Check that method `transcribe_streaming` produces correctly-shaped outputs.""" + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + segment_length = input_config["segment_length"] + encoding_dim = input_config["encoding_dim"] + time_reduction_stride = input_config["time_reduction_stride"] + + input, lengths = self._get_transcriber_streaming_input() + + rnnt = self._get_model() + + state = None + for _ in range(2): + out, out_lengths, state = rnnt.transcribe_streaming(input, lengths, state) + self.assertEqual( + (batch_size, segment_length // time_reduction_stride, encoding_dim), + out.shape, + ) + self.assertEqual((batch_size,), out_lengths.shape) + + def test_output_shape_predict(self): + r"""Check that method `predict` produces correctly-shaped outputs.""" + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + max_target_length = input_config["max_target_length"] + + model_config = self._get_model_config() + encoding_dim = model_config["encoding_dim"] + input, lengths = self._get_predictor_input() + + rnnt = self._get_model() + + state = None + for _ in range(2): + out, out_lengths, state = rnnt.predict(input, lengths, state) + self.assertEqual((batch_size, max_target_length, encoding_dim), out.shape) + self.assertEqual((batch_size,), out_lengths.shape) + + def test_output_shape_join(self): + r"""Check that method `join` produces correctly-shaped outputs.""" + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + joiner_max_input_length = input_config["joiner_max_input_length"] + max_target_length = input_config["max_target_length"] + num_symbols = input_config["num_symbols"] + + ( + utterance_encodings, + utterance_lengths, + target_encodings, + target_lengths, + ) = self._get_joiner_input() + + rnnt = self._get_model() + + out, src_lengths, tgt_lengths = rnnt.join( + utterance_encodings, utterance_lengths, target_encodings, target_lengths + ) + self.assertEqual( + (batch_size, joiner_max_input_length, max_target_length, num_symbols), + out.shape, + ) + self.assertEqual((batch_size,), src_lengths.shape) + self.assertEqual((batch_size,), tgt_lengths.shape) diff --git a/torchaudio/prototype/__init__.py b/torchaudio/prototype/__init__.py index fc544bf863..57f549d740 100644 --- a/torchaudio/prototype/__init__.py +++ b/torchaudio/prototype/__init__.py @@ -1,4 +1,5 @@ from .emformer import Emformer +from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model -__all__ = ["Emformer"] +__all__ = ["Emformer", "RNNT", "emformer_rnnt_base", "emformer_rnnt_model"] diff --git a/torchaudio/prototype/rnnt.py b/torchaudio/prototype/rnnt.py new file mode 100644 index 0000000000..9bcf88f6aa --- /dev/null +++ b/torchaudio/prototype/rnnt.py @@ -0,0 +1,789 @@ +from typing import List, Optional, Tuple + +import torch + +from .emformer import Emformer + + +__all__ = ["RNNT", "emformer_rnnt_base", "emformer_rnnt_model"] + + +class _TimeReduction(torch.nn.Module): + r"""Coalesces frames along time dimension into a + fewer number of frames with higher feature dimensionality. + + Args: + stride (int): number of frames to merge for each output frame. + """ + def __init__(self, stride: int) -> None: + super().__init__() + self.stride = stride + + def forward( + self, input: torch.Tensor, lengths: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + r"""Forward pass. + + B: batch size; + T: maximum input sequence length in batch; + D: feature dimension of each input sequence frame. + + Args: + input (torch.Tensor): input sequences, with shape `(B, T, D)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor + output sequences, with shape + `(B, T // stride, D * stride)` + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in output sequences. + """ + B, T, D = input.shape + num_frames = T - (T % self.stride) + input = input[:, :num_frames, :] + lengths = lengths.div(self.stride, rounding_mode="trunc") + T_max = num_frames // self.stride + + output = input.reshape(B, T_max, D * self.stride) + output = output.contiguous() + return output, lengths + + +class _CustomLSTM(torch.nn.Module): + r"""Custom long-short-term memory (LSTM) block that applies layer normalization + to internal nodes. + + Args: + input_dim (int): input dimension. + hidden_dim (int): hidden dimension. + layer_norm (bool, optional): if ``True``, enables layer normalization. (Default: ``False``) + layer_norm_epsilon (float, optional): value of epsilon to use in + layer normalization layers (Default: 1e-5) + """ + def __init__( + self, + input_dim: int, + hidden_dim: int, + layer_norm: bool = False, + layer_norm_epsilon: float = 1e-5, + ) -> None: + super().__init__() + self.x2g = torch.nn.Linear(input_dim, 4 * hidden_dim, bias=(not layer_norm)) + self.p2g = torch.nn.Linear(hidden_dim, 4 * hidden_dim, bias=False) + if layer_norm: + self.c_norm = torch.nn.LayerNorm(hidden_dim, eps=layer_norm_epsilon) + self.g_norm = torch.nn.LayerNorm(4 * hidden_dim, eps=layer_norm_epsilon) + else: + self.c_norm = torch.nn.Identity() + self.g_norm = torch.nn.Identity() + + self.hidden_dim = hidden_dim + + def forward( + self, input: torch.Tensor, state: Optional[List[torch.Tensor]] + ) -> Tuple[torch.Tensor, List[torch.Tensor]]: + r"""Forward pass. + + B: batch size; + T: maximum sequence length in batch; + D: feature dimension of each input sequence element. + + Args: + input (torch.Tensor): with shape `(T, B, D)`. + state (List[torch.Tensor] or None): list of tensors + representing internal state generated in preceding invocation + of ``forward``. + + Returns: + (torch.Tensor, List[torch.Tensor]): + torch.Tensor + output, with shape `(T, B, hidden_dim)`. + List[torch.Tensor] + list of tensors representing internal state generated + in current invocation of ``forward``. + """ + if state is None: + B = input.size(1) + h = torch.zeros(B, self.hidden_dim, device=input.device, dtype=input.dtype) + c = torch.zeros(B, self.hidden_dim, device=input.device, dtype=input.dtype) + else: + h, c = state + + gated_input = self.x2g(input) + outputs = [] + for gates in gated_input.unbind(0): + gates = gates + self.p2g(h) + gates = self.g_norm(gates) + input_gate, forget_gate, cell_gate, output_gate = gates.chunk(4, 1) + input_gate = input_gate.sigmoid() + forget_gate = forget_gate.sigmoid() + cell_gate = cell_gate.tanh() + output_gate = output_gate.sigmoid() + c = forget_gate * c + input_gate * cell_gate + c = self.c_norm(c) + h = output_gate * c.tanh() + outputs.append(h) + + output = torch.stack(outputs, dim=0) + state = [h, c] + + return output, state + + +class _Transcriber(torch.nn.Module): + r"""Recurrent neural network transducer (RNN-T) transcription network. + + Args: + input_dim (int): feature dimension of each input sequence element. + output_dim (int): feature dimension of each output sequence element. + segment_length (int): length of input segment expressed as number of frames. + right_context_length (int): length of right context expressed as number of frames. + time_reduction_input_dim (int): dimension to scale each element in input sequences to + prior to applying time reduction block. + time_reduction_stride (int): factor by which to reduce length of input sequence. + transformer_num_heads (int): number of attention heads in each Emformer layer. + transformer_ffn_dim (int): hidden layer dimension of each Emformer layer's feedforward network. + transformer_num_layers (int): number of Emformer layers to instantiate. + transformer_left_context_length (int): length of left context. + transformer_dropout (float, optional): transformer dropout probability. (Default: 0.0) + transformer_activation (str, optional): activation function to use in each Emformer layer's + feedforward network. Must be one of ("relu", "gelu", "silu"). (Default: "relu") + transformer_max_memory_size (int, optional): maximum number of memory elements to use. (Default: 0) + transformer_weight_init_scale_strategy (str, optional): per-layer weight initialization scaling + strategy. Must be one of ("depthwise", "constant", ``None``). (Default: "depthwise") + transformer_tanh_on_mem (bool, optional): if ``True``, applies tanh to memory elements. (Default: ``False``) + """ + + def __init__( + self, + *, + input_dim: int, + output_dim: int, + segment_length: int, + right_context_length: int, + time_reduction_input_dim: int, + time_reduction_stride: int, + transformer_num_heads: int, + transformer_ffn_dim: int, + transformer_num_layers: int, + transformer_left_context_length: int, + transformer_dropout: float = 0.0, + transformer_activation: str = "relu", + transformer_max_memory_size: int = 0, + transformer_weight_init_scale_strategy: str = "depthwise", + transformer_tanh_on_mem: bool = False, + ) -> None: + super().__init__() + self.input_linear = torch.nn.Linear( + input_dim, time_reduction_input_dim, bias=False, + ) + self.time_reduction = _TimeReduction(time_reduction_stride) + transformer_input_dim = time_reduction_input_dim * time_reduction_stride + self.transformer = Emformer( + transformer_input_dim, + transformer_num_heads, + transformer_ffn_dim, + transformer_num_layers, + dropout=transformer_dropout, + activation=transformer_activation, + left_context_length=transformer_left_context_length, + right_context_length=right_context_length // time_reduction_stride, + segment_length=segment_length // time_reduction_stride, + max_memory_size=transformer_max_memory_size, + weight_init_scale_strategy=transformer_weight_init_scale_strategy, + tanh_on_mem=transformer_tanh_on_mem, + ) + self.output_linear = torch.nn.Linear(transformer_input_dim, output_dim) + self.layer_norm = torch.nn.LayerNorm(output_dim) + + def forward( + self, input: torch.Tensor, lengths: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + r"""Forward pass for training. + + B: batch size; + T: maximum input sequence length in batch; + D: feature dimension of each input sequence frame (input_dim). + + Args: + input (torch.Tensor): input frame sequences right-padded with right context, with + shape `(B, T + right context length, D)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor + output frame sequences, with + shape `(B, T // time_reduction_stride, output_dim)`. + torch.Tensor + output input lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output frame sequences. + """ + input_linear_out = self.input_linear(input) + time_reduction_out, time_reduction_lengths = self.time_reduction( + input_linear_out, lengths + ) + transformer_out, transformer_lengths = self.transformer( + time_reduction_out, time_reduction_lengths + ) + output_linear_out = self.output_linear(transformer_out) + layer_norm_out = self.layer_norm(output_linear_out) + return layer_norm_out, transformer_lengths + + @torch.jit.export + def infer( + self, + input: torch.Tensor, + lengths: torch.Tensor, + states: Optional[List[List[torch.Tensor]]], + ) -> Tuple[torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: + r"""Forward pass for inference. + + B: batch size; + T: maximum input sequence segment length in batch; + D: feature dimension of each input sequence frame (input_dim). + + Args: + input (torch.Tensor): input frame sequence segments right-padded with right context, with + shape `(B, T + right context length, D)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + state (List[List[torch.Tensor]] or None): list of lists of tensors + representing internal state generated in preceding invocation + of ``infer``. + + Returns: + (torch.Tensor, torch.Tensor, List[List[torch.Tensor]]): + torch.Tensor + output frame sequences, with + shape `(B, T // time_reduction_stride, output_dim)`. + torch.Tensor + output input lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output. + List[List[torch.Tensor]] + output states; list of lists of tensors + representing internal state generated in current invocation + of ``infer``. + """ + input_linear_out = self.input_linear(input) + time_reduction_out, time_reduction_lengths = self.time_reduction( + input_linear_out, lengths + ) + ( + transformer_out, + transformer_lengths, + transformer_states, + ) = self.transformer.infer(time_reduction_out, time_reduction_lengths, states) + output_linear_out = self.output_linear(transformer_out) + layer_norm_out = self.layer_norm(output_linear_out) + return layer_norm_out, transformer_lengths, transformer_states + + +class _Predictor(torch.nn.Module): + r"""Recurrent neural network transducer (RNN-T) transcription network. + + Args: + num_symbols (int): size of target token lexicon. + output_dim (int): feature dimension of each output sequence element. + symbol_embedding_dim (int): dimension of each target token embedding. + num_lstm_layers (int): number of LSTM layers to instantiate. + lstm_layer_norm (bool, optional): if ``True``, enables layer normalization + for LSTM layers. (Default: ``False``) + lstm_layer_norm_epsilon (float, optional): value of epsilon to use in + LSTM layer normalization layers. (Default: 1e-5) + lstm_dropout (float, optional): LSTM dropout probability. (Default: 0.0) + + """ + def __init__( + self, + num_symbols: int, + output_dim: int, + symbol_embedding_dim: int, + num_lstm_layers: int, + lstm_layer_norm: bool = False, + lstm_layer_norm_epsilon: float = 1e-5, + lstm_dropout: float = 0.0, + ) -> None: + super().__init__() + self.embedding = torch.nn.Embedding(num_symbols, symbol_embedding_dim) + self.input_layer_norm = torch.nn.LayerNorm(symbol_embedding_dim) + self.lstm_layers = torch.nn.ModuleList( + [ + _CustomLSTM( + symbol_embedding_dim, + symbol_embedding_dim, + layer_norm=lstm_layer_norm, + layer_norm_epsilon=lstm_layer_norm_epsilon, + ) + for idx in range(num_lstm_layers) + ] + ) + self.dropout = torch.nn.Dropout(p=lstm_dropout) + self.linear = torch.nn.Linear(symbol_embedding_dim, output_dim) + self.output_layer_norm = torch.nn.LayerNorm(output_dim) + + self.lstm_dropout = lstm_dropout + + def forward( + self, + input: torch.Tensor, + lengths: torch.Tensor, + state: Optional[List[List[torch.Tensor]]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: + r"""Forward pass. + + B: batch size; + U: maximum sequence length in batch; + D: feature dimension of each input sequence element. + + Args: + input (torch.Tensor): target sequences, with shape `(B, U)` and each element + mapping to a target symbol, i.e. in range `[0, num_symbols)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + state (List[List[torch.Tensor]] or None, optional): list of lists of tensors + representing internal state generated in preceding invocation + of ``forward``. (Default: ``None``) + + Returns: + (torch.Tensor, torch.Tensor, List[List[torch.Tensor]]): + torch.Tensor + output encoding sequences, with shape `(B, U, output_dim)` + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output encoding sequences. + List[List[torch.Tensor]] + output states; list of lists of tensors + representing internal state generated in current invocation of ``forward``. + """ + input_tb = input.permute(1, 0) + embedding_out = self.embedding(input_tb) + input_layer_norm_out = self.input_layer_norm(embedding_out) + + lstm_out = input_layer_norm_out + state_out: List[List[torch.Tensor]] = [] + for layer_idx, lstm in enumerate(self.lstm_layers): + lstm_out, lstm_state_out = lstm( + lstm_out, None if state is None else state[layer_idx] + ) + lstm_out = self.dropout(lstm_out) + state_out.append(lstm_state_out) + + linear_out = self.linear(lstm_out) + output_layer_norm_out = self.output_layer_norm(linear_out) + return output_layer_norm_out.permute(1, 0, 2), lengths, state_out + + +class _Joiner(torch.nn.Module): + r"""Recurrent neural network transducer (RNN-T) joint network. + + Args: + input_dim (int): source and target input dimension. + output_dim (int): output dimension. + """ + + def __init__(self, input_dim: int, output_dim: int) -> None: + super().__init__() + self.linear = torch.nn.Linear(input_dim, output_dim, bias=True) + self.relu = torch.nn.ReLU() + + def forward( + self, + source_encodings: torch.Tensor, + source_lengths: torch.Tensor, + target_encodings: torch.Tensor, + target_lengths: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + r"""Forward pass for training. + + B: batch size; + T: maximum source sequence length in batch; + U: maximum target sequence length in batch; + D: dimension of each source and target sequence encoding. + + Args: + source_encodings (torch.Tensor): source encoding sequences, with + shape `(B, T, D)`. + source_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + valid sequence length of i-th batch element in ``source_encodings``. + target_encodings (torch.Tensor): target encoding sequences, with shape `(B, U, D)`. + target_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + valid sequence length of i-th batch element in ``target_encodings``. + + Returns: + (torch.Tensor, torch.Tensor, torch.Tensor): + torch.Tensor + joint network output, with shape `(B, T, U, D)`. + torch.Tensor + output source lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 1 for i-th batch element in joint network output. + torch.Tensor + output target lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 2 for i-th batch element in joint network output. + """ + joint_encodings = ( + source_encodings.unsqueeze(2).contiguous() + + target_encodings.unsqueeze(1).contiguous() + ) + relu_out = self.relu(joint_encodings) + output = self.linear(relu_out) + return output, source_lengths, target_lengths + + +class RNNT(torch.nn.Module): + r"""Recurrent neural network transducer (RNN-T) model. + + Note: + To build the model, please use one of the factory functions. + + Args: + transcriber (torch.nn.Module): transcription network. + predictor (torch.nn.Module): prediction network. + joiner (torch.nn.Module): joint network. + """ + + def __init__( + self, transcriber: _Transcriber, predictor: _Predictor, joiner: _Joiner + ) -> None: + super().__init__() + self.transcriber = transcriber + self.predictor = predictor + self.joiner = joiner + + def forward( + self, + sources: torch.Tensor, + source_lengths: torch.Tensor, + targets: torch.Tensor, + target_lengths: torch.Tensor, + predictor_state: Optional[List[List[torch.Tensor]]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: + r"""Forward pass for training. + + B: batch size; + T: maximum source sequence length in batch; + U: maximum target sequence length in batch; + D: feature dimension of each source sequence element. + + Args: + sources (torch.Tensor): source frame sequences right-padded with right context, with + shape `(B, T, D)`. + source_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``sources``. + targets (torch.Tensor): target sequences, with shape `(B, U)` and each element + mapping to a target symbol. + target_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``targets``. + predictor_state (List[List[torch.Tensor]] or None, optional): list of lists of tensors + representing prediction network internal state generated in preceding invocation + of ``forward``. (Default: ``None``) + + Returns: + (torch.Tensor, torch.Tensor, torch.Tensor, List[List[torch.Tensor]]): + torch.Tensor + joint network output, with shape + `(B, max output source length, max output target length, number of target symbols)`. + torch.Tensor + output source lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 1 for i-th batch element in joint network output. + torch.Tensor + output target lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 2 for i-th batch element in joint network output. + List[List[torch.Tensor]] + output states; list of lists of tensors + representing prediction network internal state generated in current invocation + of ``forward``. + """ + source_encodings, source_lengths = self.transcriber( + input=sources, lengths=source_lengths, + ) + target_encodings, target_lengths, predictor_state = self.predictor( + input=targets, lengths=target_lengths, state=predictor_state, + ) + output, source_lengths, target_lengths = self.joiner( + source_encodings=source_encodings, + source_lengths=source_lengths, + target_encodings=target_encodings, + target_lengths=target_lengths, + ) + + return ( + output, + source_lengths, + target_lengths, + predictor_state, + ) + + @torch.jit.export + def transcribe_streaming( + self, + sources: torch.Tensor, + source_lengths: torch.Tensor, + state: Optional[List[List[torch.Tensor]]], + ) -> Tuple[torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: + r"""Applies transcription network to sources in streaming mode. + + B: batch size; + T: maximum source sequence segment length in batch; + D: feature dimension of each source sequence frame. + + Args: + sources (torch.Tensor): source frame sequence segments right-padded with right context, with + shape `(B, T + right context length, D)`. + source_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``sources``. + state (List[List[torch.Tensor]] or None): list of lists of tensors + representing transcription network internal state generated in preceding invocation + of ``transcribe_streaming``. + + Returns: + (torch.Tensor, torch.Tensor, List[List[torch.Tensor]]): + torch.Tensor + output frame sequences, with + shape `(B, T // time_reduction_stride, output_dim)`. + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output. + List[List[torch.Tensor]] + output states; list of lists of tensors + representing transcription network internal state generated in current invocation + of ``transcribe_streaming``. + """ + return self.transcriber.infer(sources, source_lengths, state) + + @torch.jit.export + def transcribe( + self, sources: torch.Tensor, source_lengths: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + r"""Applies transcription network to sources in non-streaming mode. + + B: batch size; + T: maximum source sequence length in batch; + D: feature dimension of each source sequence frame. + + Args: + sources (torch.Tensor): source frame sequences right-padded with right context, with + shape `(B, T + right context length, D)`. + source_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``sources``. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor + output frame sequences, with + shape `(B, T // time_reduction_stride, output_dim)`. + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output frame sequences. + """ + return self.transcriber(sources, source_lengths) + + @torch.jit.export + def predict( + self, + targets: torch.Tensor, + target_lengths: torch.Tensor, + state: Optional[List[List[torch.Tensor]]], + ) -> Tuple[torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: + r"""Applies prediction network to targets. + + B: batch size; + U: maximum target sequence length in batch; + D: feature dimension of each target sequence frame. + + Args: + targets (torch.Tensor): target sequences, with shape `(B, U)` and each element + mapping to a target symbol, i.e. in range `[0, num_symbols)`. + target_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``targets``. + state (List[List[torch.Tensor]] or None): list of lists of tensors + representing internal state generated in preceding invocation + of ``predict``. + + Returns: + (torch.Tensor, torch.Tensor, List[List[torch.Tensor]]): + torch.Tensor + output frame sequences, with shape `(B, U, output_dim)`. + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid elements for i-th batch element in output. + List[List[torch.Tensor]] + output states; list of lists of tensors + representing internal state generated in current invocation of ``predict``. + """ + return self.predictor(input=targets, lengths=target_lengths, state=state) + + @torch.jit.export + def join( + self, + source_encodings: torch.Tensor, + source_lengths: torch.Tensor, + target_encodings: torch.Tensor, + target_lengths: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + r"""Applies joint network to source and target encodings. + + B: batch size; + T: maximum source sequence length in batch; + U: maximum target sequence length in batch; + D: dimension of each source and target sequence encoding. + + Args: + source_encodings (torch.Tensor): source encoding sequences, with + shape `(B, T, D)`. + source_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + valid sequence length of i-th batch element in ``source_encodings``. + target_encodings (torch.Tensor): target encoding sequences, with shape `(B, U, D)`. + target_lengths (torch.Tensor): with shape `(B,)` and i-th element representing + valid sequence length of i-th batch element in ``target_encodings``. + + Returns: + (torch.Tensor, torch.Tensor, torch.Tensor): + torch.Tensor + joint network output, with shape `(B, T, U, D)`. + torch.Tensor + output source lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 1 for i-th batch element in joint network output. + torch.Tensor + output target lengths, with shape `(B,)` and i-th element representing + number of valid elements along dim 2 for i-th batch element in joint network output. + """ + output, source_lengths, target_lengths = self.joiner( + source_encodings=source_encodings, + source_lengths=source_lengths, + target_encodings=target_encodings, + target_lengths=target_lengths, + ) + return output, source_lengths, target_lengths + + +def emformer_rnnt_model( + *, + input_dim: int, + encoding_dim: int, + num_symbols: int, + segment_length: int, + right_context_length: int, + time_reduction_input_dim: int, + time_reduction_stride: int, + transformer_num_heads: int, + transformer_ffn_dim: int, + transformer_num_layers: int, + transformer_dropout: float, + transformer_activation: str, + transformer_left_context_length: int, + transformer_max_memory_size: int, + transformer_weight_init_scale_strategy: str, + transformer_tanh_on_mem: bool, + symbol_embedding_dim: int, + num_lstm_layers: int, + lstm_layer_norm: bool, + lstm_layer_norm_epsilon: float, + lstm_dropout: float, +) -> RNNT: + r"""Builds Emformer-based recurrent neural network transducer (RNN-T) model. + + Note: + For non-streaming inference, the expectation is for `transcribe` to be called on input + sequences right-concatenated with `right_context_length` frames. + + For streaming inference, the expectation is for `transcribe_streaming` to be called + on input chunks comprising `segment_length` frames right-concatenated with `right_context_length` + frames. + + Args: + input_dim (int): dimension of input sequence frames passed to transcription network. + encoding_dim (int): dimension of transcription- and prediction-network-generated encodings + passed to joint network. + num_symbols (int): cardinality of set of target tokens. + segment_length (int): length of input segment expressed as number of frames. + right_context_length (int): length of right context expressed as number of frames. + time_reduction_input_dim (int): dimension to scale each element in input sequences to + prior to applying time reduction block. + time_reduction_stride (int): factor by which to reduce length of input sequence. + transformer_num_heads (int): number of attention heads in each Emformer layer. + transformer_ffn_dim (int): hidden layer dimension of each Emformer layer's feedforward network. + transformer_num_layers (int): number of Emformer layers to instantiate. + transformer_left_context_length (int): length of left context considered by Emformer. + transformer_dropout (float): Emformer dropout probability. + transformer_activation (str): activation function to use in each Emformer layer's + feedforward network. Must be one of ("relu", "gelu", "silu"). + transformer_max_memory_size (int): maximum number of memory elements to use. + transformer_weight_init_scale_strategy (str): per-layer weight initialization scaling + strategy. Must be one of ("depthwise", "constant", ``None``). + transformer_tanh_on_mem (bool): if ``True``, applies tanh to memory elements. + symbol_embedding_dim (int): dimension of each target token embedding. + num_lstm_layers (int): number of LSTM layers to instantiate. + lstm_layer_norm (bool): if ``True``, enables layer normalization for LSTM layers. + lstm_layer_norm_epsilon (float): value of epsilon to use in LSTM layer normalization layers. + lstm_dropout (float): LSTM dropout probability. + + Returns: + RNNT: + Emformer RNN-T model. + """ + transcriber = _Transcriber( + input_dim=input_dim, + output_dim=encoding_dim, + segment_length=segment_length, + right_context_length=right_context_length, + time_reduction_input_dim=time_reduction_input_dim, + time_reduction_stride=time_reduction_stride, + transformer_num_heads=transformer_num_heads, + transformer_ffn_dim=transformer_ffn_dim, + transformer_num_layers=transformer_num_layers, + transformer_dropout=transformer_dropout, + transformer_activation=transformer_activation, + transformer_left_context_length=transformer_left_context_length, + transformer_max_memory_size=transformer_max_memory_size, + transformer_weight_init_scale_strategy=transformer_weight_init_scale_strategy, + transformer_tanh_on_mem=transformer_tanh_on_mem, + ) + predictor = _Predictor( + num_symbols, + encoding_dim, + symbol_embedding_dim=symbol_embedding_dim, + num_lstm_layers=num_lstm_layers, + lstm_layer_norm=lstm_layer_norm, + lstm_layer_norm_epsilon=lstm_layer_norm_epsilon, + lstm_dropout=lstm_dropout, + ) + joiner = _Joiner(encoding_dim, num_symbols) + return RNNT(transcriber, predictor, joiner) + + +def emformer_rnnt_base() -> RNNT: + r"""Builds basic version of Emformer RNN-T model. + + Returns: + RNNT: + Emformer RNN-T model. + """ + return emformer_rnnt_model( + input_dim=80, + encoding_dim=1024, + num_symbols=4097, + segment_length=16, + right_context_length=4, + time_reduction_input_dim=128, + time_reduction_stride=4, + transformer_num_heads=8, + transformer_ffn_dim=2048, + transformer_num_layers=20, + transformer_dropout=0.1, + transformer_activation="gelu", + transformer_left_context_length=30, + transformer_max_memory_size=0, + transformer_weight_init_scale_strategy="depthwise", + transformer_tanh_on_mem=True, + symbol_embedding_dim=512, + num_lstm_layers=3, + lstm_layer_norm=True, + lstm_layer_norm_epsilon=1e-3, + lstm_dropout=0.3, + ) From b3830bac7ee2f59fa2e94a1090216629577c8ef3 Mon Sep 17 00:00:00 2001 From: krishnakalyan3 Date: Fri, 19 Nov 2021 00:26:43 -0800 Subject: [PATCH 0009/1144] Update to xavier_uniform and avoid legacy data.uniform_ initialization (#2018) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2018 Reviewed By: hwangjeff, mthrok Differential Revision: D32540498 Pulled By: nateanl fbshipit-source-id: f104fbf84c0f489906b6555f21f931b60fedb59e --- torchaudio/models/tacotron2.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/torchaudio/models/tacotron2.py b/torchaudio/models/tacotron2.py index 109d396a7f..bd0b863ff3 100644 --- a/torchaudio/models/tacotron2.py +++ b/torchaudio/models/tacotron2.py @@ -26,7 +26,6 @@ # ***************************************************************************** import warnings -from math import sqrt from typing import Tuple, List, Optional, Union import torch @@ -984,9 +983,7 @@ def __init__( self.n_mels = n_mels self.n_frames_per_step = n_frames_per_step self.embedding = nn.Embedding(n_symbol, symbol_embedding_dim) - std = sqrt(2.0 / (n_symbol + symbol_embedding_dim)) - val = sqrt(3.0) * std - self.embedding.weight.data.uniform_(-val, val) + torch.nn.init.xavier_uniform_(self.embedding.weight) self.encoder = _Encoder( encoder_embedding_dim, encoder_n_convolution, encoder_kernel_size ) From e076f37c132a040297c2af89d6da7babec3bcd7d Mon Sep 17 00:00:00 2001 From: krishnakalyan3 Date: Fri, 19 Nov 2021 03:48:21 -0800 Subject: [PATCH 0010/1144] Inplace initialisation of RNN weights (#2010) Summary: Ref: https://github.com/pytorch/audio/issues/1993 Pull Request resolved: https://github.com/pytorch/audio/pull/2010 Reviewed By: mthrok Differential Revision: D32539511 Pulled By: nateanl fbshipit-source-id: e99be963123cbc039d79bdb514450b7e8f5a84fc --- torchaudio/models/wavernn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchaudio/models/wavernn.py b/torchaudio/models/wavernn.py index 9f92061906..0a41a2aa96 100644 --- a/torchaudio/models/wavernn.py +++ b/torchaudio/models/wavernn.py @@ -174,7 +174,7 @@ def __init__(self, kernel_size=(1, scale * 2 + 1), padding=(0, scale), bias=False) - conv.weight.data.fill_(1. / (scale * 2 + 1)) + torch.nn.init.constant_(conv.weight, 1. / (scale * 2 + 1)) up_layers.append(stretch) up_layers.append(conv) self.upsample_layers = nn.Sequential(*up_layers) From 3ff46bfa6ce06e8a625f02a0763fca2887af2ef6 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 19 Nov 2021 07:48:33 -0800 Subject: [PATCH 0011/1144] Disable SPHINXOPT=-W for local env (#2013) Summary: With the introduction of tutorials, the turn around time for doc build has become longer. By default, the tutorial is not built but SPHINXOPT=-W treats it as error. This commit disable the option for the local build while keeping it for the CI. Pull Request resolved: https://github.com/pytorch/audio/pull/2013 Reviewed By: carolineechen Differential Revision: D32538952 Pulled By: mthrok fbshipit-source-id: eae4ffd87100dff466f91abfe26a82aa702d605a --- .circleci/build_docs/build_docs.sh | 2 +- CONTRIBUTING.md | 7 +++++++ docs/Makefile | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.circleci/build_docs/build_docs.sh b/.circleci/build_docs/build_docs.sh index f7f24c4f72..531e58ab7d 100755 --- a/.circleci/build_docs/build_docs.sh +++ b/.circleci/build_docs/build_docs.sh @@ -11,5 +11,5 @@ pushd docs pip install -r requirements.txt yum install -y -q libsndfile-devel pip install -r requirements-tutorials.txt -BUILD_GALLERY=1 make html +BUILD_GALLERY=1 make 'SPHINXOPTS=-W' html popd diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0ecf61582..0e68720b08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,6 +133,13 @@ make html ``` The built docs should now be available in `docs/build/html`. +If docstrings are mal-formed, warnings will be shown. +In CI doc build job, `SPHINXOPTS=-W` option is enabled and warnings are treated as error. +Please fix all the warnings when submitting a PR. +(You can use `SPHINXOPTS=-W` in local env, but by default, +tutorials are not built and it will be treated as error. +To use the option, please set `BUILD_GALLERY` as well. +e.g. `BUILD_GALLERY=1 make 'SPHINXOPTS=-W' html`.) By default, the documentation only builds API reference. If you are working to add a new example/tutorial with sphinx-gallery then diff --git a/docs/Makefile b/docs/Makefile index 76b604393a..630a95dc19 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = -W # converts warnings into error +SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = torchaudio SOURCEDIR = source From fb2f9538c5877c1529528a4d9067ea3ec3969aea Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Mon, 22 Nov 2021 07:38:23 -0800 Subject: [PATCH 0012/1144] Improve MVDR stability (#2004) Summary: Division first, multiplication second. This helps avoid the value overflow issue. It also helps the ``stv_evd`` solution pass the gradient check. Pull Request resolved: https://github.com/pytorch/audio/pull/2004 Reviewed By: mthrok Differential Revision: D32539827 Pulled By: nateanl fbshipit-source-id: 70a386608324bb6e1b1c7238c78d403698590f22 --- test/torchaudio_unittest/transforms/autograd_test_impl.py | 3 +-- torchaudio/transforms.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/torchaudio_unittest/transforms/autograd_test_impl.py b/test/torchaudio_unittest/transforms/autograd_test_impl.py index 669165c18f..88a36cd8a0 100644 --- a/test/torchaudio_unittest/transforms/autograd_test_impl.py +++ b/test/torchaudio_unittest/transforms/autograd_test_impl.py @@ -276,9 +276,8 @@ def test_psd_with_mask(self, multi_mask): @parameterized.expand([ "ref_channel", - # stv_power test time too long, comment for now + # stv_power and stv_evd test time too long, comment for now # "stv_power", - # stv_evd will fail since the eigenvalues are not distinct # "stv_evd", ]) def test_mvdr(self, solution): diff --git a/torchaudio/transforms.py b/torchaudio/transforms.py index bb64a43b14..56d4f3076a 100644 --- a/torchaudio/transforms.py +++ b/torchaudio/transforms.py @@ -1829,7 +1829,7 @@ def _get_mvdr_vector( denominator = torch.einsum("...d,...d->...", [stv.conj().squeeze(-1), numerator]) # normalzie the numerator scale = stv.squeeze(-1)[..., self.ref_channel, None].conj() - beamform_vector = numerator * scale / (denominator.real.unsqueeze(-1) + eps) + beamform_vector = numerator / (denominator.real.unsqueeze(-1) + eps) * scale return beamform_vector From 358354aa0d98757e5471f6a6826cdf7551e27ce0 Mon Sep 17 00:00:00 2001 From: Albert Villanova del Moral <8515462+albertvillanova@users.noreply.github.com> Date: Mon, 22 Nov 2021 09:30:02 -0800 Subject: [PATCH 0013/1144] Fix minor typo (#2012) Summary: Fix minor typo in docs. Pull Request resolved: https://github.com/pytorch/audio/pull/2012 Reviewed By: nateanl Differential Revision: D32562618 Pulled By: mthrok fbshipit-source-id: 79262a14d9b10381249602a63f400232031abaa2 --- torchaudio/backend/sox_io_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/torchaudio/backend/sox_io_backend.py b/torchaudio/backend/sox_io_backend.py index 35f70cef44..cf0423c192 100644 --- a/torchaudio/backend/sox_io_backend.py +++ b/torchaudio/backend/sox_io_backend.py @@ -40,7 +40,7 @@ def info( format (str or None, optional): Override the format detection with the given format. Providing the argument might help when libsox can not infer the format - from header or extension, + from header or extension. Returns: AudioMetaData: Metadata of the given audio. @@ -136,7 +136,7 @@ def load( format (str or None, optional): Override the format detection with the given format. Providing the argument might help when libsox can not infer the format - from header or extension, + from header or extension. Returns: (torch.Tensor, int): Resulting Tensor and sample rate. From 392a03c86d94d2747e1a0fc270a74c3845535173 Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Mon, 22 Nov 2021 11:12:10 -0800 Subject: [PATCH 0014/1144] Relax dtype for MVDR (#2024) Summary: Allow users to use `torch.cfloat` dtype input for MVDR module. It internally convert the spectrogram into `torch.cdouble` and output the tensor with the original dtype of the spectrogram. Pull Request resolved: https://github.com/pytorch/audio/pull/2024 Reviewed By: carolineechen Differential Revision: D32594051 Pulled By: nateanl fbshipit-source-id: e32609ccdc881b36300d579c90daba41c9234b46 --- .../transforms/batch_consistency_test.py | 1 - .../torchscript_consistency_cpu_test.py | 4 +-- .../torchscript_consistency_cuda_test.py | 4 +-- .../torchscript_consistency_impl.py | 35 +++++++++---------- torchaudio/transforms.py | 15 +++++--- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/test/torchaudio_unittest/transforms/batch_consistency_test.py b/test/torchaudio_unittest/transforms/batch_consistency_test.py index f1ba7a98eb..3e36b1a5d3 100644 --- a/test/torchaudio_unittest/transforms/batch_consistency_test.py +++ b/test/torchaudio_unittest/transforms/batch_consistency_test.py @@ -203,7 +203,6 @@ def test_batch_PSD_with_mask(self): ]) def test_MVDR(self, multi_mask): waveform = common_utils.get_whitenoise(sample_rate=8000, duration=1, n_channels=6) - waveform = waveform.to(torch.double) specgram = common_utils.get_spectrogram(waveform, n_fft=400) specgram = specgram.reshape(3, 2, specgram.shape[-2], specgram.shape[-1]) if multi_mask: diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py b/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py index eb5b2d8b04..9f16922a09 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py @@ -1,7 +1,7 @@ import torch from torchaudio_unittest.common_utils import PytorchTestCase -from .torchscript_consistency_impl import Transforms, TransformsFloat32Only, TransformsFloat64Only +from .torchscript_consistency_impl import Transforms, TransformsFloat32Only class TestTransformsFloat32(Transforms, TransformsFloat32Only, PytorchTestCase): @@ -9,6 +9,6 @@ class TestTransformsFloat32(Transforms, TransformsFloat32Only, PytorchTestCase): device = torch.device('cpu') -class TestTransformsFloat64(Transforms, TransformsFloat64Only, PytorchTestCase): +class TestTransformsFloat64(Transforms, PytorchTestCase): dtype = torch.float64 device = torch.device('cpu') diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py b/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py index 81a81b8242..b805449cd5 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py @@ -1,7 +1,7 @@ import torch from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase -from .torchscript_consistency_impl import Transforms, TransformsFloat32Only, TransformsFloat64Only +from .torchscript_consistency_impl import Transforms, TransformsFloat32Only @skipIfNoCuda @@ -11,6 +11,6 @@ class TestTransformsFloat32(Transforms, TransformsFloat32Only, PytorchTestCase): @skipIfNoCuda -class TestTransformsFloat64(Transforms, TransformsFloat64Only, PytorchTestCase): +class TestTransformsFloat64(Transforms, PytorchTestCase): dtype = torch.float64 device = torch.device('cuda') diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py b/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py index 3d73eafff7..c85b4a0236 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py @@ -157,24 +157,6 @@ def test_PSD_with_mask(self): mask = torch.rand(spectrogram.shape[-2:], device=self.device) self._assert_consistency_complex(T.PSD(), spectrogram, mask) - -class TransformsFloat32Only(TestBaseMixin): - def test_rnnt_loss(self): - logits = torch.tensor([[[[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.6, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.8, 0.1]], - [[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.1, 0.1], - [0.7, 0.1, 0.2, 0.1, 0.1]]]]) - tensor = logits.to(device=self.device, dtype=torch.float32) - targets = torch.tensor([[1, 2]], device=tensor.device, dtype=torch.int32) - logit_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) - target_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) - - self._assert_consistency(T.RNNTLoss(), logits, targets, logit_lengths, target_lengths) - - -class TransformsFloat64Only(TestBaseMixin): @parameterized.expand([ ["ref_channel", True], ["stv_evd", True], @@ -186,10 +168,25 @@ class TransformsFloat64Only(TestBaseMixin): def test_MVDR(self, solution, online): tensor = common_utils.get_whitenoise(sample_rate=8000, n_channels=4) spectrogram = common_utils.get_spectrogram(tensor, n_fft=400, hop_length=100) - spectrogram = spectrogram.to(device=self.device, dtype=torch.cdouble) mask_s = torch.rand(spectrogram.shape[-2:], device=self.device) mask_n = torch.rand(spectrogram.shape[-2:], device=self.device) self._assert_consistency_complex( T.MVDR(solution=solution, online=online), spectrogram, mask_s, mask_n ) + + +class TransformsFloat32Only(TestBaseMixin): + def test_rnnt_loss(self): + logits = torch.tensor([[[[0.1, 0.6, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.6, 0.1, 0.1], + [0.1, 0.1, 0.2, 0.8, 0.1]], + [[0.1, 0.6, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.2, 0.1, 0.1], + [0.7, 0.1, 0.2, 0.1, 0.1]]]]) + tensor = logits.to(device=self.device, dtype=torch.float32) + targets = torch.tensor([[1, 2]], device=tensor.device, dtype=torch.int32) + logit_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) + target_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) + + self._assert_consistency(T.RNNTLoss(), logits, targets, logit_lengths, target_lengths) diff --git a/torchaudio/transforms.py b/torchaudio/transforms.py index 56d4f3076a..bc2065962f 100644 --- a/torchaudio/transforms.py +++ b/torchaudio/transforms.py @@ -1665,9 +1665,9 @@ class MVDR(torch.nn.Module): (Default: ``False``) Note: - The MVDR Module requires the input STFT to be double precision (``torch.complex128`` or ``torch.cdouble``), - to improve the numerical stability. You can downgrade the precision to ``torch.float`` after generating the - enhanced waveform for ASR joint training. + To improve the numerical stability, the input spectrogram will be converted to double precision + (``torch.complex128`` or ``torch.cdouble``) dtype for internal computation. The output spectrogram + is converted to the dtype of the input spectrogram to be compatible with other modules. Note: If you use ``stv_evd`` solution, the gradient of the same input may not be identical if the @@ -1944,14 +1944,18 @@ def forward( torch.Tensor: The single-channel STFT of the enhanced speech. Tensor of dimension `(..., freq, time)` """ + dtype = specgram.dtype if specgram.ndim < 3: raise ValueError( f"Expected at least 3D tensor (..., channel, freq, time). Found: {specgram.shape}" ) - if specgram.dtype != torch.cdouble: + if not specgram.is_complex(): raise ValueError( - f"The type of ``specgram`` tensor must be ``torch.cdouble``. Found: {specgram.dtype}" + f"The type of ``specgram`` tensor must be ``torch.cfloat`` or ``torch.cdouble``.\ + Found: {specgram.dtype}" ) + if specgram.dtype == torch.cfloat: + specgram = specgram.cdouble() # Convert specgram to ``torch.cdouble``. if mask_n is None: warnings.warn( @@ -2006,4 +2010,5 @@ def forward( # unpack batch specgram_enhanced = specgram_enhanced.reshape(shape[:-3] + shape[-2:]) + specgram_enhanced.to(dtype) return specgram_enhanced From 05ae795ae51c5f72ef7747cd3c06fadccff97e5a Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 23 Nov 2021 07:18:42 -0800 Subject: [PATCH 0015/1144] Temporarily skip threadpool test (#2025) Summary: The sox_effects test in `concurrent.future.ThreadPoolExecutor` started failing since couple of days. While investigate this, skipping the test. Pull Request resolved: https://github.com/pytorch/audio/pull/2025 Reviewed By: nateanl Differential Revision: D32615933 Pulled By: mthrok fbshipit-source-id: 4f7301c0d3c0d11f687011e42e06d9c87ce4197f --- .circleci/config.yml | 2 +- .circleci/config.yml.in | 2 +- test/torchaudio_unittest/sox_effect/dataset_test.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cb9e32302a..37ed6add17 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -474,7 +474,7 @@ jobs: command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests - command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh + command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh - store_test_results: path: test-results - store_artifacts: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 5c3d709a47..4bb19316a7 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -474,7 +474,7 @@ jobs: command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests - command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh + command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh - store_test_results: path: test-results - store_artifacts: diff --git a/test/torchaudio_unittest/sox_effect/dataset_test.py b/test/torchaudio_unittest/sox_effect/dataset_test.py index 0b8cc9c4d8..37ee83aa7c 100644 --- a/test/torchaudio_unittest/sox_effect/dataset_test.py +++ b/test/torchaudio_unittest/sox_effect/dataset_test.py @@ -1,3 +1,4 @@ +import os import sys import platform from unittest import skipIf @@ -147,6 +148,7 @@ def setUp(self): save_wav(path, data, sample_rate) self.flist.append(path) + @skipIf(os.environ.get("CI") == 'true', "This test now hangs in CI") def test_executor(self): """Test that apply_effects_tensor with speed + rate does not crush From 9c9aef881cd5714fe6fbd546e4ea989723fed95d Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 23 Nov 2021 15:42:18 -0800 Subject: [PATCH 0016/1144] Update datasets document (#2029) Summary: - Remove unnecessary content list - Remove legacy description Pull Request resolved: https://github.com/pytorch/audio/pull/2029 Reviewed By: carolineechen Differential Revision: D32629917 Pulled By: mthrok fbshipit-source-id: bc9a9366c681bcf8b74907c2a6459c73fb6a7424 --- docs/source/datasets.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/source/datasets.rst b/docs/source/datasets.rst index 0b6e708144..2970b4e855 100644 --- a/docs/source/datasets.rst +++ b/docs/source/datasets.rst @@ -15,15 +15,6 @@ For example: :: shuffle=True, num_workers=args.nThreads) -The following datasets are available: - -.. contents:: Datasets - :local: - -All the datasets have almost similar API. They all have two common arguments: -``transform`` and ``target_transform`` to transform the input and target respectively. - - .. currentmodule:: torchaudio.datasets From 60a85b50ded95598f06e6aeac70bde86b6bd1dc6 Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Tue, 23 Nov 2021 18:21:59 -0800 Subject: [PATCH 0017/1144] Add RNN-T beam search decoder (#2028) Summary: Adds beam search decoder for RNN-T implementation ``torchaudio.prototype.RNNT`` that is TorchScript-able and supports both streaming and non-streaming inference. Pull Request resolved: https://github.com/pytorch/audio/pull/2028 Reviewed By: mthrok Differential Revision: D32627919 Pulled By: hwangjeff fbshipit-source-id: aab99e346d6514a3207a9fb69d4b42978b4cdbbd --- docs/source/prototype.rst | 16 + .../prototype/rnnt_decoder_cpu_test.py | 13 + .../prototype/rnnt_decoder_gpu_test.py | 15 + .../prototype/rnnt_decoder_test_impl.py | 120 ++++++ torchaudio/prototype/__init__.py | 10 +- torchaudio/prototype/rnnt_decoder.py | 377 ++++++++++++++++++ 6 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py create mode 100644 test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py create mode 100644 test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py create mode 100644 torchaudio/prototype/rnnt_decoder.py diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index 82cc97be2e..c67b5c0130 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -49,6 +49,22 @@ emformer_rnnt_model .. autofunction:: emformer_rnnt_model +RNNTBeamSearch +~~~~~~~~~~~~~~ + +.. autoclass:: RNNTBeamSearch + + .. automethod:: forward + + .. automethod:: infer + + +Hypothesis +~~~~~~~~~~ + +.. autoclass:: Hypothesis + + References ~~~~~~~~~~ diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py b/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py new file mode 100644 index 0000000000..03382386d7 --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py @@ -0,0 +1,13 @@ +import torch +from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl +from torchaudio_unittest.common_utils import PytorchTestCase + + +class RNNTBeamSearchFloat32CPUTest(RNNTBeamSearchTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cpu") + + +class RNNTBeamSearchFloat64CPUTest(RNNTBeamSearchTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py b/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py new file mode 100644 index 0000000000..09b0264f78 --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py @@ -0,0 +1,15 @@ +import torch +from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl +from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + + +@skipIfNoCuda +class RNNTBeamSearchFloat32GPUTest(RNNTBeamSearchTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cuda") + + +@skipIfNoCuda +class RNNTBeamSearchFloat64GPUTest(RNNTBeamSearchTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py new file mode 100644 index 0000000000..8845fc1687 --- /dev/null +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py @@ -0,0 +1,120 @@ +import torch + +from torchaudio.prototype import RNNTBeamSearch, emformer_rnnt_model +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script + + +class RNNTBeamSearchTestImpl(TestBaseMixin): + def _get_input_config(self): + model_config = self._get_model_config() + return { + "batch_size": 1, + "max_input_length": 61, + "num_symbols": model_config["num_symbols"], + "input_dim": model_config["input_dim"], + "right_context_length": model_config["right_context_length"], + "segment_length": model_config["segment_length"], + } + + def _get_model_config(self): + return { + "input_dim": 80, + "encoding_dim": 128, + "num_symbols": 256, + "segment_length": 16, + "right_context_length": 4, + "time_reduction_input_dim": 128, + "time_reduction_stride": 4, + "transformer_num_heads": 4, + "transformer_ffn_dim": 64, + "transformer_num_layers": 3, + "transformer_dropout": 0.0, + "transformer_activation": "relu", + "transformer_left_context_length": 30, + "transformer_max_memory_size": 0, + "transformer_weight_init_scale_strategy": "depthwise", + "transformer_tanh_on_mem": True, + "symbol_embedding_dim": 64, + "num_lstm_layers": 2, + "lstm_layer_norm": True, + "lstm_layer_norm_epsilon": 1e-3, + "lstm_dropout": 0.0, + } + + def _get_model(self): + return ( + emformer_rnnt_model(**self._get_model_config()) + .to(device=self.device, dtype=self.dtype) + .eval() + ) + + def test_torchscript_consistency_forward(self): + r"""Verify that scripting RNNTBeamSearch does not change the behavior of method `forward`.""" + + torch.random.manual_seed(31) + + input_config = self._get_input_config() + batch_size = input_config["batch_size"] + max_input_length = input_config["max_input_length"] + right_context_length = input_config["right_context_length"] + input_dim = input_config["input_dim"] + num_symbols = input_config["num_symbols"] + blank_idx = num_symbols - 1 + beam_width = 5 + + input = torch.rand( + batch_size, max_input_length + right_context_length, input_dim + ).to(device=self.device, dtype=self.dtype) + lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) + + model = self._get_model() + beam_search = RNNTBeamSearch(model, blank_idx) + scripted = torch_script(beam_search) + + res = beam_search(input, lengths, beam_width) + scripted_res = scripted(input, lengths, beam_width) + + self.assertEqual(res, scripted_res) + + def test_torchscript_consistency_infer(self): + r"""Verify that scripting RNNTBeamSearch does not change the behavior of method `infer`.""" + + torch.random.manual_seed(31) + + input_config = self._get_input_config() + segment_length = input_config["segment_length"] + right_context_length = input_config["right_context_length"] + input_dim = input_config["input_dim"] + num_symbols = input_config["num_symbols"] + blank_idx = num_symbols - 1 + beam_width = 5 + + input = torch.rand( + segment_length + right_context_length, input_dim + ).to(device=self.device, dtype=self.dtype) + lengths = torch.randint( + 1, segment_length + right_context_length + 1, () + ).to(device=self.device, dtype=torch.int32) + + model = self._get_model() + + state, hypo = None, None + scripted_state, scripted_hypo = None, None + for _ in range(2): + beam_search = RNNTBeamSearch(model, blank_idx) + scripted = torch_script(beam_search) + + res = beam_search.infer(input, lengths, beam_width, state=state, hypothesis=hypo) + scripted_res = scripted.infer( + input, lengths, beam_width, state=scripted_state, hypothesis=scripted_hypo + ) + + self.assertEqual(res, scripted_res) + + state = res[1] + hypo = res[0][0] + + scripted_state = scripted_res[1] + scripted_hypo = scripted_res[0][0] diff --git a/torchaudio/prototype/__init__.py b/torchaudio/prototype/__init__.py index 57f549d740..0b90a9cd6d 100644 --- a/torchaudio/prototype/__init__.py +++ b/torchaudio/prototype/__init__.py @@ -1,5 +1,13 @@ from .emformer import Emformer from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model +from .rnnt_decoder import Hypothesis, RNNTBeamSearch -__all__ = ["Emformer", "RNNT", "emformer_rnnt_base", "emformer_rnnt_model"] +__all__ = [ + "Emformer", + "Hypothesis", + "RNNT", + "RNNTBeamSearch", + "emformer_rnnt_base", + "emformer_rnnt_model", +] diff --git a/torchaudio/prototype/rnnt_decoder.py b/torchaudio/prototype/rnnt_decoder.py new file mode 100644 index 0000000000..378dd60b01 --- /dev/null +++ b/torchaudio/prototype/rnnt_decoder.py @@ -0,0 +1,377 @@ +from typing import Callable, Dict, List, Optional, NamedTuple, Tuple + +import torch + +from .rnnt import RNNT + + +__all__ = ["Hypothesis", "RNNTBeamSearch"] + + +class Hypothesis(NamedTuple): + r"""Represents hypothesis generated by beam search decoder ``RNNTBeamSearch``. + + :ivar List[int] tokens: Predicted sequence of tokens. + :ivar torch.Tensor predictor_out: Prediction network output. + :ivar List[List[torch.Tensor]] state: Prediction network internal state. + :ivar float score: Score of hypothesis. + :ivar List[int] alignment: Sequence of timesteps, with the i-th value mapping + to the i-th predicted token in ``tokens``. + :ivar int blank: Token index corresponding to blank token. + :ivar str key: Value used to determine equivalence in token sequences + between ``Hypothesis`` instances. + """ + tokens: List[int] + predictor_out: torch.Tensor + state: List[List[torch.Tensor]] + score: float + alignment: List[int] + blank: int + key: str + + +def _batch_state(hypos: List[Hypothesis]) -> List[List[torch.Tensor]]: + states: List[List[torch.Tensor]] = [] + for i in range(len(hypos[0].state)): + batched_state_components: List[torch.Tensor] = [] + for j in range(len(hypos[0].state[i])): + batched_state_components.append( + torch.cat([hypo.state[i][j] for hypo in hypos]) + ) + states.append(batched_state_components) + return states + + +def _slice_state( + states: List[List[torch.Tensor]], idx: int, device: torch.device +) -> List[List[torch.Tensor]]: + idx_tensor = torch.tensor([idx], device=device) + return [ + [state.index_select(0, idx_tensor) for state in state_tuple] + for state_tuple in states + ] + + +def _default_hypo_sort_key(hypo: Hypothesis) -> float: + return hypo.score / (len(hypo.tokens) + 1) + + +def _compute_updated_scores( + hypos: List[Hypothesis], next_token_probs: torch.Tensor, beam_width: int, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + hypo_scores = torch.tensor([h.score for h in hypos]).unsqueeze(1) + nonblank_scores = ( + hypo_scores + next_token_probs[:, :-1] + ) # [beam_width, num_tokens - 1] + nonblank_nbest_scores, nonblank_nbest_idx = nonblank_scores.reshape(-1).topk( + beam_width + ) + nonblank_nbest_hypo_idx = nonblank_nbest_idx.div( + nonblank_scores.shape[1], rounding_mode="trunc" + ) + nonblank_nbest_token = nonblank_nbest_idx % nonblank_scores.shape[1] + return nonblank_nbest_scores, nonblank_nbest_hypo_idx, nonblank_nbest_token + + +def _remove_hypo(hypo: Hypothesis, hypo_list: List[Hypothesis]) -> None: + for i, elem in enumerate(hypo_list): + if hypo.key == elem.key: + del hypo_list[i] + break + + +class RNNTBeamSearch(torch.nn.Module): + r"""Beam search decoder for RNN-T model. + + Args: + model (RNNT): RNN-T model to use. + blank (int): index of blank token in vocabulary. + temperature (float, optional): temperature to apply to joint network output. + Larger values yield more uniform samples. (Default: 1.0) + hypo_sort_key (Callable[[Hypothesis], float] or None, optional): callable that computes a score + for a given hypothesis to rank hypotheses by. If ``None``, defaults to callable that returns + hypothesis score normalized by token sequence length. (Default: None) + step_max_tokens (int, optional): maximum number of tokens to emit per input time step. (Default: 100) + """ + + def __init__( + self, + model: RNNT, + blank: int, + temperature: float = 1.0, + hypo_sort_key: Optional[Callable[[Hypothesis], float]] = None, + step_max_tokens: int = 100, + ) -> None: + super().__init__() + self.model = model + self.blank = blank + self.temperature = temperature + + if hypo_sort_key is None: + self.hypo_sort_key = _default_hypo_sort_key + else: + self.hypo_sort_key = hypo_sort_key + + self.step_max_tokens = step_max_tokens + + def _init_b_hypos( + self, hypo: Optional[Hypothesis], device: torch.device + ) -> List[Hypothesis]: + if hypo is not None: + token = hypo.tokens[-1] + state = hypo.state + else: + token = self.blank + state = None + + one_tensor = torch.tensor([1], device=device) + pred_out, _, pred_state = self.model.predict( + torch.tensor([[token]], device=device), one_tensor, state + ) + init_hypo = Hypothesis( + tokens=[token], + predictor_out=pred_out[0].detach(), + state=pred_state, + score=0.0, + alignment=[-1], + blank=self.blank, + key=str([token]), + ) + return [init_hypo] + + def _gen_next_token_probs( + self, enc_out: torch.Tensor, hypos: List[Hypothesis], device: torch.device + ) -> torch.Tensor: + one_tensor = torch.tensor([1], device=device) + predictor_out = torch.stack([h.predictor_out for h in hypos], dim=0) + joined_out, _, _ = self.model.join( + enc_out, + one_tensor, + predictor_out, + torch.tensor([1] * len(hypos), device=device), + ) # [beam_width, 1, 1, num_tokens] + joined_out = torch.nn.functional.log_softmax( + joined_out / self.temperature, dim=3 + ) + joined_out[:, :, :, :4].add_(-99999) # blank out invalid tokens + return joined_out[:, 0, 0] + + def _gen_b_hypos( + self, + b_hypos: List[Hypothesis], + a_hypos: List[Hypothesis], + next_token_probs: torch.Tensor, + key_to_b_hypo: Dict[str, Hypothesis], + ) -> List[Hypothesis]: + for i in range(len(a_hypos)): + h_a = a_hypos[i] + append_blank_score = h_a.score + next_token_probs[i, -1] + if h_a.key in key_to_b_hypo: + h_b = key_to_b_hypo[h_a.key] + _remove_hypo(h_b, b_hypos) + score = float(torch.tensor(h_b.score).logaddexp(append_blank_score)) + alignment = h_a.alignment if h_b.score < h_a.score else h_b.alignment + else: + score = float(append_blank_score) + alignment = h_a.alignment + h_b = Hypothesis( + tokens=h_a.tokens, + predictor_out=h_a.predictor_out, + state=h_a.state, + score=score, + alignment=alignment, + blank=self.blank, + key=h_a.key, + ) + b_hypos.append(h_b) + key_to_b_hypo[h_b.key] = h_b + _, sorted_idx = torch.tensor([hypo.score for hypo in b_hypos]).sort() + return [b_hypos[idx] for idx in sorted_idx] + + def _gen_a_hypos( + self, + a_hypos: List[Hypothesis], + b_hypos: List[Hypothesis], + next_token_probs: torch.Tensor, + t: int, + beam_width: int, + device: torch.device, + ) -> List[Hypothesis]: + ( + nonblank_nbest_scores, + nonblank_nbest_hypo_idx, + nonblank_nbest_token, + ) = _compute_updated_scores(a_hypos, next_token_probs, beam_width) + + if len(b_hypos) < beam_width: + b_nbest_score = -float("inf") + else: + b_nbest_score = b_hypos[-beam_width].score + + base_hypos: List[Hypothesis] = [] + new_tokens: List[int] = [] + new_scores: List[float] = [] + for i in range(beam_width): + score = float(nonblank_nbest_scores[i]) + if score > b_nbest_score: + a_hypo_idx = int(nonblank_nbest_hypo_idx[i]) + base_hypos.append(a_hypos[a_hypo_idx]) + new_tokens.append(int(nonblank_nbest_token[i])) + new_scores.append(score) + + if base_hypos: + new_hypos = self._gen_new_hypos( + base_hypos, new_tokens, new_scores, t, device + ) + else: + new_hypos: List[Hypothesis] = [] + + return new_hypos + + def _gen_new_hypos( + self, + base_hypos: List[Hypothesis], + tokens: List[int], + scores: List[float], + t: int, + device: torch.device, + ) -> List[Hypothesis]: + tgt_tokens = torch.tensor([[token] for token in tokens], device=device) + states = _batch_state(base_hypos) + pred_out, _, pred_states = self.model.predict( + tgt_tokens, torch.tensor([1] * len(base_hypos), device=device), states, + ) + new_hypos: List[Hypothesis] = [] + for i, h_a in enumerate(base_hypos): + new_tokens = h_a.tokens + [tokens[i]] + new_hypos.append( + Hypothesis( + tokens=new_tokens, + predictor_out=pred_out[i].detach(), + state=_slice_state(pred_states, i, device), + score=scores[i], + alignment=h_a.alignment + [t], + blank=self.blank, + key=str(new_tokens), + ) + ) + return new_hypos + + def _search( + self, enc_out: torch.Tensor, hypo: Optional[Hypothesis], beam_width: int, + ) -> List[Hypothesis]: + n_time_steps = enc_out.shape[1] + device = enc_out.device + + a_hypos: List[Hypothesis] = [] + b_hypos = self._init_b_hypos(hypo, device) + for t in range(n_time_steps): + a_hypos = b_hypos + b_hypos = torch.jit.annotate(List[Hypothesis], []) + key_to_b_hypo: Dict[str, Hypothesis] = {} + symbols_current_t = 0 + + while a_hypos: + next_token_probs = self._gen_next_token_probs( + enc_out[:, t: t + 1], a_hypos, device + ) + next_token_probs = next_token_probs.cpu() + b_hypos = self._gen_b_hypos( + b_hypos, a_hypos, next_token_probs, key_to_b_hypo, + ) + + if symbols_current_t == self.step_max_tokens: + break + + a_hypos = self._gen_a_hypos( + a_hypos, b_hypos, next_token_probs, t, beam_width, device, + ) + if a_hypos: + symbols_current_t += 1 + + _, sorted_idx = torch.tensor( + [self.hypo_sort_key(hypo) for hypo in b_hypos] + ).topk(beam_width) + b_hypos = [b_hypos[idx] for idx in sorted_idx] + + return b_hypos + + def forward( + self, input: torch.Tensor, length: torch.Tensor, beam_width: int + ) -> List[Hypothesis]: + r"""Performs beam search for the given input sequence. + + T: number of frames; + D: feature dimension of each frame. + + Args: + input (torch.Tensor): sequence of input frames, with shape (T, D) or (1, T, D). + length (torch.Tensor): number of valid frames in input + sequence, with shape () or (1,). + beam_width (int): beam size to use during search. + + Returns: + List[Hypothesis]: top-``beam_width`` hypotheses found by beam search. + """ + assert input.dim() == 2 or ( + input.dim() == 3 and input.shape[0] == 1 + ), "input must be of shape (T, D) or (1, T, D)" + if input.dim() == 2: + input = input.unsqueeze(0) + + assert length.shape == () or length.shape == ( + 1, + ), "length must be of shape () or (1,)" + if input.dim() == 0: + input = input.unsqueeze(0) + + enc_out, _ = self.model.transcribe(input, length) + return self._search(enc_out, None, beam_width) + + @torch.jit.export + def infer( + self, + input: torch.Tensor, + length: torch.Tensor, + beam_width: int, + state: Optional[List[List[torch.Tensor]]] = None, + hypothesis: Optional[Hypothesis] = None, + ) -> Tuple[List[Hypothesis], List[List[torch.Tensor]]]: + r"""Performs beam search for the given input sequence in streaming mode. + + T: number of frames; + D: feature dimension of each frame. + + Args: + input (torch.Tensor): sequence of input frames, with shape (T, D) or (1, T, D). + length (torch.Tensor): number of valid frames in input + sequence, with shape () or (1,). + beam_width (int): beam size to use during search. + state (List[List[torch.Tensor]] or None, optional): list of lists of tensors + representing transcription network internal state generated in preceding + invocation. (Default: ``None``) + hypothesis (Hypothesis or None): hypothesis from preceding invocation to seed + search with. (Default: ``None``) + + Returns: + (List[Hypothesis], List[List[torch.Tensor]]): + List[Hypothesis] + top-``beam_width`` hypotheses found by beam search. + List[List[torch.Tensor]] + list of lists of tensors representing transcription network + internal state generated in current invocation. + """ + assert input.dim() == 2 or ( + input.dim() == 3 and input.shape[0] == 1 + ), "input must be of shape (T, D) or (1, T, D)" + if input.dim() == 2: + input = input.unsqueeze(0) + + assert length.shape == () or length.shape == ( + 1, + ), "length must be of shape () or (1,)" + if input.dim() == 0: + input = input.unsqueeze(0) + + enc_out, _, state = self.model.transcribe_streaming(input, length, state) + return self._search(enc_out, hypothesis, beam_width), state From 9392c9e072f79179d389b6372b488f079afbf6d0 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Tue, 23 Nov 2021 19:23:41 -0800 Subject: [PATCH 0018/1144] Update script for getting PR merger and labels (#2030) Summary: The previous way of detecting the merger and labels given a commit hash no longer works with ShipIt, as PRs are closed and not merged and are not associated with a commit hash. To work around this, update the script to get the merger (pulled by: ) and PR number from the commit hash message, and then collect labels from the corresponding PR. Pull Request resolved: https://github.com/pytorch/audio/pull/2030 Reviewed By: mthrok Differential Revision: D32634870 Pulled By: carolineechen fbshipit-source-id: a8fcfc5912871d3cca056de43ab25b5d0acb2226 --- .github/process_commit.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/process_commit.py b/.github/process_commit.py index 73eaabcb06..f9ef302533 100644 --- a/.github/process_commit.py +++ b/.github/process_commit.py @@ -1,9 +1,8 @@ """ -This script finds the merger responsible for labeling a PR by a commit SHA. It is used by the workflow in -'.github/workflows/pr-labels.yml'. If there exists no PR associated with the commit or the PR is properly labeled, -this script is a no-op. -Note: we ping the merger only, not the reviewers, as the reviewers can sometimes be external to torchaudio -with no labeling responsibility, so we don't want to bother them. +This script finds the person responsible for labeling a PR by a commit SHA. It is used by the workflow in +'.github/workflows/pr-labels.yml'. +Note: we only ping the person who pulls the pr, not the reviewers, as the reviewers can sometimes be external +to torchaudio with no labeling responsibility, so we don't want to bother them. """ import sys @@ -44,29 +43,30 @@ def query_torchaudio(cmd: str, *, accept) -> Any: return response.json() -def get_pr_number(commit_hash: str) -> Optional[int]: - # See https://docs.github.com/en/rest/reference/repos#list-pull-requests-associated-with-a-commit - data = query_torchaudio(f"commits/{commit_hash}/pulls", accept="application/vnd.github.groot-preview+json") - if not data: - return None - return data[0]["number"] +def get_pr_merger_and_number(commit_hash: str) -> Optional[str]: + data = query_torchaudio(f"commits/{commit_hash}", accept="application/vnd.github.v3+json") + commit_message = data['commit']['message'] + pulled_by = commit_message.split('Pulled By: ') + pulled_by = pulled_by[1].split('\n')[0] if len(pulled_by) > 1 else None -def get_pr_merger_and_labels(pr_number: int) -> Tuple[str, Set[str]]: - # See https://docs.github.com/en/rest/reference/pulls#get-a-pull-request + pr_number = commit_message.split('Pull Request resolved: https://github.com/pytorch/audio/pull/') + pr_number = pr_number[1].split('\n')[0] if len(pr_number) > 1 else None + + return pulled_by, pr_number + + +def get_labels(pr_number: int) -> Set[str]: data = query_torchaudio(f"pulls/{pr_number}", accept="application/vnd.github.v3+json") - merger = data["merged_by"]["login"] labels = {label["name"] for label in data["labels"]} - return merger, labels + return labels if __name__ == "__main__": commit_hash = sys.argv[1] - pr_number = get_pr_number(commit_hash) - if not pr_number: - sys.exit(0) - merger, labels = get_pr_merger_and_labels(pr_number) + merger, pr_number = get_pr_merger_and_number(commit_hash) + labels = get_labels(pr_number) is_properly_labeled = bool(PRIMARY_LABELS.intersection(labels) and SECONDARY_LABELS.intersection(labels)) if not is_properly_labeled: From fce431cd55b8c67c5504d8417fd471f7f73e800c Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 24 Nov 2021 06:31:22 -0800 Subject: [PATCH 0019/1144] improve installing nightly pytorch (#2026) Summary: Similar to https://github.com/pytorch/vision/pull/4788 Make sure the workflow could download right PyTorch with cpu or cuda in case nightly build wasn't ready at that day. https://app.circleci.com/pipelines/github/pytorch/audio/8427/workflows/11a80738-bcdd-45e3-b37f-328be36c60ee/jobs/438285?invite=true#step-107-542 ![image](https://user-images.githubusercontent.com/16190118/142969390-142df7ec-6040-40c1-9a02-17d43f5de05e.png) Pull Request resolved: https://github.com/pytorch/audio/pull/2026 Reviewed By: hwangjeff, nateanl Differential Revision: D32634926 Pulled By: mthrok fbshipit-source-id: 30d6349a0a2ce174b789a5888b1c8e0544a23a37 --- .circleci/unittest/linux/scripts/install.sh | 6 +++++- .circleci/unittest/windows/scripts/install.sh | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.circleci/unittest/linux/scripts/install.sh b/.circleci/unittest/linux/scripts/install.sh index 1f285bc345..869799da8d 100755 --- a/.circleci/unittest/linux/scripts/install.sh +++ b/.circleci/unittest/linux/scripts/install.sh @@ -29,21 +29,25 @@ if [ -z "${CUDA_VERSION:-}" ] ; then else cudatoolkit="cpuonly" fi + version="cpu" else version="$(python -c "print('.'.join(\"${CUDA_VERSION}\".split('.')[:2]))")" cudatoolkit="cudatoolkit=${version}" fi + printf "Installing PyTorch with %s\n" "${cudatoolkit}" ( if [ "${os}" == MacOSX ] ; then # TODO: this can be removed as soon as linking issue could be resolved # see https://github.com/pytorch/pytorch/issues/62424 from details MKL_CONSTRAINT='mkl==2021.2.0' + pytorch_build=pytorch else MKL_CONSTRAINT='' + pytorch_build="pytorch[build="*${version}*"]" fi set -x - conda install ${CONDA_CHANNEL_FLAGS:-} -y -c "pytorch-${UPLOAD_CHANNEL}" $MKL_CONSTRAINT "pytorch-${UPLOAD_CHANNEL}::pytorch" ${cudatoolkit} + conda install ${CONDA_CHANNEL_FLAGS:-} -y -c "pytorch-${UPLOAD_CHANNEL}" $MKL_CONSTRAINT "pytorch-${UPLOAD_CHANNEL}::${pytorch_build}" ${cudatoolkit} ) # 2. Install torchaudio diff --git a/.circleci/unittest/windows/scripts/install.sh b/.circleci/unittest/windows/scripts/install.sh index 0bbdb86781..7f9a19419f 100644 --- a/.circleci/unittest/windows/scripts/install.sh +++ b/.circleci/unittest/windows/scripts/install.sh @@ -23,12 +23,13 @@ source "$this_dir/set_cuda_envs.sh" # 1. Install PyTorch if [ -z "${CUDA_VERSION:-}" ] ; then cudatoolkit="cpuonly" + version="cpu" else version="$(python -c "print('.'.join(\"${CUDA_VERSION}\".split('.')[:2]))")" cudatoolkit="cudatoolkit=${version}" fi printf "Installing PyTorch with %s\n" "${cudatoolkit}" -conda install -y -c "pytorch-${UPLOAD_CHANNEL}" -c conda-forge "pytorch-${UPLOAD_CHANNEL}"::pytorch "${cudatoolkit}" pytest +conda install -y -c "pytorch-${UPLOAD_CHANNEL}" -c conda-forge "pytorch-${UPLOAD_CHANNEL}"::pytorch[build="*${version}*"] "${cudatoolkit}" pytest torch_cuda=$(python -c "import torch; print(torch.cuda.is_available())") echo torch.cuda.is_available is $torch_cuda From e83d417721548902298f3a45b446924883d2e009 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:04:49 -0800 Subject: [PATCH 0020/1144] Allow whitespace as TORCH_CUDA_ARCH_LIST delimiter (#2050) Summary: Resolves https://github.com/pytorch/audio/issues/2049, https://github.com/pytorch/audio/issues/1940 Pull Request resolved: https://github.com/pytorch/audio/pull/2050 Reviewed By: nateanl Differential Revision: D32712513 Pulled By: mthrok fbshipit-source-id: e1db81786bcca67605ff765d27e0527e20967d1c --- tools/setup_helpers/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/setup_helpers/extension.py b/tools/setup_helpers/extension.py index 945c7e8c21..739b28229a 100644 --- a/tools/setup_helpers/extension.py +++ b/tools/setup_helpers/extension.py @@ -101,7 +101,7 @@ def build_extension(self, ext): if _TORCH_CUDA_ARCH_LIST is not None: # Convert MAJOR.MINOR[+PTX] list to new style one # defined at https://cmake.org/cmake/help/latest/prop_tgt/CUDA_ARCHITECTURES.html - _arches = _TORCH_CUDA_ARCH_LIST.replace('.', '').split(";") + _arches = _TORCH_CUDA_ARCH_LIST.replace('.', '').replace(' ', ';').split(";") _arches = [arch[:-4] if arch.endswith("+PTX") else f"{arch}-real" for arch in _arches] cmake_args += [f"-DCMAKE_CUDA_ARCHITECTURES={';'.join(_arches)}"] From 96b1fa720235f2af085c8b88eec755f19249dc0c Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Tue, 30 Nov 2021 12:19:51 -0800 Subject: [PATCH 0021/1144] Revise Griffin-Lim transform test to reduce execution time (#2037) Summary: Our Griffin-Lim autograd tests take a long time to run. This PR adjusts some parameters to shorten the run time. For one of the four tests: Before: ``` test/torchaudio_unittest/transforms/autograd_cpu_test.py . [100%] ======================== 1 passed in 517.35s (0:08:37) ========================= ``` After: ``` test/torchaudio_unittest/transforms/autograd_cpu_test.py . [100%] ======================== 1 passed in 104.59s (0:01:44) ========================= ``` Pull Request resolved: https://github.com/pytorch/audio/pull/2037 Reviewed By: mthrok Differential Revision: D32726213 Pulled By: hwangjeff fbshipit-source-id: c785323ab380aea4b63fb1683b557c8ae842f54e --- test/torchaudio_unittest/transforms/autograd_test_impl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/torchaudio_unittest/transforms/autograd_test_impl.py b/test/torchaudio_unittest/transforms/autograd_test_impl.py index 88a36cd8a0..bde7af8aee 100644 --- a/test/torchaudio_unittest/transforms/autograd_test_impl.py +++ b/test/torchaudio_unittest/transforms/autograd_test_impl.py @@ -101,11 +101,12 @@ def test_melspectrogram(self): [False, True], ) def test_griffinlim(self, momentum, rand_init): - n_fft = 400 + n_fft = 80 power = 1 - n_iter = 3 + n_iter = 2 + spec = get_spectrogram( - get_whitenoise(sample_rate=8000, duration=0.05, n_channels=2), + get_whitenoise(sample_rate=8000, duration=0.01, n_channels=2), n_fft=n_fft, power=power) transform = _DeterministicWrapper( T.GriffinLim(n_fft=n_fft, n_iter=n_iter, momentum=momentum, rand_init=rand_init, power=power)) From 9114e6367d19b4d9d10d693da246f544216e0119 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:26:03 -0800 Subject: [PATCH 0022/1144] Refactor the library loading mechanism (#2038) Summary: (This is a part of refactor series, followed up by https://github.com/pytorch/audio/issues/2039 and https://github.com/pytorch/audio/issues/2040. The goal is to make it easy to add a new library artifact alongside with `libtorchudio`, as in https://github.com/pytorch/audio/pull/2048/commits/4ced990849e60f6d19e87ae22819b04d1726648e https://github.com/pytorch/audio/issues/2048 .) We plan to add prototype/beta third party library integrations, which could be unstable. (segfault, missing dynamic library dependencies etc...) If we add such integrations into the existing libtorchaudio, in the worst case, it will prevent users from just `import torchaudio`. Instead, we would like to separate the prototype/beta integrations into separate libraries, so that such issues would not impact all users but users who attempt to use these prototytpe/beta features. Say, a prototype feature `foo` is added in `torchaudio.prototype.foo`. The following initialization procedure will achieve the above mechanism. 1. Place the library file `libtorchaudio_foo` in `torchaudio/lib`. 2. In `torchaudio.prototype.foo.__init__.py`, load the `libtorchaudio_foo`. Note: The approach will be slightly different for fbcode, because of how buck deploys C++ libraries and standardized environment, but the code change here is still applicable. Pull Request resolved: https://github.com/pytorch/audio/pull/2038 Reviewed By: carolineechen, nateanl Differential Revision: D32682900 Pulled By: mthrok fbshipit-source-id: 0f402a92a366fba8c2894a0fe01f47f8cdd51376 --- torchaudio/_extension.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/torchaudio/_extension.py b/torchaudio/_extension.py index 6bb217c684..0309d3ce06 100644 --- a/torchaudio/_extension.py +++ b/torchaudio/_extension.py @@ -5,14 +5,17 @@ import torch from torchaudio._internal import module_utils as _mod_utils # noqa: F401 +_LIB_DIR = Path(__file__).parent / 'lib' -def _init_extension(): - if not _mod_utils.is_module_available('torchaudio._torchaudio'): - warnings.warn('torchaudio C++ extension is not available.') - return +def _get_lib_path(lib: str): suffix = 'pyd' if os.name == 'nt' else 'so' - path = Path(__file__).parent / 'lib' / f'libtorchaudio.{suffix}' + path = _LIB_DIR / f'{lib}.{suffix}' + return path + + +def _load_lib(lib: str): + path = _get_lib_path(lib) # In case `torchaudio` is deployed with `pex` format, this file does not exist. # In this case, we expect that `libtorchaudio` is available somewhere # in the search path of dynamic loading mechanism, and importing `_torchaudio`, @@ -20,7 +23,16 @@ def _init_extension(): if path.exists(): torch.ops.load_library(path) torch.classes.load_library(path) + + +def _init_extension(): + if not _mod_utils.is_module_available('torchaudio._torchaudio'): + warnings.warn('torchaudio C++ extension is not available.') + return + + _load_lib('libtorchaudio') # This import is for initializing the methods registered via PyBind11 + # This has to happen after the base library is loaded from torchaudio import _torchaudio # noqa From 4b11eee82e5d97d5a0d10bc9c5d589cf875eae1b Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Thu, 2 Dec 2021 18:26:10 -0800 Subject: [PATCH 0023/1144] improve cuda installation on windows (#2032) Summary: 1. stop&disable the windows upgrade that's the major reason of the failure of cuda installation https://app.circleci.com/pipelines/github/pytorch/audio/8458/workflows/feb65e3b-1093-4724-b849-1a2ac166f354/jobs/441331 For more details please check out https://github.com/pytorch/pytorch/issues/64536 2. print the log when the cuda installation fails Pull Request resolved: https://github.com/pytorch/audio/pull/2032 Reviewed By: mthrok Differential Revision: D32816145 Pulled By: malfet fbshipit-source-id: 44a2ef0dd4c43469472a6e518ed64841e2dcd5bb --- packaging/windows/internal/cuda_install.bat | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packaging/windows/internal/cuda_install.bat b/packaging/windows/internal/cuda_install.bat index 7dacc18e68..f760ceabf0 100644 --- a/packaging/windows/internal/cuda_install.bat +++ b/packaging/windows/internal/cuda_install.bat @@ -193,7 +193,12 @@ if not exist "%SRC_DIR%\temp_build\NvToolsExt.7z" ( echo Installing CUDA toolkit... 7z x %CUDA_SETUP_FILE% -o"%SRC_DIR%\temp_build\cuda" pushd "%SRC_DIR%\temp_build\cuda" -start /wait setup.exe -s %ARGS% +sc config wuauserv start= disabled +sc stop wuauserv +sc query wuauserv + +start /wait setup.exe -s %ARGS% -loglevel:6 -log:"%cd%/cuda_install_logs" +echo %errorlevel% popd echo Installing VS integration... @@ -222,6 +227,10 @@ set "NVTOOLSEXT_PATH=%ProgramFiles%\NVIDIA Corporation\NvToolsExt\bin\x64" if not exist "%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v%CUDA_VERSION_STR%\bin\nvcc.exe" ( echo CUDA %CUDA_VERSION_STR% installed failed. + echo --------- RunDll32.exe.log + type "%SRC_DIR%\temp_build\cuda\cuda_install_logs\LOG.RunDll32.exe.log" + echo --------- setup.exe.log ------- + type "%SRC_DIR%\temp_build\cuda\cuda_install_logs\LOG.setup.exe.log" exit /b 1 ) From 7ac525e7d190ded34b1b717423070ae4410f16ca Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Fri, 3 Dec 2021 13:35:07 -0800 Subject: [PATCH 0024/1144] Add training recipe for RNN-T Emformer ASR model (#2052) Summary: Add training recipe for RNN-T Emformer ASR model to examples directory. Pull Request resolved: https://github.com/pytorch/audio/pull/2052 Reviewed By: nateanl Differential Revision: D32814096 Pulled By: hwangjeff fbshipit-source-id: a5153044efc16cb39f0e6413369a6791637af76a --- .../asr/librispeech_emformer_rnnt/README.md | 35 ++ .../asr/librispeech_emformer_rnnt/eval.py | 74 ++++ .../global_stats.json | 166 ++++++++ .../librispeech_emformer_rnnt/lightning.py | 385 ++++++++++++++++++ .../asr/librispeech_emformer_rnnt/train.py | 95 +++++ 5 files changed, 755 insertions(+) create mode 100644 examples/asr/librispeech_emformer_rnnt/README.md create mode 100644 examples/asr/librispeech_emformer_rnnt/eval.py create mode 100644 examples/asr/librispeech_emformer_rnnt/global_stats.json create mode 100644 examples/asr/librispeech_emformer_rnnt/lightning.py create mode 100644 examples/asr/librispeech_emformer_rnnt/train.py diff --git a/examples/asr/librispeech_emformer_rnnt/README.md b/examples/asr/librispeech_emformer_rnnt/README.md new file mode 100644 index 0000000000..35a392dec2 --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/README.md @@ -0,0 +1,35 @@ +# Emformer RNN-T ASR Example + +This directory contains sample implementations of training and evaluation pipelines for an on-device-oriented streaming-capable Emformer RNN-T ASR model. + +## Usage + +### Training + +[`train.py`](./train.py) trains an Emformer RNN-T model on LibriSpeech using PyTorch Lightning. Note that the script expects users to have access to GPU nodes for training and provide paths to the full LibriSpeech dataset and the SentencePiece model to be used to encode targets. + +Sample SLURM command: +``` +srun --cpus-per-task=12 --gpus-per-node=8 -N 4 --ntasks-per-node=8 python train.py --exp_dir ./experiments --librispeech_path ./librispeech/ --global_stats_path ./global_stats.json --sp_model_path ./spm_bpe_4096.model +``` + +### Evaluation + +[`eval.py`](./eval.py) evaluates a trained Emformer RNN-T model on LibriSpeech test-clean. + +Using the default configuration along with a SentencePiece model trained on LibriSpeech with vocab size 4096 and type bpe, [`train.py`](./train.py) produces a model with 76.7M parameters (307MB) that achieves an WER of 0.0466 when evaluated on test-clean with [`eval.py`](./eval.py). + +The table below contains WER results for various splits. + +| | WER | +|:-------------------:|-------------:| +| test-clean | 0.0466 | +| test-other | 0.1239 | +| dev-clean | 0.0445 | +| dev-other | 0.1217 | + + +Sample SLURM command: +``` +srun python eval.py --checkpoint_path ./experiments/checkpoints/epoch=119-step=208079.ckpt --librispeech_path ./librispeech/ --sp_model_path ./spm_bpe_4096.model --use_cuda +``` diff --git a/examples/asr/librispeech_emformer_rnnt/eval.py b/examples/asr/librispeech_emformer_rnnt/eval.py new file mode 100644 index 0000000000..c85ad46388 --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/eval.py @@ -0,0 +1,74 @@ +from argparse import ArgumentParser +import logging +import pathlib + +import torch +import torchaudio + +from lightning import RNNTModule + + +logger = logging.getLogger() + + +def compute_word_level_distance(seq1, seq2): + return torchaudio.functional.edit_distance( + seq1.lower().split(), seq2.lower().split() + ) + + +def run_eval(args): + model = RNNTModule.load_from_checkpoint( + args.checkpoint_path, + librispeech_path=str(args.librispeech_path), + sp_model_path=str(args.sp_model_path), + global_stats_path=str(args.global_stats_path), + ).eval() + + if args.use_cuda: + model = model.to(device="cuda") + + total_edit_distance = 0 + total_length = 0 + dataloader = model.test_dataloader() + with torch.no_grad(): + for idx, (batch, sample) in enumerate(dataloader): + actual = sample[0][2] + predicted = model(batch) + total_edit_distance += compute_word_level_distance(actual, predicted) + total_length += len(actual.split()) + if idx % 100 == 0: + logger.info( + f"Processed elem {idx}; WER: {total_edit_distance / total_length}" + ) + logger.info(f"Final WER: {total_edit_distance / total_length}") + + +def cli_main(): + parser = ArgumentParser() + parser.add_argument( + "--checkpoint_path", + type=pathlib.Path, + help="Path to checkpoint to use for evaluation.", + ) + parser.add_argument( + "--global_stats_path", + default=pathlib.Path("global_stats.json"), + type=pathlib.Path, + help="Path to JSON file containing feature means and stddevs.", + ) + parser.add_argument( + "--librispeech_path", type=pathlib.Path, help="Path to LibriSpeech datasets.", + ) + parser.add_argument( + "--sp_model_path", type=pathlib.Path, help="Path to SentencePiece model.", + ) + parser.add_argument( + "--use_cuda", action="store_true", default=False, help="Run using CUDA.", + ) + args = parser.parse_args() + run_eval(args) + + +if __name__ == "__main__": + cli_main() diff --git a/examples/asr/librispeech_emformer_rnnt/global_stats.json b/examples/asr/librispeech_emformer_rnnt/global_stats.json new file mode 100644 index 0000000000..9ceb496bf1 --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/global_stats.json @@ -0,0 +1,166 @@ +{ + "mean": [ + 16.462461471557617, + 17.020158767700195, + 17.27733039855957, + 17.273637771606445, + 17.78028678894043, + 18.112783432006836, + 18.322141647338867, + 18.3536319732666, + 18.220436096191406, + 17.93610191345215, + 17.650646209716797, + 17.505868911743164, + 17.450956344604492, + 17.420780181884766, + 17.36254119873047, + 17.24843978881836, + 17.073762893676758, + 16.893953323364258, + 16.62371826171875, + 16.279895782470703, + 16.046218872070312, + 15.789617538452148, + 15.458984375, + 15.335075378417969, + 15.103074073791504, + 14.993032455444336, + 14.818647384643555, + 14.713132858276367, + 14.576343536376953, + 14.482580184936523, + 14.431093215942383, + 14.392385482788086, + 14.357626914978027, + 14.335031509399414, + 14.344644546508789, + 14.341029167175293, + 14.338135719299316, + 14.311485290527344, + 14.266831398010254, + 14.205205917358398, + 14.159194946289062, + 14.07589054107666, + 14.02244758605957, + 13.954248428344727, + 13.897454261779785, + 13.856722831726074, + 13.80321216583252, + 13.75955867767334, + 13.718783378601074, + 13.67695426940918, + 13.626880645751953, + 13.554975509643555, + 13.465453147888184, + 13.372663497924805, + 13.269320487976074, + 13.184920310974121, + 13.094778060913086, + 12.998514175415039, + 12.891039848327637, + 12.765382766723633, + 12.638651847839355, + 12.50733470916748, + 12.345802307128906, + 12.195826530456543, + 12.019110679626465, + 11.842704772949219, + 11.680868148803711, + 11.518675804138184, + 11.37252426147461, + 11.252099990844727, + 11.12936019897461, + 11.029287338256836, + 10.927411079406738, + 10.825841903686523, + 10.717211723327637, + 10.499553680419922, + 9.722028732299805, + 8.256664276123047, + 7.897761344909668, + 7.252806663513184 + ], + "invstddev": [ + 0.2532021571066031, + 0.2597563367511928, + 0.2579079373215276, + 0.2416085222005694, + 0.23003407153886749, + 0.21714598348479108, + 0.20868966256973892, + 0.20397882792073063, + 0.20346486748979434, + 0.20568288111895272, + 0.20795624145573485, + 0.20848980415063503, + 0.20735096423640872, + 0.2060772210458722, + 0.20577174595523076, + 0.20655349986725383, + 0.2080547906859301, + 0.21015748217276387, + 0.2127639989370032, + 0.2156462785763535, + 0.21848300746868443, + 0.22174608140608748, + 0.22541974458780933, + 0.22897465119671973, + 0.23207484606149037, + 0.2353556049061462, + 0.23820711835547867, + 0.24016651485087528, + 0.24200318561465783, + 0.2435905301766702, + 0.24527147180928432, + 0.2493368450351618, + 0.25120444993308483, + 0.2521961451825939, + 0.25358032484699955, + 0.25349767201088286, + 0.2534676894845623, + 0.25149125467665234, + 0.25001929593946776, + 0.25064096375066197, + 0.25194505955280033, + 0.25270402089338095, + 0.2535205901701615, + 0.25363568106276674, + 0.2535307075541985, + 0.25315144026701186, + 0.2523683857532224, + 0.25200854739575596, + 0.2516561583169735, + 0.25147053419035553, + 0.25187638352086095, + 0.25176343344798546, + 0.25256615785525305, + 0.25310796555079107, + 0.2535568871416053, + 0.2542411936874833, + 0.2544978632482573, + 0.2553210332506536, + 0.2567248511819892, + 0.2559665595456875, + 0.2564729970835735, + 0.2585267417223537, + 0.2573770145474615, + 0.2585495460828127, + 0.2593605768768532, + 0.25906572100606984, + 0.26026752519153573, + 0.2609952847918467, + 0.26222905157170767, + 0.26395874733435604, + 0.26404203898769246, + 0.26501581381370537, + 0.2666259054856709, + 0.2676190865432322, + 0.26813030555166134, + 0.26873271506658997, + 0.2624062353014993, + 0.2289515918968408, + 0.22755587298227964, + 0.24719513536827162 + ] +} \ No newline at end of file diff --git a/examples/asr/librispeech_emformer_rnnt/lightning.py b/examples/asr/librispeech_emformer_rnnt/lightning.py new file mode 100644 index 0000000000..fbc900f2de --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/lightning.py @@ -0,0 +1,385 @@ +from collections import namedtuple +import json +import math +import os +from typing import List, Tuple + +import sentencepiece as spm + +import torch +import torchaudio +import torchaudio.functional as F +from torchaudio.prototype.rnnt import emformer_rnnt_base +from torchaudio.prototype.rnnt_decoder import Hypothesis, RNNTBeamSearch +from pytorch_lightning import LightningModule + + +Batch = namedtuple( + "Batch", ["features", "feature_lengths", "targets", "target_lengths"] +) + + +_decibel = 2 * 20 * math.log10(torch.iinfo(torch.int16).max) +_gain = pow(10, 0.05 * _decibel) + +_spectrogram_transform = torchaudio.transforms.MelSpectrogram( + sample_rate=16000, n_fft=400, n_mels=80, hop_length=160 +) + + +def _batch_by_token_count(idx_target_lengths, token_limit): + batches = [] + current_batch = [] + current_token_count = 0 + for idx, target_length in idx_target_lengths: + if current_token_count + target_length > token_limit: + batches.append(current_batch) + current_batch = [idx] + current_token_count = target_length + else: + current_batch.append(idx) + current_token_count += target_length + + if current_batch: + batches.append(current_batch) + + return batches + + +class CustomDataset(torch.utils.data.Dataset): + r"""Sort samples by target length and batch to max token count.""" + + def __init__(self, base_dataset, max_token_limit): + super().__init__() + self.base_dataset = base_dataset + + fileid_to_target_length = {} + idx_target_lengths = [ + (idx, self._target_length(fileid, fileid_to_target_length)) + for idx, fileid in enumerate(self.base_dataset._walker) + ] + + assert len(idx_target_lengths) > 0 + + idx_target_lengths = sorted( + idx_target_lengths, key=lambda x: x[1], reverse=True + ) + + assert max_token_limit >= idx_target_lengths[0][1] + + self.batches = _batch_by_token_count(idx_target_lengths, max_token_limit) + + def _target_length(self, fileid, fileid_to_target_length): + if fileid not in fileid_to_target_length: + speaker_id, chapter_id, _ = fileid.split("-") + + file_text = speaker_id + "-" + chapter_id + self.base_dataset._ext_txt + file_text = os.path.join( + self.base_dataset._path, speaker_id, chapter_id, file_text + ) + + with open(file_text) as ft: + for line in ft: + fileid_text, transcript = line.strip().split(" ", 1) + fileid_to_target_length[fileid_text] = len(transcript) + + return fileid_to_target_length[fileid] + + def __getitem__(self, idx): + return [self.base_dataset[subidx] for subidx in self.batches[idx]] + + def __len__(self): + return len(self.batches) + + +class TimeMasking(torchaudio.transforms._AxisMasking): + def __init__( + self, time_mask_param: int, min_mask_p: float, iid_masks: bool = False + ) -> None: + super(TimeMasking, self).__init__(time_mask_param, 2, iid_masks) + self.min_mask_p = min_mask_p + + def forward(self, specgram: torch.Tensor, mask_value: float = 0.0) -> torch.Tensor: + if self.iid_masks and specgram.dim() == 4: + mask_param = min( + self.mask_param, self.min_mask_p * specgram.shape[self.axis + 1] + ) + return F.mask_along_axis_iid( + specgram, mask_param, mask_value, self.axis + 1 + ) + else: + mask_param = min( + self.mask_param, self.min_mask_p * specgram.shape[self.axis] + ) + return F.mask_along_axis(specgram, mask_param, mask_value, self.axis) + + +class FunctionalModule(torch.nn.Module): + def __init__(self, functional): + super().__init__() + self.functional = functional + + def forward(self, input): + return self.functional(input) + + +class GlobalStatsNormalization(torch.nn.Module): + def __init__(self, global_stats_path): + super().__init__() + + with open(global_stats_path) as f: + blob = json.loads(f.read()) + + self.mean = torch.tensor(blob["mean"]) + self.invstddev = torch.tensor(blob["invstddev"]) + + def forward(self, input): + return (input - self.mean) * self.invstddev + + +def _piecewise_linear_log(x): + x[x > math.e] = torch.log(x[x > math.e]) + x[x <= math.e] = x[x <= math.e] / math.e + return x + + +class WarmupLR(torch.optim.lr_scheduler._LRScheduler): + def __init__(self, optimizer, warmup_updates, last_epoch=-1, verbose=False): + self.warmup_updates = warmup_updates + super().__init__(optimizer, last_epoch=last_epoch, verbose=verbose) + + def get_lr(self): + return [ + (min(1.0, self._step_count / self.warmup_updates)) * base_lr + for base_lr in self.base_lrs + ] + + +def post_process_hypos( + hypos: List[Hypothesis], sp_model: spm.SentencePieceProcessor +) -> List[Tuple[str, float, List[int], List[int]]]: + post_process_remove_list = [ + sp_model.unk_id(), + sp_model.eos_id(), + sp_model.pad_id(), + ] + filtered_hypo_tokens = [ + [ + token_index + for token_index in h.tokens[1:] + if token_index not in post_process_remove_list + ] + for h in hypos + ] + hypos_str = [sp_model.decode(s) for s in filtered_hypo_tokens] + hypos_ali = [h.alignment[1:] for h in hypos] + hypos_ids = [h.tokens[1:] for h in hypos] + hypos_score = [[math.exp(h.score)] for h in hypos] + + nbest_batch = list(zip(hypos_str, hypos_score, hypos_ali, hypos_ids)) + + return nbest_batch + + +class RNNTModule(LightningModule): + def __init__( + self, + *, + librispeech_path: str, + sp_model_path: str, + global_stats_path: str, + ): + super().__init__() + + self.model = emformer_rnnt_base() + self.loss = torchaudio.transforms.RNNTLoss(reduction="sum", clamp=1.0) + self.optimizer = torch.optim.Adam( + self.model.parameters(), lr=5e-4, betas=(0.9, 0.999), eps=1e-8 + ) + self.lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + self.optimizer, factor=0.96, patience=0 + ) + self.warmup_lr_scheduler = WarmupLR(self.optimizer, 10000) + + self.train_data_pipeline = torch.nn.Sequential( + FunctionalModule(lambda x: _piecewise_linear_log(x * _gain)), + GlobalStatsNormalization(global_stats_path), + FunctionalModule(lambda x: x.transpose(1, 2)), + torchaudio.transforms.FrequencyMasking(27), + torchaudio.transforms.FrequencyMasking(27), + TimeMasking(100, 0.2), + TimeMasking(100, 0.2), + FunctionalModule(lambda x: torch.nn.functional.pad(x, (0, 4))), + FunctionalModule(lambda x: x.transpose(1, 2)), + ) + self.valid_data_pipeline = torch.nn.Sequential( + FunctionalModule(lambda x: _piecewise_linear_log(x * _gain)), + GlobalStatsNormalization(global_stats_path), + FunctionalModule(lambda x: x.transpose(1, 2)), + FunctionalModule(lambda x: torch.nn.functional.pad(x, (0, 4))), + FunctionalModule(lambda x: x.transpose(1, 2)), + ) + + self.librispeech_path = librispeech_path + + self.sp_model = spm.SentencePieceProcessor(model_file=sp_model_path) + self.blank_idx = self.sp_model.get_piece_size() + + def _extract_labels(self, samples: List): + targets = [self.sp_model.encode(sample[2].lower()) for sample in samples] + lengths = torch.tensor([len(elem) for elem in targets]).to(dtype=torch.int32) + targets = torch.nn.utils.rnn.pad_sequence( + [torch.tensor(elem) for elem in targets], + batch_first=True, + padding_value=1.0, + ).to(dtype=torch.int32) + return targets, lengths + + def _train_extract_features(self, samples: List): + mel_features = [ + _spectrogram_transform(sample[0].squeeze()).transpose(1, 0) + for sample in samples + ] + features = torch.nn.utils.rnn.pad_sequence(mel_features, batch_first=True) + features = self.train_data_pipeline(features) + lengths = torch.tensor( + [elem.shape[0] for elem in mel_features], dtype=torch.int32 + ) + return features, lengths + + def _valid_extract_features(self, samples: List): + mel_features = [ + _spectrogram_transform(sample[0].squeeze()).transpose(1, 0) + for sample in samples + ] + features = torch.nn.utils.rnn.pad_sequence(mel_features, batch_first=True) + features = self.valid_data_pipeline(features) + lengths = torch.tensor( + [elem.shape[0] for elem in mel_features], dtype=torch.int32 + ) + return features, lengths + + def _train_collate_fn(self, samples: List): + features, feature_lengths = self._train_extract_features(samples) + targets, target_lengths = self._extract_labels(samples) + return Batch(features, feature_lengths, targets, target_lengths) + + def _valid_collate_fn(self, samples: List): + features, feature_lengths = self._valid_extract_features(samples) + targets, target_lengths = self._extract_labels(samples) + return Batch(features, feature_lengths, targets, target_lengths) + + def _test_collate_fn(self, samples: List): + return self._valid_collate_fn(samples), samples + + def _step(self, batch, batch_idx, step_type): + if batch is None: + return None + + prepended_targets = batch.targets.new_empty( + [batch.targets.size(0), batch.targets.size(1) + 1] + ) + prepended_targets[:, 1:] = batch.targets + prepended_targets[:, 0] = self.blank_idx + prepended_target_lengths = batch.target_lengths + 1 + output, src_lengths, _, _ = self.model( + batch.features, + batch.feature_lengths, + prepended_targets, + prepended_target_lengths, + ) + loss = self.loss(output, batch.targets, src_lengths, batch.target_lengths) + self.log(f"Losses/{step_type}_loss", loss, on_step=True, on_epoch=True) + return loss + + def configure_optimizers(self): + return ( + [self.optimizer], + [ + { + "scheduler": self.lr_scheduler, + "monitor": "Losses/val_loss", + "interval": "epoch", + }, + {"scheduler": self.warmup_lr_scheduler, "interval": "step"}, + ], + ) + + def forward(self, batch: Batch): + decoder = RNNTBeamSearch(self.model, self.blank_idx) + hypotheses = decoder( + batch.features.to(self.device), batch.feature_lengths.to(self.device), 20 + ) + return post_process_hypos(hypotheses, self.sp_model)[0][0] + + def training_step(self, batch: Batch, batch_idx): + return self._step(batch, batch_idx, "train") + + def validation_step(self, batch, batch_idx): + return self._step(batch, batch_idx, "val") + + def test_step(self, batch, batch_idx): + return self._step(batch, batch_idx, "test") + + def train_dataloader(self): + dataset = torch.utils.data.ConcatDataset( + [ + CustomDataset( + torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="train-clean-360" + ), + 1000, + ), + CustomDataset( + torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="train-clean-100" + ), + 1000, + ), + CustomDataset( + torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="train-other-500" + ), + 1000, + ), + ] + ) + dataloader = torch.utils.data.DataLoader( + dataset, + batch_size=None, + collate_fn=self._train_collate_fn, + num_workers=10, + shuffle=True, + ) + return dataloader + + def val_dataloader(self): + dataset = torch.utils.data.ConcatDataset( + [ + CustomDataset( + torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="dev-clean" + ), + 1000, + ), + CustomDataset( + torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="dev-other" + ), + 1000, + ), + ] + ) + dataloader = torch.utils.data.DataLoader( + dataset, batch_size=None, collate_fn=self._valid_collate_fn, num_workers=10, + ) + return dataloader + + def test_dataloader(self): + dataset = torchaudio.datasets.LIBRISPEECH( + self.librispeech_path, url="test-clean" + ) + dataloader = torch.utils.data.DataLoader( + dataset, batch_size=1, collate_fn=self._test_collate_fn + ) + return dataloader diff --git a/examples/asr/librispeech_emformer_rnnt/train.py b/examples/asr/librispeech_emformer_rnnt/train.py new file mode 100644 index 0000000000..435b8eff7b --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/train.py @@ -0,0 +1,95 @@ +from argparse import ArgumentParser +import pathlib + +from pytorch_lightning import Trainer +from pytorch_lightning.callbacks import ModelCheckpoint + +from lightning import RNNTModule + + +def run_train(args): + checkpoint_dir = args.exp_dir / "checkpoints" + checkpoint = ModelCheckpoint( + checkpoint_dir, + monitor="Losses/val_loss", + mode="min", + save_top_k=5, + save_weights_only=True, + verbose=True, + ) + train_checkpoint = ModelCheckpoint( + checkpoint_dir, + monitor="Losses/train_loss", + mode="min", + save_top_k=5, + save_weights_only=True, + verbose=True, + ) + callbacks = [ + checkpoint, + train_checkpoint, + ] + trainer = Trainer( + default_root_dir=args.exp_dir, + max_epochs=args.epochs, + num_nodes=args.num_nodes, + gpus=args.gpus, + accelerator="gpu", + strategy="ddp", + gradient_clip_val=10.0, + callbacks=callbacks, + ) + + model = RNNTModule( + librispeech_path=str(args.librispeech_path), + sp_model_path=str(args.sp_model_path), + global_stats_path=str(args.global_stats_path), + ) + trainer.fit(model) + + +def cli_main(): + parser = ArgumentParser() + parser.add_argument( + "--exp_dir", + default=pathlib.Path("./exp"), + type=pathlib.Path, + help="Directory to save checkpoints and logs to. (Default: './exp')", + ) + parser.add_argument( + "--global_stats_path", + default=pathlib.Path("global_stats.json"), + type=pathlib.Path, + help="Path to JSON file containing feature means and stddevs.", + ) + parser.add_argument( + "--librispeech_path", type=pathlib.Path, help="Path to LibriSpeech datasets.", + ) + parser.add_argument( + "--sp_model_path", type=pathlib.Path, help="Path to SentencePiece model.", + ) + parser.add_argument( + "--num_nodes", + default=4, + type=int, + help="Number of nodes to use for training. (Default: 4)", + ) + parser.add_argument( + "--gpus", + default=8, + type=int, + help="Number of GPUs per node to use for training. (Default: 8)", + ) + parser.add_argument( + "--epochs", + default=120, + type=int, + help="Number of epochs to train for. (Default: 120)", + ) + args = parser.parse_args() + + run_train(args) + + +if __name__ == "__main__": + cli_main() From 338d38a27003aa9e030ec66122804f4fcf1ea2b5 Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Fri, 3 Dec 2021 14:00:53 -0800 Subject: [PATCH 0025/1144] Adding warnings in mu_law* for the wrong input type (#2034) Summary: Addresses https://github.com/pytorch/audio/issues/1493 cc mthrok hwangjeff Pull Request resolved: https://github.com/pytorch/audio/pull/2034 Reviewed By: hwangjeff Differential Revision: D32807006 Pulled By: mthrok fbshipit-source-id: badf148646c5f768328c5a4e51bd6016b0be46f3 --- torchaudio/functional/functional.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/torchaudio/functional/functional.py b/torchaudio/functional/functional.py index 9730791b21..74ea1fd2ea 100644 --- a/torchaudio/functional/functional.py +++ b/torchaudio/functional/functional.py @@ -612,7 +612,7 @@ def mu_law_encoding( r"""Encode signal based on mu-law companding. For more info see the `Wikipedia Entry `_ - This algorithm assumes the signal has been scaled to between -1 and 1 and + This algorithm expects the signal has been scaled to between -1 and 1 and returns a signal encoded with values from 0 to quantization_channels - 1. Args: @@ -624,6 +624,8 @@ def mu_law_encoding( """ mu = quantization_channels - 1.0 if not x.is_floating_point(): + warnings.warn("The input Tensor must be of floating type. \ + This will be an error in the v0.12 release.") x = x.to(torch.float) mu = torch.tensor(mu, dtype=x.dtype) x_mu = torch.sign(x) * torch.log1p(mu * torch.abs(x)) / torch.log1p(mu) From a401dcb8f0d209f6174c41a9fc73c308d1822afb Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:02:12 -0800 Subject: [PATCH 0026/1144] Clean up libtorchaudio customization logic (#2039) Summary: (See https://github.com/pytorch/audio/issues/2038 description for the overall goal.) This PR cleans up CMake customization logic for `libtorchaudio`. It introduces base variables LIBTORCHAUDIO_INCLUDE_DIRS, LIBTORCHAUDIO_LINK_LIBRARIES and LIBTORCHAUDIO_COMPILE_DEFINITIONS, which are respectively used when calling `target_include_directories`, `target_link_libraries` and `target_compile_definitions`. The customization logic only modifies these variables. The original implementation called these functions multiple times (once par customization logic) and it is getting difficult to understand the customization logic. Pull Request resolved: https://github.com/pytorch/audio/pull/2039 Reviewed By: carolineechen, nateanl Differential Revision: D32683004 Pulled By: mthrok fbshipit-source-id: 4d41f21692ac139b1185a6ab69eb45d881ee7e73 --- torchaudio/csrc/CMakeLists.txt | 94 +++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index 9ca8efe0cb..e634e6ad64 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -10,6 +10,23 @@ set( utils.cpp ) +set( + LIBTORCHAUDIO_INCLUDE_DIRS + ${PROJECT_SOURCE_DIR} + ) + +set( + LIBTORCHAUDIO_LINK_LIBRARIES + torch + ${TORCHAUDIO_THIRD_PARTIES} + ) + +set( + LIBTORCHAUDIO_COMPILE_DEFINITIONS) + +#------------------------------------------------------------------------------# +# START OF CUSTOMIZATION LOGICS +#------------------------------------------------------------------------------# if(BUILD_RNNT) list( APPEND @@ -33,8 +50,28 @@ if(BUILD_RNNT) endif() endif() +if(USE_CUDA) + list( + APPEND + LIBTORCHAUDIO_INCLUDE_DIRS + ${CUDA_TOOLKIT_INCLUDE} + ) + list( + APPEND + LIBTORCHAUDIO_LINK_LIBRARIES + ${C10_CUDA_LIBRARY} + ${CUDA_CUDART_LIBRARY} + ) + list( + APPEND + LIBTORCHAUDIO_COMPILE_DEFINITIONS + USE_CUDA + ) +endif() + if(BUILD_KALDI) list(APPEND LIBTORCHAUDIO_SOURCES kaldi.cpp) + list(APPEND LIBTORCHAUDIO_COMPILE_DEFINITIONS INCLUDE_KALDI) endif() if(BUILD_SOX) @@ -47,8 +84,24 @@ if(BUILD_SOX) sox/effects_chain.cpp sox/types.cpp ) + list( + APPEND + LIBTORCHAUDIO_COMPILE_DEFINITIONS + INCLUDE_SOX + ) +endif() + +if(OpenMP_CXX_FOUND) + list( + APPEND + LIBTORCHAUDIO_LINK_LIBRARIES + OpenMP::OpenMP_CXX + ) endif() +#------------------------------------------------------------------------------# +# END OF CUSTOMIZATION LOGICS +#------------------------------------------------------------------------------# add_library( libtorchaudio SHARED @@ -59,45 +112,21 @@ set_target_properties(libtorchaudio PROPERTIES PREFIX "") target_include_directories( libtorchaudio PRIVATE - ${PROJECT_SOURCE_DIR} + ${LIBTORCHAUDIO_INCLUDE_DIRS} ) - target_link_libraries( libtorchaudio - torch - ${TORCHAUDIO_THIRD_PARTIES} + ${LIBTORCHAUDIO_LINK_LIBRARIES} + ) +target_compile_definitions( + libtorchaudio + PRIVATE ${LIBTORCHAUDIO_COMPILE_DEFINITIONS} ) - -if (BUILD_SOX) - target_compile_definitions(libtorchaudio PUBLIC INCLUDE_SOX) -endif() - -if (BUILD_KALDI) - target_compile_definitions(libtorchaudio PUBLIC INCLUDE_KALDI) -endif() - -if(USE_CUDA) - target_compile_definitions(libtorchaudio PRIVATE USE_CUDA) - target_include_directories( - libtorchaudio - PRIVATE - ${CUDA_TOOLKIT_INCLUDE} - ) - target_link_libraries( - libtorchaudio - ${C10_CUDA_LIBRARY} - ${CUDA_CUDART_LIBRARY} - ) -endif() if (MSVC) set_target_properties(libtorchaudio PROPERTIES SUFFIX ".pyd") endif(MSVC) -if(OpenMP_CXX_FOUND) - target_link_libraries(libtorchaudio OpenMP::OpenMP_CXX) -endif() - install( TARGETS libtorchaudio LIBRARY DESTINATION lib @@ -134,6 +163,11 @@ if (BUILD_TORCHAUDIO_PYTHON_EXTENSION) ${EXTENSION_SOURCES} ) + target_compile_definitions( + _torchaudio + PRIVATE ${LIBTORCHAUDIO_COMPILE_DEFINITIONS} + ) + set_target_properties(_torchaudio PROPERTIES PREFIX "") if (MSVC) set_target_properties(_torchaudio PROPERTIES SUFFIX ".pyd") From 70c7e7e125e37abc63c8e011c175878af8c3ee71 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:23:47 -0800 Subject: [PATCH 0027/1144] Add "+cpu" suffix to doc build job (#2060) Summary: While updating the documentation in release/0.10, a HIP error was raised. https://app.circleci.com/pipelines/github/pytorch/audio/8577/workflows/02c6ff44-a042-4f9a-8fb8-573a231f60db/jobs/452639 This happens because `pip install torchaudio -f https://...` defaults to ROCm version while `build_doc` is supposed to pick the CPU version. Adding suffix `+cpu` should resolve the isssue. It is validated on https://github.com/pytorch/audio/pull/2060 https://app.circleci.com/pipelines/github/pytorch/audio/8584/workflows/25ae26e5-273f-46f8-805d-ffc7b6b8eb58/jobs/453337 Pull Request resolved: https://github.com/pytorch/audio/pull/2060 Reviewed By: carolineechen Differential Revision: D32846765 Pulled By: mthrok fbshipit-source-id: e6b3b32646388b8c4ba864639f8b62d8b9d39844 --- .circleci/build_docs/install_wheels.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/build_docs/install_wheels.sh b/.circleci/build_docs/install_wheels.sh index 3e15154092..4f14c4d1bd 100755 --- a/.circleci/build_docs/install_wheels.sh +++ b/.circleci/build_docs/install_wheels.sh @@ -7,6 +7,8 @@ source ./packaging/pkg_helpers.bash export NO_CUDA_PACKAGE=1 setup_env 0.8.0 setup_wheel_python +# Starting 0.10, `pip install pytorch` defaults to ROCm. +export PYTORCH_VERSION_SUFFIX="+cpu" setup_pip_pytorch_version # pytorch is already installed pip install --no-deps ~/workspace/torchaudio* From 5a2d114db4853ee9d81b7607b82f8453d3bbfe55 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 3 Dec 2021 20:30:27 -0800 Subject: [PATCH 0028/1144] Refactor and functionize the library definition (#2040) Summary: (See https://github.com/pytorch/audio/issues/2038 description for the overall goal.) This commit turns the part that defines `libtorchaudio` into a function so that it becomes easy to define libraries in the same way as `libtorchaudio`. Built on top of https://github.com/pytorch/audio/issues/2039 Pull Request resolved: https://github.com/pytorch/audio/pull/2040 Reviewed By: hwangjeff Differential Revision: D32851990 Pulled By: mthrok fbshipit-source-id: a8206c62b076bc0849ada1a66c7502ae5ea35e28 --- torchaudio/csrc/CMakeLists.txt | 47 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index e634e6ad64..8c124ea713 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -102,35 +102,28 @@ endif() #------------------------------------------------------------------------------# # END OF CUSTOMIZATION LOGICS #------------------------------------------------------------------------------# -add_library( - libtorchaudio - SHARED - ${LIBTORCHAUDIO_SOURCES} - ) -set_target_properties(libtorchaudio PROPERTIES PREFIX "") +function (define_library name source include_dirs link_libraries compile_defs) + add_library(${name} SHARED ${source}) + target_include_directories(${name} PRIVATE ${include_dirs}) + target_link_libraries(${name} ${link_libraries}) + target_compile_definitions(${name} PRIVATE ${compile_defs}) + set_target_properties(${name} PROPERTIES PREFIX "") + if (MSVC) + set_target_properties(${name} PROPERTIES SUFFIX ".pyd") + endif(MSVC) + install( + TARGETS ${name} + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib # For Windows + ) +endfunction() -target_include_directories( +define_library( libtorchaudio - PRIVATE - ${LIBTORCHAUDIO_INCLUDE_DIRS} - ) -target_link_libraries( - libtorchaudio - ${LIBTORCHAUDIO_LINK_LIBRARIES} - ) -target_compile_definitions( - libtorchaudio - PRIVATE ${LIBTORCHAUDIO_COMPILE_DEFINITIONS} - ) - -if (MSVC) - set_target_properties(libtorchaudio PROPERTIES SUFFIX ".pyd") -endif(MSVC) - -install( - TARGETS libtorchaudio - LIBRARY DESTINATION lib - RUNTIME DESTINATION lib # For Windows + "${LIBTORCHAUDIO_SOURCES}" + "${LIBTORCHAUDIO_INCLUDE_DIRS}" + "${LIBTORCHAUDIO_LINK_LIBRARIES}" + "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" ) if (APPLE) From e0280cf5aa285d38fe377e44acc2a92a9eb2b6c9 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 7 Dec 2021 13:50:17 -0800 Subject: [PATCH 0029/1144] Add wrapper classes that manage memories allocated by ffmpeg (#2041) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add wrapper classes that auto release memories allocated by ffmpeg libraries. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. - [x] Needs to be imported after updating TARGETS file. Pull Request resolved: https://github.com/pytorch/audio/pull/2041 Reviewed By: carolineechen Differential Revision: D32688964 Pulled By: mthrok fbshipit-source-id: 165bef5b292dbedae4e9599d53fb2a3f06978db8 --- torchaudio/csrc/ffmpeg/ffmpeg.cpp | 160 ++++++++++++++++++++++++++++++ torchaudio/csrc/ffmpeg/ffmpeg.h | 137 +++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/ffmpeg.cpp create mode 100644 torchaudio/csrc/ffmpeg/ffmpeg.h diff --git a/torchaudio/csrc/ffmpeg/ffmpeg.cpp b/torchaudio/csrc/ffmpeg/ffmpeg.cpp new file mode 100644 index 0000000000..ed434dc0aa --- /dev/null +++ b/torchaudio/csrc/ffmpeg/ffmpeg.cpp @@ -0,0 +1,160 @@ +#include + +namespace torchaudio { +namespace ffmpeg { + +//////////////////////////////////////////////////////////////////////////////// +// AVFormatContext +//////////////////////////////////////////////////////////////////////////////// +void AVFormatContextDeleter::operator()(AVFormatContext* p) { + avformat_close_input(&p); +}; + +namespace { +AVFormatContext* get_format_context( + const std::string& src, + const std::string& device, + AVDictionary** option) { + AVFormatContext* pFormat = NULL; + AVInputFormat* pInput = + device.empty() ? NULL : av_find_input_format(device.c_str()); + + if (avformat_open_input(&pFormat, src.c_str(), pInput, option) < 0) + throw std::runtime_error("Failed to open the input: " + src); + return pFormat; +} +} // namespace + +AVFormatContextPtr::AVFormatContextPtr( + const std::string& src, + const std::string& device, + AVDictionary** option) + : Wrapper( + get_format_context(src, device, option)) { + if (avformat_find_stream_info(ptr.get(), NULL) < 0) + throw std::runtime_error("Failed to find stream information."); +} + +//////////////////////////////////////////////////////////////////////////////// +// AVPacket +//////////////////////////////////////////////////////////////////////////////// +void AVPacketDeleter::operator()(AVPacket* p) { + av_packet_free(&p); +}; + +namespace { +AVPacket* get_av_packet() { + AVPacket* pPacket = av_packet_alloc(); + if (!pPacket) + throw std::runtime_error("Failed to allocate AVPacket object."); + return pPacket; +} +} // namespace + +AVPacketPtr::AVPacketPtr() + : Wrapper(get_av_packet()) {} + +//////////////////////////////////////////////////////////////////////////////// +// AVPacket - buffer unref +//////////////////////////////////////////////////////////////////////////////// +AutoPacketUnref::AutoPacketUnref(AVPacketPtr& p) : p_(p){}; +AutoPacketUnref::~AutoPacketUnref() { + av_packet_unref(p_); +} +AutoPacketUnref::operator AVPacket*() const { + return p_; +} + +//////////////////////////////////////////////////////////////////////////////// +// AVFrame +//////////////////////////////////////////////////////////////////////////////// +void AVFrameDeleter::operator()(AVFrame* p) { + av_frame_free(&p); +}; +namespace { +AVFrame* get_av_frame() { + AVFrame* pFrame = av_frame_alloc(); + if (!pFrame) + throw std::runtime_error("Failed to allocate AVFrame object."); + return pFrame; +} +} // namespace + +AVFramePtr::AVFramePtr() : Wrapper(get_av_frame()) {} + +/////////////////////////////////////////////////////////////////////////////// +// AVFrame - buffer unref +//////////////////////////////////////////////////////////////////////////////// +AutoFrameUnref::AutoFrameUnref(AVFramePtr& p) : p_(p){}; +AutoFrameUnref::~AutoFrameUnref() { + av_frame_unref(p_); +} +AutoFrameUnref::operator AVFrame*() const { + return p_; +} + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecContext +//////////////////////////////////////////////////////////////////////////////// +void AVCodecContextDeleter::operator()(AVCodecContext* p) { + avcodec_free_context(&p); +}; + +namespace { +AVCodecContext* get_codec_context(AVCodecParameters* pParams) { + const AVCodec* pCodec = avcodec_find_decoder(pParams->codec_id); + + if (!pCodec) { + throw std::runtime_error("Unknown codec."); + } + + AVCodecContext* pCodecContext = avcodec_alloc_context3(pCodec); + if (!pCodecContext) { + throw std::runtime_error("Failed to allocate CodecContext."); + } + return pCodecContext; +} + +void init_codec_context( + AVCodecContext* pCodecContext, + AVCodecParameters* pParams) { + const AVCodec* pCodec = avcodec_find_decoder(pParams->codec_id); + + if (avcodec_parameters_to_context(pCodecContext, pParams) < 0) { + throw std::runtime_error("Failed to set CodecContext parameter."); + } + + if (avcodec_open2(pCodecContext, pCodec, NULL) < 0) { + throw std::runtime_error("Failed to initialize CodecContext."); + } + + if (pParams->codec_type == AVMEDIA_TYPE_AUDIO && !pParams->channel_layout) + pParams->channel_layout = + av_get_default_channel_layout(pCodecContext->channels); +} +} // namespace + +AVCodecContextPtr::AVCodecContextPtr(AVCodecParameters* pParam) + : Wrapper( + get_codec_context(pParam)) { + init_codec_context(ptr.get(), pParam); +} +//////////////////////////////////////////////////////////////////////////////// +// AVFilterGraph +//////////////////////////////////////////////////////////////////////////////// +void AVFilterGraphDeleter::operator()(AVFilterGraph* p) { + avfilter_graph_free(&p); +}; + +namespace { +AVFilterGraph* get_filter_graph() { + AVFilterGraph* ptr = avfilter_graph_alloc(); + if (!ptr) + throw std::runtime_error("Failed to allocate resouce."); + return ptr; +} +} // namespace +AVFilterGraphPtr::AVFilterGraphPtr() + : Wrapper(get_filter_graph()) {} +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/ffmpeg.h b/torchaudio/csrc/ffmpeg/ffmpeg.h new file mode 100644 index 0000000000..da058e33c8 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/ffmpeg.h @@ -0,0 +1,137 @@ +// One stop header for all ffmepg needs +#pragma once +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +namespace torchaudio { +namespace ffmpeg { + +// Base structure that handles memory management. +// Resource is freed by the destructor of unique_ptr, +// which will call custom delete mechanism provided via Deleter +// https://stackoverflow.com/a/19054280 +// +// The resource allocation will be provided by custom constructors. +template +class Wrapper { + protected: + std::unique_ptr ptr; + + public: + Wrapper() = delete; + Wrapper(T* t) : ptr(t){}; + T* operator->() const { + return ptr.get(); + }; + explicit operator bool() const { + return (bool)ptr; + }; + operator T*() const { + return ptr.get(); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVFormatContext +//////////////////////////////////////////////////////////////////////////////// +struct AVFormatContextDeleter { + void operator()(AVFormatContext* p); +}; + +struct AVFormatContextPtr + : public Wrapper { + AVFormatContextPtr( + const std::string& src, + const std::string& device, + AVDictionary** option); +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVPacket +//////////////////////////////////////////////////////////////////////////////// +struct AVPacketDeleter { + void operator()(AVPacket* p); +}; + +struct AVPacketPtr : public Wrapper { + AVPacketPtr(); +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVPacket - buffer unref +//////////////////////////////////////////////////////////////////////////////// +// AVPacket structure employs two-staged memory allocation. +// The first-stage is for allocating AVPacket object itself, and it typically +// happens only once throughout the lifetime of application. +// The second-stage is for allocating the content (media data) each time the +// input file is processed and a chunk of data is read. The memory allocated +// during this time has to be released before the next iteration. +// The first-stage memory management is handled by `AVPacketPtr`. +// `AutoPacketUnref` handles the second-stage memory management. +struct AutoPacketUnref { + AVPacketPtr& p_; + AutoPacketUnref(AVPacketPtr& p); + ~AutoPacketUnref(); + operator AVPacket*() const; +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVFrame +//////////////////////////////////////////////////////////////////////////////// +struct AVFrameDeleter { + void operator()(AVFrame* p); +}; + +struct AVFramePtr : public Wrapper { + AVFramePtr(); +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVFrame - buffer unref +//////////////////////////////////////////////////////////////////////////////// +// Similar to `AutoPacketUnref`, this structure will release the memory +// allocated for frame content. +struct AutoFrameUnref { + AVFramePtr& p_; + AutoFrameUnref(AVFramePtr& p); + ~AutoFrameUnref(); + operator AVFrame*() const; +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecContext +//////////////////////////////////////////////////////////////////////////////// +struct AVCodecContextDeleter { + void operator()(AVCodecContext* p); +}; +struct AVCodecContextPtr + : public Wrapper { + AVCodecContextPtr(AVCodecParameters* pParam); +}; + +//////////////////////////////////////////////////////////////////////////////// +// AVFilterGraph +//////////////////////////////////////////////////////////////////////////////// +struct AVFilterGraphDeleter { + void operator()(AVFilterGraph* p); +}; +struct AVFilterGraphPtr : public Wrapper { + AVFilterGraphPtr(); +}; +} // namespace ffmpeg +} // namespace torchaudio From 34e1d24f0d64e989500d4fed39096a44087109ef Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 7 Dec 2021 18:48:51 -0800 Subject: [PATCH 0030/1144] Add decoder class (#2042) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add `Decoder` class that manages `AVCodecContext` resource and process input `AVPacket`. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2041. Pull Request resolved: https://github.com/pytorch/audio/pull/2042 Reviewed By: carolineechen Differential Revision: D32933294 Pulled By: mthrok fbshipit-source-id: e443debadb44d491462fb641cd5b7b20c413b5b9 --- torchaudio/csrc/ffmpeg/decoder.cpp | 20 ++++++++++++++++++++ torchaudio/csrc/ffmpeg/decoder.h | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/decoder.cpp create mode 100644 torchaudio/csrc/ffmpeg/decoder.h diff --git a/torchaudio/csrc/ffmpeg/decoder.cpp b/torchaudio/csrc/ffmpeg/decoder.cpp new file mode 100644 index 0000000000..052e588989 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/decoder.cpp @@ -0,0 +1,20 @@ +#include + +namespace torchaudio { +namespace ffmpeg { + +//////////////////////////////////////////////////////////////////////////////// +// Decoder +//////////////////////////////////////////////////////////////////////////////// +Decoder::Decoder(AVCodecParameters* pParam) : pCodecContext(pParam) {} + +int Decoder::process_packet(AVPacket* pPacket) { + return avcodec_send_packet(pCodecContext, pPacket); +} + +int Decoder::get_frame(AVFrame* pFrame) { + return avcodec_receive_frame(pCodecContext, pFrame); +} + +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/decoder.h b/torchaudio/csrc/ffmpeg/decoder.h new file mode 100644 index 0000000000..34b19715b4 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/decoder.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace torchaudio { +namespace ffmpeg { + +class Decoder { + AVCodecContextPtr pCodecContext; + + public: + // Default constructable + Decoder(AVCodecParameters* pParam); + // Custom destructor to clean up the resources + ~Decoder() = default; + // Non-copyable + Decoder(const Decoder&) = delete; + Decoder& operator=(const Decoder&) = delete; + // Movable + Decoder(Decoder&&) = default; + Decoder& operator=(Decoder&&) = default; + + // Process incoming packet + int process_packet(AVPacket* pPacket); + // Fetch a decoded frame + int get_frame(AVFrame* pFrame); +}; + +} // namespace ffmpeg +} // namespace torchaudio From 7d092896a59e8f91dcf59e9ee4ccd0fbe93166da Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Fri, 10 Dec 2021 02:40:33 -0800 Subject: [PATCH 0031/1144] OSS config for lint checks (#2066) Summary: Following up on [this comment ](https://github.com/pytorch/audio/pull/2056#issuecomment-988356439) I am separating the config changes from the formatting. cc NicolasHug mthrok Pull Request resolved: https://github.com/pytorch/audio/pull/2066 Reviewed By: mthrok Differential Revision: D32990377 Pulled By: jdsgomes fbshipit-source-id: 67a6251a51901702ad10ae43c35609a09cbf5c5c --- .circleci/config.yml | 22 ++++++++++++++++++++++ .circleci/config.yml.in | 22 ++++++++++++++++++++++ .pre-commit-config.yaml | 17 +++++++++++++++++ pyproject.toml | 10 ++++++++++ 4 files changed, 71 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml diff --git a/.circleci/config.yml b/.circleci/config.yml index 37ed6add17..2dd8bae578 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -112,6 +112,25 @@ jobs: python .circleci/regenerate.py git diff --exit-code || (echo ".circleci/config.yml not in sync with config.yml.in! Run .circleci/regenerate.py to update config"; exit 1) + lint_python_and_config: + docker: + - image: circleci/python:3.7 + steps: + - checkout + - run: + name: Install pre-commit + command: pip install --user --progress-bar off pre-commit + - run: + name: Install pre-commit hooks + command: pre-commit install-hooks + - run: + name: Lint Python code and config files + command: pre-commit run --all-files || true + - run: + name: Required lint modifications + when: on_fail + command: git --no-pager diff + download_third_parties_nix: docker: - image: "pytorch/torchaudio_unittest_base:manylinux" @@ -642,6 +661,9 @@ jobs: pydocstyle torchaudio workflows: + lint: + jobs: + - lint_python_and_config build: jobs: - circleci_consistency diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 4bb19316a7..5c1a770c38 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -112,6 +112,25 @@ jobs: python .circleci/regenerate.py git diff --exit-code || (echo ".circleci/config.yml not in sync with config.yml.in! Run .circleci/regenerate.py to update config"; exit 1) + lint_python_and_config: + docker: + - image: circleci/python:3.7 + steps: + - checkout + - run: + name: Install pre-commit + command: pip install --user --progress-bar off pre-commit + - run: + name: Install pre-commit hooks + command: pre-commit install-hooks + - run: + name: Lint Python code and config files + command: pre-commit run --all-files || true + - run: + name: Required lint modifications + when: on_fail + command: git --no-pager diff + download_third_parties_nix: docker: - image: "pytorch/torchaudio_unittest_base:manylinux" @@ -642,6 +661,9 @@ jobs: pydocstyle torchaudio workflows: + lint: + jobs: + - lint_python_and_config build: jobs: - circleci_consistency diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..cbddd74ff4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-docstring-first + - id: check-toml + - id: check-yaml + exclude: packaging/.* + - id: end-of-file-fixer + + - repo: https://github.com/omnilib/ufmt + rev: v1.3.0 + hooks: + - id: ufmt + additional_dependencies: + - black == 21.9b0 + - usort == 0.6.4 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..bda3b89bb7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.usort] + +first_party_detection = false + +[tool.black] + +line-length = 120 +target-version = ["py37"] + +[tool.ufmt] From ddb9fb5bbf26ec8cbf50004dfc252792ab13bd54 Mon Sep 17 00:00:00 2001 From: nateanl Date: Fri, 10 Dec 2021 10:17:15 -0800 Subject: [PATCH 0032/1144] Add bucketize sampler and dataset for HuBERT Base model training pipeline (#2000) Summary: The PR adds PyTorch Lightning based training script for HuBERT Base model. There are two iterations of pre-training and 1 iteration of ASR fine-tuning on LibriSpeech dataset. Pull Request resolved: https://github.com/pytorch/audio/pull/2000 Reviewed By: carolineechen Differential Revision: D33021467 Pulled By: nateanl fbshipit-source-id: 77fe5a751943b56b63d5f1fb4e6ef35946e081db --- examples/hubert/dataset/__init__.py | 12 + examples/hubert/dataset/hubert_dataset.py | 333 ++++++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 examples/hubert/dataset/__init__.py create mode 100644 examples/hubert/dataset/hubert_dataset.py diff --git a/examples/hubert/dataset/__init__.py b/examples/hubert/dataset/__init__.py new file mode 100644 index 0000000000..a971085f3c --- /dev/null +++ b/examples/hubert/dataset/__init__.py @@ -0,0 +1,12 @@ +from .hubert_dataset import ( + BucketizeSampler, + CollateFnHubert, + HuBERTDataSet, +) + + +__all__ = [ + "BucketizeSampler", + "CollateFnHubert", + "HuBERTDataSet", +] diff --git a/examples/hubert/dataset/hubert_dataset.py b/examples/hubert/dataset/hubert_dataset.py new file mode 100644 index 0000000000..8d415e763f --- /dev/null +++ b/examples/hubert/dataset/hubert_dataset.py @@ -0,0 +1,333 @@ +from pathlib import Path +from typing import ( + Dict, + Iterator, + List, + Optional, + Tuple, + Union, +) +from torch import Tensor +import numpy as np +import random +import torch +import torchaudio +from torch.utils.data import Dataset, BatchSampler + + +class BucketizeSampler(BatchSampler): + """Buketize sampler for data with different lengths to reduce number of paddings. + + Args: + data_source (Dataset): The dataset to sample + num_buckets (int): The number of buckets to split the data samples. + max_token_count (int or None, optional): The max number of tokens in one mini-batch. + (Default: ``None``) + batch_size (int or None, optional): The number of samples in one mini-batch. + (Default: ``None``) + + Note: If ``max_token_count`` is not ``None``, the ``batch_size`` couldn't be set since + the lengths of samples are unknown, the batch size may be different for different + mini-batches. + """ + def __init__( + self, + data_source: Dataset, + num_buckets: int, + max_token_count: Optional[int] = None, + batch_size: Optional[int] = None + ) -> None: + if max_token_count is not None and batch_size is not None: + raise AssertionError( + "The ``max_token_count`` and ``batch_size`` can't be both set." + ) + self.data_source = data_source + self.max_token_count = max_token_count + self.batch_size = batch_size + self.buckets = self._get_buckets(self.data_source, num_buckets) + + def _get_buckets( + self, + data_source: Dataset, + num_buckets: int + ) -> Dict[int, Tensor]: + """Generate buckets based on the dataset. + Args: + data_source (Dataset): The dataset object to bucketize. + num_buckets (int): The number of buckets. + + Returns: + (dict[int, Tensor]): A dictionary in which the key is the bucket index, the value is + the Tensor of corresponding sample indices. + """ + buckets = {} + len_list = data_source.len_list + min_len, max_len = min(len_list), max(len_list) + + boundaries = [min_len - 1] + interval = (max_len - min_len) // num_buckets + for i in range(1, num_buckets): + boundaries.append(min_len + i * interval) + boundaries.append(max_len + 1) + bucket_ids = torch.bucketize(torch.tensor(len_list), torch.tensor(boundaries)) + for i, _ in enumerate(len_list): + bucket_id = bucket_ids[i] + if bucket_id in buckets: + buckets[bucket_id].append(i) + else: + buckets[bucket_id] = [i] + for k in buckets: + random.shuffle(buckets[k]) + buckets[k] = torch.as_tensor(buckets[k], dtype=torch.int) + return buckets + + def __iter__(self) -> Iterator[List[int]]: + iter_list = [] + total_len = 0 + batch = [] + len_list = self.data_source.len_list + if self.max_token_count: + for k in self.buckets.keys(): + for i in range(self.buckets[k].size(0)): + index = self.buckets[k][i] + if total_len > self.max_token_count: + iter_list.append(batch) + batch = [index] + total_len = len_list[index] + else: + batch.append(index) + total_len += len_list[index] + else: + for k in self.buckets.keys(): + for i in range(self.buckets[k].size(0)): + index = self.buckets[k][i] + if total_len == self.batch_size: + iter_list.append(batch) + batch = [index] + total_len = 1 + else: + batch.append(index) + total_len += 1 + + for batch in iter_list: + yield batch + + def __len__(self): + return len(self.data_source) + + +class HuBERTDataSet(Dataset): + """Create a Dataset for HuBERT model training and fine-tuning. + + Args: + exp_dir (str or Path): The root directory of the ``.tsv`` file list. + dataset (str): The dataset for training. Options: [``librispeech``, ``librilight``]. + subset (str): The subset of the dataset. Options: [``train``, ``valid``]. + min_sample (int): The minimum number of audio samples in the dataset. (Default: 32000) + max_sample (int): The maximum number of audio samples in the dataset. (Default: 250000) + """ + def __init__( + self, + exp_dir: Union[str, Path], + dataset: str, + subset: str, + min_sample: int = 32000, + max_sample: int = 250000, + ) -> None: + self.exp_dir = Path(exp_dir) + tsv_dir = self.exp_dir / "tsv" + label_dir = self.exp_dir / "label" + f_list, ind_list, len_list = self._get_lists( + tsv_dir, + dataset, + subset, + min_sample, + max_sample + ) + self.f_list, self.ind_list, self.len_list = f_list, ind_list, len_list + self.labels = self._load_labels(label_dir, dataset, subset) + + def __len__(self): + return len(self.f_list) + + def _get_lists( + self, + tsv_dir: Path, + dataset: str, + subset: str, + min_sample: int, + max_sample: int, + ) -> Tuple[List[Path], List[int], List[int]]: + """Get the list of paths for iteration. + Args: + tsv_dir (Path): The root directory of the ``.tsv`` file list. + dataset (str): The dataset for training. Options: [``librispeech``, ``librilight``]. + subset (str): The subset of the dataset. Options: [``train``, ``valid``]. + min_sample (int): The minimum number of audio samples in the dataset. + max_sample (int): The maximum number of audio samples in the dataset. + + Returns: + (numpy.array) List of file paths. + (numpy.array) List of indices that qualify ``min_sample`` <= length <= ``max_sample``. + (numpy.array) List of waveform lengths. + """ + f_ind_len_list = [] + with open(tsv_dir / f"{dataset}_{subset}.tsv") as f: + root = f.readline().rstrip() + for index, line in enumerate(f): + path, nsample = line.split("\t") + path = f"{root}/{path}" + nsample = int(nsample) + if min_sample <= nsample <= max_sample: + f_ind_len_list.append((path, index, nsample)) + f_ind_len_list.sort(key=lambda x: x[2]) # sort the file lists by the sequence length + f_list, ind_list, len_list = [], [], [] + for ele in f_ind_len_list: + f_list.append(ele[0]) + ind_list.append(ele[1]) + len_list.append(ele[2]) + return np.asarray(f_list), np.asarray(ind_list), np.asarray(len_list) + + def _load_audio( + self, + index: int + ) -> Tensor: + """Load waveform given the sample index of the dataset. + Args: + index (int): The sample index. + + Returns: + (Tensor): The corresponding waveform Tensor. + """ + wav_path = self.f_list[index] + waveform, sample_rate = torchaudio.load(wav_path) + assert waveform.shape[1] == self.len_list[index] + return waveform + + def _load_labels( + self, + label_dir: Path, + dataset: str, + subset: str + ) -> np.array: + """Load all labels to memory into a numpy array. + Args: + label_dir (Path): The directory that contains the label file. + dataset (str): The dataset for training. Options: [``librispeech``, ``librilight``]. + subset (str): The subset of the dataset. Options: [``train``, ``valid``]. + + Returns: + (np.array): The numpy arrary that contains the labels for each audio file. + """ + with open(label_dir / f"{dataset}_{subset}.pt") as f: + labels = [line.rstrip() for line in f] + labels = [labels[i] for i in self.ind_list] + return np.asarray(labels, dtype=np.string_) + + def __getitem__(self, index): + waveform = self._load_audio(index) + length = waveform.shape[1] + label = [int(ele) for ele in self.labels[index].split()] + label = torch.tensor(label) + return (waveform, label, length) + + +class CollateFnHubert: + """The collate class for HuBERT pre-training and fine-tuning. + Args: + feature_type (str): The type of features for KMeans clustering. + Options: [``mfcc``, ``hubert``]. + pad (bool): If ``pad`` is True, the waveforms and labels will be padded + to the max length in the mini-batch. If ``pad`` is False, the waveforms + and labels will be cropped to the minimum length in the mini-batch. + (Default: False) + rand_crop (bool): if ``rand_crop`` is True, the starting index of the + waveform and label is random if the length is longer than the minimum + length in the mini-batch. + """ + def __init__( + self, + feature_type: str, + pad: bool = False, + rand_crop: bool = True, + ) -> None: + self.feature_type = feature_type + self.pad = pad + self.rand_crop = rand_crop + + def __call__(self, batch: Tuple[Tensor, Tensor, int]) -> Tuple[Tensor, Tensor, Tensor]: + """ + Args: + batch (List[Tuple(Tensor, Tensor, int)]): + The list of tuples that contains the waveforms, labels, and audio lengths. + + Returns: + (Tuple(Tensor, Tensor, Tensor)): + The Tensor of waveforms of dimension `[batch, time]`. + The Tensor of labels of dimension `[batch, seq]`. + The Tensor of audio lengths of dimension `[batch,]`. + """ + audio_sizes = [sample[0].shape[1] for sample in batch] + if self.pad: + audio_size = max(audio_sizes) + else: + audio_size = min(audio_sizes) + waveforms, labels, lengths = [], [], [] + for sample in batch: + waveform, label, length = sample + if self.feature_type == "mfcc": + label = label[::2] + waveform, label, length = self._collate_audio_label(waveform, label, length, audio_size, self.rand_crop) + waveforms.append(waveform) + lengths.append(length) + labels.append(label) + + data = torch.zeros(len(batch), audio_size) + for i in range(len(waveforms)): + data[i][0:waveforms[i].shape[1]] = waveforms[i][0] + lengths = torch.tensor(lengths) + labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True) + return data, labels, lengths + + def _collate_audio_label( + self, + waveform: Tensor, + label: Tensor, + length: Tensor, + audio_size: int, + rand_crop: bool, + ) -> Tuple[Tensor, Tensor, Tensor]: + """Collate the audio and label at the same time. + Args: + waveform (Tensor): The waveform Tensor of dimension `[1, time]`. + label (Tensor): The label Tensor of dimension `[1, seq]`. + length (Tensor): The length Tensor of dimension `[1,]`. + audio_size (int): The final length of the waveform. + rand_crop (bool): if ``rand_crop`` is True, the starting index of the + waveform and label is random if the length is longer than the minimum + length in the mini-batch. + + Returns: + (Tuple(Tensor, Tensor, Tensor)): Returns the Tensors for the waveform, + label, and the waveform length. + """ + kernel_size = 25 + stride = 20 + sample_rate = 16 # 16 per millisecond + if waveform.shape[1] > audio_size: + diff = waveform.size(1) - audio_size + audio_start = torch.randint(diff, size=(1,)) if rand_crop else 0 + label_start = torch.div( + audio_start - kernel_size * sample_rate, + stride * sample_rate, + rounding_mode='floor' + ) + label_size = torch.div( + audio_size - kernel_size * sample_rate, + stride * sample_rate, + rounding_mode='floor' + ) + waveform = waveform[:, audio_start:audio_start + audio_size] + label = label[label_start:label_start + label_size] + length = audio_size + return waveform, label, length From 71c2ae7793de0c1ceb9cc96ef6d14bf3e302fb4f Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Fri, 10 Dec 2021 14:05:08 -0800 Subject: [PATCH 0033/1144] Fix CircleCI test failures (#2069) Summary: The unit test failures seems to be caused by [conda 4.11](https://github.com/conda/conda/issues/11096) Remove conda update line fixes the issue. Pull Request resolved: https://github.com/pytorch/audio/pull/2069 Reviewed By: carolineechen Differential Revision: D33023851 Pulled By: nateanl fbshipit-source-id: 73246189d4ccc541e366a5367f532a5b456af8f8 --- .circleci/unittest/linux/scripts/setup_env.sh | 1 - .circleci/unittest/windows/scripts/setup_env.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/.circleci/unittest/linux/scripts/setup_env.sh b/.circleci/unittest/linux/scripts/setup_env.sh index 085dc61a5b..25a9eed3d7 100755 --- a/.circleci/unittest/linux/scripts/setup_env.sh +++ b/.circleci/unittest/linux/scripts/setup_env.sh @@ -24,7 +24,6 @@ if [ ! -d "${conda_dir}" ]; then wget --quiet -O miniconda.sh "http://repo.continuum.io/miniconda/Miniconda3-latest-${os}-x86_64.sh" bash ./miniconda.sh -b -f -p "${conda_dir}" eval "$("${conda_dir}/bin/conda" shell.bash hook)" - conda update --quiet -y conda printf "* Updating the base Python version to %s\n" "${PYTHON_VERSION}" conda install --quiet -y python="${PYTHON_VERSION}" else diff --git a/.circleci/unittest/windows/scripts/setup_env.sh b/.circleci/unittest/windows/scripts/setup_env.sh index 5f092bfceb..07a8efc091 100644 --- a/.circleci/unittest/windows/scripts/setup_env.sh +++ b/.circleci/unittest/windows/scripts/setup_env.sh @@ -24,7 +24,6 @@ if [ ! -d "${conda_dir}" ]; then unset tmp_conda unset miniconda_exe eval "$("${conda_dir}/Scripts/conda.exe" 'shell.bash' 'hook')" - conda update --quiet -y conda printf "* Updating the base Python version to %s\n" "${PYTHON_VERSION}" conda install --quiet -y python="$PYTHON_VERSION" else From 0a701058b432dd602bba3461866bfb3c3a352e04 Mon Sep 17 00:00:00 2001 From: Andrey Talman Date: Fri, 10 Dec 2021 18:28:23 -0800 Subject: [PATCH 0034/1144] Add CUDA-11.5 builds to torchaudio (#2067) Summary: cc peterjc123 maxluk nbcsm guyang3532 gunandrose4u smartcat2010 mszhanyi Pull Request resolved: https://github.com/pytorch/audio/pull/2067 Reviewed By: seemethere Differential Revision: D33032607 Pulled By: atalman fbshipit-source-id: a5767e9af27690d3a7ab762ddf30178b3069cd35 --- .circleci/config.yml | 664 ++++++++++++++++++++ .circleci/regenerate.py | 4 +- packaging/build_conda.sh | 10 +- packaging/pkg_helpers.bash | 11 + packaging/windows/internal/cuda_install.bat | 22 +- 5 files changed, 706 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2dd8bae578..e6d35351a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -696,6 +696,13 @@ workflows: requires: - download_third_parties_nix wheel_docker_image: pytorch/manylinux-cuda113 + - binary_linux_wheel: + cuda_version: cu115 + name: binary_linux_wheel_py3.6_cu115 + python_version: '3.6' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 - binary_linux_wheel: cuda_version: rocm4.1 name: binary_linux_wheel_py3.6_rocm4.1 @@ -730,6 +737,13 @@ workflows: requires: - download_third_parties_nix wheel_docker_image: pytorch/manylinux-cuda113 + - binary_linux_wheel: + cuda_version: cu115 + name: binary_linux_wheel_py3.7_cu115 + python_version: '3.7' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 - binary_linux_wheel: cuda_version: rocm4.1 name: binary_linux_wheel_py3.7_rocm4.1 @@ -770,6 +784,13 @@ workflows: requires: - download_third_parties_nix wheel_docker_image: pytorch/manylinux-cuda113 + - binary_linux_wheel: + cuda_version: cu115 + name: binary_linux_wheel_py3.8_cu115 + python_version: '3.8' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 - binary_linux_wheel: cuda_version: rocm4.1 name: binary_linux_wheel_py3.8_rocm4.1 @@ -804,6 +825,13 @@ workflows: requires: - download_third_parties_nix wheel_docker_image: pytorch/manylinux-cuda113 + - binary_linux_wheel: + cuda_version: cu115 + name: binary_linux_wheel_py3.9_cu115 + python_version: '3.9' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 - binary_linux_wheel: cuda_version: rocm4.1 name: binary_linux_wheel_py3.9_rocm4.1 @@ -844,6 +872,11 @@ workflows: name: binary_windows_wheel_py3.6_cu113 python_version: '3.6' wheel_docker_image: pytorch/manylinux-cuda113 + - binary_windows_wheel: + cuda_version: cu115 + name: binary_windows_wheel_py3.6_cu115 + python_version: '3.6' + wheel_docker_image: pytorch/manylinux-cuda115 - binary_windows_wheel: cuda_version: cpu name: binary_windows_wheel_py3.7_cpu @@ -853,6 +886,11 @@ workflows: name: binary_windows_wheel_py3.7_cu113 python_version: '3.7' wheel_docker_image: pytorch/manylinux-cuda113 + - binary_windows_wheel: + cuda_version: cu115 + name: binary_windows_wheel_py3.7_cu115 + python_version: '3.7' + wheel_docker_image: pytorch/manylinux-cuda115 - binary_windows_wheel: cuda_version: cpu name: binary_windows_wheel_py3.8_cpu @@ -862,6 +900,11 @@ workflows: name: binary_windows_wheel_py3.8_cu113 python_version: '3.8' wheel_docker_image: pytorch/manylinux-cuda113 + - binary_windows_wheel: + cuda_version: cu115 + name: binary_windows_wheel_py3.8_cu115 + python_version: '3.8' + wheel_docker_image: pytorch/manylinux-cuda115 - binary_windows_wheel: cuda_version: cpu name: binary_windows_wheel_py3.9_cpu @@ -871,6 +914,11 @@ workflows: name: binary_windows_wheel_py3.9_cu113 python_version: '3.9' wheel_docker_image: pytorch/manylinux-cuda113 + - binary_windows_wheel: + cuda_version: cu115 + name: binary_windows_wheel_py3.9_cu115 + python_version: '3.9' + wheel_docker_image: pytorch/manylinux-cuda115 - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -899,6 +947,13 @@ workflows: python_version: '3.6' requires: - download_third_parties_nix + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_linux_conda_py3.6_cu115 + python_version: '3.6' + requires: + - download_third_parties_nix - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -927,6 +982,13 @@ workflows: python_version: '3.7' requires: - download_third_parties_nix + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_linux_conda_py3.7_cu115 + python_version: '3.7' + requires: + - download_third_parties_nix - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -955,6 +1017,13 @@ workflows: python_version: '3.8' requires: - download_third_parties_nix + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_linux_conda_py3.8_cu115 + python_version: '3.8' + requires: + - download_third_parties_nix - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -983,6 +1052,13 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_linux_conda_py3.9_cu115 + python_version: '3.9' + requires: + - download_third_parties_nix - binary_macos_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1021,6 +1097,11 @@ workflows: cuda_version: cu113 name: binary_windows_conda_py3.6_cu113 python_version: '3.6' + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_windows_conda_py3.6_cu115 + python_version: '3.6' - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1031,6 +1112,11 @@ workflows: cuda_version: cu113 name: binary_windows_conda_py3.7_cu113 python_version: '3.7' + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_windows_conda_py3.7_cu115 + python_version: '3.7' - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1041,6 +1127,11 @@ workflows: cuda_version: cu113 name: binary_windows_conda_py3.8_cu113 python_version: '3.8' + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_windows_conda_py3.8_cu115 + python_version: '3.8' - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1051,6 +1142,11 @@ workflows: cuda_version: cu113 name: binary_windows_conda_py3.9_cu113 python_version: '3.9' + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + name: binary_windows_conda_py3.9_cu115 + python_version: '3.9' - build_docs: filters: branches: @@ -1352,6 +1448,43 @@ workflows: python_version: '3.6' requires: - nightly_binary_linux_wheel_py3.6_cu113_upload + - binary_linux_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.6_cu115 + python_version: '3.6' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.6_cu115_upload + requires: + - nightly_binary_linux_wheel_py3.6_cu115 + subfolder: cu115/ + - smoke_test_linux_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.6_cu115_smoke_test_pip + python_version: '3.6' + requires: + - nightly_binary_linux_wheel_py3.6_cu115_upload - binary_linux_wheel: cuda_version: rocm4.1 filters: @@ -1536,6 +1669,43 @@ workflows: python_version: '3.7' requires: - nightly_binary_linux_wheel_py3.7_cu113_upload + - binary_linux_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.7_cu115 + python_version: '3.7' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.7_cu115_upload + requires: + - nightly_binary_linux_wheel_py3.7_cu115 + subfolder: cu115/ + - smoke_test_linux_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.7_cu115_smoke_test_pip + python_version: '3.7' + requires: + - nightly_binary_linux_wheel_py3.7_cu115_upload - binary_linux_wheel: cuda_version: rocm4.1 filters: @@ -1720,6 +1890,43 @@ workflows: python_version: '3.8' requires: - nightly_binary_linux_wheel_py3.8_cu113_upload + - binary_linux_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.8_cu115 + python_version: '3.8' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.8_cu115_upload + requires: + - nightly_binary_linux_wheel_py3.8_cu115 + subfolder: cu115/ + - smoke_test_linux_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.8_cu115_smoke_test_pip + python_version: '3.8' + requires: + - nightly_binary_linux_wheel_py3.8_cu115_upload - binary_linux_wheel: cuda_version: rocm4.1 filters: @@ -1904,6 +2111,43 @@ workflows: python_version: '3.9' requires: - nightly_binary_linux_wheel_py3.9_cu113_upload + - binary_linux_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.9_cu115 + python_version: '3.9' + requires: + - download_third_parties_nix + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.9_cu115_upload + requires: + - nightly_binary_linux_wheel_py3.9_cu115 + subfolder: cu115/ + - smoke_test_linux_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_wheel_py3.9_cu115_smoke_test_pip + python_version: '3.9' + requires: + - nightly_binary_linux_wheel_py3.9_cu115_upload - binary_linux_wheel: cuda_version: rocm4.1 filters: @@ -2106,6 +2350,41 @@ workflows: python_version: '3.6' requires: - nightly_binary_windows_wheel_py3.6_cu113_upload + - binary_windows_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.6_cu115 + python_version: '3.6' + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.6_cu115_upload + requires: + - nightly_binary_windows_wheel_py3.6_cu115 + subfolder: cu115/ + - smoke_test_windows_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.6_cu115_smoke_test_pip + python_version: '3.6' + requires: + - nightly_binary_windows_wheel_py3.6_cu115_upload - binary_windows_wheel: cuda_version: cpu filters: @@ -2175,6 +2454,41 @@ workflows: python_version: '3.7' requires: - nightly_binary_windows_wheel_py3.7_cu113_upload + - binary_windows_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.7_cu115 + python_version: '3.7' + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.7_cu115_upload + requires: + - nightly_binary_windows_wheel_py3.7_cu115 + subfolder: cu115/ + - smoke_test_windows_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.7_cu115_smoke_test_pip + python_version: '3.7' + requires: + - nightly_binary_windows_wheel_py3.7_cu115_upload - binary_windows_wheel: cuda_version: cpu filters: @@ -2244,6 +2558,41 @@ workflows: python_version: '3.8' requires: - nightly_binary_windows_wheel_py3.8_cu113_upload + - binary_windows_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.8_cu115 + python_version: '3.8' + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.8_cu115_upload + requires: + - nightly_binary_windows_wheel_py3.8_cu115 + subfolder: cu115/ + - smoke_test_windows_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.8_cu115_smoke_test_pip + python_version: '3.8' + requires: + - nightly_binary_windows_wheel_py3.8_cu115_upload - binary_windows_wheel: cuda_version: cpu filters: @@ -2313,6 +2662,41 @@ workflows: python_version: '3.9' requires: - nightly_binary_windows_wheel_py3.9_cu113_upload + - binary_windows_wheel: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.9_cu115 + python_version: '3.9' + wheel_docker_image: pytorch/manylinux-cuda115 + - binary_wheel_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.9_cu115_upload + requires: + - nightly_binary_windows_wheel_py3.9_cu115 + subfolder: cu115/ + - smoke_test_windows_pip: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_wheel_py3.9_cu115_smoke_test_pip + python_version: '3.9' + requires: + - nightly_binary_windows_wheel_py3.9_cu115_upload - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -2457,6 +2841,42 @@ workflows: python_version: '3.6' requires: - nightly_binary_linux_conda_py3.6_cu113_upload + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.6_cu115 + python_version: '3.6' + requires: + - download_third_parties_nix + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.6_cu115_upload + requires: + - nightly_binary_linux_conda_py3.6_cu115 + - smoke_test_linux_conda_gpu: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.6_cu115_smoke_test_conda + python_version: '3.6' + requires: + - nightly_binary_linux_conda_py3.6_cu115_upload - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -2601,6 +3021,42 @@ workflows: python_version: '3.7' requires: - nightly_binary_linux_conda_py3.7_cu113_upload + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.7_cu115 + python_version: '3.7' + requires: + - download_third_parties_nix + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.7_cu115_upload + requires: + - nightly_binary_linux_conda_py3.7_cu115 + - smoke_test_linux_conda_gpu: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.7_cu115_smoke_test_conda + python_version: '3.7' + requires: + - nightly_binary_linux_conda_py3.7_cu115_upload - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -2745,6 +3201,42 @@ workflows: python_version: '3.8' requires: - nightly_binary_linux_conda_py3.8_cu113_upload + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.8_cu115 + python_version: '3.8' + requires: + - download_third_parties_nix + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.8_cu115_upload + requires: + - nightly_binary_linux_conda_py3.8_cu115 + - smoke_test_linux_conda_gpu: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.8_cu115_smoke_test_conda + python_version: '3.8' + requires: + - nightly_binary_linux_conda_py3.8_cu115_upload - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -2889,6 +3381,42 @@ workflows: python_version: '3.9' requires: - nightly_binary_linux_conda_py3.9_cu113_upload + - binary_linux_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.9_cu115 + python_version: '3.9' + requires: + - download_third_parties_nix + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.9_cu115_upload + requires: + - nightly_binary_linux_conda_py3.9_cu115 + - smoke_test_linux_conda_gpu: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_linux_conda_py3.9_cu115_smoke_test_conda + python_version: '3.9' + requires: + - nightly_binary_linux_conda_py3.9_cu115_upload - binary_macos_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3053,6 +3581,40 @@ workflows: python_version: '3.6' requires: - nightly_binary_windows_conda_py3.6_cu113_upload + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.6_cu115 + python_version: '3.6' + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.6_cu115_upload + requires: + - nightly_binary_windows_conda_py3.6_cu115 + - smoke_test_windows_conda: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.6_cu115_smoke_test_conda + python_version: '3.6' + requires: + - nightly_binary_windows_conda_py3.6_cu115_upload - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3121,6 +3683,40 @@ workflows: python_version: '3.7' requires: - nightly_binary_windows_conda_py3.7_cu113_upload + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.7_cu115 + python_version: '3.7' + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.7_cu115_upload + requires: + - nightly_binary_windows_conda_py3.7_cu115 + - smoke_test_windows_conda: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.7_cu115_smoke_test_conda + python_version: '3.7' + requires: + - nightly_binary_windows_conda_py3.7_cu115_upload - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3189,6 +3785,40 @@ workflows: python_version: '3.8' requires: - nightly_binary_windows_conda_py3.8_cu113_upload + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.8_cu115 + python_version: '3.8' + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.8_cu115_upload + requires: + - nightly_binary_windows_conda_py3.8_cu115 + - smoke_test_windows_conda: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.8_cu115_smoke_test_conda + python_version: '3.8' + requires: + - nightly_binary_windows_conda_py3.8_cu115_upload - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3257,3 +3887,37 @@ workflows: python_version: '3.9' requires: - nightly_binary_windows_conda_py3.9_cu113_upload + - binary_windows_conda: + conda_docker_image: pytorch/conda-builder:cuda115 + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.9_cu115 + python_version: '3.9' + - binary_conda_upload: + context: org-member + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.9_cu115_upload + requires: + - nightly_binary_windows_conda_py3.9_cu115 + - smoke_test_windows_conda: + cuda_version: cu115 + filters: + branches: + only: + - nightly + tags: + only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ + name: nightly_binary_windows_conda_py3.9_cu115_smoke_test_conda + python_version: '3.9' + requires: + - nightly_binary_windows_conda_py3.9_cu115_upload diff --git a/.circleci/regenerate.py b/.circleci/regenerate.py index 6113467652..859a0a2f5c 100755 --- a/.circleci/regenerate.py +++ b/.circleci/regenerate.py @@ -21,8 +21,8 @@ PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] -CU_VERSIONS_DICT = {"linux": ["cpu", "cu102", "cu111","cu113", "rocm4.1"], - "windows": ["cpu", "cu113"], +CU_VERSIONS_DICT = {"linux": ["cpu", "cu102", "cu111","cu113", "cu115", "rocm4.1"], + "windows": ["cpu", "cu113", "cu115"], "macos": ["cpu"]} diff --git a/packaging/build_conda.sh b/packaging/build_conda.sh index e48ae8e3be..f6a1466b28 100755 --- a/packaging/build_conda.sh +++ b/packaging/build_conda.sh @@ -10,5 +10,11 @@ export SOURCE_ROOT_DIR="$PWD" setup_conda_pytorch_constraint setup_conda_cudatoolkit_constraint setup_visual_studio_constraint -# nvidia channel included for cudatoolkit >= 11 -conda build -c defaults -c nvidia $CONDA_CHANNEL_FLAGS --no-anaconda-upload --python "$PYTHON_VERSION" packaging/torchaudio + +# nvidia channel included for cudatoolkit >= 11 however for 11.5 we use conda-forge +export CUDATOOLKIT_CHANNEL="nvidia" +if [[ "$CU_VERSION" == cu115 ]]; then + export CUDATOOLKIT_CHANNEL="conda-forge" +fi + +conda build -c defaults -c $CUDATOOLKIT_CHANNEL $CONDA_CHANNEL_FLAGS --no-anaconda-upload --python "$PYTHON_VERSION" packaging/torchaudio diff --git a/packaging/pkg_helpers.bash b/packaging/pkg_helpers.bash index ffb7df4968..0fc639c96b 100644 --- a/packaging/pkg_helpers.bash +++ b/packaging/pkg_helpers.bash @@ -46,6 +46,14 @@ setup_cuda() { # Now work out the CUDA settings case "$CU_VERSION" in + cu115) + if [[ "$OSTYPE" == "msys" ]]; then + export CUDA_HOME="C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v11.5" + else + export CUDA_HOME=/usr/local/cuda-11.5/ + fi + export TORCH_CUDA_ARCH_LIST="3.5;5.0+PTX;6.0;7.0;7.5;8.0;8.6" + ;; cu113) if [[ "$OSTYPE" == "msys" ]]; then export CUDA_HOME="C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v11.3" @@ -251,6 +259,9 @@ setup_conda_cudatoolkit_constraint() { export CONDA_BUILD_VARIANT="cpu" else case "$CU_VERSION" in + cu115) + export CONDA_CUDATOOLKIT_CONSTRAINT="- cudatoolkit >=11.5,<11.6 # [not osx]" + ;; cu113) export CONDA_CUDATOOLKIT_CONSTRAINT="- cudatoolkit >=11.3,<11.4 # [not osx]" ;; diff --git a/packaging/windows/internal/cuda_install.bat b/packaging/windows/internal/cuda_install.bat index f760ceabf0..e21de2caed 100644 --- a/packaging/windows/internal/cuda_install.bat +++ b/packaging/windows/internal/cuda_install.bat @@ -29,6 +29,7 @@ if %CUDA_VER% EQU 110 goto cuda110 if %CUDA_VER% EQU 111 goto cuda111 if %CUDA_VER% EQU 112 goto cuda112 if %CUDA_VER% EQU 113 goto cuda113 +if %CUDA_VER% EQU 115 goto cuda115 echo CUDA %CUDA_VERSION_STR% is not supported exit /b 1 @@ -183,6 +184,25 @@ if not exist "%SRC_DIR%\temp_build\%CUDNN_INSTALL_ZIP%" ( goto cuda_common +:cuda115 + +set CUDA_INSTALL_EXE=cuda_11.5.0_496.13_win10.exe +if not exist "%SRC_DIR%\temp_build\%CUDA_INSTALL_EXE%" ( + curl -k -L "https://ossci-windows.s3.amazonaws.com/%CUDA_INSTALL_EXE%" --output "%SRC_DIR%\temp_build\%CUDA_INSTALL_EXE%" + if errorlevel 1 exit /b 1 + set "CUDA_SETUP_FILE=%SRC_DIR%\temp_build\%CUDA_INSTALL_EXE%" + set "ARGS=thrust_11.5 nvcc_11.5 cuobjdump_11.5 nvprune_11.5 nvprof_11.5 cupti_11.5 cublas_11.5 cublas_dev_11.5 cudart_11.5 cufft_11.5 cufft_dev_11.5 curand_11.5 curand_dev_11.5 cusolver_11.5 cusolver_dev_11.5 cusparse_11.5 cusparse_dev_11.5 npp_11.5 npp_dev_11.5 nvrtc_11.5 nvrtc_dev_11.5 nvml_dev_11.5" +) + +set CUDNN_INSTALL_ZIP=cudnn-11.3-windows-x64-v8.2.0.53.zip +if not exist "%SRC_DIR%\temp_build\%CUDNN_INSTALL_ZIP%" ( + curl -k -L "http://s3.amazonaws.com/ossci-windows/%CUDNN_INSTALL_ZIP%" --output "%SRC_DIR%\temp_build\%CUDNN_INSTALL_ZIP%" + if errorlevel 1 exit /b 1 + set "CUDNN_SETUP_FILE=%SRC_DIR%\temp_build\%CUDNN_INSTALL_ZIP%" +) + +goto cuda_common + :cuda_common if not exist "%SRC_DIR%\temp_build\NvToolsExt.7z" ( @@ -228,7 +248,7 @@ set "NVTOOLSEXT_PATH=%ProgramFiles%\NVIDIA Corporation\NvToolsExt\bin\x64" if not exist "%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v%CUDA_VERSION_STR%\bin\nvcc.exe" ( echo CUDA %CUDA_VERSION_STR% installed failed. echo --------- RunDll32.exe.log - type "%SRC_DIR%\temp_build\cuda\cuda_install_logs\LOG.RunDll32.exe.log" + type "%SRC_DIR%\temp_build\cuda\cuda_install_logs\LOG.RunDll32.exe.log" echo --------- setup.exe.log ------- type "%SRC_DIR%\temp_build\cuda\cuda_install_logs\LOG.setup.exe.log" exit /b 1 From dba00177d57bb323a7a01510ab60c34ce0bb8faf Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Wed, 15 Dec 2021 07:05:12 -0800 Subject: [PATCH 0035/1144] exlucluding sphinx-gallery examples (#2071) Summary: In order to align with the internal configuration and also torchvision we decided to sphinx-gallery examples from the lint checks . cc NicolasHug mthrok Pull Request resolved: https://github.com/pytorch/audio/pull/2071 Reviewed By: NicolasHug Differential Revision: D33091124 Pulled By: jdsgomes fbshipit-source-id: ffda2dde9115f0590cbde7785007cf811caca7ef --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bda3b89bb7..842008a907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,6 @@ line-length = 120 target-version = ["py37"] [tool.ufmt] +excludes = [ + "examples/tutorials", +] From c02faf04d0c5047de9758570ce5cf1cc8e514aeb Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 17 Dec 2021 10:33:46 -0800 Subject: [PATCH 0036/1144] Introduce helper function to define extension (#2077) Summary: Similar to https://github.com/pytorch/audio/issues/2040 this commit refactor the part of the CMakeLists.txt which defines extension module so that second extension can be added easily. Pull Request resolved: https://github.com/pytorch/audio/pull/2077 Reviewed By: carolineechen Differential Revision: D33189998 Pulled By: mthrok fbshipit-source-id: dc562ce5360332479a7493c21a2930c6fcc6be84 --- torchaudio/csrc/CMakeLists.txt | 88 ++++++++++++++++------------------ 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index 8c124ea713..d4c7b62d71 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -136,10 +136,46 @@ endif() # _torchaudio.so ################################################################################ if (BUILD_TORCHAUDIO_PYTHON_EXTENSION) + # See https://github.com/pytorch/pytorch/issues/38122 + find_library(TORCH_PYTHON_LIBRARY torch_python PATHS "${TORCH_INSTALL_PREFIX}/lib") + if (WIN32) + find_package(Python3 ${PYTHON_VERSION} EXACT COMPONENTS Development) + set(ADDITIONAL_ITEMS Python3::Python) + endif() + function(define_extension name sources libraries definitions) + add_library(${name} SHARED ${sources}) + target_compile_definitions(${name} PRIVATE "${definitions}") + target_include_directories( + ${name} PRIVATE ${PROJECT_SOURCE_DIR} ${Python_INCLUDE_DIR}) + target_link_libraries( + ${name} + ${libraries} + ${TORCH_PYTHON_LIBRARY} + ${ADDITIONAL_ITEMS} + ) + set_target_properties(${name} PROPERTIES PREFIX "") + if (MSVC) + set_target_properties(${name} PROPERTIES SUFFIX ".pyd") + endif(MSVC) + if (APPLE) + # https://github.com/facebookarchive/caffe2/issues/854#issuecomment-364538485 + # https://github.com/pytorch/pytorch/commit/73f6715f4725a0723d8171d3131e09ac7abf0666 + set_target_properties(${name} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") + endif() + install( + TARGETS ${name} + LIBRARY DESTINATION . + RUNTIME DESTINATION . # For Windows + ) + endfunction() + set( EXTENSION_SOURCES pybind/pybind.cpp ) + #----------------------------------------------------------------------------# + # START OF CUSTOMIZATION LOGICS + #----------------------------------------------------------------------------# if(BUILD_SOX) list( APPEND @@ -150,53 +186,13 @@ if (BUILD_TORCHAUDIO_PYTHON_EXTENSION) pybind/sox/utils.cpp ) endif() - add_library( - _torchaudio - SHARED - ${EXTENSION_SOURCES} - ) - - target_compile_definitions( - _torchaudio - PRIVATE ${LIBTORCHAUDIO_COMPILE_DEFINITIONS} - ) - - set_target_properties(_torchaudio PROPERTIES PREFIX "") - if (MSVC) - set_target_properties(_torchaudio PROPERTIES SUFFIX ".pyd") - endif(MSVC) - - if (APPLE) - # https://github.com/facebookarchive/caffe2/issues/854#issuecomment-364538485 - # https://github.com/pytorch/pytorch/commit/73f6715f4725a0723d8171d3131e09ac7abf0666 - set_target_properties(_torchaudio PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") - endif() - - target_include_directories( - _torchaudio - PRIVATE - ${PROJECT_SOURCE_DIR} - ${Python_INCLUDE_DIR} - ) - - # See https://github.com/pytorch/pytorch/issues/38122 - find_library(TORCH_PYTHON_LIBRARY torch_python PATHS "${TORCH_INSTALL_PREFIX}/lib") - - if (WIN32) - find_package(Python3 ${PYTHON_VERSION} EXACT COMPONENTS Development) - set(ADDITIONAL_ITEMS Python3::Python) - endif() - - target_link_libraries( + #----------------------------------------------------------------------------# + # END OF CUSTOMIZATION LOGICS + #----------------------------------------------------------------------------# + define_extension( _torchaudio + "${EXTENSION_SOURCES}" libtorchaudio - ${TORCH_PYTHON_LIBRARY} - ${ADDITIONAL_ITEMS} - ) - - install( - TARGETS _torchaudio - LIBRARY DESTINATION . - RUNTIME DESTINATION . # For Windows + "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" ) endif() From adc559a817c2cfa3a8c24389b5e31d4b1d50cbc6 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 17 Dec 2021 10:51:44 -0800 Subject: [PATCH 0037/1144] Add static build of KenLM (#2076) Summary: Add KenLM and its dependencies required for static build (`zlib`, `bzip2`, `lzma` and `boost-thread`). The KenLM and its dependencies are build but since no corresponding code on torchaudio side is changed, the resulting torchaudio extension module is not changed. (therefore, as long as build process passes on CI this PR should be good to go.) Pull Request resolved: https://github.com/pytorch/audio/pull/2076 Reviewed By: carolineechen Differential Revision: D33189980 Pulled By: mthrok fbshipit-source-id: 6096113128b939f3cf70990c99aacc4aaa954584 --- .gitignore | 1 + .gitmodules | 3 ++ CMakeLists.txt | 1 + setup.py | 38 +++++++++++---- third_party/CMakeLists.txt | 11 +++++ third_party/boost/CMakeLists.txt | 28 +++++++++++ third_party/bzip2/CMakeLists.txt | 27 +++++++++++ third_party/kenlm/CMakeLists.txt | 82 ++++++++++++++++++++++++++++++++ third_party/kenlm/submodule | 1 + third_party/lzma/CMakeLists.txt | 36 ++++++++++++++ third_party/zlib/CMakeLists.txt | 36 ++++++++++++++ tools/setup_helpers/extension.py | 2 + 12 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 third_party/boost/CMakeLists.txt create mode 100644 third_party/bzip2/CMakeLists.txt create mode 100644 third_party/kenlm/CMakeLists.txt create mode 160000 third_party/kenlm/submodule create mode 100644 third_party/lzma/CMakeLists.txt create mode 100644 third_party/zlib/CMakeLists.txt diff --git a/.gitignore b/.gitignore index 607d5ccce5..3d7f0c9e30 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,5 @@ examples/tutorials/_assets # third parties third_party/install/ +third_party/archives/ third_party/sox/archives/ diff --git a/.gitmodules b/.gitmodules index 724846120c..4b8cdd823c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = third_party/kaldi/submodule url = https://github.com/kaldi-asr/kaldi ignore = dirty +[submodule "third_party/kenlm/submodule"] + path = third_party/kenlm/submodule + url = https://github.com/kpu/kenlm diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e0e8346f6..9999dc3860 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ endif() option(BUILD_SOX "Build libsox statically" ON) option(BUILD_KALDI "Build kaldi statically" ON) option(BUILD_RNNT "Enable RNN transducer" ON) +option(BUILD_KENLM "Build KenLM statically" ON) option(BUILD_TORCHAUDIO_PYTHON_EXTENSION "Build Python extension" OFF) option(USE_CUDA "Enable CUDA support" OFF) option(USE_ROCM "Enable ROCM support" OFF) diff --git a/setup.py b/setup.py index a4e27891ea..cea4ee9b24 100644 --- a/setup.py +++ b/setup.py @@ -94,22 +94,39 @@ def _init_submodule(): print(' --- Initialized submodule') +def _parse_url(path): + with open(path, 'r') as file_: + for line in file_: + match = re.match(r'^\s*URL\s+(https:\/\/.+)$', line) + if match: + url = match.group(1) + yield url + + def _parse_sox_sources(): sox_dir = ROOT_DIR / 'third_party' / 'sox' cmake_file = sox_dir / 'CMakeLists.txt' archive_dir = sox_dir / 'archives' archive_dir.mkdir(exist_ok=True) - with open(cmake_file, 'r') as file_: - for line in file_: - match = re.match(r'^\s*URL\s+(https:\/\/.+)$', line) - if match: - url = match.group(1) - path = archive_dir / os.path.basename(url) - yield path, url + for url in _parse_url(cmake_file): + path = archive_dir / os.path.basename(url) + yield path, url + + +def _parse_kenlm_sources(): + third_party_dir = ROOT_DIR / 'third_party' + libs = ['zlib', 'bzip2', 'lzma', 'boost'] + archive_dir = third_party_dir / 'archives' + archive_dir.mkdir(exist_ok=True) + for lib in libs: + cmake_file = third_party_dir / lib / 'CMakeLists.txt' + for url in _parse_url(cmake_file): + path = archive_dir / os.path.basename(url) + yield path, url -def _fetch_sox_archives(): - for dest, url in _parse_sox_sources(): +def _fetch_archives(src): + for dest, url in src: if not dest.exists(): print(f' --- Fetching {os.path.basename(dest)}') torch.hub.download_url_to_file(url, dest, progress=False) @@ -119,7 +136,8 @@ def _fetch_third_party_libraries(): if not (ROOT_DIR / 'third_party' / 'kaldi' / 'submodule' / 'CMakeLists.txt').exists(): _init_submodule() if os.name != 'nt': - _fetch_sox_archives() + _fetch_archives(_parse_sox_sources()) + _fetch_archives(_parse_kenlm_sources()) def _main(): diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index ef984d57b6..6c63ed7428 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -22,3 +22,14 @@ if (BUILD_KALDI) endif() set_property(GLOBAL PROPERTY TORCHAUDIO_THIRD_PARTIES "${TORCHAUDIO_THIRD_PARTIES}") + +################################################################################ +# KenLM +################################################################################ +if (BUILD_KENLM) + add_subdirectory(zlib) + add_subdirectory(bzip2) + add_subdirectory(lzma) + add_subdirectory(boost) + add_subdirectory(kenlm) +endif() diff --git a/third_party/boost/CMakeLists.txt b/third_party/boost/CMakeLists.txt new file mode 100644 index 0000000000..367cb43523 --- /dev/null +++ b/third_party/boost/CMakeLists.txt @@ -0,0 +1,28 @@ +include(ExternalProject) + +set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) +set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) + +ExternalProject_Add(boost_ + PREFIX ${CMAKE_CURRENT_BINARY_DIR} + DOWNLOAD_DIR ${ARCHIVE_DIR} + URL https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.tar.gz + URL_HASH SHA256=94ced8b72956591c4775ae2207a9763d3600b30d9d7446562c552f0a14a63be7 + BUILD_IN_SOURCE 1 + CONFIGURE_COMMAND ./bootstrap.sh --with-libraries=thread --prefix=${INSTALL_DIR} + BUILD_COMMAND ./b2 link=static + INSTALL_COMMAND ./b2 link=static install + DOWNLOAD_NO_PROGRESS ON + LOG_DOWNLOAD ON + LOG_UPDATE ON + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_INSTALL ON + LOG_MERGED_STDOUTERR ON + LOG_OUTPUT_ON_FAILURE ON +) + +add_library(boost INTERFACE) +add_dependencies(boost boost_) +target_include_directories(boost INTERFACE ${INSTALL_DIR}/include) +target_link_libraries(boost INTERFACE ${INSTALL_DIR}/lib/libboost_thread.a) diff --git a/third_party/bzip2/CMakeLists.txt b/third_party/bzip2/CMakeLists.txt new file mode 100644 index 0000000000..70ba3ceb24 --- /dev/null +++ b/third_party/bzip2/CMakeLists.txt @@ -0,0 +1,27 @@ +include(ExternalProject) + +set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) +set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) + +ExternalProject_Add(bzip2_ + PREFIX ${CMAKE_CURRENT_BINARY_DIR} + DOWNLOAD_DIR ${ARCHIVE_DIR} + URL https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz + URL_HASH SHA256=ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269 + BUILD_IN_SOURCE 1 + CONFIGURE_COMMAND "" + INSTALL_COMMAND make install PREFIX=${INSTALL_DIR} + DOWNLOAD_NO_PROGRESS ON + LOG_DOWNLOAD ON + LOG_UPDATE ON + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_INSTALL ON + LOG_MERGED_STDOUTERR ON + LOG_OUTPUT_ON_FAILURE ON +) + +add_library(bzip2 INTERFACE) +add_dependencies(bzip2 bzip2_) +target_include_directories(bzip2 INTERFACE ${INSTALL_DIR}/include) +target_link_libraries(bzip2 INTERFACE ${INSTALL_DIR}/lib/libbz2.a) diff --git a/third_party/kenlm/CMakeLists.txt b/third_party/kenlm/CMakeLists.txt new file mode 100644 index 0000000000..00aacf27db --- /dev/null +++ b/third_party/kenlm/CMakeLists.txt @@ -0,0 +1,82 @@ +set( + KENLM_UTIL_SOURCES + submodule/util/bit_packing.cc + submodule/util/double-conversion/bignum.cc + submodule/util/double-conversion/bignum-dtoa.cc + submodule/util/double-conversion/cached-powers.cc + submodule/util/double-conversion/diy-fp.cc + submodule/util/double-conversion/double-conversion.cc + submodule/util/double-conversion/fast-dtoa.cc + submodule/util/double-conversion/fixed-dtoa.cc + submodule/util/double-conversion/strtod.cc + submodule/util/ersatz_progress.cc + submodule/util/exception.cc + submodule/util/file.cc + submodule/util/file_piece.cc + submodule/util/float_to_string.cc + submodule/util/integer_to_string.cc + submodule/util/mmap.cc + submodule/util/murmur_hash.cc + submodule/util/parallel_read.cc + submodule/util/pool.cc + submodule/util/read_compressed.cc + submodule/util/scoped.cc + submodule/util/spaces.cc + submodule/util/stream/chain.cc + submodule/util/stream/count_records.cc + submodule/util/stream/io.cc + submodule/util/stream/line_input.cc + submodule/util/stream/multi_progress.cc + submodule/util/stream/rewindable_stream.cc + submodule/util/string_piece.cc + submodule/util/usage.cc + ) + +set( + KENLM_SOURCES + submodule/lm/bhiksha.cc + submodule/lm/binary_format.cc + submodule/lm/config.cc + submodule/lm/lm_exception.cc + submodule/lm/model.cc + submodule/lm/quantize.cc + submodule/lm/read_arpa.cc + submodule/lm/search_hashed.cc + submodule/lm/search_trie.cc + submodule/lm/sizes.cc + submodule/lm/trie.cc + submodule/lm/trie_sort.cc + submodule/lm/value_build.cc + submodule/lm/virtual_interface.cc + submodule/lm/vocab.cc + ) + +add_library( + kenlm + STATIC + "${KENLM_UTIL_SOURCES};${KENLM_SOURCES}" + ) + +target_include_directories( + kenlm + BEFORE + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../install/include" + PUBLIC submodule + ) + +target_compile_definitions( + kenlm + PUBLIC KENLM_MAX_ORDER=6 + HAVE_ZLIB + HAVE_BZLIB + HAVE_XZLIB + ) + +target_link_libraries( + kenlm + zlib + bzip2 + lzma +) + +add_dependencies(kenlm boost) diff --git a/third_party/kenlm/submodule b/third_party/kenlm/submodule new file mode 160000 index 0000000000..5cea457db2 --- /dev/null +++ b/third_party/kenlm/submodule @@ -0,0 +1 @@ +Subproject commit 5cea457db26950a73d638425c183b368c06ed7c6 diff --git a/third_party/lzma/CMakeLists.txt b/third_party/lzma/CMakeLists.txt new file mode 100644 index 0000000000..ee89e67816 --- /dev/null +++ b/third_party/lzma/CMakeLists.txt @@ -0,0 +1,36 @@ +include(ExternalProject) + +set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) +set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) + +# To pass custom environment variables to ExternalProject_Add command, +# we need to do `${CMAKE_COMMAND} -E env ${envs} `. +# https://stackoverflow.com/a/62437353 +# We constrcut the custom environment variables here +set(envs + "PKG_CONFIG_PATH=${INSTALL_DIR}/lib/pkgconfig" + "LDFLAGS=-L${INSTALL_DIR}/lib $ENV{LDFLAGS}" + "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden $ENV{CFLAGS}" +) + +ExternalProject_Add(lzma_ + PREFIX ${CMAKE_CURRENT_BINARY_DIR} + DOWNLOAD_DIR ${ARCHIVE_DIR} + URL https://tukaani.org/xz/xz-5.2.5.tar.gz + URL_HASH SHA256=f6f4910fd033078738bd82bfba4f49219d03b17eb0794eb91efbae419f4aba10 + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/lzma_/configure --prefix=${INSTALL_DIR} --disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo --disable-lzma-links --disable-scripts --disable-doc --enable-static --disable-shared + DOWNLOAD_NO_PROGRESS ON + LOG_DOWNLOAD ON + LOG_UPDATE ON + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_INSTALL ON + LOG_MERGED_STDOUTERR ON + LOG_OUTPUT_ON_FAILURE ON +) + + +add_library(lzma INTERFACE) +add_dependencies(lzma lzma_) +target_include_directories(lzma INTERFACE ${INSTALL_DIR}/include) +target_link_libraries(lzma INTERFACE ${INSTALL_DIR}/lib/liblzma.a) diff --git a/third_party/zlib/CMakeLists.txt b/third_party/zlib/CMakeLists.txt new file mode 100644 index 0000000000..aebbec84e4 --- /dev/null +++ b/third_party/zlib/CMakeLists.txt @@ -0,0 +1,36 @@ +include(ExternalProject) + +set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) +set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) + +# To pass custom environment variables to ExternalProject_Add command, +# we need to do `${CMAKE_COMMAND} -E env ${envs} `. +# https://stackoverflow.com/a/62437353 +# We constrcut the custom environment variables here +set(envs + "PKG_CONFIG_PATH=${INSTALL_DIR}/lib/pkgconfig" + "LDFLAGS=-L${INSTALL_DIR}/lib $ENV{LDFLAGS}" + "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden $ENV{CFLAGS}" + "prefix=${INSTALL_DIR}" +) + +ExternalProject_Add(zlib_ + PREFIX ${CMAKE_CURRENT_BINARY_DIR} + DOWNLOAD_DIR ${ARCHIVE_DIR} + URL https://zlib.net/zlib-1.2.11.tar.gz + URL_HASH SHA256=c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/zlib_/configure --static + DOWNLOAD_NO_PROGRESS ON + LOG_DOWNLOAD ON + LOG_UPDATE ON + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_INSTALL ON + LOG_MERGED_STDOUTERR ON + LOG_OUTPUT_ON_FAILURE ON +) + +add_library(zlib INTERFACE) +add_dependencies(zlib zlib_) +target_include_directories(zlib INTERFACE ${INSTALL_DIR}/include) +target_link_libraries(zlib INTERFACE ${INSTALL_DIR}/lib/libz.a) diff --git a/tools/setup_helpers/extension.py b/tools/setup_helpers/extension.py index 739b28229a..fe4331cf93 100644 --- a/tools/setup_helpers/extension.py +++ b/tools/setup_helpers/extension.py @@ -37,6 +37,7 @@ def _get_build(var, default=False): _BUILD_SOX = False if platform.system() == 'Windows' else _get_build("BUILD_SOX", True) _BUILD_KALDI = False if platform.system() == 'Windows' else _get_build("BUILD_KALDI", True) _BUILD_RNNT = _get_build("BUILD_RNNT", True) +_BUILD_KENLM = False if platform.system() == 'Windows' else _get_build("BUILD_KENLM", True) _USE_ROCM = _get_build("USE_ROCM", torch.cuda.is_available() and torch.version.hip is not None) _USE_CUDA = _get_build("USE_CUDA", torch.cuda.is_available() and torch.version.hip is None) _USE_OPENMP = _get_build("USE_OPENMP", True) and \ @@ -89,6 +90,7 @@ def build_extension(self, ext): f"-DBUILD_SOX:BOOL={'ON' if _BUILD_SOX else 'OFF'}", f"-DBUILD_KALDI:BOOL={'ON' if _BUILD_KALDI else 'OFF'}", f"-DBUILD_RNNT:BOOL={'ON' if _BUILD_RNNT else 'OFF'}", + f"-DBUILD_KENLM:BOOL={'ON' if _BUILD_KENLM else 'OFF'}", "-DBUILD_TORCHAUDIO_PYTHON_EXTENSION:BOOL=ON", f"-DUSE_ROCM:BOOL={'ON' if _USE_ROCM else 'OFF'}", f"-DUSE_CUDA:BOOL={'ON' if _USE_CUDA else 'OFF'}", From 24baa243425f06a4223de958c0e86642a770a6c0 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Fri, 17 Dec 2021 11:01:56 -0800 Subject: [PATCH 0038/1144] Add C++ files for CTC decoder (#2075) Summary: part of https://github.com/pytorch/audio/issues/2072 -- splitting up the PR for easier review Add C++ files from [flashlight](https://github.com/flashlight/flashlight) that are needed for building CTC decoder w/ Lexicon and KenLM support Note: the code here will not be compiled until the build process is changed (future PR) Pull Request resolved: https://github.com/pytorch/audio/pull/2075 Reviewed By: mthrok Differential Revision: D33186825 Pulled By: carolineechen fbshipit-source-id: 5b69eea7634f3fae686471d988422942bb784cd9 --- torchaudio/csrc/decoder/src/decoder/Decoder.h | 79 +++++ .../decoder/src/decoder/LexiconDecoder.cpp | 328 ++++++++++++++++++ .../csrc/decoder/src/decoder/LexiconDecoder.h | 187 ++++++++++ torchaudio/csrc/decoder/src/decoder/Trie.cpp | 105 ++++++ torchaudio/csrc/decoder/src/decoder/Trie.h | 95 +++++ torchaudio/csrc/decoder/src/decoder/Utils.cpp | 15 + torchaudio/csrc/decoder/src/decoder/Utils.h | 275 +++++++++++++++ .../csrc/decoder/src/decoder/lm/KenLM.cpp | 74 ++++ .../csrc/decoder/src/decoder/lm/KenLM.h | 70 ++++ torchaudio/csrc/decoder/src/decoder/lm/LM.h | 90 +++++ .../csrc/decoder/src/dictionary/Defines.h | 21 ++ .../decoder/src/dictionary/Dictionary.cpp | 152 ++++++++ .../csrc/decoder/src/dictionary/Dictionary.h | 66 ++++ .../csrc/decoder/src/dictionary/String.cpp | 119 +++++++ .../csrc/decoder/src/dictionary/String.h | 132 +++++++ .../csrc/decoder/src/dictionary/System.cpp | 250 +++++++++++++ .../csrc/decoder/src/dictionary/System.h | 96 +++++ .../csrc/decoder/src/dictionary/Utils.cpp | 147 ++++++++ .../csrc/decoder/src/dictionary/Utils.h | 52 +++ 19 files changed, 2353 insertions(+) create mode 100644 torchaudio/csrc/decoder/src/decoder/Decoder.h create mode 100644 torchaudio/csrc/decoder/src/decoder/LexiconDecoder.cpp create mode 100644 torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h create mode 100644 torchaudio/csrc/decoder/src/decoder/Trie.cpp create mode 100644 torchaudio/csrc/decoder/src/decoder/Trie.h create mode 100644 torchaudio/csrc/decoder/src/decoder/Utils.cpp create mode 100644 torchaudio/csrc/decoder/src/decoder/Utils.h create mode 100644 torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp create mode 100644 torchaudio/csrc/decoder/src/decoder/lm/KenLM.h create mode 100644 torchaudio/csrc/decoder/src/decoder/lm/LM.h create mode 100644 torchaudio/csrc/decoder/src/dictionary/Defines.h create mode 100644 torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp create mode 100644 torchaudio/csrc/decoder/src/dictionary/Dictionary.h create mode 100644 torchaudio/csrc/decoder/src/dictionary/String.cpp create mode 100644 torchaudio/csrc/decoder/src/dictionary/String.h create mode 100644 torchaudio/csrc/decoder/src/dictionary/System.cpp create mode 100644 torchaudio/csrc/decoder/src/dictionary/System.h create mode 100644 torchaudio/csrc/decoder/src/dictionary/Utils.cpp create mode 100644 torchaudio/csrc/decoder/src/dictionary/Utils.h diff --git a/torchaudio/csrc/decoder/src/decoder/Decoder.h b/torchaudio/csrc/decoder/src/decoder/Decoder.h new file mode 100644 index 0000000000..a5dc18f571 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/Decoder.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include "torchaudio/csrc/decoder/src/decoder/Utils.h" + +namespace torchaudio { +namespace lib { +namespace text { + +enum class CriterionType { ASG = 0, CTC = 1, S2S = 2 }; + +/** + * Decoder support two typical use cases: + * Offline manner: + * decoder.decode(someData) [returns all hypothesis (transcription)] + * + * Online manner: + * decoder.decodeBegin() [called only at the beginning of the stream] + * while (stream) + * decoder.decodeStep(someData) [one or more calls] + * decoder.getBestHypothesis() [returns the best hypothesis (transcription)] + * decoder.prune() [prunes the hypothesis space] + * decoder.decodeEnd() [called only at the end of the stream] + * + * Note: function decoder.prune() deletes hypothesis up until time when called + * to supports online decoding. It will also add a offset to the scores in beam + * to avoid underflow/overflow. + * + */ +class Decoder { + public: + Decoder() = default; + virtual ~Decoder() = default; + + /* Initialize decoder before starting consume emissions */ + virtual void decodeBegin() {} + + /* Consume emissions in T x N chunks and increase the hypothesis space */ + virtual void decodeStep(const float* emissions, int T, int N) = 0; + + /* Finish up decoding after consuming all emissions */ + virtual void decodeEnd() {} + + /* Offline decode function, which consume all emissions at once */ + virtual std::vector decode( + const float* emissions, + int T, + int N) { + decodeBegin(); + decodeStep(emissions, T, N); + decodeEnd(); + return getAllFinalHypothesis(); + } + + /* Prune the hypothesis space */ + virtual void prune(int lookBack = 0) = 0; + + /* Get the number of decoded frame in buffer */ + virtual int nDecodedFramesInBuffer() const = 0; + + /* + * Get the best completed hypothesis which is `lookBack` frames ahead the last + * one in buffer. For lexicon requiredd LMs, completed hypothesis means no + * partial word appears at the end. + */ + virtual DecodeResult getBestHypothesis(int lookBack = 0) const = 0; + + /* Get all the final hypothesis */ + virtual std::vector getAllFinalHypothesis() const = 0; +}; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.cpp b/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.cpp new file mode 100644 index 0000000000..e92c5fa17a --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.cpp @@ -0,0 +1,328 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include +#include +#include +#include +#include +#include + +#include "torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h" + +namespace torchaudio { +namespace lib { +namespace text { + +void LexiconDecoder::decodeBegin() { + hyp_.clear(); + hyp_.emplace(0, std::vector()); + + /* note: the lm reset itself with :start() */ + hyp_[0].emplace_back( + 0.0, lm_->start(0), lexicon_->getRoot(), nullptr, sil_, -1); + nDecodedFrames_ = 0; + nPrunedFrames_ = 0; +} + +void LexiconDecoder::decodeStep(const float* emissions, int T, int N) { + int startFrame = nDecodedFrames_ - nPrunedFrames_; + // Extend hyp_ buffer + if (hyp_.size() < startFrame + T + 2) { + for (int i = hyp_.size(); i < startFrame + T + 2; i++) { + hyp_.emplace(i, std::vector()); + } + } + + std::vector idx(N); + for (int t = 0; t < T; t++) { + std::iota(idx.begin(), idx.end(), 0); + if (N > opt_.beamSizeToken) { + std::partial_sort( + idx.begin(), + idx.begin() + opt_.beamSizeToken, + idx.end(), + [&t, &N, &emissions](const size_t& l, const size_t& r) { + return emissions[t * N + l] > emissions[t * N + r]; + }); + } + + candidatesReset(candidatesBestScore_, candidates_, candidatePtrs_); + for (const LexiconDecoderState& prevHyp : hyp_[startFrame + t]) { + const TrieNode* prevLex = prevHyp.lex; + const int prevIdx = prevHyp.token; + const float lexMaxScore = + prevLex == lexicon_->getRoot() ? 0 : prevLex->maxScore; + + /* (1) Try children */ + for (int r = 0; r < std::min(opt_.beamSizeToken, N); ++r) { + int n = idx[r]; + auto iter = prevLex->children.find(n); + if (iter == prevLex->children.end()) { + continue; + } + const TrieNodePtr& lex = iter->second; + double amScore = emissions[t * N + n]; + if (nDecodedFrames_ + t > 0 && + opt_.criterionType == CriterionType::ASG) { + amScore += transitions_[n * N + prevIdx]; + } + double score = prevHyp.score + amScore; + if (n == sil_) { + score += opt_.silScore; + } + + LMStatePtr lmState; + double lmScore = 0.; + + if (isLmToken_) { + auto lmStateScorePair = lm_->score(prevHyp.lmState, n); + lmState = lmStateScorePair.first; + lmScore = lmStateScorePair.second; + } + + // We eat-up a new token + if (opt_.criterionType != CriterionType::CTC || prevHyp.prevBlank || + n != prevIdx) { + if (!lex->children.empty()) { + if (!isLmToken_) { + lmState = prevHyp.lmState; + lmScore = lex->maxScore - lexMaxScore; + } + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + score + opt_.lmWeight * lmScore, + lmState, + lex.get(), + &prevHyp, + n, + -1, + false, // prevBlank + prevHyp.amScore + amScore, + prevHyp.lmScore + lmScore); + } + } + + // If we got a true word + for (auto label : lex->labels) { + if (prevLex == lexicon_->getRoot() && prevHyp.token == n) { + // This is to avoid an situation that, when there is word with + // single token spelling (e.g. X -> x) in the lexicon and token `x` + // is predicted in several consecutive frames, multiple word `X` + // will be emitted. This violates the property of CTC, where + // there must be an blank token in between to predict 2 identical + // tokens consecutively. + continue; + } + + if (!isLmToken_) { + auto lmStateScorePair = lm_->score(prevHyp.lmState, label); + lmState = lmStateScorePair.first; + lmScore = lmStateScorePair.second - lexMaxScore; + } + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + score + opt_.lmWeight * lmScore + opt_.wordScore, + lmState, + lexicon_->getRoot(), + &prevHyp, + n, + label, + false, // prevBlank + prevHyp.amScore + amScore, + prevHyp.lmScore + lmScore); + } + + // If we got an unknown word + if (lex->labels.empty() && (opt_.unkScore > kNegativeInfinity)) { + if (!isLmToken_) { + auto lmStateScorePair = lm_->score(prevHyp.lmState, unk_); + lmState = lmStateScorePair.first; + lmScore = lmStateScorePair.second - lexMaxScore; + } + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + score + opt_.lmWeight * lmScore + opt_.unkScore, + lmState, + lexicon_->getRoot(), + &prevHyp, + n, + unk_, + false, // prevBlank + prevHyp.amScore + amScore, + prevHyp.lmScore + lmScore); + } + } + + /* (2) Try same lexicon node */ + if (opt_.criterionType != CriterionType::CTC || !prevHyp.prevBlank || + prevLex == lexicon_->getRoot()) { + int n = prevLex == lexicon_->getRoot() ? sil_ : prevIdx; + double amScore = emissions[t * N + n]; + if (nDecodedFrames_ + t > 0 && + opt_.criterionType == CriterionType::ASG) { + amScore += transitions_[n * N + prevIdx]; + } + double score = prevHyp.score + amScore; + if (n == sil_) { + score += opt_.silScore; + } + + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + score, + prevHyp.lmState, + prevLex, + &prevHyp, + n, + -1, + false, // prevBlank + prevHyp.amScore + amScore, + prevHyp.lmScore); + } + + /* (3) CTC only, try blank */ + if (opt_.criterionType == CriterionType::CTC) { + int n = blank_; + double amScore = emissions[t * N + n]; + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + prevHyp.score + amScore, + prevHyp.lmState, + prevLex, + &prevHyp, + n, + -1, + true, // prevBlank + prevHyp.amScore + amScore, + prevHyp.lmScore); + } + // finish proposing + } + + candidatesStore( + candidates_, + candidatePtrs_, + hyp_[startFrame + t + 1], + opt_.beamSize, + candidatesBestScore_ - opt_.beamThreshold, + opt_.logAdd, + false); + updateLMCache(lm_, hyp_[startFrame + t + 1]); + } + + nDecodedFrames_ += T; +} + +void LexiconDecoder::decodeEnd() { + candidatesReset(candidatesBestScore_, candidates_, candidatePtrs_); + bool hasNiceEnding = false; + for (const LexiconDecoderState& prevHyp : + hyp_[nDecodedFrames_ - nPrunedFrames_]) { + if (prevHyp.lex == lexicon_->getRoot()) { + hasNiceEnding = true; + break; + } + } + for (const LexiconDecoderState& prevHyp : + hyp_[nDecodedFrames_ - nPrunedFrames_]) { + const TrieNode* prevLex = prevHyp.lex; + const LMStatePtr& prevLmState = prevHyp.lmState; + + if (!hasNiceEnding || prevHyp.lex == lexicon_->getRoot()) { + auto lmStateScorePair = lm_->finish(prevLmState); + auto lmScore = lmStateScorePair.second; + candidatesAdd( + candidates_, + candidatesBestScore_, + opt_.beamThreshold, + prevHyp.score + opt_.lmWeight * lmScore, + lmStateScorePair.first, + prevLex, + &prevHyp, + sil_, + -1, + false, // prevBlank + prevHyp.amScore, + prevHyp.lmScore + lmScore); + } + } + + candidatesStore( + candidates_, + candidatePtrs_, + hyp_[nDecodedFrames_ - nPrunedFrames_ + 1], + opt_.beamSize, + candidatesBestScore_ - opt_.beamThreshold, + opt_.logAdd, + true); + ++nDecodedFrames_; +} + +std::vector LexiconDecoder::getAllFinalHypothesis() const { + int finalFrame = nDecodedFrames_ - nPrunedFrames_; + if (finalFrame < 1) { + return std::vector{}; + } + + return getAllHypothesis(hyp_.find(finalFrame)->second, finalFrame); +} + +DecodeResult LexiconDecoder::getBestHypothesis(int lookBack) const { + if (nDecodedFrames_ - nPrunedFrames_ - lookBack < 1) { + return DecodeResult(); + } + + const LexiconDecoderState* bestNode = findBestAncestor( + hyp_.find(nDecodedFrames_ - nPrunedFrames_)->second, lookBack); + return getHypothesis(bestNode, nDecodedFrames_ - nPrunedFrames_ - lookBack); +} + +int LexiconDecoder::nHypothesis() const { + int finalFrame = nDecodedFrames_ - nPrunedFrames_; + return hyp_.find(finalFrame)->second.size(); +} + +int LexiconDecoder::nDecodedFramesInBuffer() const { + return nDecodedFrames_ - nPrunedFrames_ + 1; +} + +void LexiconDecoder::prune(int lookBack) { + if (nDecodedFrames_ - nPrunedFrames_ - lookBack < 1) { + return; // Not enough decoded frames to prune + } + + /* (1) Find the last emitted word in the best path */ + const LexiconDecoderState* bestNode = findBestAncestor( + hyp_.find(nDecodedFrames_ - nPrunedFrames_)->second, lookBack); + if (!bestNode) { + return; // Not enough decoded frames to prune + } + + int startFrame = nDecodedFrames_ - nPrunedFrames_ - lookBack; + if (startFrame < 1) { + return; // Not enough decoded frames to prune + } + + /* (2) Move things from back of hyp_ to front and normalize scores */ + pruneAndNormalize(hyp_, startFrame, lookBack); + + nPrunedFrames_ = nDecodedFrames_ - lookBack; +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h b/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h new file mode 100644 index 0000000000..70bb6bfcfa --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include + +#include "torchaudio/csrc/decoder/src/decoder/Decoder.h" +#include "torchaudio/csrc/decoder/src/decoder/Trie.h" +#include "torchaudio/csrc/decoder/src/decoder/lm/LM.h" + +namespace torchaudio { +namespace lib { +namespace text { + +struct LexiconDecoderOptions { + int beamSize; // Maximum number of hypothesis we hold after each step + int beamSizeToken; // Maximum number of tokens we consider at each step + double beamThreshold; // Threshold to prune hypothesis + double lmWeight; // Weight of lm + double wordScore; // Word insertion score + double unkScore; // Unknown word insertion score + double silScore; // Silence insertion score + bool logAdd; // If or not use logadd when merging hypothesis + CriterionType criterionType; // CTC or ASG +}; + +/** + * LexiconDecoderState stores information for each hypothesis in the beam. + */ +struct LexiconDecoderState { + double score; // Accumulated total score so far + LMStatePtr lmState; // Language model state + const TrieNode* lex; // Trie node in the lexicon + const LexiconDecoderState* parent; // Parent hypothesis + int token; // Label of token + int word; // Label of word (-1 if incomplete) + bool prevBlank; // If previous hypothesis is blank (for CTC only) + + double amScore; // Accumulated AM score so far + double lmScore; // Accumulated LM score so far + + LexiconDecoderState( + const double score, + const LMStatePtr& lmState, + const TrieNode* lex, + const LexiconDecoderState* parent, + const int token, + const int word, + const bool prevBlank = false, + const double amScore = 0, + const double lmScore = 0) + : score(score), + lmState(lmState), + lex(lex), + parent(parent), + token(token), + word(word), + prevBlank(prevBlank), + amScore(amScore), + lmScore(lmScore) {} + + LexiconDecoderState() + : score(0.), + lmState(nullptr), + lex(nullptr), + parent(nullptr), + token(-1), + word(-1), + prevBlank(false), + amScore(0.), + lmScore(0.) {} + + int compareNoScoreStates(const LexiconDecoderState* node) const { + int lmCmp = lmState->compare(node->lmState); + if (lmCmp != 0) { + return lmCmp > 0 ? 1 : -1; + } else if (lex != node->lex) { + return lex > node->lex ? 1 : -1; + } else if (token != node->token) { + return token > node->token ? 1 : -1; + } else if (prevBlank != node->prevBlank) { + return prevBlank > node->prevBlank ? 1 : -1; + } + return 0; + } + + int getWord() const { + return word; + } + + bool isComplete() const { + return !parent || parent->word >= 0; + } +}; + +/** + * Decoder implements a beam seach decoder that finds the word transcription + * W maximizing: + * + * AM(W) + lmWeight_ * log(P_{lm}(W)) + wordScore_ * |W_known| + unkScore_ * + * |W_unknown| + silScore_ * |{i| pi_i = }| + * + * where P_{lm}(W) is the language model score, pi_i is the value for the i-th + * frame in the path leading to W and AM(W) is the (unnormalized) acoustic model + * score of the transcription W. Note that the lexicon is used to limit the + * search space and all candidate words are generated from it if unkScore is + * -inf, otherwise will be generated for OOVs. + */ +class LexiconDecoder : public Decoder { + public: + LexiconDecoder( + LexiconDecoderOptions opt, + const TriePtr& lexicon, + const LMPtr& lm, + const int sil, + const int blank, + const int unk, + const std::vector& transitions, + const bool isLmToken) + : opt_(std::move(opt)), + lexicon_(lexicon), + lm_(lm), + sil_(sil), + blank_(blank), + unk_(unk), + transitions_(transitions), + isLmToken_(isLmToken) {} + + void decodeBegin() override; + + void decodeStep(const float* emissions, int T, int N) override; + + void decodeEnd() override; + + int nHypothesis() const; + + void prune(int lookBack = 0) override; + + int nDecodedFramesInBuffer() const override; + + DecodeResult getBestHypothesis(int lookBack = 0) const override; + + std::vector getAllFinalHypothesis() const override; + + protected: + LexiconDecoderOptions opt_; + // Lexicon trie to restrict beam-search decoder + TriePtr lexicon_; + LMPtr lm_; + // Index of silence label + int sil_; + // Index of blank label (for CTC) + int blank_; + // Index of unknown word + int unk_; + // matrix of transitions (for ASG criterion) + std::vector transitions_; + // if LM is token-level (operates on the same level as acoustic model) + // or it is word-level (in case of false) + bool isLmToken_; + + // All the hypothesis new candidates (can be larger than beamsize) proposed + // based on the ones from previous frame + std::vector candidates_; + + // This vector is designed for efficient sorting and merging the candidates_, + // so instead of moving around objects, we only need to sort pointers + std::vector candidatePtrs_; + + // Best candidate score of current frame + double candidatesBestScore_; + + // Vector of hypothesis for all the frames so far + std::unordered_map> hyp_; + + // These 2 variables are used for online decoding, for hypothesis pruning + int nDecodedFrames_; // Total number of decoded frames. + int nPrunedFrames_; // Total number of pruned frames from hyp_. +}; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/Trie.cpp b/torchaudio/csrc/decoder/src/decoder/Trie.cpp new file mode 100644 index 0000000000..bf1a300eab --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/Trie.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include +#include +#include +#include + +#include "torchaudio/csrc/decoder/src/decoder/Trie.h" + +namespace torchaudio { +namespace lib { +namespace text { + +const double kMinusLogThreshold = -39.14; + +const TrieNode* Trie::getRoot() const { + return root_.get(); +} + +TrieNodePtr Trie::insert( + const std::vector& indices, + int label, + float score) { + TrieNodePtr node = root_; + for (int i = 0; i < indices.size(); i++) { + int idx = indices[i]; + if (idx < 0 || idx >= maxChildren_) { + throw std::out_of_range( + "[Trie] Invalid letter index: " + std::to_string(idx)); + } + if (node->children.find(idx) == node->children.end()) { + node->children[idx] = std::make_shared(idx); + } + node = node->children[idx]; + } + if (node->labels.size() < kTrieMaxLabel) { + node->labels.push_back(label); + node->scores.push_back(score); + } else { + std::cerr << "[Trie] Trie label number reached limit: " << kTrieMaxLabel + << "\n"; + } + return node; +} + +TrieNodePtr Trie::search(const std::vector& indices) { + TrieNodePtr node = root_; + for (auto idx : indices) { + if (idx < 0 || idx >= maxChildren_) { + throw std::out_of_range( + "[Trie] Invalid letter index: " + std::to_string(idx)); + } + if (node->children.find(idx) == node->children.end()) { + return nullptr; + } + node = node->children[idx]; + } + return node; +} + +/* logadd */ +double TrieLogAdd(double log_a, double log_b) { + double minusdif; + if (log_a < log_b) { + std::swap(log_a, log_b); + } + minusdif = log_b - log_a; + if (minusdif < kMinusLogThreshold) { + return log_a; + } else { + return log_a + log1p(exp(minusdif)); + } +} + +void smearNode(TrieNodePtr node, SmearingMode smearMode) { + node->maxScore = -std::numeric_limits::infinity(); + for (auto score : node->scores) { + node->maxScore = TrieLogAdd(node->maxScore, score); + } + for (auto child : node->children) { + auto childNode = child.second; + smearNode(childNode, smearMode); + if (smearMode == SmearingMode::LOGADD) { + node->maxScore = TrieLogAdd(node->maxScore, childNode->maxScore); + } else if ( + smearMode == SmearingMode::MAX && + childNode->maxScore > node->maxScore) { + node->maxScore = childNode->maxScore; + } + } +} + +void Trie::smear(SmearingMode smearMode) { + if (smearMode != SmearingMode::NONE) { + smearNode(root_, smearMode); + } +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/Trie.h b/torchaudio/csrc/decoder/src/decoder/Trie.h new file mode 100644 index 0000000000..50cfafc1d1 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/Trie.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once +#include +#include +#include + +namespace torchaudio { +namespace lib { +namespace text { + +constexpr int kTrieMaxLabel = 6; + +enum class SmearingMode { + NONE = 0, + MAX = 1, + LOGADD = 2, +}; + +/** + * TrieNode is the trie node structure in Trie. + */ +struct TrieNode { + explicit TrieNode(int idx) + : children(std::unordered_map>()), + idx(idx), + maxScore(0) { + labels.reserve(kTrieMaxLabel); + scores.reserve(kTrieMaxLabel); + } + + // Pointers to the children of a node + std::unordered_map> children; + + // Node index + int idx; + + // Labels of words that are constructed from the given path. Note that + // `labels` is nonempty only if the current node represents a completed token. + std::vector labels; + + // Scores (`scores` should have the same size as `labels`) + std::vector scores; + + // Maximum score of all the labels if this node is a leaf, + // otherwise it will be the value after trie smearing. + float maxScore; +}; + +using TrieNodePtr = std::shared_ptr; + +/** + * Trie is used to store the lexicon in langiage model. We use it to limit + * the search space in deocder and quickly look up scores for a given token + * (completed word) or make prediction for incompleted ones based on smearing. + */ +class Trie { + public: + Trie(int maxChildren, int rootIdx) + : root_(std::make_shared(rootIdx)), maxChildren_(maxChildren) {} + + /* Return the root node pointer */ + const TrieNode* getRoot() const; + + /* Insert a token into trie with label */ + TrieNodePtr insert(const std::vector& indices, int label, float score); + + /* Get the labels for a given token */ + TrieNodePtr search(const std::vector& indices); + + /** + * Smearing the trie using the valid labels inserted in the trie so as to get + * score on each node (incompleted token). + * For example, if smear_mode is MAX, then for node "a" in path "c"->"a", we + * will select the maximum score from all its children like "c"->"a"->"t", + * "c"->"a"->"n", "c"->"a"->"r"->"e" and so on. + * This process will be carry out recusively on all the nodes. + */ + void smear(const SmearingMode smear_mode); + + private: + TrieNodePtr root_; + int maxChildren_; // The maximum number of childern for each node. It is + // usually the size of letters or phonmes. +}; + +using TriePtr = std::shared_ptr; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/Utils.cpp b/torchaudio/csrc/decoder/src/decoder/Utils.cpp new file mode 100644 index 0000000000..810df2706e --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/Utils.cpp @@ -0,0 +1,15 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +namespace torchaudio { +namespace lib { +namespace text { + +// Place holder +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/Utils.h b/torchaudio/csrc/decoder/src/decoder/Utils.h new file mode 100644 index 0000000000..8aac3a9eb1 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/Utils.h @@ -0,0 +1,275 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include +#include + +#include "torchaudio/csrc/decoder/src/decoder/lm/LM.h" + +namespace torchaudio { +namespace lib { +namespace text { + +/* ===================== Definitions ===================== */ + +const double kNegativeInfinity = -std::numeric_limits::infinity(); +const int kLookBackLimit = 100; + +struct DecodeResult { + double score; + double amScore; + double lmScore; + std::vector words; + std::vector tokens; + + explicit DecodeResult(int length = 0) + : score(0), words(length, -1), tokens(length, -1) {} +}; + +/* ===================== Candidate-related operations ===================== */ + +template +void candidatesReset( + double& candidatesBestScore, + std::vector& candidates, + std::vector& candidatePtrs) { + candidatesBestScore = kNegativeInfinity; + candidates.clear(); + candidatePtrs.clear(); +} + +template +void candidatesAdd( + std::vector& candidates, + double& candidatesBestScore, + const double beamThreshold, + const double score, + const Args&... args) { + if (score >= candidatesBestScore) { + candidatesBestScore = score; + } + if (score >= candidatesBestScore - beamThreshold) { + candidates.emplace_back(score, args...); + } +} + +template +void candidatesStore( + std::vector& candidates, + std::vector& candidatePtrs, + std::vector& outputs, + const int beamSize, + const double threshold, + const bool logAdd, + const bool returnSorted) { + outputs.clear(); + if (candidates.empty()) { + return; + } + + /* 1. Select valid candidates */ + for (auto& candidate : candidates) { + if (candidate.score >= threshold) { + candidatePtrs.emplace_back(&candidate); + } + } + + /* 2. Merge candidates */ + std::sort( + candidatePtrs.begin(), + candidatePtrs.end(), + [](const DecoderState* node1, const DecoderState* node2) { + int cmp = node1->compareNoScoreStates(node2); + return cmp == 0 ? node1->score > node2->score : cmp > 0; + }); + + int nHypAfterMerging = 1; + for (int i = 1; i < candidatePtrs.size(); i++) { + if (candidatePtrs[i]->compareNoScoreStates( + candidatePtrs[nHypAfterMerging - 1]) != 0) { + // Distinct candidate + candidatePtrs[nHypAfterMerging] = candidatePtrs[i]; + nHypAfterMerging++; + } else { + // Same candidate + double maxScore = std::max( + candidatePtrs[nHypAfterMerging - 1]->score, candidatePtrs[i]->score); + if (logAdd) { + double minScore = std::min( + candidatePtrs[nHypAfterMerging - 1]->score, + candidatePtrs[i]->score); + candidatePtrs[nHypAfterMerging - 1]->score = + maxScore + std::log1p(std::exp(minScore - maxScore)); + } else { + candidatePtrs[nHypAfterMerging - 1]->score = maxScore; + } + } + } + candidatePtrs.resize(nHypAfterMerging); + + /* 3. Sort and prune */ + auto compareNodeScore = [](const DecoderState* node1, + const DecoderState* node2) { + return node1->score > node2->score; + }; + + int nValidHyp = candidatePtrs.size(); + int finalSize = std::min(nValidHyp, beamSize); + if (!returnSorted && nValidHyp > beamSize) { + std::nth_element( + candidatePtrs.begin(), + candidatePtrs.begin() + finalSize, + candidatePtrs.begin() + nValidHyp, + compareNodeScore); + } else if (returnSorted) { + std::partial_sort( + candidatePtrs.begin(), + candidatePtrs.begin() + finalSize, + candidatePtrs.begin() + nValidHyp, + compareNodeScore); + } + + for (int i = 0; i < finalSize; i++) { + outputs.emplace_back(std::move(*candidatePtrs[i])); + } +} + +/* ===================== Result-related operations ===================== */ + +template +DecodeResult getHypothesis(const DecoderState* node, const int finalFrame) { + const DecoderState* node_ = node; + if (!node_) { + return DecodeResult(); + } + + DecodeResult res(finalFrame + 1); + res.score = node_->score; + res.amScore = node_->amScore; + res.lmScore = node_->lmScore; + + int i = 0; + while (node_) { + res.words[finalFrame - i] = node_->getWord(); + res.tokens[finalFrame - i] = node_->token; + node_ = node_->parent; + i++; + } + + return res; +} + +template +std::vector getAllHypothesis( + const std::vector& finalHyps, + const int finalFrame) { + int nHyp = finalHyps.size(); + + std::vector res(nHyp); + + for (int r = 0; r < nHyp; r++) { + const DecoderState* node = &finalHyps[r]; + res[r] = getHypothesis(node, finalFrame); + } + + return res; +} + +template +const DecoderState* findBestAncestor( + const std::vector& finalHyps, + int& lookBack) { + int nHyp = finalHyps.size(); + if (nHyp == 0) { + return nullptr; + } + + double bestScore = finalHyps.front().score; + const DecoderState* bestNode = finalHyps.data(); + for (int r = 1; r < nHyp; r++) { + const DecoderState* node = &finalHyps[r]; + if (node->score > bestScore) { + bestScore = node->score; + bestNode = node; + } + } + + int n = 0; + while (bestNode && n < lookBack) { + n++; + bestNode = bestNode->parent; + } + + const int maxLookBack = lookBack + kLookBackLimit; + while (bestNode) { + // Check for first emitted word. + if (bestNode->isComplete()) { + break; + } + + n++; + bestNode = bestNode->parent; + + if (n == maxLookBack) { + break; + } + } + + lookBack = n; + return bestNode; +} + +template +void pruneAndNormalize( + std::unordered_map>& hypothesis, + const int startFrame, + const int lookBack) { + /* 1. Move things from back of hypothesis to front. */ + for (int i = 0; i < hypothesis.size(); i++) { + if (i <= lookBack) { + hypothesis[i].swap(hypothesis[i + startFrame]); + } else { + hypothesis[i].clear(); + } + } + + /* 2. Avoid further back-tracking */ + for (DecoderState& hyp : hypothesis[0]) { + hyp.parent = nullptr; + } + + /* 3. Avoid score underflow/overflow. */ + double largestScore = hypothesis[lookBack].front().score; + for (int i = 1; i < hypothesis[lookBack].size(); i++) { + if (largestScore < hypothesis[lookBack][i].score) { + largestScore = hypothesis[lookBack][i].score; + } + } + + for (int i = 0; i < hypothesis[lookBack].size(); i++) { + hypothesis[lookBack][i].score -= largestScore; + } +} + +/* ===================== LM-related operations ===================== */ + +template +void updateLMCache(const LMPtr& lm, std::vector& hypothesis) { + // For ConvLM update cache + std::vector states; + for (const auto& hyp : hypothesis) { + states.emplace_back(hyp.lmState); + } + lm->updateCache(states); +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp new file mode 100644 index 0000000000..6d7bc52834 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include "torchaudio/csrc/decoder/src/decoder/lm/KenLM.h" + +#include + +#include "kenlm/lm/model.hh" + +namespace torchaudio { +namespace lib { +namespace text { + +KenLMState::KenLMState() : ken_(std::make_unique()) {} + +KenLM::KenLM(const std::string& path, const Dictionary& usrTknDict) { + // Load LM + model_.reset(lm::ngram::LoadVirtual(path.c_str())); + if (!model_) { + throw std::runtime_error("[KenLM] LM loading failed."); + } + vocab_ = &model_->BaseVocabulary(); + if (!vocab_) { + throw std::runtime_error("[KenLM] LM vocabulary loading failed."); + } + + // Create index map + usrToLmIdxMap_.resize(usrTknDict.indexSize()); + for (int i = 0; i < usrTknDict.indexSize(); i++) { + auto token = usrTknDict.getEntry(i); + int lmIdx = vocab_->Index(token.c_str()); + usrToLmIdxMap_[i] = lmIdx; + } +} + +LMStatePtr KenLM::start(bool startWithNothing) { + auto outState = std::make_shared(); + if (startWithNothing) { + model_->NullContextWrite(outState->ken()); + } else { + model_->BeginSentenceWrite(outState->ken()); + } + + return outState; +} + +std::pair KenLM::score( + const LMStatePtr& state, + const int usrTokenIdx) { + if (usrTokenIdx < 0 || usrTokenIdx >= usrToLmIdxMap_.size()) { + throw std::runtime_error( + "[KenLM] Invalid user token index: " + std::to_string(usrTokenIdx)); + } + auto inState = std::static_pointer_cast(state); + auto outState = inState->child(usrTokenIdx); + float score = model_->BaseScore( + inState->ken(), usrToLmIdxMap_[usrTokenIdx], outState->ken()); + return std::make_pair(std::move(outState), score); +} + +std::pair KenLM::finish(const LMStatePtr& state) { + auto inState = std::static_pointer_cast(state); + auto outState = inState->child(-1); + float score = + model_->BaseScore(inState->ken(), vocab_->EndSentence(), outState->ken()); + return std::make_pair(std::move(outState), score); +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.h b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.h new file mode 100644 index 0000000000..45566aef12 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include + +#include "torchaudio/csrc/decoder/src/decoder/lm/LM.h" +#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" + +// Forward declarations to avoid including KenLM headers +namespace lm { +namespace base { + +struct Vocabulary; +struct Model; + +} // namespace base +namespace ngram { + +struct State; + +} // namespace ngram +} // namespace lm + +namespace torchaudio { +namespace lib { +namespace text { + +/** + * KenLMState is a state object from KenLM, which contains context length, + * indicies and compare functions + * https://github.com/kpu/kenlm/blob/master/lm/state.hh. + */ +struct KenLMState : LMState { + KenLMState(); + std::unique_ptr ken_; + lm::ngram::State* ken() { + return ken_.get(); + } +}; + +/** + * KenLM extends LM by using the toolkit https://kheafield.com/code/kenlm/. + */ +class KenLM : public LM { + public: + KenLM(const std::string& path, const Dictionary& usrTknDict); + + LMStatePtr start(bool startWithNothing) override; + + std::pair score( + const LMStatePtr& state, + const int usrTokenIdx) override; + + std::pair finish(const LMStatePtr& state) override; + + private: + std::shared_ptr model_; + const lm::base::Vocabulary* vocab_; +}; + +using KenLMPtr = std::shared_ptr; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/decoder/lm/LM.h b/torchaudio/csrc/decoder/src/decoder/lm/LM.h new file mode 100644 index 0000000000..32af9d1201 --- /dev/null +++ b/torchaudio/csrc/decoder/src/decoder/lm/LM.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace torchaudio { +namespace lib { +namespace text { + +struct LMState { + std::unordered_map> children; + + template + std::shared_ptr child(int usrIdx) { + auto s = children.find(usrIdx); + if (s == children.end()) { + auto state = std::make_shared(); + children[usrIdx] = state; + return state; + } else { + return std::static_pointer_cast(s->second); + } + } + + /* Compare two language model states. */ + int compare(const std::shared_ptr& state) const { + LMState* inState = state.get(); + if (!state) { + throw std::runtime_error("a state is null"); + } + if (this == inState) { + return 0; + } else if (this < inState) { + return -1; + } else { + return 1; + } + }; +}; + +/** + * LMStatePtr is a shared LMState* tracking LM states generated during decoding. + */ +using LMStatePtr = std::shared_ptr; + +/** + * LM is a thin wrapper for laguage models. We abstrct several common methods + * here which can be shared for KenLM, ConvLM, RNNLM, etc. + */ +class LM { + public: + /* Initialize or reset language model */ + virtual LMStatePtr start(bool startWithNothing) = 0; + + /** + * Query the language model given input language model state and a specific + * token, return a new language model state and score. + */ + virtual std::pair score( + const LMStatePtr& state, + const int usrTokenIdx) = 0; + + /* Query the language model and finish decoding. */ + virtual std::pair finish(const LMStatePtr& state) = 0; + + /* Update LM caches (optional) given a bunch of new states generated */ + virtual void updateCache(std::vector stateIdices) {} + + virtual ~LM() = default; + + protected: + /* Map indices from acoustic model to LM for each valid token. */ + std::vector usrToLmIdxMap_; +}; + +using LMPtr = std::shared_ptr; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/Defines.h b/torchaudio/csrc/decoder/src/dictionary/Defines.h new file mode 100644 index 0000000000..de9cbcad42 --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/Defines.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +namespace torchaudio { +namespace lib { +namespace text { + +constexpr const char* kUnkToken = ""; +constexpr const char* kEosToken = ""; +constexpr const char* kPadToken = ""; +constexpr const char* kMaskToken = ""; + +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp b/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp new file mode 100644 index 0000000000..fe533cb2c4 --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include +#include + +#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" +#include "torchaudio/csrc/decoder/src/dictionary/String.h" +#include "torchaudio/csrc/decoder/src/dictionary/System.h" +#include "torchaudio/csrc/decoder/src/dictionary/Utils.h" + +namespace torchaudio { +namespace lib { +namespace text { + +Dictionary::Dictionary(std::istream& stream) { + createFromStream(stream); +} + +Dictionary::Dictionary(const std::string& filename) { + std::ifstream stream = createInputStream(filename); + createFromStream(stream); +} + +void Dictionary::createFromStream(std::istream& stream) { + if (!stream) { + throw std::runtime_error("Unable to open dictionary input stream."); + } + std::string line; + while (std::getline(stream, line)) { + if (line.empty()) { + continue; + } + auto tkns = splitOnWhitespace(line, true); + auto idx = idx2entry_.size(); + // All entries on the same line map to the same index + for (const auto& tkn : tkns) { + addEntry(tkn, idx); + } + } + if (!isContiguous()) { + throw std::runtime_error("Invalid dictionary format - not contiguous"); + } +} + +void Dictionary::addEntry(const std::string& entry, int idx) { + if (entry2idx_.find(entry) != entry2idx_.end()) { + throw std::invalid_argument( + "Duplicate entry name in dictionary '" + entry + "'"); + } + entry2idx_[entry] = idx; + if (idx2entry_.find(idx) == idx2entry_.end()) { + idx2entry_[idx] = entry; + } +} + +void Dictionary::addEntry(const std::string& entry) { + // Check if the entry already exists in the dictionary + if (entry2idx_.find(entry) != entry2idx_.end()) { + throw std::invalid_argument( + "Duplicate entry in dictionary '" + entry + "'"); + } + int idx = idx2entry_.size(); + // Find first available index. + while (idx2entry_.find(idx) != idx2entry_.end()) { + ++idx; + } + addEntry(entry, idx); +} + +std::string Dictionary::getEntry(int idx) const { + auto iter = idx2entry_.find(idx); + if (iter == idx2entry_.end()) { + throw std::invalid_argument( + "Unknown index in dictionary '" + std::to_string(idx) + "'"); + } + return iter->second; +} + +void Dictionary::setDefaultIndex(int idx) { + defaultIndex_ = idx; +} + +int Dictionary::getIndex(const std::string& entry) const { + auto iter = entry2idx_.find(entry); + if (iter == entry2idx_.end()) { + if (defaultIndex_ < 0) { + throw std::invalid_argument( + "Unknown entry in dictionary: '" + entry + "'"); + } else { + return defaultIndex_; + } + } + return iter->second; +} + +bool Dictionary::contains(const std::string& entry) const { + auto iter = entry2idx_.find(entry); + if (iter == entry2idx_.end()) { + return false; + } + return true; +} + +size_t Dictionary::entrySize() const { + return entry2idx_.size(); +} + +bool Dictionary::isContiguous() const { + for (size_t i = 0; i < indexSize(); ++i) { + if (idx2entry_.find(i) == idx2entry_.end()) { + return false; + } + } + for (const auto& tknidx : entry2idx_) { + if (idx2entry_.find(tknidx.second) == idx2entry_.end()) { + return false; + } + } + return true; +} + +std::vector Dictionary::mapEntriesToIndices( + const std::vector& entries) const { + std::vector indices; + indices.reserve(entries.size()); + for (const auto& tkn : entries) { + indices.emplace_back(getIndex(tkn)); + } + return indices; +} + +std::vector Dictionary::mapIndicesToEntries( + const std::vector& indices) const { + std::vector entries; + entries.reserve(indices.size()); + for (const auto& idx : indices) { + entries.emplace_back(getEntry(idx)); + } + return entries; +} + +size_t Dictionary::indexSize() const { + return idx2entry_.size(); +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/Dictionary.h b/torchaudio/csrc/decoder/src/dictionary/Dictionary.h new file mode 100644 index 0000000000..d473694725 --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/Dictionary.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include +#include + +namespace torchaudio { +namespace lib { +namespace text { +// A simple dictionary class which holds a bidirectional map +// entry (strings) <--> integer indices. Not thread-safe ! +class Dictionary { + public: + // Creates an empty dictionary + Dictionary() {} + + explicit Dictionary(std::istream& stream); + + explicit Dictionary(const std::string& filename); + + size_t entrySize() const; + + size_t indexSize() const; + + void addEntry(const std::string& entry, int idx); + + void addEntry(const std::string& entry); + + std::string getEntry(int idx) const; + + void setDefaultIndex(int idx); + + int getIndex(const std::string& entry) const; + + bool contains(const std::string& entry) const; + + // checks if all the indices are contiguous + bool isContiguous() const; + + std::vector mapEntriesToIndices( + const std::vector& entries) const; + + std::vector mapIndicesToEntries( + const std::vector& indices) const; + + private: + // Creates a dictionary from an input stream + void createFromStream(std::istream& stream); + + std::unordered_map entry2idx_; + std::unordered_map idx2entry_; + int defaultIndex_ = -1; +}; + +typedef std::unordered_map DictionaryMap; +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/String.cpp b/torchaudio/csrc/decoder/src/dictionary/String.cpp new file mode 100644 index 0000000000..3aa33183ae --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/String.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include "torchaudio/csrc/decoder/src/dictionary/String.h" + +#include + +#include +#include +#include +#include + +static constexpr const char* kSpaceChars = "\t\n\v\f\r "; + +namespace torchaudio { +namespace lib { + +std::string trim(const std::string& str) { + auto i = str.find_first_not_of(kSpaceChars); + if (i == std::string::npos) { + return ""; + } + auto j = str.find_last_not_of(kSpaceChars); + if (j == std::string::npos || i > j) { + return ""; + } + return str.substr(i, j - i + 1); +} + +void replaceAll( + std::string& str, + const std::string& from, + const std::string& repl) { + if (from.empty()) { + return; + } + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), repl); + pos += repl.length(); + } +} + +bool startsWith(const std::string& input, const std::string& pattern) { + return (input.find(pattern) == 0); +} + +bool endsWith(const std::string& input, const std::string& pattern) { + if (pattern.size() > input.size()) { + return false; + } + return std::equal(pattern.rbegin(), pattern.rend(), input.rbegin()); +} + +template +static std::vector splitImpl( + const Delim& delim, + std::string::size_type delimSize, + const std::string& input, + bool ignoreEmpty = false) { + std::vector result; + std::string::size_type i = 0; + while (true) { + auto j = Any ? input.find_first_of(delim, i) : input.find(delim, i); + if (j == std::string::npos) { + break; + } + if (!(ignoreEmpty && i == j)) { + result.emplace_back(input.begin() + i, input.begin() + j); + } + i = j + delimSize; + } + if (!(ignoreEmpty && i == input.size())) { + result.emplace_back(input.begin() + i, input.end()); + } + return result; +} + +std::vector split( + char delim, + const std::string& input, + bool ignoreEmpty) { + return splitImpl(delim, 1, input, ignoreEmpty); +} + +std::vector split( + const std::string& delim, + const std::string& input, + bool ignoreEmpty) { + if (delim.empty()) { + throw std::invalid_argument("delimiter is empty string"); + } + return splitImpl(delim, delim.size(), input, ignoreEmpty); +} + +std::vector splitOnAnyOf( + const std::string& delim, + const std::string& input, + bool ignoreEmpty) { + return splitImpl(delim, 1, input, ignoreEmpty); +} + +std::vector splitOnWhitespace( + const std::string& input, + bool ignoreEmpty) { + return splitOnAnyOf(kSpaceChars, input, ignoreEmpty); +} + +std::string join( + const std::string& delim, + const std::vector& vec) { + return join(delim, vec.begin(), vec.end()); +} +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/String.h b/torchaudio/csrc/decoder/src/dictionary/String.h new file mode 100644 index 0000000000..27cae1c1ee --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/String.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace torchaudio { +namespace lib { + +// ============================ Types and Templates ============================ + +template +using DecayDereference = + typename std::decay())>::type; + +template +using EnableIfSame = typename std::enable_if::value>::type; + +// ================================== Functions +// ================================== + +std::string trim(const std::string& str); + +void replaceAll( + std::string& str, + const std::string& from, + const std::string& repl); + +bool startsWith(const std::string& input, const std::string& pattern); +bool endsWith(const std::string& input, const std::string& pattern); + +std::vector split( + char delim, + const std::string& input, + bool ignoreEmpty = false); + +std::vector split( + const std::string& delim, + const std::string& input, + bool ignoreEmpty = false); + +std::vector splitOnAnyOf( + const std::string& delim, + const std::string& input, + bool ignoreEmpty = false); + +std::vector splitOnWhitespace( + const std::string& input, + bool ignoreEmpty = false); + +/** + * Join a vector of `std::string` inserting `delim` in between. + */ +std::string join(const std::string& delim, const std::vector& vec); + +/** + * Join a range of `std::string` specified by iterators. + */ +template < + typename FwdIt, + typename = EnableIfSame, std::string>> +std::string join(const std::string& delim, FwdIt begin, FwdIt end) { + if (begin == end) { + return ""; + } + + size_t totalSize = begin->size(); + for (auto it = std::next(begin); it != end; ++it) { + totalSize += delim.size() + it->size(); + } + + std::string result; + result.reserve(totalSize); + + result.append(*begin); + for (auto it = std::next(begin); it != end; ++it) { + result.append(delim); + result.append(*it); + } + return result; +} + +/** + * Create an output string using a `printf`-style format string and arguments. + * Safer than `sprintf` which is vulnerable to buffer overflow. + */ +template +std::string format(const char* fmt, Args&&... args) { + auto res = std::snprintf(nullptr, 0, fmt, std::forward(args)...); + if (res < 0) { + throw std::runtime_error(std::strerror(errno)); + } + std::string buf(res, '\0'); + // the size here is fine -- it's legal to write '\0' to buf[res] + auto res2 = std::snprintf(&buf[0], res + 1, fmt, std::forward(args)...); + if (res2 < 0) { + throw std::runtime_error(std::strerror(errno)); + } + + if (res2 != res) { + throw std::runtime_error( + "The size of the formated string is not equal to what it is expected."); + } + return buf; +} + +/** + * Dedup the elements in a vector. + */ +template +void dedup(std::vector& in) { + if (in.empty()) { + return; + } + auto it = std::unique(in.begin(), in.end()); + in.resize(std::distance(in.begin(), it)); +} +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/System.cpp b/torchaudio/csrc/decoder/src/dictionary/System.cpp new file mode 100644 index 0000000000..a4516f285d --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/System.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include "torchaudio/csrc/decoder/src/dictionary/System.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "torchaudio/csrc/decoder/src/dictionary/String.h" + +namespace torchaudio { +namespace lib { + +size_t getProcessId() { +#ifdef _WIN32 + return GetCurrentProcessId(); +#else + return ::getpid(); +#endif +} + +size_t getThreadId() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return std::hash()(std::this_thread::get_id()); +#endif +} + +std::string pathSeperator() { +#ifdef _WIN32 + return "\\"; +#else + return "/"; +#endif +} + +std::string pathsConcat(const std::string& p1, const std::string& p2) { + if (!p1.empty() && p1[p1.length() - 1] != pathSeperator()[0]) { + return ( + trim(p1) + pathSeperator() + trim(p2)); // Need to add a path separator + } else { + return (trim(p1) + trim(p2)); + } +} + +namespace { + +/** + * @path contains directories separated by path separator. + * Returns a vector with the directores in the original order. Vector with a + * Special cases: a vector with a single entry containing the input is returned + * when path is one of the following special cases: empty, “.”, “..” and “/” + */ +std::vector getDirsOnPath(const std::string& path) { + const std::string trimPath = trim(path); + + if (trimPath.empty() || trimPath == pathSeperator() || trimPath == "." || + trimPath == "..") { + return {trimPath}; + } + const std::vector tokens = split(pathSeperator(), trimPath); + std::vector dirs; + for (const std::string& token : tokens) { + const std::string dir = trim(token); + if (!dir.empty()) { + dirs.push_back(dir); + } + } + return dirs; +} + +} // namespace + +std::string dirname(const std::string& path) { + std::vector dirsOnPath = getDirsOnPath(path); + if (dirsOnPath.size() < 2) { + return "."; + } else { + dirsOnPath.pop_back(); + const std::string root = + ((trim(path))[0] == pathSeperator()[0]) ? pathSeperator() : ""; + return root + join(pathSeperator(), dirsOnPath); + } +} + +std::string basename(const std::string& path) { + std::vector dirsOnPath = getDirsOnPath(path); + if (dirsOnPath.empty()) { + return ""; + } else { + return dirsOnPath.back(); + } +} + +bool dirExists(const std::string& path) { + struct stat info; + if (stat(path.c_str(), &info) != 0) { + return false; + } else if (info.st_mode & S_IFDIR) { + return true; + } else { + return false; + } +} + +void dirCreate(const std::string& path) { + if (dirExists(path)) { + return; + } + mode_t nMode = 0755; + int nError = 0; +#ifdef _WIN32 + nError = _mkdir(path.c_str()); +#else + nError = mkdir(path.c_str(), nMode); +#endif + if (nError != 0) { + throw std::runtime_error( + std::string() + "Unable to create directory - " + path); + } +} + +void dirCreateRecursive(const std::string& path) { + if (dirExists(path)) { + return; + } + std::vector dirsOnPath = getDirsOnPath(path); + std::string pathFromStart; + if (path[0] == pathSeperator()[0]) { + pathFromStart = pathSeperator(); + } + for (std::string& dir : dirsOnPath) { + if (pathFromStart.empty()) { + pathFromStart = dir; + } else { + pathFromStart = pathsConcat(pathFromStart, dir); + } + + if (!dirExists(pathFromStart)) { + dirCreate(pathFromStart); + } + } +} + +bool fileExists(const std::string& path) { + std::ifstream fs(path, std::ifstream::in); + return fs.good(); +} + +std::string getEnvVar( + const std::string& key, + const std::string& dflt /*= "" */) { + char* val = getenv(key.c_str()); + return val ? std::string(val) : dflt; +} + +std::string getCurrentDate() { + time_t now = time(nullptr); + struct tm tmbuf; + struct tm* tstruct; + tstruct = localtime_r(&now, &tmbuf); + + std::array buf; + strftime(buf.data(), buf.size(), "%Y-%m-%d", tstruct); + return std::string(buf.data()); +} + +std::string getCurrentTime() { + time_t now = time(nullptr); + struct tm tmbuf; + struct tm* tstruct; + tstruct = localtime_r(&now, &tmbuf); + + std::array buf; + strftime(buf.data(), buf.size(), "%X", tstruct); + return std::string(buf.data()); +} + +std::string getTmpPath(const std::string& filename) { + std::string tmpDir = "/tmp"; + auto getTmpDir = [&tmpDir](const std::string& env) { + char* dir = std::getenv(env.c_str()); + if (dir != nullptr) { + tmpDir = std::string(dir); + } + }; + getTmpDir("TMPDIR"); + getTmpDir("TEMP"); + getTmpDir("TMP"); + return tmpDir + "/fl_tmp_" + getEnvVar("USER", "unknown") + "_" + filename; +} + +std::vector getFileContent(const std::string& file) { + std::vector data; + std::ifstream in = createInputStream(file); + std::string str; + while (std::getline(in, str)) { + data.emplace_back(str); + } + in.close(); + return data; +} + +std::vector fileGlob(const std::string& pat) { + glob_t result; + glob(pat.c_str(), GLOB_TILDE, nullptr, &result); + std::vector ret; + for (unsigned int i = 0; i < result.gl_pathc; ++i) { + ret.push_back(std::string(result.gl_pathv[i])); + } + globfree(&result); + return ret; +} + +std::ifstream createInputStream(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file for reading: " + filename); + } + return file; +} + +std::ofstream createOutputStream( + const std::string& filename, + std::ios_base::openmode mode) { + std::ofstream file(filename, mode); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file for writing: " + filename); + } + return file; +} + +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/System.h b/torchaudio/csrc/decoder/src/dictionary/System.h new file mode 100644 index 0000000000..4cdb37abe4 --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/System.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace torchaudio { +namespace lib { + +size_t getProcessId(); + +size_t getThreadId(); + +std::string pathsConcat(const std::string& p1, const std::string& p2); + +std::string pathSeperator(); + +std::string dirname(const std::string& path); + +std::string basename(const std::string& path); + +bool dirExists(const std::string& path); + +void dirCreate(const std::string& path); + +void dirCreateRecursive(const std::string& path); + +bool fileExists(const std::string& path); + +std::string getEnvVar(const std::string& key, const std::string& dflt = ""); + +std::string getCurrentDate(); + +std::string getCurrentTime(); + +std::string getTmpPath(const std::string& filename); + +std::vector getFileContent(const std::string& file); + +std::vector fileGlob(const std::string& pat); + +std::ifstream createInputStream(const std::string& filename); + +std::ofstream createOutputStream( + const std::string& filename, + std::ios_base::openmode mode = std::ios_base::out); + +/** + * Calls `f(args...)` repeatedly, retrying if an exception is thrown. + * Supports sleeps between retries, with duration starting at `initial` and + * multiplying by `factor` each retry. At most `maxIters` calls are made. + */ +template +typename std::result_of::type retryWithBackoff( + std::chrono::duration initial, + double factor, + int64_t maxIters, + Fn&& f, + Args&&... args) { + if (!(initial.count() >= 0.0)) { + throw std::invalid_argument("retryWithBackoff: bad initial"); + } else if (!(factor >= 0.0)) { + throw std::invalid_argument("retryWithBackoff: bad factor"); + } else if (maxIters <= 0) { + throw std::invalid_argument("retryWithBackoff: bad maxIters"); + } + auto sleepSecs = initial.count(); + for (int64_t i = 0; i < maxIters; ++i) { + try { + return f(std::forward(args)...); + } catch (...) { + if (i >= maxIters - 1) { + throw; + } + } + if (sleepSecs > 0.0) { + /* sleep override */ + std::this_thread::sleep_for( + std::chrono::duration(std::min(1e7, sleepSecs))); + } + sleepSecs *= factor; + } + throw std::logic_error("retryWithBackoff: hit unreachable"); +} +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/Utils.cpp b/torchaudio/csrc/decoder/src/dictionary/Utils.cpp new file mode 100644 index 0000000000..504d71f42d --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/Utils.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include "torchaudio/csrc/decoder/src/dictionary/Utils.h" +#include "torchaudio/csrc/decoder/src/dictionary/Defines.h" +#include "torchaudio/csrc/decoder/src/dictionary/String.h" +#include "torchaudio/csrc/decoder/src/dictionary/System.h" + +namespace torchaudio { +namespace lib { +namespace text { + +Dictionary createWordDict(const LexiconMap& lexicon) { + Dictionary dict; + for (const auto& it : lexicon) { + dict.addEntry(it.first); + } + dict.setDefaultIndex(dict.getIndex(kUnkToken)); + return dict; +} + +LexiconMap loadWords(const std::string& filename, int maxWords) { + LexiconMap lexicon; + + std::string line; + std::ifstream infile = createInputStream(filename); + + // Add at most `maxWords` words into the lexicon. + // If `maxWords` is negative then no limit is applied. + while (maxWords != lexicon.size() && std::getline(infile, line)) { + // Parse the line into two strings: word and spelling. + auto fields = splitOnWhitespace(line, true); + if (fields.size() < 2) { + throw std::runtime_error("[loadWords] Invalid line: " + line); + } + const std::string& word = fields[0]; + std::vector spelling(fields.size() - 1); + std::copy(fields.begin() + 1, fields.end(), spelling.begin()); + + // Add the word into the dictionary. + if (lexicon.find(word) == lexicon.end()) { + lexicon[word] = {}; + } + + // Add the current spelling of the words to the list of spellings. + lexicon[word].push_back(spelling); + } + + // Insert unknown word. + lexicon[kUnkToken] = {}; + return lexicon; +} + +std::vector splitWrd(const std::string& word) { + std::vector tokens; + tokens.reserve(word.size()); + int len = word.length(); + for (int i = 0; i < len;) { + auto c = static_cast(word[i]); + int curTknBytes = -1; + // UTF-8 checks, works for ASCII automatically + if ((c & 0x80) == 0) { + curTknBytes = 1; + } else if ((c & 0xE0) == 0xC0) { + curTknBytes = 2; + } else if ((c & 0xF0) == 0xE0) { + curTknBytes = 3; + } else if ((c & 0xF8) == 0xF0) { + curTknBytes = 4; + } + if (curTknBytes == -1 || i + curTknBytes > len) { + throw std::runtime_error("splitWrd: invalid UTF-8 : " + word); + } + tokens.emplace_back(word.begin() + i, word.begin() + i + curTknBytes); + i += curTknBytes; + } + return tokens; +} + +std::vector packReplabels( + const std::vector& tokens, + const Dictionary& dict, + int maxReps) { + if (tokens.empty() || maxReps <= 0) { + return tokens; + } + + std::vector replabelValueToIdx(maxReps + 1); + for (int i = 1; i <= maxReps; ++i) { + replabelValueToIdx[i] = dict.getIndex("<" + std::to_string(i) + ">"); + } + + std::vector result; + int prevToken = -1; + int numReps = 0; + for (int token : tokens) { + if (token == prevToken && numReps < maxReps) { + numReps++; + } else { + if (numReps > 0) { + result.push_back(replabelValueToIdx[numReps]); + numReps = 0; + } + result.push_back(token); + prevToken = token; + } + } + if (numReps > 0) { + result.push_back(replabelValueToIdx[numReps]); + } + return result; +} + +std::vector unpackReplabels( + const std::vector& tokens, + const Dictionary& dict, + int maxReps) { + if (tokens.empty() || maxReps <= 0) { + return tokens; + } + + std::unordered_map replabelIdxToValue; + for (int i = 1; i <= maxReps; ++i) { + replabelIdxToValue.emplace(dict.getIndex("<" + std::to_string(i) + ">"), i); + } + + std::vector result; + int prevToken = -1; + for (int token : tokens) { + auto it = replabelIdxToValue.find(token); + if (it == replabelIdxToValue.end()) { + result.push_back(token); + prevToken = token; + } else if (prevToken != -1) { + result.insert(result.end(), it->second, prevToken); + prevToken = -1; + } + } + return result; +} +} // namespace text +} // namespace lib +} // namespace torchaudio diff --git a/torchaudio/csrc/decoder/src/dictionary/Utils.h b/torchaudio/csrc/decoder/src/dictionary/Utils.h new file mode 100644 index 0000000000..09e67c4ae1 --- /dev/null +++ b/torchaudio/csrc/decoder/src/dictionary/Utils.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#pragma once + +#include +#include +#include + +#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" + +namespace torchaudio { +namespace lib { +namespace text { + +using LexiconMap = + std::unordered_map>>; + +Dictionary createWordDict(const LexiconMap& lexicon); + +LexiconMap loadWords(const std::string& filename, int maxWords = -1); + +// split word into tokens abc -> {"a", "b", "c"} +// Works with ASCII, UTF-8 encodings +std::vector splitWrd(const std::string& word); + +/** + * Pack a token sequence by replacing consecutive repeats with replabels, + * e.g. "abbccc" -> "ab1c2". The tokens "1", "2", ..., `to_string(maxReps)` + * must already be in `dict`. + */ +std::vector packReplabels( + const std::vector& tokens, + const Dictionary& dict, + int maxReps); + +/** + * Unpack a token sequence by replacing replabels with repeated tokens, + * e.g. "ab1c2" -> "abbccc". The tokens "1", "2", ..., `to_string(maxReps)` + * must already be in `dict`. + */ +std::vector unpackReplabels( + const std::vector& tokens, + const Dictionary& dict, + int maxReps); +} // namespace text +} // namespace lib +} // namespace torchaudio From 396d16cb96babda5c0b2f39475d34e3d031bea1d Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Fri, 17 Dec 2021 14:29:53 -0800 Subject: [PATCH 0039/1144] Add C++ files for CTC decoder bindings (#2079) Summary: part of https://github.com/pytorch/audio/issues/2072 -- splitting up the PR for easier review Add C++ files for binding CTC decoder functionality for Python Note: the code here will not be compiled until the build process is changed Pull Request resolved: https://github.com/pytorch/audio/pull/2079 Reviewed By: mthrok Differential Revision: D33196286 Pulled By: carolineechen fbshipit-source-id: 9fe4a8635b60ebfb594918bab00f5c3dccf96bd2 --- torchaudio/csrc/decoder/bindings/_decoder.cpp | 111 +++++++++ .../csrc/decoder/bindings/_dictionary.cpp | 31 +++ .../bindings/cmake/Buildpybind11.cmake | 19 ++ .../bindings/cmake/FindPythonLibsNew.cmake | 198 ++++++++++++++++ .../bindings/cmake/pybind11Tools.cmake | 224 ++++++++++++++++++ torchaudio/csrc/decoder/bindings/pybind.cpp | 139 +++++++++++ 6 files changed, 722 insertions(+) create mode 100644 torchaudio/csrc/decoder/bindings/_decoder.cpp create mode 100644 torchaudio/csrc/decoder/bindings/_dictionary.cpp create mode 100755 torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake create mode 100644 torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake create mode 100755 torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake create mode 100644 torchaudio/csrc/decoder/bindings/pybind.cpp diff --git a/torchaudio/csrc/decoder/bindings/_decoder.cpp b/torchaudio/csrc/decoder/bindings/_decoder.cpp new file mode 100644 index 0000000000..69bbe0a2d2 --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/_decoder.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include +#include + +#include "torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h" +#include "torchaudio/csrc/decoder/src/decoder/lm/KenLM.h" + +namespace py = pybind11; +using namespace torchaudio::lib::text; +using namespace py::literals; + +/** + * Some hackery that lets pybind11 handle shared_ptr (for old LMStatePtr). + * See: https://github.com/pybind/pybind11/issues/820 + * PYBIND11_MAKE_OPAQUE(std::shared_ptr); + * and inside PYBIND11_MODULE + * py::class_>(m, "encapsulated_data"); + */ + +namespace { + +/** + * A pybind11 "alias type" for abstract class LM, allowing one to subclass LM + * with a custom LM defined purely in Python. For those who don't want to build + * with KenLM, or have their own custom LM implementation. + * See: https://pybind11.readthedocs.io/en/stable/advanced/classes.html + * + * TODO: ensure this works. Last time Jeff tried this there were slicing issues, + * see https://github.com/pybind/pybind11/issues/1546 for workarounds. + * This is low-pri since we assume most people can just build with KenLM. + */ +class PyLM : public LM { + using LM::LM; + + // needed for pybind11 or else it won't compile + using LMOutput = std::pair; + + LMStatePtr start(bool startWithNothing) override { + PYBIND11_OVERLOAD_PURE(LMStatePtr, LM, start, startWithNothing); + } + + LMOutput score(const LMStatePtr& state, const int usrTokenIdx) override { + PYBIND11_OVERLOAD_PURE(LMOutput, LM, score, state, usrTokenIdx); + } + + LMOutput finish(const LMStatePtr& state) override { + PYBIND11_OVERLOAD_PURE(LMOutput, LM, finish, state); + } +}; + +/** + * Using custom python LMState derived from LMState is not working with + * custom python LM (derived from PyLM) because we need to to custing of LMState + * in score and finish functions to the derived class + * (for example vie obj.__class__ = CustomPyLMSTate) which cause the error + * "TypeError: __class__ assignment: 'CustomPyLMState' deallocator differs + * from 'flashlight.text.decoder._decoder.LMState'" + * details see in https://github.com/pybind/pybind11/issues/1640 + * To define custom LM you can introduce map inside LM which maps LMstate to + * additional state info (shared pointers pointing to the same underlying object + * will have the same id in python in functions score and finish) + * + * ```python + * from flashlight.lib.text.decoder import LM + * class MyPyLM(LM): + * mapping_states = dict() # store simple additional int for each state + * + * def __init__(self): + * LM.__init__(self) + * + * def start(self, start_with_nothing): + * state = LMState() + * self.mapping_states[state] = 0 + * return state + * + * def score(self, state, index): + * outstate = state.child(index) + * if outstate not in self.mapping_states: + * self.mapping_states[outstate] = self.mapping_states[state] + 1 + * return (outstate, -numpy.random.random()) + * + * def finish(self, state): + * outstate = state.child(-1) + * if outstate not in self.mapping_states: + * self.mapping_states[outstate] = self.mapping_states[state] + 1 + * return (outstate, -1) + *``` + */ +void LexiconDecoder_decodeStep( + LexiconDecoder& decoder, + uintptr_t emissions, + int T, + int N) { + decoder.decodeStep(reinterpret_cast(emissions), T, N); +} + +std::vector LexiconDecoder_decode( + LexiconDecoder& decoder, + uintptr_t emissions, + int T, + int N) { + return decoder.decode(reinterpret_cast(emissions), T, N); +} + +} // namespace diff --git a/torchaudio/csrc/decoder/bindings/_dictionary.cpp b/torchaudio/csrc/decoder/bindings/_dictionary.cpp new file mode 100644 index 0000000000..d3c5408ca2 --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/_dictionary.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT-style license found in + * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE + */ + +#include +#include + +#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" +#include "torchaudio/csrc/decoder/src/dictionary/Utils.h" + +namespace py = pybind11; +using namespace torchaudio::lib::text; +using namespace py::literals; + +namespace { + +void Dictionary_addEntry_0( + Dictionary& dict, + const std::string& entry, + int idx) { + dict.addEntry(entry, idx); +} + +void Dictionary_addEntry_1(Dictionary& dict, const std::string& entry) { + dict.addEntry(entry); +} + +} // namespace diff --git a/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake b/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake new file mode 100755 index 0000000000..d891b838ac --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake @@ -0,0 +1,19 @@ +include(ExternalProject) + +set(pybind11_URL https://github.com/pybind/pybind11.git) +set(pybind11_TAG 9a19306fbf30642ca331d0ec88e7da54a96860f9) # release 2.2.4 + +# Download pybind11 +ExternalProject_Add( + pybind11 + PREFIX pybind11 + GIT_REPOSITORY ${pybind11_URL} + GIT_TAG ${pybind11_TAG} + BUILD_IN_SOURCE 0 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" +) + +ExternalProject_Get_Property(pybind11 SOURCE_DIR) +set(pybind11_INCLUDE_DIR "${SOURCE_DIR}/include") \ No newline at end of file diff --git a/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake b/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake new file mode 100644 index 0000000000..919a384ea4 --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake @@ -0,0 +1,198 @@ +# - Find python libraries +# This module finds the libraries corresponding to the Python interpreter +# FindPythonInterp provides. +# This code sets the following variables: +# +# PYTHONLIBS_FOUND - have the Python libs been found +# PYTHON_PREFIX - path to the Python installation +# PYTHON_LIBRARIES - path to the python library +# PYTHON_INCLUDE_DIRS - path to where Python.h is found +# PYTHON_MODULE_EXTENSION - lib extension, e.g. '.so' or '.pyd' +# PYTHON_MODULE_PREFIX - lib name prefix: usually an empty string +# PYTHON_SITE_PACKAGES - path to installation site-packages +# PYTHON_IS_DEBUG - whether the Python interpreter is a debug build +# +# Thanks to talljimbo for the patch adding the 'LDVERSION' config +# variable usage. + +#============================================================================= +# Copyright 2001-2009 Kitware, Inc. +# Copyright 2012 Continuum Analytics, Inc. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the names of Kitware, Inc., the Insight Software Consortium, +# nor the names of their contributors may be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# Checking for the extension makes sure that `LibsNew` was found and not just `Libs`. +if(PYTHONLIBS_FOUND AND PYTHON_MODULE_EXTENSION) + return() +endif() + +# Use the Python interpreter to find the libs. +if(PythonLibsNew_FIND_REQUIRED) + find_package(PythonInterp ${PythonLibsNew_FIND_VERSION} REQUIRED) +else() + find_package(PythonInterp ${PythonLibsNew_FIND_VERSION}) +endif() + +if(NOT PYTHONINTERP_FOUND) + set(PYTHONLIBS_FOUND FALSE) + return() +endif() + +# According to http://stackoverflow.com/questions/646518/python-how-to-detect-debug-interpreter +# testing whether sys has the gettotalrefcount function is a reliable, cross-platform +# way to detect a CPython debug interpreter. +# +# The library suffix is from the config var LDVERSION sometimes, otherwise +# VERSION. VERSION will typically be like "2.7" on unix, and "27" on windows. +execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "from distutils import sysconfig as s;import sys;import struct; +print('.'.join(str(v) for v in sys.version_info)); +print(sys.prefix); +print(s.get_python_inc(plat_specific=True)); +print(s.get_python_lib(plat_specific=True)); +print(s.get_config_var('SO')); +print(hasattr(sys, 'gettotalrefcount')+0); +print(struct.calcsize('@P')); +print(s.get_config_var('LDVERSION') or s.get_config_var('VERSION')); +print(s.get_config_var('LIBDIR') or ''); +print(s.get_config_var('MULTIARCH') or ''); +" + RESULT_VARIABLE _PYTHON_SUCCESS + OUTPUT_VARIABLE _PYTHON_VALUES + ERROR_VARIABLE _PYTHON_ERROR_VALUE) + +if(NOT _PYTHON_SUCCESS MATCHES 0) + if(PythonLibsNew_FIND_REQUIRED) + message(FATAL_ERROR + "Python config failure:\n${_PYTHON_ERROR_VALUE}") + endif() + set(PYTHONLIBS_FOUND FALSE) + return() +endif() + +# Convert the process output into a list +if(WIN32) + string(REGEX REPLACE "\\\\" "/" _PYTHON_VALUES ${_PYTHON_VALUES}) +endif() +string(REGEX REPLACE ";" "\\\\;" _PYTHON_VALUES ${_PYTHON_VALUES}) +string(REGEX REPLACE "\n" ";" _PYTHON_VALUES ${_PYTHON_VALUES}) +list(GET _PYTHON_VALUES 0 _PYTHON_VERSION_LIST) +list(GET _PYTHON_VALUES 1 PYTHON_PREFIX) +list(GET _PYTHON_VALUES 2 PYTHON_INCLUDE_DIR) +list(GET _PYTHON_VALUES 3 PYTHON_SITE_PACKAGES) +list(GET _PYTHON_VALUES 4 PYTHON_MODULE_EXTENSION) +list(GET _PYTHON_VALUES 5 PYTHON_IS_DEBUG) +list(GET _PYTHON_VALUES 6 PYTHON_SIZEOF_VOID_P) +list(GET _PYTHON_VALUES 7 PYTHON_LIBRARY_SUFFIX) +list(GET _PYTHON_VALUES 8 PYTHON_LIBDIR) +list(GET _PYTHON_VALUES 9 PYTHON_MULTIARCH) + +# Make sure the Python has the same pointer-size as the chosen compiler +# Skip if CMAKE_SIZEOF_VOID_P is not defined +if(CMAKE_SIZEOF_VOID_P AND (NOT "${PYTHON_SIZEOF_VOID_P}" STREQUAL "${CMAKE_SIZEOF_VOID_P}")) + if(PythonLibsNew_FIND_REQUIRED) + math(EXPR _PYTHON_BITS "${PYTHON_SIZEOF_VOID_P} * 8") + math(EXPR _CMAKE_BITS "${CMAKE_SIZEOF_VOID_P} * 8") + message(FATAL_ERROR + "Python config failure: Python is ${_PYTHON_BITS}-bit, " + "chosen compiler is ${_CMAKE_BITS}-bit") + endif() + set(PYTHONLIBS_FOUND FALSE) + return() +endif() + +# The built-in FindPython didn't always give the version numbers +string(REGEX REPLACE "\\." ";" _PYTHON_VERSION_LIST ${_PYTHON_VERSION_LIST}) +list(GET _PYTHON_VERSION_LIST 0 PYTHON_VERSION_MAJOR) +list(GET _PYTHON_VERSION_LIST 1 PYTHON_VERSION_MINOR) +list(GET _PYTHON_VERSION_LIST 2 PYTHON_VERSION_PATCH) + +# Make sure all directory separators are '/' +string(REGEX REPLACE "\\\\" "/" PYTHON_PREFIX ${PYTHON_PREFIX}) +string(REGEX REPLACE "\\\\" "/" PYTHON_INCLUDE_DIR ${PYTHON_INCLUDE_DIR}) +string(REGEX REPLACE "\\\\" "/" PYTHON_SITE_PACKAGES ${PYTHON_SITE_PACKAGES}) + +if(CMAKE_HOST_WIN32) + set(PYTHON_LIBRARY + "${PYTHON_PREFIX}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") + + # when run in a venv, PYTHON_PREFIX points to it. But the libraries remain in the + # original python installation. They may be found relative to PYTHON_INCLUDE_DIR. + if(NOT EXISTS "${PYTHON_LIBRARY}") + get_filename_component(_PYTHON_ROOT ${PYTHON_INCLUDE_DIR} DIRECTORY) + set(PYTHON_LIBRARY + "${_PYTHON_ROOT}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") + endif() + + # raise an error if the python libs are still not found. + if(NOT EXISTS "${PYTHON_LIBRARY}") + message(FATAL_ERROR "Python libraries not found") + endif() + +else() + if(PYTHON_MULTIARCH) + set(_PYTHON_LIBS_SEARCH "${PYTHON_LIBDIR}/${PYTHON_MULTIARCH}" "${PYTHON_LIBDIR}") + else() + set(_PYTHON_LIBS_SEARCH "${PYTHON_LIBDIR}") + endif() + #message(STATUS "Searching for Python libs in ${_PYTHON_LIBS_SEARCH}") + # Probably this needs to be more involved. It would be nice if the config + # information the python interpreter itself gave us were more complete. + find_library(PYTHON_LIBRARY + NAMES "python${PYTHON_LIBRARY_SUFFIX}" + PATHS ${_PYTHON_LIBS_SEARCH} + NO_DEFAULT_PATH) + + # If all else fails, just set the name/version and let the linker figure out the path. + if(NOT PYTHON_LIBRARY) + set(PYTHON_LIBRARY python${PYTHON_LIBRARY_SUFFIX}) + endif() +endif() + +MARK_AS_ADVANCED( + PYTHON_LIBRARY + PYTHON_INCLUDE_DIR +) + +# We use PYTHON_INCLUDE_DIR, PYTHON_LIBRARY and PYTHON_DEBUG_LIBRARY for the +# cache entries because they are meant to specify the location of a single +# library. We now set the variables listed by the documentation for this +# module. +SET(PYTHON_INCLUDE_DIRS "${PYTHON_INCLUDE_DIR}") +SET(PYTHON_LIBRARIES "${PYTHON_LIBRARY}") +SET(PYTHON_DEBUG_LIBRARIES "${PYTHON_DEBUG_LIBRARY}") + +find_package_message(PYTHON + "Found PythonLibs: ${PYTHON_LIBRARY}" + "${PYTHON_EXECUTABLE}${PYTHON_VERSION}") + +set(PYTHONLIBS_FOUND TRUE) diff --git a/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake b/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake new file mode 100755 index 0000000000..0fdacc78d8 --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake @@ -0,0 +1,224 @@ +# tools/pybind11Tools.cmake -- Build system for the pybind11 modules +# +# Copyright (c) 2015 Wenzel Jakob +# +# All rights reserved. Use of this source code is governed by a +# BSD-style license that can be found in the LICENSE file. + +cmake_minimum_required(VERSION 2.8.12) + +# Add a CMake parameter for choosing a desired Python version +if(NOT PYBIND11_PYTHON_VERSION) + set(PYBIND11_PYTHON_VERSION "" CACHE STRING "Python version to use for compiling modules") +endif() + +set(Python_ADDITIONAL_VERSIONS 3.7 3.6 3.5 3.4) +find_package(PythonLibsNew ${PYBIND11_PYTHON_VERSION} REQUIRED) + +include(CheckCXXCompilerFlag) +include(CMakeParseArguments) + +if(NOT PYBIND11_CPP_STANDARD AND NOT CMAKE_CXX_STANDARD) + if(NOT MSVC) + check_cxx_compiler_flag("-std=c++14" HAS_CPP14_FLAG) + + if (HAS_CPP14_FLAG) + set(PYBIND11_CPP_STANDARD -std=c++14) + else() + check_cxx_compiler_flag("-std=c++11" HAS_CPP11_FLAG) + if (HAS_CPP11_FLAG) + set(PYBIND11_CPP_STANDARD -std=c++11) + else() + message(FATAL_ERROR "Unsupported compiler -- pybind11 requires C++11 support!") + endif() + endif() + elseif(MSVC) + set(PYBIND11_CPP_STANDARD /std:c++14) + endif() + + set(PYBIND11_CPP_STANDARD ${PYBIND11_CPP_STANDARD} CACHE STRING + "C++ standard flag, e.g. -std=c++11, -std=c++14, /std:c++14. Defaults to C++14 mode." FORCE) +endif() + +# Checks whether the given CXX/linker flags can compile and link a cxx file. cxxflags and +# linkerflags are lists of flags to use. The result variable is a unique variable name for each set +# of flags: the compilation result will be cached base on the result variable. If the flags work, +# sets them in cxxflags_out/linkerflags_out internal cache variables (in addition to ${result}). +function(_pybind11_return_if_cxx_and_linker_flags_work result cxxflags linkerflags cxxflags_out linkerflags_out) + set(CMAKE_REQUIRED_LIBRARIES ${linkerflags}) + check_cxx_compiler_flag("${cxxflags}" ${result}) + if (${result}) + set(${cxxflags_out} "${cxxflags}" CACHE INTERNAL "" FORCE) + set(${linkerflags_out} "${linkerflags}" CACHE INTERNAL "" FORCE) + endif() +endfunction() + +# Internal: find the appropriate link time optimization flags for this compiler +function(_pybind11_add_lto_flags target_name prefer_thin_lto) + if (NOT DEFINED PYBIND11_LTO_CXX_FLAGS) + set(PYBIND11_LTO_CXX_FLAGS "" CACHE INTERNAL "") + set(PYBIND11_LTO_LINKER_FLAGS "" CACHE INTERNAL "") + + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set(cxx_append "") + set(linker_append "") + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT APPLE) + # Clang Gold plugin does not support -Os; append -O3 to MinSizeRel builds to override it + set(linker_append ";$<$:-O3>") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(cxx_append ";-fno-fat-lto-objects") + endif() + + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND prefer_thin_lto) + _pybind11_return_if_cxx_and_linker_flags_work(HAS_FLTO_THIN + "-flto=thin${cxx_append}" "-flto=thin${linker_append}" + PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) + endif() + + if (NOT HAS_FLTO_THIN) + _pybind11_return_if_cxx_and_linker_flags_work(HAS_FLTO + "-flto${cxx_append}" "-flto${linker_append}" + PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) + endif() + elseif (CMAKE_CXX_COMPILER_ID MATCHES "Intel") + # Intel equivalent to LTO is called IPO + _pybind11_return_if_cxx_and_linker_flags_work(HAS_INTEL_IPO + "-ipo" "-ipo" PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) + elseif(MSVC) + # cmake only interprets libraries as linker flags when they start with a - (otherwise it + # converts /LTCG to \LTCG as if it was a Windows path). Luckily MSVC supports passing flags + # with - instead of /, even if it is a bit non-standard: + _pybind11_return_if_cxx_and_linker_flags_work(HAS_MSVC_GL_LTCG + "/GL" "-LTCG" PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) + endif() + + if (PYBIND11_LTO_CXX_FLAGS) + message(STATUS "LTO enabled") + else() + message(STATUS "LTO disabled (not supported by the compiler and/or linker)") + endif() + endif() + + # Enable LTO flags if found, except for Debug builds + if (PYBIND11_LTO_CXX_FLAGS) + target_compile_options(${target_name} PRIVATE "$<$>:${PYBIND11_LTO_CXX_FLAGS}>") + endif() + if (PYBIND11_LTO_LINKER_FLAGS) + target_link_libraries(${target_name} PRIVATE "$<$>:${PYBIND11_LTO_LINKER_FLAGS}>") + endif() +endfunction() + +# Build a Python extension module: +# pybind11_add_module( [MODULE | SHARED] [EXCLUDE_FROM_ALL] +# [NO_EXTRAS] [SYSTEM] [THIN_LTO] source1 [source2 ...]) +# +function(pybind11_add_module target_name) + set(options MODULE SHARED EXCLUDE_FROM_ALL NO_EXTRAS SYSTEM THIN_LTO) + cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) + + if(ARG_MODULE AND ARG_SHARED) + message(FATAL_ERROR "Can't be both MODULE and SHARED") + elseif(ARG_SHARED) + set(lib_type SHARED) + else() + set(lib_type MODULE) + endif() + + if(ARG_EXCLUDE_FROM_ALL) + set(exclude_from_all EXCLUDE_FROM_ALL) + endif() + + add_library(${target_name} ${lib_type} ${exclude_from_all} ${ARG_UNPARSED_ARGUMENTS}) + message("ADDING LIBRARY ${target_name}") + + if(ARG_SYSTEM) + set(inc_isystem SYSTEM) + endif() + + target_include_directories(${target_name} ${inc_isystem} + PRIVATE ${PYBIND11_INCLUDE_DIR} # from project CMakeLists.txt + PRIVATE ${pybind11_INCLUDE_DIR} # from pybind11Config + PRIVATE ${PYTHON_INCLUDE_DIRS}) + + # Python debug libraries expose slightly different objects + # https://docs.python.org/3.6/c-api/intro.html#debugging-builds + # https://stackoverflow.com/questions/39161202/how-to-work-around-missing-pymodule-create2-in-amd64-win-python35-d-lib + if(PYTHON_IS_DEBUG) + target_compile_definitions(${target_name} PRIVATE Py_DEBUG) + endif() + + # The prefix and extension are provided by FindPythonLibsNew.cmake + set_target_properties(${target_name} PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}") + set_target_properties(${target_name} PROPERTIES SUFFIX "${PYTHON_MODULE_EXTENSION}") + + # -fvisibility=hidden is required to allow multiple modules compiled against + # different pybind versions to work properly, and for some features (e.g. + # py::module_local). We force it on everything inside the `pybind11` + # namespace; also turning it on for a pybind module compilation here avoids + # potential warnings or issues from having mixed hidden/non-hidden types. + set_target_properties(${target_name} PROPERTIES CXX_VISIBILITY_PRESET "hidden") + set_target_properties(${target_name} PROPERTIES CUDA_VISIBILITY_PRESET "hidden") + + if(WIN32 OR CYGWIN) + # Link against the Python shared library on Windows + target_link_libraries(${target_name} PRIVATE ${PYTHON_LIBRARIES}) + elseif(APPLE) + # It's quite common to have multiple copies of the same Python version + # installed on one's system. E.g.: one copy from the OS and another copy + # that's statically linked into an application like Blender or Maya. + # If we link our plugin library against the OS Python here and import it + # into Blender or Maya later on, this will cause segfaults when multiple + # conflicting Python instances are active at the same time (even when they + # are of the same version). + + # Windows is not affected by this issue since it handles DLL imports + # differently. The solution for Linux and Mac OS is simple: we just don't + # link against the Python library. The resulting shared library will have + # missing symbols, but that's perfectly fine -- they will be resolved at + # import time. + + target_link_libraries(${target_name} PRIVATE "-undefined dynamic_lookup") + + if(ARG_SHARED) + # Suppress CMake >= 3.0 warning for shared libraries + set_target_properties(${target_name} PROPERTIES MACOSX_RPATH ON) + endif() + endif() + + # Make sure C++11/14 are enabled + target_compile_options(${target_name} PUBLIC ${PYBIND11_CPP_STANDARD}) + + if(ARG_NO_EXTRAS) + return() + endif() + + _pybind11_add_lto_flags(${target_name} ${ARG_THIN_LTO}) + + if (NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug) + # Strip unnecessary sections of the binary on Linux/Mac OS + if(CMAKE_STRIP) + if(APPLE) + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_STRIP} -x $) + else() + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_STRIP} $) + endif() + endif() + endif() + + if(MSVC) + # /MP enables multithreaded builds (relevant when there are many files), /bigobj is + # needed for bigger binding projects due to the limit to 64k addressable sections + target_compile_options(${target_name} PRIVATE /bigobj) + if(CMAKE_VERSION VERSION_LESS 3.11) + target_compile_options(${target_name} PRIVATE $<$>:/MP>) + else() + # Only set these options for C++ files. This is important so that, for + # instance, projects that include other types of source files like CUDA + # .cu files don't get these options propagated to nvcc since that would + # cause the build to fail. + target_compile_options(${target_name} PRIVATE $<$>:$<$:/MP>>) + endif() + endif() +endfunction() \ No newline at end of file diff --git a/torchaudio/csrc/decoder/bindings/pybind.cpp b/torchaudio/csrc/decoder/bindings/pybind.cpp new file mode 100644 index 0000000000..9278b2965c --- /dev/null +++ b/torchaudio/csrc/decoder/bindings/pybind.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include + +PYBIND11_MODULE(_torchaudio_decoder, m) { +#ifdef BUILD_CTC_DECODER + py::enum_(m, "_SmearingMode") + .value("NONE", SmearingMode::NONE) + .value("MAX", SmearingMode::MAX) + .value("LOGADD", SmearingMode::LOGADD); + + py::class_(m, "_TrieNode") + .def(py::init(), "idx"_a) + .def_readwrite("children", &TrieNode::children) + .def_readwrite("idx", &TrieNode::idx) + .def_readwrite("labels", &TrieNode::labels) + .def_readwrite("scores", &TrieNode::scores) + .def_readwrite("max_score", &TrieNode::maxScore); + + py::class_(m, "_Trie") + .def(py::init(), "max_children"_a, "root_idx"_a) + .def("get_root", &Trie::getRoot) + .def("insert", &Trie::insert, "indices"_a, "label"_a, "score"_a) + .def("search", &Trie::search, "indices"_a) + .def("smear", &Trie::smear, "smear_mode"_a); + + py::class_(m, "_LM") + .def(py::init<>()) + .def("start", &LM::start, "start_with_nothing"_a) + .def("score", &LM::score, "state"_a, "usr_token_idx"_a) + .def("finish", &LM::finish, "state"_a); + + py::class_(m, "_LMState") + .def(py::init<>()) + .def_readwrite("children", &LMState::children) + .def("compare", &LMState::compare, "state"_a) + .def("child", &LMState::child, "usr_index"_a); + + py::class_(m, "_KenLM") + .def( + py::init(), + "path"_a, + "usr_token_dict"_a); + + py::enum_(m, "_CriterionType") + .value("ASG", CriterionType::ASG) + .value("CTC", CriterionType::CTC); + + py::class_(m, "_LexiconDecoderOptions") + .def( + py::init< + const int, + const int, + const double, + const double, + const double, + const double, + const double, + const bool, + const CriterionType>(), + "beam_size"_a, + "beam_size_token"_a, + "beam_threshold"_a, + "lm_weight"_a, + "word_score"_a, + "unk_score"_a, + "sil_score"_a, + "log_add"_a, + "criterion_type"_a) + .def_readwrite("beam_size", &LexiconDecoderOptions::beamSize) + .def_readwrite("beam_size_token", &LexiconDecoderOptions::beamSizeToken) + .def_readwrite("beam_threshold", &LexiconDecoderOptions::beamThreshold) + .def_readwrite("lm_weight", &LexiconDecoderOptions::lmWeight) + .def_readwrite("word_score", &LexiconDecoderOptions::wordScore) + .def_readwrite("unk_score", &LexiconDecoderOptions::unkScore) + .def_readwrite("sil_score", &LexiconDecoderOptions::silScore) + .def_readwrite("log_add", &LexiconDecoderOptions::logAdd) + .def_readwrite("criterion_type", &LexiconDecoderOptions::criterionType); + + py::class_(m, "_DecodeResult") + .def(py::init(), "length"_a) + .def_readwrite("score", &DecodeResult::score) + .def_readwrite("amScore", &DecodeResult::amScore) + .def_readwrite("lmScore", &DecodeResult::lmScore) + .def_readwrite("words", &DecodeResult::words) + .def_readwrite("tokens", &DecodeResult::tokens); + + // NB: `decode` and `decodeStep` expect raw emissions pointers. + py::class_(m, "_LexiconDecoder") + .def(py::init< + LexiconDecoderOptions, + const TriePtr, + const LMPtr, + const int, + const int, + const int, + const std::vector&, + const bool>()) + .def("decode_begin", &LexiconDecoder::decodeBegin) + .def( + "decode_step", + &LexiconDecoder_decodeStep, + "emissions"_a, + "T"_a, + "N"_a) + .def("decode_end", &LexiconDecoder::decodeEnd) + .def("decode", &LexiconDecoder_decode, "emissions"_a, "T"_a, "N"_a) + .def("prune", &LexiconDecoder::prune, "look_back"_a = 0) + .def( + "get_best_hypothesis", + &LexiconDecoder::getBestHypothesis, + "look_back"_a = 0) + .def("get_all_final_hypothesis", &LexiconDecoder::getAllFinalHypothesis); + + py::class_(m, "_Dictionary") + .def(py::init<>()) + .def(py::init(), "filename"_a) + .def("entry_size", &Dictionary::entrySize) + .def("index_size", &Dictionary::indexSize) + .def("add_entry", &Dictionary_addEntry_0, "entry"_a, "idx"_a) + .def("add_entry", &Dictionary_addEntry_1, "entry"_a) + .def("get_entry", &Dictionary::getEntry, "idx"_a) + .def("set_default_index", &Dictionary::setDefaultIndex, "idx"_a) + .def("get_index", &Dictionary::getIndex, "entry"_a) + .def("contains", &Dictionary::contains, "entry"_a) + .def("is_contiguous", &Dictionary::isContiguous) + .def( + "map_entries_to_indices", + &Dictionary::mapEntriesToIndices, + "entries"_a) + .def( + "map_indices_to_entries", + &Dictionary::mapIndicesToEntries, + "indices"_a); + m.def("_create_word_dict", &createWordDict, "lexicon"_a); + m.def("_load_words", &loadWords, "filename"_a, "max_words"_a = -1); +#endif +} From 246dd52acadbd7785c1a1fa5da23922154cfc0fb Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Sat, 18 Dec 2021 10:49:29 -0800 Subject: [PATCH 0040/1144] Add FL Decoder / KenLM integration to build process (#2078) Summary: After all the C++ code from https://github.com/pytorch/audio/issues/2072 are added, this commit will enable decoder/KenLM integration in the build process. Pull Request resolved: https://github.com/pytorch/audio/pull/2078 Reviewed By: carolineechen Differential Revision: D33198183 Pulled By: mthrok fbshipit-source-id: 9d7fa76151d06fbbac3785183c7c2ff9862d3128 --- CMakeLists.txt | 2 +- third_party/CMakeLists.txt | 2 +- third_party/boost/CMakeLists.txt | 16 +- third_party/bzip2/CMakeLists.txt | 13 +- third_party/lzma/CMakeLists.txt | 17 +- third_party/zlib/CMakeLists.txt | 18 +- tools/setup_helpers/extension.py | 12 +- torchaudio/csrc/CMakeLists.txt | 57 +++++ .../bindings/cmake/Buildpybind11.cmake | 19 -- .../bindings/cmake/FindPythonLibsNew.cmake | 198 ---------------- .../bindings/cmake/pybind11Tools.cmake | 224 ------------------ .../csrc/decoder/src/decoder/lm/KenLM.cpp | 2 +- 12 files changed, 109 insertions(+), 471 deletions(-) delete mode 100755 torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake delete mode 100644 torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake delete mode 100755 torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 9999dc3860..89c8bd66b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,7 @@ endif() option(BUILD_SOX "Build libsox statically" ON) option(BUILD_KALDI "Build kaldi statically" ON) option(BUILD_RNNT "Enable RNN transducer" ON) -option(BUILD_KENLM "Build KenLM statically" ON) +option(BUILD_CTC_DECODER "Build Flashlight CTC decoder" ON) option(BUILD_TORCHAUDIO_PYTHON_EXTENSION "Build Python extension" OFF) option(USE_CUDA "Enable CUDA support" OFF) option(USE_ROCM "Enable ROCM support" OFF) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 6c63ed7428..197e35db9b 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -26,7 +26,7 @@ set_property(GLOBAL PROPERTY TORCHAUDIO_THIRD_PARTIES "${TORCHAUDIO_THIRD_PARTIE ################################################################################ # KenLM ################################################################################ -if (BUILD_KENLM) +if (BUILD_CTC_DECODER) add_subdirectory(zlib) add_subdirectory(bzip2) add_subdirectory(lzma) diff --git a/third_party/boost/CMakeLists.txt b/third_party/boost/CMakeLists.txt index 367cb43523..2f7ff591e2 100644 --- a/third_party/boost/CMakeLists.txt +++ b/third_party/boost/CMakeLists.txt @@ -1,17 +1,18 @@ include(ExternalProject) -set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) +set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install/include) set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) -ExternalProject_Add(boost_ + +ExternalProject_Add(boost PREFIX ${CMAKE_CURRENT_BINARY_DIR} DOWNLOAD_DIR ${ARCHIVE_DIR} URL https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.tar.gz URL_HASH SHA256=94ced8b72956591c4775ae2207a9763d3600b30d9d7446562c552f0a14a63be7 BUILD_IN_SOURCE 1 - CONFIGURE_COMMAND ./bootstrap.sh --with-libraries=thread --prefix=${INSTALL_DIR} - BUILD_COMMAND ./b2 link=static - INSTALL_COMMAND ./b2 link=static install + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory boost ${INCLUDE_DIR}/boost DOWNLOAD_NO_PROGRESS ON LOG_DOWNLOAD ON LOG_UPDATE ON @@ -21,8 +22,3 @@ ExternalProject_Add(boost_ LOG_MERGED_STDOUTERR ON LOG_OUTPUT_ON_FAILURE ON ) - -add_library(boost INTERFACE) -add_dependencies(boost boost_) -target_include_directories(boost INTERFACE ${INSTALL_DIR}/include) -target_link_libraries(boost INTERFACE ${INSTALL_DIR}/lib/libboost_thread.a) diff --git a/third_party/bzip2/CMakeLists.txt b/third_party/bzip2/CMakeLists.txt index 70ba3ceb24..dfccfc7b08 100644 --- a/third_party/bzip2/CMakeLists.txt +++ b/third_party/bzip2/CMakeLists.txt @@ -3,13 +3,20 @@ include(ExternalProject) set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) -ExternalProject_Add(bzip2_ +set( + BZIP_LIBRARIES + ${INSTALL_DIR}/lib/libbz2.a + ) + +ExternalProject_Add(bzip2- PREFIX ${CMAKE_CURRENT_BINARY_DIR} DOWNLOAD_DIR ${ARCHIVE_DIR} URL https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz URL_HASH SHA256=ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269 + BUILD_BYPRODUCTS ${BZIP_LIBRARIES} BUILD_IN_SOURCE 1 CONFIGURE_COMMAND "" + BUILD_COMMAND make VERBOSE=1 "CFLAGS=-fPIC -fvisibility=hidden -Wall -Winline -O2 -g -D_FILE_OFFSET_BITS=64" INSTALL_COMMAND make install PREFIX=${INSTALL_DIR} DOWNLOAD_NO_PROGRESS ON LOG_DOWNLOAD ON @@ -22,6 +29,6 @@ ExternalProject_Add(bzip2_ ) add_library(bzip2 INTERFACE) -add_dependencies(bzip2 bzip2_) +add_dependencies(bzip2 bzip2-) target_include_directories(bzip2 INTERFACE ${INSTALL_DIR}/include) -target_link_libraries(bzip2 INTERFACE ${INSTALL_DIR}/lib/libbz2.a) +target_link_libraries(bzip2 INTERFACE ${BZIP_LIBRARIES}) diff --git a/third_party/lzma/CMakeLists.txt b/third_party/lzma/CMakeLists.txt index ee89e67816..8da79c5af2 100644 --- a/third_party/lzma/CMakeLists.txt +++ b/third_party/lzma/CMakeLists.txt @@ -10,15 +10,22 @@ set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) set(envs "PKG_CONFIG_PATH=${INSTALL_DIR}/lib/pkgconfig" "LDFLAGS=-L${INSTALL_DIR}/lib $ENV{LDFLAGS}" - "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden $ENV{CFLAGS}" + "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden -fPIC $ENV{CFLAGS}" ) -ExternalProject_Add(lzma_ +set( + LZMA_LIBRARIES + ${INSTALL_DIR}/lib/liblzma.a + ) + +ExternalProject_Add(lzma- PREFIX ${CMAKE_CURRENT_BINARY_DIR} DOWNLOAD_DIR ${ARCHIVE_DIR} URL https://tukaani.org/xz/xz-5.2.5.tar.gz URL_HASH SHA256=f6f4910fd033078738bd82bfba4f49219d03b17eb0794eb91efbae419f4aba10 - CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/lzma_/configure --prefix=${INSTALL_DIR} --disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo --disable-lzma-links --disable-scripts --disable-doc --enable-static --disable-shared + BUILD_BYPRODUCTS ${LZMA_LIBRARIES} + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/lzma-/configure --prefix=${INSTALL_DIR} --disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo --disable-lzma-links --disable-scripts --disable-doc --enable-static --disable-shared + BUILD_COMMAND ${CMAKE_COMMAND} -E env ${envs} make VERBOSE=1 DOWNLOAD_NO_PROGRESS ON LOG_DOWNLOAD ON LOG_UPDATE ON @@ -31,6 +38,6 @@ ExternalProject_Add(lzma_ add_library(lzma INTERFACE) -add_dependencies(lzma lzma_) +add_dependencies(lzma lzma-) target_include_directories(lzma INTERFACE ${INSTALL_DIR}/include) -target_link_libraries(lzma INTERFACE ${INSTALL_DIR}/lib/liblzma.a) +target_link_libraries(lzma INTERFACE ${LZMA_LIBRARIES}) diff --git a/third_party/zlib/CMakeLists.txt b/third_party/zlib/CMakeLists.txt index aebbec84e4..c305edc161 100644 --- a/third_party/zlib/CMakeLists.txt +++ b/third_party/zlib/CMakeLists.txt @@ -10,17 +10,23 @@ set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) set(envs "PKG_CONFIG_PATH=${INSTALL_DIR}/lib/pkgconfig" "LDFLAGS=-L${INSTALL_DIR}/lib $ENV{LDFLAGS}" - "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden $ENV{CFLAGS}" + "CFLAGS=-I${INSTALL_DIR}/include -fvisibility=hidden -fPIC $ENV{CFLAGS}" "prefix=${INSTALL_DIR}" ) -ExternalProject_Add(zlib_ +set( + ZLIB_LIBRARIES + ${INSTALL_DIR}/lib/libz.a + ) + +ExternalProject_Add(zlib- PREFIX ${CMAKE_CURRENT_BINARY_DIR} DOWNLOAD_DIR ${ARCHIVE_DIR} URL https://zlib.net/zlib-1.2.11.tar.gz URL_HASH SHA256=c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 - CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/zlib_/configure --static - DOWNLOAD_NO_PROGRESS ON + BUILD_BYPRODUCTS ${ZLIB_LIBRARIES} + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${envs} ${CMAKE_CURRENT_BINARY_DIR}/src/zlib-/configure --static + BUILD_COMMAND ${CMAKE_COMMAND} -E env ${envs} make VERBOSE=1 LOG_DOWNLOAD ON LOG_UPDATE ON LOG_CONFIGURE ON @@ -31,6 +37,6 @@ ExternalProject_Add(zlib_ ) add_library(zlib INTERFACE) -add_dependencies(zlib zlib_) +add_dependencies(zlib zlib-) target_include_directories(zlib INTERFACE ${INSTALL_DIR}/include) -target_link_libraries(zlib INTERFACE ${INSTALL_DIR}/lib/libz.a) +target_link_libraries(zlib INTERFACE ${ZLIB_LIBRARIES}) diff --git a/tools/setup_helpers/extension.py b/tools/setup_helpers/extension.py index fe4331cf93..a9d8592d4d 100644 --- a/tools/setup_helpers/extension.py +++ b/tools/setup_helpers/extension.py @@ -37,7 +37,7 @@ def _get_build(var, default=False): _BUILD_SOX = False if platform.system() == 'Windows' else _get_build("BUILD_SOX", True) _BUILD_KALDI = False if platform.system() == 'Windows' else _get_build("BUILD_KALDI", True) _BUILD_RNNT = _get_build("BUILD_RNNT", True) -_BUILD_KENLM = False if platform.system() == 'Windows' else _get_build("BUILD_KENLM", True) +_BUILD_CTC_DECODER = False if platform.system() == 'Windows' else _get_build("BUILD_CTC_DECODER", True) _USE_ROCM = _get_build("USE_ROCM", torch.cuda.is_available() and torch.version.hip is not None) _USE_CUDA = _get_build("USE_CUDA", torch.cuda.is_available() and torch.version.hip is None) _USE_OPENMP = _get_build("USE_OPENMP", True) and \ @@ -46,10 +46,16 @@ def _get_build(var, default=False): def get_ext_modules(): - return [ + modules = [ Extension(name='torchaudio.lib.libtorchaudio', sources=[]), Extension(name='torchaudio._torchaudio', sources=[]), ] + if _BUILD_CTC_DECODER: + modules.extend([ + Extension(name='torchaudio.lib.libtorchaudio_decoder', sources=[]), + Extension(name='torchaudio._torchaudio_decoder', sources=[]), + ]) + return modules # Based off of @@ -90,7 +96,7 @@ def build_extension(self, ext): f"-DBUILD_SOX:BOOL={'ON' if _BUILD_SOX else 'OFF'}", f"-DBUILD_KALDI:BOOL={'ON' if _BUILD_KALDI else 'OFF'}", f"-DBUILD_RNNT:BOOL={'ON' if _BUILD_RNNT else 'OFF'}", - f"-DBUILD_KENLM:BOOL={'ON' if _BUILD_KENLM else 'OFF'}", + f"-DBUILD_CTC_DECODER:BOOL={'ON' if _BUILD_CTC_DECODER else 'OFF'}", "-DBUILD_TORCHAUDIO_PYTHON_EXTENSION:BOOL=ON", f"-DUSE_ROCM:BOOL={'ON' if _USE_ROCM else 'OFF'}", f"-DUSE_CUDA:BOOL={'ON' if _USE_CUDA else 'OFF'}", diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index d4c7b62d71..9c58b08b43 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -126,6 +126,51 @@ define_library( "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" ) + +################################################################################ +# libtorchaudio_decoder.so +################################################################################ +if (BUILD_CTC_DECODER) + set( + LIBTORCHAUDIO_DECODER_SOURCES + decoder/src/decoder/Decoder.h + decoder/src/decoder/LexiconDecoder.cpp + decoder/src/decoder/LexiconDecoder.h + decoder/src/decoder/Trie.cpp + decoder/src/decoder/Trie.h + decoder/src/decoder/Utils.cpp + decoder/src/decoder/Utils.h + decoder/src/decoder/lm/KenLM.cpp + decoder/src/decoder/lm/KenLM.h + decoder/src/decoder/lm/LM.h + decoder/src/dictionary/Defines.h + decoder/src/dictionary/Dictionary.cpp + decoder/src/dictionary/Dictionary.h + decoder/src/dictionary/String.cpp + decoder/src/dictionary/String.h + decoder/src/dictionary/System.cpp + decoder/src/dictionary/System.h + decoder/src/dictionary/Utils.cpp + decoder/src/dictionary/Utils.h + ) + set( + LIBTORCHAUDIO_DECODER_DEFINITIONS + BUILD_CTC_DECODER + ) + set( + LIBTORCHAUDIO_DECODER_DEPS + libtorchaudio + kenlm) + define_library( + libtorchaudio_decoder + "${LIBTORCHAUDIO_DECODER_SOURCES}" + "${PROJECT_SOURCE_DIR}" + "${LIBTORCHAUDIO_DECODER_DEPS}" + "${LIBTORCHAUDIO_COMPILE_DEFINITIONS};${LIBTORCHAUDIO_DECODER_DEFINITIONS}" + ) +endif() + +# TODO: Add libtorchaudio_decoder if (APPLE) set(TORCHAUDIO_LIBRARY libtorchaudio CACHE INTERNAL "") else() @@ -195,4 +240,16 @@ if (BUILD_TORCHAUDIO_PYTHON_EXTENSION) libtorchaudio "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" ) + if(BUILD_CTC_DECODER) + set( + DECODER_EXTENSION_SOURCES + decoder/bindings/pybind.cpp + ) + define_extension( + _torchaudio_decoder + "${DECODER_EXTENSION_SOURCES}" + "libtorchaudio;libtorchaudio_decoder" + "${LIBTORCHAUDIO_DECODER_DEFINITIONS}" + ) + endif() endif() diff --git a/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake b/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake deleted file mode 100755 index d891b838ac..0000000000 --- a/torchaudio/csrc/decoder/bindings/cmake/Buildpybind11.cmake +++ /dev/null @@ -1,19 +0,0 @@ -include(ExternalProject) - -set(pybind11_URL https://github.com/pybind/pybind11.git) -set(pybind11_TAG 9a19306fbf30642ca331d0ec88e7da54a96860f9) # release 2.2.4 - -# Download pybind11 -ExternalProject_Add( - pybind11 - PREFIX pybind11 - GIT_REPOSITORY ${pybind11_URL} - GIT_TAG ${pybind11_TAG} - BUILD_IN_SOURCE 0 - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" -) - -ExternalProject_Get_Property(pybind11 SOURCE_DIR) -set(pybind11_INCLUDE_DIR "${SOURCE_DIR}/include") \ No newline at end of file diff --git a/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake b/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake deleted file mode 100644 index 919a384ea4..0000000000 --- a/torchaudio/csrc/decoder/bindings/cmake/FindPythonLibsNew.cmake +++ /dev/null @@ -1,198 +0,0 @@ -# - Find python libraries -# This module finds the libraries corresponding to the Python interpreter -# FindPythonInterp provides. -# This code sets the following variables: -# -# PYTHONLIBS_FOUND - have the Python libs been found -# PYTHON_PREFIX - path to the Python installation -# PYTHON_LIBRARIES - path to the python library -# PYTHON_INCLUDE_DIRS - path to where Python.h is found -# PYTHON_MODULE_EXTENSION - lib extension, e.g. '.so' or '.pyd' -# PYTHON_MODULE_PREFIX - lib name prefix: usually an empty string -# PYTHON_SITE_PACKAGES - path to installation site-packages -# PYTHON_IS_DEBUG - whether the Python interpreter is a debug build -# -# Thanks to talljimbo for the patch adding the 'LDVERSION' config -# variable usage. - -#============================================================================= -# Copyright 2001-2009 Kitware, Inc. -# Copyright 2012 Continuum Analytics, Inc. -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the names of Kitware, Inc., the Insight Software Consortium, -# nor the names of their contributors may be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#============================================================================= - -# Checking for the extension makes sure that `LibsNew` was found and not just `Libs`. -if(PYTHONLIBS_FOUND AND PYTHON_MODULE_EXTENSION) - return() -endif() - -# Use the Python interpreter to find the libs. -if(PythonLibsNew_FIND_REQUIRED) - find_package(PythonInterp ${PythonLibsNew_FIND_VERSION} REQUIRED) -else() - find_package(PythonInterp ${PythonLibsNew_FIND_VERSION}) -endif() - -if(NOT PYTHONINTERP_FOUND) - set(PYTHONLIBS_FOUND FALSE) - return() -endif() - -# According to http://stackoverflow.com/questions/646518/python-how-to-detect-debug-interpreter -# testing whether sys has the gettotalrefcount function is a reliable, cross-platform -# way to detect a CPython debug interpreter. -# -# The library suffix is from the config var LDVERSION sometimes, otherwise -# VERSION. VERSION will typically be like "2.7" on unix, and "27" on windows. -execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" - "from distutils import sysconfig as s;import sys;import struct; -print('.'.join(str(v) for v in sys.version_info)); -print(sys.prefix); -print(s.get_python_inc(plat_specific=True)); -print(s.get_python_lib(plat_specific=True)); -print(s.get_config_var('SO')); -print(hasattr(sys, 'gettotalrefcount')+0); -print(struct.calcsize('@P')); -print(s.get_config_var('LDVERSION') or s.get_config_var('VERSION')); -print(s.get_config_var('LIBDIR') or ''); -print(s.get_config_var('MULTIARCH') or ''); -" - RESULT_VARIABLE _PYTHON_SUCCESS - OUTPUT_VARIABLE _PYTHON_VALUES - ERROR_VARIABLE _PYTHON_ERROR_VALUE) - -if(NOT _PYTHON_SUCCESS MATCHES 0) - if(PythonLibsNew_FIND_REQUIRED) - message(FATAL_ERROR - "Python config failure:\n${_PYTHON_ERROR_VALUE}") - endif() - set(PYTHONLIBS_FOUND FALSE) - return() -endif() - -# Convert the process output into a list -if(WIN32) - string(REGEX REPLACE "\\\\" "/" _PYTHON_VALUES ${_PYTHON_VALUES}) -endif() -string(REGEX REPLACE ";" "\\\\;" _PYTHON_VALUES ${_PYTHON_VALUES}) -string(REGEX REPLACE "\n" ";" _PYTHON_VALUES ${_PYTHON_VALUES}) -list(GET _PYTHON_VALUES 0 _PYTHON_VERSION_LIST) -list(GET _PYTHON_VALUES 1 PYTHON_PREFIX) -list(GET _PYTHON_VALUES 2 PYTHON_INCLUDE_DIR) -list(GET _PYTHON_VALUES 3 PYTHON_SITE_PACKAGES) -list(GET _PYTHON_VALUES 4 PYTHON_MODULE_EXTENSION) -list(GET _PYTHON_VALUES 5 PYTHON_IS_DEBUG) -list(GET _PYTHON_VALUES 6 PYTHON_SIZEOF_VOID_P) -list(GET _PYTHON_VALUES 7 PYTHON_LIBRARY_SUFFIX) -list(GET _PYTHON_VALUES 8 PYTHON_LIBDIR) -list(GET _PYTHON_VALUES 9 PYTHON_MULTIARCH) - -# Make sure the Python has the same pointer-size as the chosen compiler -# Skip if CMAKE_SIZEOF_VOID_P is not defined -if(CMAKE_SIZEOF_VOID_P AND (NOT "${PYTHON_SIZEOF_VOID_P}" STREQUAL "${CMAKE_SIZEOF_VOID_P}")) - if(PythonLibsNew_FIND_REQUIRED) - math(EXPR _PYTHON_BITS "${PYTHON_SIZEOF_VOID_P} * 8") - math(EXPR _CMAKE_BITS "${CMAKE_SIZEOF_VOID_P} * 8") - message(FATAL_ERROR - "Python config failure: Python is ${_PYTHON_BITS}-bit, " - "chosen compiler is ${_CMAKE_BITS}-bit") - endif() - set(PYTHONLIBS_FOUND FALSE) - return() -endif() - -# The built-in FindPython didn't always give the version numbers -string(REGEX REPLACE "\\." ";" _PYTHON_VERSION_LIST ${_PYTHON_VERSION_LIST}) -list(GET _PYTHON_VERSION_LIST 0 PYTHON_VERSION_MAJOR) -list(GET _PYTHON_VERSION_LIST 1 PYTHON_VERSION_MINOR) -list(GET _PYTHON_VERSION_LIST 2 PYTHON_VERSION_PATCH) - -# Make sure all directory separators are '/' -string(REGEX REPLACE "\\\\" "/" PYTHON_PREFIX ${PYTHON_PREFIX}) -string(REGEX REPLACE "\\\\" "/" PYTHON_INCLUDE_DIR ${PYTHON_INCLUDE_DIR}) -string(REGEX REPLACE "\\\\" "/" PYTHON_SITE_PACKAGES ${PYTHON_SITE_PACKAGES}) - -if(CMAKE_HOST_WIN32) - set(PYTHON_LIBRARY - "${PYTHON_PREFIX}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") - - # when run in a venv, PYTHON_PREFIX points to it. But the libraries remain in the - # original python installation. They may be found relative to PYTHON_INCLUDE_DIR. - if(NOT EXISTS "${PYTHON_LIBRARY}") - get_filename_component(_PYTHON_ROOT ${PYTHON_INCLUDE_DIR} DIRECTORY) - set(PYTHON_LIBRARY - "${_PYTHON_ROOT}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") - endif() - - # raise an error if the python libs are still not found. - if(NOT EXISTS "${PYTHON_LIBRARY}") - message(FATAL_ERROR "Python libraries not found") - endif() - -else() - if(PYTHON_MULTIARCH) - set(_PYTHON_LIBS_SEARCH "${PYTHON_LIBDIR}/${PYTHON_MULTIARCH}" "${PYTHON_LIBDIR}") - else() - set(_PYTHON_LIBS_SEARCH "${PYTHON_LIBDIR}") - endif() - #message(STATUS "Searching for Python libs in ${_PYTHON_LIBS_SEARCH}") - # Probably this needs to be more involved. It would be nice if the config - # information the python interpreter itself gave us were more complete. - find_library(PYTHON_LIBRARY - NAMES "python${PYTHON_LIBRARY_SUFFIX}" - PATHS ${_PYTHON_LIBS_SEARCH} - NO_DEFAULT_PATH) - - # If all else fails, just set the name/version and let the linker figure out the path. - if(NOT PYTHON_LIBRARY) - set(PYTHON_LIBRARY python${PYTHON_LIBRARY_SUFFIX}) - endif() -endif() - -MARK_AS_ADVANCED( - PYTHON_LIBRARY - PYTHON_INCLUDE_DIR -) - -# We use PYTHON_INCLUDE_DIR, PYTHON_LIBRARY and PYTHON_DEBUG_LIBRARY for the -# cache entries because they are meant to specify the location of a single -# library. We now set the variables listed by the documentation for this -# module. -SET(PYTHON_INCLUDE_DIRS "${PYTHON_INCLUDE_DIR}") -SET(PYTHON_LIBRARIES "${PYTHON_LIBRARY}") -SET(PYTHON_DEBUG_LIBRARIES "${PYTHON_DEBUG_LIBRARY}") - -find_package_message(PYTHON - "Found PythonLibs: ${PYTHON_LIBRARY}" - "${PYTHON_EXECUTABLE}${PYTHON_VERSION}") - -set(PYTHONLIBS_FOUND TRUE) diff --git a/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake b/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake deleted file mode 100755 index 0fdacc78d8..0000000000 --- a/torchaudio/csrc/decoder/bindings/cmake/pybind11Tools.cmake +++ /dev/null @@ -1,224 +0,0 @@ -# tools/pybind11Tools.cmake -- Build system for the pybind11 modules -# -# Copyright (c) 2015 Wenzel Jakob -# -# All rights reserved. Use of this source code is governed by a -# BSD-style license that can be found in the LICENSE file. - -cmake_minimum_required(VERSION 2.8.12) - -# Add a CMake parameter for choosing a desired Python version -if(NOT PYBIND11_PYTHON_VERSION) - set(PYBIND11_PYTHON_VERSION "" CACHE STRING "Python version to use for compiling modules") -endif() - -set(Python_ADDITIONAL_VERSIONS 3.7 3.6 3.5 3.4) -find_package(PythonLibsNew ${PYBIND11_PYTHON_VERSION} REQUIRED) - -include(CheckCXXCompilerFlag) -include(CMakeParseArguments) - -if(NOT PYBIND11_CPP_STANDARD AND NOT CMAKE_CXX_STANDARD) - if(NOT MSVC) - check_cxx_compiler_flag("-std=c++14" HAS_CPP14_FLAG) - - if (HAS_CPP14_FLAG) - set(PYBIND11_CPP_STANDARD -std=c++14) - else() - check_cxx_compiler_flag("-std=c++11" HAS_CPP11_FLAG) - if (HAS_CPP11_FLAG) - set(PYBIND11_CPP_STANDARD -std=c++11) - else() - message(FATAL_ERROR "Unsupported compiler -- pybind11 requires C++11 support!") - endif() - endif() - elseif(MSVC) - set(PYBIND11_CPP_STANDARD /std:c++14) - endif() - - set(PYBIND11_CPP_STANDARD ${PYBIND11_CPP_STANDARD} CACHE STRING - "C++ standard flag, e.g. -std=c++11, -std=c++14, /std:c++14. Defaults to C++14 mode." FORCE) -endif() - -# Checks whether the given CXX/linker flags can compile and link a cxx file. cxxflags and -# linkerflags are lists of flags to use. The result variable is a unique variable name for each set -# of flags: the compilation result will be cached base on the result variable. If the flags work, -# sets them in cxxflags_out/linkerflags_out internal cache variables (in addition to ${result}). -function(_pybind11_return_if_cxx_and_linker_flags_work result cxxflags linkerflags cxxflags_out linkerflags_out) - set(CMAKE_REQUIRED_LIBRARIES ${linkerflags}) - check_cxx_compiler_flag("${cxxflags}" ${result}) - if (${result}) - set(${cxxflags_out} "${cxxflags}" CACHE INTERNAL "" FORCE) - set(${linkerflags_out} "${linkerflags}" CACHE INTERNAL "" FORCE) - endif() -endfunction() - -# Internal: find the appropriate link time optimization flags for this compiler -function(_pybind11_add_lto_flags target_name prefer_thin_lto) - if (NOT DEFINED PYBIND11_LTO_CXX_FLAGS) - set(PYBIND11_LTO_CXX_FLAGS "" CACHE INTERNAL "") - set(PYBIND11_LTO_LINKER_FLAGS "" CACHE INTERNAL "") - - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - set(cxx_append "") - set(linker_append "") - if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT APPLE) - # Clang Gold plugin does not support -Os; append -O3 to MinSizeRel builds to override it - set(linker_append ";$<$:-O3>") - elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - set(cxx_append ";-fno-fat-lto-objects") - endif() - - if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND prefer_thin_lto) - _pybind11_return_if_cxx_and_linker_flags_work(HAS_FLTO_THIN - "-flto=thin${cxx_append}" "-flto=thin${linker_append}" - PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) - endif() - - if (NOT HAS_FLTO_THIN) - _pybind11_return_if_cxx_and_linker_flags_work(HAS_FLTO - "-flto${cxx_append}" "-flto${linker_append}" - PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) - endif() - elseif (CMAKE_CXX_COMPILER_ID MATCHES "Intel") - # Intel equivalent to LTO is called IPO - _pybind11_return_if_cxx_and_linker_flags_work(HAS_INTEL_IPO - "-ipo" "-ipo" PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) - elseif(MSVC) - # cmake only interprets libraries as linker flags when they start with a - (otherwise it - # converts /LTCG to \LTCG as if it was a Windows path). Luckily MSVC supports passing flags - # with - instead of /, even if it is a bit non-standard: - _pybind11_return_if_cxx_and_linker_flags_work(HAS_MSVC_GL_LTCG - "/GL" "-LTCG" PYBIND11_LTO_CXX_FLAGS PYBIND11_LTO_LINKER_FLAGS) - endif() - - if (PYBIND11_LTO_CXX_FLAGS) - message(STATUS "LTO enabled") - else() - message(STATUS "LTO disabled (not supported by the compiler and/or linker)") - endif() - endif() - - # Enable LTO flags if found, except for Debug builds - if (PYBIND11_LTO_CXX_FLAGS) - target_compile_options(${target_name} PRIVATE "$<$>:${PYBIND11_LTO_CXX_FLAGS}>") - endif() - if (PYBIND11_LTO_LINKER_FLAGS) - target_link_libraries(${target_name} PRIVATE "$<$>:${PYBIND11_LTO_LINKER_FLAGS}>") - endif() -endfunction() - -# Build a Python extension module: -# pybind11_add_module( [MODULE | SHARED] [EXCLUDE_FROM_ALL] -# [NO_EXTRAS] [SYSTEM] [THIN_LTO] source1 [source2 ...]) -# -function(pybind11_add_module target_name) - set(options MODULE SHARED EXCLUDE_FROM_ALL NO_EXTRAS SYSTEM THIN_LTO) - cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) - - if(ARG_MODULE AND ARG_SHARED) - message(FATAL_ERROR "Can't be both MODULE and SHARED") - elseif(ARG_SHARED) - set(lib_type SHARED) - else() - set(lib_type MODULE) - endif() - - if(ARG_EXCLUDE_FROM_ALL) - set(exclude_from_all EXCLUDE_FROM_ALL) - endif() - - add_library(${target_name} ${lib_type} ${exclude_from_all} ${ARG_UNPARSED_ARGUMENTS}) - message("ADDING LIBRARY ${target_name}") - - if(ARG_SYSTEM) - set(inc_isystem SYSTEM) - endif() - - target_include_directories(${target_name} ${inc_isystem} - PRIVATE ${PYBIND11_INCLUDE_DIR} # from project CMakeLists.txt - PRIVATE ${pybind11_INCLUDE_DIR} # from pybind11Config - PRIVATE ${PYTHON_INCLUDE_DIRS}) - - # Python debug libraries expose slightly different objects - # https://docs.python.org/3.6/c-api/intro.html#debugging-builds - # https://stackoverflow.com/questions/39161202/how-to-work-around-missing-pymodule-create2-in-amd64-win-python35-d-lib - if(PYTHON_IS_DEBUG) - target_compile_definitions(${target_name} PRIVATE Py_DEBUG) - endif() - - # The prefix and extension are provided by FindPythonLibsNew.cmake - set_target_properties(${target_name} PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}") - set_target_properties(${target_name} PROPERTIES SUFFIX "${PYTHON_MODULE_EXTENSION}") - - # -fvisibility=hidden is required to allow multiple modules compiled against - # different pybind versions to work properly, and for some features (e.g. - # py::module_local). We force it on everything inside the `pybind11` - # namespace; also turning it on for a pybind module compilation here avoids - # potential warnings or issues from having mixed hidden/non-hidden types. - set_target_properties(${target_name} PROPERTIES CXX_VISIBILITY_PRESET "hidden") - set_target_properties(${target_name} PROPERTIES CUDA_VISIBILITY_PRESET "hidden") - - if(WIN32 OR CYGWIN) - # Link against the Python shared library on Windows - target_link_libraries(${target_name} PRIVATE ${PYTHON_LIBRARIES}) - elseif(APPLE) - # It's quite common to have multiple copies of the same Python version - # installed on one's system. E.g.: one copy from the OS and another copy - # that's statically linked into an application like Blender or Maya. - # If we link our plugin library against the OS Python here and import it - # into Blender or Maya later on, this will cause segfaults when multiple - # conflicting Python instances are active at the same time (even when they - # are of the same version). - - # Windows is not affected by this issue since it handles DLL imports - # differently. The solution for Linux and Mac OS is simple: we just don't - # link against the Python library. The resulting shared library will have - # missing symbols, but that's perfectly fine -- they will be resolved at - # import time. - - target_link_libraries(${target_name} PRIVATE "-undefined dynamic_lookup") - - if(ARG_SHARED) - # Suppress CMake >= 3.0 warning for shared libraries - set_target_properties(${target_name} PROPERTIES MACOSX_RPATH ON) - endif() - endif() - - # Make sure C++11/14 are enabled - target_compile_options(${target_name} PUBLIC ${PYBIND11_CPP_STANDARD}) - - if(ARG_NO_EXTRAS) - return() - endif() - - _pybind11_add_lto_flags(${target_name} ${ARG_THIN_LTO}) - - if (NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug) - # Strip unnecessary sections of the binary on Linux/Mac OS - if(CMAKE_STRIP) - if(APPLE) - add_custom_command(TARGET ${target_name} POST_BUILD - COMMAND ${CMAKE_STRIP} -x $) - else() - add_custom_command(TARGET ${target_name} POST_BUILD - COMMAND ${CMAKE_STRIP} $) - endif() - endif() - endif() - - if(MSVC) - # /MP enables multithreaded builds (relevant when there are many files), /bigobj is - # needed for bigger binding projects due to the limit to 64k addressable sections - target_compile_options(${target_name} PRIVATE /bigobj) - if(CMAKE_VERSION VERSION_LESS 3.11) - target_compile_options(${target_name} PRIVATE $<$>:/MP>) - else() - # Only set these options for C++ files. This is important so that, for - # instance, projects that include other types of source files like CUDA - # .cu files don't get these options propagated to nvcc since that would - # cause the build to fail. - target_compile_options(${target_name} PRIVATE $<$>:$<$:/MP>>) - endif() - endif() -endfunction() \ No newline at end of file diff --git a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp index 6d7bc52834..591a27da01 100644 --- a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp +++ b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp @@ -9,7 +9,7 @@ #include -#include "kenlm/lm/model.hh" +#include "lm/model.hh" namespace torchaudio { namespace lib { From db5ac7ded3902653417263afe2e1700fd0299a20 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Mon, 20 Dec 2021 06:26:11 -0800 Subject: [PATCH 0041/1144] Remove unnecessary sources from KenLM build (#2085) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2085 Reviewed By: carolineechen Differential Revision: D33235225 Pulled By: mthrok fbshipit-source-id: 47fe9ec4c93a26322b3a362202ddd3c4654c3f8c --- third_party/kenlm/CMakeLists.txt | 9 --------- torchaudio/csrc/CMakeLists.txt | 11 ----------- 2 files changed, 20 deletions(-) diff --git a/third_party/kenlm/CMakeLists.txt b/third_party/kenlm/CMakeLists.txt index 00aacf27db..e26e0451ea 100644 --- a/third_party/kenlm/CMakeLists.txt +++ b/third_party/kenlm/CMakeLists.txt @@ -17,19 +17,11 @@ set( submodule/util/integer_to_string.cc submodule/util/mmap.cc submodule/util/murmur_hash.cc - submodule/util/parallel_read.cc submodule/util/pool.cc submodule/util/read_compressed.cc submodule/util/scoped.cc submodule/util/spaces.cc - submodule/util/stream/chain.cc - submodule/util/stream/count_records.cc - submodule/util/stream/io.cc - submodule/util/stream/line_input.cc - submodule/util/stream/multi_progress.cc - submodule/util/stream/rewindable_stream.cc submodule/util/string_piece.cc - submodule/util/usage.cc ) set( @@ -43,7 +35,6 @@ set( submodule/lm/read_arpa.cc submodule/lm/search_hashed.cc submodule/lm/search_trie.cc - submodule/lm/sizes.cc submodule/lm/trie.cc submodule/lm/trie_sort.cc submodule/lm/value_build.cc diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index 9c58b08b43..9acdc0fe07 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -133,25 +133,14 @@ define_library( if (BUILD_CTC_DECODER) set( LIBTORCHAUDIO_DECODER_SOURCES - decoder/src/decoder/Decoder.h decoder/src/decoder/LexiconDecoder.cpp - decoder/src/decoder/LexiconDecoder.h decoder/src/decoder/Trie.cpp - decoder/src/decoder/Trie.h decoder/src/decoder/Utils.cpp - decoder/src/decoder/Utils.h decoder/src/decoder/lm/KenLM.cpp - decoder/src/decoder/lm/KenLM.h - decoder/src/decoder/lm/LM.h - decoder/src/dictionary/Defines.h decoder/src/dictionary/Dictionary.cpp - decoder/src/dictionary/Dictionary.h decoder/src/dictionary/String.cpp - decoder/src/dictionary/String.h decoder/src/dictionary/System.cpp - decoder/src/dictionary/System.h decoder/src/dictionary/Utils.cpp - decoder/src/dictionary/Utils.h ) set( LIBTORCHAUDIO_DECODER_DEFINITIONS From f3f23e42cac030576d6c1d367530467f55b37b42 Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Mon, 20 Dec 2021 08:55:57 -0800 Subject: [PATCH 0042/1144] Update URLs for libritts (#2074) Summary: The urls for this dataset seem to have changed so I am updating to the new location Pull Request resolved: https://github.com/pytorch/audio/pull/2074 Reviewed By: mthrok Differential Revision: D33234996 Pulled By: jdsgomes fbshipit-source-id: e09c35a122e8227fcce7fa97aeeeea312cb89173 --- torchaudio/datasets/libritts.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/torchaudio/datasets/libritts.py b/torchaudio/datasets/libritts.py index 2c978c426e..62736d3313 100644 --- a/torchaudio/datasets/libritts.py +++ b/torchaudio/datasets/libritts.py @@ -13,13 +13,13 @@ URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriTTS" _CHECKSUMS = { - "http://www.openslr.org/60/dev-clean.tar.gz": "0c3076c1e5245bb3f0af7d82087ee207", - "http://www.openslr.org/60/dev-other.tar.gz": "815555d8d75995782ac3ccd7f047213d", - "http://www.openslr.org/60/test-clean.tar.gz": "7bed3bdb047c4c197f1ad3bc412db59f", - "http://www.openslr.org/60/test-other.tar.gz": "ae3258249472a13b5abef2a816f733e4", - "http://www.openslr.org/60/train-clean-100.tar.gz": "4a8c202b78fe1bc0c47916a98f3a2ea8", - "http://www.openslr.org/60/train-clean-360.tar.gz": "a84ef10ddade5fd25df69596a2767b2d", - "http://www.openslr.org/60/train-other-500.tar.gz": "7b181dd5ace343a5f38427999684aa6f", + "http://www.openslr.org/resources/60/dev-clean.tar.gz": "0c3076c1e5245bb3f0af7d82087ee207", + "http://www.openslr.org/resources/60/dev-other.tar.gz": "815555d8d75995782ac3ccd7f047213d", + "http://www.openslr.org/resources/60/test-clean.tar.gz": "7bed3bdb047c4c197f1ad3bc412db59f", + "http://www.openslr.org/resources/60/test-other.tar.gz": "ae3258249472a13b5abef2a816f733e4", + "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "4a8c202b78fe1bc0c47916a98f3a2ea8", + "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "a84ef10ddade5fd25df69596a2767b2d", + "http://www.openslr.org/resources/60/train-other-500.tar.gz": "7b181dd5ace343a5f38427999684aa6f", } From 2476dd2da27395bfeb7f909492b6f0920cfb6164 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Mon, 20 Dec 2021 10:43:23 -0800 Subject: [PATCH 0043/1144] Standardize the location of third-party source code (#2086) Summary: Previously sox-related third-party source code was archived at `third_party/sox/archives`. Recently KenLM-related third-party source code was added and they are archived at `third_party/archives`. This PR changes the sox archive location to `third_party/archives`, so that all the archvies are cached at the same location. Pull Request resolved: https://github.com/pytorch/audio/pull/2086 Reviewed By: carolineechen Differential Revision: D33236927 Pulled By: mthrok fbshipit-source-id: 2f2aa5f4b386fefb46d7c98f7179c04995219f3c --- .circleci/config.yml | 4 ++-- .circleci/config.yml.in | 4 ++-- .gitignore | 1 - setup.py | 17 +++-------------- third_party/sox/CMakeLists.txt | 2 +- 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e6d35351a8..2286325042 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -145,8 +145,8 @@ jobs: - run: command: | - mkdir -p third_party/sox/archives/ - wget --no-clobber --directory-prefix=third_party/sox/archives/ $(awk '/URL /{print $2}' third_party/sox/CMakeLists.txt) + mkdir -p third_party/archives/ + wget --no-clobber --directory-prefix=third_party/archives/ $(awk '/URL /{print $2}' third_party/*/CMakeLists.txt) - save_cache: key: tp-nix-v2-{{ checksum ".cachekey" }} diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 5c1a770c38..cdc076c4d4 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -145,8 +145,8 @@ jobs: {% endraw %} - run: command: | - mkdir -p third_party/sox/archives/ - wget --no-clobber --directory-prefix=third_party/sox/archives/ $(awk '/URL /{print $2}' third_party/sox/CMakeLists.txt) + mkdir -p third_party/archives/ + wget --no-clobber --directory-prefix=third_party/archives/ $(awk '/URL /{print $2}' third_party/*/CMakeLists.txt) - save_cache: {% raw %} key: tp-nix-v2-{{ checksum ".cachekey" }} diff --git a/.gitignore b/.gitignore index 3d7f0c9e30..248c95b5b9 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,3 @@ examples/tutorials/_assets # third parties third_party/install/ third_party/archives/ -third_party/sox/archives/ diff --git a/setup.py b/setup.py index cea4ee9b24..324a618a98 100644 --- a/setup.py +++ b/setup.py @@ -103,19 +103,9 @@ def _parse_url(path): yield url -def _parse_sox_sources(): - sox_dir = ROOT_DIR / 'third_party' / 'sox' - cmake_file = sox_dir / 'CMakeLists.txt' - archive_dir = sox_dir / 'archives' - archive_dir.mkdir(exist_ok=True) - for url in _parse_url(cmake_file): - path = archive_dir / os.path.basename(url) - yield path, url - - -def _parse_kenlm_sources(): +def _parse_sources(): third_party_dir = ROOT_DIR / 'third_party' - libs = ['zlib', 'bzip2', 'lzma', 'boost'] + libs = ['zlib', 'bzip2', 'lzma', 'boost', 'sox'] archive_dir = third_party_dir / 'archives' archive_dir.mkdir(exist_ok=True) for lib in libs: @@ -136,8 +126,7 @@ def _fetch_third_party_libraries(): if not (ROOT_DIR / 'third_party' / 'kaldi' / 'submodule' / 'CMakeLists.txt').exists(): _init_submodule() if os.name != 'nt': - _fetch_archives(_parse_sox_sources()) - _fetch_archives(_parse_kenlm_sources()) + _fetch_archives(_parse_sources()) def _main(): diff --git a/third_party/sox/CMakeLists.txt b/third_party/sox/CMakeLists.txt index 124d605e8e..ff35e70bff 100644 --- a/third_party/sox/CMakeLists.txt +++ b/third_party/sox/CMakeLists.txt @@ -1,7 +1,7 @@ include(ExternalProject) set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) -set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/archives) +set(ARCHIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../archives) set(COMMON_ARGS --quiet --disable-shared --enable-static --prefix=${INSTALL_DIR} --with-pic --disable-dependency-tracking --disable-debug --disable-examples --disable-doc) # To pass custom environment variables to ExternalProject_Add command, From 4554d242d4e5d294ac5008bbe7f2e98d428532ad Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:54:39 -0800 Subject: [PATCH 0044/1144] Fix load behavior for 24-bit input (#2084) Summary: ## bug description When a 24 bits-par-sample audio is loaded via file-like object, the loaded Tensor is wrong. It was fine if the audio is loaded from local file. ## The cause of the bug The core of the sox's decoding mechanism is `sox_read` function, one of which parameter is the maximum number of samples to decode from the given buffer. https://fossies.org/dox/sox-14.4.2/formats_8c.html#a2a4f0194a0f919d4f38c57b81aa2c06f)] The `sox_read` function is called in what is called `drain` effect, callback and this callback receives output buffer and its size in byte. The previous implementation passed this size value as the argument of `sox_read` for the maximum number of samples to read. Since buffer size is larger than the number of samples fit in the buffer, `sox_read` function always consumed the entire buffer. (This behavior is not wrong except when the input is 24 bits-per-sample and file-like object.) When the input is read from file-like object, inside of drain callback, new data are fetched via Python's `read` method and loaded on fixed-size memory region. The size of this memory region can be adjusted via `torchaudio.utils.sox_utils.set_buffer_size`, but the default value is 8096. If the input format is 24 bits-per-sample, the end of memory region does not necessarily correspond to the end of a valid sample. When `sox_read` consumes all the data in the buffer region, the data at the end introduces some unexpected values. This causes the aforementioned bug ## Fix Pass proper (better estimated) maximum number of samples decodable to `sox_read`. Pull Request resolved: https://github.com/pytorch/audio/pull/2084 Reviewed By: carolineechen Differential Revision: D33236947 Pulled By: mthrok fbshipit-source-id: 171d9b7945f81db54f98362a68b20f2f95bb11a4 --- .../backend/sox_io/load_test.py | 149 +++++++++--------- torchaudio/csrc/pybind/sox/effects_chain.cpp | 8 + 2 files changed, 86 insertions(+), 71 deletions(-) diff --git a/test/torchaudio_unittest/backend/sox_io/load_test.py b/test/torchaudio_unittest/backend/sox_io/load_test.py index 824d012cfd..031c0f2d0d 100644 --- a/test/torchaudio_unittest/backend/sox_io/load_test.py +++ b/test/torchaudio_unittest/backend/sox_io/load_test.py @@ -317,25 +317,26 @@ class TestFileObject(TempDirMixin, PytorchTestCase): because `load` function is rigrously tested for file path inputs to match libsox's result, """ @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_fileobj(self, ext, compression): + def test_fileobj(self, ext, kwargs): """Loading audio via file object returns the same result as via file path.""" sample_rate = 16000 format_ = ext if ext in ['mp3'] else None path = self.get_temp_path(f'test.{ext}') sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, - compression=compression) + path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) with open(path, 'rb') as fileobj: @@ -345,25 +346,26 @@ def test_fileobj(self, ext, compression): self.assertEqual(expected, found) @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_bytesio(self, ext, compression): + def test_bytesio(self, ext, kwargs): """Loading audio via BytesIO object returns the same result as via file path.""" sample_rate = 16000 format_ = ext if ext in ['mp3'] else None path = self.get_temp_path(f'test.{ext}') sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, - compression=compression) + path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) with open(path, 'rb') as file_: @@ -374,17 +376,19 @@ def test_bytesio(self, ext, compression): self.assertEqual(expected, found) @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_bytesio_clogged(self, ext, compression): + def test_bytesio_clogged(self, ext, kwargs): """Loading audio via clogged file object returns the same result as via file path. This test case validates the case where fileobject returns shorter bytes than requeted. @@ -394,8 +398,7 @@ def test_bytesio_clogged(self, ext, compression): path = self.get_temp_path(f'test.{ext}') sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, - compression=compression) + path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) with open(path, 'rb') as file_: @@ -406,17 +409,19 @@ def test_bytesio_clogged(self, ext, compression): self.assertEqual(expected, found) @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_bytesio_tiny(self, ext, compression): + def test_bytesio_tiny(self, ext, kwargs): """Loading very small audio via file object returns the same result as via file path. """ sample_rate = 16000 @@ -424,8 +429,7 @@ def test_bytesio_tiny(self, ext, compression): path = self.get_temp_path(f'test.{ext}') sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, - compression=compression, duration=1 / 1600) + path, sample_rate, num_channels=2, duration=1 / 1600, **kwargs) expected, _ = sox_io_backend.load(path) with open(path, 'rb') as file_: @@ -436,17 +440,19 @@ def test_bytesio_tiny(self, ext, compression): self.assertEqual(expected, found) @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_tarfile(self, ext, compression): + def test_tarfile(self, ext, kwargs): """Loading compressed audio via file-like object returns the same result as via file path.""" sample_rate = 16000 format_ = ext if ext in ['mp3'] else None @@ -455,8 +461,7 @@ def test_tarfile(self, ext, compression): archive_path = self.get_temp_path('archive.tar.gz') sox_utils.gen_audio_file( - audio_path, sample_rate, num_channels=2, - compression=compression) + audio_path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(audio_path) with tarfile.TarFile(archive_path, 'w') as tarobj: @@ -474,24 +479,26 @@ def test_tarfile(self, ext, compression): @skipIfNoModule("requests") class TestFileObjectHttp(HttpServerMixin, PytorchTestCase): @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), + ('wav', {'bit_depth': 16}), + ('wav', {'bit_depth': 24}), + ('wav', {'bit_depth': 32}), + ('mp3', {'compression': 128}), + ('mp3', {'compression': 320}), + ('flac', {'compression': 0}), + ('flac', {'compression': 5}), + ('flac', {'compression': 8}), + ('vorbis', {'compression': -1}), + ('vorbis', {'compression': 10}), + ('amb', {}), ]) - def test_requests(self, ext, compression): + def test_requests(self, ext, kwargs): sample_rate = 16000 format_ = ext if ext in ['mp3'] else None audio_file = f'test.{ext}' audio_path = self.get_temp_path(audio_file) sox_utils.gen_audio_file( - audio_path, sample_rate, num_channels=2, compression=compression) + audio_path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(audio_path) url = self.get_url(audio_file) diff --git a/torchaudio/csrc/pybind/sox/effects_chain.cpp b/torchaudio/csrc/pybind/sox/effects_chain.cpp index 12f8d31d66..9770028815 100644 --- a/torchaudio/csrc/pybind/sox/effects_chain.cpp +++ b/torchaudio/csrc/pybind/sox/effects_chain.cpp @@ -103,6 +103,14 @@ auto fileobj_input_drain(sox_effect_t* effp, sox_sample_t* obuf, size_t* osamp) // The following part is practically same as "input" effect // https://github.com/dmkrepo/libsox/blob/b9dd1a86e71bbd62221904e3e59dfaa9e5e72046/src/input.c#L30-L48 + // At this point, osamp represents the buffer size in bytes, + // but sox_read expects the maximum number of samples ready to read. + // Normally, this is fine, but in case when the samples are not 4-byte + // aligned, (e.g. sample is 24bits), the resulting signal is not correct. + // https://github.com/pytorch/audio/issues/2083 + if (sf->encoding.bits_per_sample > 0) + *osamp /= (sf->encoding.bits_per_sample / 8); + // Ensure that it's a multiple of the number of channels *osamp -= *osamp % effp->out_signal.channels; From 3a03d8c026475fd5f4f2d1f7eff26384e06829ec Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:18:03 -0800 Subject: [PATCH 0045/1144] Update audio augmentation tutorial (#2082) Summary: 1. Reorder Audio display so that audios are playable from browser in doc 2. Add link to function documentations https://470342-90321822-gh.circle-artifacts.com/0/docs/tutorials/audio_data_augmentation_tutorial.html Pull Request resolved: https://github.com/pytorch/audio/pull/2082 Reviewed By: carolineechen Differential Revision: D33227725 Pulled By: mthrok fbshipit-source-id: c7ee360b6f9b84c8e0a9b72193b98487d03b57ab --- .../audio_data_augmentation_tutorial.py | 222 ++++++++++++++---- 1 file changed, 171 insertions(+), 51 deletions(-) diff --git a/examples/tutorials/audio_data_augmentation_tutorial.py b/examples/tutorials/audio_data_augmentation_tutorial.py index d191b07959..4e983be4df 100644 --- a/examples/tutorials/audio_data_augmentation_tutorial.py +++ b/examples/tutorials/audio_data_augmentation_tutorial.py @@ -6,10 +6,6 @@ ``torchaudio`` provides a variety of ways to augment audio data. """ -# When running this tutorial in Google Colab, install the required packages -# with the following. -# !pip install torchaudio - import torch import torchaudio import torchaudio.functional as F @@ -157,9 +153,9 @@ def play_audio(waveform, sample_rate): num_channels, num_frames = waveform.shape if num_channels == 1: - display(Audio(waveform[0], rate=sample_rate)) + return Audio(waveform[0], rate=sample_rate) elif num_channels == 2: - display(Audio((waveform[0], waveform[1]), rate=sample_rate)) + return Audio((waveform[0], waveform[1]), rate=sample_rate) else: raise ValueError("Waveform with more than 2 channels are not supported.") @@ -182,14 +178,14 @@ def get_noise_sample(*, resample=None): # Applying effects and filtering # ------------------------------ # -# ``torchaudio.sox_effects`` allows for directly applying filters similar to +# :py:func:`torchaudio.sox_effects` allows for directly applying filters similar to # those available in ``sox`` to Tensor objects and file object audio sources. # # There are two functions for this: # -# - ``torchaudio.sox_effects.apply_effects_tensor`` for applying effects +# - :py:func:`torchaudio.sox_effects.apply_effects_tensor` for applying effects # to Tensor. -# - ``torchaudio.sox_effects.apply_effects_file`` for applying effects to +# - :py:func:`torchaudio.sox_effects.apply_effects_file` for applying effects to # other audio sources. # # Both functions accept effect definitions in the form @@ -202,11 +198,12 @@ def get_noise_sample(*, resample=None): # documentation `__. # # **Tip** If you need to load and resample your audio data on the fly, -# then you can use ``torchaudio.sox_effects.apply_effects_file`` with -# effect ``"rate"``. +# then you can use :py:func:`torchaudio.sox_effects.apply_effects_file` +# with effect ``"rate"``. # -# **Note** ``apply_effects_file`` accepts a file-like object or path-like -# object. Similar to ``torchaudio.load``, when the audio format cannot be +# **Note** :py:func:`torchaudio.sox_effects.apply_effects_file` accepts a +# file-like object or path-like object. +# Similar to :py:func:`torchaudio.load`, when the audio format cannot be # inferred from either the file extension or header, you can provide # argument ``format`` to specify the format of the audio source. # @@ -232,22 +229,36 @@ def get_noise_sample(*, resample=None): waveform1, sample_rate1, effects ) -plot_waveform(waveform1, sample_rate1, title="Original", xlim=(-0.1, 3.2)) -plot_waveform(waveform2, sample_rate2, title="Effects Applied", xlim=(-0.1, 3.2)) print_stats(waveform1, sample_rate=sample_rate1, src="Original") print_stats(waveform2, sample_rate=sample_rate2, src="Effects Applied") ###################################################################### # Note that the number of frames and number of channels are different from # those of the original after the effects are applied. Let’s listen to the -# audio. Doesn’t it sound more dramatic? +# audio. # +###################################################################### +# Original: +# ~~~~~~~~~ +# + +plot_waveform(waveform1, sample_rate1, title="Original", xlim=(-0.1, 3.2)) plot_specgram(waveform1, sample_rate1, title="Original", xlim=(0, 3.04)) play_audio(waveform1, sample_rate1) + +###################################################################### +# Effects applied: +# ~~~~~~~~~~~~~~~~ +# + +plot_waveform(waveform2, sample_rate2, title="Effects Applied", xlim=(-0.1, 3.2)) plot_specgram(waveform2, sample_rate2, title="Effects Applied", xlim=(0, 3.04)) play_audio(waveform2, sample_rate2) +###################################################################### +# Doesn’t it sound more dramatic? +# ###################################################################### # Simulating room reverberation @@ -296,12 +307,21 @@ def get_noise_sample(*, resample=None): speech_ = torch.nn.functional.pad(speech, (rir.shape[1] - 1, 0)) augmented = torch.nn.functional.conv1d(speech_[None, ...], rir[None, ...])[0] -plot_waveform(speech, sample_rate, title="Original", ylim=None) -plot_waveform(augmented, sample_rate, title="RIR Applied", ylim=None) +###################################################################### +# Original: +# ~~~~~~~~~ +# +plot_waveform(speech, sample_rate, title="Original", ylim=None) plot_specgram(speech, sample_rate, title="Original") play_audio(speech, sample_rate) +###################################################################### +# RIR applied: +# ~~~~~~~~~~~~ +# + +plot_waveform(augmented, sample_rate, title="RIR Applied", ylim=None) plot_specgram(augmented, sample_rate, title="RIR Applied") play_audio(augmented, sample_rate) @@ -315,9 +335,9 @@ def get_noise_sample(*, resample=None): # intensity of noise is changing the Signal-to-Noise Ratio (SNR). # [`wikipedia `__] # -# \begin{align}\mathrm{SNR} = \frac{P_{\mathrm{signal}}}{P_{\mathrm{noise}}}\end{align} +# $$ \\mathrm{SNR} = \\frac{P_{signal}}{P_{noise}} $$ # -# \begin{align}{\mathrm {SNR_{{dB}}}}=10\log _{{10}}\left({\mathrm {SNR}}\right)\end{align} +# $$ \\mathrm{SNR_{dB}} = 10 \\log _{{10}} \\mathrm {SNR} $$ # @@ -326,27 +346,61 @@ def get_noise_sample(*, resample=None): noise, _ = get_noise_sample(resample=sample_rate) noise = noise[:, : speech.shape[1]] -plot_waveform(noise, sample_rate, title="Background noise") -plot_specgram(noise, sample_rate, title="Background noise") -play_audio(noise, sample_rate) - speech_power = speech.norm(p=2) noise_power = noise.norm(p=2) -for snr_db in [20, 10, 3]: +snr_dbs = [20, 10, 3] +noisy_speeches = [] +for snr_db in snr_dbs: snr = math.exp(snr_db / 10) scale = snr * noise_power / speech_power - noisy_speech = (scale * speech + noise) / 2 + noisy_speeches.append((scale * speech + noise) / 2) + +###################################################################### +# Background noise: +# ~~~~~~~~~~~~~~~~~ +# + +plot_waveform(noise, sample_rate, title="Background noise") +plot_specgram(noise, sample_rate, title="Background noise") +play_audio(noise, sample_rate) + +###################################################################### +# SNR 20 dB: +# ~~~~~~~~~~ +# - plot_waveform(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") - plot_specgram(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") - play_audio(noisy_speech, sample_rate) +snr_db, noisy_speech = snr_dbs[0], noisy_speeches[0] +plot_waveform(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +plot_specgram(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +play_audio(noisy_speech, sample_rate) + +###################################################################### +# SNR 10 dB: +# ~~~~~~~~~~ +# + +snr_db, noisy_speech = snr_dbs[1], noisy_speeches[1] +plot_waveform(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +plot_specgram(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +play_audio(noisy_speech, sample_rate) + +###################################################################### +# SNR 3 dB: +# ~~~~~~~~~~ +# + +snr_db, noisy_speech = snr_dbs[2], noisy_speeches[2] +plot_waveform(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +plot_specgram(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]") +play_audio(noisy_speech, sample_rate) ###################################################################### # Applying codec to Tensor object # ------------------------------- # -# ``torchaudio.functional.apply_codec`` can apply codecs to a Tensor object. +# :py:func:`torchaudio.functional.apply_codec` can apply codecs to +# a Tensor object. # # **Note** This process is not differentiable. # @@ -355,7 +409,6 @@ def get_noise_sample(*, resample=None): waveform, sample_rate = get_speech_sample(resample=8000) plot_specgram(waveform, sample_rate, title="Original") -play_audio(waveform, sample_rate) configs = [ ({"format": "wav", "encoding": "ULAW", "bits_per_sample": 8}, "8 bit mu-law"), @@ -363,10 +416,46 @@ def get_noise_sample(*, resample=None): ({"format": "mp3", "compression": -9}, "MP3"), ({"format": "vorbis", "compression": -1}, "Vorbis"), ] +waveforms = [] for param, title in configs: augmented = F.apply_codec(waveform, sample_rate, **param) plot_specgram(augmented, sample_rate, title=title) - play_audio(augmented, sample_rate) + waveforms.append(augmented) + +###################################################################### +# Original: +# ~~~~~~~~~ +# + +play_audio(waveform, sample_rate) + +###################################################################### +# 8 bit mu-law: +# ~~~~~~~~~~~~~ +# + +play_audio(waveforms[0], sample_rate) + +###################################################################### +# GSM-FR: +# ~~~~~~~ +# + +play_audio(waveforms[1], sample_rate) + +###################################################################### +# MP3: +# ~~~~ +# + +play_audio(waveforms[2], sample_rate) + +###################################################################### +# Vorbis: +# ~~~~~~~ +# + +play_audio(waveforms[3], sample_rate) ###################################################################### # Simulating a phone recoding @@ -378,36 +467,33 @@ def get_noise_sample(*, resample=None): # sample_rate = 16000 -speech, _ = get_speech_sample(resample=sample_rate) +original_speech, _ = get_speech_sample(resample=sample_rate) -plot_specgram(speech, sample_rate, title="Original") -play_audio(speech, sample_rate) +plot_specgram(original_speech, sample_rate, title="Original") # Apply RIR rir, _ = get_rir_sample(resample=sample_rate, processed=True) -speech_ = torch.nn.functional.pad(speech, (rir.shape[1] - 1, 0)) -speech = torch.nn.functional.conv1d(speech_[None, ...], rir[None, ...])[0] +speech_ = torch.nn.functional.pad(original_speech, (rir.shape[1] - 1, 0)) +rir_applied = torch.nn.functional.conv1d(speech_[None, ...], rir[None, ...])[0] -plot_specgram(speech, sample_rate, title="RIR Applied") -play_audio(speech, sample_rate) +plot_specgram(rir_applied, sample_rate, title="RIR Applied") # Add background noise # Because the noise is recorded in the actual environment, we consider that # the noise contains the acoustic feature of the environment. Therefore, we add # the noise after RIR application. noise, _ = get_noise_sample(resample=sample_rate) -noise = noise[:, : speech.shape[1]] +noise = noise[:, : rir_applied.shape[1]] snr_db = 8 -scale = math.exp(snr_db / 10) * noise.norm(p=2) / speech.norm(p=2) -speech = (scale * speech + noise) / 2 +scale = math.exp(snr_db / 10) * noise.norm(p=2) / rir_applied.norm(p=2) +bg_added = (scale * rir_applied + noise) / 2 -plot_specgram(speech, sample_rate, title="BG noise added") -play_audio(speech, sample_rate) +plot_specgram(bg_added, sample_rate, title="BG noise added") # Apply filtering and change sample rate -speech, sample_rate = torchaudio.sox_effects.apply_effects_tensor( - speech, +filtered, sample_rate2 = torchaudio.sox_effects.apply_effects_tensor( + bg_added, sample_rate, effects=[ ["lowpass", "4000"], @@ -423,11 +509,45 @@ def get_noise_sample(*, resample=None): ], ) -plot_specgram(speech, sample_rate, title="Filtered") -play_audio(speech, sample_rate) +plot_specgram(filtered, sample_rate2, title="Filtered") # Apply telephony codec -speech = F.apply_codec(speech, sample_rate, format="gsm") +codec_applied = F.apply_codec(filtered, sample_rate2, format="gsm") -plot_specgram(speech, sample_rate, title="GSM Codec Applied") -play_audio(speech, sample_rate) +plot_specgram(codec_applied, sample_rate2, title="GSM Codec Applied") + + +###################################################################### +# Original speech: +# ~~~~~~~~~~~~~~~~ +# + +play_audio(original_speech, sample_rate) + +###################################################################### +# RIR applied: +# ~~~~~~~~~~~~ +# + +play_audio(rir_applied, sample_rate) + +###################################################################### +# Background noise added: +# ~~~~~~~~~~~~~~~~~~~~~~~ +# + +play_audio(bg_added, sample_rate) + +###################################################################### +# Filtered: +# ~~~~~~~~~ +# + +play_audio(filtered, sample_rate2) + +###################################################################### +# Codec aplied: +# ~~~~~~~~~~~~~ +# + +play_audio(codec_applied, sample_rate2) From 4c2edd21768e9c10df45bb7ff0e2a9921d4a8e70 Mon Sep 17 00:00:00 2001 From: Moto Hira Date: Tue, 21 Dec 2021 08:47:06 -0800 Subject: [PATCH 0046/1144] Clean up CTC decoder bynding code (#2092) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2092 Reviewed By: carolineechen Differential Revision: D33169110 fbshipit-source-id: e422ad93efe50b91f1ac5d572dc82768c1000c05 --- torchaudio/csrc/decoder/bindings/_decoder.cpp | 111 ----------------- .../csrc/decoder/bindings/_dictionary.cpp | 31 ----- torchaudio/csrc/decoder/bindings/pybind.cpp | 116 +++++++++++++++++- 3 files changed, 114 insertions(+), 144 deletions(-) delete mode 100644 torchaudio/csrc/decoder/bindings/_decoder.cpp delete mode 100644 torchaudio/csrc/decoder/bindings/_dictionary.cpp diff --git a/torchaudio/csrc/decoder/bindings/_decoder.cpp b/torchaudio/csrc/decoder/bindings/_decoder.cpp deleted file mode 100644 index 69bbe0a2d2..0000000000 --- a/torchaudio/csrc/decoder/bindings/_decoder.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT-style license found in - * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE - */ - -#include -#include - -#include "torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h" -#include "torchaudio/csrc/decoder/src/decoder/lm/KenLM.h" - -namespace py = pybind11; -using namespace torchaudio::lib::text; -using namespace py::literals; - -/** - * Some hackery that lets pybind11 handle shared_ptr (for old LMStatePtr). - * See: https://github.com/pybind/pybind11/issues/820 - * PYBIND11_MAKE_OPAQUE(std::shared_ptr); - * and inside PYBIND11_MODULE - * py::class_>(m, "encapsulated_data"); - */ - -namespace { - -/** - * A pybind11 "alias type" for abstract class LM, allowing one to subclass LM - * with a custom LM defined purely in Python. For those who don't want to build - * with KenLM, or have their own custom LM implementation. - * See: https://pybind11.readthedocs.io/en/stable/advanced/classes.html - * - * TODO: ensure this works. Last time Jeff tried this there were slicing issues, - * see https://github.com/pybind/pybind11/issues/1546 for workarounds. - * This is low-pri since we assume most people can just build with KenLM. - */ -class PyLM : public LM { - using LM::LM; - - // needed for pybind11 or else it won't compile - using LMOutput = std::pair; - - LMStatePtr start(bool startWithNothing) override { - PYBIND11_OVERLOAD_PURE(LMStatePtr, LM, start, startWithNothing); - } - - LMOutput score(const LMStatePtr& state, const int usrTokenIdx) override { - PYBIND11_OVERLOAD_PURE(LMOutput, LM, score, state, usrTokenIdx); - } - - LMOutput finish(const LMStatePtr& state) override { - PYBIND11_OVERLOAD_PURE(LMOutput, LM, finish, state); - } -}; - -/** - * Using custom python LMState derived from LMState is not working with - * custom python LM (derived from PyLM) because we need to to custing of LMState - * in score and finish functions to the derived class - * (for example vie obj.__class__ = CustomPyLMSTate) which cause the error - * "TypeError: __class__ assignment: 'CustomPyLMState' deallocator differs - * from 'flashlight.text.decoder._decoder.LMState'" - * details see in https://github.com/pybind/pybind11/issues/1640 - * To define custom LM you can introduce map inside LM which maps LMstate to - * additional state info (shared pointers pointing to the same underlying object - * will have the same id in python in functions score and finish) - * - * ```python - * from flashlight.lib.text.decoder import LM - * class MyPyLM(LM): - * mapping_states = dict() # store simple additional int for each state - * - * def __init__(self): - * LM.__init__(self) - * - * def start(self, start_with_nothing): - * state = LMState() - * self.mapping_states[state] = 0 - * return state - * - * def score(self, state, index): - * outstate = state.child(index) - * if outstate not in self.mapping_states: - * self.mapping_states[outstate] = self.mapping_states[state] + 1 - * return (outstate, -numpy.random.random()) - * - * def finish(self, state): - * outstate = state.child(-1) - * if outstate not in self.mapping_states: - * self.mapping_states[outstate] = self.mapping_states[state] + 1 - * return (outstate, -1) - *``` - */ -void LexiconDecoder_decodeStep( - LexiconDecoder& decoder, - uintptr_t emissions, - int T, - int N) { - decoder.decodeStep(reinterpret_cast(emissions), T, N); -} - -std::vector LexiconDecoder_decode( - LexiconDecoder& decoder, - uintptr_t emissions, - int T, - int N) { - return decoder.decode(reinterpret_cast(emissions), T, N); -} - -} // namespace diff --git a/torchaudio/csrc/decoder/bindings/_dictionary.cpp b/torchaudio/csrc/decoder/bindings/_dictionary.cpp deleted file mode 100644 index d3c5408ca2..0000000000 --- a/torchaudio/csrc/decoder/bindings/_dictionary.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT-style license found in - * https://github.com/flashlight/flashlight/blob/d385b2150872fd7bf106601475d8719a703fe9ee/LICENSE - */ - -#include -#include - -#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" -#include "torchaudio/csrc/decoder/src/dictionary/Utils.h" - -namespace py = pybind11; -using namespace torchaudio::lib::text; -using namespace py::literals; - -namespace { - -void Dictionary_addEntry_0( - Dictionary& dict, - const std::string& entry, - int idx) { - dict.addEntry(entry, idx); -} - -void Dictionary_addEntry_1(Dictionary& dict, const std::string& entry) { - dict.addEntry(entry); -} - -} // namespace diff --git a/torchaudio/csrc/decoder/bindings/pybind.cpp b/torchaudio/csrc/decoder/bindings/pybind.cpp index 9278b2965c..9203797cf1 100644 --- a/torchaudio/csrc/decoder/bindings/pybind.cpp +++ b/torchaudio/csrc/decoder/bindings/pybind.cpp @@ -1,7 +1,117 @@ #include -#include -#include +#include "torchaudio/csrc/decoder/src/decoder/LexiconDecoder.h" +#include "torchaudio/csrc/decoder/src/decoder/lm/KenLM.h" +#include "torchaudio/csrc/decoder/src/dictionary/Dictionary.h" +#include "torchaudio/csrc/decoder/src/dictionary/Utils.h" + +namespace py = pybind11; +using namespace torchaudio::lib::text; +using namespace py::literals; + +/** + * Some hackery that lets pybind11 handle shared_ptr (for old LMStatePtr). + * See: https://github.com/pybind/pybind11/issues/820 + * PYBIND11_MAKE_OPAQUE(std::shared_ptr); + * and inside PYBIND11_MODULE + * py::class_>(m, "encapsulated_data"); + */ + +namespace { + +/** + * A pybind11 "alias type" for abstract class LM, allowing one to subclass LM + * with a custom LM defined purely in Python. For those who don't want to build + * with KenLM, or have their own custom LM implementation. + * See: https://pybind11.readthedocs.io/en/stable/advanced/classes.html + * + * TODO: ensure this works. Last time Jeff tried this there were slicing issues, + * see https://github.com/pybind/pybind11/issues/1546 for workarounds. + * This is low-pri since we assume most people can just build with KenLM. + */ +class PyLM : public LM { + using LM::LM; + + // needed for pybind11 or else it won't compile + using LMOutput = std::pair; + + LMStatePtr start(bool startWithNothing) override { + PYBIND11_OVERLOAD_PURE(LMStatePtr, LM, start, startWithNothing); + } + + LMOutput score(const LMStatePtr& state, const int usrTokenIdx) override { + PYBIND11_OVERLOAD_PURE(LMOutput, LM, score, state, usrTokenIdx); + } + + LMOutput finish(const LMStatePtr& state) override { + PYBIND11_OVERLOAD_PURE(LMOutput, LM, finish, state); + } +}; + +/** + * Using custom python LMState derived from LMState is not working with + * custom python LM (derived from PyLM) because we need to to custing of LMState + * in score and finish functions to the derived class + * (for example vie obj.__class__ = CustomPyLMSTate) which cause the error + * "TypeError: __class__ assignment: 'CustomPyLMState' deallocator differs + * from 'flashlight.text.decoder._decoder.LMState'" + * details see in https://github.com/pybind/pybind11/issues/1640 + * To define custom LM you can introduce map inside LM which maps LMstate to + * additional state info (shared pointers pointing to the same underlying object + * will have the same id in python in functions score and finish) + * + * ```python + * from flashlight.lib.text.decoder import LM + * class MyPyLM(LM): + * mapping_states = dict() # store simple additional int for each state + * + * def __init__(self): + * LM.__init__(self) + * + * def start(self, start_with_nothing): + * state = LMState() + * self.mapping_states[state] = 0 + * return state + * + * def score(self, state, index): + * outstate = state.child(index) + * if outstate not in self.mapping_states: + * self.mapping_states[outstate] = self.mapping_states[state] + 1 + * return (outstate, -numpy.random.random()) + * + * def finish(self, state): + * outstate = state.child(-1) + * if outstate not in self.mapping_states: + * self.mapping_states[outstate] = self.mapping_states[state] + 1 + * return (outstate, -1) + *``` + */ +void LexiconDecoder_decodeStep( + LexiconDecoder& decoder, + uintptr_t emissions, + int T, + int N) { + decoder.decodeStep(reinterpret_cast(emissions), T, N); +} + +std::vector LexiconDecoder_decode( + LexiconDecoder& decoder, + uintptr_t emissions, + int T, + int N) { + return decoder.decode(reinterpret_cast(emissions), T, N); +} + +void Dictionary_addEntry_0( + Dictionary& dict, + const std::string& entry, + int idx) { + dict.addEntry(entry, idx); +} + +void Dictionary_addEntry_1(Dictionary& dict, const std::string& entry) { + dict.addEntry(entry); +} PYBIND11_MODULE(_torchaudio_decoder, m) { #ifdef BUILD_CTC_DECODER @@ -137,3 +247,5 @@ PYBIND11_MODULE(_torchaudio_decoder, m) { m.def("_load_words", &loadWords, "filename"_a, "max_words"_a = -1); #endif } + +} // namespace From 575d221ec4b394fcc8e5bb8147d0e7ba012afe56 Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Wed, 22 Dec 2021 05:10:43 -0800 Subject: [PATCH 0047/1144] Revert linting exemptions introduced in #2071 (#2087) Summary: After discussing with Moto Hira, we decided to revert linting exemptions introduced previously in order to keep the entire audio project as formatted as possible, to reduce the time we spend on formatting discussion. Pull Request resolved: https://github.com/pytorch/audio/pull/2087 Reviewed By: mthrok Differential Revision: D33236949 Pulled By: jdsgomes fbshipit-source-id: e13079f532c4534d8a168059b0ded6fa375fdecf --- .circleci/config.yml | 2 +- .circleci/config.yml.in | 2 +- pyproject.toml | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2286325042..99339b49de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,7 +128,7 @@ jobs: command: pre-commit run --all-files || true - run: name: Required lint modifications - when: on_fail + when: always command: git --no-pager diff download_third_parties_nix: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index cdc076c4d4..ab9e2cef2f 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -128,7 +128,7 @@ jobs: command: pre-commit run --all-files || true - run: name: Required lint modifications - when: on_fail + when: always command: git --no-pager diff download_third_parties_nix: diff --git a/pyproject.toml b/pyproject.toml index 842008a907..bda3b89bb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,3 @@ line-length = 120 target-version = ["py37"] [tool.ufmt] -excludes = [ - "examples/tutorials", -] From b18e583e77a199dbd8b981dd963c4ca1aea8cb33 Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Wed, 22 Dec 2021 06:42:10 -0800 Subject: [PATCH 0048/1144] Deprecating data utils (#2073) Summary: - Deprecates data utils (with warning that will be removed in v0.12) - replaces all usages of `torchaudio.datasets.utils.download_url` with `torch.hub.download_url_to_file` - replaces all MD5 hashes with SHA256 hash #Addresses https://github.com/pytorch/audio/issues/1883 Pull Request resolved: https://github.com/pytorch/audio/pull/2073 Reviewed By: mthrok Differential Revision: D33241756 Pulled By: jdsgomes fbshipit-source-id: 49388ec5965bfc91d9a1d8d0786eeafb2969f6cf --- torchaudio/datasets/cmuarctic.py | 40 +++++++++++++-------------- torchaudio/datasets/cmudict.py | 10 +++---- torchaudio/datasets/dr_vctk.py | 15 ++-------- torchaudio/datasets/gtzan.py | 6 ++-- torchaudio/datasets/librispeech.py | 5 ++-- torchaudio/datasets/libritts.py | 25 +++++++++++------ torchaudio/datasets/ljspeech.py | 6 ++-- torchaudio/datasets/speechcommands.py | 9 +++--- torchaudio/datasets/tedlium.py | 5 ++-- torchaudio/datasets/utils.py | 3 +- torchaudio/datasets/vctk.py | 8 ++++-- torchaudio/datasets/yesno.py | 4 +-- 12 files changed, 71 insertions(+), 65 deletions(-) diff --git a/torchaudio/datasets/cmuarctic.py b/torchaudio/datasets/cmuarctic.py index a01399198b..49dbed338f 100644 --- a/torchaudio/datasets/cmuarctic.py +++ b/torchaudio/datasets/cmuarctic.py @@ -6,8 +6,8 @@ import torchaudio from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -15,41 +15,41 @@ FOLDER_IN_ARCHIVE = "ARCTIC" _CHECKSUMS = { "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": - "4382b116efcc8339c37e01253cb56295", + "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": - "b072d6e961e3f36a2473042d097d6da9", + "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": - "5301c7aee8919d2abd632e2667adfa7f", + "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": - "280fdff1e9857119d9a2c57b50e12db7", + "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": - "5e21cb26c6529c533df1d02ccde5a186", + "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": - "b2c3e558f656af2e0a65da0ac0c3377a", + "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": - "3957c503748e3ce17a3b73c1b9861fb0", + "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": - "59708e932d27664f9eda3e8e6859969b", + "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": - "dba4f992ff023347c07c304bf72f4c73", + "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": - "24a876ea7335c1b0ff21460e1241340f", + "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": - "afb69d95f02350537e8a28df5ab6004b", + "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": - "4ce5b3b91a0a54b6b685b1b05aa0b3be", + "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": - "6f45a3b2c86a4ed0465b353be291f77d", + "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": - "c6a15abad5c14d27f4ee856502f0232f", + "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": - "71072c983df1e590d9e9519e2a621f6e", + "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": - "3771ff03a2f5b5c3b53aa0a68b9ad0d5", + "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": - "9cbf984a832ea01b5058ba9a96862850", + "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": - "959eecb2cbbc4ac304c6b92269380c81", + "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", } @@ -148,7 +148,7 @@ def __init__(self, if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) self._text = os.path.join(self._path, self._folder_text, self._file_text) diff --git a/torchaudio/datasets/cmudict.py b/torchaudio/datasets/cmudict.py index 2c6acccf21..df0a7ce51c 100644 --- a/torchaudio/datasets/cmudict.py +++ b/torchaudio/datasets/cmudict.py @@ -4,13 +4,13 @@ from typing import Iterable, Tuple, Union, List from torch.utils.data import Dataset -from torchaudio.datasets.utils import download_url +from torch.hub import download_url_to_file _CHECKSUMS = { "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": - "825f4ebd9183f2417df9f067a9cabe86", + "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": - "385e490aabc71b48e772118e3d02923e", + "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", } _PUNCTUATIONS = set([ "!EXCLAMATION-POINT", @@ -144,14 +144,14 @@ def __init__(self, 'The dictionary file is not found in the following location. ' f'Set `download=True` to download it. {dict_file}') checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url, dict_file, checksum) if not os.path.exists(symbol_file): if not download: raise RuntimeError( 'The symbol file is not found in the following location. ' f'Set `download=True` to download it. {symbol_file}') checksum = _CHECKSUMS.get(url_symbols, None) - download_url(url_symbols, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url_symbols, symbol_file, checksum) with open(symbol_file, "r") as text: self._symbols = [line.strip() for line in text.readlines()] diff --git a/torchaudio/datasets/dr_vctk.py b/torchaudio/datasets/dr_vctk.py index bfd2e3e400..8bd5409e00 100644 --- a/torchaudio/datasets/dr_vctk.py +++ b/torchaudio/datasets/dr_vctk.py @@ -3,17 +3,16 @@ from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file import torchaudio from torchaudio.datasets.utils import ( - download_url, extract_archive, - validate_file, ) _URL = "https://datashare.ed.ac.uk/bitstream/handle/10283/3038/DR-VCTK.zip" -_CHECKSUM = "29e93debeb0e779986542229a81ff29b" +_CHECKSUM = "781f12f4406ed36ed27ae3bce55da47ba176e2d8bae67319e389e07b2c9bd769" _SUPPORTED_SUBSETS = {"train", "test"} @@ -55,20 +54,12 @@ def __init__( if not archive.is_file(): if not download: raise RuntimeError("Dataset not found. Please use `download=True` to download it.") - download_url(url, root) - self._validate_checksum(archive) + download_url_to_file(url, archive, hash_prefix=_CHECKSUM) extract_archive(archive, root) self._config = self._load_config(self._config_filepath) self._filename_list = sorted(self._config) - def _validate_checksum(self, archive): - with open(archive, "rb") as file_obj: - if not validate_file(file_obj, _CHECKSUM, "md5"): - raise RuntimeError( - f"The hash of {str(archive)} does not match. Delete the file manually and retry." - ) - def _load_config(self, filepath: str) -> Dict[str, Tuple[str, int]]: # Skip header skip_rows = 2 if self._subset == "train" else 1 diff --git a/torchaudio/datasets/gtzan.py b/torchaudio/datasets/gtzan.py index b78104cacb..d98d827bfb 100644 --- a/torchaudio/datasets/gtzan.py +++ b/torchaudio/datasets/gtzan.py @@ -5,8 +5,8 @@ import torchaudio from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -977,7 +977,7 @@ URL = "http://opihi.cs.uvic.ca/sound/genres.tar.gz" FOLDER_IN_ARCHIVE = "genres" _CHECKSUMS = { - "http://opihi.cs.uvic.ca/sound/genres.tar.gz": "5b3d6dddb579ab49814ab86dba69e7c7" + "http://opihi.cs.uvic.ca/sound/genres.tar.gz": "24347e0223d2ba798e0a558c4c172d9d4a19c00bb7963fe055d183dadb4ef2c6" } @@ -1051,7 +1051,7 @@ def __init__( if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) if not os.path.isdir(self._path): diff --git a/torchaudio/datasets/librispeech.py b/torchaudio/datasets/librispeech.py index ad8a26493c..4aea9fac5b 100644 --- a/torchaudio/datasets/librispeech.py +++ b/torchaudio/datasets/librispeech.py @@ -5,8 +5,9 @@ import torchaudio from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file + from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -121,7 +122,7 @@ def __init__(self, if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum) + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) self._walker = sorted(str(p.stem) for p in Path(self._path).glob('*/*/*' + self._ext_audio)) diff --git a/torchaudio/datasets/libritts.py b/torchaudio/datasets/libritts.py index 62736d3313..6913383ba0 100644 --- a/torchaudio/datasets/libritts.py +++ b/torchaudio/datasets/libritts.py @@ -5,21 +5,28 @@ import torchaudio from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file from torchaudio.datasets.utils import ( - download_url, extract_archive, ) URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriTTS" _CHECKSUMS = { - "http://www.openslr.org/resources/60/dev-clean.tar.gz": "0c3076c1e5245bb3f0af7d82087ee207", - "http://www.openslr.org/resources/60/dev-other.tar.gz": "815555d8d75995782ac3ccd7f047213d", - "http://www.openslr.org/resources/60/test-clean.tar.gz": "7bed3bdb047c4c197f1ad3bc412db59f", - "http://www.openslr.org/resources/60/test-other.tar.gz": "ae3258249472a13b5abef2a816f733e4", - "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "4a8c202b78fe1bc0c47916a98f3a2ea8", - "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "a84ef10ddade5fd25df69596a2767b2d", - "http://www.openslr.org/resources/60/train-other-500.tar.gz": "7b181dd5ace343a5f38427999684aa6f", + "http://www.openslr.org/resources/60/dev-clean.tar.gz": + "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", + "http://www.openslr.org/resources/60/dev-other.tar.gz": + "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", + "http://www.openslr.org/resources/60/test-clean.tar.gz": + "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", + "http://www.openslr.org/resources/60/test-other.tar.gz": + "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", + "http://www.openslr.org/resources/60/train-clean-100.tar.gz": + "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", + "http://www.openslr.org/resources/60/train-clean-360.tar.gz": + "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", + "http://www.openslr.org/resources/60/train-other-500.tar.gz": + "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", } @@ -122,7 +129,7 @@ def __init__( if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum) + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) self._walker = sorted(str(p.stem) for p in Path(self._path).glob('*/*/*' + self._ext_audio)) diff --git a/torchaudio/datasets/ljspeech.py b/torchaudio/datasets/ljspeech.py index a0abcbb9ba..e2fd0f0cb9 100644 --- a/torchaudio/datasets/ljspeech.py +++ b/torchaudio/datasets/ljspeech.py @@ -4,9 +4,11 @@ from pathlib import Path import torchaudio -from torchaudio.datasets.utils import download_url, extract_archive +from torchaudio.datasets.utils import extract_archive from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file + _RELEASE_CONFIGS = { "release1": { @@ -54,7 +56,7 @@ def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, downloa if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _RELEASE_CONFIGS["release1"]["checksum"] - download_url(url, root, hash_value=checksum) + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) with open(self._metadata_path, "r", newline='') as metadata: diff --git a/torchaudio/datasets/speechcommands.py b/torchaudio/datasets/speechcommands.py index d92d6d44df..47522cbdef 100644 --- a/torchaudio/datasets/speechcommands.py +++ b/torchaudio/datasets/speechcommands.py @@ -5,8 +5,9 @@ import torchaudio from torch.utils.data import Dataset from torch import Tensor +from torch.hub import download_url_to_file + from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -16,9 +17,9 @@ EXCEPT_FOLDER = "_background_noise_" _CHECKSUMS = { "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz": - "3cd23799cb2bbdec517f1cc028f8d43c", + "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.02.tar.gz": - "6b74f3901214cb2c2934e98196829835", + "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", } @@ -111,7 +112,7 @@ def __init__(self, if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive, self._path) if subset == "validation": diff --git a/torchaudio/datasets/tedlium.py b/torchaudio/datasets/tedlium.py index fe6222a46f..5f4dbb499d 100644 --- a/torchaudio/datasets/tedlium.py +++ b/torchaudio/datasets/tedlium.py @@ -5,8 +5,9 @@ import torchaudio from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file + from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -101,7 +102,7 @@ def __init__( if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _RELEASE_CONFIGS[release]["checksum"] - download_url(url, root, hash_value=checksum) + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) # Create list for all samples diff --git a/torchaudio/datasets/utils.py b/torchaudio/datasets/utils.py index fd76f43d09..ea75f642fd 100644 --- a/torchaudio/datasets/utils.py +++ b/torchaudio/datasets/utils.py @@ -5,6 +5,7 @@ import urllib import urllib.request import zipfile +import warnings from typing import Any, Iterable, List, Optional from torch.utils.model_zoo import tqdm @@ -71,7 +72,7 @@ def download_url(url: str, progress_bar (bool, optional): Display a progress bar (Default: ``True``). resume (bool, optional): Enable resuming download (Default: ``False``). """ - + warnings.warn("download_url is deprecated and will be removed in the v0.12 release.") req = urllib.request.Request(url, method="HEAD") req_info = urllib.request.urlopen(req).info() diff --git a/torchaudio/datasets/vctk.py b/torchaudio/datasets/vctk.py index f0cdfdb410..43f2a93b87 100644 --- a/torchaudio/datasets/vctk.py +++ b/torchaudio/datasets/vctk.py @@ -3,16 +3,18 @@ from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file + import torchaudio from torchaudio.datasets.utils import ( - download_url, extract_archive, ) URL = "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip" _CHECKSUMS = { - "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": "8a6ba2946b36fcbef0212cad601f4bfa" + "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": + "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" } @@ -63,7 +65,7 @@ def __init__( if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _CHECKSUMS.get(url, None) - download_url(url, root, hash_value=checksum, hash_type="md5") + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive, self._path) if not os.path.isdir(self._path): diff --git a/torchaudio/datasets/yesno.py b/torchaudio/datasets/yesno.py index f33c11852c..095c01abd5 100644 --- a/torchaudio/datasets/yesno.py +++ b/torchaudio/datasets/yesno.py @@ -4,10 +4,10 @@ from torch import Tensor from torch.utils.data import Dataset +from torch.hub import download_url_to_file import torchaudio from torchaudio.datasets.utils import ( - download_url, extract_archive, ) @@ -54,7 +54,7 @@ def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, downloa if not os.path.isdir(self._path): if not os.path.isfile(archive): checksum = _RELEASE_CONFIGS["release1"]["checksum"] - download_url(url, root, hash_value=checksum) + download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) if not os.path.isdir(self._path): From 1b17b0116548f60ccb55ef97d09afa02bca7aa26 Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Wed, 22 Dec 2021 16:10:22 -0800 Subject: [PATCH 0049/1144] Introduce Conformer (#2068) Summary: Adds implementation of Conformer module. Adapted from sravyapopuri388's implementation for fairseq at https://github.com/fairinternal/fairseq-py/pull/2770. Pull Request resolved: https://github.com/pytorch/audio/pull/2068 Reviewed By: mthrok Differential Revision: D33236957 Pulled By: hwangjeff fbshipit-source-id: 382d99394996ff5249522b5899e1a4b4a95de9e6 --- docs/source/prototype.rst | 8 + docs/source/refs.bib | 8 + .../prototype/conformer_cpu_test.py | 13 + .../prototype/conformer_gpu_test.py | 15 + .../prototype/conformer_test_impl.py | 53 +++ torchaudio/prototype/__init__.py | 2 + torchaudio/prototype/conformer.py | 424 ++++++++++++++++++ 7 files changed, 523 insertions(+) create mode 100644 test/torchaudio_unittest/prototype/conformer_cpu_test.py create mode 100644 test/torchaudio_unittest/prototype/conformer_gpu_test.py create mode 100644 test/torchaudio_unittest/prototype/conformer_test_impl.py create mode 100644 torchaudio/prototype/conformer.py diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index c67b5c0130..e42376a9c0 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -13,6 +13,14 @@ see `here `_ for more information on prototype featur The module is available only within nightly builds and must be imported explicitly, e.g. ``import torchaudio.prototype``. +Conformer +~~~~~~~~~ + +.. autoclass:: Conformer + + .. automethod:: forward + + Emformer ~~~~~~~~ diff --git a/docs/source/refs.bib b/docs/source/refs.bib index 80df0e6161..f7e14b1177 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -140,6 +140,14 @@ @misc{kalchbrenner2018efficient archivePrefix={arXiv}, primaryClass={cs.SD} } +@misc{gulati2020conformer, + title={Conformer: Convolution-augmented Transformer for Speech Recognition}, + author={Anmol Gulati and James Qin and Chung-Cheng Chiu and Niki Parmar and Yu Zhang and Jiahui Yu and Wei Han and Shibo Wang and Zhengdong Zhang and Yonghui Wu and Ruoming Pang}, + year={2020}, + eprint={2005.08100}, + archivePrefix={arXiv}, + primaryClass={eess.AS} +} @article{Luo_2019, title={Conv-TasNet: Surpassing Ideal Time–Frequency Magnitude Masking for Speech Separation}, volume={27}, diff --git a/test/torchaudio_unittest/prototype/conformer_cpu_test.py b/test/torchaudio_unittest/prototype/conformer_cpu_test.py new file mode 100644 index 0000000000..a566b1d697 --- /dev/null +++ b/test/torchaudio_unittest/prototype/conformer_cpu_test.py @@ -0,0 +1,13 @@ +import torch +from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl +from torchaudio_unittest.common_utils import PytorchTestCase + + +class ConformerFloat32CPUTest(ConformerTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cpu") + + +class ConformerFloat64CPUTest(ConformerTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/prototype/conformer_gpu_test.py b/test/torchaudio_unittest/prototype/conformer_gpu_test.py new file mode 100644 index 0000000000..85e46c6386 --- /dev/null +++ b/test/torchaudio_unittest/prototype/conformer_gpu_test.py @@ -0,0 +1,15 @@ +import torch +from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl +from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + + +@skipIfNoCuda +class ConformerFloat32GPUTest(ConformerTestImpl, PytorchTestCase): + dtype = torch.float32 + device = torch.device("cuda") + + +@skipIfNoCuda +class ConformerFloat64GPUTest(ConformerTestImpl, PytorchTestCase): + dtype = torch.float64 + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/prototype/conformer_test_impl.py b/test/torchaudio_unittest/prototype/conformer_test_impl.py new file mode 100644 index 0000000000..cf16b704c3 --- /dev/null +++ b/test/torchaudio_unittest/prototype/conformer_test_impl.py @@ -0,0 +1,53 @@ +import torch +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script +from torchaudio.prototype import Conformer + + +class ConformerTestImpl(TestBaseMixin): + def _gen_model(self): + conformer = ( + Conformer( + num_layers=4, + input_dim=80, + conv_channels=64, + conformer_layer_input_dim=256, + conv_kernel_sizes=[5, 5], + max_source_positions=6000, + ffn_dim=128, + num_attention_heads=4, + depthwise_conv_kernel_size=31, + dropout=0.1, + ) + .to(device=self.device, dtype=self.dtype) + .eval() + ) + return conformer + + def _gen_inputs(self, input_dim, batch_size, num_frames): + lengths = torch.randint(1, num_frames, (batch_size,)).to( + device=self.device, dtype=self.dtype + ) + input = torch.rand(batch_size, int(lengths.max()), input_dim).to( + device=self.device, dtype=self.dtype + ) + return input, lengths + + def setUp(self): + super().setUp() + torch.random.manual_seed(31) + + def test_torchscript_consistency_forward(self): + r"""Verify that scripting Conformer does not change the behavior of method `forward`.""" + input_dim = 80 + batch_size = 10 + num_frames = 400 + + conformer = self._gen_model() + input, lengths = self._gen_inputs(input_dim, batch_size, num_frames) + scripted = torch_script(conformer) + + ref_out, ref_len = conformer(input, lengths) + scripted_out, scripted_len = scripted(input, lengths) + + self.assertEqual(ref_out, scripted_out) + self.assertEqual(ref_len, scripted_len) diff --git a/torchaudio/prototype/__init__.py b/torchaudio/prototype/__init__.py index 0b90a9cd6d..872e31d291 100644 --- a/torchaudio/prototype/__init__.py +++ b/torchaudio/prototype/__init__.py @@ -1,9 +1,11 @@ +from .conformer import Conformer from .emformer import Emformer from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model from .rnnt_decoder import Hypothesis, RNNTBeamSearch __all__ = [ + "Conformer", "Emformer", "Hypothesis", "RNNT", diff --git a/torchaudio/prototype/conformer.py b/torchaudio/prototype/conformer.py new file mode 100644 index 0000000000..f02e16ce31 --- /dev/null +++ b/torchaudio/prototype/conformer.py @@ -0,0 +1,424 @@ +import math +import torch +from typing import List, Optional, Tuple + + +__all__ = ["Conformer"] + + +PADDING_IDX = 1 + + +def _lengths_to_padding_mask(lengths: torch.Tensor) -> torch.Tensor: + batch_size = lengths.shape[0] + max_length = int(torch.max(lengths).item()) + padding_mask = torch.arange( + max_length, device=lengths.device, dtype=lengths.dtype + ).expand(batch_size, max_length) >= lengths.unsqueeze(1) + return padding_mask + + +def _make_positions(input, padding_idx: int): + mask = input.ne(padding_idx).int() + return (torch.cumsum(mask, dim=1).to(mask) * mask).long() + padding_idx + + +def _get_sinusoidal_embeddings( + num_embeddings: int, embedding_dim: int, padding_idx: Optional[int] = None +) -> torch.Tensor: + r"""Build sinusoidal embeddings. + This matches the implementation in tensor2tensor, but differs slightly + from the description in Section 3.5 of "Attention Is All You Need". + """ + half_dim = embedding_dim // 2 + t = ( + torch.arange(half_dim, dtype=torch.float) * -math.log(10000) / (half_dim - 1) + ).exp() + embedding_t = torch.arange(num_embeddings, dtype=torch.float).unsqueeze( + 1 + ) * t.unsqueeze(0) + embeddings = torch.cat([embedding_t.sin(), embedding_t.cos()], dim=1) + if embedding_dim % 2 == 1: + embeddings = torch.cat([embeddings, torch.zeros(num_embeddings, 1)], dim=1) + if padding_idx is not None: + embeddings[padding_idx, :] = 0 + return embeddings.to(dtype=torch.float32) + + +class ConvolutionModule(torch.nn.Module): + r"""Conformer convolution module. + + Args: + input_dim (int): input dimension. + num_channels (int): number of depthwise convolution layer input channels. + depthwise_kernel_size (int): kernel size of depthwise convolution layer. + bias (bool, optional): indicates whether to add bias term to each convolution layer. (Default: ``False``) + """ + + def __init__( + self, + input_dim: int, + num_channels: int, + depthwise_kernel_size: int, + bias: bool = False, + dropout: float = 0.0, + ) -> None: + super().__init__() + assert ( + depthwise_kernel_size - 1 + ) % 2 == 0, "depthwise_kernel_size must be odd to achieve 'SAME' padding." + self.layer_norm = torch.nn.LayerNorm(input_dim) + self.sequential = torch.nn.Sequential( + torch.nn.Conv1d( + input_dim, 2 * num_channels, 1, stride=1, padding=0, bias=bias, + ), + torch.nn.GLU(dim=1), + torch.nn.Conv1d( + num_channels, + num_channels, + depthwise_kernel_size, + stride=1, + padding=(depthwise_kernel_size - 1) // 2, + groups=num_channels, + bias=bias, + ), + torch.nn.BatchNorm1d(num_channels), + torch.nn.SiLU(), + torch.nn.Conv1d( + num_channels, input_dim, kernel_size=1, stride=1, padding=0, bias=bias, + ), + torch.nn.Dropout(dropout), + ) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + r""" + Args: + input (torch.Tensor): with shape `(B, T, D)`. + + Returns: + torch.Tensor: output, with shape `(B, T, D)`. + """ + x = self.layer_norm(input) + x = x.transpose(1, 2) + x = self.sequential(x) + return x.transpose(1, 2) + + +class FeedForwardModule(torch.nn.Module): + r"""Positionwise feed forward layer. + + Args: + input_dim (int): input dimension. + hidden_dim (int): hidden dimension. + dropout (float, optional): dropout probability. (Default: 0.0) + """ + + def __init__(self, input_dim: int, hidden_dim: int, dropout: float = 0.0) -> None: + super().__init__() + self.sequential = torch.nn.Sequential( + torch.nn.LayerNorm(input_dim), + torch.nn.Linear(input_dim, hidden_dim, bias=True), + torch.nn.SiLU(), + torch.nn.Dropout(dropout), + torch.nn.Linear(hidden_dim, input_dim, bias=True), + torch.nn.Dropout(dropout), + ) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + r""" + Args: + input (torch.Tensor): with shape `(*, D)`. + + Returns: + torch.Tensor: output, with shape `(*, D)`. + """ + return self.sequential(input) + + +class ConformerLayer(torch.nn.Module): + r"""Conformer layer that constitutes Conformer. + + Args: + input_dim (int): input dimension. + ffn_dim (int): hidden layer dimension of feedforward network. + num_attention_heads (int): number of attention heads. + depthwise_conv_kernel_size (int): kernel size of depthwise convolution layer. + dropout (float, optional): dropout probability. (Default: 0.0) + """ + + def __init__( + self, + input_dim: int, + ffn_dim: int, + num_attention_heads: int, + depthwise_conv_kernel_size: int, + dropout: float = 0.0, + ) -> None: + super().__init__() + + self.ffn1 = FeedForwardModule(input_dim, ffn_dim, dropout=dropout) + + self.self_attn_layer_norm = torch.nn.LayerNorm(input_dim) + self.self_attn = torch.nn.MultiheadAttention( + input_dim, num_attention_heads, dropout=dropout + ) + self.self_attn_dropout = torch.nn.Dropout(dropout) + + self.conv_module = ConvolutionModule( + input_dim=input_dim, + num_channels=input_dim, + depthwise_kernel_size=depthwise_conv_kernel_size, + ) + + self.ffn2 = FeedForwardModule(input_dim, ffn_dim, dropout=dropout) + self.final_layer_norm = torch.nn.LayerNorm(input_dim) + + def forward( + self, input: torch.Tensor, key_padding_mask: Optional[torch.Tensor] + ) -> torch.Tensor: + r""" + Args: + input (torch.Tensor): input, with shape `(T, B, D)`. + key_padding_mask (torch.Tensor or None): key padding mask to use in self attention layer. + + Returns: + torch.Tensor: output, with shape `(T, B, D)`. + """ + residual = input + x = self.ffn1(input) + x = x * 0.5 + residual + + residual = x + x = self.self_attn_layer_norm(x) + x, _ = self.self_attn( + query=x, + key=x, + value=x, + key_padding_mask=key_padding_mask, + need_weights=False, + ) + x = self.self_attn_dropout(x) + x = x + residual + + residual = x + x = x.transpose(0, 1) + x = self.conv_module(x) + x = x.transpose(0, 1) + x = residual + x + + residual = x + x = self.ffn2(x) + x = x * 0.5 + residual + + x = self.final_layer_norm(x) + return x + + +class Conv1dSubsampler(torch.nn.Module): + r"""Convolutional subsampler: a stack of 1D convolution (along temporal + dimension) followed by non-linear activation via gated linear units + (https://arxiv.org/abs/1911.08460) + + Args: + in_channels (int): number of input channels. + mid_channels (int): number of intermediate channels. + out_channels (int): number of output channels. + kernel_sizes (List[int]): kernel size for each convolutional layer. + """ + + def __init__( + self, + in_channels: int, + mid_channels: int, + out_channels: int, + kernel_sizes: List[int], + ) -> None: + super().__init__() + self.num_layers = len(kernel_sizes) + conv_glus = [ + torch.nn.Sequential( + torch.nn.Conv1d( + in_channels if i == 0 else mid_channels // 2, + mid_channels if i < self.num_layers - 1 else out_channels * 2, + kernel_size, + stride=2, + padding=kernel_size // 2, + ), + torch.nn.GLU(dim=1), + ) + for i, kernel_size in enumerate(kernel_sizes) + ] + self.sequential = torch.nn.Sequential(*conv_glus) + + def _get_output_lengths(self, lengths: torch.Tensor) -> torch.Tensor: + out = lengths + for _ in range(self.num_layers): + out = ((out.float() - 1) / 2 + 1).floor().long() + return out.to(torch.int32) + + def forward( + self, input: torch.Tensor, lengths: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + r""" + Args: + input (torch.Tensor): input frames, with shape `(B, T_in, in_channels)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor + output frames, with shape `(B, T_out, out_channels)`. + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in output frames. + """ + x = input.transpose(1, 2).contiguous() + x = self.sequential(x) + x = x.transpose(1, 2).contiguous() + return x, self._get_output_lengths(lengths) + + +class SinusoidalPositionalEmbedding(torch.nn.Module): + r"""Produces sinusoidal positional embeddings of any length. + Padding symbols are ignored. + + Args: + embedding_dim (int): embedding dimension. + padding_idx (int, optional): index corresponding to last padding symbol. (Default: 0) + init_size (int, optional): initial embedding count. (Default: 1024) + """ + + def __init__( + self, embedding_dim: int, padding_idx: int = 0, init_size: int = 1024 + ) -> None: + super().__init__() + self.embedding_dim = embedding_dim + self.padding_idx = padding_idx + self.embeddings = _get_sinusoidal_embeddings( + init_size, embedding_dim, padding_idx + ) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + r""" + Args: + input (torch.Tensor): with shape `(B, T)`. + + Returns: + torch.Tensor: output, with shape `(B, T, embedding_dim)`. + """ + B, T = input.shape + max_pos = self.padding_idx + 1 + T + if max_pos > self.embeddings.size(0): + self.embeddings = _get_sinusoidal_embeddings( + max_pos, self.embedding_dim, self.padding_idx + ) + self.embeddings = self.embeddings.to(input) + positions = _make_positions(input, self.padding_idx) + return ( + self.embeddings.index_select(0, positions.view(-1)).view(B, T, -1).detach() + ) + + +class Conformer(torch.nn.Module): + r"""Implements the Conformer architecture introduced in + *Conformer: Convolution-augmented Transformer for Speech Recognition* + [:footcite:`gulati2020conformer`]. + + Args: + num_layers (int): number of Conformer layers to instantiate. + input_dim (int): input dimension. + conv_channels (int): number of intermediate convolutional subsampler channels. + conformer_layer_input_dim (int): Conformer layer input dimension. + conv_kernel_sizes (List[int]): convolutional subsampler kernel sizes. + max_source_positions (int): maximum input length. + ffn_dim (int): hidden layer dimension of feedforward network. + num_attention_heads (int): number of attention heads. + depthwise_conv_kernel_size (int): kernel size of depthwise convolution layer. + dropout (float, optional): dropout probability. (Default: 0.0) + + Examples: + >>> conformer = Conformer( + >>> num_layers=4, + >>> input_dim=80, + >>> conv_channels=64, + >>> conformer_layer_input_dim=256, + >>> conv_kernel_sizes=[5, 5], + >>> max_source_positions=1000, + >>> ffn_dim=128, + >>> num_attention_heads=4, + >>> depthwise_conv_kernel_size=31, + >>> ) + >>> lengths = torch.randint(1, 400, (10,)) # (batch,) + >>> input = torch.rand(10, int(lengths.max()), input_dim) # (batch, num_frames, input_dim) + >>> output = conformer(input, lengths) + """ + + def __init__( + self, + num_layers: int, + input_dim: int, + conv_channels: int, + conformer_layer_input_dim: int, + conv_kernel_sizes: List[int], + max_source_positions: int, + ffn_dim: int, + num_attention_heads: int, + depthwise_conv_kernel_size: int, + dropout: float = 0.0, + ): + super().__init__() + + self.subsample = Conv1dSubsampler( + input_dim, conv_channels, conformer_layer_input_dim, conv_kernel_sizes, + ) + self.position_embedding = SinusoidalPositionalEmbedding( + conformer_layer_input_dim, + padding_idx=PADDING_IDX, + init_size=max_source_positions + PADDING_IDX + 1, + ) + self.linear = torch.nn.Linear( + conformer_layer_input_dim, conformer_layer_input_dim + ) + self.dropout = torch.nn.Dropout(dropout) + self.conformer_layers = torch.nn.ModuleList( + [ + ConformerLayer( + conformer_layer_input_dim, + ffn_dim, + num_attention_heads, + depthwise_conv_kernel_size, + dropout, + ) + for _ in range(num_layers) + ] + ) + + def forward( + self, input: torch.Tensor, lengths: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + r""" + Args: + input (torch.Tensor): with shape `(B, T_in, input_dim)`. + lengths (torch.Tensor): with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in ``input``. + + Returns: + (torch.Tensor, torch.Tensor) + torch.Tensor + output frames, with shape `(B, T_out, conformer_layer_input_dim)` + torch.Tensor + output lengths, with shape `(B,)` and i-th element representing + number of valid frames for i-th batch element in output frames. + """ + x, lengths = self.subsample(input, lengths) + encoder_padding_mask = _lengths_to_padding_mask(lengths) + positions = self.position_embedding(encoder_padding_mask) + x += positions + x = self.linear(x) + x = self.dropout(x) + + x = x.transpose(0, 1) + for layer in self.conformer_layers: + x = layer(x, encoder_padding_mask) + return x.transpose(0, 1), lengths From 6d3fe9913b65f6b45b2c460ba47c5b4a372b766e Mon Sep 17 00:00:00 2001 From: Moto Hira Date: Wed, 22 Dec 2021 19:18:47 -0800 Subject: [PATCH 0050/1144] Accomodate internal dependency change (#2094) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2094 Reviewed By: nateanl Differential Revision: D33288439 fbshipit-source-id: 385e0e4257755dbaf143287f612e19bede189757 --- torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp index 591a27da01..7ef66edc53 100644 --- a/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp +++ b/torchaudio/csrc/decoder/src/decoder/lm/KenLM.cpp @@ -9,7 +9,11 @@ #include +#ifdef USE_KENLM_FROM_LANGTECH +#include "language_technology/jedi/lm/model.hh" +#else #include "lm/model.hh" +#endif namespace torchaudio { namespace lib { From 0e5913d523e10e231258e1a4670431e4dab589f0 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 23 Dec 2021 07:17:16 -0800 Subject: [PATCH 0051/1144] Fix third party archive fetch job (#2095) Summary: Follow-up of https://github.com/pytorch/audio/issues/2086 The CI job to download the third party code and cache daily has not been properly updated. Pull Request resolved: https://github.com/pytorch/audio/pull/2095 Reviewed By: hwangjeff Differential Revision: D33291738 Pulled By: mthrok fbshipit-source-id: 6fc61f76b35c6f032085eda9d6053eefd2a1e0a9 --- .circleci/config.yml | 4 ++-- .circleci/config.yml.in | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 99339b49de..468fcbdec6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -152,11 +152,11 @@ jobs: key: tp-nix-v2-{{ checksum ".cachekey" }} paths: - - third_party/sox/archives + - third_party/archives - persist_to_workspace: root: third_party paths: - - sox/archives + - archives binary_linux_wheel: <<: *binary_common diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index ab9e2cef2f..4b8aaabcbe 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -152,11 +152,11 @@ jobs: key: tp-nix-v2-{{ checksum ".cachekey" }} {% endraw %} paths: - - third_party/sox/archives + - third_party/archives - persist_to_workspace: root: third_party paths: - - sox/archives + - archives binary_linux_wheel: <<: *binary_common From 5859923adf577410f672f72b208f1e4367cef1ca Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Thu, 23 Dec 2021 11:10:26 -0800 Subject: [PATCH 0052/1144] Apply arc lint to pytorch audio (#2096) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2096 run: `arc lint --apply-patches --paths-cmd 'hg files -I "./**/*.py"'` Reviewed By: mthrok Differential Revision: D33297351 fbshipit-source-id: 7bf5956edf0717c5ca90219f72414ff4eeaf5aa8 --- .circleci/regenerate.py | 110 +-- .../linux/scripts/run_clang_format.py | 134 ++-- .github/process_commit.py | 10 +- docs/source/conf.py | 174 +++-- .../asr/librispeech_emformer_rnnt/eval.py | 24 +- .../librispeech_emformer_rnnt/lightning.py | 116 +-- .../asr/librispeech_emformer_rnnt/train.py | 13 +- examples/hubert/dataset/hubert_dataset.py | 56 +- examples/hubert/preprocess.py | 24 +- examples/hubert/utils/common_utils.py | 23 +- examples/hubert/utils/feature_utils.py | 15 +- examples/hubert/utils/kmeans.py | 8 +- examples/interactive_asr/__init__.py | 2 +- examples/interactive_asr/asr.py | 7 +- examples/interactive_asr/utils.py | 39 +- examples/interactive_asr/vad.py | 29 +- .../augmentation/create_jittable_pipeline.py | 20 +- .../build_pipeline_from_fairseq.py | 80 +-- ..._pipeline_from_huggingface_transformers.py | 61 +- .../speech_recognition/greedy_decoder.py | 8 +- .../speech_recognition/parse_librispeech.py | 16 +- .../speech_recognition/parse_voxforge.py | 20 +- examples/pipeline_tacotron2/datasets.py | 35 +- examples/pipeline_tacotron2/inference.py | 225 +++--- examples/pipeline_tacotron2/text/numbers.py | 51 +- .../text/text_preprocessing.py | 101 +-- examples/pipeline_tacotron2/train.py | 304 ++++---- examples/pipeline_tacotron2/utils.py | 8 +- examples/pipeline_wav2letter/datasets.py | 11 +- examples/pipeline_wav2letter/main.py | 67 +- examples/pipeline_wavernn/datasets.py | 24 +- examples/pipeline_wavernn/inference.py | 66 +- examples/pipeline_wavernn/losses.py | 22 +- examples/pipeline_wavernn/main.py | 99 ++- examples/pipeline_wavernn/processing.py | 13 +- examples/pipeline_wavernn/utils.py | 6 +- .../wavernn_inference_wrapper.py | 23 +- .../source_separation/conv_tasnet/__init__.py | 7 +- .../source_separation/conv_tasnet/train.py | 49 +- .../source_separation/conv_tasnet/trainer.py | 22 +- examples/source_separation/eval.py | 29 +- examples/source_separation/lightning_train.py | 53 +- examples/source_separation/train.py | 30 +- examples/source_separation/utils/__init__.py | 2 +- .../utils/dataset/__init__.py | 2 +- .../source_separation/utils/dataset/utils.py | 12 +- .../utils/dataset/wsj0mix.py | 8 +- .../source_separation/utils/dist_utils.py | 8 +- examples/source_separation/utils/metrics.py | 37 +- examples/test/test_interactive_asr.py | 4 +- .../audio_data_augmentation_tutorial.py | 10 +- .../audio_feature_augmentation_tutorial.py | 10 +- .../audio_feature_extractions_tutorial.py | 6 +- examples/tutorials/audio_io_tutorial.py | 10 +- .../tutorials/audio_resampling_tutorial.py | 74 +- .../tutorials/forced_alignment_tutorial.py | 24 +- examples/tutorials/mvdr_tutorial.py | 15 +- .../speech_recognition_pipeline_tutorial.py | 8 +- .../tutorials/tacotron2_pipeline_tutorial.py | 16 +- setup.py | 94 +-- test/integration_tests/conftest.py | 26 +- .../tacotron2_pipeline_test.py | 6 +- .../wav2vec2_pipeline_test.py | 64 +- .../assets/io/generate_opus.py | 26 +- .../fairseq/generate_hubert_model_config.py | 45 +- .../fairseq/generate_wav2vec2_model_config.py | 39 +- .../generate_huggingface_model_config.py | 14 +- test/torchaudio_unittest/backend/common.py | 20 +- .../backend/soundfile/common.py | 27 +- .../backend/soundfile/info_test.py | 59 +- .../backend/soundfile/load_test.py | 128 ++-- .../backend/soundfile/save_test.py | 146 ++-- .../backend/sox_io/common.py | 18 +- .../backend/sox_io/info_test.py | 373 +++++----- .../backend/sox_io/load_test.py | 527 ++++++++------ .../backend/sox_io/roundtrip_test.py | 43 +- .../backend/sox_io/save_test.py | 316 ++++---- .../backend/sox_io/smoke_test.py | 167 +++-- .../backend/sox_io/torchscript_test.py | 116 +-- .../torchaudio_unittest/backend/utils_test.py | 8 +- .../common_utils/__init__.py | 69 +- .../common_utils/backend_utils.py | 16 +- .../common_utils/case_utils.py | 48 +- .../common_utils/data_utils.py | 31 +- .../common_utils/func_utils.py | 1 + .../common_utils/kaldi_utils.py | 20 +- .../common_utils/parameterized_utils.py | 11 +- .../common_utils/psd_utils.py | 6 +- .../common_utils/rnnt_utils.py | 21 +- .../common_utils/sox_utils.py | 92 +-- .../common_utils/wav_utils.py | 46 +- .../compliance_kaldi_test.py | 10 +- .../datasets/cmuarctic_test.py | 1 - .../datasets/cmudict_test.py | 5 +- .../datasets/commonvoice_test.py | 69 +- .../datasets/dr_vctk_test.py | 10 +- .../datasets/gtzan_test.py | 23 +- .../datasets/librispeech_test.py | 69 +- .../datasets/libritts_test.py | 37 +- .../datasets/ljspeech_test.py | 27 +- .../datasets/speechcommands_test.py | 15 +- .../datasets/tedlium_test.py | 10 +- .../torchaudio_unittest/datasets/vctk_test.py | 59 +- .../datasets/yesno_test.py | 7 +- test/torchaudio_unittest/example/__init__.py | 5 +- .../example/souce_sepration/metrics_test.py | 6 +- .../example/souce_sepration/sdr_reference.py | 196 ++--- .../example/souce_sepration/wsj0mix_test.py | 7 +- .../tacotron2/tacotron2_loss_cpu_test.py | 4 +- .../tacotron2/tacotron2_loss_gpu_test.py | 4 +- .../example/tacotron2/tacotron2_loss_impl.py | 22 +- .../tacotron2/test_text_preprocessing.py | 72 +- .../functional/autograd_cpu_test.py | 7 +- .../functional/autograd_cuda_test.py | 7 +- .../functional/autograd_impl.py | 160 +++-- .../functional/batch_consistency_test.py | 170 +++-- .../functional/functional_cpu_test.py | 16 +- .../functional/functional_cuda_test.py | 7 +- .../functional/functional_impl.py | 227 +++--- .../kaldi_compatibility_cpu_test.py | 8 +- .../kaldi_compatibility_cuda_test.py | 6 +- .../kaldi_compatibility_test_impl.py | 33 +- .../librosa_compatibility_cpu_test.py | 5 +- .../librosa_compatibility_cuda_test.py | 5 +- .../librosa_compatibility_test_impl.py | 61 +- .../functional/sox_compatibility_test.py | 109 +-- .../torchscript_consistency_cpu_test.py | 6 +- .../torchscript_consistency_cuda_test.py | 6 +- .../torchscript_consistency_impl.py | 128 ++-- test/torchaudio_unittest/kaldi_io_test.py | 8 +- .../torchaudio_unittest/models/models_test.py | 99 ++- .../models/tacotron2/model_test_cpu_test.py | 2 +- .../models/tacotron2/model_test_gpu_test.py | 2 +- .../models/tacotron2/model_test_impl.py | 139 ++-- .../wav2vec2/fairseq_integration_test.py | 175 +++-- .../wav2vec2/huggingface_intergration_test.py | 89 ++- .../models/wav2vec2/model_test.py | 107 +-- .../prototype/conformer_cpu_test.py | 2 +- .../prototype/conformer_gpu_test.py | 2 +- .../prototype/conformer_test_impl.py | 10 +- .../prototype/emformer_cpu_test.py | 2 +- .../prototype/emformer_gpu_test.py | 2 +- .../prototype/emformer_test_impl.py | 34 +- .../prototype/rnnt_cpu_test.py | 2 +- .../prototype/rnnt_decoder_cpu_test.py | 2 +- .../prototype/rnnt_decoder_gpu_test.py | 2 +- .../prototype/rnnt_decoder_test_impl.py | 29 +- .../prototype/rnnt_gpu_test.py | 2 +- .../prototype/rnnt_test_impl.py | 60 +- test/torchaudio_unittest/sox_effect/common.py | 7 +- .../sox_effect/dataset_test.py | 62 +- .../sox_effect/smoke_test.py | 31 +- .../sox_effect/sox_effect_test.py | 369 +++++----- .../sox_effect/torchscript_test.py | 24 +- .../transforms/autograd_cpu_test.py | 5 +- .../transforms/autograd_cuda_test.py | 5 +- .../transforms/autograd_test_impl.py | 150 ++-- .../transforms/batch_consistency_test.py | 28 +- .../kaldi_compatibility_cpu_test.py | 6 +- .../kaldi_compatibility_cuda_test.py | 6 +- .../transforms/kaldi_compatibility_impl.py | 31 +- .../librosa_compatibility_cpu_test.py | 4 +- .../librosa_compatibility_cuda_test.py | 4 +- .../librosa_compatibility_test_impl.py | 143 ++-- .../transforms/sox_compatibility_test.py | 41 +- .../torchscript_consistency_cpu_test.py | 6 +- .../torchscript_consistency_cuda_test.py | 6 +- .../torchscript_consistency_impl.py | 44 +- .../transforms/transforms_cpu_test.py | 8 +- .../transforms/transforms_cuda_test.py | 8 +- .../transforms/transforms_test.py | 147 ++-- .../transforms/transforms_test_impl.py | 61 +- .../utils/sox_utils_test.py | 12 +- tools/convert_fairseq_models.py | 30 +- tools/convert_voxpopuli_models.py | 34 +- tools/release_notes/retrieve_prs.py | 8 +- tools/setup_helpers/extension.py | 75 +- torchaudio/__init__.py | 25 +- torchaudio/_extension.py | 12 +- torchaudio/_internal/module_utils.py | 50 +- torchaudio/backend/__init__.py | 2 +- torchaudio/backend/common.py | 13 +- torchaudio/backend/no_backend.py | 22 +- torchaudio/backend/soundfile_backend.py | 111 ++- torchaudio/backend/sox_io_backend.py | 53 +- torchaudio/backend/utils.py | 39 +- torchaudio/compliance/__init__.py | 2 +- torchaudio/compliance/kaldi.py | 461 ++++++------ torchaudio/datasets/__init__.py | 14 +- torchaudio/datasets/cmuarctic.py | 80 +-- torchaudio/datasets/cmudict.py | 171 ++--- torchaudio/datasets/commonvoice.py | 15 +- torchaudio/datasets/dr_vctk.py | 5 +- torchaudio/datasets/gtzan.py | 9 +- torchaudio/datasets/librimix.py | 12 +- torchaudio/datasets/librispeech.py | 43 +- torchaudio/datasets/libritts.py | 27 +- torchaudio/datasets/ljspeech.py | 24 +- torchaudio/datasets/speechcommands.py | 40 +- torchaudio/datasets/tedlium.py | 14 +- torchaudio/datasets/utils.py | 51 +- torchaudio/datasets/vctk.py | 37 +- torchaudio/datasets/yesno.py | 11 +- torchaudio/functional/__init__.py | 138 ++-- torchaudio/functional/filtering.py | 187 ++--- torchaudio/functional/functional.py | 493 ++++++------- torchaudio/kaldi_io.py | 30 +- torchaudio/models/__init__.py | 30 +- torchaudio/models/conv_tasnet.py | 48 +- torchaudio/models/deepspeech.py | 12 +- torchaudio/models/tacotron2.py | 123 +--- torchaudio/models/wav2letter.py | 8 +- torchaudio/models/wav2vec2/__init__.py | 20 +- torchaudio/models/wav2vec2/components.py | 220 +++--- torchaudio/models/wav2vec2/model.py | 136 ++-- torchaudio/models/wav2vec2/utils/__init__.py | 6 +- .../models/wav2vec2/utils/import_fairseq.py | 68 +- .../wav2vec2/utils/import_huggingface.py | 46 +- torchaudio/models/wavernn.py | 94 ++- torchaudio/pipelines/__init__.py | 74 +- torchaudio/pipelines/_tts/__init__.py | 12 +- torchaudio/pipelines/_tts/impl.py | 64 +- torchaudio/pipelines/_tts/interface.py | 1 - torchaudio/pipelines/_tts/utils.py | 153 ++-- torchaudio/pipelines/_wav2vec2/impl.py | 316 ++++---- torchaudio/pipelines/_wav2vec2/utils.py | 54 +- torchaudio/prototype/conformer.py | 76 +- torchaudio/prototype/emformer.py | 196 ++--- torchaudio/prototype/rnnt.py | 51 +- torchaudio/prototype/rnnt_decoder.py | 86 +-- torchaudio/sox_effects/__init__.py | 12 +- torchaudio/sox_effects/sox_effects.py | 30 +- torchaudio/transforms.py | 672 +++++++++--------- torchaudio/utils/__init__.py | 3 +- 234 files changed, 6612 insertions(+), 7037 deletions(-) diff --git a/.circleci/regenerate.py b/.circleci/regenerate.py index 859a0a2f5c..91515f15fe 100755 --- a/.circleci/regenerate.py +++ b/.circleci/regenerate.py @@ -14,22 +14,25 @@ https://github.com/pytorch/vision/pull/1321#issuecomment-531033978 """ +import os.path + import jinja2 -from jinja2 import select_autoescape import yaml -import os.path +from jinja2 import select_autoescape PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] -CU_VERSIONS_DICT = {"linux": ["cpu", "cu102", "cu111","cu113", "cu115", "rocm4.1"], - "windows": ["cpu", "cu113", "cu115"], - "macos": ["cpu"]} +CU_VERSIONS_DICT = { + "linux": ["cpu", "cu102", "cu111", "cu113", "cu115", "rocm4.1"], + "windows": ["cpu", "cu113", "cu115"], + "macos": ["cpu"], +} -DOC_VERSION = ('linux', '3.8') +DOC_VERSION = ("linux", "3.8") -def build_workflows(prefix='', upload=False, filter_branch=None, indentation=6): +def build_workflows(prefix="", upload=False, filter_branch=None, indentation=6): w = [] w += build_download_job(filter_branch) for btype in ["wheel", "conda"]: @@ -37,23 +40,21 @@ def build_workflows(prefix='', upload=False, filter_branch=None, indentation=6): for python_version in PYTHON_VERSIONS: for cu_version in CU_VERSIONS_DICT[os_type]: fb = filter_branch - if cu_version.startswith("rocm") and btype=="conda": + if cu_version.startswith("rocm") and btype == "conda": continue - if not fb and (os_type == 'linux' and - btype == 'wheel' and - python_version == '3.8' and - cu_version == 'cpu'): + if not fb and ( + os_type == "linux" and btype == "wheel" and python_version == "3.8" and cu_version == "cpu" + ): # the fields must match the build_docs "requires" dependency - fb = '/.*/' + fb = "/.*/" w += build_workflow_pair(btype, os_type, python_version, cu_version, fb, prefix, upload) if not filter_branch: # Build on every pull request, but upload only on nightly and tags - w += build_doc_job('/.*/') - w += upload_doc_job('nightly') + w += build_doc_job("/.*/") + w += upload_doc_job("nightly") w += docstring_parameters_sync_job(None) - return indent(indentation, w) @@ -67,7 +68,7 @@ def build_download_job(filter_branch): return [{"download_third_parties_nix": job}] -def build_workflow_pair(btype, os_type, python_version, cu_version, filter_branch, prefix='', upload=False): +def build_workflow_pair(btype, os_type, python_version, cu_version, filter_branch, prefix="", upload=False): w = [] base_workflow_name = f"{prefix}binary_{os_type}_{btype}_py{python_version}_{cu_version}" @@ -77,9 +78,13 @@ def build_workflow_pair(btype, os_type, python_version, cu_version, filter_branc w.append(generate_upload_workflow(base_workflow_name, filter_branch, os_type, btype, cu_version)) - if filter_branch == 'nightly' and os_type != 'macos': - pydistro = 'pip' if btype == 'wheel' else 'conda' - w.append(generate_smoketest_workflow(pydistro, base_workflow_name, filter_branch, python_version, cu_version, os_type)) + if filter_branch == "nightly" and os_type != "macos": + pydistro = "pip" if btype == "wheel" else "conda" + w.append( + generate_smoketest_workflow( + pydistro, base_workflow_name, filter_branch, python_version, cu_version, os_type + ) + ) return w @@ -88,7 +93,9 @@ def build_doc_job(filter_branch): job = { "name": "build_docs", "python_version": "3.8", - "requires": ["binary_linux_wheel_py3.8_cpu", ], + "requires": [ + "binary_linux_wheel_py3.8_cpu", + ], } if filter_branch: @@ -101,7 +108,9 @@ def upload_doc_job(filter_branch): "name": "upload_docs", "context": "org-member", "python_version": "3.8", - "requires": ["build_docs", ], + "requires": [ + "build_docs", + ], } if filter_branch: @@ -113,7 +122,9 @@ def docstring_parameters_sync_job(filter_branch): job = { "name": "docstring_parameters_sync", "python_version": "3.8", - "requires": ["binary_linux_wheel_py3.8_cpu", ], + "requires": [ + "binary_linux_wheel_py3.8_cpu", + ], } if filter_branch: @@ -129,13 +140,13 @@ def generate_base_workflow(base_workflow_name, python_version, cu_version, filte "cuda_version": cu_version, } - if os_type in ['linux', 'macos']: - d['requires'] = ['download_third_parties_nix'] - if btype == 'conda': - d['conda_docker_image'] = f'pytorch/conda-builder:{cu_version.replace("cu1","cuda1")}' - elif cu_version.startswith('cu'): - d['wheel_docker_image'] = f'pytorch/manylinux-{cu_version.replace("cu1","cuda1")}' - elif cu_version.startswith('rocm'): + if os_type in ["linux", "macos"]: + d["requires"] = ["download_third_parties_nix"] + if btype == "conda": + d["conda_docker_image"] = f'pytorch/conda-builder:{cu_version.replace("cu1","cuda1")}' + elif cu_version.startswith("cu"): + d["wheel_docker_image"] = f'pytorch/manylinux-{cu_version.replace("cu1","cuda1")}' + elif cu_version.startswith("rocm"): d["wheel_docker_image"] = f"pytorch/manylinux-rocm:{cu_version[len('rocm'):]}" if filter_branch: @@ -153,7 +164,7 @@ def gen_filter_branch_tree(*branches): # Using a raw string here to avoid having to escape # anything "only": r"/v[0-9]+(\.[0-9]+)*-rc[0-9]+/" - } + }, } @@ -164,9 +175,8 @@ def generate_upload_workflow(base_workflow_name, filter_branch, os_type, btype, "requires": [base_workflow_name], } - if btype == 'wheel': - d["subfolder"] = "" if os_type == 'macos' else cu_version + "/" - + if btype == "wheel": + d["subfolder"] = "" if os_type == "macos" else cu_version + "/" if filter_branch: d["filters"] = gen_filter_branch_tree(filter_branch) @@ -212,22 +222,24 @@ def unittest_workflows(indentation=6): job = { "name": f"unittest_{os_type}_{device_type}_py{python_version}", "python_version": python_version, - "cuda_version": 'cpu' if device_type == "cpu" else "cu113", + "cuda_version": "cpu" if device_type == "cpu" else "cu113", } if os_type != "windows": - job['requires'] = ['download_third_parties_nix'] + job["requires"] = ["download_third_parties_nix"] jobs.append({f"unittest_{os_type}_{device_type}": job}) if i == 0 and os_type == "linux" and device_type == "cpu": - jobs.append({ - "stylecheck": { - "name": f"stylecheck_py{python_version}", - "python_version": python_version, - "cuda_version": "cpu", + jobs.append( + { + "stylecheck": { + "name": f"stylecheck_py{python_version}", + "python_version": python_version, + "cuda_version": "cpu", + } } - }) + ) return indent(indentation, jobs) @@ -236,12 +248,14 @@ def unittest_workflows(indentation=6): env = jinja2.Environment( loader=jinja2.FileSystemLoader(d), lstrip_blocks=True, - autoescape=select_autoescape(enabled_extensions=('html', 'xml')), + autoescape=select_autoescape(enabled_extensions=("html", "xml")), ) - with open(os.path.join(d, 'config.yml'), 'w') as f: - f.write(env.get_template('config.yml.in').render( - build_workflows=build_workflows, - unittest_workflows=unittest_workflows, - )) + with open(os.path.join(d, "config.yml"), "w") as f: + f.write( + env.get_template("config.yml.in").render( + build_workflows=build_workflows, + unittest_workflows=unittest_workflows, + ) + ) f.write("\n") diff --git a/.circleci/unittest/linux/scripts/run_clang_format.py b/.circleci/unittest/linux/scripts/run_clang_format.py index fd2913bd70..250cc6e387 100755 --- a/.circleci/unittest/linux/scripts/run_clang_format.py +++ b/.circleci/unittest/linux/scripts/run_clang_format.py @@ -19,7 +19,6 @@ import subprocess import sys import traceback - from functools import partial try: @@ -28,7 +27,7 @@ DEVNULL = open(os.devnull, "wb") -DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx,cu' +DEFAULT_EXTENSIONS = "c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx,cu" class ExitStatus: @@ -52,14 +51,8 @@ def list_files(files, recursive=False, extensions=None, exclude=None): # os.walk() supports trimming down the dnames list # by modifying it in-place, # to avoid unnecessary directory listings. - dnames[:] = [ - x for x in dnames - if - not fnmatch.fnmatch(os.path.join(dirpath, x), pattern) - ] - fpaths = [ - x for x in fpaths if not fnmatch.fnmatch(x, pattern) - ] + dnames[:] = [x for x in dnames if not fnmatch.fnmatch(os.path.join(dirpath, x), pattern)] + fpaths = [x for x in fpaths if not fnmatch.fnmatch(x, pattern)] for f in fpaths: ext = os.path.splitext(f)[1][1:] if ext in extensions: @@ -72,11 +65,9 @@ def list_files(files, recursive=False, extensions=None, exclude=None): def make_diff(file, original, reformatted): return list( difflib.unified_diff( - original, - reformatted, - fromfile='{}\t(original)'.format(file), - tofile='{}\t(reformatted)'.format(file), - n=3)) + original, reformatted, fromfile="{}\t(original)".format(file), tofile="{}\t(reformatted)".format(file), n=3 + ) + ) class DiffError(Exception): @@ -99,13 +90,12 @@ def run_clang_format_diff_wrapper(args, file): except DiffError: raise except Exception as e: - raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__, - e), e) + raise UnexpectedError("{}: {}: {}".format(file, e.__class__.__name__, e), e) def run_clang_format_diff(args, file): try: - with io.open(file, 'r', encoding='utf-8') as f: + with io.open(file, "r", encoding="utf-8") as f: original = f.readlines() except IOError as exc: raise DiffError(str(exc)) @@ -130,17 +120,10 @@ def run_clang_format_diff(args, file): try: proc = subprocess.Popen( - invocation, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - encoding='utf-8') - except OSError as exc: - raise DiffError( - "Command '{}' failed to start: {}".format( - subprocess.list2cmdline(invocation), exc - ) + invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding="utf-8" ) + except OSError as exc: + raise DiffError("Command '{}' failed to start: {}".format(subprocess.list2cmdline(invocation), exc)) proc_stdout = proc.stdout proc_stderr = proc.stderr @@ -159,30 +142,30 @@ def run_clang_format_diff(args, file): def bold_red(s): - return '\x1b[1m\x1b[31m' + s + '\x1b[0m' + return "\x1b[1m\x1b[31m" + s + "\x1b[0m" def colorize(diff_lines): def bold(s): - return '\x1b[1m' + s + '\x1b[0m' + return "\x1b[1m" + s + "\x1b[0m" def cyan(s): - return '\x1b[36m' + s + '\x1b[0m' + return "\x1b[36m" + s + "\x1b[0m" def green(s): - return '\x1b[32m' + s + '\x1b[0m' + return "\x1b[32m" + s + "\x1b[0m" def red(s): - return '\x1b[31m' + s + '\x1b[0m' + return "\x1b[31m" + s + "\x1b[0m" for line in diff_lines: - if line[:4] in ['--- ', '+++ ']: + if line[:4] in ["--- ", "+++ "]: yield bold(line) - elif line.startswith('@@ '): + elif line.startswith("@@ "): yield cyan(line) - elif line.startswith('+'): + elif line.startswith("+"): yield green(line) - elif line.startswith('-'): + elif line.startswith("-"): yield red(line) else: yield line @@ -195,7 +178,7 @@ def print_diff(diff_lines, use_color): def print_trouble(prog, message, use_colors): - error_text = 'error:' + error_text = "error:" if use_colors: error_text = bold_red(error_text) print("{}: {} {}".format(prog, error_text, message), file=sys.stderr) @@ -204,45 +187,37 @@ def print_trouble(prog, message, use_colors): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - '--clang-format-executable', - metavar='EXECUTABLE', - help='path to the clang-format executable', - default='clang-format') - parser.add_argument( - '--extensions', - help='comma separated list of file extensions (default: {})'.format( - DEFAULT_EXTENSIONS), - default=DEFAULT_EXTENSIONS) + "--clang-format-executable", + metavar="EXECUTABLE", + help="path to the clang-format executable", + default="clang-format", + ) parser.add_argument( - '-r', - '--recursive', - action='store_true', - help='run recursively over directories') - parser.add_argument('files', metavar='file', nargs='+') + "--extensions", + help="comma separated list of file extensions (default: {})".format(DEFAULT_EXTENSIONS), + default=DEFAULT_EXTENSIONS, + ) + parser.add_argument("-r", "--recursive", action="store_true", help="run recursively over directories") + parser.add_argument("files", metavar="file", nargs="+") + parser.add_argument("-q", "--quiet", action="store_true") parser.add_argument( - '-q', - '--quiet', - action='store_true') - parser.add_argument( - '-j', - metavar='N', + "-j", + metavar="N", type=int, default=0, - help='run N clang-format jobs in parallel' - ' (default number of cpus + 1)') + help="run N clang-format jobs in parallel" " (default number of cpus + 1)", + ) parser.add_argument( - '--color', - default='auto', - choices=['auto', 'always', 'never'], - help='show colored diff (default: auto)') + "--color", default="auto", choices=["auto", "always", "never"], help="show colored diff (default: auto)" + ) parser.add_argument( - '-e', - '--exclude', - metavar='PATTERN', - action='append', + "-e", + "--exclude", + metavar="PATTERN", + action="append", default=[], - help='exclude paths matching the given glob-like pattern(s)' - ' from recursive search') + help="exclude paths matching the given glob-like pattern(s)" " from recursive search", + ) args = parser.parse_args() @@ -259,10 +234,10 @@ def main(): colored_stdout = False colored_stderr = False - if args.color == 'always': + if args.color == "always": colored_stdout = True colored_stderr = True - elif args.color == 'auto': + elif args.color == "auto": colored_stdout = sys.stdout.isatty() colored_stderr = sys.stderr.isatty() @@ -275,19 +250,15 @@ def main(): except OSError as e: print_trouble( parser.prog, - "Command '{}' failed to start: {}".format( - subprocess.list2cmdline(version_invocation), e - ), + "Command '{}' failed to start: {}".format(subprocess.list2cmdline(version_invocation), e), use_colors=colored_stderr, ) return ExitStatus.TROUBLE retcode = ExitStatus.SUCCESS files = list_files( - args.files, - recursive=args.recursive, - exclude=args.exclude, - extensions=args.extensions.split(',')) + args.files, recursive=args.recursive, exclude=args.exclude, extensions=args.extensions.split(",") + ) if not files: return @@ -304,8 +275,7 @@ def main(): pool = None else: pool = multiprocessing.Pool(njobs) - it = pool.imap_unordered( - partial(run_clang_format_diff_wrapper, args), files) + it = pool.imap_unordered(partial(run_clang_format_diff_wrapper, args), files) while True: try: outs, errs = next(it) @@ -336,5 +306,5 @@ def main(): return retcode -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/.github/process_commit.py b/.github/process_commit.py index f9ef302533..1e38ea70e9 100644 --- a/.github/process_commit.py +++ b/.github/process_commit.py @@ -45,13 +45,13 @@ def query_torchaudio(cmd: str, *, accept) -> Any: def get_pr_merger_and_number(commit_hash: str) -> Optional[str]: data = query_torchaudio(f"commits/{commit_hash}", accept="application/vnd.github.v3+json") - commit_message = data['commit']['message'] + commit_message = data["commit"]["message"] - pulled_by = commit_message.split('Pulled By: ') - pulled_by = pulled_by[1].split('\n')[0] if len(pulled_by) > 1 else None + pulled_by = commit_message.split("Pulled By: ") + pulled_by = pulled_by[1].split("\n")[0] if len(pulled_by) > 1 else None - pr_number = commit_message.split('Pull Request resolved: https://github.com/pytorch/audio/pull/') - pr_number = pr_number[1].split('\n')[0] if len(pr_number) > 1 else None + pr_number = commit_message.split("Pull Request resolved: https://github.com/pytorch/audio/pull/") + pr_number = pr_number[1].split("\n")[0] if len(pr_number) > 1 else None return pulled_by, pr_number diff --git a/docs/source/conf.py b/docs/source/conf.py index a181d74066..ffe8c61c22 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,97 +22,98 @@ # sys.path.insert(0, os.path.abspath('.')) import os import re + import pytorch_sphinx_theme # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = '1.6' +needs_sphinx = "1.6" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinxcontrib.katex', - 'sphinxcontrib.bibtex', - 'sphinx_gallery.gen_gallery', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinxcontrib.katex", + "sphinxcontrib.bibtex", + "sphinx_gallery.gen_gallery", ] # katex options # # -katex_options = r''' +katex_options = r""" delimiters : [ {left: "$$", right: "$$", display: true}, {left: "\\(", right: "\\)", display: false}, {left: "\\[", right: "\\]", display: true} ] -''' +""" -bibtex_bibfiles = ['refs.bib'] +bibtex_bibfiles = ["refs.bib"] def _get_var(var, default=False): if var not in os.environ: return default - val = os.environ.get(var, '0') - trues = ['1', 'true', 'TRUE', 'on', 'ON', 'yes', 'YES'] - falses = ['0', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO'] + val = os.environ.get(var, "0") + trues = ["1", "true", "TRUE", "on", "ON", "yes", "YES"] + falses = ["0", "false", "FALSE", "off", "OFF", "no", "NO"] if val in trues: return True if val not in falses: print( - f' --- WARNING: Unexpected environment variable value `{var}={val}`. ' - f'Expected one of {trues + falses}') + f" --- WARNING: Unexpected environment variable value `{var}={val}`. " f"Expected one of {trues + falses}" + ) return False def _get_pattern(): - pattern = os.getenv('GALLERY_PATTERN') + pattern = os.getenv("GALLERY_PATTERN") # If BUILD_GALLERY is falsy -> no build # If BUILD_GALLERY is truey -> build # If BUILD_GALLERY is undefined # If GALLERY_PATTERN is defined -> build # If GALLERY_PATTERN is not defined -> not build - if not _get_var('BUILD_GALLERY', default=False if pattern is None else True): + if not _get_var("BUILD_GALLERY", default=False if pattern is None else True): if pattern is not None: print( ' --- WARNING: "GALLERY_PATTERN" is provided, but "BUILD_GALLERY" value is falsy. ' - 'Sphinx galleries are not built. To build galleries, set `BUILD_GALLERY=1`.' + "Sphinx galleries are not built. To build galleries, set `BUILD_GALLERY=1`." ) return { - 'ignore_pattern': r'\.py', + "ignore_pattern": r"\.py", } - ret = {'filename_pattern': 'tutorial.py'} - if os.getenv('GALLERY_PATTERN'): + ret = {"filename_pattern": "tutorial.py"} + if os.getenv("GALLERY_PATTERN"): # See https://github.com/pytorch/tutorials/blob/cbf2238df0e78d84c15bd94288966d2f4b2e83ae/conf.py#L75-L83 - ret['ignore_pattern'] = r'/(?!' + re.escape(os.getenv('GALLERY_PATTERN')) + r')[^/]+$' + ret["ignore_pattern"] = r"/(?!" + re.escape(os.getenv("GALLERY_PATTERN")) + r")[^/]+$" return ret sphinx_gallery_conf = { - 'examples_dirs': [ - '../../examples/tutorials', + "examples_dirs": [ + "../../examples/tutorials", ], - 'gallery_dirs': [ - 'tutorials', + "gallery_dirs": [ + "tutorials", ], **_get_pattern(), - 'backreferences_dir': 'gen_modules/backreferences', - 'first_notebook_cell': None, - 'doc_module': ('torchaudio',), + "backreferences_dir": "gen_modules/backreferences", + "first_notebook_cell": None, + "doc_module": ("torchaudio",), } autosummary_generate = True @@ -121,21 +122,21 @@ def _get_pattern(): napoleon_google_docstring = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Torchaudio' -copyright = '2018, Torchaudio Contributors' -author = 'Torchaudio Contributors' +project = "Torchaudio" +copyright = "2018, Torchaudio Contributors" +author = "Torchaudio Contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -143,10 +144,10 @@ def _get_pattern(): # # The short X.Y version. # TODO: change to [:2] at v1.0 -version = 'main ' +version = "main " # The full version, including alpha/beta/rc tags. # TODO: verify this works as expected -release = 'main' +release = "main" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -158,10 +159,10 @@ def _get_pattern(): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['*/index.rst'] +exclude_patterns = ["*/index.rst"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -172,7 +173,7 @@ def _get_pattern(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'pytorch_sphinx_theme' +html_theme = "pytorch_sphinx_theme" html_theme_path = [pytorch_sphinx_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme @@ -180,28 +181,26 @@ def _get_pattern(): # documentation. # html_theme_options = { - 'pytorch_project': 'audio', - 'collapse_navigation': False, - 'display_version': True, - 'logo_only': True, - 'navigation_with_keys': True, - 'analytics_id': 'UA-117752657-2', + "pytorch_project": "audio", + "collapse_navigation": False, + "display_version": True, + "logo_only": True, + "navigation_with_keys": True, + "analytics_id": "UA-117752657-2", } -html_logo = '_static/img/pytorch-logo-dark.svg' +html_logo = "_static/img/pytorch-logo-dark.svg" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_css_files = [ - 'https://cdn.jsdelivr.net/npm/katex@0.10.0-beta/dist/katex.min.css' -] +html_static_path = ["_static"] +html_css_files = ["https://cdn.jsdelivr.net/npm/katex@0.10.0-beta/dist/katex.min.css"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'TorchAudiodoc' +htmlhelp_basename = "TorchAudiodoc" # -- Options for LaTeX output --------------------------------------------- @@ -210,15 +209,12 @@ def _get_pattern(): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -228,8 +224,7 @@ def _get_pattern(): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pytorch.tex', 'Torchaudio Documentation', - 'Torch Contributors', 'manual'), + (master_doc, "pytorch.tex", "Torchaudio Documentation", "Torch Contributors", "manual"), ] @@ -237,10 +232,7 @@ def _get_pattern(): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'Torchaudio', 'Torchaudio Documentation', - [author], 1) -] +man_pages = [(master_doc, "Torchaudio", "Torchaudio Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -249,25 +241,31 @@ def _get_pattern(): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Torchaudio', 'Torchaudio Documentation', - author, 'Torchaudio', 'Load audio files into pytorch tensors.', - 'Miscellaneous'), + ( + master_doc, + "Torchaudio", + "Torchaudio Documentation", + author, + "Torchaudio", + "Load audio files into pytorch tensors.", + "Miscellaneous", + ), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'torch': ('https://pytorch.org/docs/stable/', None), + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "torch": ("https://pytorch.org/docs/stable/", None), } # -- A patch that prevents Sphinx from cross-referencing ivar tags ------- # See http://stackoverflow.com/a/41184353/3343043 from docutils import nodes -from sphinx.util.docfields import TypedField from sphinx import addnodes +from sphinx.util.docfields import TypedField def patched_make_field(self, types, domain, items, **kw): @@ -277,39 +275,39 @@ def patched_make_field(self, types, domain, items, **kw): # type: (list, str, tuple) -> nodes.field def handle_item(fieldarg, content): par = nodes.paragraph() - par += addnodes.literal_strong('', fieldarg) # Patch: this line added + par += addnodes.literal_strong("", fieldarg) # Patch: this line added # par.extend(self.make_xrefs(self.rolename, domain, fieldarg, # addnodes.literal_strong)) if fieldarg in types: - par += nodes.Text(' (') + par += nodes.Text(" (") # NOTE: using .pop() here to prevent a single type node to be # inserted twice into the doctree, which leads to # inconsistencies later when references are resolved fieldtype = types.pop(fieldarg) if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text): - typename = u''.join(n.astext() for n in fieldtype) - typename = typename.replace('int', 'python:int') - typename = typename.replace('long', 'python:long') - typename = typename.replace('float', 'python:float') - typename = typename.replace('type', 'python:type') - par.extend(self.make_xrefs(self.typerolename, domain, typename, - addnodes.literal_emphasis, **kw)) + typename = "".join(n.astext() for n in fieldtype) + typename = typename.replace("int", "python:int") + typename = typename.replace("long", "python:long") + typename = typename.replace("float", "python:float") + typename = typename.replace("type", "python:type") + par.extend(self.make_xrefs(self.typerolename, domain, typename, addnodes.literal_emphasis, **kw)) else: par += fieldtype - par += nodes.Text(')') - par += nodes.Text(' -- ') + par += nodes.Text(")") + par += nodes.Text(" -- ") par += content return par - fieldname = nodes.field_name('', self.label) + fieldname = nodes.field_name("", self.label) if len(items) == 1 and self.can_collapse: fieldarg, content = items[0] bodynode = handle_item(fieldarg, content) else: bodynode = self.list_type() for fieldarg, content in items: - bodynode += nodes.list_item('', handle_item(fieldarg, content)) - fieldbody = nodes.field_body('', bodynode) - return nodes.field('', fieldname, fieldbody) + bodynode += nodes.list_item("", handle_item(fieldarg, content)) + fieldbody = nodes.field_body("", bodynode) + return nodes.field("", fieldname, fieldbody) + TypedField.make_field = patched_make_field diff --git a/examples/asr/librispeech_emformer_rnnt/eval.py b/examples/asr/librispeech_emformer_rnnt/eval.py index c85ad46388..2cae5de8b6 100644 --- a/examples/asr/librispeech_emformer_rnnt/eval.py +++ b/examples/asr/librispeech_emformer_rnnt/eval.py @@ -1,10 +1,9 @@ -from argparse import ArgumentParser import logging import pathlib +from argparse import ArgumentParser import torch import torchaudio - from lightning import RNNTModule @@ -12,9 +11,7 @@ def compute_word_level_distance(seq1, seq2): - return torchaudio.functional.edit_distance( - seq1.lower().split(), seq2.lower().split() - ) + return torchaudio.functional.edit_distance(seq1.lower().split(), seq2.lower().split()) def run_eval(args): @@ -38,9 +35,7 @@ def run_eval(args): total_edit_distance += compute_word_level_distance(actual, predicted) total_length += len(actual.split()) if idx % 100 == 0: - logger.info( - f"Processed elem {idx}; WER: {total_edit_distance / total_length}" - ) + logger.info(f"Processed elem {idx}; WER: {total_edit_distance / total_length}") logger.info(f"Final WER: {total_edit_distance / total_length}") @@ -58,13 +53,20 @@ def cli_main(): help="Path to JSON file containing feature means and stddevs.", ) parser.add_argument( - "--librispeech_path", type=pathlib.Path, help="Path to LibriSpeech datasets.", + "--librispeech_path", + type=pathlib.Path, + help="Path to LibriSpeech datasets.", ) parser.add_argument( - "--sp_model_path", type=pathlib.Path, help="Path to SentencePiece model.", + "--sp_model_path", + type=pathlib.Path, + help="Path to SentencePiece model.", ) parser.add_argument( - "--use_cuda", action="store_true", default=False, help="Run using CUDA.", + "--use_cuda", + action="store_true", + default=False, + help="Run using CUDA.", ) args = parser.parse_args() run_eval(args) diff --git a/examples/asr/librispeech_emformer_rnnt/lightning.py b/examples/asr/librispeech_emformer_rnnt/lightning.py index fbc900f2de..0bf5cb2895 100644 --- a/examples/asr/librispeech_emformer_rnnt/lightning.py +++ b/examples/asr/librispeech_emformer_rnnt/lightning.py @@ -1,30 +1,25 @@ -from collections import namedtuple import json import math import os +from collections import namedtuple from typing import List, Tuple import sentencepiece as spm - import torch import torchaudio import torchaudio.functional as F +from pytorch_lightning import LightningModule from torchaudio.prototype.rnnt import emformer_rnnt_base from torchaudio.prototype.rnnt_decoder import Hypothesis, RNNTBeamSearch -from pytorch_lightning import LightningModule -Batch = namedtuple( - "Batch", ["features", "feature_lengths", "targets", "target_lengths"] -) +Batch = namedtuple("Batch", ["features", "feature_lengths", "targets", "target_lengths"]) _decibel = 2 * 20 * math.log10(torch.iinfo(torch.int16).max) _gain = pow(10, 0.05 * _decibel) -_spectrogram_transform = torchaudio.transforms.MelSpectrogram( - sample_rate=16000, n_fft=400, n_mels=80, hop_length=160 -) +_spectrogram_transform = torchaudio.transforms.MelSpectrogram(sample_rate=16000, n_fft=400, n_mels=80, hop_length=160) def _batch_by_token_count(idx_target_lengths, token_limit): @@ -61,9 +56,7 @@ def __init__(self, base_dataset, max_token_limit): assert len(idx_target_lengths) > 0 - idx_target_lengths = sorted( - idx_target_lengths, key=lambda x: x[1], reverse=True - ) + idx_target_lengths = sorted(idx_target_lengths, key=lambda x: x[1], reverse=True) assert max_token_limit >= idx_target_lengths[0][1] @@ -74,9 +67,7 @@ def _target_length(self, fileid, fileid_to_target_length): speaker_id, chapter_id, _ = fileid.split("-") file_text = speaker_id + "-" + chapter_id + self.base_dataset._ext_txt - file_text = os.path.join( - self.base_dataset._path, speaker_id, chapter_id, file_text - ) + file_text = os.path.join(self.base_dataset._path, speaker_id, chapter_id, file_text) with open(file_text) as ft: for line in ft: @@ -93,24 +84,16 @@ def __len__(self): class TimeMasking(torchaudio.transforms._AxisMasking): - def __init__( - self, time_mask_param: int, min_mask_p: float, iid_masks: bool = False - ) -> None: + def __init__(self, time_mask_param: int, min_mask_p: float, iid_masks: bool = False) -> None: super(TimeMasking, self).__init__(time_mask_param, 2, iid_masks) self.min_mask_p = min_mask_p def forward(self, specgram: torch.Tensor, mask_value: float = 0.0) -> torch.Tensor: if self.iid_masks and specgram.dim() == 4: - mask_param = min( - self.mask_param, self.min_mask_p * specgram.shape[self.axis + 1] - ) - return F.mask_along_axis_iid( - specgram, mask_param, mask_value, self.axis + 1 - ) + mask_param = min(self.mask_param, self.min_mask_p * specgram.shape[self.axis + 1]) + return F.mask_along_axis_iid(specgram, mask_param, mask_value, self.axis + 1) else: - mask_param = min( - self.mask_param, self.min_mask_p * specgram.shape[self.axis] - ) + mask_param = min(self.mask_param, self.min_mask_p * specgram.shape[self.axis]) return F.mask_along_axis(specgram, mask_param, mask_value, self.axis) @@ -149,10 +132,7 @@ def __init__(self, optimizer, warmup_updates, last_epoch=-1, verbose=False): super().__init__(optimizer, last_epoch=last_epoch, verbose=verbose) def get_lr(self): - return [ - (min(1.0, self._step_count / self.warmup_updates)) * base_lr - for base_lr in self.base_lrs - ] + return [(min(1.0, self._step_count / self.warmup_updates)) * base_lr for base_lr in self.base_lrs] def post_process_hypos( @@ -164,12 +144,7 @@ def post_process_hypos( sp_model.pad_id(), ] filtered_hypo_tokens = [ - [ - token_index - for token_index in h.tokens[1:] - if token_index not in post_process_remove_list - ] - for h in hypos + [token_index for token_index in h.tokens[1:] if token_index not in post_process_remove_list] for h in hypos ] hypos_str = [sp_model.decode(s) for s in filtered_hypo_tokens] hypos_ali = [h.alignment[1:] for h in hypos] @@ -193,12 +168,8 @@ def __init__( self.model = emformer_rnnt_base() self.loss = torchaudio.transforms.RNNTLoss(reduction="sum", clamp=1.0) - self.optimizer = torch.optim.Adam( - self.model.parameters(), lr=5e-4, betas=(0.9, 0.999), eps=1e-8 - ) - self.lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( - self.optimizer, factor=0.96, patience=0 - ) + self.optimizer = torch.optim.Adam(self.model.parameters(), lr=5e-4, betas=(0.9, 0.999), eps=1e-8) + self.lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, factor=0.96, patience=0) self.warmup_lr_scheduler = WarmupLR(self.optimizer, 10000) self.train_data_pipeline = torch.nn.Sequential( @@ -236,27 +207,17 @@ def _extract_labels(self, samples: List): return targets, lengths def _train_extract_features(self, samples: List): - mel_features = [ - _spectrogram_transform(sample[0].squeeze()).transpose(1, 0) - for sample in samples - ] + mel_features = [_spectrogram_transform(sample[0].squeeze()).transpose(1, 0) for sample in samples] features = torch.nn.utils.rnn.pad_sequence(mel_features, batch_first=True) features = self.train_data_pipeline(features) - lengths = torch.tensor( - [elem.shape[0] for elem in mel_features], dtype=torch.int32 - ) + lengths = torch.tensor([elem.shape[0] for elem in mel_features], dtype=torch.int32) return features, lengths def _valid_extract_features(self, samples: List): - mel_features = [ - _spectrogram_transform(sample[0].squeeze()).transpose(1, 0) - for sample in samples - ] + mel_features = [_spectrogram_transform(sample[0].squeeze()).transpose(1, 0) for sample in samples] features = torch.nn.utils.rnn.pad_sequence(mel_features, batch_first=True) features = self.valid_data_pipeline(features) - lengths = torch.tensor( - [elem.shape[0] for elem in mel_features], dtype=torch.int32 - ) + lengths = torch.tensor([elem.shape[0] for elem in mel_features], dtype=torch.int32) return features, lengths def _train_collate_fn(self, samples: List): @@ -276,9 +237,7 @@ def _step(self, batch, batch_idx, step_type): if batch is None: return None - prepended_targets = batch.targets.new_empty( - [batch.targets.size(0), batch.targets.size(1) + 1] - ) + prepended_targets = batch.targets.new_empty([batch.targets.size(0), batch.targets.size(1) + 1]) prepended_targets[:, 1:] = batch.targets prepended_targets[:, 0] = self.blank_idx prepended_target_lengths = batch.target_lengths + 1 @@ -307,9 +266,7 @@ def configure_optimizers(self): def forward(self, batch: Batch): decoder = RNNTBeamSearch(self.model, self.blank_idx) - hypotheses = decoder( - batch.features.to(self.device), batch.feature_lengths.to(self.device), 20 - ) + hypotheses = decoder(batch.features.to(self.device), batch.feature_lengths.to(self.device), 20) return post_process_hypos(hypotheses, self.sp_model)[0][0] def training_step(self, batch: Batch, batch_idx): @@ -325,21 +282,15 @@ def train_dataloader(self): dataset = torch.utils.data.ConcatDataset( [ CustomDataset( - torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="train-clean-360" - ), + torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="train-clean-360"), 1000, ), CustomDataset( - torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="train-clean-100" - ), + torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="train-clean-100"), 1000, ), CustomDataset( - torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="train-other-500" - ), + torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="train-other-500"), 1000, ), ] @@ -357,29 +308,24 @@ def val_dataloader(self): dataset = torch.utils.data.ConcatDataset( [ CustomDataset( - torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="dev-clean" - ), + torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="dev-clean"), 1000, ), CustomDataset( - torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="dev-other" - ), + torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="dev-other"), 1000, ), ] ) dataloader = torch.utils.data.DataLoader( - dataset, batch_size=None, collate_fn=self._valid_collate_fn, num_workers=10, + dataset, + batch_size=None, + collate_fn=self._valid_collate_fn, + num_workers=10, ) return dataloader def test_dataloader(self): - dataset = torchaudio.datasets.LIBRISPEECH( - self.librispeech_path, url="test-clean" - ) - dataloader = torch.utils.data.DataLoader( - dataset, batch_size=1, collate_fn=self._test_collate_fn - ) + dataset = torchaudio.datasets.LIBRISPEECH(self.librispeech_path, url="test-clean") + dataloader = torch.utils.data.DataLoader(dataset, batch_size=1, collate_fn=self._test_collate_fn) return dataloader diff --git a/examples/asr/librispeech_emformer_rnnt/train.py b/examples/asr/librispeech_emformer_rnnt/train.py index 435b8eff7b..e18ba21f6e 100644 --- a/examples/asr/librispeech_emformer_rnnt/train.py +++ b/examples/asr/librispeech_emformer_rnnt/train.py @@ -1,11 +1,10 @@ -from argparse import ArgumentParser import pathlib +from argparse import ArgumentParser +from lightning import RNNTModule from pytorch_lightning import Trainer from pytorch_lightning.callbacks import ModelCheckpoint -from lightning import RNNTModule - def run_train(args): checkpoint_dir = args.exp_dir / "checkpoints" @@ -63,10 +62,14 @@ def cli_main(): help="Path to JSON file containing feature means and stddevs.", ) parser.add_argument( - "--librispeech_path", type=pathlib.Path, help="Path to LibriSpeech datasets.", + "--librispeech_path", + type=pathlib.Path, + help="Path to LibriSpeech datasets.", ) parser.add_argument( - "--sp_model_path", type=pathlib.Path, help="Path to SentencePiece model.", + "--sp_model_path", + type=pathlib.Path, + help="Path to SentencePiece model.", ) parser.add_argument( "--num_nodes", diff --git a/examples/hubert/dataset/hubert_dataset.py b/examples/hubert/dataset/hubert_dataset.py index 8d415e763f..36dd879b1c 100644 --- a/examples/hubert/dataset/hubert_dataset.py +++ b/examples/hubert/dataset/hubert_dataset.py @@ -1,3 +1,4 @@ +import random from pathlib import Path from typing import ( Dict, @@ -7,11 +8,11 @@ Tuple, Union, ) -from torch import Tensor + import numpy as np -import random import torch import torchaudio +from torch import Tensor from torch.utils.data import Dataset, BatchSampler @@ -30,27 +31,22 @@ class BucketizeSampler(BatchSampler): the lengths of samples are unknown, the batch size may be different for different mini-batches. """ + def __init__( self, data_source: Dataset, num_buckets: int, max_token_count: Optional[int] = None, - batch_size: Optional[int] = None + batch_size: Optional[int] = None, ) -> None: if max_token_count is not None and batch_size is not None: - raise AssertionError( - "The ``max_token_count`` and ``batch_size`` can't be both set." - ) + raise AssertionError("The ``max_token_count`` and ``batch_size`` can't be both set.") self.data_source = data_source self.max_token_count = max_token_count self.batch_size = batch_size self.buckets = self._get_buckets(self.data_source, num_buckets) - def _get_buckets( - self, - data_source: Dataset, - num_buckets: int - ) -> Dict[int, Tensor]: + def _get_buckets(self, data_source: Dataset, num_buckets: int) -> Dict[int, Tensor]: """Generate buckets based on the dataset. Args: data_source (Dataset): The dataset object to bucketize. @@ -126,6 +122,7 @@ class HuBERTDataSet(Dataset): min_sample (int): The minimum number of audio samples in the dataset. (Default: 32000) max_sample (int): The maximum number of audio samples in the dataset. (Default: 250000) """ + def __init__( self, exp_dir: Union[str, Path], @@ -137,13 +134,7 @@ def __init__( self.exp_dir = Path(exp_dir) tsv_dir = self.exp_dir / "tsv" label_dir = self.exp_dir / "label" - f_list, ind_list, len_list = self._get_lists( - tsv_dir, - dataset, - subset, - min_sample, - max_sample - ) + f_list, ind_list, len_list = self._get_lists(tsv_dir, dataset, subset, min_sample, max_sample) self.f_list, self.ind_list, self.len_list = f_list, ind_list, len_list self.labels = self._load_labels(label_dir, dataset, subset) @@ -188,10 +179,7 @@ def _get_lists( len_list.append(ele[2]) return np.asarray(f_list), np.asarray(ind_list), np.asarray(len_list) - def _load_audio( - self, - index: int - ) -> Tensor: + def _load_audio(self, index: int) -> Tensor: """Load waveform given the sample index of the dataset. Args: index (int): The sample index. @@ -204,12 +192,7 @@ def _load_audio( assert waveform.shape[1] == self.len_list[index] return waveform - def _load_labels( - self, - label_dir: Path, - dataset: str, - subset: str - ) -> np.array: + def _load_labels(self, label_dir: Path, dataset: str, subset: str) -> np.array: """Load all labels to memory into a numpy array. Args: label_dir (Path): The directory that contains the label file. @@ -245,6 +228,7 @@ class CollateFnHubert: waveform and label is random if the length is longer than the minimum length in the mini-batch. """ + def __init__( self, feature_type: str, @@ -284,7 +268,7 @@ def __call__(self, batch: Tuple[Tensor, Tensor, int]) -> Tuple[Tensor, Tensor, T data = torch.zeros(len(batch), audio_size) for i in range(len(waveforms)): - data[i][0:waveforms[i].shape[1]] = waveforms[i][0] + data[i][0 : waveforms[i].shape[1]] = waveforms[i][0] lengths = torch.tensor(lengths) labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True) return data, labels, lengths @@ -318,16 +302,10 @@ def _collate_audio_label( diff = waveform.size(1) - audio_size audio_start = torch.randint(diff, size=(1,)) if rand_crop else 0 label_start = torch.div( - audio_start - kernel_size * sample_rate, - stride * sample_rate, - rounding_mode='floor' - ) - label_size = torch.div( - audio_size - kernel_size * sample_rate, - stride * sample_rate, - rounding_mode='floor' + audio_start - kernel_size * sample_rate, stride * sample_rate, rounding_mode="floor" ) - waveform = waveform[:, audio_start:audio_start + audio_size] - label = label[label_start:label_start + label_size] + label_size = torch.div(audio_size - kernel_size * sample_rate, stride * sample_rate, rounding_mode="floor") + waveform = waveform[:, audio_start : audio_start + audio_size] + label = label[label_start : label_start + label_size] length = audio_size return waveform, label, length diff --git a/examples/hubert/preprocess.py b/examples/hubert/preprocess.py index b1864e2a67..eb817af521 100644 --- a/examples/hubert/preprocess.py +++ b/examples/hubert/preprocess.py @@ -21,9 +21,7 @@ def _init_logger(debug=False): - message_fmt = ( - "%(levelname)5s: %(funcName)10s: %(message)s" if debug else "%(message)s" - ) + message_fmt = "%(levelname)5s: %(funcName)10s: %(message)s" if debug else "%(message)s" logging.basicConfig( level=logging.DEBUG if debug else logging.INFO, format=f"%(asctime)s: {message_fmt}", @@ -84,15 +82,17 @@ def main(args): for split in ["train", "valid"]: p = Pool(args.num_rank) - inputs = [( - tsv_dir / f"{args.dataset}_{split}.tsv", - feat_dir, - split, - rank, - args.num_rank, - device, - args.feat_type, - 16_000,) + inputs = [ + ( + tsv_dir / f"{args.dataset}_{split}.tsv", + feat_dir, + split, + rank, + args.num_rank, + device, + args.feat_type, + 16_000, + ) for rank in range(args.num_rank) ] _ = p.starmap(dump_features, inputs) diff --git a/examples/hubert/utils/common_utils.py b/examples/hubert/utils/common_utils.py index 6ba5b81279..c83b0fa384 100644 --- a/examples/hubert/utils/common_utils.py +++ b/examples/hubert/utils/common_utils.py @@ -37,7 +37,7 @@ def create_tsv( [``librispeech``, ``libri-light``]. (Default: ``librispeech``) valid_percent (float, optional): The percentage of data for validation. (Default: 0.01) seed (int): The seed for randomly selecting the validation files. - extension (str, optional): The extention of audio files. (Default: ``flac``) + extension (str, optional): The extension of audio files. (Default: ``flac``) Returns: None @@ -51,11 +51,7 @@ def create_tsv( if not out_dir.exists(): out_dir.mkdir() - valid_f = ( - open(out_dir / f"{dataset}_valid.tsv", "w") - if valid_percent > 0 - else None - ) + valid_f = open(out_dir / f"{dataset}_valid.tsv", "w") if valid_percent > 0 else None search_pattern = ".*train.*" with open(out_dir / f"{dataset}_train.tsv", "w") as train_f: print(root_dir, file=train_f) @@ -67,20 +63,13 @@ def create_tsv( if re.match(search_pattern, str(fname)): frames = torchaudio.info(fname).num_frames dest = train_f if torch.rand(1) > valid_percent else valid_f - print( - f"{fname.relative_to(root_dir)}\t{frames}", file=dest - ) + print(f"{fname.relative_to(root_dir)}\t{frames}", file=dest) if valid_f is not None: valid_f.close() _LG.info("Finished creating the file lists successfully") -def _get_feat_lens_paths( - feat_dir: Path, - split: str, - rank: int, - num_rank: int -) -> Tuple[Path, Path]: +def _get_feat_lens_paths(feat_dir: Path, split: str, rank: int, num_rank: int) -> Tuple[Path, Path]: r"""Get the feature and lengths paths based on feature directory, data split, rank, and number of ranks. Args: @@ -99,9 +88,7 @@ def _get_feat_lens_paths( return feat_path, len_path -def _get_model_path( - km_dir: Path -) -> Path: +def _get_model_path(km_dir: Path) -> Path: r"""Get the file path of the KMeans clustering model Args: km_dir (Path): The directory to store the KMeans clustering model. diff --git a/examples/hubert/utils/feature_utils.py b/examples/hubert/utils/feature_utils.py index 26078065b5..25e40ba1b9 100644 --- a/examples/hubert/utils/feature_utils.py +++ b/examples/hubert/utils/feature_utils.py @@ -19,11 +19,7 @@ _LG = logging.getLogger(__name__) -def get_shard_range( - num_lines: int, - num_rank: int, - rank: int -) -> Tuple[int, int]: +def get_shard_range(num_lines: int, num_rank: int, rank: int) -> Tuple[int, int]: r"""Get the range of indices for the current rank in multi-processing. Args: num_lines (int): The number of lines to process. @@ -39,10 +35,7 @@ def get_shard_range( assert num_lines > 0, f"Found {num_lines} files, make sure you specify the correct root directory" start = round(num_lines / num_rank * rank) end = round(num_lines / num_rank * (rank + 1)) - _LG.info( - f"rank {rank} of {num_rank}, process {end-start} " - f"({start}-{end}) out of {num_lines}" - ) + _LG.info(f"rank {rank} of {num_rank}, process {end-start} " f"({start}-{end}) out of {num_lines}") return start, end @@ -68,9 +61,7 @@ def extract_feature( waveform = waveform[0].to(device) if feature_type == "mfcc": feature_extractor = torchaudio.transforms.MFCC( - sample_rate=sample_rate, - n_mfcc=13, - melkwargs={'n_fft': 400, 'hop_length': 160, 'center': False} + sample_rate=sample_rate, n_mfcc=13, melkwargs={"n_fft": 400, "hop_length": 160, "center": False} ).to(device) mfccs = feature_extractor(waveform) # (freq, time) # mfccs = torchaudio.compliance.kaldi.mfcc( diff --git a/examples/hubert/utils/kmeans.py b/examples/hubert/utils/kmeans.py index 46a99c4ca2..c172bf1ff8 100644 --- a/examples/hubert/utils/kmeans.py +++ b/examples/hubert/utils/kmeans.py @@ -126,11 +126,7 @@ def __init__(self, km_path, device): self.Cnorm = torch.from_numpy(self.Cnorm_np).to(device) def __call__(self, x): - dist = ( - x.pow(2).sum(1, keepdim=True) - - 2 * torch.matmul(x, self.C) - + self.Cnorm - ) + dist = x.pow(2).sum(1, keepdim=True) - 2 * torch.matmul(x, self.C) + self.Cnorm return dist.argmin(dim=1).cpu().numpy() @@ -171,7 +167,7 @@ def get_km_label( assert feats.shape[0] == lens.sum() with open(label_path, "w") as f: for i in range(lens.shape[0]): - feat = feats[offset:offset + lens[i]].to(device) + feat = feats[offset : offset + lens[i]].to(device) offset += lens[i] label = apply_kmeans(feat).tolist() f.write(" ".join(map(str, label)) + "\n") diff --git a/examples/interactive_asr/__init__.py b/examples/interactive_asr/__init__.py index 57e1c91022..3862787d59 100644 --- a/examples/interactive_asr/__init__.py +++ b/examples/interactive_asr/__init__.py @@ -1,3 +1,3 @@ from . import utils, vad -__all__ = ['utils', 'vad'] +__all__ = ["utils", "vad"] diff --git a/examples/interactive_asr/asr.py b/examples/interactive_asr/asr.py index 8e2510f347..469cbf6c3a 100644 --- a/examples/interactive_asr/asr.py +++ b/examples/interactive_asr/asr.py @@ -13,7 +13,6 @@ import logging from fairseq import options - from interactive_asr.utils import add_asr_eval_argument, setup_asr, get_microphone_transcription, transcribe_file @@ -29,11 +28,7 @@ def main(args): print("transcription_time:", transcription_time) else: for transcription in get_microphone_transcription(args, task, generator, models, sp, tgt_dict): - print( - "{}: {}".format( - dt.datetime.now().strftime("%H:%M:%S"), transcription[0][0] - ) - ) + print("{}: {}".format(dt.datetime.now().strftime("%H:%M:%S"), transcription[0][0])) def cli_main(): diff --git a/examples/interactive_asr/utils.py b/examples/interactive_asr/utils.py index 3aae6c6864..444f1de9f3 100644 --- a/examples/interactive_asr/utils.py +++ b/examples/interactive_asr/utils.py @@ -9,13 +9,11 @@ import sys import time +import sentencepiece as spm import torch import torchaudio -import sentencepiece as spm - from fairseq import tasks from fairseq.utils import load_ensemble_for_inference, import_user_module - from interactive_asr.vad import get_microphone_chunks @@ -24,9 +22,7 @@ def add_asr_eval_argument(parser): parser.add_argument("--ctc", action="store_true", help="decode a ctc model") parser.add_argument("--rnnt", default=False, help="decode a rnnt model") parser.add_argument("--kspmodel", default=None, help="sentence piece model") - parser.add_argument( - "--wfstlm", default=None, help="wfstlm on dictonary output units" - ) + parser.add_argument("--wfstlm", default=None, help="wfstlm on dictonary output units") parser.add_argument( "--rnnt_decoding_type", default="greedy", @@ -37,20 +33,14 @@ def add_asr_eval_argument(parser): default=0.2, help="weight for wfstlm while interpolating with neural score", ) - parser.add_argument( - "--rnnt_len_penalty", default=-0.5, help="rnnt length penalty on word level" - ) + parser.add_argument("--rnnt_len_penalty", default=-0.5, help="rnnt length penalty on word level") return parser def check_args(args): assert args.path is not None, "--path required for generation!" - assert ( - not args.sampling or args.nbest == args.beam - ), "--sampling requires --nbest to be equal to --beam" - assert ( - args.replace_unk is None or args.raw_text - ), "--replace-unk requires a raw text dataset (--raw-text)" + assert not args.sampling or args.nbest == args.beam, "--sampling requires --nbest to be equal to --beam" + assert args.replace_unk is None or args.raw_text, "--replace-unk requires a raw text dataset (--raw-text)" def process_predictions(args, hypos, sp, tgt_dict): @@ -64,8 +54,7 @@ def process_predictions(args, hypos, sp, tgt_dict): def optimize_models(args, use_cuda, models): - """Optimize ensemble for generation - """ + """Optimize ensemble for generation""" for model in models: model.make_generation_fast_( beamable_mm_beam_size=None if args.no_beamable_mm else args.beam, @@ -166,24 +155,16 @@ def transcribe_file(args, task, generator, models, sp, tgt_dict): raise FileNotFoundError("Audio file not found: {}".format(path)) waveform, sample_rate = torchaudio.load_wav(path) waveform = waveform.mean(0, True) - waveform = torchaudio.transforms.Resample( - orig_freq=sample_rate, new_freq=16000 - )(waveform) + waveform = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(waveform) start = time.time() - transcription = transcribe( - waveform, args, task, generator, models, sp, tgt_dict - ) + transcription = transcribe(waveform, args, task, generator, models, sp, tgt_dict) transcription_time = time.time() - start return transcription_time, transcription def get_microphone_transcription(args, task, generator, models, sp, tgt_dict): for (waveform, sample_rate) in get_microphone_chunks(): - waveform = torchaudio.transforms.Resample( - orig_freq=sample_rate, new_freq=16000 - )(waveform.reshape(1, -1)) - transcription = transcribe( - waveform, args, task, generator, models, sp, tgt_dict - ) + waveform = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(waveform.reshape(1, -1)) + transcription = transcribe(waveform, args, task, generator, models, sp, tgt_dict) yield transcription diff --git a/examples/interactive_asr/vad.py b/examples/interactive_asr/vad.py index bc942032b0..fba13bd963 100644 --- a/examples/interactive_asr/vad.py +++ b/examples/interactive_asr/vad.py @@ -17,14 +17,13 @@ from speech to silence or vice versa. """ -from collections import deque - -import numpy as np -import torch import queue +from collections import deque import librosa +import numpy as np import pyaudio +import torch import torchaudio @@ -95,9 +94,7 @@ def iter(self, frame): elif self.n < self.num_init_frames: self.min_energy = min(energy, self.min_energy) self.min_frequency = min(frequency, self.min_frequency) - self.min_spectral_flatness = min( - spectral_flatness, self.min_spectral_flatness - ) + self.min_spectral_flatness = min(spectral_flatness, self.min_spectral_flatness) self.n += 1 @@ -121,10 +118,7 @@ def iter(self, frame): # Speech detected self.speech_count += 1 # Inertia against switching - if ( - self.n >= self.num_init_frames - and self.speech_count <= self.ignore_speech_count - ): + if self.n >= self.num_init_frames and self.speech_count <= self.ignore_speech_count: # Too soon to change return self.silence_mark else: @@ -132,15 +126,10 @@ def iter(self, frame): return self.speech_mark else: # Silence detected - self.min_energy = ((self.silent_count * self.min_energy) + energy) / ( - self.silent_count + 1 - ) + self.min_energy = ((self.silent_count * self.min_energy) + energy) / (self.silent_count + 1) self.silent_count += 1 # Inertia against switching - if ( - self.n >= self.num_init_frames - and self.silent_count <= self.ignore_silent_count - ): + if self.n >= self.num_init_frames and self.silent_count <= self.ignore_silent_count: # Too soon to change return self.speech_mark else: @@ -260,9 +249,7 @@ def get_microphone_chunks( else: precumulated.append(chunk) - if (not is_speech and len(cumulated) >= min_to_cumulate) or ( - len(cumulated) > max_to_cumulate - ): + if (not is_speech and len(cumulated) >= min_to_cumulate) or (len(cumulated) > max_to_cumulate): waveform = torch.cat(list(precumulated) + cumulated, -1) yield (waveform * stream._rate, stream._rate) cumulated = [] diff --git a/examples/libtorchaudio/augmentation/create_jittable_pipeline.py b/examples/libtorchaudio/augmentation/create_jittable_pipeline.py index ac33a837da..d466c7fd5b 100755 --- a/examples/libtorchaudio/augmentation/create_jittable_pipeline.py +++ b/examples/libtorchaudio/augmentation/create_jittable_pipeline.py @@ -2,8 +2,8 @@ """ Create a data preprocess pipeline that can be run with libtorchaudio """ -import os import argparse +import os import torch import torchaudio @@ -14,10 +14,11 @@ class Pipeline(torch.nn.Module): This example load waveform from a file then apply effects and save it to a file. """ + def __init__(self, rir_path: str): super().__init__() rir, sample_rate = torchaudio.load(rir_path) - self.register_buffer('rir', rir) + self.register_buffer("rir", rir) self.rir_sample_rate: int = sample_rate def forward(self, input_path: str, output_path: str): @@ -32,7 +33,8 @@ def forward(self, input_path: str, output_path: str): # 3. Reample the RIR filter to much the audio sample rate rir, _ = torchaudio.sox_effects.apply_effects_tensor( - self.rir, self.rir_sample_rate, effects=[["rate", str(sample_rate)]]) + self.rir, self.rir_sample_rate, effects=[["rate", str(sample_rate)]] + ) rir = rir / torch.norm(rir, p=2) rir = torch.flip(rir, [1]) @@ -62,15 +64,9 @@ def _get_path(*paths): def _parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - "--rir-path", - default=_get_path("..", "data", "rir.wav"), - help="Audio dara for room impulse response." - ) - parser.add_argument( - "--output-path", - default=_get_path("pipeline.zip"), - help="Output JIT file." + "--rir-path", default=_get_path("..", "data", "rir.wav"), help="Audio dara for room impulse response." ) + parser.add_argument("--output-path", default=_get_path("pipeline.zip"), help="Output JIT file.") return parser.parse_args() @@ -79,5 +75,5 @@ def _main(): _create_jit_pipeline(args.rir_path, args.output_path) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/examples/libtorchaudio/speech_recognition/build_pipeline_from_fairseq.py b/examples/libtorchaudio/speech_recognition/build_pipeline_from_fairseq.py index a87ea3cf4e..dcbe3c011a 100644 --- a/examples/libtorchaudio/speech_recognition/build_pipeline_from_fairseq.py +++ b/examples/libtorchaudio/speech_recognition/build_pipeline_from_fairseq.py @@ -3,18 +3,17 @@ To use this script, you need `fairseq`. """ -import os import argparse import logging +import os from typing import Tuple +import fairseq import torch -from torch.utils.mobile_optimizer import optimize_for_mobile import torchaudio -from torchaudio.models.wav2vec2.utils.import_fairseq import import_fairseq_model -import fairseq - from greedy_decoder import Decoder +from torch.utils.mobile_optimizer import optimize_for_mobile +from torchaudio.models.wav2vec2.utils.import_fairseq import import_fairseq_model TORCH_VERSION: Tuple[int, ...] = tuple(int(x) for x in torch.__version__.split(".")[:2]) if TORCH_VERSION >= (1, 10): @@ -29,44 +28,31 @@ def _parse_args(): parser = argparse.ArgumentParser( description=__doc__, ) + parser.add_argument("--model-file", required=True, help="Path to the input pretrained weight file.") parser.add_argument( - '--model-file', - required=True, - help='Path to the input pretrained weight file.' - ) - parser.add_argument( - '--dict-dir', + "--dict-dir", help=( - 'Path to the directory in which `dict.ltr.txt` file is found. ' - 'Required only when the model is finetuned.' - ) + "Path to the directory in which `dict.ltr.txt` file is found. " "Required only when the model is finetuned." + ), ) parser.add_argument( - '--output-path', - help='Path to the directory, where the TorchScript-ed pipelines are saved.', + "--output-path", + help="Path to the directory, where the TorchScript-ed pipelines are saved.", ) parser.add_argument( - '--test-file', - help='Path to a test audio file.', + "--test-file", + help="Path to a test audio file.", ) parser.add_argument( - '--debug', - action='store_true', + "--debug", + action="store_true", help=( - 'When enabled, individual components are separately tested ' - 'for the numerical compatibility and TorchScript compatibility.' - ) - ) - parser.add_argument( - '--quantize', - action='store_true', - help='Apply quantization to model.' - ) - parser.add_argument( - '--optimize-for-mobile', - action='store_true', - help='Apply optmization for mobile.' + "When enabled, individual components are separately tested " + "for the numerical compatibility and TorchScript compatibility." + ), ) + parser.add_argument("--quantize", action="store_true", help="Apply quantization to model.") + parser.add_argument("--optimize-for-mobile", action="store_true", help="Apply optmization for mobile.") return parser.parse_args() @@ -74,7 +60,7 @@ class Loader(torch.nn.Module): def forward(self, audio_path: str) -> torch.Tensor: waveform, sample_rate = torchaudio.load(audio_path) if sample_rate != 16000: - waveform = torchaudio.functional.resample(waveform, float(sample_rate), 16000.) + waveform = torchaudio.functional.resample(waveform, float(sample_rate), 16000.0) return waveform @@ -129,11 +115,9 @@ def _get_decoder(): def _load_fairseq_model(input_file, data_dir=None): overrides = {} if data_dir: - overrides['data'] = data_dir + overrides["data"] = data_dir - model, _, _ = fairseq.checkpoint_utils.load_model_ensemble_and_task( - [input_file], arg_overrides=overrides - ) + model, _, _ = fairseq.checkpoint_utils.load_model_ensemble_and_task([input_file], arg_overrides=overrides) model = model[0] return model @@ -154,36 +138,32 @@ def _main(): _LG.info(encoder) if args.quantize: - _LG.info('Quantizing the model') + _LG.info("Quantizing the model") model.encoder.transformer.pos_conv_embed.__prepare_scriptable__() - encoder = tq.quantize_dynamic( - encoder, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) + encoder = tq.quantize_dynamic(encoder, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) _LG.info(encoder) # test if args.test_file: - _LG.info('Testing with %s', args.test_file) + _LG.info("Testing with %s", args.test_file) waveform = loader(args.test_file) emission = encoder(waveform) transcript = decoder(emission) _LG.info(transcript) - torch.jit.script(loader).save(os.path.join(args.output_path, 'loader.zip')) - torch.jit.script(decoder).save(os.path.join(args.output_path, 'decoder.zip')) + torch.jit.script(loader).save(os.path.join(args.output_path, "loader.zip")) + torch.jit.script(decoder).save(os.path.join(args.output_path, "decoder.zip")) scripted = torch.jit.script(encoder) if args.optimize_for_mobile: scripted = optimize_for_mobile(scripted) - scripted.save(os.path.join(args.output_path, 'encoder.zip')) + scripted.save(os.path.join(args.output_path, "encoder.zip")) def _init_logging(debug=False): level = logging.DEBUG if debug else logging.INFO - format_ = ( - '%(message)s' if not debug else - '%(asctime)s: %(levelname)7s: %(funcName)10s: %(message)s' - ) + format_ = "%(message)s" if not debug else "%(asctime)s: %(levelname)7s: %(funcName)10s: %(message)s" logging.basicConfig(level=level, format=format_) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/examples/libtorchaudio/speech_recognition/build_pipeline_from_huggingface_transformers.py b/examples/libtorchaudio/speech_recognition/build_pipeline_from_huggingface_transformers.py index f7cc49e2ee..344d3d09a2 100644 --- a/examples/libtorchaudio/speech_recognition/build_pipeline_from_huggingface_transformers.py +++ b/examples/libtorchaudio/speech_recognition/build_pipeline_from_huggingface_transformers.py @@ -6,8 +6,8 @@ import torch import torchaudio -from torchaudio.models.wav2vec2.utils.import_huggingface import import_huggingface_model from greedy_decoder import Decoder +from torchaudio.models.wav2vec2.utils.import_huggingface import import_huggingface_model TORCH_VERSION: Tuple[int, ...] = tuple(int(x) for x in torch.__version__.split(".")[:2]) if TORCH_VERSION >= (1, 10): @@ -22,31 +22,27 @@ def _parse_args(): parser = argparse.ArgumentParser( description=__doc__, ) + parser.add_argument("--model", required=True, help="Path to the input pretrained weight file.") parser.add_argument( - '--model', - required=True, - help='Path to the input pretrained weight file.' - ) - parser.add_argument( - '--output-path', - help='Path to the directory, where the Torchscript-ed pipelines are saved.', + "--output-path", + help="Path to the directory, where the Torchscript-ed pipelines are saved.", ) parser.add_argument( - '--test-file', - help='Path to a test audio file.', + "--test-file", + help="Path to a test audio file.", ) parser.add_argument( - '--quantize', - action='store_true', - help='Quantize the model.', + "--quantize", + action="store_true", + help="Quantize the model.", ) parser.add_argument( - '--debug', - action='store_true', + "--debug", + action="store_true", help=( - 'When enabled, individual components are separately tested ' - 'for the numerical compatibility and TorchScript compatibility.' - ) + "When enabled, individual components are separately tested " + "for the numerical compatibility and TorchScript compatibility." + ), ) return parser.parse_args() @@ -55,7 +51,7 @@ class Loader(torch.nn.Module): def forward(self, audio_path: str) -> torch.Tensor: waveform, sample_rate = torchaudio.load(audio_path) if sample_rate != 16000: - waveform = torchaudio.functional.resample(waveform, float(sample_rate), 16000.) + waveform = torchaudio.functional.resample(waveform, float(sample_rate), 16000.0) return waveform @@ -71,6 +67,7 @@ def forward(self, waveform: torch.Tensor) -> torch.Tensor: def _get_model(model_id): from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor + tokenizer = Wav2Vec2Processor.from_pretrained(model_id).tokenizer labels = [k for k, v in sorted(tokenizer.get_vocab().items(), key=lambda kv: kv[1])] original = Wav2Vec2ForCTC.from_pretrained(model_id) @@ -85,43 +82,39 @@ def _get_decoder(labels): def _main(): args = _parse_args() _init_logging(args.debug) - _LG.info('Loading model: %s', args.model) + _LG.info("Loading model: %s", args.model) model, labels = _get_model(args.model) - _LG.info('Labels: %s', labels) - _LG.info('Building pipeline') + _LG.info("Labels: %s", labels) + _LG.info("Building pipeline") loader = Loader() encoder = Encoder(model) decoder = _get_decoder(labels) _LG.info(encoder) if args.quantize: - _LG.info('Quantizing the model') + _LG.info("Quantizing the model") model.encoder.transformer.pos_conv_embed.__prepare_scriptable__() - encoder = tq.quantize_dynamic( - encoder, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) + encoder = tq.quantize_dynamic(encoder, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) _LG.info(encoder) # test if args.test_file: - _LG.info('Testing with %s', args.test_file) + _LG.info("Testing with %s", args.test_file) waveform = loader(args.test_file) emission = encoder(waveform) transcript = decoder(emission) _LG.info(transcript) - torch.jit.script(loader).save(os.path.join(args.output_path, 'loader.zip')) - torch.jit.script(encoder).save(os.path.join(args.output_path, 'encoder.zip')) - torch.jit.script(decoder).save(os.path.join(args.output_path, 'decoder.zip')) + torch.jit.script(loader).save(os.path.join(args.output_path, "loader.zip")) + torch.jit.script(encoder).save(os.path.join(args.output_path, "encoder.zip")) + torch.jit.script(decoder).save(os.path.join(args.output_path, "decoder.zip")) def _init_logging(debug=False): level = logging.DEBUG if debug else logging.INFO - format_ = ( - '%(message)s' if not debug else - '%(asctime)s: %(levelname)7s: %(funcName)10s: %(message)s' - ) + format_ = "%(message)s" if not debug else "%(asctime)s: %(levelname)7s: %(funcName)10s: %(message)s" logging.basicConfig(level=level, format=format_) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/examples/libtorchaudio/speech_recognition/greedy_decoder.py b/examples/libtorchaudio/speech_recognition/greedy_decoder.py index 3303c33070..c73e23c8dc 100644 --- a/examples/libtorchaudio/speech_recognition/greedy_decoder.py +++ b/examples/libtorchaudio/speech_recognition/greedy_decoder.py @@ -17,12 +17,12 @@ def forward(self, logits: torch.Tensor) -> str: """ best_path = torch.argmax(logits, dim=-1) # [num_seq,] best_path = torch.unique_consecutive(best_path, dim=-1) - hypothesis = '' + hypothesis = "" for i in best_path: char = self.labels[i] - if char in ['', '']: + if char in ["", ""]: continue - if char == '|': - char = ' ' + if char == "|": + char = " " hypothesis += char return hypothesis diff --git a/examples/libtorchaudio/speech_recognition/parse_librispeech.py b/examples/libtorchaudio/speech_recognition/parse_librispeech.py index dcd7aaf938..b56f4608d4 100644 --- a/examples/libtorchaudio/speech_recognition/parse_librispeech.py +++ b/examples/libtorchaudio/speech_recognition/parse_librispeech.py @@ -21,11 +21,7 @@ def _parse_args(): description=__doc__, formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument( - 'input_dir', - type=Path, - help='Directory where `*.trans.txt` files are searched.' - ) + parser.add_argument("input_dir", type=Path, help="Directory where `*.trans.txt` files are searched.") return parser.parse_args() @@ -34,22 +30,22 @@ def _parse_transcript(path): for line in trans_fileobj: line = line.strip() if line: - yield line.split(' ', maxsplit=1) + yield line.split(" ", maxsplit=1) def _parse_directory(root_dir: Path): - for trans_file in root_dir.glob('**/*.trans.txt'): + for trans_file in root_dir.glob("**/*.trans.txt"): trans_dir = trans_file.parent for id_, transcription in _parse_transcript(trans_file): - audio_path = trans_dir / f'{id_}.flac' + audio_path = trans_dir / f"{id_}.flac" yield id_, audio_path, transcription def _main(): args = _parse_args() for id_, path, transcription in _parse_directory(args.input_dir): - print(f'{id_}\t{path}\t{transcription}') + print(f"{id_}\t{path}\t{transcription}") -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/examples/libtorchaudio/speech_recognition/parse_voxforge.py b/examples/libtorchaudio/speech_recognition/parse_voxforge.py index ea88c60851..4fb5ecd14a 100644 --- a/examples/libtorchaudio/speech_recognition/parse_voxforge.py +++ b/examples/libtorchaudio/speech_recognition/parse_voxforge.py @@ -12,8 +12,8 @@ Dataset can be obtained from http://www.repository.voxforge1.org/downloads/de/Trunk/Audio/Main/16kHz_16bit/ """ # noqa: E501 -import os import argparse +import os from pathlib import Path @@ -22,11 +22,7 @@ def _parse_args(): description=__doc__, formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument( - 'input_dir', - type=Path, - help='Directory where `*.trans.txt` files are searched.' - ) + parser.add_argument("input_dir", type=Path, help="Directory where `*.trans.txt` files are searched.") return parser.parse_args() @@ -38,19 +34,19 @@ def _parse_prompts(path): if not line: continue - id_, transcript = line.split(' ', maxsplit=1) + id_, transcript = line.split(" ", maxsplit=1) if not transcript: continue transcript = transcript.upper() - filename = id_.split('/')[-1] - audio_path = base_dir / 'wav' / f'{filename}.wav' + filename = id_.split("/")[-1] + audio_path = base_dir / "wav" / f"{filename}.wav" if os.path.exists(audio_path): yield id_, audio_path, transcript def _parse_directory(root_dir: Path): - for prompt_file in root_dir.glob('**/PROMPTS'): + for prompt_file in root_dir.glob("**/PROMPTS"): try: yield from _parse_prompts(prompt_file) except UnicodeDecodeError: @@ -60,8 +56,8 @@ def _parse_directory(root_dir: Path): def _main(): args = _parse_args() for id_, path, transcription in _parse_directory(args.input_dir): - print(f'{id_}\t{path}\t{transcription}') + print(f"{id_}\t{path}\t{transcription}") -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/examples/pipeline_tacotron2/datasets.py b/examples/pipeline_tacotron2/datasets.py index a5a6820383..1e05b9a373 100644 --- a/examples/pipeline_tacotron2/datasets.py +++ b/examples/pipeline_tacotron2/datasets.py @@ -29,7 +29,6 @@ import torch from torch import Tensor - from torch.utils.data.dataset import random_split from torchaudio.datasets import LJSPEECH @@ -45,8 +44,7 @@ def forward(self, input): class MapMemoryCache(torch.utils.data.Dataset): - r"""Wrap a dataset so that, whenever a new item is returned, it is saved to memory. - """ + r"""Wrap a dataset so that, whenever a new item is returned, it is saved to memory.""" def __init__(self, dataset): self.dataset = dataset @@ -84,16 +82,17 @@ def process_datapoint(self, item): return text_norm, torch.squeeze(melspec, 0) -def split_process_dataset(dataset: str, - file_path: str, - val_ratio: float, - transforms: Callable, - text_preprocessor: Callable[[str], List[int]], - ) -> Tuple[torch.utils.data.Dataset, torch.utils.data.Dataset]: +def split_process_dataset( + dataset: str, + file_path: str, + val_ratio: float, + transforms: Callable, + text_preprocessor: Callable[[str], List[int]], +) -> Tuple[torch.utils.data.Dataset, torch.utils.data.Dataset]: """Returns the Training and validation datasets. Args: - dataset (str): The dataset to use. Avaliable options: [`'ljspeech'`] + dataset (str): The dataset to use. Available options: [`'ljspeech'`] file_path (str): Path to the data. val_ratio (float): Path to the data. transforms (callable): A function/transform that takes in a waveform and @@ -105,7 +104,7 @@ def split_process_dataset(dataset: str, train_dataset (`torch.utils.data.Dataset`): The training set. val_dataset (`torch.utils.data.Dataset`): The validation set. """ - if dataset == 'ljspeech': + if dataset == "ljspeech": data = LJSPEECH(root=file_path, download=False) val_length = int(len(data) * val_ratio) @@ -123,8 +122,9 @@ def split_process_dataset(dataset: str, return train_dataset, val_dataset -def text_mel_collate_fn(batch: Tuple[Tensor, Tensor], - n_frames_per_step: int = 1) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: +def text_mel_collate_fn( + batch: Tuple[Tensor, Tensor], n_frames_per_step: int = 1 +) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: """The collate function padding and adjusting the data based on `n_frames_per_step`. Modified from https://github.com/NVIDIA/DeepLearningExamples @@ -143,13 +143,14 @@ def text_mel_collate_fn(batch: Tuple[Tensor, Tensor], with shape (n_batch, max of ``mel_specgram_lengths``) """ text_lengths, ids_sorted_decreasing = torch.sort( - torch.LongTensor([len(x[0]) for x in batch]), dim=0, descending=True) + torch.LongTensor([len(x[0]) for x in batch]), dim=0, descending=True + ) max_input_len = text_lengths[0] text_padded = torch.zeros((len(batch), max_input_len), dtype=torch.int64) for i in range(len(ids_sorted_decreasing)): text = batch[ids_sorted_decreasing[i]][0] - text_padded[i, :text.size(0)] = text + text_padded[i, : text.size(0)] = text # Right zero-pad mel-spec num_mels = batch[0][1].size(0) @@ -164,8 +165,8 @@ def text_mel_collate_fn(batch: Tuple[Tensor, Tensor], mel_specgram_lengths = torch.LongTensor(len(batch)) for i in range(len(ids_sorted_decreasing)): mel = batch[ids_sorted_decreasing[i]][1] - mel_specgram_padded[i, :, :mel.size(1)] = mel + mel_specgram_padded[i, :, : mel.size(1)] = mel mel_specgram_lengths[i] = mel.size(1) - gate_padded[i, mel.size(1) - 1:] = 1 + gate_padded[i, mel.size(1) - 1 :] = 1 return text_padded, text_lengths, mel_specgram_padded, mel_specgram_lengths, gate_padded diff --git a/examples/pipeline_tacotron2/inference.py b/examples/pipeline_tacotron2/inference.py index a89fffb5cf..b01fe9c726 100644 --- a/examples/pipeline_tacotron2/inference.py +++ b/examples/pipeline_tacotron2/inference.py @@ -2,19 +2,15 @@ Text-to-speech pipeline using Tacotron2. """ -from functools import partial import argparse import os import random import sys +from functools import partial +import numpy as np import torch import torchaudio -import numpy as np -from torchaudio.models import Tacotron2 -from torchaudio.models import tacotron2 as pretrained_tacotron2 - -from utils import prepare_input_sequence from datasets import InverseSpectralNormalization from text.text_preprocessing import ( available_symbol_set, @@ -22,6 +18,9 @@ get_symbol_list, text_to_sequence, ) +from torchaudio.models import Tacotron2 +from torchaudio.models import tacotron2 as pretrained_tacotron2 +from utils import prepare_input_sequence def parse_args(): @@ -33,113 +32,72 @@ def parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - '--checkpoint-name', + "--checkpoint-name", type=str, default=None, choices=list(tacotron2_config_and_urls.keys()), - help='[string] The name of the checkpoint to load.' - ) - parser.add_argument( - '--checkpoint-path', - type=str, - default=None, - help='[string] Path to the checkpoint file.' - ) - parser.add_argument( - '--output-path', - type=str, - default="./audio.wav", - help='[string] Path to the output .wav file.' + help="[string] The name of the checkpoint to load.", ) + parser.add_argument("--checkpoint-path", type=str, default=None, help="[string] Path to the checkpoint file.") + parser.add_argument("--output-path", type=str, default="./audio.wav", help="[string] Path to the output .wav file.") parser.add_argument( - '--input-text', - '-i', + "--input-text", + "-i", type=str, default="Hello world", - help='[string] Type in something here and TTS will generate it!' + help="[string] Type in something here and TTS will generate it!", ) parser.add_argument( - '--vocoder', - default='nvidia_waveglow', - choices=['griffin_lim', 'wavernn', 'nvidia_waveglow'], + "--vocoder", + default="nvidia_waveglow", + choices=["griffin_lim", "wavernn", "nvidia_waveglow"], type=str, help="Select the vocoder to use.", ) parser.add_argument( - "--jit", - default=False, - action="store_true", - help="If used, the model and inference function is jitted." + "--jit", default=False, action="store_true", help="If used, the model and inference function is jitted." ) - preprocessor = parser.add_argument_group('text preprocessor setup') + preprocessor = parser.add_argument_group("text preprocessor setup") preprocessor.add_argument( - '--text-preprocessor', - default='english_characters', + "--text-preprocessor", + default="english_characters", type=str, choices=available_symbol_set, - help='select text preprocessor to use.' + help="select text preprocessor to use.", ) preprocessor.add_argument( - '--phonemizer', + "--phonemizer", default="DeepPhonemizer", type=str, choices=available_phonemizers, - help='select phonemizer to use, only used when text-preprocessor is "english_phonemes"' + help='select phonemizer to use, only used when text-preprocessor is "english_phonemes"', ) preprocessor.add_argument( - '--phonemizer-checkpoint', + "--phonemizer-checkpoint", default="./en_us_cmudict_forward.pt", type=str, - help='the path or name of the checkpoint for the phonemizer, ' - 'only used when text-preprocessor is "english_phonemes"' + help="the path or name of the checkpoint for the phonemizer, " + 'only used when text-preprocessor is "english_phonemes"', ) preprocessor.add_argument( - '--cmudict-root', - default="./", - type=str, - help='the root directory for storing CMU dictionary files' + "--cmudict-root", default="./", type=str, help="the root directory for storing CMU dictionary files" ) - audio = parser.add_argument_group('audio parameters') - audio.add_argument( - '--sample-rate', - default=22050, - type=int, - help='Sampling rate' - ) - audio.add_argument( - '--n-fft', - default=1024, - type=int, - help='Filter length for STFT' - ) - audio.add_argument( - '--n-mels', - default=80, - type=int, - help='' - ) - audio.add_argument( - '--mel-fmin', - default=0.0, - type=float, - help='Minimum mel frequency' - ) - audio.add_argument( - '--mel-fmax', - default=8000.0, - type=float, - help='Maximum mel frequency' - ) + audio = parser.add_argument_group("audio parameters") + audio.add_argument("--sample-rate", default=22050, type=int, help="Sampling rate") + audio.add_argument("--n-fft", default=1024, type=int, help="Filter length for STFT") + audio.add_argument("--n-mels", default=80, type=int, help="") + audio.add_argument("--mel-fmin", default=0.0, type=float, help="Minimum mel frequency") + audio.add_argument("--mel-fmax", default=8000.0, type=float, help="Maximum mel frequency") # parameters for WaveRNN - wavernn = parser.add_argument_group('WaveRNN parameters') + wavernn = parser.add_argument_group("WaveRNN parameters") wavernn.add_argument( - '--wavernn-checkpoint-name', + "--wavernn-checkpoint-name", default="wavernn_10k_epochs_8bits_ljspeech", choices=list(wavernn_config_and_urls.keys()), - help="Select the WaveRNN checkpoint." + help="Select the WaveRNN checkpoint.", ) wavernn.add_argument( "--wavernn-loss", @@ -152,13 +110,10 @@ def parse_args(): "--wavernn-no-batch-inference", default=False, action="store_true", - help="Don't use batch inference for WaveRNN inference." + help="Don't use batch inference for WaveRNN inference.", ) wavernn.add_argument( - "--wavernn-no-mulaw", - default=False, - action="store_true", - help="Don't use mulaw decoder to decode the signal." + "--wavernn-no-mulaw", default=False, action="store_true", help="Don't use mulaw decoder to decode the signal." ) wavernn.add_argument( "--wavernn-batch-timesteps", @@ -187,11 +142,11 @@ def unwrap_distributed(state_dict): unwrapped_state_dict: Unwrapped state_dict. """ - return {k.replace('module.', ''): v for k, v in state_dict.items()} + return {k.replace("module.", ""): v for k, v in state_dict.items()} def nvidia_waveglow_vocode(mel_specgram, device, jit=False): - waveglow = torch.hub.load('NVIDIA/DeepLearningExamples:torchhub', 'nvidia_waveglow', model_math='fp16') + waveglow = torch.hub.load("NVIDIA/DeepLearningExamples:torchhub", "nvidia_waveglow", model_math="fp16") waveglow = waveglow.remove_weightnorm(waveglow) waveglow = waveglow.to(device) waveglow.eval() @@ -205,13 +160,22 @@ def nvidia_waveglow_vocode(mel_specgram, device, jit=False): return waveform -def wavernn_vocode(mel_specgram, wavernn_checkpoint_name, wavernn_loss, wavernn_no_mulaw, - wavernn_no_batch_inference, wavernn_batch_timesteps, wavernn_batch_overlap, - device, jit): +def wavernn_vocode( + mel_specgram, + wavernn_checkpoint_name, + wavernn_loss, + wavernn_no_mulaw, + wavernn_no_batch_inference, + wavernn_batch_timesteps, + wavernn_batch_overlap, + device, + jit, +): from torchaudio.models import wavernn + sys.path.append(os.path.join(os.path.dirname(__file__), "../pipeline_wavernn")) - from wavernn_inference_wrapper import WaveRNNInferenceWrapper from processing import NormalizeDB + from wavernn_inference_wrapper import WaveRNNInferenceWrapper wavernn_model = wavernn(wavernn_checkpoint_name).eval().to(device) wavernn_inference_model = WaveRNNInferenceWrapper(wavernn_model) @@ -234,16 +198,26 @@ def wavernn_vocode(mel_specgram, wavernn_checkpoint_name, wavernn_loss, wavernn_ mel_specgram = transforms(mel_specgram.cpu()) with torch.no_grad(): - waveform = wavernn_inference_model(mel_specgram.to(device), - loss_name=wavernn_loss, - mulaw=(not wavernn_no_mulaw), - batched=(not wavernn_no_batch_inference), - timesteps=wavernn_batch_timesteps, - overlap=wavernn_batch_overlap,) + waveform = wavernn_inference_model( + mel_specgram.to(device), + loss_name=wavernn_loss, + mulaw=(not wavernn_no_mulaw), + batched=(not wavernn_no_batch_inference), + timesteps=wavernn_batch_timesteps, + overlap=wavernn_batch_overlap, + ) return waveform.unsqueeze(0) -def griffin_lim_vocode(mel_specgram, n_fft, n_mels, sample_rate, mel_fmin, mel_fmax, jit, ): +def griffin_lim_vocode( + mel_specgram, + n_fft, + n_mels, + sample_rate, + mel_fmin, + mel_fmax, + jit, +): from torchaudio.transforms import GriffinLim, InverseMelScale inv_norm = InverseSpectralNormalization() @@ -254,7 +228,7 @@ def griffin_lim_vocode(mel_specgram, n_fft, n_mels, sample_rate, mel_fmin, mel_f f_min=mel_fmin, f_max=mel_fmax, mel_scale="slaney", - norm='slaney', + norm="slaney", ) griffin_lim = GriffinLim( n_fft=n_fft, @@ -263,11 +237,7 @@ def griffin_lim_vocode(mel_specgram, n_fft, n_mels, sample_rate, mel_fmin, mel_f win_length=1024, ) - vocoder = torch.nn.Sequential( - inv_norm, - inv_mel, - griffin_lim - ) + vocoder = torch.nn.Sequential(inv_norm, inv_mel, griffin_lim) if jit: vocoder = torch.jit.script(vocoder) @@ -286,8 +256,7 @@ def main(args): if args.checkpoint_path is None and args.checkpoint_name is None: raise ValueError("Either --checkpoint-path or --checkpoint-name must be specified.") elif args.checkpoint_path is not None and args.checkpoint_name is not None: - raise ValueError("Both --checkpoint-path and --checkpoint-name are specified, " - "can only specify one.") + raise ValueError("Both --checkpoint-path and --checkpoint-name are specified, " "can only specify one.") n_symbols = len(get_symbol_list(args.text_preprocessor)) text_preprocessor = partial( @@ -301,21 +270,23 @@ def main(args): if args.checkpoint_path is not None: tacotron2 = Tacotron2(n_symbol=n_symbols) tacotron2.load_state_dict( - unwrap_distributed(torch.load(args.checkpoint_path, map_location=device)['state_dict'])) + unwrap_distributed(torch.load(args.checkpoint_path, map_location=device)["state_dict"]) + ) tacotron2 = tacotron2.to(device).eval() elif args.checkpoint_name is not None: tacotron2 = pretrained_tacotron2(args.checkpoint_name).to(device).eval() if n_symbols != tacotron2.n_symbols: - raise ValueError("the number of symbols for text_preprocessor ({n_symbols}) " - "should match the number of symbols for the" - "pretrained tacotron2 ({tacotron2.n_symbols}).") + raise ValueError( + "the number of symbols for text_preprocessor ({n_symbols}) " + "should match the number of symbols for the" + "pretrained tacotron2 ({tacotron2.n_symbols})." + ) if args.jit: tacotron2 = torch.jit.script(tacotron2) - sequences, lengths = prepare_input_sequence([args.input_text], - text_processor=text_preprocessor) + sequences, lengths = prepare_input_sequence([args.input_text], text_processor=text_preprocessor) sequences, lengths = sequences.long().to(device), lengths.long().to(device) with torch.no_grad(): mel_specgram, _, _ = tacotron2.infer(sequences, lengths) @@ -324,24 +295,28 @@ def main(args): waveform = nvidia_waveglow_vocode(mel_specgram=mel_specgram, device=device, jit=args.jit) elif args.vocoder == "wavernn": - waveform = wavernn_vocode(mel_specgram=mel_specgram, - wavernn_checkpoint_name=args.wavernn_checkpoint_name, - wavernn_loss=args.wavernn_loss, - wavernn_no_mulaw=args.wavernn_no_mulaw, - wavernn_no_batch_inference=args.wavernn_no_batch_inference, - wavernn_batch_timesteps=args.wavernn_batch_timesteps, - wavernn_batch_overlap=args.wavernn_batch_overlap, - device=device, - jit=args.jit) + waveform = wavernn_vocode( + mel_specgram=mel_specgram, + wavernn_checkpoint_name=args.wavernn_checkpoint_name, + wavernn_loss=args.wavernn_loss, + wavernn_no_mulaw=args.wavernn_no_mulaw, + wavernn_no_batch_inference=args.wavernn_no_batch_inference, + wavernn_batch_timesteps=args.wavernn_batch_timesteps, + wavernn_batch_overlap=args.wavernn_batch_overlap, + device=device, + jit=args.jit, + ) elif args.vocoder == "griffin_lim": - waveform = griffin_lim_vocode(mel_specgram=mel_specgram, - n_fft=args.n_fft, - n_mels=args.n_mels, - sample_rate=args.sample_rate, - mel_fmin=args.mel_fmin, - mel_fmax=args.mel_fmax, - jit=args.jit) + waveform = griffin_lim_vocode( + mel_specgram=mel_specgram, + n_fft=args.n_fft, + n_mels=args.n_mels, + sample_rate=args.sample_rate, + mel_fmin=args.mel_fmin, + mel_fmax=args.mel_fmax, + jit=args.jit, + ) torchaudio.save(args.output_path, waveform, args.sample_rate) diff --git a/examples/pipeline_tacotron2/text/numbers.py b/examples/pipeline_tacotron2/text/numbers.py index d42e82e134..0837d3e173 100644 --- a/examples/pipeline_tacotron2/text/numbers.py +++ b/examples/pipeline_tacotron2/text/numbers.py @@ -24,33 +24,34 @@ Modified from https://github.com/keithito/tacotron """ -import inflect import re +import inflect + _inflect = inflect.engine() -_comma_number_re = re.compile(r'([0-9][0-9\,]+[0-9])') -_pounds_re = re.compile(r'£([0-9\,]*[0-9]+)') -_dollars_re = re.compile(r'\$([0-9\.\,]*[0-9]+)') -_decimal_number_re = re.compile(r'([0-9]+\.[0-9]+)') -_ordinal_re = re.compile(r'[0-9]+(st|nd|rd|th)') -_number_re = re.compile(r'[0-9]+') +_comma_number_re = re.compile(r"([0-9][0-9\,]+[0-9])") +_pounds_re = re.compile(r"£([0-9\,]*[0-9]+)") +_dollars_re = re.compile(r"\$([0-9\.\,]*[0-9]+)") +_decimal_number_re = re.compile(r"([0-9]+\.[0-9]+)") +_ordinal_re = re.compile(r"[0-9]+(st|nd|rd|th)") +_number_re = re.compile(r"[0-9]+") def _remove_commas(text: str) -> str: - return re.sub(_comma_number_re, lambda m: m.group(1).replace(',', ''), text) + return re.sub(_comma_number_re, lambda m: m.group(1).replace(",", ""), text) def _expand_pounds(text: str) -> str: - return re.sub(_pounds_re, r'\1 pounds', text) + return re.sub(_pounds_re, r"\1 pounds", text) def _expand_dollars_repl_fn(m): """The replacement function for expanding dollars.""" match = m.group(1) - parts = match.split('.') + parts = match.split(".") if len(parts) > 2: - return match + ' dollars' # Unexpected format + return match + " dollars" # Unexpected format dollars = int(parts[0]) if parts[0] else 0 if len(parts) > 1 and parts[1]: if len(parts[1]) == 1: @@ -61,17 +62,17 @@ def _expand_dollars_repl_fn(m): else: cents = 0 if dollars and cents: - dollar_unit = 'dollar' if dollars == 1 else 'dollars' - cent_unit = 'cent' if cents == 1 else 'cents' - return '%s %s, %s %s' % (dollars, dollar_unit, cents, cent_unit) + dollar_unit = "dollar" if dollars == 1 else "dollars" + cent_unit = "cent" if cents == 1 else "cents" + return "%s %s, %s %s" % (dollars, dollar_unit, cents, cent_unit) elif dollars: - dollar_unit = 'dollar' if dollars == 1 else 'dollars' - return '%s %s' % (dollars, dollar_unit) + dollar_unit = "dollar" if dollars == 1 else "dollars" + return "%s %s" % (dollars, dollar_unit) elif cents: - cent_unit = 'cent' if cents == 1 else 'cents' - return '%s %s' % (cents, cent_unit) + cent_unit = "cent" if cents == 1 else "cents" + return "%s %s" % (cents, cent_unit) else: - return 'zero dollars' + return "zero dollars" def _expand_dollars(text: str) -> str: @@ -79,7 +80,7 @@ def _expand_dollars(text: str) -> str: def _expand_decimal_point(text: str) -> str: - return re.sub(_decimal_number_re, lambda m: m.group(1).replace('.', ' point '), text) + return re.sub(_decimal_number_re, lambda m: m.group(1).replace(".", " point "), text) def _expand_ordinal(text: str) -> str: @@ -91,15 +92,15 @@ def _expand_number_repl_fn(m): num = int(m.group(0)) if num > 1000 and num < 3000: if num == 2000: - return 'two thousand' + return "two thousand" elif num > 2000 and num < 2010: - return 'two thousand ' + _inflect.number_to_words(num % 100) + return "two thousand " + _inflect.number_to_words(num % 100) elif num % 100 == 0: - return _inflect.number_to_words(num // 100) + ' hundred' + return _inflect.number_to_words(num // 100) + " hundred" else: - return _inflect.number_to_words(num, andword='', zero='oh', group=2).replace(', ', ' ') + return _inflect.number_to_words(num, andword="", zero="oh", group=2).replace(", ", " ") else: - return _inflect.number_to_words(num, andword='') + return _inflect.number_to_words(num, andword="") def _expand_number(text: str) -> str: diff --git a/examples/pipeline_tacotron2/text/text_preprocessing.py b/examples/pipeline_tacotron2/text/text_preprocessing.py index 8ca6ae497c..a6832d14d5 100644 --- a/examples/pipeline_tacotron2/text/text_preprocessing.py +++ b/examples/pipeline_tacotron2/text/text_preprocessing.py @@ -24,44 +24,47 @@ Modified from https://github.com/keithito/tacotron """ -from typing import List, Union, Optional import re +from typing import List, Union, Optional -from unidecode import unidecode from torchaudio.datasets import CMUDict +from unidecode import unidecode from .numbers import normalize_numbers # Regular expression matching whitespace: -_whitespace_re = re.compile(r'\s+') +_whitespace_re = re.compile(r"\s+") # List of (regular expression, replacement) pairs for abbreviations: -_abbreviations = [(re.compile('\\b%s\\.' % x[0], re.IGNORECASE), x[1]) for x in [ - ('mrs', 'misess'), - ('mr', 'mister'), - ('dr', 'doctor'), - ('st', 'saint'), - ('co', 'company'), - ('jr', 'junior'), - ('maj', 'major'), - ('gen', 'general'), - ('drs', 'doctors'), - ('rev', 'reverend'), - ('lt', 'lieutenant'), - ('hon', 'honorable'), - ('sgt', 'sergeant'), - ('capt', 'captain'), - ('esq', 'esquire'), - ('ltd', 'limited'), - ('col', 'colonel'), - ('ft', 'fort'), -]] - -_pad = '_' -_punctuation = '!\'(),.:;? ' -_special = '-' -_letters = 'abcdefghijklmnopqrstuvwxyz' +_abbreviations = [ + (re.compile("\\b%s\\." % x[0], re.IGNORECASE), x[1]) + for x in [ + ("mrs", "misess"), + ("mr", "mister"), + ("dr", "doctor"), + ("st", "saint"), + ("co", "company"), + ("jr", "junior"), + ("maj", "major"), + ("gen", "general"), + ("drs", "doctors"), + ("rev", "reverend"), + ("lt", "lieutenant"), + ("hon", "honorable"), + ("sgt", "sergeant"), + ("capt", "captain"), + ("esq", "esquire"), + ("ltd", "limited"), + ("col", "colonel"), + ("ft", "fort"), + ] +] + +_pad = "_" +_punctuation = "!'(),.:;? " +_special = "-" +_letters = "abcdefghijklmnopqrstuvwxyz" symbols = [_pad] + list(_special) + list(_punctuation) + list(_letters) _phonemizer = None @@ -71,23 +74,25 @@ available_phonemizers = set(["DeepPhonemizer"]) -def get_symbol_list(symbol_list: str = "english_characters", - cmudict_root: Optional[str] = "./") -> List[str]: +def get_symbol_list(symbol_list: str = "english_characters", cmudict_root: Optional[str] = "./") -> List[str]: if symbol_list == "english_characters": return [_pad] + list(_special) + list(_punctuation) + list(_letters) elif symbol_list == "english_phonemes": return [_pad] + list(_special) + list(_punctuation) + CMUDict(cmudict_root).symbols else: - raise ValueError(f"The `symbol_list` {symbol_list} is not supported." - f"Supported `symbol_list` includes {available_symbol_set}.") + raise ValueError( + f"The `symbol_list` {symbol_list} is not supported." + f"Supported `symbol_list` includes {available_symbol_set}." + ) def word_to_phonemes(sent: str, phonemizer: str, checkpoint: str) -> List[str]: if phonemizer == "DeepPhonemizer": from dp.phonemizer import Phonemizer + global _phonemizer - _other_symbols = ''.join(list(_special) + list(_punctuation)) - _phone_symbols_re = r'(\[[A-Z]+?\]|' + '[' + _other_symbols + '])' # [\[([A-Z]+?)\]|[-!'(),.:;? ]] + _other_symbols = "".join(list(_special) + list(_punctuation)) + _phone_symbols_re = r"(\[[A-Z]+?\]|" + "[" + _other_symbols + "])" # [\[([A-Z]+?)\]|[-!'(),.:;? ]] if _phonemizer is None: # using a global variable so that we don't have to relode checkpoint @@ -97,7 +102,7 @@ def word_to_phonemes(sent: str, phonemizer: str, checkpoint: str) -> List[str]: # Example: # sent = "hello world!" # '[HH][AH][L][OW] [W][ER][L][D]!' - sent = _phonemizer(sent, lang='en_us') + sent = _phonemizer(sent, lang="en_us") # ['[HH]', '[AH]', '[L]', '[OW]', ' ', '[W]', '[ER]', '[L]', '[D]', '!'] ret = re.findall(_phone_symbols_re, sent) @@ -107,16 +112,19 @@ def word_to_phonemes(sent: str, phonemizer: str, checkpoint: str) -> List[str]: return ret else: - raise ValueError(f"The `phonemizer` {phonemizer} is not supported. " - "Supported `symbol_list` includes `'DeepPhonemizer'`.") + raise ValueError( + f"The `phonemizer` {phonemizer} is not supported. " "Supported `symbol_list` includes `'DeepPhonemizer'`." + ) -def text_to_sequence(sent: str, - symbol_list: Union[str, List[str]] = "english_characters", - phonemizer: Optional[str] = "DeepPhonemizer", - checkpoint: Optional[str] = "./en_us_cmudict_forward.pt", - cmudict_root: Optional[str] = "./") -> List[int]: - r'''Converts a string of text to a sequence of IDs corresponding to the symbols in the text. +def text_to_sequence( + sent: str, + symbol_list: Union[str, List[str]] = "english_characters", + phonemizer: Optional[str] = "DeepPhonemizer", + checkpoint: Optional[str] = "./en_us_cmudict_forward.pt", + cmudict_root: Optional[str] = "./", +) -> List[int]: + r"""Converts a string of text to a sequence of IDs corresponding to the symbols in the text. Args: sent (str): The input sentence to convert to a sequence. @@ -138,19 +146,20 @@ def text_to_sequence(sent: str, [19, 16, 23, 23, 26, 11, 34, 26, 29, 23, 15, 2] >>> text_to_sequence("hello world!", "english_phonemes") [54, 20, 65, 69, 11, 92, 44, 65, 38, 2] - ''' + """ if symbol_list == "english_phonemes": if any(param is None for param in [phonemizer, checkpoint, cmudict_root]): raise ValueError( "When `symbol_list` is 'english_phonemes', " - "all of `phonemizer`, `checkpoint`, and `cmudict_root` must be provided.") + "all of `phonemizer`, `checkpoint`, and `cmudict_root` must be provided." + ) sent = unidecode(sent) # convert to ascii sent = sent.lower() # lower case sent = normalize_numbers(sent) # expand numbers for regex, replacement in _abbreviations: # expand abbreviations sent = re.sub(regex, replacement, sent) - sent = re.sub(_whitespace_re, ' ', sent) # collapse whitespace + sent = re.sub(_whitespace_re, " ", sent) # collapse whitespace if isinstance(symbol_list, list): symbols = symbol_list diff --git a/examples/pipeline_tacotron2/train.py b/examples/pipeline_tacotron2/train.py index 4fe93000b8..d2b6292f1f 100644 --- a/examples/pipeline_tacotron2/train.py +++ b/examples/pipeline_tacotron2/train.py @@ -30,27 +30,27 @@ """ import argparse -from datetime import datetime -from functools import partial import logging -import random import os +import random +from datetime import datetime +from functools import partial from time import time +import matplotlib.pyplot as plt import torch -import torchaudio -import torch.multiprocessing as mp import torch.distributed as dist -from torch.utils.tensorboard import SummaryWriter -from torch.utils.data import DataLoader +import torch.multiprocessing as mp +import torchaudio from torch.optim import Adam +from torch.utils.data import DataLoader +from torch.utils.tensorboard import SummaryWriter from torchaudio.models import Tacotron2 from tqdm import tqdm -import matplotlib.pyplot as plt -plt.switch_backend('agg') + +plt.switch_backend("agg") from datasets import text_mel_collate_fn, split_process_dataset, SpectralNormalization -from utils import save_checkpoint from loss import Tacotron2Loss from text.text_preprocessing import ( available_symbol_set, @@ -58,145 +58,144 @@ get_symbol_list, text_to_sequence, ) +from utils import save_checkpoint -logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', - level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S') +logging.basicConfig(format="%(asctime)s %(levelname)-8s %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S") logger = logging.getLogger(os.path.basename(__file__)) def parse_args(parser): """Parse commandline arguments.""" - parser.add_argument("--dataset", default="ljspeech", choices=["ljspeech"], type=str, - help="select dataset to train with") - parser.add_argument('--logging-dir', type=str, default=None, - help='directory to save the log files') - parser.add_argument('--dataset-path', type=str, default='./', - help='path to dataset') - parser.add_argument("--val-ratio", default=0.1, type=float, - help="the ratio of waveforms for validation") - - parser.add_argument('--anneal-steps', nargs='*', - help='epochs after which decrease learning rate') - parser.add_argument('--anneal-factor', type=float, choices=[0.1, 0.3], default=0.1, - help='factor for annealing learning rate') - - parser.add_argument('--master-addr', default=None, type=str, - help='the address to use for distributed training') - parser.add_argument('--master-port', default=None, type=str, - help='the port to use for distributed training') - - preprocessor = parser.add_argument_group('text preprocessor setup') - preprocessor.add_argument('--text-preprocessor', default='english_characters', type=str, - choices=available_symbol_set, - help='select text preprocessor to use.') - preprocessor.add_argument('--phonemizer', type=str, choices=available_phonemizers, - help='select phonemizer to use, only used when text-preprocessor is "english_phonemes"') - preprocessor.add_argument('--phonemizer-checkpoint', type=str, - help='the path or name of the checkpoint for the phonemizer, ' - 'only used when text-preprocessor is "english_phonemes"') - preprocessor.add_argument('--cmudict-root', default="./", type=str, - help='the root directory for storing cmudictionary files') + parser.add_argument( + "--dataset", default="ljspeech", choices=["ljspeech"], type=str, help="select dataset to train with" + ) + parser.add_argument("--logging-dir", type=str, default=None, help="directory to save the log files") + parser.add_argument("--dataset-path", type=str, default="./", help="path to dataset") + parser.add_argument("--val-ratio", default=0.1, type=float, help="the ratio of waveforms for validation") + + parser.add_argument("--anneal-steps", nargs="*", help="epochs after which decrease learning rate") + parser.add_argument( + "--anneal-factor", type=float, choices=[0.1, 0.3], default=0.1, help="factor for annealing learning rate" + ) + + parser.add_argument("--master-addr", default=None, type=str, help="the address to use for distributed training") + parser.add_argument("--master-port", default=None, type=str, help="the port to use for distributed training") + + preprocessor = parser.add_argument_group("text preprocessor setup") + preprocessor.add_argument( + "--text-preprocessor", + default="english_characters", + type=str, + choices=available_symbol_set, + help="select text preprocessor to use.", + ) + preprocessor.add_argument( + "--phonemizer", + type=str, + choices=available_phonemizers, + help='select phonemizer to use, only used when text-preprocessor is "english_phonemes"', + ) + preprocessor.add_argument( + "--phonemizer-checkpoint", + type=str, + help="the path or name of the checkpoint for the phonemizer, " + 'only used when text-preprocessor is "english_phonemes"', + ) + preprocessor.add_argument( + "--cmudict-root", default="./", type=str, help="the root directory for storing cmudictionary files" + ) # training - training = parser.add_argument_group('training setup') - training.add_argument('--epochs', type=int, required=True, - help='number of total epochs to run') - training.add_argument('--checkpoint-path', type=str, default='', - help='checkpoint path. If a file exists, ' - 'the program will load it and resume training.') - training.add_argument('--workers', default=8, type=int, - help="number of data loading workers") - training.add_argument("--validate-and-checkpoint-freq", default=10, type=int, metavar="N", - help="validation and saving checkpoint frequency in epochs",) - training.add_argument("--logging-freq", default=10, type=int, metavar="N", - help="logging frequency in epochs") - - optimization = parser.add_argument_group('optimization setup') - optimization.add_argument('--learning-rate', default=1e-3, type=float, - help='initial learing rate') - optimization.add_argument('--weight-decay', default=1e-6, type=float, - help='weight decay') - optimization.add_argument('--batch-size', default=32, type=int, - help='batch size per GPU') - optimization.add_argument('--grad-clip', default=5.0, type=float, - help='clipping gradient with maximum gradient norm value') + training = parser.add_argument_group("training setup") + training.add_argument("--epochs", type=int, required=True, help="number of total epochs to run") + training.add_argument( + "--checkpoint-path", + type=str, + default="", + help="checkpoint path. If a file exists, " "the program will load it and resume training.", + ) + training.add_argument("--workers", default=8, type=int, help="number of data loading workers") + training.add_argument( + "--validate-and-checkpoint-freq", + default=10, + type=int, + metavar="N", + help="validation and saving checkpoint frequency in epochs", + ) + training.add_argument("--logging-freq", default=10, type=int, metavar="N", help="logging frequency in epochs") + + optimization = parser.add_argument_group("optimization setup") + optimization.add_argument("--learning-rate", default=1e-3, type=float, help="initial learing rate") + optimization.add_argument("--weight-decay", default=1e-6, type=float, help="weight decay") + optimization.add_argument("--batch-size", default=32, type=int, help="batch size per GPU") + optimization.add_argument( + "--grad-clip", default=5.0, type=float, help="clipping gradient with maximum gradient norm value" + ) # model parameters - model = parser.add_argument_group('model parameters') - model.add_argument('--mask-padding', action='store_true', default=False, - help='use mask padding') - model.add_argument('--symbols-embedding-dim', default=512, type=int, - help='input embedding dimension') + model = parser.add_argument_group("model parameters") + model.add_argument("--mask-padding", action="store_true", default=False, help="use mask padding") + model.add_argument("--symbols-embedding-dim", default=512, type=int, help="input embedding dimension") # encoder - model.add_argument('--encoder-embedding-dim', default=512, type=int, - help='encoder embedding dimension') - model.add_argument('--encoder-n-convolution', default=3, type=int, - help='number of encoder convolutions') - model.add_argument('--encoder-kernel-size', default=5, type=int, - help='encoder kernel size') + model.add_argument("--encoder-embedding-dim", default=512, type=int, help="encoder embedding dimension") + model.add_argument("--encoder-n-convolution", default=3, type=int, help="number of encoder convolutions") + model.add_argument("--encoder-kernel-size", default=5, type=int, help="encoder kernel size") # decoder - model.add_argument('--n-frames-per-step', default=1, type=int, - help='number of frames processed per step (currently only 1 is supported)') - model.add_argument('--decoder-rnn-dim', default=1024, type=int, - help='number of units in decoder LSTM') - model.add_argument('--decoder-dropout', default=0.1, type=float, - help='dropout probability for decoder LSTM') - model.add_argument('--decoder-max-step', default=2000, type=int, - help='maximum number of output mel spectrograms') - model.add_argument('--decoder-no-early-stopping', action='store_true', default=False, - help='stop decoding only when all samples are finished') + model.add_argument( + "--n-frames-per-step", + default=1, + type=int, + help="number of frames processed per step (currently only 1 is supported)", + ) + model.add_argument("--decoder-rnn-dim", default=1024, type=int, help="number of units in decoder LSTM") + model.add_argument("--decoder-dropout", default=0.1, type=float, help="dropout probability for decoder LSTM") + model.add_argument("--decoder-max-step", default=2000, type=int, help="maximum number of output mel spectrograms") + model.add_argument( + "--decoder-no-early-stopping", + action="store_true", + default=False, + help="stop decoding only when all samples are finished", + ) # attention model - model.add_argument('--attention-hidden-dim', default=128, type=int, - help='dimension of attention hidden representation') - model.add_argument('--attention-rnn-dim', default=1024, type=int, - help='number of units in attention LSTM') - model.add_argument('--attention-location-n-filter', default=32, type=int, - help='number of filters for location-sensitive attention') - model.add_argument('--attention-location-kernel-size', default=31, type=int, - help='kernel size for location-sensitive attention') - model.add_argument('--attention-dropout', default=0.1, type=float, - help='dropout probability for attention LSTM') - - model.add_argument('--prenet-dim', default=256, type=int, - help='number of ReLU units in prenet layers') + model.add_argument( + "--attention-hidden-dim", default=128, type=int, help="dimension of attention hidden representation" + ) + model.add_argument("--attention-rnn-dim", default=1024, type=int, help="number of units in attention LSTM") + model.add_argument( + "--attention-location-n-filter", default=32, type=int, help="number of filters for location-sensitive attention" + ) + model.add_argument( + "--attention-location-kernel-size", default=31, type=int, help="kernel size for location-sensitive attention" + ) + model.add_argument("--attention-dropout", default=0.1, type=float, help="dropout probability for attention LSTM") + + model.add_argument("--prenet-dim", default=256, type=int, help="number of ReLU units in prenet layers") # mel-post processing network parameters - model.add_argument('--postnet-n-convolution', default=5, type=float, - help='number of postnet convolutions') - model.add_argument('--postnet-kernel-size', default=5, type=float, - help='postnet kernel size') - model.add_argument('--postnet-embedding-dim', default=512, type=float, - help='postnet embedding dimension') + model.add_argument("--postnet-n-convolution", default=5, type=float, help="number of postnet convolutions") + model.add_argument("--postnet-kernel-size", default=5, type=float, help="postnet kernel size") + model.add_argument("--postnet-embedding-dim", default=512, type=float, help="postnet embedding dimension") - model.add_argument('--gate-threshold', default=0.5, type=float, - help='probability threshold for stop token') + model.add_argument("--gate-threshold", default=0.5, type=float, help="probability threshold for stop token") # audio parameters - audio = parser.add_argument_group('audio parameters') - audio.add_argument('--sample-rate', default=22050, type=int, - help='Sampling rate') - audio.add_argument('--n-fft', default=1024, type=int, - help='Filter length for STFT') - audio.add_argument('--hop-length', default=256, type=int, - help='Hop (stride) length') - audio.add_argument('--win-length', default=1024, type=int, - help='Window length') - audio.add_argument('--n-mels', default=80, type=int, - help='') - audio.add_argument('--mel-fmin', default=0.0, type=float, - help='Minimum mel frequency') - audio.add_argument('--mel-fmax', default=8000.0, type=float, - help='Maximum mel frequency') + audio = parser.add_argument_group("audio parameters") + audio.add_argument("--sample-rate", default=22050, type=int, help="Sampling rate") + audio.add_argument("--n-fft", default=1024, type=int, help="Filter length for STFT") + audio.add_argument("--hop-length", default=256, type=int, help="Hop (stride) length") + audio.add_argument("--win-length", default=1024, type=int, help="Window length") + audio.add_argument("--n-mels", default=80, type=int, help="") + audio.add_argument("--mel-fmin", default=0.0, type=float, help="Minimum mel frequency") + audio.add_argument("--mel-fmax", default=8000.0, type=float, help="Maximum mel frequency") return parser -def adjust_learning_rate(epoch, optimizer, learning_rate, - anneal_steps, anneal_factor): +def adjust_learning_rate(epoch, optimizer, learning_rate, anneal_steps, anneal_factor): """Adjust learning rate base on the initial setting.""" p = 0 if anneal_steps is not None: @@ -210,7 +209,7 @@ def adjust_learning_rate(epoch, optimizer, learning_rate, lr = learning_rate * (anneal_factor ** p) for param_group in optimizer.param_groups: - param_group['lr'] = lr + param_group["lr"] = lr def to_gpu(x): @@ -296,15 +295,16 @@ def get_datasets(args): f_min=args.mel_fmin, f_max=args.mel_fmax, n_mels=args.n_mels, - mel_scale='slaney', + mel_scale="slaney", normalized=False, power=1, - norm='slaney', + norm="slaney", ), - SpectralNormalization() + SpectralNormalization(), ) trainset, valset = split_process_dataset( - args.dataset, args.dataset_path, args.val_ratio, transforms, text_preprocessor) + args.dataset, args.dataset_path, args.val_ratio, transforms, text_preprocessor + ) return trainset, valset @@ -314,7 +314,7 @@ def train(rank, world_size, args): if rank == 0 and args.logging_dir: if not os.path.isdir(args.logging_dir): os.makedirs(args.logging_dir) - filehandler = logging.FileHandler(os.path.join(args.logging_dir, 'train.log')) + filehandler = logging.FileHandler(os.path.join(args.logging_dir, "train.log")) filehandler.setLevel(logging.INFO) logger.addHandler(filehandler) @@ -362,7 +362,7 @@ def train(rank, world_size, args): if args.checkpoint_path and os.path.isfile(args.checkpoint_path): logger.info(f"Checkpoint: loading '{args.checkpoint_path}'") - map_location = {'cuda:%d' % 0: 'cuda:%d' % rank} + map_location = {"cuda:%d" % 0: "cuda:%d" % rank} checkpoint = torch.load(args.checkpoint_path, map_location=map_location) start_epoch = checkpoint["epoch"] @@ -371,9 +371,7 @@ def train(rank, world_size, args): model.load_state_dict(checkpoint["state_dict"]) optimizer.load_state_dict(checkpoint["optimizer"]) - logger.info( - f"Checkpoint: loaded '{args.checkpoint_path}' at epoch {checkpoint['epoch']}" - ) + logger.info(f"Checkpoint: loaded '{args.checkpoint_path}' at epoch {checkpoint['epoch']}") trainset, valset = get_datasets(args) @@ -394,7 +392,7 @@ def train(rank, world_size, args): "batch_size": args.batch_size, "num_workers": args.workers, "prefetch_factor": 1024, - 'persistent_workers': True, + "persistent_workers": True, "shuffle": False, "pin_memory": True, "drop_last": False, @@ -417,16 +415,14 @@ def train(rank, world_size, args): iterator = enumerate(train_loader) for i, batch in iterator: - adjust_learning_rate(epoch, optimizer, args.learning_rate, - args.anneal_steps, args.anneal_factor) + adjust_learning_rate(epoch, optimizer, args.learning_rate, args.anneal_steps, args.anneal_factor) model.zero_grad() loss, losses = training_step(model, batch, i) loss.backward() - torch.nn.utils.clip_grad_norm_( - model.parameters(), args.grad_clip) + torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) optimizer.step() @@ -492,36 +488,44 @@ def main(args): random.seed(0) if args.master_addr is not None: - os.environ['MASTER_ADDR'] = args.master_addr - elif 'MASTER_ADDR' not in os.environ: - os.environ['MASTER_ADDR'] = 'localhost' + os.environ["MASTER_ADDR"] = args.master_addr + elif "MASTER_ADDR" not in os.environ: + os.environ["MASTER_ADDR"] = "localhost" if args.master_port is not None: - os.environ['MASTER_PORT'] = args.master_port - elif 'MASTER_PORT' not in os.environ: - os.environ['MASTER_PORT'] = '17778' + os.environ["MASTER_PORT"] = args.master_port + elif "MASTER_PORT" not in os.environ: + os.environ["MASTER_PORT"] = "17778" device_counts = torch.cuda.device_count() logger.info(f"# available GPUs: {device_counts}") # download dataset is not already downloaded - if args.dataset == 'ljspeech': - if not os.path.exists(os.path.join(args.dataset_path, 'LJSpeech-1.1')): + if args.dataset == "ljspeech": + if not os.path.exists(os.path.join(args.dataset_path, "LJSpeech-1.1")): from torchaudio.datasets import LJSPEECH + LJSPEECH(root=args.dataset_path, download=True) if device_counts == 1: train(0, 1, args) else: - mp.spawn(train, args=(device_counts, args, ), - nprocs=device_counts, join=True) + mp.spawn( + train, + args=( + device_counts, + args, + ), + nprocs=device_counts, + join=True, + ) logger.info(f"End time: {datetime.now()}") -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='PyTorch Tacotron 2 Training') +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="PyTorch Tacotron 2 Training") parser = parse_args(parser) args, _ = parser.parse_known_args() diff --git a/examples/pipeline_tacotron2/utils.py b/examples/pipeline_tacotron2/utils.py index dca668a17a..27cc119884 100644 --- a/examples/pipeline_tacotron2/utils.py +++ b/examples/pipeline_tacotron2/utils.py @@ -53,21 +53,19 @@ def pad_sequences(batch: List[Tensor]) -> Tuple[Tensor, Tensor]: Modified from https://github.com/NVIDIA/DeepLearningExamples. """ - input_lengths, ids_sorted_decreasing = torch.sort( - torch.LongTensor([len(x) for x in batch]), dim=0, descending=True) + input_lengths, ids_sorted_decreasing = torch.sort(torch.LongTensor([len(x) for x in batch]), dim=0, descending=True) max_input_len = input_lengths[0] text_padded = torch.LongTensor(len(batch), max_input_len) text_padded.zero_() for i in range(len(ids_sorted_decreasing)): text = batch[ids_sorted_decreasing[i]] - text_padded[i, :text.size(0)] = text + text_padded[i, : text.size(0)] = text return text_padded, input_lengths -def prepare_input_sequence(texts: List[str], - text_processor: Callable[[str], List[int]]) -> Tuple[Tensor, Tensor]: +def prepare_input_sequence(texts: List[str], text_processor: Callable[[str], List[int]]) -> Tuple[Tensor, Tensor]: d = [] for text in texts: d.append(torch.IntTensor(text_processor(text)[:])) diff --git a/examples/pipeline_wav2letter/datasets.py b/examples/pipeline_wav2letter/datasets.py index 79b05b2c5b..5f4dc477bf 100644 --- a/examples/pipeline_wav2letter/datasets.py +++ b/examples/pipeline_wav2letter/datasets.py @@ -51,7 +51,11 @@ def process_datapoint(self, item): def split_process_librispeech( - datasets, transforms, language_model, root, folder_in_archive, + datasets, + transforms, + language_model, + root, + folder_in_archive, ): def create(tags, cache=True): @@ -66,7 +70,10 @@ def create(tags, cache=True): [ Processed( LIBRISPEECH( - root, tag, folder_in_archive=folder_in_archive, download=False, + root, + tag, + folder_in_archive=folder_in_archive, + download=False, ), transform, language_model.encode, diff --git a/examples/pipeline_wav2letter/main.py b/examples/pipeline_wav2letter/main.py index 1668223067..25e247cc9e 100644 --- a/examples/pipeline_wav2letter/main.py +++ b/examples/pipeline_wav2letter/main.py @@ -7,16 +7,15 @@ import torch import torchaudio +from ctc_decoders import GreedyDecoder +from datasets import collate_factory, split_process_librispeech +from languagemodels import LanguageModel from torch.optim import SGD, Adadelta, Adam, AdamW from torch.optim.lr_scheduler import ExponentialLR, ReduceLROnPlateau from torch.utils.data import DataLoader from torchaudio.datasets.utils import bg_iterator from torchaudio.functional import edit_distance from torchaudio.models.wav2letter import Wav2Letter - -from ctc_decoders import GreedyDecoder -from datasets import collate_factory, split_process_librispeech -from languagemodels import LanguageModel from transforms import Normalize, UnsqueezeFirst from utils import MetricLogger, count_parameters, save_checkpoint @@ -80,20 +79,14 @@ def parse_args(): metavar="N", help="number of total epochs to run", ) - parser.add_argument( - "--start-epoch", default=0, type=int, metavar="N", help="manual epoch number" - ) + parser.add_argument("--start-epoch", default=0, type=int, metavar="N", help="manual epoch number") parser.add_argument( "--reduce-lr-valid", action="store_true", help="reduce learning rate based on validation loss", ) - parser.add_argument( - "--normalize", action="store_true", help="normalize model input" - ) - parser.add_argument( - "--progress-bar", action="store_true", help="use progress bar while training" - ) + parser.add_argument("--normalize", action="store_true", help="normalize model input") + parser.add_argument("--progress-bar", action="store_true", help="use progress bar while training") parser.add_argument( "--decoder", metavar="D", @@ -101,9 +94,7 @@ def parse_args(): choices=["greedy"], help="decoder to use", ) - parser.add_argument( - "--batch-size", default=128, type=int, metavar="N", help="mini-batch size" - ) + parser.add_argument("--batch-size", default=128, type=int, metavar="N", help="mini-batch size") parser.add_argument( "--n-bins", default=13, @@ -139,12 +130,8 @@ def parse_args(): metavar="GAMMA", help="learning rate exponential decay constant", ) - parser.add_argument( - "--momentum", default=0.8, type=float, metavar="M", help="momentum" - ) - parser.add_argument( - "--weight-decay", default=1e-5, type=float, metavar="W", help="weight decay" - ) + parser.add_argument("--momentum", default=0.8, type=float, metavar="M", help="momentum") + parser.add_argument("--weight-decay", default=1e-5, type=float, metavar="W", help="weight decay") parser.add_argument("--eps", metavar="EPS", type=float, default=1e-8) parser.add_argument("--rho", metavar="RHO", type=float, default=0.95) parser.add_argument("--clip-grad", metavar="NORM", type=float, default=0.0) @@ -172,13 +159,9 @@ def parse_args(): type=str, help="select which part of librispeech to validate with", ) - parser.add_argument( - "--distributed", action="store_true", help="enable DistributedDataParallel" - ) + parser.add_argument("--distributed", action="store_true", help="enable DistributedDataParallel") parser.add_argument("--seed", type=int, default=0, help="random seed") - parser.add_argument( - "--world-size", type=int, default=8, help="the world size to initiate DPP" - ) + parser.add_argument("--world-size", type=int, default=8, help="the world size to initiate DPP") parser.add_argument("--jit", action="store_true", help="if used, model is jitted") args = parser.parse_args() @@ -263,9 +246,7 @@ def train_one_epoch( metric = MetricLogger("train", disable=disable_logger) metric["epoch"] = epoch - for inputs, targets, tensors_lengths, target_lengths in bg_iterator( - data_loader, maxsize=2 - ): + for inputs, targets, tensors_lengths, target_lengths in bg_iterator(data_loader, maxsize=2): start = time() inputs = inputs.to(device, non_blocking=True) @@ -286,9 +267,7 @@ def train_one_epoch( loss.backward() if clip_grad > 0: - metric["gradient"] = torch.nn.utils.clip_grad_norm_( - model.parameters(), clip_grad - ) + metric["gradient"] = torch.nn.utils.clip_grad_norm_(model.parameters(), clip_grad) optimizer.step() @@ -335,9 +314,7 @@ def evaluate( metric = MetricLogger("validation", disable=disable_logger) metric["epoch"] = epoch - for inputs, targets, tensors_lengths, target_lengths in bg_iterator( - data_loader, maxsize=2 - ): + for inputs, targets, tensors_lengths, target_lengths in bg_iterator(data_loader, maxsize=2): inputs = inputs.to(device, non_blocking=True) targets = targets.to(device, non_blocking=True) @@ -351,9 +328,7 @@ def evaluate( # input_lengths: batch size # target_lengths: batch size - metric["cumulative loss"] += criterion( - outputs, targets, tensors_lengths, target_lengths - ).item() + metric["cumulative loss"] += criterion(outputs, targets, tensors_lengths, target_lengths).item() metric["dataset length"] += len(inputs) metric["iteration"] += 1 @@ -518,9 +493,7 @@ def main(rank, args): else: raise ValueError("Selected scheduler not supported") - criterion = torch.nn.CTCLoss( - blank=language_model.mapping[char_blank], zero_infinity=False - ) + criterion = torch.nn.CTCLoss(blank=language_model.mapping[char_blank], zero_infinity=False) # Data Loader @@ -569,9 +542,7 @@ def main(rank, args): optimizer.load_state_dict(checkpoint["optimizer"]) scheduler.load_state_dict(checkpoint["scheduler"]) - logging.info( - "Checkpoint: loaded '%s' at epoch %s", args.checkpoint, checkpoint["epoch"] - ) + logging.info("Checkpoint: loaded '%s' at epoch %s", args.checkpoint, checkpoint["epoch"]) else: logging.info("Checkpoint: not found") @@ -649,9 +620,7 @@ def main(rank, args): def spawn_main(main, args): if args.distributed: - torch.multiprocessing.spawn( - main, args=(args,), nprocs=args.world_size, join=True - ) + torch.multiprocessing.spawn(main, args=(args,), nprocs=args.world_size, join=True) else: main(0, args) diff --git a/examples/pipeline_wavernn/datasets.py b/examples/pipeline_wavernn/datasets.py index ba836690b4..6cc73dd777 100644 --- a/examples/pipeline_wavernn/datasets.py +++ b/examples/pipeline_wavernn/datasets.py @@ -1,16 +1,14 @@ import random import torch +from processing import bits_to_normalized_waveform, normalized_waveform_to_bits from torch.utils.data.dataset import random_split from torchaudio.datasets import LJSPEECH, LIBRITTS from torchaudio.transforms import MuLawEncoding -from processing import bits_to_normalized_waveform, normalized_waveform_to_bits - class MapMemoryCache(torch.utils.data.Dataset): - r"""Wrap a dataset so that, whenever a new item is returned, it is saved to memory. - """ + r"""Wrap a dataset so that, whenever a new item is returned, it is saved to memory.""" def __init__(self, dataset): self.dataset = dataset @@ -47,16 +45,16 @@ def process_datapoint(self, item): def split_process_dataset(args, transforms): - if args.dataset == 'ljspeech': + if args.dataset == "ljspeech": data = LJSPEECH(root=args.file_path, download=False) val_length = int(len(data) * args.val_ratio) lengths = [len(data) - val_length, val_length] train_dataset, val_dataset = random_split(data, lengths) - elif args.dataset == 'libritts': - train_dataset = LIBRITTS(root=args.file_path, url='train-clean-100', download=False) - val_dataset = LIBRITTS(root=args.file_path, url='dev-clean', download=False) + elif args.dataset == "libritts": + train_dataset = LIBRITTS(root=args.file_path, url="train-clean-100", download=False) + val_dataset = LIBRITTS(root=args.file_path, url="dev-clean", download=False) else: raise ValueError(f"Expected dataset: `ljspeech` or `libritts`, but found {args.dataset}") @@ -88,14 +86,8 @@ def raw_collate(batch): # random start postion in waveform wave_offsets = [(offset + pad) * args.hop_length for offset in spec_offsets] - waveform_combine = [ - x[0][wave_offsets[i]: wave_offsets[i] + wave_length + 1] - for i, x in enumerate(batch) - ] - specgram = [ - x[1][:, spec_offsets[i]: spec_offsets[i] + spec_length] - for i, x in enumerate(batch) - ] + waveform_combine = [x[0][wave_offsets[i] : wave_offsets[i] + wave_length + 1] for i, x in enumerate(batch)] + specgram = [x[1][:, spec_offsets[i] : spec_offsets[i] + spec_length] for i, x in enumerate(batch)] specgram = torch.stack(specgram) waveform_combine = torch.stack(waveform_combine) diff --git a/examples/pipeline_wavernn/inference.py b/examples/pipeline_wavernn/inference.py index 08b608a4f9..9e5f5ae543 100644 --- a/examples/pipeline_wavernn/inference.py +++ b/examples/pipeline_wavernn/inference.py @@ -2,44 +2,46 @@ import torch import torchaudio -from torchaudio.transforms import MelSpectrogram +from processing import NormalizeDB +from torchaudio.datasets import LJSPEECH from torchaudio.models import wavernn from torchaudio.models.wavernn import _MODEL_CONFIG_AND_URLS -from torchaudio.datasets import LJSPEECH - +from torchaudio.transforms import MelSpectrogram from wavernn_inference_wrapper import WaveRNNInferenceWrapper -from processing import NormalizeDB def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( - "--output-wav-path", default="./output.wav", type=str, metavar="PATH", + "--output-wav-path", + default="./output.wav", + type=str, + metavar="PATH", help="The path to output the reconstructed wav file.", ) parser.add_argument( - "--jit", default=False, action="store_true", - help="If used, the model and inference function is jitted." - ) - parser.add_argument( - "--no-batch-inference", default=False, action="store_true", - help="Don't use batch inference." + "--jit", default=False, action="store_true", help="If used, the model and inference function is jitted." ) + parser.add_argument("--no-batch-inference", default=False, action="store_true", help="Don't use batch inference.") parser.add_argument( - "--no-mulaw", default=False, action="store_true", - help="Don't use mulaw decoder to decoder the signal." + "--no-mulaw", default=False, action="store_true", help="Don't use mulaw decoder to decoder the signal." ) parser.add_argument( - "--checkpoint-name", default="wavernn_10k_epochs_8bits_ljspeech", + "--checkpoint-name", + default="wavernn_10k_epochs_8bits_ljspeech", choices=list(_MODEL_CONFIG_AND_URLS.keys()), - help="Select the WaveRNN checkpoint." + help="Select the WaveRNN checkpoint.", ) parser.add_argument( - "--batch-timesteps", default=100, type=int, + "--batch-timesteps", + default=100, + type=int, help="The time steps for each batch. Only used when batch inference is used", ) parser.add_argument( - "--batch-overlap", default=5, type=int, + "--batch-overlap", + default=5, + type=int, help="The overlapping time steps between batches. Only used when batch inference is used", ) args = parser.parse_args() @@ -51,15 +53,15 @@ def main(args): waveform, sample_rate, _, _ = LJSPEECH("./", download=True)[0] mel_kwargs = { - 'sample_rate': sample_rate, - 'n_fft': 2048, - 'f_min': 40., - 'n_mels': 80, - 'win_length': 1100, - 'hop_length': 275, - 'mel_scale': 'slaney', - 'norm': 'slaney', - 'power': 1, + "sample_rate": sample_rate, + "n_fft": 2048, + "f_min": 40.0, + "n_mels": 80, + "win_length": 1100, + "hop_length": 275, + "mel_scale": "slaney", + "norm": "slaney", + "power": 1, } transforms = torch.nn.Sequential( MelSpectrogram(**mel_kwargs), @@ -74,11 +76,13 @@ def main(args): wavernn_inference_model = torch.jit.script(wavernn_inference_model) with torch.no_grad(): - output = wavernn_inference_model(mel_specgram.to(device), - mulaw=(not args.no_mulaw), - batched=(not args.no_batch_inference), - timesteps=args.batch_timesteps, - overlap=args.batch_overlap,) + output = wavernn_inference_model( + mel_specgram.to(device), + mulaw=(not args.no_mulaw), + batched=(not args.no_batch_inference), + timesteps=args.batch_timesteps, + overlap=args.batch_overlap, + ) torchaudio.save(args.output_wav_path, output, sample_rate=sample_rate) diff --git a/examples/pipeline_wavernn/losses.py b/examples/pipeline_wavernn/losses.py index a4494b05fb..0d0608801d 100644 --- a/examples/pipeline_wavernn/losses.py +++ b/examples/pipeline_wavernn/losses.py @@ -6,8 +6,7 @@ class LongCrossEntropyLoss(nn.Module): - r""" CrossEntropy loss - """ + r"""CrossEntropy loss""" def __init__(self): super(LongCrossEntropyLoss, self).__init__() @@ -21,7 +20,7 @@ def forward(self, output, target): class MoLLoss(nn.Module): - r""" Discretized mixture of logistic distributions loss + r"""Discretized mixture of logistic distributions loss Adapted from wavenet vocoder (https://github.com/r9y9/wavenet_vocoder/blob/master/wavenet_vocoder/mixture.py) @@ -57,10 +56,8 @@ def forward(self, y_hat, y): # unpack parameters (n_batch, n_time, num_mixtures) x 3 logit_probs = y_hat[:, :, :nr_mix] - means = y_hat[:, :, nr_mix: 2 * nr_mix] - log_scales = torch.clamp( - y_hat[:, :, 2 * nr_mix: 3 * nr_mix], min=self.log_scale_min - ) + means = y_hat[:, :, nr_mix : 2 * nr_mix] + log_scales = torch.clamp(y_hat[:, :, 2 * nr_mix : 3 * nr_mix], min=self.log_scale_min) # (n_batch x n_time x 1) to (n_batch x n_time x num_mixtures) y = y.expand_as(means) @@ -89,15 +86,11 @@ def forward(self, y_hat, y): inner_inner_cond = (cdf_delta > 1e-5).float() - inner_inner_out = inner_inner_cond * torch.log( - torch.clamp(cdf_delta, min=1e-12) - ) + (1.0 - inner_inner_cond) * ( + inner_inner_out = inner_inner_cond * torch.log(torch.clamp(cdf_delta, min=1e-12)) + (1.0 - inner_inner_cond) * ( log_pdf_mid - math.log((self.num_classes - 1) / 2) ) inner_cond = (y > 0.999).float() - inner_out = ( - inner_cond * log_one_minus_cdf_min + (1.0 - inner_cond) * inner_inner_out - ) + inner_out = inner_cond * log_one_minus_cdf_min + (1.0 - inner_cond) * inner_inner_out cond = (y < -0.999).float() log_probs = cond * log_cdf_plus + (1.0 - cond) * inner_out @@ -110,8 +103,7 @@ def forward(self, y_hat, y): def _log_sum_exp(x): - r""" Numerically stable log_sum_exp implementation that prevents overflow - """ + r"""Numerically stable log_sum_exp implementation that prevents overflow""" axis = len(x.size()) - 1 m, _ = torch.max(x, dim=axis) diff --git a/examples/pipeline_wavernn/main.py b/examples/pipeline_wavernn/main.py index 3d561f091c..fa11c40402 100644 --- a/examples/pipeline_wavernn/main.py +++ b/examples/pipeline_wavernn/main.py @@ -8,14 +8,13 @@ import torch import torchaudio +from datasets import collate_factory, split_process_dataset +from losses import LongCrossEntropyLoss, MoLLoss +from processing import NormalizeDB from torch.optim import Adam from torch.utils.data import DataLoader from torchaudio.datasets.utils import bg_iterator from torchaudio.models.wavernn import WaveRNN - -from datasets import collate_factory, split_process_dataset -from losses import LongCrossEntropyLoss, MoLLoss -from processing import NormalizeDB from utils import MetricLogger, count_parameters, save_checkpoint @@ -43,9 +42,7 @@ def parse_args(): metavar="N", help="number of total epochs to run", ) - parser.add_argument( - "--start-epoch", default=0, type=int, metavar="N", help="manual epoch number" - ) + parser.add_argument("--start-epoch", default=0, type=int, metavar="N", help="manual epoch number") parser.add_argument( "--print-freq", default=10, @@ -60,11 +57,13 @@ def parse_args(): type=str, help="select dataset to train with", ) + parser.add_argument("--batch-size", default=256, type=int, metavar="N", help="mini-batch size") parser.add_argument( - "--batch-size", default=256, type=int, metavar="N", help="mini-batch size" - ) - parser.add_argument( - "--learning-rate", default=1e-4, type=float, metavar="LR", help="learning rate", + "--learning-rate", + default=1e-4, + type=float, + metavar="LR", + help="learning rate", ) parser.add_argument("--clip-grad", metavar="NORM", type=float, default=4.0) parser.add_argument( @@ -73,9 +72,7 @@ def parse_args(): action="store_true", help="if used, waveform is mulaw encoded", ) - parser.add_argument( - "--jit", default=False, action="store_true", help="if used, model is jitted" - ) + parser.add_argument("--jit", default=False, action="store_true", help="if used, model is jitted") parser.add_argument( "--upsample-scales", default=[5, 5, 11], @@ -83,7 +80,10 @@ def parse_args(): help="the list of upsample scales", ) parser.add_argument( - "--n-bits", default=8, type=int, help="the bits of output waveform", + "--n-bits", + default=8, + type=int, + help="the bits of output waveform", ) parser.add_argument( "--sample-rate", @@ -98,10 +98,16 @@ def parse_args(): help="the number of samples between the starts of consecutive frames", ) parser.add_argument( - "--win-length", default=1100, type=int, help="the length of the STFT window", + "--win-length", + default=1100, + type=int, + help="the length of the STFT window", ) parser.add_argument( - "--f-min", default=40.0, type=float, help="the minimum frequency", + "--f-min", + default=40.0, + type=float, + help="the minimum frequency", ) parser.add_argument( "--min-level-db", @@ -110,13 +116,22 @@ def parse_args(): help="the minimum db value for spectrogam normalization", ) parser.add_argument( - "--n-res-block", default=10, type=int, help="the number of ResBlock in stack", + "--n-res-block", + default=10, + type=int, + help="the number of ResBlock in stack", ) parser.add_argument( - "--n-rnn", default=512, type=int, help="the dimension of RNN layer", + "--n-rnn", + default=512, + type=int, + help="the dimension of RNN layer", ) parser.add_argument( - "--n-fc", default=512, type=int, help="the dimension of fully connected layer", + "--n-fc", + default=512, + type=int, + help="the dimension of fully connected layer", ) parser.add_argument( "--kernel-size", @@ -125,7 +140,10 @@ def parse_args(): help="the number of kernel size in the first Conv1d layer", ) parser.add_argument( - "--n-freq", default=80, type=int, help="the number of spectrogram bins to use", + "--n-freq", + default=80, + type=int, + help="the number of spectrogram bins to use", ) parser.add_argument( "--n-hidden-melresnet", @@ -134,10 +152,16 @@ def parse_args(): help="the number of hidden dimensions of resblock in melresnet", ) parser.add_argument( - "--n-output-melresnet", default=128, type=int, help="the output dimension of melresnet", + "--n-output-melresnet", + default=128, + type=int, + help="the output dimension of melresnet", ) parser.add_argument( - "--n-fft", default=2048, type=int, help="the number of Fourier bins", + "--n-fft", + default=2048, + type=int, + help="the number of Fourier bins", ) parser.add_argument( "--loss", @@ -159,10 +183,16 @@ def parse_args(): help="the ratio of waveforms for validation", ) parser.add_argument( - "--file-path", default="", type=str, help="the path of audio files", + "--file-path", + default="", + type=str, + help="the path of audio files", ) parser.add_argument( - "--normalization", default=True, action="store_true", help="if True, spectrogram is normalized", + "--normalization", + default=True, + action="store_true", + help="if True, spectrogram is normalized", ) args = parser.parse_args() @@ -199,9 +229,7 @@ def train_one_epoch(model, criterion, optimizer, data_loader, device, epoch): loss.backward() if args.clip_grad > 0: - gradient = torch.nn.utils.clip_grad_norm_( - model.parameters(), args.clip_grad - ) + gradient = torch.nn.utils.clip_grad_norm_(model.parameters(), args.clip_grad) sums["gradient"] += gradient.item() metric["gradient"] = gradient.item() @@ -271,8 +299,8 @@ def main(args): sample_rate=args.sample_rate, n_mels=args.n_freq, f_min=args.f_min, - mel_scale='slaney', - norm='slaney', + mel_scale="slaney", + norm="slaney", **melkwargs, ), NormalizeDB(min_level_db=args.min_level_db, normalization=args.normalization), @@ -349,9 +377,7 @@ def main(args): model.load_state_dict(checkpoint["state_dict"]) optimizer.load_state_dict(checkpoint["optimizer"]) - logging.info( - f"Checkpoint: loaded '{args.checkpoint}' at epoch {checkpoint['epoch']}" - ) + logging.info(f"Checkpoint: loaded '{args.checkpoint}' at epoch {checkpoint['epoch']}") else: logging.info("Checkpoint: not found") @@ -369,7 +395,12 @@ def main(args): for epoch in range(args.start_epoch, args.epochs): train_one_epoch( - model, criterion, optimizer, train_loader, devices[0], epoch, + model, + criterion, + optimizer, + train_loader, + devices[0], + epoch, ) if not (epoch + 1) % args.print_freq or epoch == args.epochs - 1: diff --git a/examples/pipeline_wavernn/processing.py b/examples/pipeline_wavernn/processing.py index db2b1ee9f7..6f795b9461 100644 --- a/examples/pipeline_wavernn/processing.py +++ b/examples/pipeline_wavernn/processing.py @@ -3,8 +3,7 @@ class NormalizeDB(nn.Module): - r"""Normalize the spectrogram with a minimum db value - """ + r"""Normalize the spectrogram with a minimum db value""" def __init__(self, min_level_db, normalization): super().__init__() @@ -14,15 +13,12 @@ def __init__(self, min_level_db, normalization): def forward(self, specgram): specgram = torch.log10(torch.clamp(specgram.squeeze(0), min=1e-5)) if self.normalization: - return torch.clamp( - (self.min_level_db - 20 * specgram) / self.min_level_db, min=0, max=1 - ) + return torch.clamp((self.min_level_db - 20 * specgram) / self.min_level_db, min=0, max=1) return specgram def normalized_waveform_to_bits(waveform: torch.Tensor, bits: int) -> torch.Tensor: - r"""Transform waveform [-1, 1] to label [0, 2 ** bits - 1] - """ + r"""Transform waveform [-1, 1] to label [0, 2 ** bits - 1]""" assert abs(waveform).max() <= 1.0 waveform = (waveform + 1.0) * (2 ** bits - 1) / 2 @@ -30,7 +26,6 @@ def normalized_waveform_to_bits(waveform: torch.Tensor, bits: int) -> torch.Tens def bits_to_normalized_waveform(label: torch.Tensor, bits: int) -> torch.Tensor: - r"""Transform label [0, 2 ** bits - 1] to waveform [-1, 1] - """ + r"""Transform label [0, 2 ** bits - 1] to waveform [-1, 1]""" return 2 * label / (2 ** bits - 1.0) - 1.0 diff --git a/examples/pipeline_wavernn/utils.py b/examples/pipeline_wavernn/utils.py index e924c9f512..c9e9dd17ba 100644 --- a/examples/pipeline_wavernn/utils.py +++ b/examples/pipeline_wavernn/utils.py @@ -7,8 +7,7 @@ class MetricLogger: - r"""Logger for model metrics - """ + r"""Logger for model metrics""" def __init__(self, group, print_freq=1): self.print_freq = print_freq @@ -55,7 +54,6 @@ def save_checkpoint(state, is_best, filename): def count_parameters(model): - r"""Count the total number of parameters in the model - """ + r"""Count the total number of parameters in the model""" return sum(p.numel() for p in model.parameters() if p.requires_grad) diff --git a/examples/pipeline_wavernn/wavernn_inference_wrapper.py b/examples/pipeline_wavernn/wavernn_inference_wrapper.py index 5d5c4db71f..8a889af1c6 100644 --- a/examples/pipeline_wavernn/wavernn_inference_wrapper.py +++ b/examples/pipeline_wavernn/wavernn_inference_wrapper.py @@ -21,16 +21,15 @@ # ***************************************************************************** -from torchaudio.models.wavernn import WaveRNN import torch import torchaudio -from torch import Tensor - from processing import normalized_waveform_to_bits +from torch import Tensor +from torchaudio.models.wavernn import WaveRNN def _fold_with_overlap(x: Tensor, timesteps: int, overlap: int) -> Tensor: - r'''Fold the tensor with overlap for quick batched inference. + r"""Fold the tensor with overlap for quick batched inference. Overlap will be used for crossfading in xfade_and_unfold(). x = [[h1, h2, ... hn]] @@ -47,7 +46,7 @@ def _fold_with_overlap(x: Tensor, timesteps: int, overlap: int) -> Tensor: Return: folded (tensor): folded tensor of size (n_folds, timesteps + 2 * overlap, channel). - ''' + """ _, channels, total_len = x.size() @@ -74,7 +73,7 @@ def _fold_with_overlap(x: Tensor, timesteps: int, overlap: int) -> Tensor: def _xfade_and_unfold(y: Tensor, overlap: int) -> Tensor: - r'''Applies a crossfade and unfolds into a 1d array. + r"""Applies a crossfade and unfolds into a 1d array. y = [[seq1], [seq2], @@ -93,7 +92,7 @@ def _xfade_and_unfold(y: Tensor, overlap: int) -> Tensor: Returns: unfolded waveform (Tensor) : waveform in a 1d tensor of size (channels, total_len). - ''' + """ num_folds, channels, length = y.shape timesteps = length - 2 * overlap @@ -130,17 +129,13 @@ def _xfade_and_unfold(y: Tensor, overlap: int) -> Tensor: class WaveRNNInferenceWrapper(torch.nn.Module): - def __init__(self, wavernn: WaveRNN): super().__init__() self.wavernn_model = wavernn - def forward(self, - specgram: Tensor, - mulaw: bool = True, - batched: bool = True, - timesteps: int = 100, - overlap: int = 5) -> Tensor: + def forward( + self, specgram: Tensor, mulaw: bool = True, batched: bool = True, timesteps: int = 100, overlap: int = 5 + ) -> Tensor: r"""Inference function for WaveRNN. Based on the implementation from diff --git a/examples/source_separation/conv_tasnet/__init__.py b/examples/source_separation/conv_tasnet/__init__.py index 9c31ed253f..0d56b09c7c 100644 --- a/examples/source_separation/conv_tasnet/__init__.py +++ b/examples/source_separation/conv_tasnet/__init__.py @@ -1,6 +1,3 @@ -from . import ( - train, - trainer -) +from . import train, trainer -__all__ = ['train', 'trainer'] +__all__ = ["train", "trainer"] diff --git a/examples/source_separation/conv_tasnet/train.py b/examples/source_separation/conv_tasnet/train.py index 691e7f3415..133b1f4f5e 100644 --- a/examples/source_separation/conv_tasnet/train.py +++ b/examples/source_separation/conv_tasnet/train.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 """Train Conv-TasNet""" -import time -import pathlib import argparse +import pathlib +import time +import conv_tasnet import torch import torchaudio import torchaudio.models - -import conv_tasnet from utils import dist_utils from utils.dataset import utils as dataset_utils @@ -16,15 +15,14 @@ def _parse_args(args): - parser = argparse.ArgumentParser(description=__doc__,) + parser = argparse.ArgumentParser( + description=__doc__, + ) parser.add_argument( - "--debug", - action="store_true", - help="Enable debug behavior. Each epoch will end with just one batch.") - group = parser.add_argument_group("Model Options") - group.add_argument( - "--num-speakers", required=True, type=int, help="The number of speakers." + "--debug", action="store_true", help="Enable debug behavior. Each epoch will end with just one batch." ) + group = parser.add_argument_group("Model Options") + group.add_argument("--num-speakers", required=True, type=int, help="The number of speakers.") group = parser.add_argument_group("Dataset Options") group.add_argument( "--sample-rate", @@ -132,7 +130,8 @@ def _get_model( _LG.info_on_master(" - X: %d", msk_num_layers) _LG.info_on_master(" - R: %d", msk_num_stacks) _LG.info_on_master( - " - Receptive Field: %s [samples]", model.mask_generator.receptive_field, + " - Receptive Field: %s [samples]", + model.mask_generator.receptive_field, ) return model @@ -141,11 +140,9 @@ def _get_dataloader(dataset_type, dataset_dir, num_speakers, sample_rate, batch_ train_dataset, valid_dataset, eval_dataset = dataset_utils.get_dataset( dataset_type, dataset_dir, num_speakers, sample_rate, task ) - train_collate_fn = dataset_utils.get_collate_fn( - dataset_type, mode='train', sample_rate=sample_rate, duration=4 - ) + train_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode="train", sample_rate=sample_rate, duration=4) - test_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode='test') + test_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode="test") train_loader = torch.utils.data.DataLoader( train_dataset, @@ -173,8 +170,12 @@ def _get_dataloader(dataset_type, dataset_dir, num_speakers, sample_rate, batch_ def _write_header(log_path, args): rows = [ - [f"# torch: {torch.__version__}", ], - [f"# torchaudio: {torchaudio.__version__}", ] + [ + f"# torch: {torch.__version__}", + ], + [ + f"# torchaudio: {torchaudio.__version__}", + ], ] rows.append(["# arguments"]) for key, item in vars(args).items(): @@ -212,9 +213,7 @@ def train(args): model = _get_model(num_sources=args.num_speakers) model.to(device) - model = torch.nn.parallel.DistributedDataParallel( - model, device_ids=[device] if torch.cuda.is_available() else None - ) + model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[device] if torch.cuda.is_available() else None) optimizer = torch.optim.Adam(model.parameters(), lr=args.learning_rate) if args.resume: @@ -222,13 +221,9 @@ def train(args): model.module.load_state_dict(checkpoint["model"]) optimizer.load_state_dict(checkpoint["optimizer"]) else: - dist_utils.synchronize_params( - str(args.save_dir / "tmp.pt"), device, model, optimizer - ) + dist_utils.synchronize_params(str(args.save_dir / "tmp.pt"), device, model, optimizer) - lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( - optimizer, mode="max", factor=0.5, patience=3 - ) + lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="max", factor=0.5, patience=3) train_loader, valid_loader, eval_loader = _get_dataloader( args.dataset, diff --git a/examples/source_separation/conv_tasnet/trainer.py b/examples/source_separation/conv_tasnet/trainer.py index af4e715305..b2282f260f 100644 --- a/examples/source_separation/conv_tasnet/trainer.py +++ b/examples/source_separation/conv_tasnet/trainer.py @@ -1,25 +1,19 @@ import time -from typing import Tuple from collections import namedtuple +from typing import Tuple import torch import torch.distributed as dist - from utils import dist_utils, metrics _LG = dist_utils.getLogger(__name__) Metric = namedtuple("SNR", ["si_snri", "sdri"]) -Metric.__str__ = ( - lambda self: f"SI-SNRi: {self.si_snri:10.3e}, SDRi: {self.sdri:10.3e}" -) +Metric.__str__ = lambda self: f"SI-SNRi: {self.si_snri:10.3e}, SDRi: {self.sdri:10.3e}" def si_sdr_improvement( - estimate: torch.Tensor, - reference: torch.Tensor, - mix: torch.Tensor, - mask: torch.Tensor + estimate: torch.Tensor, reference: torch.Tensor, mix: torch.Tensor, mask: torch.Tensor ) -> Tuple[torch.Tensor, torch.Tensor]: """Compute the improvement of scale-invariant SDR. (SI-SNRi) and bare SDR (SDRi). @@ -66,11 +60,7 @@ def __init__(self, time_interval=180, progress_interval=0.1): def log(self, metric, progress, force=False): now = time.monotonic() - if ( - force - or now > self.last_time + self.time_interval - or progress > self.last_progress + self.progress_interval - ): + if force or now > self.last_time + self.time_interval or progress > self.last_progress + self.progress_interval: self.last_time = now self.last_progress = progress _LG.info_on_master("train: %s [%3d%%]", metric, 100 * progress) @@ -117,9 +107,7 @@ def train_one_epoch(self): loss = -si_snri self.optimizer.zero_grad() loss.backward() - torch.nn.utils.clip_grad_norm_( - self.model.parameters(), self.grad_clip, norm_type=2.0 - ) + torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip, norm_type=2.0) self.optimizer.step() metric = Metric(si_snri.item(), sdri.item()) diff --git a/examples/source_separation/eval.py b/examples/source_separation/eval.py index c8ab87ea11..33d46a9a65 100644 --- a/examples/source_separation/eval.py +++ b/examples/source_separation/eval.py @@ -1,9 +1,9 @@ from argparse import ArgumentParser from pathlib import Path -from lightning_train import _get_model, _get_dataloader, sisdri_metric import mir_eval import torch +from lightning_train import _get_model, _get_dataloader, sisdri_metric def _eval(model, data_loader, device): @@ -19,12 +19,9 @@ def _eval(model, data_loader, device): mix = mix.repeat(1, src.shape[1], 1).cpu().detach().numpy() sdr, sir, sar, _ = mir_eval.separation.bss_eval_sources(src[0], est[0]) sdr_mix, sir_mix, sar_mix, _ = mir_eval.separation.bss_eval_sources(src[0], mix[0]) - results += torch.tensor([ - sdr.mean() - sdr_mix.mean(), - sisdri, - sir.mean() - sir_mix.mean(), - sar.mean() - sar_mix.mean() - ]) + results += torch.tensor( + [sdr.mean() - sdr_mix.mean(), sisdri, sir.mean() - sir_mix.mean(), sar.mean() - sar_mix.mean()] + ) results /= len(data_loader) print("SDR improvement: ", results[0].item()) print("Si-SDR improvement: ", results[1].item()) @@ -63,28 +60,20 @@ def cli_main(): help="Sample rate of audio files in the given dataset. (default: 8000)", ) parser.add_argument( - "--exp-dir", - default=Path("./exp"), - type=Path, - help="The directory to save checkpoints and logs." - ) - parser.add_argument( - "--gpu-device", - default=-1, - type=int, - help="The gpu device for model inference. (default: -1)" + "--exp-dir", default=Path("./exp"), type=Path, help="The directory to save checkpoints and logs." ) + parser.add_argument("--gpu-device", default=-1, type=int, help="The gpu device for model inference. (default: -1)") args = parser.parse_args() model = _get_model(num_sources=2) - state_dict = torch.load(args.exp_dir / 'best_model.pth') + state_dict = torch.load(args.exp_dir / "best_model.pth") model.load_state_dict(state_dict) if args.gpu_device != -1: - device = torch.device('cuda:' + str(args.gpu_device)) + device = torch.device("cuda:" + str(args.gpu_device)) else: - device = torch.device('cpu') + device = torch.device("cpu") model = model.to(device) diff --git a/examples/source_separation/lightning_train.py b/examples/source_separation/lightning_train.py index 65e06da9a2..b73751b8d2 100644 --- a/examples/source_separation/lightning_train.py +++ b/examples/source_separation/lightning_train.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # pyre-strict -from pathlib import Path from argparse import ArgumentParser +from pathlib import Path from typing import ( Any, Callable, @@ -34,10 +34,7 @@ class Batch(TypedDict): def sisdri_metric( - estimate: torch.Tensor, - reference: torch.Tensor, - mix: torch.Tensor, - mask: torch.Tensor + estimate: torch.Tensor, reference: torch.Tensor, mix: torch.Tensor, mask: torch.Tensor ) -> torch.Tensor: """Compute the improvement of scale-invariant SDR. (SI-SDRi). @@ -100,11 +97,7 @@ def sdri_metric( return sdri.mean().item() -def si_sdr_loss( - estimate: torch.Tensor, - reference: torch.Tensor, - mask: torch.Tensor -) -> torch.Tensor: +def si_sdr_loss(estimate: torch.Tensor, reference: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: """Compute the Si-SDR loss. Args: @@ -181,22 +174,16 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: """ return self.model(x) - def training_step( - self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any - ) -> Dict[str, Any]: + def training_step(self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any) -> Dict[str, Any]: return self._step(batch, batch_idx, "train") - def validation_step( - self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any - ) -> Dict[str, Any]: + def validation_step(self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any) -> Dict[str, Any]: """ Operates on a single batch of data from the validation set. """ return self._step(batch, batch_idx, "val") - def test_step( - self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any - ) -> Optional[Dict[str, Any]]: + def test_step(self, batch: Batch, batch_idx: int, *args: Any, **kwargs: Any) -> Optional[Dict[str, Any]]: """ Operates on a single batch of data from the test set. """ @@ -222,11 +209,7 @@ def configure_optimizers( lr_scheduler = self.lr_scheduler if not lr_scheduler: return self.optim - epoch_schedulers = { - 'scheduler': lr_scheduler, - 'monitor': 'Losses/val_loss', - 'interval': 'epoch' - } + epoch_schedulers = {"scheduler": lr_scheduler, "monitor": "Losses/val_loss", "interval": "epoch"} return [self.optim], [epoch_schedulers] def _compute_metrics( @@ -305,11 +288,9 @@ def _get_dataloader( train_dataset, valid_dataset, eval_dataset = dataset_utils.get_dataset( dataset_type, root_dir, num_speakers, sample_rate, librimix_task, librimix_tr_split ) - train_collate_fn = dataset_utils.get_collate_fn( - dataset_type, mode='train', sample_rate=sample_rate, duration=3 - ) + train_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode="train", sample_rate=sample_rate, duration=3) - test_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode='test', sample_rate=sample_rate) + test_collate_fn = dataset_utils.get_collate_fn(dataset_type, mode="test", sample_rate=sample_rate) train_loader = DataLoader( train_dataset, @@ -367,10 +348,7 @@ def cli_main(): help="Sample rate of audio files in the given dataset. (default: 8000)", ) parser.add_argument( - "--exp-dir", - default=Path("./exp"), - type=Path, - help="The directory to save checkpoints and logs." + "--exp-dir", default=Path("./exp"), type=Path, help="The directory to save checkpoints and logs." ) parser.add_argument( "--epochs", @@ -409,9 +387,7 @@ def cli_main(): model = _get_model(num_sources=args.num_speakers) optimizer = torch.optim.Adam(model.parameters(), lr=args.learning_rate) - lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( - optimizer, mode="min", factor=0.5, patience=5 - ) + lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=5) train_loader, valid_loader, eval_loader = _get_dataloader( args.dataset, args.root_dir, @@ -438,12 +414,7 @@ def cli_main(): ) checkpoint_dir = args.exp_dir / "checkpoints" checkpoint = ModelCheckpoint( - checkpoint_dir, - monitor="Losses/val_loss", - mode="min", - save_top_k=5, - save_weights_only=True, - verbose=True + checkpoint_dir, monitor="Losses/val_loss", mode="min", save_top_k=5, save_weights_only=True, verbose=True ) callbacks = [ checkpoint, diff --git a/examples/source_separation/train.py b/examples/source_separation/train.py index a969d33fb8..5c4e66640c 100644 --- a/examples/source_separation/train.py +++ b/examples/source_separation/train.py @@ -15,13 +15,12 @@ When launching the script as a worker process of a distributed training, you need to configure the coordination of the workers. """ -import sys -import logging import argparse +import logging import subprocess +import sys import torch - from utils import dist_utils _LG = dist_utils.getLogger(__name__) @@ -88,19 +87,13 @@ def _parse_args(args=None): type=int, help="Set random seed value. (default: None)", ) - parser.add_argument( - "rest", nargs=argparse.REMAINDER, help="Model-specific arguments." - ) + parser.add_argument("rest", nargs=argparse.REMAINDER, help="Model-specific arguments.") namespace = parser.parse_args(args) if namespace.worker_id is None: if namespace.device_id is not None: - raise ValueError( - "`--device-id` cannot be provided when runing as master process." - ) + raise ValueError("`--device-id` cannot be provided when runing as master process.") if namespace.num_workers > max_world_size: - raise ValueError( - "--num-workers ({num_workers}) cannot exceed {device_count}." - ) + raise ValueError("--num-workers ({num_workers}) cannot exceed {device_count}.") if namespace.rest[:1] == ["--"]: namespace.rest = namespace.rest[1:] return namespace @@ -120,7 +113,7 @@ def _main(cli_args): world_size=args.num_workers, rank=args.worker_id, local_rank=args.device_id, - backend='nccl' if torch.cuda.is_available() else 'gloo', + backend="nccl" if torch.cuda.is_available() else "gloo", init_method=args.sync_protocol, ) if args.random_seed is not None: @@ -137,12 +130,7 @@ def _run_training_subprocesses(num_workers, original_args): for i in range(num_workers): worker_arg = ["--worker-id", f"{i}", "--num-workers", f"{num_workers}"] device_arg = ["--device-id", f"{i}"] if torch.cuda.is_available() else [] - command = ( - [sys.executable, "-u", sys.argv[0]] - + worker_arg - + device_arg - + original_args - ) + command = [sys.executable, "-u", sys.argv[0]] + worker_arg + device_arg + original_args _LG.info("Launching worker %s: `%s`", i, " ".join(command)) worker = subprocess.Popen(command) workers.append(worker) @@ -163,9 +151,7 @@ def _run_training(args): def _init_logger(rank=None, debug=False): worker_fmt = "[master]" if rank is None else f"[worker {rank:2d}]" - message_fmt = ( - "%(levelname)5s: %(funcName)10s: %(message)s" if debug else "%(message)s" - ) + message_fmt = "%(levelname)5s: %(funcName)10s: %(message)s" if debug else "%(message)s" logging.basicConfig( level=logging.DEBUG if debug else logging.INFO, format=f"%(asctime)s: {worker_fmt} {message_fmt}", diff --git a/examples/source_separation/utils/__init__.py b/examples/source_separation/utils/__init__.py index 0d9f0206e3..4bdc4a3c33 100644 --- a/examples/source_separation/utils/__init__.py +++ b/examples/source_separation/utils/__init__.py @@ -4,4 +4,4 @@ metrics, ) -__all__ = ['dataset', 'dist_utils', 'metrics'] +__all__ = ["dataset", "dist_utils", "metrics"] diff --git a/examples/source_separation/utils/dataset/__init__.py b/examples/source_separation/utils/dataset/__init__.py index 45eb6a785d..d88ad09775 100644 --- a/examples/source_separation/utils/dataset/__init__.py +++ b/examples/source_separation/utils/dataset/__init__.py @@ -1,3 +1,3 @@ from . import utils, wsj0mix -__all__ = ['utils', 'wsj0mix'] +__all__ = ["utils", "wsj0mix"] diff --git a/examples/source_separation/utils/dataset/utils.py b/examples/source_separation/utils/dataset/utils.py index a53cc1231a..77160340b2 100644 --- a/examples/source_separation/utils/dataset/utils.py +++ b/examples/source_separation/utils/dataset/utils.py @@ -1,9 +1,9 @@ -from typing import List -from functools import partial from collections import namedtuple +from functools import partial +from typing import List -from torchaudio.datasets import LibriMix import torch +from torchaudio.datasets import LibriMix from . import wsj0mix @@ -30,8 +30,8 @@ def _fix_num_frames(sample: wsj0mix.SampleType, target_num_frames: int, sample_r src = torch.cat(sample[2], 0) # [num_sources, time] num_channels, num_frames = src.shape - num_seconds = torch.div(num_frames, sample_rate, rounding_mode='floor') - target_seconds = torch.div(target_num_frames, sample_rate, rounding_mode='floor') + num_seconds = torch.div(num_frames, sample_rate, rounding_mode="floor") + target_seconds = torch.div(target_num_frames, sample_rate, rounding_mode="floor") if num_frames >= target_num_frames: if random_start and num_frames > target_num_frames: start_frame = torch.randint(num_seconds - target_seconds + 1, [1]) * sample_rate @@ -81,7 +81,7 @@ def collate_fn_wsj0mix_test(samples: List[wsj0mix.SampleType], sample_rate): def get_collate_fn(dataset_type, mode, sample_rate=None, duration=4): assert mode in ["train", "test"] if dataset_type in ["wsj0mix", "librimix"]: - if mode == 'train': + if mode == "train": if sample_rate is None: raise ValueError("sample_rate is not given.") return partial(collate_fn_wsj0mix_train, sample_rate=sample_rate, duration=duration) diff --git a/examples/source_separation/utils/dataset/wsj0mix.py b/examples/source_separation/utils/dataset/wsj0mix.py index 89d59f3ac3..0b4a21f65d 100644 --- a/examples/source_separation/utils/dataset/wsj0mix.py +++ b/examples/source_separation/utils/dataset/wsj0mix.py @@ -2,9 +2,8 @@ from typing import Union, Tuple, List import torch -from torch.utils.data import Dataset - import torchaudio +from torch.utils.data import Dataset SampleType = Tuple[int, torch.Tensor, List[torch.Tensor]] @@ -21,6 +20,7 @@ class WSJ0Mix(Dataset): different sample rate, raises ``ValueError``. audio_ext (str, optional): The extension of audio files to find. (default: ".wav") """ + def __init__( self, root: Union[str, Path], @@ -51,9 +51,7 @@ def _load_sample(self, filename) -> SampleType: for i, dir_ in enumerate(self.src_dirs): src = self._load_audio(str(dir_ / filename)) if mixed.shape != src.shape: - raise ValueError( - f"Different waveform shapes. mixed: {mixed.shape}, src[{i}]: {src.shape}" - ) + raise ValueError(f"Different waveform shapes. mixed: {mixed.shape}, src[{i}]: {src.shape}") srcs.append(src) return self.sample_rate, mixed, srcs diff --git a/examples/source_separation/utils/dist_utils.py b/examples/source_separation/utils/dist_utils.py index 380358b87b..998ca475b6 100644 --- a/examples/source_separation/utils/dist_utils.py +++ b/examples/source_separation/utils/dist_utils.py @@ -1,7 +1,7 @@ -import os import csv -import types import logging +import os +import types import torch import torch.distributed as dist @@ -22,9 +22,7 @@ def getLogger(name): _LG = getLogger(__name__) -def setup_distributed( - world_size, rank, local_rank, backend="nccl", init_method="env://" -): +def setup_distributed(world_size, rank, local_rank, backend="nccl", init_method="env://"): """Perform env setup and initialization for distributed training""" if init_method == "env://": _set_env_vars(world_size, rank, local_rank) diff --git a/examples/source_separation/utils/metrics.py b/examples/source_separation/utils/metrics.py index 03859aef2b..1c4017ff39 100644 --- a/examples/source_separation/utils/metrics.py +++ b/examples/source_separation/utils/metrics.py @@ -1,15 +1,12 @@ import math -from typing import Optional from itertools import permutations +from typing import Optional import torch def sdr( - estimate: torch.Tensor, - reference: torch.Tensor, - mask: Optional[torch.Tensor] = None, - epsilon: float = 1e-8 + estimate: torch.Tensor, reference: torch.Tensor, mask: Optional[torch.Tensor] = None, epsilon: float = 1e-8 ) -> torch.Tensor: """Computes source-to-distortion ratio. @@ -86,11 +83,11 @@ def __init__(self, utility_func): self.utility_func = utility_func def forward( - self, - estimate: torch.Tensor, - reference: torch.Tensor, - mask: Optional[torch.Tensor] = None, - epsilon: float = 1e-8 + self, + estimate: torch.Tensor, + reference: torch.Tensor, + mask: Optional[torch.Tensor] = None, + epsilon: float = 1e-8, ) -> torch.Tensor: """Compute utterance-level PIT Loss @@ -112,9 +109,7 @@ def forward( batch_size, num_speakers = reference.shape[:2] num_permute = math.factorial(num_speakers) - util_mat = torch.zeros( - batch_size, num_permute, dtype=estimate.dtype, device=estimate.device - ) + util_mat = torch.zeros(batch_size, num_permute, dtype=estimate.dtype, device=estimate.device) for i, idx in enumerate(permutations(range(num_speakers))): util = self.utility_func(estimate, reference[:, idx, :], mask=mask, epsilon=epsilon) util_mat[:, i] = util.mean(dim=1) # take the average over speaker dimension @@ -125,10 +120,8 @@ def forward( def sdr_pit( - estimate: torch.Tensor, - reference: torch.Tensor, - mask: Optional[torch.Tensor] = None, - epsilon: float = 1e-8): + estimate: torch.Tensor, reference: torch.Tensor, mask: Optional[torch.Tensor] = None, epsilon: float = 1e-8 +): """Computes scale-invariant source-to-distortion ratio. 1. adjust both estimate and reference to have 0-mean @@ -164,11 +157,11 @@ def sdr_pit( def sdri( - estimate: torch.Tensor, - reference: torch.Tensor, - mix: torch.Tensor, - mask: Optional[torch.Tensor] = None, - epsilon: float = 1e-8, + estimate: torch.Tensor, + reference: torch.Tensor, + mix: torch.Tensor, + mask: Optional[torch.Tensor] = None, + epsilon: float = 1e-8, ) -> torch.Tensor: """Compute the improvement of SDR (SDRi). diff --git a/examples/test/test_interactive_asr.py b/examples/test/test_interactive_asr.py index 40867a0f96..0cb8110d41 100644 --- a/examples/test/test_interactive_asr.py +++ b/examples/test/test_interactive_asr.py @@ -93,9 +93,7 @@ class ASRTest(unittest.TestCase): def test_transcribe_file(self): task, generator, models, sp, tgt_dict = setup_asr(self.args, self.logger) - _, transcription = transcribe_file( - self.args, task, generator, models, sp, tgt_dict - ) + _, transcription = transcribe_file(self.args, task, generator, models, sp, tgt_dict) expected_transcription = [["THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"]] self.assertEqual(transcription, expected_transcription, msg=str(transcription)) diff --git a/examples/tutorials/audio_data_augmentation_tutorial.py b/examples/tutorials/audio_data_augmentation_tutorial.py index 4e983be4df..3129d04392 100644 --- a/examples/tutorials/audio_data_augmentation_tutorial.py +++ b/examples/tutorials/audio_data_augmentation_tutorial.py @@ -32,9 +32,9 @@ import math import os -import requests import matplotlib.pyplot as plt +import requests from IPython.display import Audio, display @@ -164,7 +164,7 @@ def get_rir_sample(*, resample=None, processed=False): rir_raw, sample_rate = _get_sample(SAMPLE_RIR_PATH, resample=resample) if not processed: return rir_raw, sample_rate - rir = rir_raw[:, int(sample_rate * 1.01): int(sample_rate * 1.3)] + rir = rir_raw[:, int(sample_rate * 1.01) : int(sample_rate * 1.3)] rir = rir / torch.norm(rir, p=2) rir = torch.flip(rir, [1]) return rir, sample_rate @@ -225,9 +225,7 @@ def get_noise_sample(*, resample=None): ] # Apply effects -waveform2, sample_rate2 = torchaudio.sox_effects.apply_effects_tensor( - waveform1, sample_rate1, effects -) +waveform2, sample_rate2 = torchaudio.sox_effects.apply_effects_tensor(waveform1, sample_rate1, effects) print_stats(waveform1, sample_rate=sample_rate1, src="Original") print_stats(waveform2, sample_rate=sample_rate2, src="Effects Applied") @@ -291,7 +289,7 @@ def get_noise_sample(*, resample=None): # the signal power, then flip along the time axis. # -rir = rir_raw[:, int(sample_rate * 1.01): int(sample_rate * 1.3)] +rir = rir_raw[:, int(sample_rate * 1.01) : int(sample_rate * 1.3)] rir = rir / torch.norm(rir, p=2) rir = torch.flip(rir, [1]) diff --git a/examples/tutorials/audio_feature_augmentation_tutorial.py b/examples/tutorials/audio_feature_augmentation_tutorial.py index 06664c6d95..a46f603be6 100644 --- a/examples/tutorials/audio_feature_augmentation_tutorial.py +++ b/examples/tutorials/audio_feature_augmentation_tutorial.py @@ -33,10 +33,10 @@ # ------------------------------------------------------------------------------- import os -import requests import librosa import matplotlib.pyplot as plt +import requests _SAMPLE_DIR = "_assets" @@ -125,17 +125,13 @@ def plot_spectrogram(spec, title=None, ylabel="freq_bin", aspect="auto", xmax=No rate = 1.2 spec_ = stretch(spec, rate) -plot_spectrogram( - torch.abs(spec_[0]), title=f"Stretched x{rate}", aspect="equal", xmax=304 -) +plot_spectrogram(torch.abs(spec_[0]), title=f"Stretched x{rate}", aspect="equal", xmax=304) plot_spectrogram(torch.abs(spec[0]), title="Original", aspect="equal", xmax=304) rate = 0.9 spec_ = stretch(spec, rate) -plot_spectrogram( - torch.abs(spec_[0]), title=f"Stretched x{rate}", aspect="equal", xmax=304 -) +plot_spectrogram(torch.abs(spec_[0]), title=f"Stretched x{rate}", aspect="equal", xmax=304) ###################################################################### # TimeMasking diff --git a/examples/tutorials/audio_feature_extractions_tutorial.py b/examples/tutorials/audio_feature_extractions_tutorial.py index 4adac10749..fb1ae74254 100644 --- a/examples/tutorials/audio_feature_extractions_tutorial.py +++ b/examples/tutorials/audio_feature_extractions_tutorial.py @@ -51,10 +51,10 @@ # ------------------------------------------------------------------------------- import os -import requests import librosa import matplotlib.pyplot as plt +import requests from IPython.display import Audio, display @@ -199,9 +199,7 @@ def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc): axis2 = axis.twinx() time_axis = torch.linspace(0, end_time, nfcc.shape[1]) - ln2 = axis2.plot( - time_axis, nfcc[0], linewidth=2, label="NFCC", color="blue", linestyle="--" - ) + ln2 = axis2.plot(time_axis, nfcc[0], linewidth=2, label="NFCC", color="blue", linestyle="--") lns = ln1 + ln2 labels = [l.get_label() for l in lns] diff --git a/examples/tutorials/audio_io_tutorial.py b/examples/tutorials/audio_io_tutorial.py index 94f013e300..af4acc0cca 100644 --- a/examples/tutorials/audio_io_tutorial.py +++ b/examples/tutorials/audio_io_tutorial.py @@ -32,13 +32,13 @@ import io import os -import requests import tarfile import boto3 +import matplotlib.pyplot as plt +import requests from botocore import UNSIGNED from botocore.config import Config -import matplotlib.pyplot as plt from IPython.display import Audio, display @@ -348,14 +348,12 @@ def inspect_file(path): print("Fetching all the data...") with requests.get(SAMPLE_WAV_SPEECH_URL, stream=True) as response: waveform1, sample_rate1 = torchaudio.load(response.raw) - waveform1 = waveform1[:, frame_offset: frame_offset + num_frames] + waveform1 = waveform1[:, frame_offset : frame_offset + num_frames] print(f" - Fetched {response.raw.tell()} bytes") print("Fetching until the requested frames are available...") with requests.get(SAMPLE_WAV_SPEECH_URL, stream=True) as response: - waveform2, sample_rate2 = torchaudio.load( - response.raw, frame_offset=frame_offset, num_frames=num_frames - ) + waveform2, sample_rate2 = torchaudio.load(response.raw, frame_offset=frame_offset, num_frames=num_frames) print(f" - Fetched {response.raw.tell()} bytes") print("Checking the resulting waveform ... ", end="") diff --git a/examples/tutorials/audio_resampling_tutorial.py b/examples/tutorials/audio_resampling_tutorial.py index 833549aba3..32b54201a8 100644 --- a/examples/tutorials/audio_resampling_tutorial.py +++ b/examples/tutorials/audio_resampling_tutorial.py @@ -38,8 +38,8 @@ import librosa import matplotlib.pyplot as plt -from IPython.display import Audio, display import pandas as pd +from IPython.display import Audio, display DEFAULT_OFFSET = 201 @@ -56,9 +56,7 @@ def _get_log_freq(sample_rate, max_sweep_rate, offset): """ start, stop = math.log(offset), math.log(offset + max_sweep_rate // 2) - return ( - torch.exp(torch.linspace(start, stop, sample_rate, dtype=torch.double)) - offset - ) + return torch.exp(torch.linspace(start, stop, sample_rate, dtype=torch.double)) - offset def _get_inverse_log_freq(freq, sample_rate, offset): @@ -192,9 +190,7 @@ def benchmark_resample( waveform_np = waveform.squeeze().numpy() begin = time.time() for _ in range(iters): - librosa.resample( - waveform_np, sample_rate, resample_rate, res_type=librosa_type - ) + librosa.resample(waveform_np, sample_rate, resample_rate, res_type=librosa_type) elapsed = time.time() - begin return elapsed / iters @@ -264,14 +260,10 @@ def benchmark_resample( sample_rate = 48000 resample_rate = 32000 -resampled_waveform = F.resample( - waveform, sample_rate, resample_rate, lowpass_filter_width=6 -) +resampled_waveform = F.resample(waveform, sample_rate, resample_rate, lowpass_filter_width=6) plot_sweep(resampled_waveform, resample_rate, title="lowpass_filter_width=6") -resampled_waveform = F.resample( - waveform, sample_rate, resample_rate, lowpass_filter_width=128 -) +resampled_waveform = F.resample(waveform, sample_rate, resample_rate, lowpass_filter_width=128) plot_sweep(resampled_waveform, resample_rate, title="lowpass_filter_width=128") @@ -315,14 +307,10 @@ def benchmark_resample( sample_rate = 48000 resample_rate = 32000 -resampled_waveform = F.resample( - waveform, sample_rate, resample_rate, resampling_method="sinc_interpolation" -) +resampled_waveform = F.resample(waveform, sample_rate, resample_rate, resampling_method="sinc_interpolation") plot_sweep(resampled_waveform, resample_rate, title="Hann Window Default") -resampled_waveform = F.resample( - waveform, sample_rate, resample_rate, resampling_method="kaiser_window" -) +resampled_waveform = F.resample(waveform, sample_rate, resample_rate, resampling_method="kaiser_window") plot_sweep(resampled_waveform, resample_rate, title="Kaiser Window Default") @@ -351,13 +339,9 @@ def benchmark_resample( plot_sweep(resampled_waveform, resample_rate, title="Kaiser Window Best (torchaudio)") librosa_resampled_waveform = torch.from_numpy( - librosa.resample( - waveform.squeeze().numpy(), sample_rate, resample_rate, res_type="kaiser_best" - ) + librosa.resample(waveform.squeeze().numpy(), sample_rate, resample_rate, res_type="kaiser_best") ).unsqueeze(0) -plot_sweep( - librosa_resampled_waveform, resample_rate, title="Kaiser Window Best (librosa)" -) +plot_sweep(librosa_resampled_waveform, resample_rate, title="Kaiser Window Best (librosa)") mse = torch.square(resampled_waveform - librosa_resampled_waveform).mean().item() print("torchaudio and librosa kaiser best MSE:", mse) @@ -372,18 +356,12 @@ def benchmark_resample( resampling_method="kaiser_window", beta=8.555504641634386, ) -plot_specgram( - resampled_waveform, resample_rate, title="Kaiser Window Fast (torchaudio)" -) +plot_specgram(resampled_waveform, resample_rate, title="Kaiser Window Fast (torchaudio)") librosa_resampled_waveform = torch.from_numpy( - librosa.resample( - waveform.squeeze().numpy(), sample_rate, resample_rate, res_type="kaiser_fast" - ) + librosa.resample(waveform.squeeze().numpy(), sample_rate, resample_rate, res_type="kaiser_fast") ).unsqueeze(0) -plot_sweep( - librosa_resampled_waveform, resample_rate, title="Kaiser Window Fast (librosa)" -) +plot_sweep(librosa_resampled_waveform, resample_rate, title="Kaiser Window Fast (librosa)") mse = torch.square(resampled_waveform - librosa_resampled_waveform).mean().item() print("torchaudio and librosa kaiser fast MSE:", mse) @@ -426,29 +404,19 @@ def benchmark_resample( waveform = get_sine_sweep(sample_rate) # sinc 64 zero-crossings - f_time = benchmark_resample( - "functional", waveform, sample_rate, resample_rate, lowpass_filter_width=64 - ) - t_time = benchmark_resample( - "transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=64 - ) + f_time = benchmark_resample("functional", waveform, sample_rate, resample_rate, lowpass_filter_width=64) + t_time = benchmark_resample("transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=64) times.append([None, 1000 * f_time, 1000 * t_time]) rows.append("sinc (width 64)") # sinc 6 zero-crossings - f_time = benchmark_resample( - "functional", waveform, sample_rate, resample_rate, lowpass_filter_width=16 - ) - t_time = benchmark_resample( - "transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=16 - ) + f_time = benchmark_resample("functional", waveform, sample_rate, resample_rate, lowpass_filter_width=16) + t_time = benchmark_resample("transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=16) times.append([None, 1000 * f_time, 1000 * t_time]) rows.append("sinc (width 16)") # kaiser best - lib_time = benchmark_resample( - "librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_best" - ) + lib_time = benchmark_resample("librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_best") f_time = benchmark_resample( "functional", waveform, @@ -473,9 +441,7 @@ def benchmark_resample( rows.append("kaiser_best") # kaiser fast - lib_time = benchmark_resample( - "librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_fast" - ) + lib_time = benchmark_resample("librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_fast") f_time = benchmark_resample( "functional", waveform, @@ -499,8 +465,6 @@ def benchmark_resample( times.append([1000 * lib_time, 1000 * f_time, 1000 * t_time]) rows.append("kaiser_fast") - df = pd.DataFrame( - times, columns=["librosa", "functional", "transforms"], index=rows - ) + df = pd.DataFrame(times, columns=["librosa", "functional", "transforms"], index=rows) df.columns = pd.MultiIndex.from_product([[f"{label} time (ms)"], df.columns]) display(df.round(2)) diff --git a/examples/tutorials/forced_alignment_tutorial.py b/examples/tutorials/forced_alignment_tutorial.py index f11823e8d3..c39ea7d2a5 100644 --- a/examples/tutorials/forced_alignment_tutorial.py +++ b/examples/tutorials/forced_alignment_tutorial.py @@ -40,12 +40,12 @@ import os from dataclasses import dataclass -import torch -import torchaudio -import requests +import IPython import matplotlib import matplotlib.pyplot as plt -import IPython +import requests +import torch +import torchaudio matplotlib.rcParams["figure.figsize"] = [16.0, 4.8] @@ -325,7 +325,7 @@ def plot_trellis_with_segments(trellis, segments, transcript): trellis_with_path = trellis.clone() for i, seg in enumerate(segments): if seg.label != "|": - trellis_with_path[seg.start + 1: seg.end + 1, i + 1] = float("nan") + trellis_with_path[seg.start + 1 : seg.end + 1, i + 1] = float("nan") fig, [ax1, ax2] = plt.subplots(2, 1, figsize=(16, 9.5)) ax1.set_title("Path, label and probability for each label") @@ -383,12 +383,8 @@ def merge_words(segments, separator="|"): if i1 != i2: segs = segments[i1:i2] word = "".join([seg.label for seg in segs]) - score = sum(seg.score * seg.length for seg in segs) / sum( - seg.length for seg in segs - ) - words.append( - Segment(word, segments[i1].start, segments[i2 - 1].end, score) - ) + score = sum(seg.score * seg.length for seg in segs) / sum(seg.length for seg in segs) + words.append(Segment(word, segments[i1].start, segments[i2 - 1].end, score)) i1 = i2 + 1 i2 = i1 else: @@ -408,7 +404,7 @@ def plot_alignments(trellis, segments, word_segments, waveform): trellis_with_path = trellis.clone() for i, seg in enumerate(segments): if seg.label != "|": - trellis_with_path[seg.start + 1: seg.end + 1, i + 1] = float("nan") + trellis_with_path[seg.start + 1 : seg.end + 1, i + 1] = float("nan") fig, [ax1, ax2] = plt.subplots(2, 1, figsize=(16, 9.5)) @@ -464,9 +460,7 @@ def display_segment(i): x1 = int(ratio * word.end) filename = f"_assets/{i}_{word.label}.wav" torchaudio.save(filename, waveform[:, x0:x1], bundle.sample_rate) - print( - f"{word.label} ({word.score:.2f}): {x0 / bundle.sample_rate:.3f} - {x1 / bundle.sample_rate:.3f} sec" - ) + print(f"{word.label} ({word.score:.2f}): {x0 / bundle.sample_rate:.3f} - {x1 / bundle.sample_rate:.3f} sec") return IPython.display.Audio(filename) diff --git a/examples/tutorials/mvdr_tutorial.py b/examples/tutorials/mvdr_tutorial.py index d5d06c1959..b05d5ec9ff 100644 --- a/examples/tutorials/mvdr_tutorial.py +++ b/examples/tutorials/mvdr_tutorial.py @@ -44,10 +44,11 @@ # import os + +import IPython.display as ipd import requests import torch import torchaudio -import IPython.display as ipd torch.random.manual_seed(0) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") @@ -167,9 +168,7 @@ def get_irms(spec_clean, spec_noise): results_single = {} for solution in ["ref_channel", "stv_evd", "stv_power"]: - mvdr = torchaudio.transforms.MVDR( - ref_channel=0, solution=solution, multi_mask=False - ) + mvdr = torchaudio.transforms.MVDR(ref_channel=0, solution=solution, multi_mask=False) stft_est = mvdr(spec_mix, irm_speech[0], irm_noise[0]) est = istft(stft_est, length=mix.shape[-1]) results_single[solution] = est @@ -211,9 +210,7 @@ def si_sdr(estimate, reference, epsilon=1e-8): # for solution in results_single: - print( - solution + ": ", si_sdr(results_single[solution][None, ...], reverb_clean[0:1]) - ) + print(solution + ": ", si_sdr(results_single[solution][None, ...], reverb_clean[0:1])) ###################################################################### # Multi-channel mask results @@ -221,9 +218,7 @@ def si_sdr(estimate, reference, epsilon=1e-8): # for solution in results_multi: - print( - solution + ": ", si_sdr(results_multi[solution][None, ...], reverb_clean[0:1]) - ) + print(solution + ": ", si_sdr(results_multi[solution][None, ...], reverb_clean[0:1])) ###################################################################### # Original audio diff --git a/examples/tutorials/speech_recognition_pipeline_tutorial.py b/examples/tutorials/speech_recognition_pipeline_tutorial.py index 09875fe4d2..33518ae3c5 100644 --- a/examples/tutorials/speech_recognition_pipeline_tutorial.py +++ b/examples/tutorials/speech_recognition_pipeline_tutorial.py @@ -41,12 +41,12 @@ import os -import torch -import torchaudio -import requests +import IPython import matplotlib import matplotlib.pyplot as plt -import IPython +import requests +import torch +import torchaudio matplotlib.rcParams["figure.figsize"] = [16.0, 4.8] diff --git a/examples/tutorials/tacotron2_pipeline_tutorial.py b/examples/tutorials/tacotron2_pipeline_tutorial.py index 4fddd8f173..d684bc77e7 100644 --- a/examples/tutorials/tacotron2_pipeline_tutorial.py +++ b/examples/tutorials/tacotron2_pipeline_tutorial.py @@ -7,6 +7,10 @@ """ +import IPython +import matplotlib +import matplotlib.pyplot as plt + ###################################################################### # Overview # -------- @@ -58,10 +62,6 @@ import torch import torchaudio -import matplotlib -import matplotlib.pyplot as plt - -import IPython matplotlib.rcParams["figure.figsize"] = [16.0, 4.8] @@ -271,9 +271,7 @@ def text_to_sequence(text): ax1.imshow(spec[0].cpu().detach()) ax2.plot(waveforms[0].cpu().detach()) -torchaudio.save( - "_assets/output_wavernn.wav", waveforms[0:1].cpu(), sample_rate=vocoder.sample_rate -) +torchaudio.save("_assets/output_wavernn.wav", waveforms[0:1].cpu(), sample_rate=vocoder.sample_rate) IPython.display.Audio("_assets/output_wavernn.wav") @@ -332,9 +330,7 @@ def text_to_sequence(text): progress=False, map_location=device, ) -state_dict = { - key.replace("module.", ""): value for key, value in checkpoint["state_dict"].items() -} +state_dict = {key.replace("module.", ""): value for key, value in checkpoint["state_dict"].items()} waveglow.load_state_dict(state_dict) waveglow = waveglow.remove_weightnorm(waveglow) diff --git a/setup.py b/setup.py index 324a618a98..439e3c12eb 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ #!/usr/bin/env python +import distutils.command.clean import os import re -import sys import shutil import subprocess +import sys from pathlib import Path -from setuptools import setup, find_packages -import distutils.command.clean import torch +from setuptools import setup, find_packages from tools import setup_helpers ROOT_DIR = Path(__file__).parent.resolve() @@ -16,32 +16,32 @@ def _run_cmd(cmd): try: - return subprocess.check_output(cmd, cwd=ROOT_DIR).decode('ascii').strip() + return subprocess.check_output(cmd, cwd=ROOT_DIR).decode("ascii").strip() except Exception: return None def _get_version(sha): - version = '0.11.0a0' - if os.getenv('BUILD_VERSION'): - version = os.getenv('BUILD_VERSION') + version = "0.11.0a0" + if os.getenv("BUILD_VERSION"): + version = os.getenv("BUILD_VERSION") elif sha is not None: - version += '+' + sha[:7] + version += "+" + sha[:7] return version def _make_version_file(version, sha): - sha = 'Unknown' if sha is None else sha - version_path = ROOT_DIR / 'torchaudio' / 'version.py' - with open(version_path, 'w') as f: + sha = "Unknown" if sha is None else sha + version_path = ROOT_DIR / "torchaudio" / "version.py" + with open(version_path, "w") as f: f.write(f"__version__ = '{version}'\n") f.write(f"git_version = '{sha}'\n") def _get_pytorch_version(): - if 'PYTORCH_VERSION' in os.environ: + if "PYTORCH_VERSION" in os.environ: return f"torch=={os.environ['PYTORCH_VERSION']}" - return 'torch' + return "torch" class clean(distutils.command.clean.clean): @@ -50,16 +50,16 @@ def run(self): distutils.command.clean.clean.run(self) # Remove torchaudio extension - for path in (ROOT_DIR / 'torchaudio').glob('**/*.so'): - print(f'removing \'{path}\'') + for path in (ROOT_DIR / "torchaudio").glob("**/*.so"): + print(f"removing '{path}'") path.unlink() # Remove build directory build_dirs = [ - ROOT_DIR / 'build', + ROOT_DIR / "build", ] for path in build_dirs: if path.exists(): - print(f'removing \'{path}\' (and everything under it)') + print(f"removing '{path}' (and everything under it)") shutil.rmtree(str(path), ignore_errors=True) @@ -72,44 +72,44 @@ def _get_packages(branch_name, tag): "tools*", ] exclude_prototype = False - if branch_name is not None and branch_name.startswith('release/'): + if branch_name is not None and branch_name.startswith("release/"): exclude_prototype = True - if tag is not None and re.match(r'v[\d.]+(-rc\d+)?', tag): + if tag is not None and re.match(r"v[\d.]+(-rc\d+)?", tag): exclude_prototype = True if exclude_prototype: - print('Excluding torchaudio.prototype from the package.') + print("Excluding torchaudio.prototype from the package.") exclude.append("torchaudio.prototype") return find_packages(exclude=exclude) def _init_submodule(): - print(' --- Initializing submodules') + print(" --- Initializing submodules") try: - subprocess.check_call(['git', 'submodule', 'init']) - subprocess.check_call(['git', 'submodule', 'update']) + subprocess.check_call(["git", "submodule", "init"]) + subprocess.check_call(["git", "submodule", "update"]) except Exception: - print(' --- Submodule initalization failed') - print('Please run:\n\tgit submodule update --init --recursive') + print(" --- Submodule initalization failed") + print("Please run:\n\tgit submodule update --init --recursive") sys.exit(1) - print(' --- Initialized submodule') + print(" --- Initialized submodule") def _parse_url(path): - with open(path, 'r') as file_: + with open(path, "r") as file_: for line in file_: - match = re.match(r'^\s*URL\s+(https:\/\/.+)$', line) + match = re.match(r"^\s*URL\s+(https:\/\/.+)$", line) if match: url = match.group(1) yield url def _parse_sources(): - third_party_dir = ROOT_DIR / 'third_party' - libs = ['zlib', 'bzip2', 'lzma', 'boost', 'sox'] - archive_dir = third_party_dir / 'archives' + third_party_dir = ROOT_DIR / "third_party" + libs = ["zlib", "bzip2", "lzma", "boost", "sox"] + archive_dir = third_party_dir / "archives" archive_dir.mkdir(exist_ok=True) for lib in libs: - cmake_file = third_party_dir / lib / 'CMakeLists.txt' + cmake_file = third_party_dir / lib / "CMakeLists.txt" for url in _parse_url(cmake_file): path = archive_dir / os.path.basename(url) yield path, url @@ -118,28 +118,28 @@ def _parse_sources(): def _fetch_archives(src): for dest, url in src: if not dest.exists(): - print(f' --- Fetching {os.path.basename(dest)}') + print(f" --- Fetching {os.path.basename(dest)}") torch.hub.download_url_to_file(url, dest, progress=False) def _fetch_third_party_libraries(): - if not (ROOT_DIR / 'third_party' / 'kaldi' / 'submodule' / 'CMakeLists.txt').exists(): + if not (ROOT_DIR / "third_party" / "kaldi" / "submodule" / "CMakeLists.txt").exists(): _init_submodule() - if os.name != 'nt': + if os.name != "nt": _fetch_archives(_parse_sources()) def _main(): - sha = _run_cmd(['git', 'rev-parse', 'HEAD']) - branch = _run_cmd(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) - tag = _run_cmd(['git', 'describe', '--tags', '--exact-match', '@']) - print('-- Git branch:', branch) - print('-- Git SHA:', sha) - print('-- Git tag:', tag) + sha = _run_cmd(["git", "rev-parse", "HEAD"]) + branch = _run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]) + tag = _run_cmd(["git", "describe", "--tags", "--exact-match", "@"]) + print("-- Git branch:", branch) + print("-- Git SHA:", sha) + print("-- Git tag:", tag) pytorch_package_dep = _get_pytorch_version() - print('-- PyTorch dependency:', pytorch_package_dep) + print("-- PyTorch dependency:", pytorch_package_dep) version = _get_version(sha) - print('-- Building version', version) + print("-- Building version", version) _make_version_file(version, sha) _fetch_third_party_libraries() @@ -166,18 +166,18 @@ def _main(): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Multimedia :: Sound/Audio", - "Topic :: Scientific/Engineering :: Artificial Intelligence" + "Topic :: Scientific/Engineering :: Artificial Intelligence", ], packages=_get_packages(branch, tag), ext_modules=setup_helpers.get_ext_modules(), cmdclass={ - 'build_ext': setup_helpers.CMakeBuild, - 'clean': clean, + "build_ext": setup_helpers.CMakeBuild, + "clean": clean, }, install_requires=[pytorch_package_dep], zip_safe=False, ) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/test/integration_tests/conftest.py b/test/integration_tests/conftest.py index 465ebe0d1a..219c10bd07 100644 --- a/test/integration_tests/conftest.py +++ b/test/integration_tests/conftest.py @@ -1,6 +1,6 @@ +import pytest import torch from torchaudio._internal import download_url_to_file -import pytest class GreedyCTCDecoder(torch.nn.Module): @@ -24,7 +24,7 @@ def forward(self, logits: torch.Tensor) -> str: for i in best_path: if i != self.blank: hypothesis.append(self.labels[i]) - return ''.join(hypothesis) + return "".join(hypothesis) @pytest.fixture @@ -33,24 +33,24 @@ def ctc_decoder(): _FILES = { - 'en': 'Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.flac', - 'de': '20090505-0900-PLENARY-16-de_20090505-21_56_00_8.flac', - 'en2': '20120613-0900-PLENARY-8-en_20120613-13_46_50_3.flac', - 'es': '20130207-0900-PLENARY-7-es_20130207-13_02_05_5.flac', - 'fr': '20121212-0900-PLENARY-5-fr_20121212-11_37_04_10.flac', - 'it': '20170516-0900-PLENARY-16-it_20170516-18_56_31_1.flac', + "en": "Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.flac", + "de": "20090505-0900-PLENARY-16-de_20090505-21_56_00_8.flac", + "en2": "20120613-0900-PLENARY-8-en_20120613-13_46_50_3.flac", + "es": "20130207-0900-PLENARY-7-es_20130207-13_02_05_5.flac", + "fr": "20121212-0900-PLENARY-5-fr_20121212-11_37_04_10.flac", + "it": "20170516-0900-PLENARY-16-it_20170516-18_56_31_1.flac", } @pytest.fixture def sample_speech(tmp_path, lang): if lang not in _FILES: - raise NotImplementedError(f'Unexpected lang: {lang}') + raise NotImplementedError(f"Unexpected lang: {lang}") filename = _FILES[lang] path = tmp_path.parent / filename if not path.exists(): - url = f'https://download.pytorch.org/torchaudio/test-assets/{filename}' - print(f'downloading from {url}') + url = f"https://download.pytorch.org/torchaudio/test-assets/{filename}" + print(f"downloading from {url}") download_url_to_file(url, path, progress=False) return path @@ -62,13 +62,13 @@ def pytest_addoption(parser): help=( "When provided, tests will use temporary directory as Torch Hub directory. " "Downloaded models will be deleted after each test." - ) + ), ) @pytest.fixture(autouse=True) def temp_hub_dir(tmpdir, pytestconfig): - if not pytestconfig.getoption('use_tmp_hub_dir'): + if not pytestconfig.getoption("use_tmp_hub_dir"): yield else: org_dir = torch.hub.get_dir() diff --git a/test/integration_tests/tacotron2_pipeline_test.py b/test/integration_tests/tacotron2_pipeline_test.py index 3b81b1847d..2e50cd01e8 100644 --- a/test/integration_tests/tacotron2_pipeline_test.py +++ b/test/integration_tests/tacotron2_pipeline_test.py @@ -1,20 +1,20 @@ +import pytest from torchaudio.pipelines import ( TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH, TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH, TACOTRON2_WAVERNN_CHAR_LJSPEECH, TACOTRON2_WAVERNN_PHONE_LJSPEECH, ) -import pytest @pytest.mark.parametrize( - 'bundle', + "bundle", [ TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH, TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH, TACOTRON2_WAVERNN_CHAR_LJSPEECH, TACOTRON2_WAVERNN_PHONE_LJSPEECH, - ] + ], ) def test_tts_models(bundle): """Smoke test of TTS pipeline""" diff --git a/test/integration_tests/wav2vec2_pipeline_test.py b/test/integration_tests/wav2vec2_pipeline_test.py index 367e43c242..e5e7117a31 100644 --- a/test/integration_tests/wav2vec2_pipeline_test.py +++ b/test/integration_tests/wav2vec2_pipeline_test.py @@ -1,3 +1,4 @@ +import pytest import torchaudio from torchaudio.pipelines import ( WAV2VEC2_BASE, @@ -24,7 +25,6 @@ VOXPOPULI_ASR_BASE_10K_FR, VOXPOPULI_ASR_BASE_10K_IT, ) -import pytest @pytest.mark.parametrize( @@ -37,7 +37,7 @@ HUBERT_BASE, HUBERT_LARGE, HUBERT_XLARGE, - ] + ], ) def test_pretraining_models(bundle): """Smoke test of downloading weights for pretraining models""" @@ -47,30 +47,46 @@ def test_pretraining_models(bundle): @pytest.mark.parametrize( "bundle,lang,expected", [ - (WAV2VEC2_ASR_BASE_10M, 'en', 'I|HAD|THAT|CURIYOSSITY|BESID|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_BASE_100H, 'en', 'I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_BASE_960H, 'en', 'I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_10M, 'en', 'I|HAD|THAT|CURIOUSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_100H, 'en', 'I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_960H, 'en', 'I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_LV60K_10M, 'en', 'I|HAD|THAT|CURIOUSSITY|BESID|ME|AT|THISS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_LV60K_100H, 'en', 'I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (WAV2VEC2_ASR_LARGE_LV60K_960H, 'en', 'I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (HUBERT_ASR_LARGE, 'en', 'I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (HUBERT_ASR_XLARGE, 'en', 'I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|'), - (VOXPOPULI_ASR_BASE_10K_EN, 'en2', 'i|hope|that|we|will|see|a|ddrasstic|decrease|of|funding|for|the|failed|eu|project|and|that|more|money|will|come|back|to|the|taxpayers'), # noqa: E501 - (VOXPOPULI_ASR_BASE_10K_ES, 'es', "la|primera|que|es|imprescindible|pensar|a|pequeña|a|escala|para|implicar|y|complementar|así|la|actuación|global"), # noqa: E501 - (VOXPOPULI_ASR_BASE_10K_DE, 'de', "dabei|spielt|auch|eine|sorgfältige|berichterstattung|eine|wichtige|rolle"), - (VOXPOPULI_ASR_BASE_10K_FR, 'fr', 'la|commission|va|faire|des|propositions|sur|ce|sujet|comment|mettre|en|place|cette|capacité|fiscale|et|le|conseil|européen|y|reviendra|sour|les|sujets|au|moins|de|mars'), # noqa: E501 - (VOXPOPULI_ASR_BASE_10K_IT, 'it', 'credo|che|illatino|non|sia|contemplato|tra|le|traduzioni|e|quindi|mi|attengo|allitaliano') # noqa: E501 - ] + (WAV2VEC2_ASR_BASE_10M, "en", "I|HAD|THAT|CURIYOSSITY|BESID|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_BASE_100H, "en", "I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_BASE_960H, "en", "I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_10M, "en", "I|HAD|THAT|CURIOUSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_100H, "en", "I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_960H, "en", "I|HAD|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_LV60K_10M, "en", "I|HAD|THAT|CURIOUSSITY|BESID|ME|AT|THISS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_LV60K_100H, "en", "I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (WAV2VEC2_ASR_LARGE_LV60K_960H, "en", "I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (HUBERT_ASR_LARGE, "en", "I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + (HUBERT_ASR_XLARGE, "en", "I|HAVE|THAT|CURIOSITY|BESIDE|ME|AT|THIS|MOMENT|"), + ( + VOXPOPULI_ASR_BASE_10K_EN, + "en2", + "i|hope|that|we|will|see|a|ddrasstic|decrease|of|funding|for|the|failed|eu|project|and|that|more|money|will|come|back|to|the|taxpayers", + ), # noqa: E501 + ( + VOXPOPULI_ASR_BASE_10K_ES, + "es", + "la|primera|que|es|imprescindible|pensar|a|pequeña|a|escala|para|implicar|y|complementar|así|la|actuación|global", + ), # noqa: E501 + (VOXPOPULI_ASR_BASE_10K_DE, "de", "dabei|spielt|auch|eine|sorgfältige|berichterstattung|eine|wichtige|rolle"), + ( + VOXPOPULI_ASR_BASE_10K_FR, + "fr", + "la|commission|va|faire|des|propositions|sur|ce|sujet|comment|mettre|en|place|cette|capacité|fiscale|et|le|conseil|européen|y|reviendra|sour|les|sujets|au|moins|de|mars", + ), # noqa: E501 + ( + VOXPOPULI_ASR_BASE_10K_IT, + "it", + "credo|che|illatino|non|sia|contemplato|tra|le|traduzioni|e|quindi|mi|attengo|allitaliano", + ), # noqa: E501 + ], ) def test_finetune_asr_model( - bundle, - lang, - expected, - sample_speech, - ctc_decoder, + bundle, + lang, + expected, + sample_speech, + ctc_decoder, ): """Smoke test of downloading weights for fine-tuning models and simple transcription""" model = bundle.get_model().eval() diff --git a/test/torchaudio_unittest/assets/io/generate_opus.py b/test/torchaudio_unittest/assets/io/generate_opus.py index e6b99c471c..d0e0c20cca 100644 --- a/test/torchaudio_unittest/assets/io/generate_opus.py +++ b/test/torchaudio_unittest/assets/io/generate_opus.py @@ -8,30 +8,26 @@ def _parse_args(): - parser = argparse.ArgumentParser( - description='Generate opus files for test' - ) - parser.add_argument('--num-channels', required=True, type=int) - parser.add_argument('--compression-level', required=True, type=int, choices=list(range(11))) - parser.add_argument('--bitrate', default='96k') + parser = argparse.ArgumentParser(description="Generate opus files for test") + parser.add_argument("--num-channels", required=True, type=int) + parser.add_argument("--compression-level", required=True, type=int, choices=list(range(11))) + parser.add_argument("--bitrate", default="96k") return parser.parse_args() -def convert_to_opus( - src_path, dst_path, - *, bitrate, compression_level): +def convert_to_opus(src_path, dst_path, *, bitrate, compression_level): """Convert audio file with `ffmpeg` command.""" - command = ['ffmpeg', '-y', '-i', src_path, '-c:a', 'libopus', '-b:a', bitrate] + command = ["ffmpeg", "-y", "-i", src_path, "-c:a", "libopus", "-b:a", bitrate] if compression_level is not None: - command += ['-compression_level', str(compression_level)] + command += ["-compression_level", str(compression_level)] command += [dst_path] - print(' '.join(command)) + print(" ".join(command)) subprocess.run(command, check=True) def _generate(num_channels, compression_level, bitrate): - org_path = 'original.wav' - ops_path = f'{bitrate}_{compression_level}_{num_channels}ch.opus' + org_path = "original.wav" + ops_path = f"{bitrate}_{compression_level}_{num_channels}ch.opus" # Note: ffmpeg forces sample rate 48k Hz for opus https://stackoverflow.com/a/39186779 # 1. generate original wav @@ -46,5 +42,5 @@ def _main(): _generate(args.num_channels, args.compression_level, args.bitrate) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_hubert_model_config.py b/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_hubert_model_config.py index 28bf3349bf..bb455ee54a 100644 --- a/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_hubert_model_config.py +++ b/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_hubert_model_config.py @@ -32,8 +32,8 @@ > hubert_large_ll60k_finetune_ls960.json ``` """ -import json import argparse +import json def _parse_args(): @@ -42,12 +42,9 @@ def _parse_args(): formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( - '--model-file', + "--model-file", required=True, - help=( - 'A pt file from ' - 'https://github.com/pytorch/fairseq/tree/main/examples/hubert' - ) + help=("A pt file from " "https://github.com/pytorch/fairseq/tree/main/examples/hubert"), ) return parser.parse_args() @@ -66,27 +63,27 @@ def _main(): args = _parse_args() model, cfg = _load(args.model_file) - if model.__class__.__name__ == 'HubertModel': - cfg['task']['data'] = '/foo/bar' - cfg['task']['label_dir'] = None + if model.__class__.__name__ == "HubertModel": + cfg["task"]["data"] = "/foo/bar" + cfg["task"]["label_dir"] = None conf = { - '_name': 'hubert', - 'model': cfg['model'], - 'task': cfg['task'], - 'num_classes': model.num_classes, + "_name": "hubert", + "model": cfg["model"], + "task": cfg["task"], + "num_classes": model.num_classes, } - elif model.__class__.__name__ == 'HubertCtc': - conf = cfg['model'] - del conf['w2v_path'] - keep = ['_name', 'task', 'model'] - for key in list(k for k in conf['w2v_args'] if k not in keep): - del conf['w2v_args'][key] - conf['data'] = '/foo/bar/' - conf['w2v_args']['task']['data'] = '/foo/bar' - conf['w2v_args']['task']['labels'] = [] - conf['w2v_args']['task']['label_dir'] = '/foo/bar' + elif model.__class__.__name__ == "HubertCtc": + conf = cfg["model"] + del conf["w2v_path"] + keep = ["_name", "task", "model"] + for key in list(k for k in conf["w2v_args"] if k not in keep): + del conf["w2v_args"][key] + conf["data"] = "/foo/bar/" + conf["w2v_args"]["task"]["data"] = "/foo/bar" + conf["w2v_args"]["task"]["labels"] = [] + conf["w2v_args"]["task"]["label_dir"] = "/foo/bar" print(json.dumps(conf, indent=4, sort_keys=True)) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_wav2vec2_model_config.py b/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_wav2vec2_model_config.py index 2d651a1b77..03d47ce576 100644 --- a/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_wav2vec2_model_config.py +++ b/test/torchaudio_unittest/assets/wav2vec2/fairseq/generate_wav2vec2_model_config.py @@ -41,9 +41,9 @@ > wav2vec_large_lv60_self_960h.json ``` """ -import os -import json import argparse +import json +import os def _parse_args(): @@ -52,19 +52,13 @@ def _parse_args(): formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( - '--model-file', + "--model-file", required=True, - help=( - 'A point file from ' - 'https://github.com/pytorch/fairseq/tree/main/examples/wav2vec' - ) + help=("A point file from " "https://github.com/pytorch/fairseq/tree/main/examples/wav2vec"), ) parser.add_argument( - '--dict-dir', - help=( - 'Directory where `dict.ltr.txt` file is found. ' - 'Default: the directory of the given model.' - ) + "--dict-dir", + help=("Directory where `dict.ltr.txt` file is found. " "Default: the directory of the given model."), ) args = parser.parse_args() if args.dict_dir is None: @@ -75,32 +69,29 @@ def _parse_args(): def _to_json(conf): import yaml from omegaconf import OmegaConf + return yaml.safe_load(OmegaConf.to_yaml(conf)) def _load(model_file, dict_dir): import fairseq - overrides = {'data': dict_dir} - _, args, _ = fairseq.checkpoint_utils.load_model_ensemble_and_task( - [model_file], arg_overrides=overrides - ) - return _to_json(args['model']) + overrides = {"data": dict_dir} + _, args, _ = fairseq.checkpoint_utils.load_model_ensemble_and_task([model_file], arg_overrides=overrides) + return _to_json(args["model"]) def _main(): args = _parse_args() conf = _load(args.model_file, args.dict_dir) - if conf['_name'] == 'wav2vec_ctc': - del conf['data'] - del conf['w2v_args']['task']['data'] - conf['w2v_args'] = { - key: conf['w2v_args'][key] for key in ['model', 'task'] - } + if conf["_name"] == "wav2vec_ctc": + del conf["data"] + del conf["w2v_args"]["task"]["data"] + conf["w2v_args"] = {key: conf["w2v_args"][key] for key in ["model", "task"]} print(json.dumps(conf, indent=4, sort_keys=True)) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/test/torchaudio_unittest/assets/wav2vec2/huggingface/generate_huggingface_model_config.py b/test/torchaudio_unittest/assets/wav2vec2/huggingface/generate_huggingface_model_config.py index d221793010..68f15a6e70 100644 --- a/test/torchaudio_unittest/assets/wav2vec2/huggingface/generate_huggingface_model_config.py +++ b/test/torchaudio_unittest/assets/wav2vec2/huggingface/generate_huggingface_model_config.py @@ -1,5 +1,5 @@ -import os import json +import os from transformers import Wav2Vec2Model @@ -22,16 +22,16 @@ def _main(): "facebook/wav2vec2-large-xlsr-53-german", ] for key in keys: - path = os.path.join(_THIS_DIR, f'{key}.json') - print('Generating ', path) + path = os.path.join(_THIS_DIR, f"{key}.json") + print("Generating ", path) cfg = Wav2Vec2Model.from_pretrained(key).config cfg = json.loads(cfg.to_json_string()) - del cfg['_name_or_path'] + del cfg["_name_or_path"] - with open(path, 'w') as file_: + with open(path, "w") as file_: file_.write(json.dumps(cfg, indent=4, sort_keys=True)) - file_.write('\n') + file_.write("\n") -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/test/torchaudio_unittest/backend/common.py b/test/torchaudio_unittest/backend/common.py index dc8e90bf77..b9dbddb67a 100644 --- a/test/torchaudio_unittest/backend/common.py +++ b/test/torchaudio_unittest/backend/common.py @@ -3,23 +3,23 @@ def get_encoding(ext, dtype): exts = { - 'mp3', - 'flac', - 'vorbis', + "mp3", + "flac", + "vorbis", } encodings = { - 'float32': 'PCM_F', - 'int32': 'PCM_S', - 'int16': 'PCM_S', - 'uint8': 'PCM_U', + "float32": "PCM_F", + "int32": "PCM_S", + "int16": "PCM_S", + "uint8": "PCM_U", } return ext.upper() if ext in exts else encodings[dtype] def get_bits_per_sample(ext, dtype): bits_per_samples = { - 'flac': 24, - 'mp3': 0, - 'vorbis': 0, + "flac": 24, + "mp3": 0, + "vorbis": 0, } return bits_per_samples.get(ext, sox_utils.get_bit_depth(dtype)) diff --git a/test/torchaudio_unittest/backend/soundfile/common.py b/test/torchaudio_unittest/backend/soundfile/common.py index c6b014dd4c..90905e98ab 100644 --- a/test/torchaudio_unittest/backend/soundfile/common.py +++ b/test/torchaudio_unittest/backend/soundfile/common.py @@ -38,20 +38,19 @@ def fetch_wav_subtype(dtype, encoding, bits_per_sample): subtype = { (None, None): dtype2subtype(dtype), (None, 8): "PCM_U8", - ('PCM_U', None): "PCM_U8", - ('PCM_U', 8): "PCM_U8", - ('PCM_S', None): "PCM_32", - ('PCM_S', 16): "PCM_16", - ('PCM_S', 32): "PCM_32", - ('PCM_F', None): "FLOAT", - ('PCM_F', 32): "FLOAT", - ('PCM_F', 64): "DOUBLE", - ('ULAW', None): "ULAW", - ('ULAW', 8): "ULAW", - ('ALAW', None): "ALAW", - ('ALAW', 8): "ALAW", + ("PCM_U", None): "PCM_U8", + ("PCM_U", 8): "PCM_U8", + ("PCM_S", None): "PCM_32", + ("PCM_S", 16): "PCM_16", + ("PCM_S", 32): "PCM_32", + ("PCM_F", None): "FLOAT", + ("PCM_F", 32): "FLOAT", + ("PCM_F", 64): "DOUBLE", + ("ULAW", None): "ULAW", + ("ULAW", 8): "ULAW", + ("ALAW", None): "ALAW", + ("ALAW", 8): "ALAW", }.get((encoding, bits_per_sample)) if subtype: return subtype - raise ValueError( - f"wav does not support ({encoding}, {bits_per_sample}).") + raise ValueError(f"wav does not support ({encoding}, {bits_per_sample}).") diff --git a/test/torchaudio_unittest/backend/soundfile/info_test.py b/test/torchaudio_unittest/backend/soundfile/info_test.py index b6b8722410..49ac261a65 100644 --- a/test/torchaudio_unittest/backend/soundfile/info_test.py +++ b/test/torchaudio_unittest/backend/soundfile/info_test.py @@ -1,11 +1,14 @@ -from unittest.mock import patch -import warnings import tarfile +import warnings +from unittest.mock import patch import torch -from torchaudio.backend import soundfile_backend from torchaudio._internal import module_utils as _mod_utils - +from torchaudio.backend import soundfile_backend +from torchaudio_unittest.backend.common import ( + get_bits_per_sample, + get_encoding, +) from torchaudio_unittest.common_utils import ( TempDirMixin, PytorchTestCase, @@ -14,10 +17,7 @@ save_wav, nested_params, ) -from torchaudio_unittest.backend.common import ( - get_bits_per_sample, - get_encoding, -) + from .common import skipIfFormatNotSupported, parameterize if _mod_utils.is_module_available("soundfile"): @@ -27,15 +27,15 @@ @skipIfNoModule("soundfile") class TestInfo(TempDirMixin, PytorchTestCase): @parameterize( - ["float32", "int32", "int16", "uint8"], [8000, 16000], [1, 2], + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], ) def test_wav(self, dtype, sample_rate, num_channels): """`soundfile_backend.info` can check wav file correctly""" duration = 1 path = self.get_temp_path("data.wav") - data = get_wav_data( - dtype, num_channels, normalize=False, num_frames=duration * sample_rate - ) + data = get_wav_data(dtype, num_channels, normalize=False, num_frames=duration * sample_rate) save_wav(path, data, sample_rate) info = soundfile_backend.info(path) assert info.sample_rate == sample_rate @@ -81,10 +81,7 @@ def test_ogg(self, sample_rate, num_channels): @nested_params( [8000, 16000], [1, 2], - [ - ('PCM_24', 24), - ('PCM_32', 32) - ], + [("PCM_24", 24), ("PCM_32", 32)], ) @skipIfFormatNotSupported("NIST") def test_sphere(self, sample_rate, num_channels, subtype_and_bit_depth): @@ -109,13 +106,15 @@ def test_unknown_subtype_warning(self): This will happen if a new subtype is supported in SoundFile: the _SUBTYPE_TO_BITS_PER_SAMPLE dict should be updated. """ + def _mock_info_func(_): class MockSoundFileInfo: samplerate = 8000 frames = 356 channels = 2 - subtype = 'UNSEEN_SUBTYPE' - format = 'UNKNOWN' + subtype = "UNSEEN_SUBTYPE" + format = "UNKNOWN" + return MockSoundFileInfo() with patch("soundfile.info", _mock_info_func): @@ -134,27 +133,27 @@ def _test_fileobj(self, ext, subtype, bits_per_sample): sample_rate = 16000 num_channels = 2 num_frames = sample_rate * duration - path = self.get_temp_path(f'test.{ext}') + path = self.get_temp_path(f"test.{ext}") data = torch.randn(num_frames, num_channels).numpy() soundfile.write(path, data, sample_rate, subtype=subtype) - with open(path, 'rb') as fileobj: + with open(path, "rb") as fileobj: info = soundfile_backend.info(fileobj) assert info.sample_rate == sample_rate assert info.num_frames == num_frames assert info.num_channels == num_channels assert info.bits_per_sample == bits_per_sample - assert info.encoding == "FLAC" if ext == 'flac' else "PCM_S" + assert info.encoding == "FLAC" if ext == "flac" else "PCM_S" def test_fileobj_wav(self): """Loading audio via file-like object works""" - self._test_fileobj('wav', 'PCM_16', 16) + self._test_fileobj("wav", "PCM_16", 16) @skipIfFormatNotSupported("FLAC") def test_fileobj_flac(self): """Loading audio via file-like object works""" - self._test_fileobj('flac', 'PCM_16', 16) + self._test_fileobj("flac", "PCM_16", 16) def _test_tarobj(self, ext, subtype, bits_per_sample): """Query compressed audio via file-like object works""" @@ -162,29 +161,29 @@ def _test_tarobj(self, ext, subtype, bits_per_sample): sample_rate = 16000 num_channels = 2 num_frames = sample_rate * duration - audio_file = f'test.{ext}' + audio_file = f"test.{ext}" audio_path = self.get_temp_path(audio_file) - archive_path = self.get_temp_path('archive.tar.gz') + archive_path = self.get_temp_path("archive.tar.gz") data = torch.randn(num_frames, num_channels).numpy() soundfile.write(audio_path, data, sample_rate, subtype=subtype) - with tarfile.TarFile(archive_path, 'w') as tarobj: + with tarfile.TarFile(archive_path, "w") as tarobj: tarobj.add(audio_path, arcname=audio_file) - with tarfile.TarFile(archive_path, 'r') as tarobj: + with tarfile.TarFile(archive_path, "r") as tarobj: fileobj = tarobj.extractfile(audio_file) info = soundfile_backend.info(fileobj) assert info.sample_rate == sample_rate assert info.num_frames == num_frames assert info.num_channels == num_channels assert info.bits_per_sample == bits_per_sample - assert info.encoding == "FLAC" if ext == 'flac' else "PCM_S" + assert info.encoding == "FLAC" if ext == "flac" else "PCM_S" def test_tarobj_wav(self): """Query compressed audio via file-like object works""" - self._test_tarobj('wav', 'PCM_16', 16) + self._test_tarobj("wav", "PCM_16", 16) @skipIfFormatNotSupported("FLAC") def test_tarobj_flac(self): """Query compressed audio via file-like object works""" - self._test_tarobj('flac', 'PCM_16', 16) + self._test_tarobj("flac", "PCM_16", 16) diff --git a/test/torchaudio_unittest/backend/soundfile/load_test.py b/test/torchaudio_unittest/backend/soundfile/load_test.py index 0e3a240d26..b8d4705a13 100644 --- a/test/torchaudio_unittest/backend/soundfile/load_test.py +++ b/test/torchaudio_unittest/backend/soundfile/load_test.py @@ -3,10 +3,9 @@ from unittest.mock import patch import torch +from parameterized import parameterized from torchaudio._internal import module_utils as _mod_utils from torchaudio.backend import soundfile_backend -from parameterized import parameterized - from torchaudio_unittest.common_utils import ( TempDirMixin, PytorchTestCase, @@ -16,6 +15,7 @@ load_wav, save_wav, ) + from .common import ( parameterize, dtype2subtype, @@ -27,7 +27,11 @@ def _get_mock_path( - ext: str, dtype: str, sample_rate: int, num_channels: int, num_frames: int, + ext: str, + dtype: str, + sample_rate: int, + num_channels: int, + num_frames: int, ): return f"{dtype}_{sample_rate}_{num_channels}_{num_frames}.{ext}" @@ -86,7 +90,7 @@ def read(self, frames, dtype, always_2d): num_frames=self._params["num_frames"], channels_first=False, ).numpy() - return data[self._start:self._start + frames] + return data[self._start : self._start + frames] def __enter__(self): return self @@ -96,21 +100,13 @@ def __exit__(self, *args, **kwargs): class MockedLoadTest(PytorchTestCase): - def assert_dtype( - self, ext, dtype, sample_rate, num_channels, normalize, channels_first - ): + def assert_dtype(self, ext, dtype, sample_rate, num_channels, normalize, channels_first): """When format is WAV or NIST, normalize=False will return the native dtype Tensor, otherwise float32""" num_frames = 3 * sample_rate path = _get_mock_path(ext, dtype, sample_rate, num_channels, num_frames) - expected_dtype = ( - torch.float32 - if normalize or ext not in ["wav", "nist"] - else getattr(torch, dtype) - ) + expected_dtype = torch.float32 if normalize or ext not in ["wav", "nist"] else getattr(torch, dtype) with patch("soundfile.SoundFile", SoundFileMock): - found, sr = soundfile_backend.load( - path, normalize=normalize, channels_first=channels_first - ) + found, sr = soundfile_backend.load(path, normalize=normalize, channels_first=channels_first) assert found.dtype == expected_dtype assert sample_rate == sr @@ -123,32 +119,28 @@ def assert_dtype( ) def test_wav(self, dtype, sample_rate, num_channels, normalize, channels_first): """Returns native dtype when normalize=False else float32""" - self.assert_dtype( - "wav", dtype, sample_rate, num_channels, normalize, channels_first - ) + self.assert_dtype("wav", dtype, sample_rate, num_channels, normalize, channels_first) @parameterize( - ["int8", "int16", "int32"], [8000, 16000], [1, 2], [True, False], [True, False], + ["int8", "int16", "int32"], + [8000, 16000], + [1, 2], + [True, False], + [True, False], ) def test_sphere(self, dtype, sample_rate, num_channels, normalize, channels_first): """Returns float32 always""" - self.assert_dtype( - "sph", dtype, sample_rate, num_channels, normalize, channels_first - ) + self.assert_dtype("sph", dtype, sample_rate, num_channels, normalize, channels_first) @parameterize([8000, 16000], [1, 2], [True, False], [True, False]) def test_ogg(self, sample_rate, num_channels, normalize, channels_first): """Returns float32 always""" - self.assert_dtype( - "ogg", "int16", sample_rate, num_channels, normalize, channels_first - ) + self.assert_dtype("ogg", "int16", sample_rate, num_channels, normalize, channels_first) @parameterize([8000, 16000], [1, 2], [True, False], [True, False]) def test_flac(self, sample_rate, num_channels, normalize, channels_first): """`soundfile_backend.load` can load ogg format.""" - self.assert_dtype( - "flac", "int16", sample_rate, num_channels, normalize, channels_first - ) + self.assert_dtype("flac", "int16", sample_rate, num_channels, normalize, channels_first) class LoadTestBase(TempDirMixin, PytorchTestCase): @@ -176,14 +168,17 @@ def assert_wav( ) save_wav(path, data, sample_rate, channels_first=channels_first) expected = load_wav(path, normalize=normalize, channels_first=channels_first)[0] - data, sr = soundfile_backend.load( - path, normalize=normalize, channels_first=channels_first - ) + data, sr = soundfile_backend.load(path, normalize=normalize, channels_first=channels_first) assert sr == sample_rate self.assertEqual(data, expected) def assert_sphere( - self, dtype, sample_rate, num_channels, channels_first=True, duration=1, + self, + dtype, + sample_rate, + num_channels, + channels_first=True, + duration=1, ): """`soundfile_backend.load` can load SPHERE format correctly.""" path = self.get_temp_path("reference.sph") @@ -195,16 +190,19 @@ def assert_sphere( normalize=False, channels_first=False, ) - soundfile.write( - path, raw, sample_rate, subtype=dtype2subtype(dtype), format="NIST" - ) + soundfile.write(path, raw, sample_rate, subtype=dtype2subtype(dtype), format="NIST") expected = normalize_wav(raw.t() if channels_first else raw) data, sr = soundfile_backend.load(path, channels_first=channels_first) assert sr == sample_rate self.assertEqual(data, expected, atol=1e-4, rtol=1e-8) def assert_flac( - self, dtype, sample_rate, num_channels, channels_first=True, duration=1, + self, + dtype, + sample_rate, + num_channels, + channels_first=True, + duration=1, ): """`soundfile_backend.load` can load FLAC format correctly.""" path = self.get_temp_path("reference.flac") @@ -239,7 +237,10 @@ def test_wav(self, dtype, sample_rate, num_channels, normalize, channels_first): self.assert_wav(dtype, sample_rate, num_channels, normalize, channels_first) @parameterize( - ["int16"], [16000], [2], [False], + ["int16"], + [16000], + [2], + [False], ) def test_wav_large(self, dtype, sample_rate, num_channels, normalize): """`soundfile_backend.load` can load large wav file correctly.""" @@ -269,15 +270,16 @@ def test_flac(self, dtype, sample_rate, num_channels, channels_first): @skipIfNoModule("soundfile") class TestLoadFormat(TempDirMixin, PytorchTestCase): """Given `format` parameter, `so.load` can load files without extension""" + original = None path = None def _make_file(self, format_): sample_rate = 8000 - path_with_ext = self.get_temp_path(f'test.{format_}') - data = get_wav_data('float32', num_channels=2).numpy().T + path_with_ext = self.get_temp_path(f"test.{format_}") + data = get_wav_data("float32", num_channels=2).numpy().T soundfile.write(path_with_ext, data, sample_rate) - expected = soundfile.read(path_with_ext, dtype='float32')[0].T + expected = soundfile.read(path_with_ext, dtype="float32")[0].T path = os.path.splitext(path_with_ext)[0] os.rename(path_with_ext, path) return path, expected @@ -288,15 +290,21 @@ def _test_format(self, format_): found, _ = soundfile_backend.load(path) self.assertEqual(found, expected) - @parameterized.expand([ - ('WAV', ), ('wav', ), - ]) + @parameterized.expand( + [ + ("WAV",), + ("wav",), + ] + ) def test_wav(self, format_): self._test_format(format_) - @parameterized.expand([ - ('FLAC', ), ('flac',), - ]) + @parameterized.expand( + [ + ("FLAC",), + ("flac",), + ] + ) @skipIfFormatNotSupported("FLAC") def test_flac(self, format_): self._test_format(format_) @@ -307,40 +315,40 @@ class TestFileObject(TempDirMixin, PytorchTestCase): def _test_fileobj(self, ext): """Loading audio via file-like object works""" sample_rate = 16000 - path = self.get_temp_path(f'test.{ext}') + path = self.get_temp_path(f"test.{ext}") - data = get_wav_data('float32', num_channels=2).numpy().T + data = get_wav_data("float32", num_channels=2).numpy().T soundfile.write(path, data, sample_rate) - expected = soundfile.read(path, dtype='float32')[0].T + expected = soundfile.read(path, dtype="float32")[0].T - with open(path, 'rb') as fileobj: + with open(path, "rb") as fileobj: found, sr = soundfile_backend.load(fileobj) assert sr == sample_rate self.assertEqual(expected, found) def test_fileobj_wav(self): """Loading audio via file-like object works""" - self._test_fileobj('wav') + self._test_fileobj("wav") @skipIfFormatNotSupported("FLAC") def test_fileobj_flac(self): """Loading audio via file-like object works""" - self._test_fileobj('flac') + self._test_fileobj("flac") def _test_tarfile(self, ext): """Loading audio via file-like object works""" sample_rate = 16000 - audio_file = f'test.{ext}' + audio_file = f"test.{ext}" audio_path = self.get_temp_path(audio_file) - archive_path = self.get_temp_path('archive.tar.gz') + archive_path = self.get_temp_path("archive.tar.gz") - data = get_wav_data('float32', num_channels=2).numpy().T + data = get_wav_data("float32", num_channels=2).numpy().T soundfile.write(audio_path, data, sample_rate) - expected = soundfile.read(audio_path, dtype='float32')[0].T + expected = soundfile.read(audio_path, dtype="float32")[0].T - with tarfile.TarFile(archive_path, 'w') as tarobj: + with tarfile.TarFile(archive_path, "w") as tarobj: tarobj.add(audio_path, arcname=audio_file) - with tarfile.TarFile(archive_path, 'r') as tarobj: + with tarfile.TarFile(archive_path, "r") as tarobj: fileobj = tarobj.extractfile(audio_file) found, sr = soundfile_backend.load(fileobj) @@ -349,9 +357,9 @@ def _test_tarfile(self, ext): def test_tarfile_wav(self): """Loading audio via file-like object works""" - self._test_tarfile('wav') + self._test_tarfile("wav") @skipIfFormatNotSupported("FLAC") def test_tarfile_flac(self): """Loading audio via file-like object works""" - self._test_tarfile('flac') + self._test_tarfile("flac") diff --git a/test/torchaudio_unittest/backend/soundfile/save_test.py b/test/torchaudio_unittest/backend/soundfile/save_test.py index e4e8c93631..c3a50be8b4 100644 --- a/test/torchaudio_unittest/backend/soundfile/save_test.py +++ b/test/torchaudio_unittest/backend/soundfile/save_test.py @@ -3,7 +3,6 @@ from torchaudio._internal import module_utils as _mod_utils from torchaudio.backend import soundfile_backend - from torchaudio_unittest.common_utils import ( TempDirMixin, PytorchTestCase, @@ -12,6 +11,7 @@ load_wav, nested_params, ) + from .common import ( fetch_wav_subtype, parameterize, @@ -30,23 +30,22 @@ class MockedSaveTest(PytorchTestCase): [False, True], [ (None, None), - ('PCM_U', None), - ('PCM_U', 8), - ('PCM_S', None), - ('PCM_S', 16), - ('PCM_S', 32), - ('PCM_F', None), - ('PCM_F', 32), - ('PCM_F', 64), - ('ULAW', None), - ('ULAW', 8), - ('ALAW', None), - ('ALAW', 8), + ("PCM_U", None), + ("PCM_U", 8), + ("PCM_S", None), + ("PCM_S", 16), + ("PCM_S", 32), + ("PCM_F", None), + ("PCM_F", 32), + ("PCM_F", 64), + ("ULAW", None), + ("ULAW", 8), + ("ALAW", None), + ("ALAW", 8), ], ) @patch("soundfile.write") - def test_wav(self, dtype, sample_rate, num_channels, channels_first, - enc_params, mocked_write): + def test_wav(self, dtype, sample_rate, num_channels, channels_first, enc_params, mocked_write): """soundfile_backend.save passes correct subtype to soundfile.write when WAV""" filepath = "foo.wav" input_tensor = get_wav_data( @@ -59,25 +58,33 @@ def test_wav(self, dtype, sample_rate, num_channels, channels_first, encoding, bits_per_sample = enc_params soundfile_backend.save( - filepath, input_tensor, sample_rate, channels_first=channels_first, - encoding=encoding, bits_per_sample=bits_per_sample + filepath, + input_tensor, + sample_rate, + channels_first=channels_first, + encoding=encoding, + bits_per_sample=bits_per_sample, ) # on +Py3.8 call_args.kwargs is more descreptive args = mocked_write.call_args[1] assert args["file"] == filepath assert args["samplerate"] == sample_rate - assert args["subtype"] == fetch_wav_subtype( - dtype, encoding, bits_per_sample) + assert args["subtype"] == fetch_wav_subtype(dtype, encoding, bits_per_sample) assert args["format"] is None - self.assertEqual( - args["data"], input_tensor.t() if channels_first else input_tensor - ) + self.assertEqual(args["data"], input_tensor.t() if channels_first else input_tensor) @patch("soundfile.write") def assert_non_wav( - self, fmt, dtype, sample_rate, num_channels, channels_first, mocked_write, - encoding=None, bits_per_sample=None, + self, + fmt, + dtype, + sample_rate, + num_channels, + channels_first, + mocked_write, + encoding=None, + bits_per_sample=None, ): """soundfile_backend.save passes correct subtype and format to soundfile.write when SPHERE""" filepath = f"foo.{fmt}" @@ -91,8 +98,12 @@ def assert_non_wav( expected_data = input_tensor.t() if channels_first else input_tensor soundfile_backend.save( - filepath, input_tensor, sample_rate, channels_first, - encoding=encoding, bits_per_sample=bits_per_sample, + filepath, + input_tensor, + sample_rate, + channels_first, + encoding=encoding, + bits_per_sample=bits_per_sample, ) # on +Py3.8 call_args.kwargs is more descreptive @@ -112,38 +123,42 @@ def assert_non_wav( [1, 2], [False, True], [ - ('PCM_S', 8), - ('PCM_S', 16), - ('PCM_S', 24), - ('PCM_S', 32), - ('ULAW', 8), - ('ALAW', 8), - ('ALAW', 16), - ('ALAW', 24), - ('ALAW', 32), + ("PCM_S", 8), + ("PCM_S", 16), + ("PCM_S", 24), + ("PCM_S", 32), + ("ULAW", 8), + ("ALAW", 8), + ("ALAW", 16), + ("ALAW", 24), + ("ALAW", 32), ], ) def test_sph(self, fmt, dtype, sample_rate, num_channels, channels_first, enc_params): """soundfile_backend.save passes default format and subtype (None-s) to soundfile.write when not WAV""" encoding, bits_per_sample = enc_params - self.assert_non_wav(fmt, dtype, sample_rate, num_channels, - channels_first, encoding=encoding, - bits_per_sample=bits_per_sample) + self.assert_non_wav( + fmt, dtype, sample_rate, num_channels, channels_first, encoding=encoding, bits_per_sample=bits_per_sample + ) @parameterize( - ["int32", "int16"], [8000, 16000], [1, 2], [False, True], + ["int32", "int16"], + [8000, 16000], + [1, 2], + [False, True], [8, 16, 24], ) - def test_flac(self, dtype, sample_rate, num_channels, - channels_first, bits_per_sample): + def test_flac(self, dtype, sample_rate, num_channels, channels_first, bits_per_sample): """soundfile_backend.save passes default format and subtype (None-s) to soundfile.write when not WAV""" - self.assert_non_wav("flac", dtype, sample_rate, num_channels, - channels_first, bits_per_sample=bits_per_sample) + self.assert_non_wav("flac", dtype, sample_rate, num_channels, channels_first, bits_per_sample=bits_per_sample) @parameterize( - ["int32", "int16"], [8000, 16000], [1, 2], [False, True], + ["int32", "int16"], + [8000, 16000], + [1, 2], + [False, True], ) def test_ogg(self, dtype, sample_rate, num_channels, channels_first): """soundfile_backend.save passes default format and subtype (None-s) to @@ -156,9 +171,7 @@ class SaveTestBase(TempDirMixin, PytorchTestCase): def assert_wav(self, dtype, sample_rate, num_channels, num_frames): """`soundfile_backend.save` can save wav format.""" path = self.get_temp_path("data.wav") - expected = get_wav_data( - dtype, num_channels, num_frames=num_frames, normalize=False - ) + expected = get_wav_data(dtype, num_channels, num_frames=num_frames, normalize=False) soundfile_backend.save(path, expected, sample_rate) found, sr = load_wav(path, normalize=False) assert sample_rate == sr @@ -172,9 +185,7 @@ def _assert_non_wav(self, fmt, dtype, sample_rate, num_channels): """ num_frames = sample_rate * 3 path = self.get_temp_path(f"data.{fmt}") - expected = get_wav_data( - dtype, num_channels, num_frames=num_frames, normalize=False - ) + expected = get_wav_data(dtype, num_channels, num_frames=num_frames, normalize=False) soundfile_backend.save(path, expected, sample_rate) sinfo = soundfile.info(path) assert sinfo.format == fmt.upper() @@ -201,14 +212,17 @@ def assert_ogg(self, dtype, sample_rate, num_channels): @skipIfNoModule("soundfile") class TestSave(SaveTestBase): @parameterize( - ["float32", "int32", "int16"], [8000, 16000], [1, 2], + ["float32", "int32", "int16"], + [8000, 16000], + [1, 2], ) def test_wav(self, dtype, sample_rate, num_channels): """`soundfile_backend.save` can save wav format.""" self.assert_wav(dtype, sample_rate, num_channels, num_frames=None) @parameterize( - ["float32", "int32", "int16"], [4, 8, 16, 32], + ["float32", "int32", "int16"], + [4, 8, 16, 32], ) def test_multiple_channels(self, dtype, num_channels): """`soundfile_backend.save` can save wav with more than 2 channels.""" @@ -216,7 +230,9 @@ def test_multiple_channels(self, dtype, num_channels): self.assert_wav(dtype, sample_rate, num_channels, num_frames=None) @parameterize( - ["int32", "int16"], [8000, 16000], [1, 2], + ["int32", "int16"], + [8000, 16000], + [1, 2], ) @skipIfFormatNotSupported("NIST") def test_sphere(self, dtype, sample_rate, num_channels): @@ -224,7 +240,8 @@ def test_sphere(self, dtype, sample_rate, num_channels): self.assert_sphere(dtype, sample_rate, num_channels) @parameterize( - [8000, 16000], [1, 2], + [8000, 16000], + [1, 2], ) @skipIfFormatNotSupported("FLAC") def test_flac(self, sample_rate, num_channels): @@ -232,7 +249,8 @@ def test_flac(self, sample_rate, num_channels): self.assert_flac("float32", sample_rate, num_channels) @parameterize( - [8000, 16000], [1, 2], + [8000, 16000], + [1, 2], ) @skipIfFormatNotSupported("OGG") def test_ogg(self, sample_rate, num_channels): @@ -260,36 +278,36 @@ class TestFileObject(TempDirMixin, PytorchTestCase): def _test_fileobj(self, ext): """Saving audio to file-like object works""" sample_rate = 16000 - path = self.get_temp_path(f'test.{ext}') + path = self.get_temp_path(f"test.{ext}") - subtype = 'FLOAT' if ext == 'wav' else None - data = get_wav_data('float32', num_channels=2) + subtype = "FLOAT" if ext == "wav" else None + data = get_wav_data("float32", num_channels=2) soundfile.write(path, data.numpy().T, sample_rate, subtype=subtype) - expected = soundfile.read(path, dtype='float32')[0] + expected = soundfile.read(path, dtype="float32")[0] fileobj = io.BytesIO() soundfile_backend.save(fileobj, data, sample_rate, format=ext) fileobj.seek(0) - found, sr = soundfile.read(fileobj, dtype='float32') + found, sr = soundfile.read(fileobj, dtype="float32") assert sr == sample_rate self.assertEqual(expected, found, atol=1e-4, rtol=1e-8) def test_fileobj_wav(self): """Saving audio via file-like object works""" - self._test_fileobj('wav') + self._test_fileobj("wav") @skipIfFormatNotSupported("FLAC") def test_fileobj_flac(self): """Saving audio via file-like object works""" - self._test_fileobj('flac') + self._test_fileobj("flac") @skipIfFormatNotSupported("NIST") def test_fileobj_nist(self): """Saving audio via file-like object works""" - self._test_fileobj('NIST') + self._test_fileobj("NIST") @skipIfFormatNotSupported("OGG") def test_fileobj_ogg(self): """Saving audio via file-like object works""" - self._test_fileobj('OGG') + self._test_fileobj("OGG") diff --git a/test/torchaudio_unittest/backend/sox_io/common.py b/test/torchaudio_unittest/backend/sox_io/common.py index c2538b2bc4..8564cabf31 100644 --- a/test/torchaudio_unittest/backend/sox_io/common.py +++ b/test/torchaudio_unittest/backend/sox_io/common.py @@ -3,12 +3,12 @@ def name_func(func, _, params): def get_enc_params(dtype): - if dtype == 'float32': - return 'PCM_F', 32 - if dtype == 'int32': - return 'PCM_S', 32 - if dtype == 'int16': - return 'PCM_S', 16 - if dtype == 'uint8': - return 'PCM_U', 8 - raise ValueError(f'Unexpected dtype: {dtype}') + if dtype == "float32": + return "PCM_F", 32 + if dtype == "int32": + return "PCM_S", 32 + if dtype == "int16": + return "PCM_S", 16 + if dtype == "uint8": + return "PCM_U", 8 + raise ValueError(f"Unexpected dtype: {dtype}") diff --git a/test/torchaudio_unittest/backend/sox_io/info_test.py b/test/torchaudio_unittest/backend/sox_io/info_test.py index 0405d1ba9c..4ca03b4567 100644 --- a/test/torchaudio_unittest/backend/sox_io/info_test.py +++ b/test/torchaudio_unittest/backend/sox_io/info_test.py @@ -1,14 +1,13 @@ -from contextlib import contextmanager import io -import os import itertools +import os import tarfile +from contextlib import contextmanager from parameterized import parameterized +from torchaudio._internal import module_utils as _mod_utils from torchaudio.backend import sox_io_backend from torchaudio.utils.sox_utils import get_buffer_size, set_buffer_size -from torchaudio._internal import module_utils as _mod_utils - from torchaudio_unittest.backend.common import ( get_bits_per_sample, get_encoding, @@ -25,6 +24,7 @@ save_wav, sox_utils, ) + from .common import ( name_func, ) @@ -34,18 +34,23 @@ import requests -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestInfo(TempDirMixin, PytorchTestCase): - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_wav(self, dtype, sample_rate, num_channels): """`sox_io_backend.info` can check wav file correctly""" duration = 1 - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") data = get_wav_data(dtype, num_channels, normalize=False, num_frames=duration * sample_rate) save_wav(path, data, sample_rate) info = sox_io_backend.info(path) @@ -53,17 +58,22 @@ def test_wav(self, dtype, sample_rate, num_channels): assert info.num_frames == sample_rate * duration assert info.num_channels == num_channels assert info.bits_per_sample == sox_utils.get_bit_depth(dtype) - assert info.encoding == get_encoding('wav', dtype) - - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [4, 8, 16, 32], - )), name_func=name_func) + assert info.encoding == get_encoding("wav", dtype) + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [4, 8, 16, 32], + ) + ), + name_func=name_func, + ) def test_wav_multiple_channels(self, dtype, sample_rate, num_channels): """`sox_io_backend.info` can check wav file with channels more than 2 correctly""" duration = 1 - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") data = get_wav_data(dtype, num_channels, normalize=False, num_frames=duration * sample_rate) save_wav(path, data, sample_rate) info = sox_io_backend.info(path) @@ -71,20 +81,28 @@ def test_wav_multiple_channels(self, dtype, sample_rate, num_channels): assert info.num_frames == sample_rate * duration assert info.num_channels == num_channels assert info.bits_per_sample == sox_utils.get_bit_depth(dtype) - assert info.encoding == get_encoding('wav', dtype) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [96, 128, 160, 192, 224, 256, 320], - )), name_func=name_func) + assert info.encoding == get_encoding("wav", dtype) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [96, 128, 160, 192, 224, 256, 320], + ) + ), + name_func=name_func, + ) def test_mp3(self, sample_rate, num_channels, bit_rate): """`sox_io_backend.info` can check mp3 file correctly""" duration = 1 - path = self.get_temp_path('data.mp3') + path = self.get_temp_path("data.mp3") sox_utils.gen_audio_file( - path, sample_rate, num_channels, - compression=bit_rate, duration=duration, + path, + sample_rate, + num_channels, + compression=bit_rate, + duration=duration, ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate @@ -94,18 +112,26 @@ def test_mp3(self, sample_rate, num_channels, bit_rate): assert info.bits_per_sample == 0 # bit_per_sample is irrelevant for compressed formats assert info.encoding == "MP3" - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_flac(self, sample_rate, num_channels, compression_level): """`sox_io_backend.info` can check flac file correctly""" duration = 1 - path = self.get_temp_path('data.flac') + path = self.get_temp_path("data.flac") sox_utils.gen_audio_file( - path, sample_rate, num_channels, - compression=compression_level, duration=duration, + path, + sample_rate, + num_channels, + compression=compression_level, + duration=duration, ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate @@ -114,18 +140,26 @@ def test_flac(self, sample_rate, num_channels, compression_level): assert info.bits_per_sample == 24 # FLAC standard assert info.encoding == "FLAC" - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-1, 0, 1, 2, 3, 3.6, 5, 10], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-1, 0, 1, 2, 3, 3.6, 5, 10], + ) + ), + name_func=name_func, + ) def test_vorbis(self, sample_rate, num_channels, quality_level): """`sox_io_backend.info` can check vorbis file correctly""" duration = 1 - path = self.get_temp_path('data.vorbis') + path = self.get_temp_path("data.vorbis") sox_utils.gen_audio_file( - path, sample_rate, num_channels, - compression=quality_level, duration=duration, + path, + sample_rate, + num_channels, + compression=quality_level, + duration=duration, ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate @@ -134,18 +168,21 @@ def test_vorbis(self, sample_rate, num_channels, quality_level): assert info.bits_per_sample == 0 # bit_per_sample is irrelevant for compressed formats assert info.encoding == "VORBIS" - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [16, 32], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [16, 32], + ) + ), + name_func=name_func, + ) def test_sphere(self, sample_rate, num_channels, bits_per_sample): """`sox_io_backend.info` can check sph file correctly""" duration = 1 - path = self.get_temp_path('data.sph') - sox_utils.gen_audio_file( - path, sample_rate, num_channels, duration=duration, - bit_depth=bits_per_sample) + path = self.get_temp_path("data.sph") + sox_utils.gen_audio_file(path, sample_rate, num_channels, duration=duration, bit_depth=bits_per_sample) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -153,19 +190,22 @@ def test_sphere(self, sample_rate, num_channels, bits_per_sample): assert info.bits_per_sample == bits_per_sample assert info.encoding == "PCM_S" - @parameterized.expand(list(itertools.product( - ['int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_amb(self, dtype, sample_rate, num_channels): """`sox_io_backend.info` can check amb file correctly""" duration = 1 - path = self.get_temp_path('data.amb') + path = self.get_temp_path("data.amb") bits_per_sample = sox_utils.get_bit_depth(dtype) - sox_utils.gen_audio_file( - path, sample_rate, num_channels, - bit_depth=bits_per_sample, duration=duration) + sox_utils.gen_audio_file(path, sample_rate, num_channels, bit_depth=bits_per_sample, duration=duration) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -178,10 +218,10 @@ def test_amr_nb(self): duration = 1 num_channels = 1 sample_rate = 8000 - path = self.get_temp_path('data.amr-nb') + path = self.get_temp_path("data.amr-nb") sox_utils.gen_audio_file( - path, sample_rate=sample_rate, num_channels=num_channels, bit_depth=16, - duration=duration) + path, sample_rate=sample_rate, num_channels=num_channels, bit_depth=16, duration=duration + ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -194,11 +234,10 @@ def test_ulaw(self): duration = 1 num_channels = 1 sample_rate = 8000 - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") sox_utils.gen_audio_file( - path, sample_rate=sample_rate, num_channels=num_channels, - bit_depth=8, encoding='u-law', - duration=duration) + path, sample_rate=sample_rate, num_channels=num_channels, bit_depth=8, encoding="u-law", duration=duration + ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -211,11 +250,10 @@ def test_alaw(self): duration = 1 num_channels = 1 sample_rate = 8000 - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") sox_utils.gen_audio_file( - path, sample_rate=sample_rate, num_channels=num_channels, - bit_depth=8, encoding='a-law', - duration=duration) + path, sample_rate=sample_rate, num_channels=num_channels, bit_depth=8, encoding="a-law", duration=duration + ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -228,10 +266,8 @@ def test_gsm(self): duration = 1 num_channels = 1 sample_rate = 8000 - path = self.get_temp_path('data.gsm') - sox_utils.gen_audio_file( - path, sample_rate=sample_rate, num_channels=num_channels, - duration=duration) + path = self.get_temp_path("data.gsm") + sox_utils.gen_audio_file(path, sample_rate=sample_rate, num_channels=num_channels, duration=duration) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_channels == num_channels @@ -243,10 +279,10 @@ def test_htk(self): duration = 1 num_channels = 1 sample_rate = 8000 - path = self.get_temp_path('data.htk') + path = self.get_temp_path("data.htk") sox_utils.gen_audio_file( - path, sample_rate=sample_rate, num_channels=num_channels, - bit_depth=16, duration=duration) + path, sample_rate=sample_rate, num_channels=num_channels, bit_depth=16, duration=duration + ) info = sox_io_backend.info(path) assert info.sample_rate == sample_rate assert info.num_frames == sample_rate * duration @@ -257,14 +293,19 @@ def test_htk(self): @skipIfNoSox class TestInfoOpus(PytorchTestCase): - @parameterized.expand(list(itertools.product( - ['96k'], - [1, 2], - [0, 5, 10], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["96k"], + [1, 2], + [0, 5, 10], + ) + ), + name_func=name_func, + ) def test_opus(self, bitrate, num_channels, compression_level): """`sox_io_backend.info` can check opus file correcty""" - path = get_asset_path('io', f'{bitrate}_{compression_level}_{num_channels}ch.opus') + path = get_asset_path("io", f"{bitrate}_{compression_level}_{num_channels}ch.opus") info = sox_io_backend.info(path) assert info.sample_rate == 48000 assert info.num_frames == 32768 @@ -296,13 +337,15 @@ def test_mp3(self): class FileObjTestBase(TempDirMixin): def _gen_file(self, ext, dtype, sample_rate, num_channels, num_frames, *, comments=None): - path = self.get_temp_path(f'test.{ext}') + path = self.get_temp_path(f"test.{ext}") bit_depth = sox_utils.get_bit_depth(dtype) duration = num_frames / sample_rate comment_file = self._gen_comment_file(comments) if comments else None sox_utils.gen_audio_file( - path, sample_rate, num_channels=num_channels, + path, + sample_rate, + num_channels=num_channels, encoding=sox_utils.get_encoding(dtype), bit_depth=bit_depth, duration=duration, @@ -318,29 +361,29 @@ def _gen_comment_file(self, comments): @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") class TestFileObject(FileObjTestBase, PytorchTestCase): def _query_fileobj(self, ext, dtype, sample_rate, num_channels, num_frames, *, comments=None): path = self._gen_file(ext, dtype, sample_rate, num_channels, num_frames, comments=comments) - format_ = ext if ext in ['mp3'] else None - with open(path, 'rb') as fileobj: + format_ = ext if ext in ["mp3"] else None + with open(path, "rb") as fileobj: return sox_io_backend.info(fileobj, format_) def _query_bytesio(self, ext, dtype, sample_rate, num_channels, num_frames): path = self._gen_file(ext, dtype, sample_rate, num_channels, num_frames) - format_ = ext if ext in ['mp3'] else None - with open(path, 'rb') as file_: + format_ = ext if ext in ["mp3"] else None + with open(path, "rb") as file_: fileobj = io.BytesIO(file_.read()) return sox_io_backend.info(fileobj, format_) def _query_tarfile(self, ext, dtype, sample_rate, num_channels, num_frames): audio_path = self._gen_file(ext, dtype, sample_rate, num_channels, num_frames) audio_file = os.path.basename(audio_path) - archive_path = self.get_temp_path('archive.tar.gz') - with tarfile.TarFile(archive_path, 'w') as tarobj: + archive_path = self.get_temp_path("archive.tar.gz") + with tarfile.TarFile(archive_path, "w") as tarobj: tarobj.add(audio_path, arcname=audio_file) - format_ = ext if ext in ['mp3'] else None - with tarfile.TarFile(archive_path, 'r') as tarobj: + format_ = ext if ext in ["mp3"] else None + with tarfile.TarFile(archive_path, "r") as tarobj: fileobj = tarobj.extractfile(audio_file) return sox_io_backend.info(fileobj, format_) @@ -353,16 +396,18 @@ def _set_buffer_size(self, buffer_size): finally: set_buffer_size(original_buffer_size) - @parameterized.expand([ - ('wav', "float32"), - ('wav', "int32"), - ('wav', "int16"), - ('wav', "uint8"), - ('mp3', "float32"), - ('flac', "float32"), - ('vorbis', "float32"), - ('amb', "int16"), - ]) + @parameterized.expand( + [ + ("wav", "float32"), + ("wav", "int32"), + ("wav", "int16"), + ("wav", "uint8"), + ("mp3", "float32"), + ("flac", "float32"), + ("vorbis", "float32"), + ("amb", "int16"), + ] + ) def test_fileobj(self, ext, dtype): """Querying audio via file object works""" sample_rate = 16000 @@ -371,7 +416,7 @@ def test_fileobj(self, ext, dtype): sinfo = self._query_fileobj(ext, dtype, sample_rate, num_channels, num_frames) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels @@ -379,9 +424,11 @@ def test_fileobj(self, ext, dtype): assert sinfo.bits_per_sample == bits_per_sample assert sinfo.encoding == get_encoding(ext, dtype) - @parameterized.expand([ - ('vorbis', "float32"), - ]) + @parameterized.expand( + [ + ("vorbis", "float32"), + ] + ) def test_fileobj_large_header(self, ext, dtype): """ For audio file with header size exceeding default buffer size: @@ -399,7 +446,7 @@ def test_fileobj_large_header(self, ext, dtype): with self._set_buffer_size(16384): sinfo = self._query_fileobj(ext, dtype, sample_rate, num_channels, num_frames, comments=comments) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels @@ -407,16 +454,18 @@ def test_fileobj_large_header(self, ext, dtype): assert sinfo.bits_per_sample == bits_per_sample assert sinfo.encoding == get_encoding(ext, dtype) - @parameterized.expand([ - ('wav', "float32"), - ('wav', "int32"), - ('wav', "int16"), - ('wav', "uint8"), - ('mp3', "float32"), - ('flac', "float32"), - ('vorbis', "float32"), - ('amb', "int16"), - ]) + @parameterized.expand( + [ + ("wav", "float32"), + ("wav", "int32"), + ("wav", "int16"), + ("wav", "uint8"), + ("mp3", "float32"), + ("flac", "float32"), + ("vorbis", "float32"), + ("amb", "int16"), + ] + ) def test_bytesio(self, ext, dtype): """Querying audio via ByteIO object works for small data""" sample_rate = 16000 @@ -425,7 +474,7 @@ def test_bytesio(self, ext, dtype): sinfo = self._query_bytesio(ext, dtype, sample_rate, num_channels, num_frames) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels @@ -433,16 +482,18 @@ def test_bytesio(self, ext, dtype): assert sinfo.bits_per_sample == bits_per_sample assert sinfo.encoding == get_encoding(ext, dtype) - @parameterized.expand([ - ('wav', "float32"), - ('wav', "int32"), - ('wav', "int16"), - ('wav', "uint8"), - ('mp3', "float32"), - ('flac', "float32"), - ('vorbis', "float32"), - ('amb', "int16"), - ]) + @parameterized.expand( + [ + ("wav", "float32"), + ("wav", "int32"), + ("wav", "int16"), + ("wav", "uint8"), + ("mp3", "float32"), + ("flac", "float32"), + ("vorbis", "float32"), + ("amb", "int16"), + ] + ) def test_bytesio_tiny(self, ext, dtype): """Querying audio via ByteIO object works for small data""" sample_rate = 8000 @@ -451,7 +502,7 @@ def test_bytesio_tiny(self, ext, dtype): sinfo = self._query_bytesio(ext, dtype, sample_rate, num_channels, num_frames) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels @@ -459,16 +510,18 @@ def test_bytesio_tiny(self, ext, dtype): assert sinfo.bits_per_sample == bits_per_sample assert sinfo.encoding == get_encoding(ext, dtype) - @parameterized.expand([ - ('wav', "float32"), - ('wav', "int32"), - ('wav', "int16"), - ('wav', "uint8"), - ('mp3', "float32"), - ('flac', "float32"), - ('vorbis', "float32"), - ('amb', "int16"), - ]) + @parameterized.expand( + [ + ("wav", "float32"), + ("wav", "int32"), + ("wav", "int16"), + ("wav", "uint8"), + ("mp3", "float32"), + ("flac", "float32"), + ("vorbis", "float32"), + ("amb", "int16"), + ] + ) def test_tarfile(self, ext, dtype): """Querying compressed audio via file-like object works""" sample_rate = 16000 @@ -477,7 +530,7 @@ def test_tarfile(self, ext, dtype): sinfo = self._query_tarfile(ext, dtype, sample_rate, num_channels, num_frames) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels @@ -487,7 +540,7 @@ def test_tarfile(self, ext, dtype): @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoModule("requests") class TestFileObjectHttp(HttpServerMixin, FileObjTestBase, PytorchTestCase): def _query_http(self, ext, dtype, sample_rate, num_channels, num_frames): @@ -495,20 +548,22 @@ def _query_http(self, ext, dtype, sample_rate, num_channels, num_frames): audio_file = os.path.basename(audio_path) url = self.get_url(audio_file) - format_ = ext if ext in ['mp3'] else None + format_ = ext if ext in ["mp3"] else None with requests.get(url, stream=True) as resp: return sox_io_backend.info(resp.raw, format=format_) - @parameterized.expand([ - ('wav', "float32"), - ('wav', "int32"), - ('wav', "int16"), - ('wav', "uint8"), - ('mp3', "float32"), - ('flac', "float32"), - ('vorbis', "float32"), - ('amb', "int16"), - ]) + @parameterized.expand( + [ + ("wav", "float32"), + ("wav", "int32"), + ("wav", "int16"), + ("wav", "uint8"), + ("mp3", "float32"), + ("flac", "float32"), + ("vorbis", "float32"), + ("amb", "int16"), + ] + ) def test_requests(self, ext, dtype): """Querying compressed audio via requests works""" sample_rate = 16000 @@ -517,7 +572,7 @@ def test_requests(self, ext, dtype): sinfo = self._query_http(ext, dtype, sample_rate, num_channels, num_frames) bits_per_sample = get_bits_per_sample(ext, dtype) - num_frames = 0 if ext in ['mp3', 'vorbis'] else num_frames + num_frames = 0 if ext in ["mp3", "vorbis"] else num_frames assert sinfo.sample_rate == sample_rate assert sinfo.num_channels == num_channels diff --git a/test/torchaudio_unittest/backend/sox_io/load_test.py b/test/torchaudio_unittest/backend/sox_io/load_test.py index 031c0f2d0d..7295a082eb 100644 --- a/test/torchaudio_unittest/backend/sox_io/load_test.py +++ b/test/torchaudio_unittest/backend/sox_io/load_test.py @@ -3,9 +3,8 @@ import tarfile from parameterized import parameterized -from torchaudio.backend import sox_io_backend from torchaudio._internal import module_utils as _mod_utils - +from torchaudio.backend import sox_io_backend from torchaudio_unittest.common_utils import ( TempDirMixin, HttpServerMixin, @@ -19,6 +18,7 @@ save_wav, sox_utils, ) + from .common import ( name_func, ) @@ -30,17 +30,17 @@ class LoadTestBase(TempDirMixin, PytorchTestCase): def assert_format( - self, - format: str, - sample_rate: float, - num_channels: int, - compression: float = None, - bit_depth: int = None, - duration: float = 1, - normalize: bool = True, - encoding: str = None, - atol: float = 4e-05, - rtol: float = 1.3e-06, + self, + format: str, + sample_rate: float, + num_channels: int, + compression: float = None, + bit_depth: int = None, + duration: float = 1, + normalize: bool = True, + encoding: str = None, + atol: float = 4e-05, + rtol: float = 1.3e-06, ): """`sox_io_backend.load` can load given format correctly. @@ -68,13 +68,18 @@ def assert_format( data without using torchaudio """ - path = self.get_temp_path(f'1.original.{format}') - ref_path = self.get_temp_path('2.reference.wav') + path = self.get_temp_path(f"1.original.{format}") + ref_path = self.get_temp_path("2.reference.wav") # 1. Generate the given format with sox sox_utils.gen_audio_file( - path, sample_rate, num_channels, encoding=encoding, - compression=compression, bit_depth=bit_depth, duration=duration, + path, + sample_rate, + num_channels, + encoding=encoding, + compression=compression, + bit_depth=bit_depth, + duration=duration, ) # 2. Convert to wav with sox wav_bit_depth = 32 if bit_depth == 24 else None # for 24-bit wav @@ -92,7 +97,7 @@ def assert_wav(self, dtype, sample_rate, num_channels, normalize, duration): Wav data loaded with sox_io backend should match those with scipy """ - path = self.get_temp_path('reference.wav') + path = self.get_temp_path("reference.wav") data = get_wav_data(dtype, num_channels, normalize=normalize, num_frames=duration * sample_rate) save_wav(path, data, sample_rate) expected = load_wav(path, normalize=normalize)[0] @@ -101,118 +106,176 @@ def assert_wav(self, dtype, sample_rate, num_channels, normalize, duration): self.assertEqual(data, expected) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestLoad(LoadTestBase): """Test the correctness of `sox_io_backend.load` for various formats""" - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - [False, True], - )), name_func=name_func) + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + [False, True], + ) + ), + name_func=name_func, + ) def test_wav(self, dtype, sample_rate, num_channels, normalize): """`sox_io_backend.load` can load wav format correctly.""" self.assert_wav(dtype, sample_rate, num_channels, normalize, duration=1) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [False, True], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [False, True], + ) + ), + name_func=name_func, + ) def test_24bit_wav(self, sample_rate, num_channels, normalize): """`sox_io_backend.load` can load 24bit wav format correctly. Corectly casts it to ``int32`` tensor dtype.""" self.assert_format("wav", sample_rate, num_channels, bit_depth=24, normalize=normalize, duration=1) - @parameterized.expand(list(itertools.product( - ['int16'], - [16000], - [2], - [False], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["int16"], + [16000], + [2], + [False], + ) + ), + name_func=name_func, + ) def test_wav_large(self, dtype, sample_rate, num_channels, normalize): """`sox_io_backend.load` can load large wav file correctly.""" two_hours = 2 * 60 * 60 self.assert_wav(dtype, sample_rate, num_channels, normalize, two_hours) - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [4, 8, 16, 32], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [4, 8, 16, 32], + ) + ), + name_func=name_func, + ) def test_multiple_channels(self, dtype, num_channels): """`sox_io_backend.load` can load wav file with more than 2 channels.""" sample_rate = 8000 normalize = False self.assert_wav(dtype, sample_rate, num_channels, normalize, duration=1) - @parameterized.expand(list(itertools.product( - [8000, 16000, 44100], - [1, 2], - [96, 128, 160, 192, 224, 256, 320], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000, 44100], + [1, 2], + [96, 128, 160, 192, 224, 256, 320], + ) + ), + name_func=name_func, + ) def test_mp3(self, sample_rate, num_channels, bit_rate): """`sox_io_backend.load` can load mp3 format correctly.""" self.assert_format("mp3", sample_rate, num_channels, compression=bit_rate, duration=1, atol=5e-05) - @parameterized.expand(list(itertools.product( - [16000], - [2], - [128], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [16000], + [2], + [128], + ) + ), + name_func=name_func, + ) def test_mp3_large(self, sample_rate, num_channels, bit_rate): """`sox_io_backend.load` can load large mp3 file correctly.""" two_hours = 2 * 60 * 60 self.assert_format("mp3", sample_rate, num_channels, compression=bit_rate, duration=two_hours, atol=5e-05) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_flac(self, sample_rate, num_channels, compression_level): """`sox_io_backend.load` can load flac format correctly.""" self.assert_format("flac", sample_rate, num_channels, compression=compression_level, bit_depth=16, duration=1) - @parameterized.expand(list(itertools.product( - [16000], - [2], - [0], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [16000], + [2], + [0], + ) + ), + name_func=name_func, + ) def test_flac_large(self, sample_rate, num_channels, compression_level): """`sox_io_backend.load` can load large flac file correctly.""" two_hours = 2 * 60 * 60 self.assert_format( - "flac", sample_rate, num_channels, compression=compression_level, bit_depth=16, duration=two_hours) + "flac", sample_rate, num_channels, compression=compression_level, bit_depth=16, duration=two_hours + ) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-1, 0, 1, 2, 3, 3.6, 5, 10], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-1, 0, 1, 2, 3, 3.6, 5, 10], + ) + ), + name_func=name_func, + ) def test_vorbis(self, sample_rate, num_channels, quality_level): """`sox_io_backend.load` can load vorbis format correctly.""" self.assert_format("vorbis", sample_rate, num_channels, compression=quality_level, bit_depth=16, duration=1) - @parameterized.expand(list(itertools.product( - [16000], - [2], - [10], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [16000], + [2], + [10], + ) + ), + name_func=name_func, + ) def test_vorbis_large(self, sample_rate, num_channels, quality_level): """`sox_io_backend.load` can load large vorbis file correctly.""" two_hours = 2 * 60 * 60 self.assert_format( - "vorbis", sample_rate, num_channels, compression=quality_level, bit_depth=16, duration=two_hours) + "vorbis", sample_rate, num_channels, compression=quality_level, bit_depth=16, duration=two_hours + ) - @parameterized.expand(list(itertools.product( - ['96k'], - [1, 2], - [0, 5, 10], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["96k"], + [1, 2], + [0, 5, 10], + ) + ), + name_func=name_func, + ) def test_opus(self, bitrate, num_channels, compression_level): """`sox_io_backend.load` can load opus file correctly.""" - ops_path = get_asset_path('io', f'{bitrate}_{compression_level}_{num_channels}ch.opus') - wav_path = self.get_temp_path(f'{bitrate}_{compression_level}_{num_channels}ch.opus.wav') + ops_path = get_asset_path("io", f"{bitrate}_{compression_level}_{num_channels}ch.opus") + wav_path = self.get_temp_path(f"{bitrate}_{compression_level}_{num_channels}ch.opus.wav") sox_utils.convert_audio_file(ops_path, wav_path) expected, sample_rate = load_wav(wav_path) @@ -221,57 +284,74 @@ def test_opus(self, bitrate, num_channels, compression_level): assert sample_rate == sr self.assertEqual(expected, found) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_sphere(self, sample_rate, num_channels): """`sox_io_backend.load` can load sph format correctly.""" self.assert_format("sph", sample_rate, num_channels, bit_depth=32, duration=1) - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16'], - [8000, 16000], - [1, 2], - [False, True], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16"], + [8000, 16000], + [1, 2], + [False, True], + ) + ), + name_func=name_func, + ) def test_amb(self, dtype, sample_rate, num_channels, normalize): """`sox_io_backend.load` can load amb format correctly.""" bit_depth = sox_utils.get_bit_depth(dtype) encoding = sox_utils.get_encoding(dtype) self.assert_format( - "amb", sample_rate, num_channels, bit_depth=bit_depth, duration=1, encoding=encoding, normalize=normalize) + "amb", sample_rate, num_channels, bit_depth=bit_depth, duration=1, encoding=encoding, normalize=normalize + ) def test_amr_nb(self): """`sox_io_backend.load` can load amr_nb format correctly.""" self.assert_format("amr-nb", sample_rate=8000, num_channels=1, bit_depth=32, duration=1) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestLoadParams(TempDirMixin, PytorchTestCase): """Test the correctness of frame parameters of `sox_io_backend.load`""" + original = None path = None def setUp(self): super().setUp() sample_rate = 8000 - self.original = get_wav_data('float32', num_channels=2) - self.path = self.get_temp_path('test.wav') + self.original = get_wav_data("float32", num_channels=2) + self.path = self.get_temp_path("test.wav") save_wav(self.path, self.original, sample_rate) - @parameterized.expand(list(itertools.product( - [0, 1, 10, 100, 1000], - [-1, 1, 10, 100, 1000], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [0, 1, 10, 100, 1000], + [-1, 1, 10, 100, 1000], + ) + ), + name_func=name_func, + ) def test_frame(self, frame_offset, num_frames): """num_frames and frame_offset correctly specify the region of data""" found, _ = sox_io_backend.load(self.path, frame_offset, num_frames) frame_end = None if num_frames == -1 else frame_offset + num_frames self.assertEqual(found, self.original[:, frame_offset:frame_end]) - @parameterized.expand([(True, ), (False, )], name_func=name_func) + @parameterized.expand([(True,), (False,)], name_func=name_func) def test_channels_first(self, channels_first): """channels_first swaps axes""" found, _ = sox_io_backend.load(self.path, channels_first=channels_first) @@ -299,7 +379,7 @@ def test_mp3(self): class CloggedFileObj: def __init__(self, fileobj): self.fileobj = fileobj - self.buffer = b'' + self.buffer = b"" def read(self, n): if not self.buffer: @@ -310,163 +390,168 @@ def read(self, n): @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") class TestFileObject(TempDirMixin, PytorchTestCase): """ In this test suite, the result of file-like object input is compared against file path input, because `load` function is rigrously tested for file path inputs to match libsox's result, """ - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_fileobj(self, ext, kwargs): """Loading audio via file object returns the same result as via file path.""" sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - path = self.get_temp_path(f'test.{ext}') + format_ = ext if ext in ["mp3"] else None + path = self.get_temp_path(f"test.{ext}") - sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, **kwargs) + sox_utils.gen_audio_file(path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) - with open(path, 'rb') as fileobj: + with open(path, "rb") as fileobj: found, sr = sox_io_backend.load(fileobj, format=format_) assert sr == sample_rate self.assertEqual(expected, found) - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_bytesio(self, ext, kwargs): """Loading audio via BytesIO object returns the same result as via file path.""" sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - path = self.get_temp_path(f'test.{ext}') + format_ = ext if ext in ["mp3"] else None + path = self.get_temp_path(f"test.{ext}") - sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, **kwargs) + sox_utils.gen_audio_file(path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) - with open(path, 'rb') as file_: + with open(path, "rb") as file_: fileobj = io.BytesIO(file_.read()) found, sr = sox_io_backend.load(fileobj, format=format_) assert sr == sample_rate self.assertEqual(expected, found) - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_bytesio_clogged(self, ext, kwargs): """Loading audio via clogged file object returns the same result as via file path. This test case validates the case where fileobject returns shorter bytes than requeted. """ sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - path = self.get_temp_path(f'test.{ext}') + format_ = ext if ext in ["mp3"] else None + path = self.get_temp_path(f"test.{ext}") - sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, **kwargs) + sox_utils.gen_audio_file(path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(path) - with open(path, 'rb') as file_: + with open(path, "rb") as file_: fileobj = CloggedFileObj(io.BytesIO(file_.read())) found, sr = sox_io_backend.load(fileobj, format=format_) assert sr == sample_rate self.assertEqual(expected, found) - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_bytesio_tiny(self, ext, kwargs): - """Loading very small audio via file object returns the same result as via file path. - """ + """Loading very small audio via file object returns the same result as via file path.""" sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - path = self.get_temp_path(f'test.{ext}') + format_ = ext if ext in ["mp3"] else None + path = self.get_temp_path(f"test.{ext}") - sox_utils.gen_audio_file( - path, sample_rate, num_channels=2, duration=1 / 1600, **kwargs) + sox_utils.gen_audio_file(path, sample_rate, num_channels=2, duration=1 / 1600, **kwargs) expected, _ = sox_io_backend.load(path) - with open(path, 'rb') as file_: + with open(path, "rb") as file_: fileobj = io.BytesIO(file_.read()) found, sr = sox_io_backend.load(fileobj, format=format_) assert sr == sample_rate self.assertEqual(expected, found) - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_tarfile(self, ext, kwargs): """Loading compressed audio via file-like object returns the same result as via file path.""" sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - audio_file = f'test.{ext}' + format_ = ext if ext in ["mp3"] else None + audio_file = f"test.{ext}" audio_path = self.get_temp_path(audio_file) - archive_path = self.get_temp_path('archive.tar.gz') + archive_path = self.get_temp_path("archive.tar.gz") - sox_utils.gen_audio_file( - audio_path, sample_rate, num_channels=2, **kwargs) + sox_utils.gen_audio_file(audio_path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(audio_path) - with tarfile.TarFile(archive_path, 'w') as tarobj: + with tarfile.TarFile(archive_path, "w") as tarobj: tarobj.add(audio_path, arcname=audio_file) - with tarfile.TarFile(archive_path, 'r') as tarobj: + with tarfile.TarFile(archive_path, "r") as tarobj: fileobj = tarobj.extractfile(audio_file) found, sr = sox_io_backend.load(fileobj, format=format_) @@ -475,30 +560,31 @@ def test_tarfile(self, ext, kwargs): @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoModule("requests") class TestFileObjectHttp(HttpServerMixin, PytorchTestCase): - @parameterized.expand([ - ('wav', {'bit_depth': 16}), - ('wav', {'bit_depth': 24}), - ('wav', {'bit_depth': 32}), - ('mp3', {'compression': 128}), - ('mp3', {'compression': 320}), - ('flac', {'compression': 0}), - ('flac', {'compression': 5}), - ('flac', {'compression': 8}), - ('vorbis', {'compression': -1}), - ('vorbis', {'compression': 10}), - ('amb', {}), - ]) + @parameterized.expand( + [ + ("wav", {"bit_depth": 16}), + ("wav", {"bit_depth": 24}), + ("wav", {"bit_depth": 32}), + ("mp3", {"compression": 128}), + ("mp3", {"compression": 320}), + ("flac", {"compression": 0}), + ("flac", {"compression": 5}), + ("flac", {"compression": 8}), + ("vorbis", {"compression": -1}), + ("vorbis", {"compression": 10}), + ("amb", {}), + ] + ) def test_requests(self, ext, kwargs): sample_rate = 16000 - format_ = ext if ext in ['mp3'] else None - audio_file = f'test.{ext}' + format_ = ext if ext in ["mp3"] else None + audio_file = f"test.{ext}" audio_path = self.get_temp_path(audio_file) - sox_utils.gen_audio_file( - audio_path, sample_rate, num_channels=2, **kwargs) + sox_utils.gen_audio_file(audio_path, sample_rate, num_channels=2, **kwargs) expected, _ = sox_io_backend.load(audio_path) url = self.get_url(audio_file) @@ -508,17 +594,22 @@ def test_requests(self, ext, kwargs): assert sr == sample_rate self.assertEqual(expected, found) - @parameterized.expand(list(itertools.product( - [0, 1, 10, 100, 1000], - [-1, 1, 10, 100, 1000], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [0, 1, 10, 100, 1000], + [-1, 1, 10, 100, 1000], + ) + ), + name_func=name_func, + ) def test_frame(self, frame_offset, num_frames): """num_frames and frame_offset correctly specify the region of data""" sample_rate = 8000 - audio_file = 'test.wav' + audio_file = "test.wav" audio_path = self.get_temp_path(audio_file) - original = get_wav_data('float32', num_channels=2) + original = get_wav_data("float32", num_channels=2) save_wav(audio_path, original, sample_rate) frame_end = None if num_frames == -1 else frame_offset + num_frames expected = original[:, frame_offset:frame_end] diff --git a/test/torchaudio_unittest/backend/sox_io/roundtrip_test.py b/test/torchaudio_unittest/backend/sox_io/roundtrip_test.py index 32c920eea1..183d2d626b 100644 --- a/test/torchaudio_unittest/backend/sox_io/roundtrip_test.py +++ b/test/torchaudio_unittest/backend/sox_io/roundtrip_test.py @@ -1,8 +1,7 @@ import itertools -from torchaudio.backend import sox_io_backend from parameterized import parameterized - +from torchaudio.backend import sox_io_backend from torchaudio_unittest.common_utils import ( TempDirMixin, PytorchTestCase, @@ -10,44 +9,56 @@ skipIfNoSox, get_wav_data, ) + from .common import ( name_func, get_enc_params, ) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestRoundTripIO(TempDirMixin, PytorchTestCase): """save/load round trip should not degrade data for lossless formats""" - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_wav(self, dtype, sample_rate, num_channels): """save/load round trip should not degrade data for wav formats""" original = get_wav_data(dtype, num_channels, normalize=False) enc, bps = get_enc_params(dtype) data = original for i in range(10): - path = self.get_temp_path(f'{i}.wav') + path = self.get_temp_path(f"{i}.wav") sox_io_backend.save(path, data, sample_rate, encoding=enc, bits_per_sample=bps) data, sr = sox_io_backend.load(path, normalize=False) assert sr == sample_rate self.assertEqual(original, data) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_flac(self, sample_rate, num_channels, compression_level): """save/load round trip should not degrade data for flac formats""" - original = get_wav_data('float32', num_channels) + original = get_wav_data("float32", num_channels) data = original for i in range(10): - path = self.get_temp_path(f'{i}.flac') + path = self.get_temp_path(f"{i}.flac") sox_io_backend.save(path, data, sample_rate, compression=compression_level) data, sr = sox_io_backend.load(path) assert sr == sample_rate diff --git a/test/torchaudio_unittest/backend/sox_io/save_test.py b/test/torchaudio_unittest/backend/sox_io/save_test.py index 9592e78d2d..8597f376c7 100644 --- a/test/torchaudio_unittest/backend/sox_io/save_test.py +++ b/test/torchaudio_unittest/backend/sox_io/save_test.py @@ -3,9 +3,8 @@ import unittest import torch -from torchaudio.backend import sox_io_backend from parameterized import parameterized - +from torchaudio.backend import sox_io_backend from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -18,6 +17,7 @@ sox_utils, nested_params, ) + from .common import ( name_func, get_enc_params, @@ -26,28 +26,28 @@ def _get_sox_encoding(encoding): encodings = { - 'PCM_F': 'floating-point', - 'PCM_S': 'signed-integer', - 'PCM_U': 'unsigned-integer', - 'ULAW': 'u-law', - 'ALAW': 'a-law', + "PCM_F": "floating-point", + "PCM_S": "signed-integer", + "PCM_U": "unsigned-integer", + "ULAW": "u-law", + "ALAW": "a-law", } return encodings.get(encoding) class SaveTestBase(TempDirMixin, TorchaudioTestCase): def assert_save_consistency( - self, - format: str, - *, - compression: float = None, - encoding: str = None, - bits_per_sample: int = None, - sample_rate: float = 8000, - num_channels: int = 2, - num_frames: float = 3 * 8000, - src_dtype: str = 'int32', - test_mode: str = "path", + self, + format: str, + *, + compression: float = None, + encoding: str = None, + bits_per_sample: int = None, + sample_rate: float = 8000, + num_channels: int = 2, + num_frames: float = 3 * 8000, + src_dtype: str = "int32", + test_mode: str = "path", ): """`save` function produces file that is comparable with `sox` command @@ -86,14 +86,14 @@ def assert_save_consistency( tensor -------> compare <--------- tensor """ - cmp_encoding = 'floating-point' + cmp_encoding = "floating-point" cmp_bit_depth = 32 - src_path = self.get_temp_path('1.source.wav') - tgt_path = self.get_temp_path(f'2.1.torchaudio.{format}') - tst_path = self.get_temp_path('2.2.result.wav') - sox_path = self.get_temp_path(f'3.1.sox.{format}') - ref_path = self.get_temp_path('3.2.ref.wav') + src_path = self.get_temp_path("1.source.wav") + tgt_path = self.get_temp_path(f"2.1.torchaudio.{format}") + tst_path = self.get_temp_path("2.2.result.wav") + sox_path = self.get_temp_path(f"3.1.sox.{format}") + ref_path = self.get_temp_path("3.2.ref.wav") # 1. Generate original wav data = get_wav_data(src_dtype, num_channels, normalize=False, num_frames=num_frames) @@ -103,78 +103,84 @@ def assert_save_consistency( data = load_wav(src_path, normalize=False)[0] if test_mode == "path": sox_io_backend.save( - tgt_path, data, sample_rate, - compression=compression, encoding=encoding, bits_per_sample=bits_per_sample) + tgt_path, data, sample_rate, compression=compression, encoding=encoding, bits_per_sample=bits_per_sample + ) elif test_mode == "fileobj": - with open(tgt_path, 'bw') as file_: + with open(tgt_path, "bw") as file_: sox_io_backend.save( - file_, data, sample_rate, - format=format, compression=compression, - encoding=encoding, bits_per_sample=bits_per_sample) + file_, + data, + sample_rate, + format=format, + compression=compression, + encoding=encoding, + bits_per_sample=bits_per_sample, + ) elif test_mode == "bytesio": file_ = io.BytesIO() sox_io_backend.save( - file_, data, sample_rate, - format=format, compression=compression, - encoding=encoding, bits_per_sample=bits_per_sample) + file_, + data, + sample_rate, + format=format, + compression=compression, + encoding=encoding, + bits_per_sample=bits_per_sample, + ) file_.seek(0) - with open(tgt_path, 'bw') as f: + with open(tgt_path, "bw") as f: f.write(file_.read()) else: raise ValueError(f"Unexpected test mode: {test_mode}") # 2.2. Convert the target format to wav with sox - sox_utils.convert_audio_file( - tgt_path, tst_path, encoding=cmp_encoding, bit_depth=cmp_bit_depth) + sox_utils.convert_audio_file(tgt_path, tst_path, encoding=cmp_encoding, bit_depth=cmp_bit_depth) # 2.3. Load with SciPy found = load_wav(tst_path, normalize=False)[0] # 3.1. Convert the original wav to target format with sox sox_encoding = _get_sox_encoding(encoding) sox_utils.convert_audio_file( - src_path, sox_path, - compression=compression, encoding=sox_encoding, bit_depth=bits_per_sample) + src_path, sox_path, compression=compression, encoding=sox_encoding, bit_depth=bits_per_sample + ) # 3.2. Convert the target format to wav with sox - sox_utils.convert_audio_file( - sox_path, ref_path, encoding=cmp_encoding, bit_depth=cmp_bit_depth) + sox_utils.convert_audio_file(sox_path, ref_path, encoding=cmp_encoding, bit_depth=cmp_bit_depth) # 3.3. Load with SciPy expected = load_wav(ref_path, normalize=False)[0] self.assertEqual(found, expected) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class SaveTest(SaveTestBase): @nested_params( ["path", "fileobj", "bytesio"], [ - ('PCM_U', 8), - ('PCM_S', 16), - ('PCM_S', 32), - ('PCM_F', 32), - ('PCM_F', 64), - ('ULAW', 8), - ('ALAW', 8), + ("PCM_U", 8), + ("PCM_S", 16), + ("PCM_S", 32), + ("PCM_F", 32), + ("PCM_F", 64), + ("ULAW", 8), + ("ALAW", 8), ], ) def test_save_wav(self, test_mode, enc_params): encoding, bits_per_sample = enc_params - self.assert_save_consistency( - "wav", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) + self.assert_save_consistency("wav", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], [ - ('float32', ), - ('int32', ), - ('int16', ), - ('uint8', ), + ("float32",), + ("int32",), + ("int16",), + ("uint8",), ], ) def test_save_wav_dtype(self, test_mode, params): - dtype, = params - self.assert_save_consistency( - "wav", src_dtype=dtype, test_mode=test_mode) + (dtype,) = params + self.assert_save_consistency("wav", src_dtype=dtype, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], @@ -197,10 +203,9 @@ def test_save_mp3(self, test_mode, bit_rate): if test_mode in ["fileobj", "bytesio"]: if bit_rate is not None and bit_rate < 1: raise unittest.SkipTest( - "mp3 format with variable bit rate is known to " - "not yield the exact same result as sox command.") - self.assert_save_consistency( - "mp3", compression=bit_rate, test_mode=test_mode) + "mp3 format with variable bit rate is known to " "not yield the exact same result as sox command." + ) + self.assert_save_consistency("mp3", compression=bit_rate, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], @@ -220,8 +225,8 @@ def test_save_mp3(self, test_mode, bit_rate): ) def test_save_flac(self, test_mode, bits_per_sample, compression_level): self.assert_save_consistency( - "flac", compression=compression_level, - bits_per_sample=bits_per_sample, test_mode=test_mode) + "flac", compression=compression_level, bits_per_sample=bits_per_sample, test_mode=test_mode + ) @nested_params( ["path", "fileobj", "bytesio"], @@ -244,45 +249,78 @@ def test_save_htk(self, test_mode): ], ) def test_save_vorbis(self, test_mode, quality_level): - self.assert_save_consistency( - "vorbis", compression=quality_level, test_mode=test_mode) + self.assert_save_consistency("vorbis", compression=quality_level, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], [ - ('PCM_S', 8, ), - ('PCM_S', 16, ), - ('PCM_S', 24, ), - ('PCM_S', 32, ), - ('ULAW', 8), - ('ALAW', 8), - ('ALAW', 16), - ('ALAW', 24), - ('ALAW', 32), + ( + "PCM_S", + 8, + ), + ( + "PCM_S", + 16, + ), + ( + "PCM_S", + 24, + ), + ( + "PCM_S", + 32, + ), + ("ULAW", 8), + ("ALAW", 8), + ("ALAW", 16), + ("ALAW", 24), + ("ALAW", 32), ], ) def test_save_sphere(self, test_mode, enc_params): encoding, bits_per_sample = enc_params - self.assert_save_consistency( - "sph", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) + self.assert_save_consistency("sph", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], [ - ('PCM_U', 8, ), - ('PCM_S', 16, ), - ('PCM_S', 24, ), - ('PCM_S', 32, ), - ('PCM_F', 32, ), - ('PCM_F', 64, ), - ('ULAW', 8, ), - ('ALAW', 8, ), + ( + "PCM_U", + 8, + ), + ( + "PCM_S", + 16, + ), + ( + "PCM_S", + 24, + ), + ( + "PCM_S", + 32, + ), + ( + "PCM_F", + 32, + ), + ( + "PCM_F", + 64, + ), + ( + "ULAW", + 8, + ), + ( + "ALAW", + 8, + ), ], ) def test_save_amb(self, test_mode, enc_params): encoding, bits_per_sample = enc_params - self.assert_save_consistency( - "amb", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) + self.assert_save_consistency("amb", encoding=encoding, bits_per_sample=bits_per_sample, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], @@ -299,90 +337,94 @@ def test_save_amb(self, test_mode, enc_params): ], ) def test_save_amr_nb(self, test_mode, bit_rate): - self.assert_save_consistency( - "amr-nb", compression=bit_rate, num_channels=1, test_mode=test_mode) + self.assert_save_consistency("amr-nb", compression=bit_rate, num_channels=1, test_mode=test_mode) @nested_params( ["path", "fileobj", "bytesio"], ) def test_save_gsm(self, test_mode): - self.assert_save_consistency( - "gsm", num_channels=1, test_mode=test_mode) - with self.assertRaises( - RuntimeError, msg="gsm format only supports single channel audio."): - self.assert_save_consistency( - "gsm", num_channels=2, test_mode=test_mode) - with self.assertRaises( - RuntimeError, msg="gsm format only supports a sampling rate of 8kHz."): - self.assert_save_consistency( - "gsm", sample_rate=16000, test_mode=test_mode) - - @parameterized.expand([ - ("wav", "PCM_S", 16), - ("mp3", ), - ("flac", ), - ("vorbis", ), - ("sph", "PCM_S", 16), - ("amr-nb", ), - ("amb", "PCM_S", 16), - ], name_func=name_func) + self.assert_save_consistency("gsm", num_channels=1, test_mode=test_mode) + with self.assertRaises(RuntimeError, msg="gsm format only supports single channel audio."): + self.assert_save_consistency("gsm", num_channels=2, test_mode=test_mode) + with self.assertRaises(RuntimeError, msg="gsm format only supports a sampling rate of 8kHz."): + self.assert_save_consistency("gsm", sample_rate=16000, test_mode=test_mode) + + @parameterized.expand( + [ + ("wav", "PCM_S", 16), + ("mp3",), + ("flac",), + ("vorbis",), + ("sph", "PCM_S", 16), + ("amr-nb",), + ("amb", "PCM_S", 16), + ], + name_func=name_func, + ) def test_save_large(self, format, encoding=None, bits_per_sample=None): """`sox_io_backend.save` can save large files.""" sample_rate = 8000 one_hour = 60 * 60 * sample_rate self.assert_save_consistency( - format, num_channels=1, sample_rate=8000, num_frames=one_hour, - encoding=encoding, bits_per_sample=bits_per_sample) - - @parameterized.expand([ - (32, ), - (64, ), - (128, ), - (256, ), - ], name_func=name_func) + format, + num_channels=1, + sample_rate=8000, + num_frames=one_hour, + encoding=encoding, + bits_per_sample=bits_per_sample, + ) + + @parameterized.expand( + [ + (32,), + (64,), + (128,), + (256,), + ], + name_func=name_func, + ) def test_save_multi_channels(self, num_channels): """`sox_io_backend.save` can save audio with many channels""" - self.assert_save_consistency( - "wav", encoding="PCM_S", bits_per_sample=16, - num_channels=num_channels) + self.assert_save_consistency("wav", encoding="PCM_S", bits_per_sample=16, num_channels=num_channels) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestSaveParams(TempDirMixin, PytorchTestCase): """Test the correctness of optional parameters of `sox_io_backend.save`""" - @parameterized.expand([(True, ), (False, )], name_func=name_func) + + @parameterized.expand([(True,), (False,)], name_func=name_func) def test_save_channels_first(self, channels_first): """channels_first swaps axes""" - path = self.get_temp_path('data.wav') - data = get_wav_data( - 'int16', 2, channels_first=channels_first, normalize=False) - sox_io_backend.save( - path, data, 8000, channels_first=channels_first) + path = self.get_temp_path("data.wav") + data = get_wav_data("int16", 2, channels_first=channels_first, normalize=False) + sox_io_backend.save(path, data, 8000, channels_first=channels_first) found = load_wav(path, normalize=False)[0] expected = data if channels_first else data.transpose(1, 0) self.assertEqual(found, expected) - @parameterized.expand([ - 'float32', 'int32', 'int16', 'uint8' - ], name_func=name_func) + @parameterized.expand(["float32", "int32", "int16", "uint8"], name_func=name_func) def test_save_noncontiguous(self, dtype): """Noncontiguous tensors are saved correctly""" - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") enc, bps = get_enc_params(dtype) expected = get_wav_data(dtype, 4, normalize=False)[::2, ::2] assert not expected.is_contiguous() - sox_io_backend.save( - path, expected, 8000, encoding=enc, bits_per_sample=bps) + sox_io_backend.save(path, expected, 8000, encoding=enc, bits_per_sample=bps) found = load_wav(path, normalize=False)[0] self.assertEqual(found, expected) - @parameterized.expand([ - 'float32', 'int32', 'int16', 'uint8', - ]) + @parameterized.expand( + [ + "float32", + "int32", + "int16", + "uint8", + ] + ) def test_save_tensor_preserve(self, dtype): """save function should not alter Tensor""" - path = self.get_temp_path('data.wav') + path = self.get_temp_path("data.wav") expected = get_wav_data(dtype, 4, normalize=False)[::2, ::2] data = expected.clone() diff --git a/test/torchaudio_unittest/backend/sox_io/smoke_test.py b/test/torchaudio_unittest/backend/sox_io/smoke_test.py index 656cfd9fbe..cc0f7b5f26 100644 --- a/test/torchaudio_unittest/backend/sox_io/smoke_test.py +++ b/test/torchaudio_unittest/backend/sox_io/smoke_test.py @@ -2,25 +2,24 @@ import itertools import unittest -from torchaudio.utils import sox_utils -from torchaudio.backend import sox_io_backend -from torchaudio._internal.module_utils import is_sox_available from parameterized import parameterized - +from torchaudio._internal.module_utils import is_sox_available +from torchaudio.backend import sox_io_backend +from torchaudio.utils import sox_utils from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, skipIfNoSox, get_wav_data, ) + from .common import name_func skipIfNoMP3 = unittest.skipIf( - not is_sox_available() or - 'mp3' not in sox_utils.list_read_formats() or - 'mp3' not in sox_utils.list_write_formats(), - '"sox_io" backend does not support MP3') + not is_sox_available() or "mp3" not in sox_utils.list_read_formats() or "mp3" not in sox_utils.list_write_formats(), + '"sox_io" backend does not support MP3', +) @skipIfNoSox @@ -33,10 +32,11 @@ class SmokeTest(TempDirMixin, TorchaudioTestCase): This test suite should be able to run without any additional tools (such as sox command), however without such tools, the correctness of each function cannot be verified. """ - def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dtype='float32'): + + def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dtype="float32"): duration = 1 num_frames = sample_rate * duration - path = self.get_temp_path(f'test.{ext}') + path = self.get_temp_path(f"test.{ext}") original = get_wav_data(dtype, num_channels, normalize=False, num_frames=num_frames) # 1. run save @@ -50,42 +50,60 @@ def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dt assert sr == sample_rate assert loaded.shape[0] == num_channels - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_wav(self, dtype, sample_rate, num_channels): """Run smoke test on wav format""" - self.run_smoke_test('wav', sample_rate, num_channels, dtype=dtype) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-4.2, -0.2, 0, 0.2, 96, 128, 160, 192, 224, 256, 320], - ))) + self.run_smoke_test("wav", sample_rate, num_channels, dtype=dtype) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-4.2, -0.2, 0, 0.2, 96, 128, 160, 192, 224, 256, 320], + ) + ) + ) @skipIfNoMP3 def test_mp3(self, sample_rate, num_channels, bit_rate): """Run smoke test on mp3 format""" - self.run_smoke_test('mp3', sample_rate, num_channels, compression=bit_rate) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-1, 0, 1, 2, 3, 3.6, 5, 10], - ))) + self.run_smoke_test("mp3", sample_rate, num_channels, compression=bit_rate) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-1, 0, 1, 2, 3, 3.6, 5, 10], + ) + ) + ) def test_vorbis(self, sample_rate, num_channels, quality_level): """Run smoke test on vorbis format""" - self.run_smoke_test('vorbis', sample_rate, num_channels, compression=quality_level) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + self.run_smoke_test("vorbis", sample_rate, num_channels, compression=quality_level) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_flac(self, sample_rate, num_channels, compression_level): """Run smoke test on flac format""" - self.run_smoke_test('flac', sample_rate, num_channels, compression=compression_level) + self.run_smoke_test("flac", sample_rate, num_channels, compression=compression_level) @skipIfNoSox @@ -98,7 +116,8 @@ class SmokeTestFileObj(TorchaudioTestCase): This test suite should be able to run without any additional tools (such as sox command), however without such tools, the correctness of each function cannot be verified. """ - def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dtype='float32'): + + def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dtype="float32"): duration = 1 num_frames = sample_rate * duration original = get_wav_data(dtype, num_channels, normalize=False, num_frames=num_frames) @@ -117,39 +136,57 @@ def run_smoke_test(self, ext, sample_rate, num_channels, *, compression=None, dt assert sr == sample_rate assert loaded.shape[0] == num_channels - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_wav(self, dtype, sample_rate, num_channels): """Run smoke test on wav format""" - self.run_smoke_test('wav', sample_rate, num_channels, dtype=dtype) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-4.2, -0.2, 0, 0.2, 96, 128, 160, 192, 224, 256, 320], - ))) + self.run_smoke_test("wav", sample_rate, num_channels, dtype=dtype) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-4.2, -0.2, 0, 0.2, 96, 128, 160, 192, 224, 256, 320], + ) + ) + ) @skipIfNoMP3 def test_mp3(self, sample_rate, num_channels, bit_rate): """Run smoke test on mp3 format""" - self.run_smoke_test('mp3', sample_rate, num_channels, compression=bit_rate) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - [-1, 0, 1, 2, 3, 3.6, 5, 10], - ))) + self.run_smoke_test("mp3", sample_rate, num_channels, compression=bit_rate) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + [-1, 0, 1, 2, 3, 3.6, 5, 10], + ) + ) + ) def test_vorbis(self, sample_rate, num_channels, quality_level): """Run smoke test on vorbis format""" - self.run_smoke_test('vorbis', sample_rate, num_channels, compression=quality_level) - - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + self.run_smoke_test("vorbis", sample_rate, num_channels, compression=quality_level) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_flac(self, sample_rate, num_channels, compression_level): """Run smoke test on flac format""" - self.run_smoke_test('flac', sample_rate, num_channels, compression=compression_level) + self.run_smoke_test("flac", sample_rate, num_channels, compression=compression_level) diff --git a/test/torchaudio_unittest/backend/sox_io/torchscript_test.py b/test/torchaudio_unittest/backend/sox_io/torchscript_test.py index 122f4bc0d0..21885a5113 100644 --- a/test/torchaudio_unittest/backend/sox_io/torchscript_test.py +++ b/test/torchaudio_unittest/backend/sox_io/torchscript_test.py @@ -4,7 +4,6 @@ import torch import torchaudio from parameterized import parameterized - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -16,6 +15,7 @@ sox_utils, torch_script, ) + from .common import ( name_func, get_enc_params, @@ -27,38 +27,41 @@ def py_info_func(filepath: str) -> torchaudio.backend.sox_io_backend.AudioMetaDa def py_load_func(filepath: str, normalize: bool, channels_first: bool): - return torchaudio.load( - filepath, normalize=normalize, channels_first=channels_first) + return torchaudio.load(filepath, normalize=normalize, channels_first=channels_first) def py_save_func( - filepath: str, - tensor: torch.Tensor, - sample_rate: int, - channels_first: bool = True, - compression: Optional[float] = None, - encoding: Optional[str] = None, - bits_per_sample: Optional[int] = None, + filepath: str, + tensor: torch.Tensor, + sample_rate: int, + channels_first: bool = True, + compression: Optional[float] = None, + encoding: Optional[str] = None, + bits_per_sample: Optional[int] = None, ): - torchaudio.save( - filepath, tensor, sample_rate, channels_first, - compression, None, encoding, bits_per_sample) + torchaudio.save(filepath, tensor, sample_rate, channels_first, compression, None, encoding, bits_per_sample) -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class SoxIO(TempDirMixin, TorchaudioTestCase): """TorchScript-ability Test suite for `sox_io_backend`""" - backend = 'sox_io' - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + backend = "sox_io" + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_info_wav(self, dtype, sample_rate, num_channels): """`sox_io_backend.info` is torchscript-able and returns the same result""" - audio_path = self.get_temp_path(f'{dtype}_{sample_rate}_{num_channels}.wav') + audio_path = self.get_temp_path(f"{dtype}_{sample_rate}_{num_channels}.wav") data = get_wav_data(dtype, num_channels, normalize=False, num_frames=1 * sample_rate) save_wav(audio_path, data, sample_rate) @@ -71,40 +74,48 @@ def test_info_wav(self, dtype, sample_rate, num_channels): assert py_info.num_frames == ts_info.num_frames assert py_info.num_channels == ts_info.num_channels - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - [False, True], - [False, True], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + [False, True], + [False, True], + ) + ), + name_func=name_func, + ) def test_load_wav(self, dtype, sample_rate, num_channels, normalize, channels_first): """`sox_io_backend.load` is torchscript-able and returns the same result""" - audio_path = self.get_temp_path(f'test_load_{dtype}_{sample_rate}_{num_channels}_{normalize}.wav') + audio_path = self.get_temp_path(f"test_load_{dtype}_{sample_rate}_{num_channels}_{normalize}.wav") data = get_wav_data(dtype, num_channels, normalize=False, num_frames=1 * sample_rate) save_wav(audio_path, data, sample_rate) ts_load_func = torch_script(py_load_func) - py_data, py_sr = py_load_func( - audio_path, normalize=normalize, channels_first=channels_first) - ts_data, ts_sr = ts_load_func( - audio_path, normalize=normalize, channels_first=channels_first) + py_data, py_sr = py_load_func(audio_path, normalize=normalize, channels_first=channels_first) + ts_data, ts_sr = ts_load_func(audio_path, normalize=normalize, channels_first=channels_first) self.assertEqual(py_sr, ts_sr) self.assertEqual(py_data, ts_data) - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=name_func, + ) def test_save_wav(self, dtype, sample_rate, num_channels): ts_save_func = torch_script(py_save_func) expected = get_wav_data(dtype, num_channels, normalize=False) - py_path = self.get_temp_path(f'test_save_py_{dtype}_{sample_rate}_{num_channels}.wav') - ts_path = self.get_temp_path(f'test_save_ts_{dtype}_{sample_rate}_{num_channels}.wav') + py_path = self.get_temp_path(f"test_save_py_{dtype}_{sample_rate}_{num_channels}.wav") + ts_path = self.get_temp_path(f"test_save_ts_{dtype}_{sample_rate}_{num_channels}.wav") enc, bps = get_enc_params(dtype) py_save_func(py_path, expected, sample_rate, True, None, enc, bps) @@ -118,24 +129,29 @@ def test_save_wav(self, dtype, sample_rate, num_channels): self.assertEqual(expected, py_data) self.assertEqual(expected, ts_data) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - list(range(9)), - )), name_func=name_func) + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + list(range(9)), + ) + ), + name_func=name_func, + ) def test_save_flac(self, sample_rate, num_channels, compression_level): ts_save_func = torch_script(py_save_func) - expected = get_wav_data('float32', num_channels) - py_path = self.get_temp_path(f'test_save_py_{sample_rate}_{num_channels}_{compression_level}.flac') - ts_path = self.get_temp_path(f'test_save_ts_{sample_rate}_{num_channels}_{compression_level}.flac') + expected = get_wav_data("float32", num_channels) + py_path = self.get_temp_path(f"test_save_py_{sample_rate}_{num_channels}_{compression_level}.flac") + ts_path = self.get_temp_path(f"test_save_ts_{sample_rate}_{num_channels}_{compression_level}.flac") py_save_func(py_path, expected, sample_rate, True, compression_level, None, None) ts_save_func(ts_path, expected, sample_rate, True, compression_level, None, None) # converting to 32 bit because flac file has 24 bit depth which scipy cannot handle. - py_path_wav = f'{py_path}.wav' - ts_path_wav = f'{ts_path}.wav' + py_path_wav = f"{py_path}.wav" + ts_path_wav = f"{ts_path}.wav" sox_utils.convert_audio_file(py_path, py_path_wav, bit_depth=32) sox_utils.convert_audio_file(ts_path, ts_path_wav, bit_depth=32) diff --git a/test/torchaudio_unittest/backend/utils_test.py b/test/torchaudio_unittest/backend/utils_test.py index c89f1e9788..5708fb56de 100644 --- a/test/torchaudio_unittest/backend/utils_test.py +++ b/test/torchaudio_unittest/backend/utils_test.py @@ -1,10 +1,10 @@ import torchaudio - from torchaudio_unittest import common_utils class BackendSwitchMixin: """Test set/get_audio_backend works""" + backend = None backend_module = None @@ -26,11 +26,11 @@ class TestBackendSwitch_NoBackend(BackendSwitchMixin, common_utils.TorchaudioTes @common_utils.skipIfNoSox class TestBackendSwitch_SoXIO(BackendSwitchMixin, common_utils.TorchaudioTestCase): - backend = 'sox_io' + backend = "sox_io" backend_module = torchaudio.backend.sox_io_backend -@common_utils.skipIfNoModule('soundfile') +@common_utils.skipIfNoModule("soundfile") class TestBackendSwitch_soundfile(BackendSwitchMixin, common_utils.TorchaudioTestCase): - backend = 'soundfile' + backend = "soundfile" backend_module = torchaudio.backend.soundfile_backend diff --git a/test/torchaudio_unittest/common_utils/__init__.py b/test/torchaudio_unittest/common_utils/__init__.py index 48d53ba89f..2dedf52f02 100644 --- a/test/torchaudio_unittest/common_utils/__init__.py +++ b/test/torchaudio_unittest/common_utils/__init__.py @@ -1,9 +1,3 @@ -from .data_utils import ( - get_asset_path, - get_whitenoise, - get_sinusoid, - get_spectrogram, -) from .backend_utils import ( set_audio_backend, ) @@ -21,43 +15,46 @@ skipIfRocm, skipIfNoQengine, ) +from .data_utils import ( + get_asset_path, + get_whitenoise, + get_sinusoid, + get_spectrogram, +) +from .func_utils import torch_script +from .parameterized_utils import load_params, nested_params from .wav_utils import ( get_wav_data, normalize_wav, load_wav, save_wav, ) -from .parameterized_utils import ( - load_params, - nested_params -) -from .func_utils import torch_script __all__ = [ - 'get_asset_path', - 'get_whitenoise', - 'get_sinusoid', - 'get_spectrogram', - 'set_audio_backend', - 'TempDirMixin', - 'HttpServerMixin', - 'TestBaseMixin', - 'PytorchTestCase', - 'TorchaudioTestCase', - 'skipIfNoCuda', - 'skipIfNoExec', - 'skipIfNoModule', - 'skipIfNoKaldi', - 'skipIfNoSox', - 'skipIfNoSoxBackend', - 'skipIfRocm', - 'skipIfNoQengine', - 'get_wav_data', - 'normalize_wav', - 'load_wav', - 'save_wav', - 'load_params', - 'nested_params', - 'torch_script', + "get_asset_path", + "get_whitenoise", + "get_sinusoid", + "get_spectrogram", + "set_audio_backend", + "TempDirMixin", + "HttpServerMixin", + "TestBaseMixin", + "PytorchTestCase", + "TorchaudioTestCase", + "skipIfNoCuda", + "skipIfNoExec", + "skipIfNoModule", + "skipIfNoKaldi", + "skipIfNoSox", + "skipIfNoSoxBackend", + "skipIfRocm", + "skipIfNoQengine", + "get_wav_data", + "normalize_wav", + "load_wav", + "save_wav", + "load_params", + "nested_params", + "torch_script", ] diff --git a/test/torchaudio_unittest/common_utils/backend_utils.py b/test/torchaudio_unittest/common_utils/backend_utils.py index 84dd73ed2e..64294a158e 100644 --- a/test/torchaudio_unittest/common_utils/backend_utils.py +++ b/test/torchaudio_unittest/common_utils/backend_utils.py @@ -6,15 +6,15 @@ def set_audio_backend(backend): """Allow additional backend value, 'default'""" backends = torchaudio.list_audio_backends() - if backend == 'soundfile': - be = 'soundfile' - elif backend == 'default': - if 'sox_io' in backends: - be = 'sox_io' - elif 'soundfile' in backends: - be = 'soundfile' + if backend == "soundfile": + be = "soundfile" + elif backend == "default": + if "sox_io" in backends: + be = "sox_io" + elif "soundfile" in backends: + be = "soundfile" else: - raise unittest.SkipTest('No default backend available') + raise unittest.SkipTest("No default backend available") else: be = backend diff --git a/test/torchaudio_unittest/common_utils/case_utils.py b/test/torchaudio_unittest/common_utils/case_utils.py index 140291a8a2..840227c75c 100644 --- a/test/torchaudio_unittest/common_utils/case_utils.py +++ b/test/torchaudio_unittest/common_utils/case_utils.py @@ -1,5 +1,5 @@ -import shutil import os.path +import shutil import subprocess import tempfile import time @@ -7,24 +7,21 @@ import torch from torch.testing._internal.common_utils import TestCase as PytorchTestCase -from torchaudio._internal.module_utils import ( - is_module_available, - is_sox_available, - is_kaldi_available -) +from torchaudio._internal.module_utils import is_module_available, is_sox_available, is_kaldi_available from .backend_utils import set_audio_backend class TempDirMixin: """Mixin to provide easy access to temp dir""" + temp_dir_ = None @classmethod def get_base_temp_dir(cls): # If TORCHAUDIO_TEST_TEMP_DIR is set, use it instead of temporary directory. # this is handy for debugging. - key = 'TORCHAUDIO_TEST_TEMP_DIR' + key = "TORCHAUDIO_TEST_TEMP_DIR" if key in os.environ: return os.environ[key] if cls.temp_dir_ is None: @@ -51,6 +48,7 @@ class HttpServerMixin(TempDirMixin): This class creates temporary directory and serve the directory as HTTP service. The server is up through the execution of all the test suite defined under the subclass. """ + _proc = None _port = 8000 @@ -58,9 +56,8 @@ class HttpServerMixin(TempDirMixin): def setUpClass(cls): super().setUpClass() cls._proc = subprocess.Popen( - ['python', '-m', 'http.server', f'{cls._port}'], - cwd=cls.get_base_temp_dir(), - stderr=subprocess.DEVNULL) # Disable server-side error log because it is confusing + ["python", "-m", "http.server", f"{cls._port}"], cwd=cls.get_base_temp_dir(), stderr=subprocess.DEVNULL + ) # Disable server-side error log because it is confusing time.sleep(2.0) @classmethod @@ -74,6 +71,7 @@ def get_url(self, *route): class TestBaseMixin: """Mixin to provide consistent way to define device/dtype/backend aware TestCase""" + dtype = None device = None backend = None @@ -84,11 +82,11 @@ def setUp(self): @property def complex_dtype(self): - if self.dtype in ['float32', 'float', torch.float, torch.float32]: + if self.dtype in ["float32", "float", torch.float, torch.float32]: return torch.cfloat - if self.dtype in ['float64', 'double', torch.double, torch.float64]: + if self.dtype in ["float64", "double", torch.double, torch.float64]: return torch.cdouble - raise ValueError(f'No corresponding complex dtype for {self.dtype}') + raise ValueError(f"No corresponding complex dtype for {self.dtype}") class TorchaudioTestCase(TestBaseMixin, PytorchTestCase): @@ -96,7 +94,7 @@ class TorchaudioTestCase(TestBaseMixin, PytorchTestCase): def skipIfNoExec(cmd): - return unittest.skipIf(shutil.which(cmd) is None, f'`{cmd}` is not available') + return unittest.skipIf(shutil.which(cmd) is None, f"`{cmd}` is not available") def skipIfNoModule(module, display_name=None): @@ -107,17 +105,19 @@ def skipIfNoModule(module, display_name=None): def skipIfNoCuda(test_item): if torch.cuda.is_available(): return test_item - force_cuda_test = os.environ.get('TORCHAUDIO_TEST_FORCE_CUDA', '0') - if force_cuda_test not in ['0', '1']: + force_cuda_test = os.environ.get("TORCHAUDIO_TEST_FORCE_CUDA", "0") + if force_cuda_test not in ["0", "1"]: raise ValueError('"TORCHAUDIO_TEST_FORCE_CUDA" must be either "0" or "1".') - if force_cuda_test == '1': + if force_cuda_test == "1": raise RuntimeError('"TORCHAUDIO_TEST_FORCE_CUDA" is set but CUDA is not available.') - return unittest.skip('CUDA is not available.')(test_item) -skipIfNoSox = unittest.skipIf(not is_sox_available(), reason='Sox not available') -skipIfNoKaldi = unittest.skipIf(not is_kaldi_available(), reason='Kaldi not available') -skipIfRocm = unittest.skipIf(os.getenv('TORCHAUDIO_TEST_WITH_ROCM', '0') == '1', - reason="test doesn't currently work on the ROCm stack") + return unittest.skip("CUDA is not available.")(test_item) + + +skipIfNoSox = unittest.skipIf(not is_sox_available(), reason="Sox not available") +skipIfNoKaldi = unittest.skipIf(not is_kaldi_available(), reason="Kaldi not available") +skipIfRocm = unittest.skipIf( + os.getenv("TORCHAUDIO_TEST_WITH_ROCM", "0") == "1", reason="test doesn't currently work on the ROCm stack" +) skipIfNoQengine = unittest.skipIf( - 'fbgemm' not in torch.backends.quantized.supported_engines, - reason="`fbgemm` is not available." + "fbgemm" not in torch.backends.quantized.supported_engines, reason="`fbgemm` is not available." ) diff --git a/test/torchaudio_unittest/common_utils/data_utils.py b/test/torchaudio_unittest/common_utils/data_utils.py index 0c97cc1ce3..7db24ea2bb 100644 --- a/test/torchaudio_unittest/common_utils/data_utils.py +++ b/test/torchaudio_unittest/common_utils/data_utils.py @@ -4,13 +4,12 @@ import torch -_TEST_DIR_PATH = os.path.realpath( - os.path.join(os.path.dirname(__file__), '..')) +_TEST_DIR_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) def get_asset_path(*paths): """Return full path of a test asset""" - return os.path.join(_TEST_DIR_PATH, 'assets', *paths) + return os.path.join(_TEST_DIR_PATH, "assets", *paths) def convert_tensor_encoding( @@ -63,13 +62,12 @@ def get_whitenoise( if isinstance(dtype, str): dtype = getattr(torch, dtype) if dtype not in [torch.float64, torch.float32, torch.int32, torch.int16, torch.uint8]: - raise NotImplementedError(f'dtype {dtype} is not supported.') + raise NotImplementedError(f"dtype {dtype} is not supported.") # According to the doc, folking rng on all CUDA devices is slow when there are many CUDA devices, # so we only fork on CPU, generate values and move the data to the given device with torch.random.fork_rng([]): torch.random.manual_seed(seed) - tensor = torch.randn([n_channels, int(sample_rate * duration)], - dtype=torch.float32, device='cpu') + tensor = torch.randn([n_channels, int(sample_rate * duration)], dtype=torch.float32, device="cpu") tensor /= 2.0 tensor *= scale_factor tensor.clamp_(-1.0, 1.0) @@ -116,15 +114,15 @@ def get_sinusoid( def get_spectrogram( - waveform, - *, - n_fft: int = 2048, - hop_length: Optional[int] = None, - win_length: Optional[int] = None, - window: Optional[torch.Tensor] = None, - center: bool = True, - pad_mode: str = 'reflect', - power: Optional[float] = None, + waveform, + *, + n_fft: int = 2048, + hop_length: Optional[int] = None, + win_length: Optional[int] = None, + window: Optional[torch.Tensor] = None, + center: bool = True, + pad_mode: str = "reflect", + power: Optional[float] = None, ): """Generate a spectrogram of the given Tensor @@ -149,7 +147,8 @@ def get_spectrogram( center=center, window=window, pad_mode=pad_mode, - return_complex=True) + return_complex=True, + ) if power is not None: spec = spec.abs() ** power return spec diff --git a/test/torchaudio_unittest/common_utils/func_utils.py b/test/torchaudio_unittest/common_utils/func_utils.py index f8dd5dd43b..4430c9f98d 100644 --- a/test/torchaudio_unittest/common_utils/func_utils.py +++ b/test/torchaudio_unittest/common_utils/func_utils.py @@ -1,4 +1,5 @@ import io + import torch diff --git a/test/torchaudio_unittest/common_utils/kaldi_utils.py b/test/torchaudio_unittest/common_utils/kaldi_utils.py index 71053bd003..42e774744e 100644 --- a/test/torchaudio_unittest/common_utils/kaldi_utils.py +++ b/test/torchaudio_unittest/common_utils/kaldi_utils.py @@ -6,11 +6,11 @@ def convert_args(**kwargs): args = [] for key, value in kwargs.items(): - if key == 'sample_rate': - key = 'sample_frequency' - key = '--' + key.replace('_', '-') + if key == "sample_rate": + key = "sample_frequency" + key = "--" + key.replace("_", "-") value = str(value).lower() if value in [True, False] else str(value) - args.append('%s=%s' % (key, value)) + args.append("%s=%s" % (key, value)) return args @@ -25,14 +25,14 @@ def run_kaldi(command, input_type, input_value): """ import kaldi_io - key = 'foo' + key = "foo" process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - if input_type == 'ark': + if input_type == "ark": kaldi_io.write_mat(process.stdin, input_value.cpu().numpy(), key=key) - elif input_type == 'scp': - process.stdin.write(f'{key} {input_value}'.encode('utf8')) + elif input_type == "scp": + process.stdin.write(f"{key} {input_value}".encode("utf8")) else: - raise NotImplementedError('Unexpected type') + raise NotImplementedError("Unexpected type") process.stdin.close() - result = dict(kaldi_io.read_mat_ark(process.stdout))['foo'] + result = dict(kaldi_io.read_mat_ark(process.stdout))["foo"] return torch.from_numpy(result.copy()) # copy supresses some torch warning diff --git a/test/torchaudio_unittest/common_utils/parameterized_utils.py b/test/torchaudio_unittest/common_utils/parameterized_utils.py index 000dccaf12..36a2c70e7c 100644 --- a/test/torchaudio_unittest/common_utils/parameterized_utils.py +++ b/test/torchaudio_unittest/common_utils/parameterized_utils.py @@ -7,7 +7,7 @@ def load_params(*paths): - with open(get_asset_path(*paths), 'r') as file: + with open(get_asset_path(*paths), "r") as file: return [param(json.loads(line)) for line in file] @@ -20,7 +20,7 @@ def _name_func(func, _, params): strs.append(str(arg)) # sanitize the test name name = "_".join(strs).replace(".", "_") - return f'{func.__name__}_{name}' + return f"{func.__name__}_{name}" def nested_params(*params_set): @@ -39,13 +39,10 @@ def nested_params(*params_set): # Parameters to be nested are given as list of `parameterized.param` if not all(isinstance(p, param) for p in flatten): - raise TypeError( - "When using ``parameterized.param``, " - "all the parameters have to be of the ``param`` type.") + raise TypeError("When using ``parameterized.param``, " "all the parameters have to be of the ``param`` type.") if any(p.args for p in flatten): raise ValueError( - "When using ``parameterized.param``, " - "all the parameters have to be provided as keyword argument." + "When using ``parameterized.param``, " "all the parameters have to be provided as keyword argument." ) args = [param()] for params in params_set: diff --git a/test/torchaudio_unittest/common_utils/psd_utils.py b/test/torchaudio_unittest/common_utils/psd_utils.py index 3ab3354672..49a206cfa5 100644 --- a/test/torchaudio_unittest/common_utils/psd_utils.py +++ b/test/torchaudio_unittest/common_utils/psd_utils.py @@ -5,11 +5,7 @@ def psd_numpy( - X: np.array, - mask: Optional[np.array], - multi_mask: bool = False, - normalize: bool = True, - eps: float = 1e-15 + X: np.array, mask: Optional[np.array], multi_mask: bool = False, normalize: bool = True, eps: float = 1e-15 ) -> np.array: X_conj = np.conj(X) psd_X = np.einsum("...cft,...eft->...ftce", X, X_conj) diff --git a/test/torchaudio_unittest/common_utils/rnnt_utils.py b/test/torchaudio_unittest/common_utils/rnnt_utils.py index 94ea300ef7..12c934d206 100644 --- a/test/torchaudio_unittest/common_utils/rnnt_utils.py +++ b/test/torchaudio_unittest/common_utils/rnnt_utils.py @@ -1,7 +1,8 @@ -import unittest import random -import torch +import unittest + import numpy as np +import torch from torchaudio.functional import rnnt_loss @@ -84,9 +85,7 @@ def compute_beta_one_sequence(log_probs, targets, blank=-1): return beta, cost @staticmethod - def compute_gradients_one_sequence( - log_probs, alpha, beta, targets, blank=-1 - ): + def compute_gradients_one_sequence(log_probs, alpha, beta, targets, blank=-1): max_T, max_U, D = log_probs.shape gradients = np.full(log_probs.shape, float("-inf")) cost = -beta[0, 0] @@ -175,9 +174,7 @@ def forward( def compute_with_numpy_transducer(data): - costs = NumpyTransducerLoss( - blank=data["blank"], - )( + costs = NumpyTransducerLoss(blank=data["blank"],)( logits=data["logits"], logit_lengths=data["logit_lengths"], target_lengths=data["target_lengths"], @@ -254,6 +251,7 @@ def get_B1_T10_U3_D4_data( def grad_hook(grad): logits.saved_grad = grad.clone() + logits.register_hook(grad_hook) data = {} @@ -307,6 +305,7 @@ def get_B1_T2_U3_D5_data(dtype=torch.float32, device=CPU_DEVICE): def grad_hook(grad): logits.saved_grad = grad.clone() + logits.register_hook(grad_hook) targets = torch.tensor([[1, 2]], dtype=torch.int32, device=device) @@ -447,6 +446,7 @@ def get_B2_T4_U3_D3_data(dtype=torch.float32, device=CPU_DEVICE): def grad_hook(grad): logits.saved_grad = grad.clone() + logits.register_hook(grad_hook) targets = torch.tensor([[1, 2], [1, 1]], dtype=torch.int32, device=device) @@ -573,9 +573,7 @@ def get_random_data( max_src_length = torch.max(logit_lengths) max_tgt_length = torch.max(target_lengths) - targets = torch.randint( - low=0, high=D - 1, size=(B, max_tgt_length), dtype=torch.int32, device=device - ) + targets = torch.randint(low=0, high=D - 1, size=(B, max_tgt_length), dtype=torch.int32, device=device) logits = torch.rand( size=(B, max_src_length, max_tgt_length + 1, D), dtype=dtype, @@ -584,6 +582,7 @@ def get_random_data( def grad_hook(grad): logits.saved_grad = grad.clone() + logits.register_hook(grad_hook) return { diff --git a/test/torchaudio_unittest/common_utils/sox_utils.py b/test/torchaudio_unittest/common_utils/sox_utils.py index e72b10e067..6ceae081e4 100644 --- a/test/torchaudio_unittest/common_utils/sox_utils.py +++ b/test/torchaudio_unittest/common_utils/sox_utils.py @@ -1,87 +1,97 @@ -import sys import subprocess +import sys import warnings def get_encoding(dtype): encodings = { - 'float32': 'floating-point', - 'int32': 'signed-integer', - 'int16': 'signed-integer', - 'uint8': 'unsigned-integer', + "float32": "floating-point", + "int32": "signed-integer", + "int16": "signed-integer", + "uint8": "unsigned-integer", } return encodings[dtype] def get_bit_depth(dtype): bit_depths = { - 'float32': 32, - 'int32': 32, - 'int16': 16, - 'uint8': 8, + "float32": 32, + "int32": 32, + "int16": 16, + "uint8": 8, } return bit_depths[dtype] def gen_audio_file( - path, sample_rate, num_channels, - *, encoding=None, bit_depth=None, compression=None, attenuation=None, duration=1, comment_file=None, + path, + sample_rate, + num_channels, + *, + encoding=None, + bit_depth=None, + compression=None, + attenuation=None, + duration=1, + comment_file=None, ): """Generate synthetic audio file with `sox` command.""" - if path.endswith('.wav'): - warnings.warn('Use get_wav_data and save_wav to generate wav file for accurate result.') + if path.endswith(".wav"): + warnings.warn("Use get_wav_data and save_wav to generate wav file for accurate result.") command = [ - 'sox', - '-V3', # verbose - '--no-dither', # disable automatic dithering - '-R', + "sox", + "-V3", # verbose + "--no-dither", # disable automatic dithering + "-R", # -R is supposed to be repeatable, though the implementation looks suspicious # and not setting the seed to a fixed value. # https://fossies.org/dox/sox-14.4.2/sox_8c_source.html # search "sox_globals.repeatable" ] if bit_depth is not None: - command += ['--bits', str(bit_depth)] + command += ["--bits", str(bit_depth)] command += [ - '--rate', str(sample_rate), - '--null', # no input - '--channels', str(num_channels), + "--rate", + str(sample_rate), + "--null", # no input + "--channels", + str(num_channels), ] if compression is not None: - command += ['--compression', str(compression)] + command += ["--compression", str(compression)] if bit_depth is not None: - command += ['--bits', str(bit_depth)] + command += ["--bits", str(bit_depth)] if encoding is not None: - command += ['--encoding', str(encoding)] + command += ["--encoding", str(encoding)] if comment_file is not None: - command += ['--comment-file', str(comment_file)] + command += ["--comment-file", str(comment_file)] command += [ str(path), - 'synth', str(duration), # synthesizes for the given duration [sec] - 'sawtooth', '1', + "synth", + str(duration), # synthesizes for the given duration [sec] + "sawtooth", + "1", # saw tooth covers the both ends of value range, which is a good property for test. # similar to linspace(-1., 1.) # this introduces bigger boundary effect than sine when converted to mp3 ] if attenuation is not None: - command += ['vol', f'-{attenuation}dB'] - print(' '.join(command), file=sys.stderr) + command += ["vol", f"-{attenuation}dB"] + print(" ".join(command), file=sys.stderr) subprocess.run(command, check=True) -def convert_audio_file( - src_path, dst_path, - *, encoding=None, bit_depth=None, compression=None): +def convert_audio_file(src_path, dst_path, *, encoding=None, bit_depth=None, compression=None): """Convert audio file with `sox` command.""" - command = ['sox', '-V3', '--no-dither', '-R', str(src_path)] + command = ["sox", "-V3", "--no-dither", "-R", str(src_path)] if encoding is not None: - command += ['--encoding', str(encoding)] + command += ["--encoding", str(encoding)] if bit_depth is not None: - command += ['--bits', str(bit_depth)] + command += ["--bits", str(bit_depth)] if compression is not None: - command += ['--compression', str(compression)] + command += ["--compression", str(compression)] command += [dst_path] - print(' '.join(command), file=sys.stderr) + print(" ".join(command), file=sys.stderr) subprocess.run(command, check=True) @@ -96,11 +106,11 @@ def _flattern(effects): def run_sox_effect(input_file, output_file, effect, *, output_sample_rate=None, output_bitdepth=None): """Run sox effects""" effect = _flattern(effect) - command = ['sox', '-V', '--no-dither', input_file] + command = ["sox", "-V", "--no-dither", input_file] if output_bitdepth: - command += ['--bits', str(output_bitdepth)] + command += ["--bits", str(output_bitdepth)] command += [output_file] + effect if output_sample_rate: - command += ['rate', str(output_sample_rate)] - print(' '.join(command)) + command += ["rate", str(output_sample_rate)] + print(" ".join(command)) subprocess.run(command, check=True) diff --git a/test/torchaudio_unittest/common_utils/wav_utils.py b/test/torchaudio_unittest/common_utils/wav_utils.py index b4b7944805..db15494dca 100644 --- a/test/torchaudio_unittest/common_utils/wav_utils.py +++ b/test/torchaudio_unittest/common_utils/wav_utils.py @@ -1,7 +1,7 @@ from typing import Optional -import torch import scipy.io.wavfile +import torch def normalize_wav(tensor: torch.Tensor) -> torch.Tensor: @@ -9,26 +9,26 @@ def normalize_wav(tensor: torch.Tensor) -> torch.Tensor: pass elif tensor.dtype == torch.int32: tensor = tensor.to(torch.float32) - tensor[tensor > 0] /= 2147483647. - tensor[tensor < 0] /= 2147483648. + tensor[tensor > 0] /= 2147483647.0 + tensor[tensor < 0] /= 2147483648.0 elif tensor.dtype == torch.int16: tensor = tensor.to(torch.float32) - tensor[tensor > 0] /= 32767. - tensor[tensor < 0] /= 32768. + tensor[tensor > 0] /= 32767.0 + tensor[tensor < 0] /= 32768.0 elif tensor.dtype == torch.uint8: tensor = tensor.to(torch.float32) - 128 - tensor[tensor > 0] /= 127. - tensor[tensor < 0] /= 128. + tensor[tensor > 0] /= 127.0 + tensor[tensor < 0] /= 128.0 return tensor def get_wav_data( - dtype: str, - num_channels: int, - *, - num_frames: Optional[int] = None, - normalize: bool = True, - channels_first: bool = True, + dtype: str, + num_channels: int, + *, + num_frames: Optional[int] = None, + normalize: bool = True, + channels_first: bool = True, ): """Generate linear signal of the given dtype and num_channels @@ -45,25 +45,25 @@ def get_wav_data( dtype_ = getattr(torch, dtype) if num_frames is None: - if dtype == 'uint8': + if dtype == "uint8": num_frames = 256 else: num_frames = 1 << 16 - if dtype == 'uint8': + if dtype == "uint8": base = torch.linspace(0, 255, num_frames, dtype=dtype_) - elif dtype == 'int8': + elif dtype == "int8": base = torch.linspace(-128, 127, num_frames, dtype=dtype_) - elif dtype == 'float32': - base = torch.linspace(-1., 1., num_frames, dtype=dtype_) - elif dtype == 'float64': - base = torch.linspace(-1., 1., num_frames, dtype=dtype_) - elif dtype == 'int32': + elif dtype == "float32": + base = torch.linspace(-1.0, 1.0, num_frames, dtype=dtype_) + elif dtype == "float64": + base = torch.linspace(-1.0, 1.0, num_frames, dtype=dtype_) + elif dtype == "int32": base = torch.linspace(-2147483648, 2147483647, num_frames, dtype=dtype_) - elif dtype == 'int16': + elif dtype == "int16": base = torch.linspace(-32768, 32767, num_frames, dtype=dtype_) else: - raise NotImplementedError(f'Unsupported dtype {dtype}') + raise NotImplementedError(f"Unsupported dtype {dtype}") data = base.repeat([num_channels, 1]) if not channels_first: data = data.transpose(1, 0) diff --git a/test/torchaudio_unittest/compliance_kaldi_test.py b/test/torchaudio_unittest/compliance_kaldi_test.py index f4e79d4228..61b37f131a 100644 --- a/test/torchaudio_unittest/compliance_kaldi_test.py +++ b/test/torchaudio_unittest/compliance_kaldi_test.py @@ -1,6 +1,5 @@ import torch import torchaudio.compliance.kaldi as kaldi - from torchaudio_unittest import common_utils @@ -20,28 +19,27 @@ def first_sample_of_frame(frame, window_size, window_shift, snip_edges): end_sample = start_sample + frame_length if snip_edges: - assert(start_sample >= sample_offset and end_sample <= num_samples) + assert start_sample >= sample_offset and end_sample <= num_samples else: - assert(sample_offset == 0 or start_sample >= sample_offset) + assert sample_offset == 0 or start_sample >= sample_offset wave_start = start_sample - sample_offset wave_end = wave_start + frame_length if wave_start >= 0 and wave_end <= wave.size(0): - window[f, :] = wave[wave_start:(wave_start + frame_length)] + window[f, :] = wave[wave_start : (wave_start + frame_length)] else: wave_dim = wave.size(0) for s in range(frame_length): s_in_wave = s + wave_start while s_in_wave < 0 or s_in_wave >= wave_dim: if s_in_wave < 0: - s_in_wave = - s_in_wave - 1 + s_in_wave = -s_in_wave - 1 else: s_in_wave = 2 * wave_dim - 1 - s_in_wave window[f, s] = wave[s_in_wave] class Test_Kaldi(common_utils.TempDirMixin, common_utils.TorchaudioTestCase): - def _test_get_strided_helper(self, num_samples, window_size, window_shift, snip_edges): waveform = torch.arange(num_samples).float() output = kaldi._get_strided(waveform, window_size, window_shift, snip_edges) diff --git a/test/torchaudio_unittest/datasets/cmuarctic_test.py b/test/torchaudio_unittest/datasets/cmuarctic_test.py index 10ff766806..6262cb18bf 100644 --- a/test/torchaudio_unittest/datasets/cmuarctic_test.py +++ b/test/torchaudio_unittest/datasets/cmuarctic_test.py @@ -2,7 +2,6 @@ from pathlib import Path from torchaudio.datasets import cmuarctic - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, diff --git a/test/torchaudio_unittest/datasets/cmudict_test.py b/test/torchaudio_unittest/datasets/cmudict_test.py index da645346ba..cc30003c24 100644 --- a/test/torchaudio_unittest/datasets/cmudict_test.py +++ b/test/torchaudio_unittest/datasets/cmudict_test.py @@ -2,7 +2,6 @@ from pathlib import Path from torchaudio.datasets import CMUDict - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -21,7 +20,7 @@ def get_mock_dataset(root_dir, return_punc=False): puncs = [ "!EXCLAMATION-POINT EH2 K S K L AH0 M EY1 SH AH0 N P OY2 N T", - "\"CLOSE-QUOTE K L OW1 Z K W OW1 T", + '"CLOSE-QUOTE K L OW1 Z K W OW1 T', "#HASH-MARK HH AE1 M AA2 R K", "%PERCENT P ER0 S EH1 N T", "&ERSAND AE1 M P ER0 S AE2 N D", @@ -43,7 +42,7 @@ def get_mock_dataset(root_dir, return_punc=False): punc_outputs = [ "!", - "\"", + '"', "#", "%", "&", diff --git a/test/torchaudio_unittest/datasets/commonvoice_test.py b/test/torchaudio_unittest/datasets/commonvoice_test.py index 4d7c269f2a..c250c2a844 100644 --- a/test/torchaudio_unittest/datasets/commonvoice_test.py +++ b/test/torchaudio_unittest/datasets/commonvoice_test.py @@ -4,6 +4,7 @@ from typing import Tuple, Dict from torch import Tensor +from torchaudio.datasets import COMMONVOICE from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -12,21 +13,40 @@ normalize_wav, ) -from torchaudio.datasets import COMMONVOICE - _ORIGINAL_EXT_AUDIO = COMMONVOICE._ext_audio _SAMPLE_RATE = 48000 _HEADERS = [u"client_ids", u"path", u"sentence", u"up_votes", u"down_votes", u"age", u"gender", u"accent"] _EN_TRAIN_CSV_CONTENTS = [ - ["9d16c5d980247861130e0480e2719f448be73d86a496c36d01a477cbdecd8cfd1399403d7a77bf458d211a70711b2da0845c", - "common_voice_en_18885784.wav", - "He was accorded a State funeral, and was buried in Drayton and Toowoomba Cemetery.", "2", "0", "", "", - ""], - ["c82eb9291328620f06025a1f8112b909099e447e485e99236cb87df008650250e79fea5ca772061fb6a370830847b9c44d20", - "common_voice_en_556542.wav", "Once more into the breach", "2", "0", "thirties", "male", "us"], - ["f74d880c5ad4c5917f314a604d3fc4805159d255796fb9f8defca35333ecc002bdf53dc463503c12674ea840b21b4a507b7c", - "common_voice_en_18607573.wav", - "Caddy, show Miss Clare and Miss Summerson their rooms.", "2", "0", "twenties", "male", "canada"], + [ + "9d16c5d980247861130e0480e2719f448be73d86a496c36d01a477cbdecd8cfd1399403d7a77bf458d211a70711b2da0845c", + "common_voice_en_18885784.wav", + "He was accorded a State funeral, and was buried in Drayton and Toowoomba Cemetery.", + "2", + "0", + "", + "", + "", + ], + [ + "c82eb9291328620f06025a1f8112b909099e447e485e99236cb87df008650250e79fea5ca772061fb6a370830847b9c44d20", + "common_voice_en_556542.wav", + "Once more into the breach", + "2", + "0", + "thirties", + "male", + "us", + ], + [ + "f74d880c5ad4c5917f314a604d3fc4805159d255796fb9f8defca35333ecc002bdf53dc463503c12674ea840b21b4a507b7c", + "common_voice_en_18607573.wav", + "Caddy, show Miss Clare and Miss Summerson their rooms.", + "2", + "0", + "twenties", + "male", + "canada", + ], ] _FR_TRAIN_CSV_CONTENTS = [ @@ -35,14 +55,25 @@ "18343441c601cae0597a4b0d3144", "89e67e7682b36786a0b4b4022c4d42090c86edd96c78c12d30088e62522b8fe466ea4912e6a1055dfb91b296a0743e0a2bbe" "16cebac98ee5349e3e8262cb9329", - "Or sur ce point nous n’avons aucune réponse de votre part.", "2", "0", "twenties", "male", "france"], + "Or sur ce point nous n’avons aucune réponse de votre part.", + "2", + "0", + "twenties", + "male", + "france", + ], [ "a2e8e1e1cc74d08c92a53d7b9ff84e077eb90410edd85b8882f16fd037cecfcb6a19413c6c63ce6458cfea9579878fa91cef18" "343441c601cae0597a4b0d3144", "87d71819a26179e93acfee149d0b21b7bf5e926e367d80b2b3792d45f46e04853a514945783ff764c1fc237b4eb0ee2b0a7a7" "cbd395acbdfcfa9d76a6e199bbd", - "Monsieur de La Verpillière, laissez parler le ministre", "2", "0", "twenties", "male", "france"], - + "Monsieur de La Verpillière, laissez parler le ministre", + "2", + "0", + "twenties", + "male", + "france", + ], ] @@ -57,8 +88,8 @@ def get_mock_dataset(root_dir, train_csv_contents, ext_audio) -> Tuple[Tensor, i tsv_filename = os.path.join(root_dir, "train.tsv") audio_base_path = os.path.join(root_dir, "clips") os.makedirs(audio_base_path, exist_ok=True) - with open(tsv_filename, "w", newline='') as tsv: - writer = csv.writer(tsv, delimiter='\t') + with open(tsv_filename, "w", newline="") as tsv: + writer = csv.writer(tsv, delimiter="\t") writer.writerow(_HEADERS) for i, content in enumerate(train_csv_contents): content[2] = str(content[2].encode("utf-8")) @@ -68,7 +99,7 @@ def get_mock_dataset(root_dir, train_csv_contents, ext_audio) -> Tuple[Tensor, i else: audio_path = os.path.join(audio_base_path, content[1]) - data = get_whitenoise(sample_rate=_SAMPLE_RATE, duration=1, n_channels=1, seed=i, dtype='float32') + data = get_whitenoise(sample_rate=_SAMPLE_RATE, duration=1, n_channels=1, seed=i, dtype="float32") save_wav(audio_path, data, _SAMPLE_RATE) # Append data entry mocked_data.append((normalize_wav(data), _SAMPLE_RATE, dict(zip(_HEADERS, content)))) @@ -117,7 +148,7 @@ def _test_commonvoice(self, dataset): class TestCommonVoiceEN(BaseTestCommonVoice, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None @classmethod @@ -135,7 +166,7 @@ def test_commonvoice_path(self): class TestCommonVoiceFR(BaseTestCommonVoice, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None @classmethod diff --git a/test/torchaudio_unittest/datasets/dr_vctk_test.py b/test/torchaudio_unittest/datasets/dr_vctk_test.py index 7e872a8ca6..6651051f50 100644 --- a/test/torchaudio_unittest/datasets/dr_vctk_test.py +++ b/test/torchaudio_unittest/datasets/dr_vctk_test.py @@ -1,9 +1,7 @@ from pathlib import Path import pytest - from torchaudio.datasets import dr_vctk - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -57,11 +55,7 @@ def get_mock_dataset(root_dir): data = {} for condition in _CONDITIONS: data[condition] = get_whitenoise( - sample_rate=sample_rate, - duration=0.01, - n_channels=1, - dtype='float32', - seed=seed + sample_rate=sample_rate, duration=0.01, n_channels=1, dtype="float32", seed=seed ) audio_dir = dataset_dir / f"{condition}_{subset}set_wav_16k" audio_file_path = audio_dir / filename @@ -85,7 +79,7 @@ def get_mock_dataset(root_dir): class TestDRVCTK(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None samples = {} diff --git a/test/torchaudio_unittest/datasets/gtzan_test.py b/test/torchaudio_unittest/datasets/gtzan_test.py index 838292f55d..7f97b8a0f5 100644 --- a/test/torchaudio_unittest/datasets/gtzan_test.py +++ b/test/torchaudio_unittest/datasets/gtzan_test.py @@ -2,7 +2,6 @@ from pathlib import Path from torchaudio.datasets import gtzan - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -24,12 +23,12 @@ def get_mock_dataset(root_dir): seed = 0 for genre in gtzan.gtzan_genres: - base_dir = os.path.join(root_dir, 'genres', genre) + base_dir = os.path.join(root_dir, "genres", genre) os.makedirs(base_dir, exist_ok=True) for i in range(100): - filename = f'{genre}.{i:05d}' - path = os.path.join(base_dir, f'{filename}.wav') - data = get_whitenoise(sample_rate=sample_rate, duration=0.01, n_channels=1, dtype='int16', seed=seed) + filename = f"{genre}.{i:05d}" + path = os.path.join(base_dir, f"{filename}.wav") + data = get_whitenoise(sample_rate=sample_rate, duration=0.01, n_channels=1, dtype="int16", seed=seed) save_wav(path, data, sample_rate) sample = (normalize_wav(data), sample_rate, genre) mocked_samples.append(sample) @@ -44,7 +43,7 @@ def get_mock_dataset(root_dir): class TestGTZAN(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None samples = [] @@ -100,28 +99,28 @@ def _test_testing(self, dataset): assert n_ite == len(self.testing) def test_training_str(self): - train_dataset = gtzan.GTZAN(self.root_dir, subset='training') + train_dataset = gtzan.GTZAN(self.root_dir, subset="training") self._test_training(train_dataset) def test_validation_str(self): - val_dataset = gtzan.GTZAN(self.root_dir, subset='validation') + val_dataset = gtzan.GTZAN(self.root_dir, subset="validation") self._test_validation(val_dataset) def test_testing_str(self): - test_dataset = gtzan.GTZAN(self.root_dir, subset='testing') + test_dataset = gtzan.GTZAN(self.root_dir, subset="testing") self._test_testing(test_dataset) def test_training_path(self): root_dir = Path(self.root_dir) - train_dataset = gtzan.GTZAN(root_dir, subset='training') + train_dataset = gtzan.GTZAN(root_dir, subset="training") self._test_training(train_dataset) def test_validation_path(self): root_dir = Path(self.root_dir) - val_dataset = gtzan.GTZAN(root_dir, subset='validation') + val_dataset = gtzan.GTZAN(root_dir, subset="validation") self._test_validation(val_dataset) def test_testing_path(self): root_dir = Path(self.root_dir) - test_dataset = gtzan.GTZAN(root_dir, subset='testing') + test_dataset = gtzan.GTZAN(root_dir, subset="testing") self._test_testing(test_dataset) diff --git a/test/torchaudio_unittest/datasets/librispeech_test.py b/test/torchaudio_unittest/datasets/librispeech_test.py index 44e98c1f47..e94e869455 100644 --- a/test/torchaudio_unittest/datasets/librispeech_test.py +++ b/test/torchaudio_unittest/datasets/librispeech_test.py @@ -1,6 +1,7 @@ import os from pathlib import Path +from torchaudio.datasets import librispeech from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -9,21 +10,8 @@ normalize_wav, ) -from torchaudio.datasets import librispeech - # Used to generate a unique transcript for each dummy audio file -_NUMBERS = [ - 'ZERO', - 'ONE', - 'TWO', - 'THREE', - 'FOUR', - 'FIVE', - 'SIX', - 'SEVEN', - 'EIGHT', - 'NINE' -] +_NUMBERS = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"] def get_mock_dataset(root_dir): @@ -31,9 +19,7 @@ def get_mock_dataset(root_dir): root_dir: directory to the mocked dataset """ mocked_data = [] - dataset_dir = os.path.join( - root_dir, librispeech.FOLDER_IN_ARCHIVE, librispeech.URL - ) + dataset_dir = os.path.join(root_dir, librispeech.FOLDER_IN_ARCHIVE, librispeech.URL) os.makedirs(dataset_dir, exist_ok=True) sample_rate = 16000 # 16kHz seed = 0 @@ -48,45 +34,28 @@ def get_mock_dataset(root_dir): trans_content = [] for utterance_id in range(10): - filename = f'{speaker_id}-{chapter_id}-{utterance_id:04d}.wav' + filename = f"{speaker_id}-{chapter_id}-{utterance_id:04d}.wav" path = os.path.join(chapter_path, filename) - transcript = ' '.join( - [_NUMBERS[x] for x in [speaker_id, chapter_id, utterance_id]] - ) - trans_content.append( - f'{speaker_id}-{chapter_id}-{utterance_id:04d} {transcript}' - ) - - data = get_whitenoise( - sample_rate=sample_rate, - duration=0.01, - n_channels=1, - dtype='float32', - seed=seed - ) + transcript = " ".join([_NUMBERS[x] for x in [speaker_id, chapter_id, utterance_id]]) + trans_content.append(f"{speaker_id}-{chapter_id}-{utterance_id:04d} {transcript}") + + data = get_whitenoise(sample_rate=sample_rate, duration=0.01, n_channels=1, dtype="float32", seed=seed) save_wav(path, data, sample_rate) - sample = ( - normalize_wav(data), - sample_rate, - transcript, - speaker_id, - chapter_id, - utterance_id - ) + sample = (normalize_wav(data), sample_rate, transcript, speaker_id, chapter_id, utterance_id) mocked_data.append(sample) seed += 1 - trans_filename = f'{speaker_id}-{chapter_id}.trans.txt' + trans_filename = f"{speaker_id}-{chapter_id}.trans.txt" trans_path = os.path.join(chapter_path, trans_filename) - with open(trans_path, 'w') as f: - f.write('\n'.join(trans_content)) + with open(trans_path, "w") as f: + f.write("\n".join(trans_content)) return mocked_data class TestLibriSpeech(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None samples = [] @@ -99,13 +68,11 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): # In case of test failure - librispeech.LIBRISPEECH._ext_audio = '.flac' + librispeech.LIBRISPEECH._ext_audio = ".flac" def _test_librispeech(self, dataset): num_samples = 0 - for i, ( - data, sample_rate, transcript, speaker_id, chapter_id, utterance_id - ) in enumerate(dataset): + for i, (data, sample_rate, transcript, speaker_id, chapter_id, utterance_id) in enumerate(dataset): self.assertEqual(data, self.samples[i][0], atol=5e-5, rtol=1e-8) assert sample_rate == self.samples[i][1] assert transcript == self.samples[i][2] @@ -115,14 +82,14 @@ def _test_librispeech(self, dataset): num_samples += 1 assert num_samples == len(self.samples) - librispeech.LIBRISPEECH._ext_audio = '.flac' + librispeech.LIBRISPEECH._ext_audio = ".flac" def test_librispeech_str(self): - librispeech.LIBRISPEECH._ext_audio = '.wav' + librispeech.LIBRISPEECH._ext_audio = ".wav" dataset = librispeech.LIBRISPEECH(self.root_dir) self._test_librispeech(dataset) def test_librispeech_path(self): - librispeech.LIBRISPEECH._ext_audio = '.wav' + librispeech.LIBRISPEECH._ext_audio = ".wav" dataset = librispeech.LIBRISPEECH(Path(self.root_dir)) self._test_librispeech(dataset) diff --git a/test/torchaudio_unittest/datasets/libritts_test.py b/test/torchaudio_unittest/datasets/libritts_test.py index 32e6dbcf73..7248ff1c72 100644 --- a/test/torchaudio_unittest/datasets/libritts_test.py +++ b/test/torchaudio_unittest/datasets/libritts_test.py @@ -1,6 +1,7 @@ import os from pathlib import Path +from torchaudio.datasets.libritts import LIBRITTS from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -9,14 +10,12 @@ normalize_wav, ) -from torchaudio.datasets.libritts import LIBRITTS - _UTTERANCE_IDS = [ - [19, 198, '000000', '000000'], - [26, 495, '000004', '000000'], + [19, 198, "000000", "000000"], + [26, 495, "000004", "000000"], ] -_ORIGINAL_TEXT = 'this is the original text.' -_NORMALIZED_TEXT = 'this is the normalized text.' +_ORIGINAL_TEXT = "this is the original text." +_NORMALIZED_TEXT = "this is the normalized text." def get_mock_dataset(root_dir): @@ -24,31 +23,31 @@ def get_mock_dataset(root_dir): root_dir: directory to the mocked dataset """ mocked_data = [] - base_dir = os.path.join(root_dir, 'LibriTTS', 'train-clean-100') + base_dir = os.path.join(root_dir, "LibriTTS", "train-clean-100") for i, utterance_id in enumerate(_UTTERANCE_IDS): filename = f'{"_".join(str(u) for u in utterance_id)}.wav' file_dir = os.path.join(base_dir, str(utterance_id[0]), str(utterance_id[1])) os.makedirs(file_dir, exist_ok=True) path = os.path.join(file_dir, filename) - data = get_whitenoise(sample_rate=24000, duration=2, n_channels=1, dtype='int16', seed=i) + data = get_whitenoise(sample_rate=24000, duration=2, n_channels=1, dtype="int16", seed=i) save_wav(path, data, 24000) mocked_data.append(normalize_wav(data)) original_text_filename = f'{"_".join(str(u) for u in utterance_id)}.original.txt' path_original = os.path.join(file_dir, original_text_filename) - with open(path_original, 'w') as file_: + with open(path_original, "w") as file_: file_.write(_ORIGINAL_TEXT) normalized_text_filename = f'{"_".join(str(u) for u in utterance_id)}.normalized.txt' path_normalized = os.path.join(file_dir, normalized_text_filename) - with open(path_normalized, 'w') as file_: + with open(path_normalized, "w") as file_: file_.write(_NORMALIZED_TEXT) return mocked_data, _UTTERANCE_IDS, _ORIGINAL_TEXT, _NORMALIZED_TEXT class TestLibriTTS(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None data = [] @@ -61,13 +60,15 @@ def setUpClass(cls): def _test_libritts(self, dataset): n_ites = 0 - for i, (waveform, - sample_rate, - original_text, - normalized_text, - speaker_id, - chapter_id, - utterance_id) in enumerate(dataset): + for i, ( + waveform, + sample_rate, + original_text, + normalized_text, + speaker_id, + chapter_id, + utterance_id, + ) in enumerate(dataset): expected_ids = self._utterance_ids[i] expected_data = self.data[i] self.assertEqual(expected_data, waveform, atol=5e-5, rtol=1e-8) diff --git a/test/torchaudio_unittest/datasets/ljspeech_test.py b/test/torchaudio_unittest/datasets/ljspeech_test.py index 4cf834b226..c155b94b73 100644 --- a/test/torchaudio_unittest/datasets/ljspeech_test.py +++ b/test/torchaudio_unittest/datasets/ljspeech_test.py @@ -2,6 +2,7 @@ import os from pathlib import Path +from torchaudio.datasets import ljspeech from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -10,20 +11,18 @@ save_wav, ) -from torchaudio.datasets import ljspeech - _TRANSCRIPTS = [ "Test transcript 1", "Test transcript 2", "Test transcript 3", - "In 1465 Sweynheim and Pannartz began printing in the monastery of Subiaco near Rome," + "In 1465 Sweynheim and Pannartz began printing in the monastery of Subiaco near Rome,", ] _NORMALIZED_TRANSCRIPT = [ "Test transcript one", "Test transcript two", "Test transcript three", - "In fourteen sixty-five Sweynheim and Pannartz began printing in the monastery of Subiaco near Rome," + "In fourteen sixty-five Sweynheim and Pannartz began printing in the monastery of Subiaco near Rome,", ] @@ -38,20 +37,14 @@ def get_mock_dataset(root_dir): metadata_path = os.path.join(base_dir, "metadata.csv") sample_rate = 22050 - with open(metadata_path, mode="w", newline='') as metadata_file: - metadata_writer = csv.writer( - metadata_file, delimiter="|", quoting=csv.QUOTE_NONE - ) - for i, (transcript, normalized_transcript) in enumerate( - zip(_TRANSCRIPTS, _NORMALIZED_TRANSCRIPT) - ): - fileid = f'LJ001-{i:04d}' + with open(metadata_path, mode="w", newline="") as metadata_file: + metadata_writer = csv.writer(metadata_file, delimiter="|", quoting=csv.QUOTE_NONE) + for i, (transcript, normalized_transcript) in enumerate(zip(_TRANSCRIPTS, _NORMALIZED_TRANSCRIPT)): + fileid = f"LJ001-{i:04d}" metadata_writer.writerow([fileid, transcript, normalized_transcript]) filename = fileid + ".wav" path = os.path.join(archive_dir, filename) - data = get_whitenoise( - sample_rate=sample_rate, duration=1, n_channels=1, dtype="int16", seed=i - ) + data = get_whitenoise(sample_rate=sample_rate, duration=1, n_channels=1, dtype="int16", seed=i) save_wav(path, data, sample_rate) mocked_data.append(normalize_wav(data)) return mocked_data, _TRANSCRIPTS, _NORMALIZED_TRANSCRIPT @@ -70,9 +63,7 @@ def setUpClass(cls): def _test_ljspeech(self, dataset): n_ite = 0 - for i, (waveform, sample_rate, transcript, normalized_transcript) in enumerate( - dataset - ): + for i, (waveform, sample_rate, transcript, normalized_transcript) in enumerate(dataset): expected_transcript = self._transcripts[i] expected_normalized_transcript = self._normalized_transcript[i] expected_data = self.data[i] diff --git a/test/torchaudio_unittest/datasets/speechcommands_test.py b/test/torchaudio_unittest/datasets/speechcommands_test.py index 19a352ee16..a365a90448 100644 --- a/test/torchaudio_unittest/datasets/speechcommands_test.py +++ b/test/torchaudio_unittest/datasets/speechcommands_test.py @@ -1,6 +1,7 @@ import os from pathlib import Path +from torchaudio.datasets import speechcommands from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -9,8 +10,6 @@ save_wav, ) -from torchaudio.datasets import speechcommands - _LABELS = [ "bed", "bird", @@ -93,10 +92,10 @@ def get_mock_dataset(dataset_dir): if j < 2: mocked_train_samples.append(sample) elif j < 4: - valid.write(f'{label}/{filename}\n') + valid.write(f"{label}/{filename}\n") mocked_valid_samples.append(sample) elif j < 6: - test.write(f'{label}/{filename}\n') + test.write(f"{label}/{filename}\n") mocked_test_samples.append(sample) return mocked_samples, mocked_train_samples, mocked_valid_samples, mocked_test_samples @@ -113,16 +112,12 @@ class TestSpeechCommands(TempDirMixin, TorchaudioTestCase): @classmethod def setUpClass(cls): cls.root_dir = cls.get_base_temp_dir() - dataset_dir = os.path.join( - cls.root_dir, speechcommands.FOLDER_IN_ARCHIVE, speechcommands.URL - ) + dataset_dir = os.path.join(cls.root_dir, speechcommands.FOLDER_IN_ARCHIVE, speechcommands.URL) cls.samples, cls.train_samples, cls.valid_samples, cls.test_samples = get_mock_dataset(dataset_dir) def _testSpeechCommands(self, dataset, data_samples): num_samples = 0 - for i, (data, sample_rate, label, speaker_id, utterance_number) in enumerate( - dataset - ): + for i, (data, sample_rate, label, speaker_id, utterance_number) in enumerate(dataset): self.assertEqual(data, data_samples[i][0], atol=5e-5, rtol=1e-8) assert sample_rate == data_samples[i][1] assert label == data_samples[i][2] diff --git a/test/torchaudio_unittest/datasets/tedlium_test.py b/test/torchaudio_unittest/datasets/tedlium_test.py index 00c3e1748e..ddc833308f 100644 --- a/test/torchaudio_unittest/datasets/tedlium_test.py +++ b/test/torchaudio_unittest/datasets/tedlium_test.py @@ -2,15 +2,8 @@ import platform from pathlib import Path -from torchaudio_unittest.common_utils import ( - TempDirMixin, - TorchaudioTestCase, - get_whitenoise, - save_wav, - skipIfNoSox -) - from torchaudio.datasets import tedlium +from torchaudio_unittest.common_utils import TempDirMixin, TorchaudioTestCase, get_whitenoise, save_wav, skipIfNoSox # Used to generate a unique utterance for each dummy audio file _UTTERANCES = [ @@ -145,6 +138,7 @@ class TestTedliumSoundfile(Tedlium, TorchaudioTestCase): if platform.system() != "Windows": + @skipIfNoSox class TestTedliumSoxIO(Tedlium, TorchaudioTestCase): backend = "sox_io" diff --git a/test/torchaudio_unittest/datasets/vctk_test.py b/test/torchaudio_unittest/datasets/vctk_test.py index 4171c3c307..e6bd64c2e1 100644 --- a/test/torchaudio_unittest/datasets/vctk_test.py +++ b/test/torchaudio_unittest/datasets/vctk_test.py @@ -2,7 +2,6 @@ from pathlib import Path from torchaudio.datasets import vctk - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -13,17 +12,17 @@ # Used to generate a unique transcript for each dummy audio file _TRANSCRIPT = [ - 'Please call Stella', - 'Ask her to bring these things', - 'with her from the store', - 'Six spoons of fresh snow peas, five thick slabs of blue cheese, and maybe a snack for her brother Bob', - 'We also need a small plastic snake and a big toy frog for the kids', - 'She can scoop these things into three red bags, and we will go meet her Wednesday at the train station', - 'When the sunlight strikes raindrops in the air, they act as a prism and form a rainbow', - 'The rainbow is a division of white light into many beautiful colors', - 'These take the shape of a long round arch, with its path high above, and its two ends \ - apparently beyond the horizon', - 'There is, according to legend, a boiling pot of gold at one end' + "Please call Stella", + "Ask her to bring these things", + "with her from the store", + "Six spoons of fresh snow peas, five thick slabs of blue cheese, and maybe a snack for her brother Bob", + "We also need a small plastic snake and a big toy frog for the kids", + "She can scoop these things into three red bags, and we will go meet her Wednesday at the train station", + "When the sunlight strikes raindrops in the air, they act as a prism and form a rainbow", + "The rainbow is a division of white light into many beautiful colors", + "These take the shape of a long round arch, with its path high above, and its two ends \ + apparently beyond the horizon", + "There is, according to legend, a boiling pot of gold at one end", ] @@ -32,51 +31,39 @@ def get_mock_dataset(root_dir): root_dir: root directory of the mocked data """ mocked_samples = [] - dataset_dir = os.path.join(root_dir, 'VCTK-Corpus-0.92') + dataset_dir = os.path.join(root_dir, "VCTK-Corpus-0.92") os.makedirs(dataset_dir, exist_ok=True) sample_rate = 48000 seed = 0 for speaker in range(225, 230): - speaker_id = 'p' + str(speaker) - audio_dir = os.path.join(dataset_dir, 'wav48_silence_trimmed', speaker_id) + speaker_id = "p" + str(speaker) + audio_dir = os.path.join(dataset_dir, "wav48_silence_trimmed", speaker_id) os.makedirs(audio_dir, exist_ok=True) - file_dir = os.path.join(dataset_dir, 'txt', speaker_id) + file_dir = os.path.join(dataset_dir, "txt", speaker_id) os.makedirs(file_dir, exist_ok=True) for utterance_id in range(1, 11): - filename = f'{speaker_id}_{utterance_id:03d}_mic2' - audio_file_path = os.path.join(audio_dir, filename + '.wav') - - data = get_whitenoise( - sample_rate=sample_rate, - duration=0.01, - n_channels=1, - dtype='float32', - seed=seed - ) + filename = f"{speaker_id}_{utterance_id:03d}_mic2" + audio_file_path = os.path.join(audio_dir, filename + ".wav") + + data = get_whitenoise(sample_rate=sample_rate, duration=0.01, n_channels=1, dtype="float32", seed=seed) save_wav(audio_file_path, data, sample_rate) - txt_file_path = os.path.join(file_dir, filename[:-5] + '.txt') + txt_file_path = os.path.join(file_dir, filename[:-5] + ".txt") transcript = _TRANSCRIPT[utterance_id - 1] - with open(txt_file_path, 'w') as f: + with open(txt_file_path, "w") as f: f.write(transcript) - sample = ( - normalize_wav(data), - sample_rate, - transcript, - speaker_id, - utterance_id - ) + sample = (normalize_wav(data), sample_rate, transcript, speaker_id, utterance_id) mocked_samples.append(sample) seed += 1 return mocked_samples class TestVCTK(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None samples = [] diff --git a/test/torchaudio_unittest/datasets/yesno_test.py b/test/torchaudio_unittest/datasets/yesno_test.py index 2f4144985d..66a34acf77 100644 --- a/test/torchaudio_unittest/datasets/yesno_test.py +++ b/test/torchaudio_unittest/datasets/yesno_test.py @@ -2,7 +2,6 @@ from pathlib import Path from torchaudio.datasets import yesno - from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -18,19 +17,19 @@ def get_mock_data(root_dir, labels): labels: list of labels """ mocked_data = [] - base_dir = os.path.join(root_dir, 'waves_yesno') + base_dir = os.path.join(root_dir, "waves_yesno") os.makedirs(base_dir, exist_ok=True) for i, label in enumerate(labels): filename = f'{"_".join(str(l) for l in label)}.wav' path = os.path.join(base_dir, filename) - data = get_whitenoise(sample_rate=8000, duration=6, n_channels=1, dtype='int16', seed=i) + data = get_whitenoise(sample_rate=8000, duration=6, n_channels=1, dtype="int16", seed=i) save_wav(path, data, 8000) mocked_data.append(normalize_wav(data)) return mocked_data class TestYesNo(TempDirMixin, TorchaudioTestCase): - backend = 'default' + backend = "default" root_dir = None data = [] diff --git a/test/torchaudio_unittest/example/__init__.py b/test/torchaudio_unittest/example/__init__.py index 9e4fa81989..1ea63b98d7 100644 --- a/test/torchaudio_unittest/example/__init__.py +++ b/test/torchaudio_unittest/example/__init__.py @@ -2,7 +2,4 @@ import sys -sys.path.append( - os.path.join( - os.path.dirname(__file__), - '..', '..', '..', 'examples')) +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..", "examples")) diff --git a/test/torchaudio_unittest/example/souce_sepration/metrics_test.py b/test/torchaudio_unittest/example/souce_sepration/metrics_test.py index b7793b07ba..453b1312b7 100644 --- a/test/torchaudio_unittest/example/souce_sepration/metrics_test.py +++ b/test/torchaudio_unittest/example/souce_sepration/metrics_test.py @@ -1,15 +1,15 @@ from itertools import product import torch -from torch.testing._internal.common_utils import TestCase from parameterized import parameterized +from source_separation.utils import metrics +from torch.testing._internal.common_utils import TestCase from . import sdr_reference -from source_separation.utils import metrics class TestSDR(TestCase): - @parameterized.expand([(1, ), (2, ), (32, )]) + @parameterized.expand([(1,), (2,), (32,)]) def test_sdr(self, batch_size): """sdr produces the same result as the reference implementation""" num_frames = 256 diff --git a/test/torchaudio_unittest/example/souce_sepration/sdr_reference.py b/test/torchaudio_unittest/example/souce_sepration/sdr_reference.py index 7652fab0e6..178767e4a1 100644 --- a/test/torchaudio_unittest/example/souce_sepration/sdr_reference.py +++ b/test/torchaudio_unittest/example/souce_sepration/sdr_reference.py @@ -1,98 +1,98 @@ -"""Reference Implementation of SDR and PIT SDR. - -This module was taken from the following implementation - -https://github.com/naplab/Conv-TasNet/blob/e66d82a8f956a69749ec8a4ae382217faa097c5c/utility/sdr.py - -which was made available by Yi Luo under the following liscence, - -Creative Commons Attribution-NonCommercial-ShareAlike 3.0 United States License. - -The module was modified in the following manner; - - Remove the functions other than `calc_sdr_torch` and `batch_SDR_torch`, - - Remove the import statements required only for the removed functions. - - Add `# flake8: noqa` so as not to report any format issue on this module. - -The implementation of the retained functions and their formats are kept as-is. -""" - -# flake8: noqa - -import numpy as np -from itertools import permutations - -import torch - - -def calc_sdr_torch(estimation, origin, mask=None): - """ - batch-wise SDR caculation for one audio file on pytorch Variables. - estimation: (batch, nsample) - origin: (batch, nsample) - mask: optional, (batch, nsample), binary - """ - - if mask is not None: - origin = origin * mask - estimation = estimation * mask - - origin_power = torch.pow(origin, 2).sum(1, keepdim=True) + 1e-8 # (batch, 1) - - scale = torch.sum(origin*estimation, 1, keepdim=True) / origin_power # (batch, 1) - - est_true = scale * origin # (batch, nsample) - est_res = estimation - est_true # (batch, nsample) - - true_power = torch.pow(est_true, 2).sum(1) - res_power = torch.pow(est_res, 2).sum(1) - - return 10*torch.log10(true_power) - 10*torch.log10(res_power) # (batch, 1) - - -def batch_SDR_torch(estimation, origin, mask=None): - """ - batch-wise SDR caculation for multiple audio files. - estimation: (batch, nsource, nsample) - origin: (batch, nsource, nsample) - mask: optional, (batch, nsample), binary - """ - - batch_size_est, nsource_est, nsample_est = estimation.size() - batch_size_ori, nsource_ori, nsample_ori = origin.size() - - assert batch_size_est == batch_size_ori, "Estimation and original sources should have same shape." - assert nsource_est == nsource_ori, "Estimation and original sources should have same shape." - assert nsample_est == nsample_ori, "Estimation and original sources should have same shape." - - assert nsource_est < nsample_est, "Axis 1 should be the number of sources, and axis 2 should be the signal." - - batch_size = batch_size_est - nsource = nsource_est - nsample = nsample_est - - # zero mean signals - estimation = estimation - torch.mean(estimation, 2, keepdim=True).expand_as(estimation) - origin = origin - torch.mean(origin, 2, keepdim=True).expand_as(estimation) - - # possible permutations - perm = list(set(permutations(np.arange(nsource)))) - - # pair-wise SDR - SDR = torch.zeros((batch_size, nsource, nsource)).type(estimation.type()) - for i in range(nsource): - for j in range(nsource): - SDR[:,i,j] = calc_sdr_torch(estimation[:,i], origin[:,j], mask) - - # choose the best permutation - SDR_max = [] - SDR_perm = [] - for permute in perm: - sdr = [] - for idx in range(len(permute)): - sdr.append(SDR[:,idx,permute[idx]].view(batch_size,-1)) - sdr = torch.sum(torch.cat(sdr, 1), 1) - SDR_perm.append(sdr.view(batch_size, 1)) - SDR_perm = torch.cat(SDR_perm, 1) - SDR_max, _ = torch.max(SDR_perm, dim=1) - - return SDR_max / nsource +"""Reference Implementation of SDR and PIT SDR. + +This module was taken from the following implementation + +https://github.com/naplab/Conv-TasNet/blob/e66d82a8f956a69749ec8a4ae382217faa097c5c/utility/sdr.py + +which was made available by Yi Luo under the following liscence, + +Creative Commons Attribution-NonCommercial-ShareAlike 3.0 United States License. + +The module was modified in the following manner; + - Remove the functions other than `calc_sdr_torch` and `batch_SDR_torch`, + - Remove the import statements required only for the removed functions. + - Add `# flake8: noqa` so as not to report any format issue on this module. + +The implementation of the retained functions and their formats are kept as-is. +""" + +from itertools import permutations + +# flake8: noqa + +import numpy as np +import torch + + +def calc_sdr_torch(estimation, origin, mask=None): + """ + batch-wise SDR caculation for one audio file on pytorch Variables. + estimation: (batch, nsample) + origin: (batch, nsample) + mask: optional, (batch, nsample), binary + """ + + if mask is not None: + origin = origin * mask + estimation = estimation * mask + + origin_power = torch.pow(origin, 2).sum(1, keepdim=True) + 1e-8 # (batch, 1) + + scale = torch.sum(origin * estimation, 1, keepdim=True) / origin_power # (batch, 1) + + est_true = scale * origin # (batch, nsample) + est_res = estimation - est_true # (batch, nsample) + + true_power = torch.pow(est_true, 2).sum(1) + res_power = torch.pow(est_res, 2).sum(1) + + return 10 * torch.log10(true_power) - 10 * torch.log10(res_power) # (batch, 1) + + +def batch_SDR_torch(estimation, origin, mask=None): + """ + batch-wise SDR caculation for multiple audio files. + estimation: (batch, nsource, nsample) + origin: (batch, nsource, nsample) + mask: optional, (batch, nsample), binary + """ + + batch_size_est, nsource_est, nsample_est = estimation.size() + batch_size_ori, nsource_ori, nsample_ori = origin.size() + + assert batch_size_est == batch_size_ori, "Estimation and original sources should have same shape." + assert nsource_est == nsource_ori, "Estimation and original sources should have same shape." + assert nsample_est == nsample_ori, "Estimation and original sources should have same shape." + + assert nsource_est < nsample_est, "Axis 1 should be the number of sources, and axis 2 should be the signal." + + batch_size = batch_size_est + nsource = nsource_est + nsample = nsample_est + + # zero mean signals + estimation = estimation - torch.mean(estimation, 2, keepdim=True).expand_as(estimation) + origin = origin - torch.mean(origin, 2, keepdim=True).expand_as(estimation) + + # possible permutations + perm = list(set(permutations(np.arange(nsource)))) + + # pair-wise SDR + SDR = torch.zeros((batch_size, nsource, nsource)).type(estimation.type()) + for i in range(nsource): + for j in range(nsource): + SDR[:, i, j] = calc_sdr_torch(estimation[:, i], origin[:, j], mask) + + # choose the best permutation + SDR_max = [] + SDR_perm = [] + for permute in perm: + sdr = [] + for idx in range(len(permute)): + sdr.append(SDR[:, idx, permute[idx]].view(batch_size, -1)) + sdr = torch.sum(torch.cat(sdr, 1), 1) + SDR_perm.append(sdr.view(batch_size, 1)) + SDR_perm = torch.cat(SDR_perm, 1) + SDR_max, _ = torch.max(SDR_perm, dim=1) + + return SDR_max / nsource diff --git a/test/torchaudio_unittest/example/souce_sepration/wsj0mix_test.py b/test/torchaudio_unittest/example/souce_sepration/wsj0mix_test.py index 46927b182f..b4c8e1d5ee 100644 --- a/test/torchaudio_unittest/example/souce_sepration/wsj0mix_test.py +++ b/test/torchaudio_unittest/example/souce_sepration/wsj0mix_test.py @@ -1,5 +1,6 @@ import os +from source_separation.utils.dataset import wsj0mix from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -8,8 +9,6 @@ normalize_wav, ) -from source_separation.utils.dataset import wsj0mix - _FILENAMES = [ "012c0207_1.9952_01cc0202_-1.9952.wav", @@ -45,9 +44,7 @@ def _mock_dataset(root_dir, num_speaker): mix = None src = [] for dirname in dirnames: - waveform = get_whitenoise( - sample_rate=8000, duration=1, n_channels=1, dtype="int16", seed=seed - ) + waveform = get_whitenoise(sample_rate=8000, duration=1, n_channels=1, dtype="int16", seed=seed) seed += 1 path = os.path.join(root_dir, dirname, filename) diff --git a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_cpu_test.py b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_cpu_test.py index f4d2e38987..ad5530becd 100644 --- a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_cpu_test.py +++ b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_cpu_test.py @@ -1,11 +1,11 @@ import torch +from torchaudio_unittest.common_utils import PytorchTestCase from .tacotron2_loss_impl import ( Tacotron2LossShapeTests, Tacotron2LossTorchscriptTests, Tacotron2LossGradcheckTests, ) -from torchaudio_unittest.common_utils import PytorchTestCase class TestTacotron2LossShapeFloat32CPU(Tacotron2LossShapeTests, PytorchTestCase): @@ -19,5 +19,5 @@ class TestTacotron2TorchsciptFloat32CPU(Tacotron2LossTorchscriptTests, PytorchTe class TestTacotron2GradcheckFloat64CPU(Tacotron2LossGradcheckTests, PytorchTestCase): - dtype = torch.float64 # gradcheck needs a higher numerical accuracy + dtype = torch.float64 # gradcheck needs a higher numerical accuracy device = torch.device("cpu") diff --git a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_gpu_test.py b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_gpu_test.py index 9c1ae252c4..9b56d5399c 100644 --- a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_gpu_test.py +++ b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_gpu_test.py @@ -1,11 +1,11 @@ import torch +from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase from .tacotron2_loss_impl import ( Tacotron2LossShapeTests, Tacotron2LossTorchscriptTests, Tacotron2LossGradcheckTests, ) -from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase @skipIfNoCuda @@ -22,5 +22,5 @@ class TestTacotron2TorchsciptFloat32CUDA(PytorchTestCase, Tacotron2LossTorchscri @skipIfNoCuda class TestTacotron2GradcheckFloat64CUDA(PytorchTestCase, Tacotron2LossGradcheckTests): - dtype = torch.float64 # gradcheck needs a higher numerical accuracy + dtype = torch.float64 # gradcheck needs a higher numerical accuracy device = torch.device("cuda") diff --git a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_impl.py b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_impl.py index 848126e696..171a870126 100644 --- a/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_impl.py +++ b/test/torchaudio_unittest/example/tacotron2/tacotron2_loss_impl.py @@ -1,7 +1,6 @@ import torch -from torch.autograd import gradcheck, gradgradcheck - from pipeline_tacotron2.loss import Tacotron2Loss +from torch.autograd import gradcheck, gradgradcheck from torchaudio_unittest.common_utils import ( TestBaseMixin, torch_script, @@ -9,18 +8,11 @@ class Tacotron2LossInputMixin(TestBaseMixin): - def _get_inputs(self, n_mel=80, n_batch=16, max_mel_specgram_length=300): - mel_specgram = torch.rand( - n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device - ) - mel_specgram_postnet = torch.rand( - n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device - ) + mel_specgram = torch.rand(n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device) + mel_specgram_postnet = torch.rand(n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device) gate_out = torch.rand(n_batch, dtype=self.dtype, device=self.device) - truth_mel_specgram = torch.rand( - n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device - ) + truth_mel_specgram = torch.rand(n_batch, n_mel, max_mel_specgram_length, dtype=self.dtype, device=self.device) truth_gate_out = torch.rand(n_batch, dtype=self.dtype, device=self.device) truth_mel_specgram.requires_grad = False @@ -36,7 +28,6 @@ def _get_inputs(self, n_mel=80, n_batch=16, max_mel_specgram_length=300): class Tacotron2LossShapeTests(Tacotron2LossInputMixin): - def test_tacotron2_loss_shape(self): """Validate the output shape of Tacotron2Loss.""" n_batch = 16 @@ -50,8 +41,7 @@ def test_tacotron2_loss_shape(self): ) = self._get_inputs(n_batch=n_batch) mel_loss, mel_postnet_loss, gate_loss = Tacotron2Loss()( - (mel_specgram, mel_specgram_postnet, gate_out), - (truth_mel_specgram, truth_gate_out) + (mel_specgram, mel_specgram_postnet, gate_out), (truth_mel_specgram, truth_gate_out) ) self.assertEqual(mel_loss.size(), torch.Size([])) @@ -60,7 +50,6 @@ def test_tacotron2_loss_shape(self): class Tacotron2LossTorchscriptTests(Tacotron2LossInputMixin): - def _assert_torchscript_consistency(self, fn, tensors): ts_func = torch_script(fn) @@ -77,7 +66,6 @@ def test_tacotron2_loss_torchscript_consistency(self): class Tacotron2LossGradcheckTests(Tacotron2LossInputMixin): - def test_tacotron2_loss_gradcheck(self): """Performing gradient check on Tacotron2Loss.""" ( diff --git a/test/torchaudio_unittest/example/tacotron2/test_text_preprocessing.py b/test/torchaudio_unittest/example/tacotron2/test_text_preprocessing.py index 8da02de88e..917cc97ce3 100644 --- a/test/torchaudio_unittest/example/tacotron2/test_text_preprocessing.py +++ b/test/torchaudio_unittest/example/tacotron2/test_text_preprocessing.py @@ -1,10 +1,8 @@ from parameterized import parameterized - from torchaudio._internal.module_utils import is_module_available from torchaudio_unittest.common_utils import TorchaudioTestCase, skipIfNoModule if is_module_available("unidecode") and is_module_available("inflect"): - from pipeline_tacotron2.text.text_preprocessing import text_to_sequence from pipeline_tacotron2.text.numbers import ( _remove_commas, _expand_pounds, @@ -13,25 +11,62 @@ _expand_ordinal, _expand_number, ) + from pipeline_tacotron2.text.text_preprocessing import text_to_sequence @skipIfNoModule("unidecode") @skipIfNoModule("inflect") class TestTextPreprocessor(TorchaudioTestCase): - @parameterized.expand( [ ["dr. Strange?", [15, 26, 14, 31, 26, 29, 11, 30, 31, 29, 12, 25, 18, 16, 10]], ["ML, is fun.", [24, 23, 6, 11, 20, 30, 11, 17, 32, 25, 7]], ["I love torchaudio!", [20, 11, 23, 26, 33, 16, 11, 31, 26, 29, 14, 19, 12, 32, 15, 20, 26, 2]], # 'one thousand dollars, twenty cents' - ["$1,000.20", [26, 25, 16, 11, 31, 19, 26, 32, 30, 12, 25, 15, 11, 15, 26, 23, 23, - 12, 29, 30, 6, 11, 31, 34, 16, 25, 31, 36, 11, 14, 16, 25, 31, 30]], + [ + "$1,000.20", + [ + 26, + 25, + 16, + 11, + 31, + 19, + 26, + 32, + 30, + 12, + 25, + 15, + 11, + 15, + 26, + 23, + 23, + 12, + 29, + 30, + 6, + 11, + 31, + 34, + 16, + 25, + 31, + 36, + 11, + 14, + 16, + 25, + 31, + 30, + ], + ], ] ) def test_text_to_sequence(self, sent, seq): - assert (text_to_sequence(sent) == seq) + assert text_to_sequence(sent) == seq @parameterized.expand( [ @@ -40,7 +75,7 @@ def test_text_to_sequence(self, sent, seq): ) def test_remove_commas(self, sent, truth): - assert (_remove_commas(sent) == truth) + assert _remove_commas(sent) == truth @parameterized.expand( [ @@ -49,19 +84,21 @@ def test_remove_commas(self, sent, truth): ) def test_expand_pounds(self, sent, truth): - assert (_expand_pounds(sent) == truth) + assert _expand_pounds(sent) == truth @parameterized.expand( [ ["He, she, and I have $1000", "He, she, and I have 1000 dollars"], ["He, she, and I have $3000.01", "He, she, and I have 3000 dollars, 1 cent"], - ["He has $500.20 and she has $1000.50.", - "He has 500 dollars, 20 cents and she has 1000 dollars, 50 cents."], + [ + "He has $500.20 and she has $1000.50.", + "He has 500 dollars, 20 cents and she has 1000 dollars, 50 cents.", + ], ] ) def test_expand_dollars(self, sent, truth): - assert (_expand_dollars(sent) == truth) + assert _expand_dollars(sent) == truth @parameterized.expand( [ @@ -71,7 +108,7 @@ def test_expand_dollars(self, sent, truth): ) def test_expand_decimal_point(self, sent, truth): - assert (_expand_decimal_point(sent) == truth) + assert _expand_decimal_point(sent) == truth @parameterized.expand( [ @@ -82,16 +119,19 @@ def test_expand_decimal_point(self, sent, truth): ) def test_expand_ordinal(self, sent, truth): - assert (_expand_ordinal(sent) == truth) + assert _expand_ordinal(sent) == truth _expand_ordinal, @parameterized.expand( [ ["100020 dollars.", "one hundred thousand twenty dollars."], - ["1234567890!", "one billion, two hundred thirty-four million, " - "five hundred sixty-seven thousand, eight hundred ninety!"], + [ + "1234567890!", + "one billion, two hundred thirty-four million, " + "five hundred sixty-seven thousand, eight hundred ninety!", + ], ] ) def test_expand_number(self, sent, truth): - assert (_expand_number(sent) == truth) + assert _expand_number(sent) == truth diff --git a/test/torchaudio_unittest/functional/autograd_cpu_test.py b/test/torchaudio_unittest/functional/autograd_cpu_test.py index a34823a76f..de371368ad 100644 --- a/test/torchaudio_unittest/functional/autograd_cpu_test.py +++ b/test/torchaudio_unittest/functional/autograd_cpu_test.py @@ -1,13 +1,14 @@ import torch -from .autograd_impl import Autograd, AutogradFloat32 from torchaudio_unittest import common_utils +from .autograd_impl import Autograd, AutogradFloat32 + class TestAutogradLfilterCPU(Autograd, common_utils.PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") class TestAutogradRNNTCPU(AutogradFloat32, common_utils.PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/functional/autograd_cuda_test.py b/test/torchaudio_unittest/functional/autograd_cuda_test.py index 341c575d87..c4c608f221 100644 --- a/test/torchaudio_unittest/functional/autograd_cuda_test.py +++ b/test/torchaudio_unittest/functional/autograd_cuda_test.py @@ -1,15 +1,16 @@ import torch -from .autograd_impl import Autograd, AutogradFloat32 from torchaudio_unittest import common_utils +from .autograd_impl import Autograd, AutogradFloat32 + @common_utils.skipIfNoCuda class TestAutogradLfilterCUDA(Autograd, common_utils.PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") @common_utils.skipIfNoCuda class TestAutogradRNNTCUDA(AutogradFloat32, common_utils.PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/functional/autograd_impl.py b/test/torchaudio_unittest/functional/autograd_impl.py index ecd27722f7..6d942d1e92 100644 --- a/test/torchaudio_unittest/functional/autograd_impl.py +++ b/test/torchaudio_unittest/functional/autograd_impl.py @@ -1,9 +1,10 @@ -from typing import Callable, Tuple from functools import partial +from typing import Callable, Tuple + import torch +import torchaudio.functional as F from parameterized import parameterized from torch import Tensor -import torchaudio.functional as F from torch.autograd import gradcheck, gradgradcheck from torchaudio_unittest.common_utils import ( TestBaseMixin, @@ -14,11 +15,11 @@ class Autograd(TestBaseMixin): def assert_grad( - self, - transform: Callable[..., Tensor], - inputs: Tuple[torch.Tensor], - *, - enable_all_grad: bool = True, + self, + transform: Callable[..., Tensor], + inputs: Tuple[torch.Tensor], + *, + enable_all_grad: bool = True, ): inputs_ = [] for i in inputs: @@ -64,19 +65,15 @@ def test_lfilter_all_inputs(self): def test_lfilter_filterbanks(self): torch.random.manual_seed(2434) x = get_whitenoise(sample_rate=22050, duration=0.01, n_channels=3) - a = torch.tensor([[0.7, 0.2, 0.6], - [0.8, 0.2, 0.9]]) - b = torch.tensor([[0.4, 0.2, 0.9], - [0.7, 0.2, 0.6]]) + a = torch.tensor([[0.7, 0.2, 0.6], [0.8, 0.2, 0.9]]) + b = torch.tensor([[0.4, 0.2, 0.9], [0.7, 0.2, 0.6]]) self.assert_grad(partial(F.lfilter, batching=False), (x, a, b)) def test_lfilter_batching(self): torch.random.manual_seed(2434) x = get_whitenoise(sample_rate=22050, duration=0.01, n_channels=2) - a = torch.tensor([[0.7, 0.2, 0.6], - [0.8, 0.2, 0.9]]) - b = torch.tensor([[0.4, 0.2, 0.9], - [0.7, 0.2, 0.6]]) + a = torch.tensor([[0.7, 0.2, 0.6], [0.8, 0.2, 0.9]]) + b = torch.tensor([[0.4, 0.2, 0.9], [0.7, 0.2, 0.6]]) self.assert_grad(F.lfilter, (x, a, b)) def test_filtfilt_a(self): @@ -105,10 +102,8 @@ def test_filtfilt_all_inputs(self): def test_filtfilt_batching(self): torch.random.manual_seed(2434) x = get_whitenoise(sample_rate=22050, duration=0.01, n_channels=2) - a = torch.tensor([[0.7, 0.2, 0.6], - [0.8, 0.2, 0.9]]) - b = torch.tensor([[0.4, 0.2, 0.9], - [0.7, 0.2, 0.6]]) + a = torch.tensor([[0.7, 0.2, 0.6], [0.8, 0.2, 0.9]]) + b = torch.tensor([[0.4, 0.2, 0.9], [0.7, 0.2, 0.6]]) self.assert_grad(F.filtfilt, (x, a, b)) def test_biquad(self): @@ -118,10 +113,12 @@ def test_biquad(self): b = torch.tensor([0.4, 0.2, 0.9]) self.assert_grad(F.biquad, (x, b[0], b[1], b[2], a[0], a[1], a[2])) - @parameterized.expand([ - (800, 0.7, True), - (800, 0.7, False), - ]) + @parameterized.expand( + [ + (800, 0.7, True), + (800, 0.7, False), + ] + ) def test_band_biquad(self, central_freq, Q, noise): torch.random.manual_seed(2434) sr = 22050 @@ -130,10 +127,12 @@ def test_band_biquad(self, central_freq, Q, noise): Q = torch.tensor(Q) self.assert_grad(F.band_biquad, (x, sr, central_freq, Q, noise)) - @parameterized.expand([ - (800, 0.7, 10), - (800, 0.7, -10), - ]) + @parameterized.expand( + [ + (800, 0.7, 10), + (800, 0.7, -10), + ] + ) def test_bass_biquad(self, central_freq, Q, gain): torch.random.manual_seed(2434) sr = 22050 @@ -143,11 +142,12 @@ def test_bass_biquad(self, central_freq, Q, gain): gain = torch.tensor(gain) self.assert_grad(F.bass_biquad, (x, sr, gain, central_freq, Q)) - @parameterized.expand([ - (3000, 0.7, 10), - (3000, 0.7, -10), - - ]) + @parameterized.expand( + [ + (3000, 0.7, 10), + (3000, 0.7, -10), + ] + ) def test_treble_biquad(self, central_freq, Q, gain): torch.random.manual_seed(2434) sr = 22050 @@ -157,9 +157,14 @@ def test_treble_biquad(self, central_freq, Q, gain): gain = torch.tensor(gain) self.assert_grad(F.treble_biquad, (x, sr, gain, central_freq, Q)) - @parameterized.expand([ - (800, 0.7, ), - ]) + @parameterized.expand( + [ + ( + 800, + 0.7, + ), + ] + ) def test_allpass_biquad(self, central_freq, Q): torch.random.manual_seed(2434) sr = 22050 @@ -168,9 +173,14 @@ def test_allpass_biquad(self, central_freq, Q): Q = torch.tensor(Q) self.assert_grad(F.allpass_biquad, (x, sr, central_freq, Q)) - @parameterized.expand([ - (800, 0.7, ), - ]) + @parameterized.expand( + [ + ( + 800, + 0.7, + ), + ] + ) def test_lowpass_biquad(self, cutoff_freq, Q): torch.random.manual_seed(2434) sr = 22050 @@ -179,9 +189,14 @@ def test_lowpass_biquad(self, cutoff_freq, Q): Q = torch.tensor(Q) self.assert_grad(F.lowpass_biquad, (x, sr, cutoff_freq, Q)) - @parameterized.expand([ - (800, 0.7, ), - ]) + @parameterized.expand( + [ + ( + 800, + 0.7, + ), + ] + ) def test_highpass_biquad(self, cutoff_freq, Q): torch.random.manual_seed(2434) sr = 22050 @@ -190,10 +205,12 @@ def test_highpass_biquad(self, cutoff_freq, Q): Q = torch.tensor(Q) self.assert_grad(F.highpass_biquad, (x, sr, cutoff_freq, Q)) - @parameterized.expand([ - (800, 0.7, True), - (800, 0.7, False), - ]) + @parameterized.expand( + [ + (800, 0.7, True), + (800, 0.7, False), + ] + ) def test_bandpass_biquad(self, central_freq, Q, const_skirt_gain): torch.random.manual_seed(2434) sr = 22050 @@ -202,10 +219,12 @@ def test_bandpass_biquad(self, central_freq, Q, const_skirt_gain): Q = torch.tensor(Q) self.assert_grad(F.bandpass_biquad, (x, sr, central_freq, Q, const_skirt_gain)) - @parameterized.expand([ - (800, 0.7, 10), - (800, 0.7, -10), - ]) + @parameterized.expand( + [ + (800, 0.7, 10), + (800, 0.7, -10), + ] + ) def test_equalizer_biquad(self, central_freq, Q, gain): torch.random.manual_seed(2434) sr = 22050 @@ -215,9 +234,14 @@ def test_equalizer_biquad(self, central_freq, Q, gain): gain = torch.tensor(gain) self.assert_grad(F.equalizer_biquad, (x, sr, central_freq, gain, Q)) - @parameterized.expand([ - (800, 0.7, ), - ]) + @parameterized.expand( + [ + ( + 800, + 0.7, + ), + ] + ) def test_bandreject_biquad(self, central_freq, Q): torch.random.manual_seed(2434) sr = 22050 @@ -229,10 +253,10 @@ def test_bandreject_biquad(self, central_freq, Q): class AutogradFloat32(TestBaseMixin): def assert_grad( - self, - transform: Callable[..., Tensor], - inputs: Tuple[torch.Tensor], - enable_all_grad: bool = True, + self, + transform: Callable[..., Tensor], + inputs: Tuple[torch.Tensor], + enable_all_grad: bool = True, ): inputs_ = [] for i in inputs: @@ -242,13 +266,15 @@ def assert_grad( i.requires_grad = True inputs_.append(i) # gradcheck with float32 requires higher atol and epsilon - assert gradcheck(transform, inputs, eps=1e-3, atol=1e-3, nondet_tol=0.) + assert gradcheck(transform, inputs, eps=1e-3, atol=1e-3, nondet_tol=0.0) - @parameterized.expand([ - (rnnt_utils.get_B1_T10_U3_D4_data, ), - (rnnt_utils.get_B2_T4_U3_D3_data, ), - (rnnt_utils.get_B1_T2_U3_D5_data, ), - ]) + @parameterized.expand( + [ + (rnnt_utils.get_B1_T10_U3_D4_data,), + (rnnt_utils.get_B2_T4_U3_D3_data,), + (rnnt_utils.get_B1_T2_U3_D5_data,), + ] + ) def test_rnnt_loss(self, data_func): def get_data(data_func, device): data = data_func() @@ -259,11 +285,11 @@ def get_data(data_func, device): data = get_data(data_func, self.device) inputs = ( data["logits"].to(torch.float32), # logits - data["targets"], # targets - data["logit_lengths"], # logit_lengths - data["target_lengths"], # target_lengths - data["blank"], # blank - -1, # clamp + data["targets"], # targets + data["logit_lengths"], # logit_lengths + data["target_lengths"], # target_lengths + data["blank"], # blank + -1, # clamp ) self.assert_grad(F.rnnt_loss, inputs, enable_all_grad=False) diff --git a/test/torchaudio_unittest/functional/batch_consistency_test.py b/test/torchaudio_unittest/functional/batch_consistency_test.py index 042bfe52a1..5beac512a6 100644 --- a/test/torchaudio_unittest/functional/batch_consistency_test.py +++ b/test/torchaudio_unittest/functional/batch_consistency_test.py @@ -2,41 +2,37 @@ import itertools import math -from parameterized import parameterized, parameterized_class import torch import torchaudio.functional as F - +from parameterized import parameterized, parameterized_class from torchaudio_unittest import common_utils def _name_from_args(func, _, params): """Return a parameterized test name, based on parameter values.""" - return "{}_{}".format( - func.__name__, - "_".join(str(arg) for arg in params.args)) + return "{}_{}".format(func.__name__, "_".join(str(arg) for arg in params.args)) -@parameterized_class([ - # Single-item batch isolates problems that come purely from adding a - # dimension (rather than processing multiple items) - {"batch_size": 1}, - {"batch_size": 3}, -]) +@parameterized_class( + [ + # Single-item batch isolates problems that come purely from adding a + # dimension (rather than processing multiple items) + {"batch_size": 1}, + {"batch_size": 3}, + ] +) class TestFunctional(common_utils.TorchaudioTestCase): """Test functions defined in `functional` module""" - backend = 'default' - def assert_batch_consistency( - self, functional, batch, *args, atol=1e-8, rtol=1e-5, seed=42, - **kwargs): + backend = "default" + + def assert_batch_consistency(self, functional, batch, *args, atol=1e-8, rtol=1e-5, seed=42, **kwargs): n = batch.size(0) # Compute items separately, then batch the result torch.random.manual_seed(seed) items_input = batch.clone() - items_result = torch.stack([ - functional(items_input[i], *args, **kwargs) for i in range(n) - ]) + items_result = torch.stack([functional(items_input[i], *args, **kwargs) for i in range(n)]) # Batch the input and run torch.random.manual_seed(seed) @@ -58,43 +54,45 @@ def test_griffinlim(self): torch.random.manual_seed(0) batch = torch.rand(self.batch_size, 1, 201, 6) self.assert_batch_consistency( - F.griffinlim, batch, window, n_fft, hop, ws, power, - n_iter, momentum, length, 0, atol=5e-5) - - @parameterized.expand(list(itertools.product( - [8000, 16000, 44100], - [1, 2], - )), name_func=_name_from_args) + F.griffinlim, batch, window, n_fft, hop, ws, power, n_iter, momentum, length, 0, atol=5e-5 + ) + + @parameterized.expand( + list( + itertools.product( + [8000, 16000, 44100], + [1, 2], + ) + ), + name_func=_name_from_args, + ) def test_detect_pitch_frequency(self, sample_rate, n_channels): # Use different frequencies to ensure each item in the batch returns a # different answer. torch.manual_seed(0) frequencies = torch.randint(100, 1000, [self.batch_size]) - waveforms = torch.stack([ - common_utils.get_sinusoid( - frequency=frequency, sample_rate=sample_rate, - n_channels=n_channels, duration=5) - for frequency in frequencies - ]) - self.assert_batch_consistency( - F.detect_pitch_frequency, waveforms, sample_rate) + waveforms = torch.stack( + [ + common_utils.get_sinusoid( + frequency=frequency, sample_rate=sample_rate, n_channels=n_channels, duration=5 + ) + for frequency in frequencies + ] + ) + self.assert_batch_consistency(F.detect_pitch_frequency, waveforms, sample_rate) def test_amplitude_to_DB(self): torch.manual_seed(0) spec = torch.rand(self.batch_size, 2, 100, 100) * 200 - amplitude_mult = 20. + amplitude_mult = 20.0 amin = 1e-10 ref = 1.0 db_mult = math.log10(max(amin, ref)) # Test with & without a `top_db` clamp - self.assert_batch_consistency( - F.amplitude_to_DB, spec, amplitude_mult, - amin, db_mult, top_db=None) - self.assert_batch_consistency( - F.amplitude_to_DB, spec, amplitude_mult, - amin, db_mult, top_db=40.) + self.assert_batch_consistency(F.amplitude_to_DB, spec, amplitude_mult, amin, db_mult, top_db=None) + self.assert_batch_consistency(F.amplitude_to_DB, spec, amplitude_mult, amin, db_mult, top_db=40.0) def test_amplitude_to_DB_itemwise_clamps(self): """Ensure that the clamps are separate for each spectrogram in a batch. @@ -106,11 +104,11 @@ def test_amplitude_to_DB_itemwise_clamps(self): https://github.com/pytorch/audio/issues/994 """ - amplitude_mult = 20. + amplitude_mult = 20.0 amin = 1e-10 ref = 1.0 db_mult = math.log10(max(amin, ref)) - top_db = 20. + top_db = 20.0 # Make a batch of noise torch.manual_seed(0) @@ -118,36 +116,30 @@ def test_amplitude_to_DB_itemwise_clamps(self): # Make one item blow out the other spec[0] += 50 - batchwise_dbs = F.amplitude_to_DB(spec, amplitude_mult, amin, - db_mult, top_db=top_db) - itemwise_dbs = torch.stack([ - F.amplitude_to_DB(item, amplitude_mult, amin, - db_mult, top_db=top_db) - for item in spec - ]) + batchwise_dbs = F.amplitude_to_DB(spec, amplitude_mult, amin, db_mult, top_db=top_db) + itemwise_dbs = torch.stack( + [F.amplitude_to_DB(item, amplitude_mult, amin, db_mult, top_db=top_db) for item in spec] + ) self.assertEqual(batchwise_dbs, itemwise_dbs) def test_amplitude_to_DB_not_channelwise_clamps(self): """Check that clamps are applied per-item, not per channel.""" - amplitude_mult = 20. + amplitude_mult = 20.0 amin = 1e-10 ref = 1.0 db_mult = math.log10(max(amin, ref)) - top_db = 40. + top_db = 40.0 torch.manual_seed(0) spec = torch.rand([1, 2, 100, 100]) * 200 # Make one channel blow out the other spec[:, 0] += 50 - specwise_dbs = F.amplitude_to_DB(spec, amplitude_mult, amin, - db_mult, top_db=top_db) - channelwise_dbs = torch.stack([ - F.amplitude_to_DB(spec[:, i], amplitude_mult, amin, - db_mult, top_db=top_db) - for i in range(spec.size(-3)) - ]) + specwise_dbs = F.amplitude_to_DB(spec, amplitude_mult, amin, db_mult, top_db=top_db) + channelwise_dbs = torch.stack( + [F.amplitude_to_DB(spec[:, i], amplitude_mult, amin, db_mult, top_db=top_db) for i in range(spec.size(-3))] + ) # Just check channelwise gives a different answer. difference = (specwise_dbs - channelwise_dbs).abs() @@ -156,27 +148,24 @@ def test_amplitude_to_DB_not_channelwise_clamps(self): def test_contrast(self): torch.random.manual_seed(0) waveforms = torch.rand(self.batch_size, 2, 100) - 0.5 - self.assert_batch_consistency( - F.contrast, waveforms, enhancement_amount=80.) + self.assert_batch_consistency(F.contrast, waveforms, enhancement_amount=80.0) def test_dcshift(self): torch.random.manual_seed(0) waveforms = torch.rand(self.batch_size, 2, 100) - 0.5 - self.assert_batch_consistency( - F.dcshift, waveforms, shift=0.5, limiter_gain=0.05) + self.assert_batch_consistency(F.dcshift, waveforms, shift=0.5, limiter_gain=0.05) def test_overdrive(self): torch.random.manual_seed(0) waveforms = torch.rand(self.batch_size, 2, 100) - 0.5 - self.assert_batch_consistency( - F.overdrive, waveforms, gain=45, colour=30) + self.assert_batch_consistency(F.overdrive, waveforms, gain=45, colour=30) def test_phaser(self): sample_rate = 44100 n_channels = 2 waveform = common_utils.get_whitenoise( - sample_rate=sample_rate, n_channels=self.batch_size * n_channels, - duration=1) + sample_rate=sample_rate, n_channels=self.batch_size * n_channels, duration=1 + ) batch = waveform.view(self.batch_size, n_channels, waveform.size(-1)) self.assert_batch_consistency(F.phaser, batch, sample_rate) @@ -186,37 +175,48 @@ def test_flanger(self): sample_rate = 44100 self.assert_batch_consistency(F.flanger, waveforms, sample_rate) - @parameterized.expand(list(itertools.product( - [True, False], # center - [True, False], # norm_vars - )), name_func=_name_from_args) + @parameterized.expand( + list( + itertools.product( + [True, False], # center + [True, False], # norm_vars + ) + ), + name_func=_name_from_args, + ) def test_sliding_window_cmn(self, center, norm_vars): torch.manual_seed(0) spectrogram = torch.rand(self.batch_size, 2, 1024, 1024) * 200 - self.assert_batch_consistency( - F.sliding_window_cmn, spectrogram, center=center, - norm_vars=norm_vars) + self.assert_batch_consistency(F.sliding_window_cmn, spectrogram, center=center, norm_vars=norm_vars) @parameterized.expand([("sinc_interpolation"), ("kaiser_window")]) def test_resample_waveform(self, resampling_method): num_channels = 3 sr = 16000 new_sr = sr // 2 - multi_sound = common_utils.get_whitenoise(sample_rate=sr, n_channels=num_channels, duration=0.5,) + multi_sound = common_utils.get_whitenoise( + sample_rate=sr, + n_channels=num_channels, + duration=0.5, + ) self.assert_batch_consistency( - F.resample, multi_sound, orig_freq=sr, new_freq=new_sr, - resampling_method=resampling_method, rtol=1e-4, atol=1e-7) + F.resample, + multi_sound, + orig_freq=sr, + new_freq=new_sr, + resampling_method=resampling_method, + rtol=1e-4, + atol=1e-7, + ) @common_utils.skipIfNoKaldi def test_compute_kaldi_pitch(self): sample_rate = 44100 n_channels = 2 - waveform = common_utils.get_whitenoise( - sample_rate=sample_rate, n_channels=self.batch_size * n_channels) + waveform = common_utils.get_whitenoise(sample_rate=sample_rate, n_channels=self.batch_size * n_channels) batch = waveform.view(self.batch_size, n_channels, waveform.size(-1)) - self.assert_batch_consistency( - F.compute_kaldi_pitch, batch, sample_rate=sample_rate) + self.assert_batch_consistency(F.compute_kaldi_pitch, batch, sample_rate=sample_rate) def test_lfilter(self): signal_length = 2048 @@ -226,10 +226,7 @@ def test_lfilter(self): b = torch.rand(self.batch_size, 3) batchwise_output = F.lfilter(x, a, b, batching=True) - itemwise_output = torch.stack([ - F.lfilter(x[i], a[i], b[i]) - for i in range(self.batch_size) - ]) + itemwise_output = torch.stack([F.lfilter(x[i], a[i], b[i]) for i in range(self.batch_size)]) self.assertEqual(batchwise_output, itemwise_output) @@ -241,9 +238,6 @@ def test_filtfilt(self): b = torch.rand(self.batch_size, 3) batchwise_output = F.filtfilt(x, a, b) - itemwise_output = torch.stack([ - F.filtfilt(x[i], a[i], b[i]) - for i in range(self.batch_size) - ]) + itemwise_output = torch.stack([F.filtfilt(x[i], a[i], b[i]) for i in range(self.batch_size)]) self.assertEqual(batchwise_output, itemwise_output) diff --git a/test/torchaudio_unittest/functional/functional_cpu_test.py b/test/torchaudio_unittest/functional/functional_cpu_test.py index 520ff86b20..e43630cf76 100644 --- a/test/torchaudio_unittest/functional/functional_cpu_test.py +++ b/test/torchaudio_unittest/functional/functional_cpu_test.py @@ -1,15 +1,16 @@ +import unittest + import torch import torchaudio.functional as F -import unittest from parameterized import parameterized - from torchaudio_unittest.common_utils import PytorchTestCase, TorchaudioTestCase, skipIfNoSox + from .functional_impl import Functional, FunctionalCPUOnly class TestFunctionalFloat32(Functional, FunctionalCPUOnly, PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") @unittest.expectedFailure def test_lfilter_9th_order_filter_stability(self): @@ -18,7 +19,7 @@ def test_lfilter_9th_order_filter_stability(self): class TestFunctionalFloat64(Functional, PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") @skipIfNoSox @@ -36,12 +37,7 @@ def _smoke_test(self, format, compression, check_num_frames): num_channels = 2 waveform = torch.rand(num_channels, num_frames) - augmented = F.apply_codec(waveform, - sample_rate, - format, - True, - compression - ) + augmented = F.apply_codec(waveform, sample_rate, format, True, compression) assert augmented.dtype == waveform.dtype assert augmented.shape[0] == num_channels if check_num_frames: diff --git a/test/torchaudio_unittest/functional/functional_cuda_test.py b/test/torchaudio_unittest/functional/functional_cuda_test.py index fd35547270..dadad25612 100644 --- a/test/torchaudio_unittest/functional/functional_cuda_test.py +++ b/test/torchaudio_unittest/functional/functional_cuda_test.py @@ -1,14 +1,15 @@ -import torch import unittest +import torch from torchaudio_unittest.common_utils import PytorchTestCase, skipIfNoCuda + from .functional_impl import Functional @skipIfNoCuda class TestFunctionalFloat32(Functional, PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") @unittest.expectedFailure def test_lfilter_9th_order_filter_stability(self): @@ -18,4 +19,4 @@ def test_lfilter_9th_order_filter_stability(self): @skipIfNoCuda class TestLFilterFloat64(Functional, PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/functional/functional_impl.py b/test/torchaudio_unittest/functional/functional_impl.py index bef8d5ed89..4d088b44e5 100644 --- a/test/torchaudio_unittest/functional/functional_impl.py +++ b/test/torchaudio_unittest/functional/functional_impl.py @@ -1,6 +1,6 @@ """Test definition common to CPU and CUDA""" -import math import itertools +import math import warnings import numpy as np @@ -8,7 +8,6 @@ import torchaudio.functional as F from parameterized import parameterized from scipy import signal - from torchaudio_unittest.common_utils import ( TestBaseMixin, get_sinusoid, @@ -19,8 +18,9 @@ class Functional(TestBaseMixin): - def _test_resample_waveform_accuracy(self, up_scale_factor=None, down_scale_factor=None, - resampling_method="sinc_interpolation", atol=1e-1, rtol=1e-4): + def _test_resample_waveform_accuracy( + self, up_scale_factor=None, down_scale_factor=None, resampling_method="sinc_interpolation", atol=1e-1, rtol=1e-4 + ): # resample the signal and compare it to the ground truth n_to_trim = 20 sample_rate = 1000 @@ -36,10 +36,9 @@ def _test_resample_waveform_accuracy(self, up_scale_factor=None, down_scale_fact original_timestamps = torch.arange(0, duration, 1.0 / sample_rate) sound = 123 * torch.cos(2 * math.pi * 3 * original_timestamps).unsqueeze(0) - estimate = F.resample(sound, sample_rate, new_sample_rate, - resampling_method=resampling_method).squeeze() + estimate = F.resample(sound, sample_rate, new_sample_rate, resampling_method=resampling_method).squeeze() - new_timestamps = torch.arange(0, duration, 1.0 / new_sample_rate)[:estimate.size(0)] + new_timestamps = torch.arange(0, duration, 1.0 / new_sample_rate)[: estimate.size(0)] ground_truth = 123 * torch.cos(2 * math.pi * 3 * new_timestamps) # trim the first/last n samples as these points have boundary effects @@ -48,9 +47,7 @@ def _test_resample_waveform_accuracy(self, up_scale_factor=None, down_scale_fact self.assertEqual(estimate, ground_truth, atol=atol, rtol=rtol) - def _test_costs_and_gradients( - self, data, ref_costs, ref_gradients, atol=1e-6, rtol=1e-2 - ): + def _test_costs_and_gradients(self, data, ref_costs, ref_gradients, atol=1e-6, rtol=1e-2): logits_shape = data["logits"].shape costs, gradients = rnnt_utils.compute_with_pytorch_transducer(data=data) self.assertEqual(costs, ref_costs, atol=atol, rtol=rtol) @@ -81,15 +78,41 @@ def test_lfilter_clamp(self): output_signal = F.lfilter(input_signal, a_coeffs, b_coeffs, clamp=False) assert output_signal.max() > 1 - @parameterized.expand([ - ((44100,), (4,), (44100,)), - ((3, 44100), (4,), (3, 44100,)), - ((2, 3, 44100), (4,), (2, 3, 44100,)), - ((1, 2, 3, 44100), (4,), (1, 2, 3, 44100,)), - ((44100,), (2, 4), (2, 44100)), - ((3, 44100), (1, 4), (3, 1, 44100)), - ((1, 2, 44100), (3, 4), (1, 2, 3, 44100)) - ]) + @parameterized.expand( + [ + ((44100,), (4,), (44100,)), + ( + (3, 44100), + (4,), + ( + 3, + 44100, + ), + ), + ( + (2, 3, 44100), + (4,), + ( + 2, + 3, + 44100, + ), + ), + ( + (1, 2, 3, 44100), + (4,), + ( + 1, + 2, + 3, + 44100, + ), + ), + ((44100,), (2, 4), (2, 44100)), + ((3, 44100), (1, 4), (3, 1, 44100)), + ((1, 2, 44100), (3, 4), (1, 2, 3, 44100)), + ] + ) def test_lfilter_shape(self, input_shape, coeff_shape, target_shape): torch.random.manual_seed(42) waveform = torch.rand(*input_shape, dtype=self.dtype, device=self.device) @@ -109,13 +132,12 @@ def test_lfilter_9th_order_filter_stability(self): x[0] = 1 # get target impulse response - sos = signal.butter(9, 850, 'hp', fs=22050, output='sos') + sos = signal.butter(9, 850, "hp", fs=22050, output="sos") y = torch.from_numpy(signal.sosfilt(sos, x.cpu().numpy())).to(self.dtype).to(self.device) # get lfilter coefficients - b, a = signal.butter(9, 850, 'hp', fs=22050, output='ba') - b, a = torch.from_numpy(b).to(self.dtype).to(self.device), torch.from_numpy( - a).to(self.dtype).to(self.device) + b, a = signal.butter(9, 850, "hp", fs=22050, output="ba") + b, a = torch.from_numpy(b).to(self.dtype).to(self.device), torch.from_numpy(a).to(self.dtype).to(self.device) # predict impulse response yhat = F.lfilter(x, a, b, False) @@ -126,14 +148,10 @@ def test_filtfilt_simple(self): Check that, for an arbitrary signal, applying filtfilt with filter coefficients corresponding to a pure delay filter imparts no time delay. """ - waveform = get_whitenoise(sample_rate=8000, n_channels=2, dtype=self.dtype).to( - device=self.device - ) + waveform = get_whitenoise(sample_rate=8000, n_channels=2, dtype=self.dtype).to(device=self.device) b_coeffs = torch.tensor([0, 0, 0, 1], dtype=self.dtype, device=self.device) a_coeffs = torch.tensor([1, 0, 0, 0], dtype=self.dtype, device=self.device) - padded_waveform = torch.cat( - (waveform, torch.zeros(2, 3, dtype=self.dtype, device=self.device)), axis=1 - ) + padded_waveform = torch.cat((waveform, torch.zeros(2, 3, dtype=self.dtype, device=self.device)), axis=1) output_waveform = F.filtfilt(padded_waveform, a_coeffs, b_coeffs) self.assertEqual(output_waveform, padded_waveform, atol=1e-5, rtol=1e-5) @@ -147,9 +165,9 @@ def test_filtfilt_filter_sinusoid(self): T = 1.0 samples = 1000 - waveform_k0 = get_sinusoid( - frequency=5, sample_rate=samples // T, dtype=self.dtype, device=self.device - ).squeeze(0) + waveform_k0 = get_sinusoid(frequency=5, sample_rate=samples // T, dtype=self.dtype, device=self.device).squeeze( + 0 + ) waveform_k1 = get_sinusoid( frequency=200, sample_rate=samples // T, @@ -202,13 +220,13 @@ def test_filtfilt_filter_sinusoid(self): # Remove padding from output waveform; confirm that result # closely matches waveform_k0. self.assertEqual( - output_waveform[samples - 1: 2 * samples - 1], + output_waveform[samples - 1 : 2 * samples - 1], waveform_k0, atol=1e-3, rtol=1e-3, ) - @parameterized.expand([(0., ), (1., ), (2., ), (3., )]) + @parameterized.expand([(0.0,), (1.0,), (2.0,), (3.0,)]) def test_spectrogram_grad_at_zero(self, power): """The gradient of power spectrogram should not be nan but zero near x=0 @@ -235,19 +253,15 @@ def test_compute_deltas_one_channel(self): self.assertEqual(computed, expected) def test_compute_deltas_two_channels(self): - specgram = torch.tensor([[[1.0, 2.0, 3.0, 4.0], - [1.0, 2.0, 3.0, 4.0]]], dtype=self.dtype, device=self.device) - expected = torch.tensor([[[0.5, 1.0, 1.0, 0.5], - [0.5, 1.0, 1.0, 0.5]]], dtype=self.dtype, device=self.device) + specgram = torch.tensor([[[1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]]], dtype=self.dtype, device=self.device) + expected = torch.tensor([[[0.5, 1.0, 1.0, 0.5], [0.5, 1.0, 1.0, 0.5]]], dtype=self.dtype, device=self.device) computed = F.compute_deltas(specgram, win_length=3) self.assertEqual(computed, expected) @parameterized.expand([(100,), (440,)]) def test_detect_pitch_frequency_pitch(self, frequency): sample_rate = 44100 - test_sine_waveform = get_sinusoid( - frequency=frequency, sample_rate=sample_rate, duration=5 - ) + test_sine_waveform = get_sinusoid(frequency=frequency, sample_rate=sample_rate, duration=5) freq = F.detect_pitch_frequency(test_sine_waveform, sample_rate) @@ -262,8 +276,8 @@ def test_amplitude_to_DB_reversible(self, shape): This implicitly also tests `DB_to_amplitude`. """ - amplitude_mult = 20. - power_mult = 10. + amplitude_mult = 20.0 + power_mult = 10.0 amin = 1e-10 ref = 1.0 db_mult = math.log10(max(amin, ref)) @@ -279,18 +293,18 @@ def test_amplitude_to_DB_reversible(self, shape): # Spectrogram power -> DB -> power db = F.amplitude_to_DB(spec, power_mult, amin, db_mult, top_db=None) - x2 = F.DB_to_amplitude(db, ref, 1.) + x2 = F.DB_to_amplitude(db, ref, 1.0) self.assertEqual(x2, spec) @parameterized.expand([([100, 100],), ([2, 100, 100],), ([2, 2, 100, 100],)]) def test_amplitude_to_DB_top_db_clamp(self, shape): """Ensure values are properly clamped when `top_db` is supplied.""" - amplitude_mult = 20. + amplitude_mult = 20.0 amin = 1e-10 ref = 1.0 db_mult = math.log10(max(amin, ref)) - top_db = 40. + top_db = 40.0 torch.manual_seed(0) # A random tensor is used for increased entropy, but the max and min for @@ -304,24 +318,17 @@ def test_amplitude_to_DB_top_db_clamp(self, shape): # Expand the range to (0, 200) - wide enough to properly test clamping. spec *= 200 - decibels = F.amplitude_to_DB(spec, amplitude_mult, amin, - db_mult, top_db=top_db) + decibels = F.amplitude_to_DB(spec, amplitude_mult, amin, db_mult, top_db=top_db) # Ensure the clamp was applied below_limit = decibels < 6.0205 - assert not below_limit.any(), ( - "{} decibel values were below the expected cutoff:\n{}".format( - below_limit.sum().item(), decibels - ) + assert not below_limit.any(), "{} decibel values were below the expected cutoff:\n{}".format( + below_limit.sum().item(), decibels ) # Ensure it didn't over-clamp close_to_limit = decibels < 6.0207 - assert close_to_limit.any(), ( - f"No values were close to the limit. Did it over-clamp?\n{decibels}" - ) + assert close_to_limit.any(), f"No values were close to the limit. Did it over-clamp?\n{decibels}" - @parameterized.expand( - list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0., 30.], [1, 2])) - ) + @parameterized.expand(list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0.0, 30.0], [1, 2]))) def test_mask_along_axis(self, shape, mask_param, mask_value, axis): torch.random.manual_seed(42) specgram = torch.randn(*shape, dtype=self.dtype, device=self.device) @@ -331,13 +338,12 @@ def test_mask_along_axis(self, shape, mask_param, mask_value, axis): masked_columns = (mask_specgram == mask_value).sum(other_axis) num_masked_columns = (masked_columns == mask_specgram.size(other_axis)).sum() - num_masked_columns = torch.div( - num_masked_columns, mask_specgram.size(0), rounding_mode='floor') + num_masked_columns = torch.div(num_masked_columns, mask_specgram.size(0), rounding_mode="floor") assert mask_specgram.size() == specgram.size() assert num_masked_columns < mask_param - @parameterized.expand(list(itertools.product([100], [0., 30.], [2, 3]))) + @parameterized.expand(list(itertools.product([100], [0.0, 30.0], [2, 3]))) def test_mask_along_axis_iid(self, mask_param, mask_value, axis): torch.random.manual_seed(42) specgrams = torch.randn(4, 2, 1025, 400, dtype=self.dtype, device=self.device) @@ -352,9 +358,7 @@ def test_mask_along_axis_iid(self, mask_param, mask_value, axis): assert mask_specgrams.size() == specgrams.size() assert (num_masked_columns < mask_param).sum() == num_masked_columns.numel() - @parameterized.expand( - list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0., 30.], [1, 2])) - ) + @parameterized.expand(list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0.0, 30.0], [1, 2]))) def test_mask_along_axis_preserve(self, shape, mask_param, mask_value, axis): """mask_along_axis should not alter original input Tensor @@ -369,7 +373,7 @@ def test_mask_along_axis_preserve(self, shape, mask_param, mask_value, axis): self.assertEqual(specgram, specgram_copy) - @parameterized.expand(list(itertools.product([100], [0., 30.], [2, 3]))) + @parameterized.expand(list(itertools.product([100], [0.0, 30.0], [2, 3]))) def test_mask_along_axis_iid_preserve(self, mask_param, mask_value, axis): """mask_along_axis_iid should not alter original input Tensor @@ -384,10 +388,14 @@ def test_mask_along_axis_iid_preserve(self, mask_param, mask_value, axis): self.assertEqual(specgrams, specgrams_copy) - @parameterized.expand(list(itertools.product( - ["sinc_interpolation", "kaiser_window"], - [16000, 44100], - ))) + @parameterized.expand( + list( + itertools.product( + ["sinc_interpolation", "kaiser_window"], + [16000, 44100], + ) + ) + ) def test_resample_identity(self, resampling_method, sample_rate): waveform = get_whitenoise(sample_rate=sample_rate, duration=1) @@ -397,35 +405,52 @@ def test_resample_identity(self, resampling_method, sample_rate): @parameterized.expand([("sinc_interpolation"), ("kaiser_window")]) def test_resample_waveform_upsample_size(self, resampling_method): sr = 16000 - waveform = get_whitenoise(sample_rate=sr, duration=0.5,) + waveform = get_whitenoise( + sample_rate=sr, + duration=0.5, + ) upsampled = F.resample(waveform, sr, sr * 2, resampling_method=resampling_method) assert upsampled.size(-1) == waveform.size(-1) * 2 @parameterized.expand([("sinc_interpolation"), ("kaiser_window")]) def test_resample_waveform_downsample_size(self, resampling_method): sr = 16000 - waveform = get_whitenoise(sample_rate=sr, duration=0.5,) + waveform = get_whitenoise( + sample_rate=sr, + duration=0.5, + ) downsampled = F.resample(waveform, sr, sr // 2, resampling_method=resampling_method) assert downsampled.size(-1) == waveform.size(-1) // 2 @parameterized.expand([("sinc_interpolation"), ("kaiser_window")]) def test_resample_waveform_identity_size(self, resampling_method): sr = 16000 - waveform = get_whitenoise(sample_rate=sr, duration=0.5,) + waveform = get_whitenoise( + sample_rate=sr, + duration=0.5, + ) resampled = F.resample(waveform, sr, sr, resampling_method=resampling_method) assert resampled.size(-1) == waveform.size(-1) - @parameterized.expand(list(itertools.product( - ["sinc_interpolation", "kaiser_window"], - list(range(1, 20)), - ))) + @parameterized.expand( + list( + itertools.product( + ["sinc_interpolation", "kaiser_window"], + list(range(1, 20)), + ) + ) + ) def test_resample_waveform_downsample_accuracy(self, resampling_method, i): self._test_resample_waveform_accuracy(down_scale_factor=i * 2, resampling_method=resampling_method) - @parameterized.expand(list(itertools.product( - ["sinc_interpolation", "kaiser_window"], - list(range(1, 20)), - ))) + @parameterized.expand( + list( + itertools.product( + ["sinc_interpolation", "kaiser_window"], + list(range(1, 20)), + ) + ) + ) def test_resample_waveform_upsample_accuracy(self, resampling_method, i): self._test_resample_waveform_accuracy(up_scale_factor=1.0 + i / 20.0, resampling_method=resampling_method) @@ -438,14 +463,9 @@ def test_phase_vocoder_shape(self, rate): batch_size = 2 torch.random.manual_seed(42) - spec = torch.randn( - batch_size, num_freq, num_frames, dtype=self.complex_dtype, device=self.device) + spec = torch.randn(batch_size, num_freq, num_frames, dtype=self.complex_dtype, device=self.device) - phase_advance = torch.linspace( - 0, - np.pi * hop_length, - num_freq, - dtype=self.dtype, device=self.device)[..., None] + phase_advance = torch.linspace(0, np.pi * hop_length, num_freq, dtype=self.dtype, device=self.device)[..., None] spec_stretch = F.phase_vocoder(spec, rate=rate, phase_advance=phase_advance) @@ -460,32 +480,31 @@ def test_phase_vocoder_shape(self, rate): ["", "", 0], # equal ["abc", "abc", 0], ["ᑌᑎIᑕO", "ᑌᑎIᑕO", 0], - ["abc", "", 3], # deletion ["aa", "aaa", 1], ["aaa", "aa", 1], ["ᑌᑎI", "ᑌᑎIᑕO", 2], - ["aaa", "aba", 1], # substitution ["aba", "aaa", 1], ["aba", " ", 3], - ["abc", "bcd", 2], # mix deletion and substitution ["0ᑌᑎI", "ᑌᑎIᑕO", 3], - # sentences [["hello", "", "Tᕮ᙭T"], ["hello", "", "Tᕮ᙭T"], 0], # equal [[], [], 0], - [["hello", "world"], ["hello", "world", "!"], 1], # deletion [["hello", "world"], ["world"], 1], [["hello", "world"], [], 2], - - [["Tᕮ᙭T", ], ["world"], 1], # substitution + [ + [ + "Tᕮ᙭T", + ], + ["world"], + 1, + ], # substitution [["Tᕮ᙭T", "XD"], ["world", "hello"], 2], [["", "XD"], ["world", ""], 2], ["aba", " ", 3], - [["hello", "world"], ["world", "hello", "!"], 2], # mix deletion and substitution [["Tᕮ᙭T", "world", "LOL", "XD"], ["world", "hello", "ʕ•́ᴥ•̀ʔっ"], 3], ] @@ -520,12 +539,14 @@ def test_rnnt_loss_basic_forward_no_grad(self): logits.requires_grad_(False) F.rnnt_loss(logits, targets, logit_lengths, target_lengths) - @parameterized.expand([ - (rnnt_utils.get_B1_T2_U3_D5_data, torch.float32, 1e-6, 1e-2), - (rnnt_utils.get_B2_T4_U3_D3_data, torch.float32, 1e-6, 1e-2), - (rnnt_utils.get_B1_T2_U3_D5_data, torch.float16, 1e-3, 1e-2), - (rnnt_utils.get_B2_T4_U3_D3_data, torch.float16, 1e-3, 1e-2), - ]) + @parameterized.expand( + [ + (rnnt_utils.get_B1_T2_U3_D5_data, torch.float32, 1e-6, 1e-2), + (rnnt_utils.get_B2_T4_U3_D3_data, torch.float32, 1e-6, 1e-2), + (rnnt_utils.get_B1_T2_U3_D5_data, torch.float16, 1e-3, 1e-2), + (rnnt_utils.get_B2_T4_U3_D3_data, torch.float16, 1e-3, 1e-2), + ] + ) def test_rnnt_loss_costs_and_gradients(self, data_func, dtype, atol, rtol): data, ref_costs, ref_gradients = data_func( dtype=dtype, @@ -544,9 +565,7 @@ def test_rnnt_loss_costs_and_gradients_random_data_with_numpy_fp32(self): for i in range(5): data = rnnt_utils.get_random_data(dtype=torch.float32, device=self.device, seed=(seed + i)) ref_costs, ref_gradients = rnnt_utils.compute_with_numpy_transducer(data=data) - self._test_costs_and_gradients( - data=data, ref_costs=ref_costs, ref_gradients=ref_gradients - ) + self._test_costs_and_gradients(data=data, ref_costs=ref_costs, ref_gradients=ref_gradients) class FunctionalCPUOnly(TestBaseMixin): diff --git a/test/torchaudio_unittest/functional/kaldi_compatibility_cpu_test.py b/test/torchaudio_unittest/functional/kaldi_compatibility_cpu_test.py index 90a634746c..2c2a0de8e0 100644 --- a/test/torchaudio_unittest/functional/kaldi_compatibility_cpu_test.py +++ b/test/torchaudio_unittest/functional/kaldi_compatibility_cpu_test.py @@ -1,19 +1,19 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase + from .kaldi_compatibility_test_impl import Kaldi, KaldiCPUOnly class TestKaldiCPUOnly(KaldiCPUOnly, PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") class TestKaldiFloat32(Kaldi, PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") class TestKaldiFloat64(Kaldi, PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/functional/kaldi_compatibility_cuda_test.py b/test/torchaudio_unittest/functional/kaldi_compatibility_cuda_test.py index 47a1bea338..ffbe437457 100644 --- a/test/torchaudio_unittest/functional/kaldi_compatibility_cuda_test.py +++ b/test/torchaudio_unittest/functional/kaldi_compatibility_cuda_test.py @@ -1,16 +1,16 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase, skipIfNoCuda + from .kaldi_compatibility_test_impl import Kaldi @skipIfNoCuda class TestKaldiFloat32(Kaldi, PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") @skipIfNoCuda class TestKaldiFloat64(Kaldi, PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/functional/kaldi_compatibility_test_impl.py b/test/torchaudio_unittest/functional/kaldi_compatibility_test_impl.py index bad30afd9a..4f408405b2 100644 --- a/test/torchaudio_unittest/functional/kaldi_compatibility_test_impl.py +++ b/test/torchaudio_unittest/functional/kaldi_compatibility_test_impl.py @@ -1,7 +1,6 @@ -from parameterized import parameterized import torch import torchaudio.functional as F - +from parameterized import parameterized from torchaudio_unittest.common_utils import ( get_sinusoid, load_params, @@ -21,20 +20,20 @@ def assert_equal(self, output, *, expected, rtol=None, atol=None): expected = expected.to(dtype=self.dtype, device=self.device) self.assertEqual(output, expected, rtol=rtol, atol=atol) - @skipIfNoExec('apply-cmvn-sliding') + @skipIfNoExec("apply-cmvn-sliding") def test_sliding_window_cmn(self): """sliding_window_cmn should be numerically compatible with apply-cmvn-sliding""" kwargs = { - 'cmn_window': 600, - 'min_cmn_window': 100, - 'center': False, - 'norm_vars': False, + "cmn_window": 600, + "min_cmn_window": 100, + "center": False, + "norm_vars": False, } tensor = torch.randn(40, 10, dtype=self.dtype, device=self.device) result = F.sliding_window_cmn(tensor, **kwargs) - command = ['apply-cmvn-sliding'] + convert_args(**kwargs) + ['ark:-', 'ark:-'] - kaldi_result = run_kaldi(command, 'ark', tensor) + command = ["apply-cmvn-sliding"] + convert_args(**kwargs) + ["ark:-", "ark:-"] + kaldi_result = run_kaldi(command, "ark", tensor) self.assert_equal(result, expected=kaldi_result) @@ -43,18 +42,18 @@ def assert_equal(self, output, *, expected, rtol=None, atol=None): expected = expected.to(dtype=self.dtype, device=self.device) self.assertEqual(output, expected, rtol=rtol, atol=atol) - @parameterized.expand(load_params('kaldi_test_pitch_args.jsonl')) - @skipIfNoExec('compute-kaldi-pitch-feats') + @parameterized.expand(load_params("kaldi_test_pitch_args.jsonl")) + @skipIfNoExec("compute-kaldi-pitch-feats") def test_pitch_feats(self, kwargs): """compute_kaldi_pitch produces numerically compatible result with compute-kaldi-pitch-feats""" - sample_rate = kwargs['sample_rate'] - waveform = get_sinusoid(dtype='float32', sample_rate=sample_rate) + sample_rate = kwargs["sample_rate"] + waveform = get_sinusoid(dtype="float32", sample_rate=sample_rate) result = F.compute_kaldi_pitch(waveform[0], **kwargs) - waveform = get_sinusoid(dtype='int16', sample_rate=sample_rate) - wave_file = self.get_temp_path('test.wav') + waveform = get_sinusoid(dtype="int16", sample_rate=sample_rate) + wave_file = self.get_temp_path("test.wav") save_wav(wave_file, waveform, sample_rate) - command = ['compute-kaldi-pitch-feats'] + convert_args(**kwargs) + ['scp:-', 'ark:-'] - kaldi_result = run_kaldi(command, 'scp', wave_file) + command = ["compute-kaldi-pitch-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] + kaldi_result = run_kaldi(command, "scp", wave_file) self.assert_equal(result, expected=kaldi_result) diff --git a/test/torchaudio_unittest/functional/librosa_compatibility_cpu_test.py b/test/torchaudio_unittest/functional/librosa_compatibility_cpu_test.py index dcfe6fcf4d..f5d1477edb 100644 --- a/test/torchaudio_unittest/functional/librosa_compatibility_cpu_test.py +++ b/test/torchaudio_unittest/functional/librosa_compatibility_cpu_test.py @@ -1,10 +1,11 @@ from torchaudio_unittest.common_utils import PytorchTestCase + from .librosa_compatibility_test_impl import Functional, FunctionalComplex class TestFunctionalCPU(Functional, PytorchTestCase): - device = 'cpu' + device = "cpu" class TestFunctionalComplexCPU(FunctionalComplex, PytorchTestCase): - device = 'cpu' + device = "cpu" diff --git a/test/torchaudio_unittest/functional/librosa_compatibility_cuda_test.py b/test/torchaudio_unittest/functional/librosa_compatibility_cuda_test.py index 9f42b4dc84..3845ab1aa4 100644 --- a/test/torchaudio_unittest/functional/librosa_compatibility_cuda_test.py +++ b/test/torchaudio_unittest/functional/librosa_compatibility_cuda_test.py @@ -1,12 +1,13 @@ from torchaudio_unittest.common_utils import PytorchTestCase, skipIfNoCuda + from .librosa_compatibility_test_impl import Functional, FunctionalComplex @skipIfNoCuda class TestFunctionalCUDA(Functional, PytorchTestCase): - device = 'cuda' + device = "cuda" @skipIfNoCuda class TestFunctionalComplexCUDA(FunctionalComplex, PytorchTestCase): - device = 'cuda' + device = "cuda" diff --git a/test/torchaudio_unittest/functional/librosa_compatibility_test_impl.py b/test/torchaudio_unittest/functional/librosa_compatibility_test_impl.py index 15d696a0d2..1edc6c74c5 100644 --- a/test/torchaudio_unittest/functional/librosa_compatibility_test_impl.py +++ b/test/torchaudio_unittest/functional/librosa_compatibility_test_impl.py @@ -2,16 +2,15 @@ from distutils.version import StrictVersion import torch -from parameterized import param - import torchaudio.functional as F +from parameterized import param from torchaudio._internal.module_utils import is_module_available -LIBROSA_AVAILABLE = is_module_available('librosa') +LIBROSA_AVAILABLE = is_module_available("librosa") if LIBROSA_AVAILABLE: - import numpy as np import librosa + import numpy as np from torchaudio_unittest.common_utils import ( @@ -25,6 +24,7 @@ @unittest.skipIf(not LIBROSA_AVAILABLE, "Librosa not available") class Functional(TestBaseMixin): """Test suite for functions in `functional` module.""" + dtype = torch.float64 @nested_params([0, 0.99]) @@ -40,8 +40,8 @@ def test_griffinlim(self, momentum): waveform = get_whitenoise(device=self.device, dtype=self.dtype) specgram = get_spectrogram( - waveform, n_fft=n_fft, hop_length=hop_length, power=power, - win_length=win_length, window=window) + waveform, n_fft=n_fft, hop_length=hop_length, power=power, win_length=win_length, window=window + ) result = F.griffinlim( specgram, @@ -53,14 +53,16 @@ def test_griffinlim(self, momentum): n_iter=n_iter, momentum=momentum, length=waveform.size(1), - rand_init=False) + rand_init=False, + ) expected = librosa.griffinlim( specgram[0].cpu().numpy(), n_iter=n_iter, hop_length=hop_length, momentum=momentum, init=None, - length=waveform.size(1))[None, ...] + length=waveform.size(1), + )[None, ...] self.assertEqual(result, torch.from_numpy(expected), atol=5e-5, rtol=1e-07) @nested_params( @@ -73,24 +75,20 @@ def test_griffinlim(self, momentum): param(n_mels=56, fmin=1900.0, fmax=900.0), param(n_mels=10, fmin=1900.0, fmax=900.0), ], - [param(norm=n) for n in [None, 'slaney']], - [param(mel_scale=s) for s in ['htk', 'slaney']], + [param(norm=n) for n in [None, "slaney"]], + [param(mel_scale=s) for s in ["htk", "slaney"]], ) - def test_create_mel_fb(self, n_mels=40, sample_rate=22050, n_fft=2048, - fmin=0.0, fmax=8000.0, norm=None, mel_scale="htk"): - if (norm == "slaney" and StrictVersion(librosa.__version__) < StrictVersion("0.7.2")): - self.skipTest('Test is known to fail with older versions of librosa.') - if self.device != 'cpu': - self.skipTest('No need to run this test on CUDA') + def test_create_mel_fb( + self, n_mels=40, sample_rate=22050, n_fft=2048, fmin=0.0, fmax=8000.0, norm=None, mel_scale="htk" + ): + if norm == "slaney" and StrictVersion(librosa.__version__) < StrictVersion("0.7.2"): + self.skipTest("Test is known to fail with older versions of librosa.") + if self.device != "cpu": + self.skipTest("No need to run this test on CUDA") expected = librosa.filters.mel( - sr=sample_rate, - n_fft=n_fft, - n_mels=n_mels, - fmax=fmax, - fmin=fmin, - htk=mel_scale == "htk", - norm=norm).T + sr=sample_rate, n_fft=n_fft, n_mels=n_mels, fmax=fmax, fmin=fmin, htk=mel_scale == "htk", norm=norm + ).T result = F.melscale_fbanks( sample_rate=sample_rate, n_mels=n_mels, @@ -98,7 +96,8 @@ def test_create_mel_fb(self, n_mels=40, sample_rate=22050, n_fft=2048, f_min=fmin, n_freqs=(n_fft // 2 + 1), norm=norm, - mel_scale=mel_scale) + mel_scale=mel_scale, + ) self.assertEqual(result, torch.from_numpy(expected), atol=7e-5, rtol=1.3e-6) def test_amplitude_to_DB_power(self): @@ -137,18 +136,12 @@ def test_phase_vocoder(self, rate): # result in bottom right values of the stretched sectrogram to not # match with librosa. spec = torch.randn(num_freq, num_frames, device=self.device, dtype=torch.complex128) - phase_advance = torch.linspace( - 0, - np.pi * hop_length, - num_freq, - device=self.device, - dtype=torch.float64)[..., None] + phase_advance = torch.linspace(0, np.pi * hop_length, num_freq, device=self.device, dtype=torch.float64)[ + ..., None + ] stretched = F.phase_vocoder(spec, rate=rate, phase_advance=phase_advance) - expected_stretched = librosa.phase_vocoder( - spec.cpu().numpy(), - rate=rate, - hop_length=hop_length) + expected_stretched = librosa.phase_vocoder(spec.cpu().numpy(), rate=rate, hop_length=hop_length) self.assertEqual(stretched, torch.from_numpy(expected_stretched)) diff --git a/test/torchaudio_unittest/functional/sox_compatibility_test.py b/test/torchaudio_unittest/functional/sox_compatibility_test.py index fe9744f22a..c05ec51b77 100644 --- a/test/torchaudio_unittest/functional/sox_compatibility_test.py +++ b/test/torchaudio_unittest/functional/sox_compatibility_test.py @@ -1,6 +1,5 @@ import torch import torchaudio.functional as F - from torchaudio_unittest.common_utils import ( skipIfNoSox, skipIfNoExec, @@ -15,10 +14,10 @@ @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") class TestFunctionalFiltering(TempDirMixin, TorchaudioTestCase): def run_sox_effect(self, input_file, effect): - output_file = self.get_temp_path('expected.wav') + output_file = self.get_temp_path("expected.wav") sox_utils.run_sox_effect(input_file, output_file, [str(e) for e in effect]) return load_wav(output_file) @@ -28,29 +27,31 @@ def assert_sox_effect(self, result, input_path, effects, atol=1e-04, rtol=1e-5): def get_whitenoise(self, sample_rate=8000): noise = get_whitenoise( - sample_rate=sample_rate, duration=3, scale_factor=0.9, + sample_rate=sample_rate, + duration=3, + scale_factor=0.9, ) path = self.get_temp_path("whitenoise.wav") save_wav(path, noise, sample_rate) return noise, path def test_gain(self): - path = get_asset_path('steam-train-whistle-daniel_simon.wav') + path = get_asset_path("steam-train-whistle-daniel_simon.wav") data, _ = load_wav(path) result = F.gain(data, 3) - self.assert_sox_effect(result, path, ['gain', 3]) + self.assert_sox_effect(result, path, ["gain", 3]) def test_dither(self): - path = get_asset_path('steam-train-whistle-daniel_simon.wav') + path = get_asset_path("steam-train-whistle-daniel_simon.wav") data, _ = load_wav(path) result = F.dither(data) - self.assert_sox_effect(result, path, ['dither']) + self.assert_sox_effect(result, path, ["dither"]) def test_dither_noise(self): - path = get_asset_path('steam-train-whistle-daniel_simon.wav') + path = get_asset_path("steam-train-whistle-daniel_simon.wav") data, _ = load_wav(path) result = F.dither(data, noise_shaping=True) - self.assert_sox_effect(result, path, ['dither', '-s'], atol=1.5e-4) + self.assert_sox_effect(result, path, ["dither", "-s"], atol=1.5e-4) def test_lowpass(self): cutoff_freq = 3000 @@ -58,7 +59,7 @@ def test_lowpass(self): data, path = self.get_whitenoise(sample_rate) result = F.lowpass_biquad(data, sample_rate, cutoff_freq) - self.assert_sox_effect(result, path, ['lowpass', cutoff_freq], atol=1.5e-4) + self.assert_sox_effect(result, path, ["lowpass", cutoff_freq], atol=1.5e-4) def test_highpass(self): cutoff_freq = 2000 @@ -66,7 +67,7 @@ def test_highpass(self): data, path = self.get_whitenoise(sample_rate) result = F.highpass_biquad(data, sample_rate, cutoff_freq) - self.assert_sox_effect(result, path, ['highpass', cutoff_freq], atol=1.5e-4) + self.assert_sox_effect(result, path, ["highpass", cutoff_freq], atol=1.5e-4) def test_allpass(self): central_freq = 1000 @@ -75,7 +76,7 @@ def test_allpass(self): data, path = self.get_whitenoise(sample_rate) result = F.allpass_biquad(data, sample_rate, central_freq, q) - self.assert_sox_effect(result, path, ['allpass', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["allpass", central_freq, f"{q}q"]) def test_bandpass_with_csg(self): central_freq = 1000 @@ -85,7 +86,7 @@ def test_bandpass_with_csg(self): data, path = self.get_whitenoise(sample_rate) result = F.bandpass_biquad(data, sample_rate, central_freq, q, const_skirt_gain) - self.assert_sox_effect(result, path, ['bandpass', '-c', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["bandpass", "-c", central_freq, f"{q}q"]) def test_bandpass_without_csg(self): central_freq = 1000 @@ -95,7 +96,7 @@ def test_bandpass_without_csg(self): data, path = self.get_whitenoise(sample_rate) result = F.bandpass_biquad(data, sample_rate, central_freq, q, const_skirt_gain) - self.assert_sox_effect(result, path, ['bandpass', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["bandpass", central_freq, f"{q}q"]) def test_bandreject(self): central_freq = 1000 @@ -104,7 +105,7 @@ def test_bandreject(self): data, path = self.get_whitenoise(sample_rate) result = F.bandreject_biquad(data, sample_rate, central_freq, q) - self.assert_sox_effect(result, path, ['bandreject', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["bandreject", central_freq, f"{q}q"]) def test_band_with_noise(self): central_freq = 1000 @@ -114,7 +115,7 @@ def test_band_with_noise(self): data, path = self.get_whitenoise(sample_rate) result = F.band_biquad(data, sample_rate, central_freq, q, noise) - self.assert_sox_effect(result, path, ['band', '-n', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["band", "-n", central_freq, f"{q}q"]) def test_band_without_noise(self): central_freq = 1000 @@ -124,7 +125,7 @@ def test_band_without_noise(self): data, path = self.get_whitenoise(sample_rate) result = F.band_biquad(data, sample_rate, central_freq, q, noise) - self.assert_sox_effect(result, path, ['band', central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["band", central_freq, f"{q}q"]) def test_treble(self): central_freq = 1000 @@ -134,7 +135,7 @@ def test_treble(self): data, path = self.get_whitenoise(sample_rate) result = F.treble_biquad(data, sample_rate, gain, central_freq, q) - self.assert_sox_effect(result, path, ['treble', gain, central_freq, f'{q}q']) + self.assert_sox_effect(result, path, ["treble", gain, central_freq, f"{q}q"]) def test_bass(self): central_freq = 1000 @@ -144,26 +145,26 @@ def test_bass(self): data, path = self.get_whitenoise(sample_rate) result = F.bass_biquad(data, sample_rate, gain, central_freq, q) - self.assert_sox_effect(result, path, ['bass', gain, central_freq, f'{q}q'], atol=1.5e-4) + self.assert_sox_effect(result, path, ["bass", gain, central_freq, f"{q}q"], atol=1.5e-4) def test_deemph(self): sample_rate = 44100 data, path = self.get_whitenoise(sample_rate) result = F.deemph_biquad(data, sample_rate) - self.assert_sox_effect(result, path, ['deemph']) + self.assert_sox_effect(result, path, ["deemph"]) def test_riaa(self): sample_rate = 44100 data, path = self.get_whitenoise(sample_rate) result = F.riaa_biquad(data, sample_rate) - self.assert_sox_effect(result, path, ['riaa']) + self.assert_sox_effect(result, path, ["riaa"]) def test_contrast(self): - enhancement_amount = 80. + enhancement_amount = 80.0 data, path = self.get_whitenoise() result = F.contrast(data, enhancement_amount) - self.assert_sox_effect(result, path, ['contrast', enhancement_amount]) + self.assert_sox_effect(result, path, ["contrast", enhancement_amount]) def test_dcshift_with_limiter(self): shift = 0.5 @@ -171,14 +172,14 @@ def test_dcshift_with_limiter(self): data, path = self.get_whitenoise() result = F.dcshift(data, shift, limiter_gain) - self.assert_sox_effect(result, path, ['dcshift', shift, limiter_gain]) + self.assert_sox_effect(result, path, ["dcshift", shift, limiter_gain]) def test_dcshift_without_limiter(self): shift = 0.6 data, path = self.get_whitenoise() result = F.dcshift(data, shift) - self.assert_sox_effect(result, path, ['dcshift', shift]) + self.assert_sox_effect(result, path, ["dcshift", shift]) def test_overdrive(self): gain = 30 @@ -186,7 +187,7 @@ def test_overdrive(self): data, path = self.get_whitenoise() result = F.overdrive(data, gain, colour) - self.assert_sox_effect(result, path, ['overdrive', gain, colour]) + self.assert_sox_effect(result, path, ["overdrive", gain, colour]) def test_phaser_sine(self): gain_in = 0.5 @@ -198,7 +199,7 @@ def test_phaser_sine(self): data, path = self.get_whitenoise(sample_rate) result = F.phaser(data, sample_rate, gain_in, gain_out, delay_ms, decay, speed, sinusoidal=True) - self.assert_sox_effect(result, path, ['phaser', gain_in, gain_out, delay_ms, decay, speed, '-s']) + self.assert_sox_effect(result, path, ["phaser", gain_in, gain_out, delay_ms, decay, speed, "-s"]) def test_phaser_triangle(self): gain_in = 0.5 @@ -210,7 +211,7 @@ def test_phaser_triangle(self): data, path = self.get_whitenoise(sample_rate) result = F.phaser(data, sample_rate, gain_in, gain_out, delay_ms, decay, speed, sinusoidal=False) - self.assert_sox_effect(result, path, ['phaser', gain_in, gain_out, delay_ms, decay, speed, '-t']) + self.assert_sox_effect(result, path, ["phaser", gain_in, gain_out, delay_ms, decay, speed, "-t"]) def test_flanger_triangle_linear(self): delay = 0.6 @@ -223,10 +224,11 @@ def test_flanger_triangle_linear(self): data, path = self.get_whitenoise(sample_rate) result = F.flanger( - data, sample_rate, delay, depth, regen, width, speed, phase, - modulation='triangular', interpolation='linear') + data, sample_rate, delay, depth, regen, width, speed, phase, modulation="triangular", interpolation="linear" + ) self.assert_sox_effect( - result, path, ['flanger', delay, depth, regen, width, speed, 'triangle', phase, 'linear']) + result, path, ["flanger", delay, depth, regen, width, speed, "triangle", phase, "linear"] + ) def test_flanger_triangle_quad(self): delay = 0.8 @@ -239,10 +241,20 @@ def test_flanger_triangle_quad(self): data, path = self.get_whitenoise(sample_rate) result = F.flanger( - data, sample_rate, delay, depth, regen, width, speed, phase, - modulation='triangular', interpolation='quadratic') + data, + sample_rate, + delay, + depth, + regen, + width, + speed, + phase, + modulation="triangular", + interpolation="quadratic", + ) self.assert_sox_effect( - result, path, ['flanger', delay, depth, regen, width, speed, 'triangle', phase, 'quadratic']) + result, path, ["flanger", delay, depth, regen, width, speed, "triangle", phase, "quadratic"] + ) def test_flanger_sine_linear(self): delay = 0.8 @@ -255,10 +267,9 @@ def test_flanger_sine_linear(self): data, path = self.get_whitenoise(sample_rate) result = F.flanger( - data, sample_rate, delay, depth, regen, width, speed, phase, - modulation='sinusoidal', interpolation='linear') - self.assert_sox_effect( - result, path, ['flanger', delay, depth, regen, width, speed, 'sine', phase, 'linear']) + data, sample_rate, delay, depth, regen, width, speed, phase, modulation="sinusoidal", interpolation="linear" + ) + self.assert_sox_effect(result, path, ["flanger", delay, depth, regen, width, speed, "sine", phase, "linear"]) def test_flanger_sine_quad(self): delay = 0.9 @@ -271,10 +282,18 @@ def test_flanger_sine_quad(self): data, path = self.get_whitenoise(sample_rate) result = F.flanger( - data, sample_rate, delay, depth, regen, width, speed, phase, - modulation='sinusoidal', interpolation='quadratic') - self.assert_sox_effect( - result, path, ['flanger', delay, depth, regen, width, speed, 'sine', phase, 'quadratic']) + data, + sample_rate, + delay, + depth, + regen, + width, + speed, + phase, + modulation="sinusoidal", + interpolation="quadratic", + ) + self.assert_sox_effect(result, path, ["flanger", delay, depth, regen, width, speed, "sine", phase, "quadratic"]) def test_equalizer(self): center_freq = 300 @@ -284,7 +303,7 @@ def test_equalizer(self): data, path = self.get_whitenoise(sample_rate) result = F.equalizer_biquad(data, sample_rate, center_freq, gain, q) - self.assert_sox_effect(result, path, ['equalizer', center_freq, q, gain]) + self.assert_sox_effect(result, path, ["equalizer", center_freq, q, gain]) def test_perf_biquad_filtering(self): b0 = 0.4 @@ -296,4 +315,4 @@ def test_perf_biquad_filtering(self): data, path = self.get_whitenoise() result = F.lfilter(data, torch.tensor([a0, a1, a2]), torch.tensor([b0, b1, b2])) - self.assert_sox_effect(result, path, ['biquad', b0, b1, b2, a0, a1, a2]) + self.assert_sox_effect(result, path, ["biquad", b0, b1, b2, a0, a1, a2]) diff --git a/test/torchaudio_unittest/functional/torchscript_consistency_cpu_test.py b/test/torchaudio_unittest/functional/torchscript_consistency_cpu_test.py index 2971067dbe..514b9bc063 100644 --- a/test/torchaudio_unittest/functional/torchscript_consistency_cpu_test.py +++ b/test/torchaudio_unittest/functional/torchscript_consistency_cpu_test.py @@ -1,14 +1,14 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase + from .torchscript_consistency_impl import Functional, FunctionalFloat32Only class TestFunctionalFloat32(Functional, FunctionalFloat32Only, PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") class TestFunctionalFloat64(Functional, PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/functional/torchscript_consistency_cuda_test.py b/test/torchaudio_unittest/functional/torchscript_consistency_cuda_test.py index 30e2b39699..5127608683 100644 --- a/test/torchaudio_unittest/functional/torchscript_consistency_cuda_test.py +++ b/test/torchaudio_unittest/functional/torchscript_consistency_cuda_test.py @@ -1,16 +1,16 @@ import torch - from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + from .torchscript_consistency_impl import Functional, FunctionalFloat32Only @skipIfNoCuda class TestFunctionalFloat32(Functional, FunctionalFloat32Only, PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") @skipIfNoCuda class TestFunctionalFloat64(Functional, PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/functional/torchscript_consistency_impl.py b/test/torchaudio_unittest/functional/torchscript_consistency_impl.py index 9746f8ac69..5b882946c4 100644 --- a/test/torchaudio_unittest/functional/torchscript_consistency_impl.py +++ b/test/torchaudio_unittest/functional/torchscript_consistency_impl.py @@ -3,7 +3,6 @@ import torch import torchaudio.functional as F - from torchaudio_unittest import common_utils from torchaudio_unittest.common_utils import ( TempDirMixin, @@ -15,6 +14,7 @@ class Functional(TempDirMixin, TestBaseMixin): """Implements test for `functional` module that are performed for different devices""" + def _assert_consistency(self, func, tensor, shape_only=False): tensor = tensor.to(device=self.device, dtype=self.dtype) ts_func = torch_script(func) @@ -79,7 +79,7 @@ def func(tensor): ws = 400 hop = 200 window = torch.hann_window(ws, device=tensor.device, dtype=tensor.dtype) - power = 2. + power = 2.0 momentum = 0.99 n_iter = 32 length = 1000 @@ -110,8 +110,8 @@ def func(tensor): self._assert_consistency(func, waveform) def test_melscale_fbanks(self): - if self.device != torch.device('cpu'): - raise unittest.SkipTest('No need to perform test on device other than CPU') + if self.device != torch.device("cpu"): + raise unittest.SkipTest("No need to perform test on device other than CPU") def func(_): n_stft = 100 @@ -126,8 +126,8 @@ def func(_): self._assert_consistency(func, dummy) def test_linear_fbanks(self): - if self.device != torch.device('cpu'): - raise unittest.SkipTest('No need to perform test on device other than CPU') + if self.device != torch.device("cpu"): + raise unittest.SkipTest("No need to perform test on device other than CPU") def func(_): n_stft = 100 @@ -153,16 +153,16 @@ def func(tensor): def test_DB_to_amplitude(self): def func(tensor): - ref = 1. - power = 1. + ref = 1.0 + power = 1.0 return F.DB_to_amplitude(tensor, ref, power) tensor = torch.rand((1, 100)) self._assert_consistency(func, tensor) def test_create_dct(self): - if self.device != torch.device('cpu'): - raise unittest.SkipTest('No need to perform test on device other than CPU') + if self.device != torch.device("cpu"): + raise unittest.SkipTest("No need to perform test on device other than CPU") def func(_): n_mfcc = 40 @@ -192,7 +192,7 @@ def func(tensor): def test_mask_along_axis(self): def func(tensor): mask_param = 100 - mask_value = 30. + mask_value = 30.0 axis = 2 return F.mask_along_axis(tensor, mask_param, mask_value, axis) @@ -202,7 +202,7 @@ def func(tensor): def test_mask_along_axis_iid(self): def func(tensor): mask_param = 100 - mask_value = 30. + mask_value = 30.0 axis = 2 return F.mask_along_axis_iid(tensor, mask_param, mask_value, axis) @@ -219,21 +219,21 @@ def func(tensor): def test_dither_TPDF(self): def func(tensor): - return F.dither(tensor, 'TPDF') + return F.dither(tensor, "TPDF") tensor = common_utils.get_whitenoise(n_channels=2) self._assert_consistency(func, tensor, shape_only=True) def test_dither_RPDF(self): def func(tensor): - return F.dither(tensor, 'RPDF') + return F.dither(tensor, "RPDF") tensor = common_utils.get_whitenoise(n_channels=2) self._assert_consistency(func, tensor, shape_only=True) def test_dither_GPDF(self): def func(tensor): - return F.dither(tensor, 'GPDF') + return F.dither(tensor, "GPDF") tensor = common_utils.get_whitenoise(n_channels=2) self._assert_consistency(func, tensor, shape_only=True) @@ -306,7 +306,7 @@ def test_lowpass(self): def func(tensor): sample_rate = 44100 - cutoff_freq = 3000. + cutoff_freq = 3000.0 return F.lowpass_biquad(tensor, sample_rate, cutoff_freq) self._assert_consistency(func, waveform) @@ -319,7 +319,7 @@ def test_highpass(self): def func(tensor): sample_rate = 44100 - cutoff_freq = 2000. + cutoff_freq = 2000.0 return F.highpass_biquad(tensor, sample_rate, cutoff_freq) self._assert_consistency(func, waveform) @@ -332,7 +332,7 @@ def test_allpass(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 return F.allpass_biquad(tensor, sample_rate, central_freq, q) @@ -346,7 +346,7 @@ def test_bandpass_with_csg(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 const_skirt_gain = True return F.bandpass_biquad(tensor, sample_rate, central_freq, q, const_skirt_gain) @@ -361,7 +361,7 @@ def test_bandpass_without_csg(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 const_skirt_gain = True return F.bandpass_biquad(tensor, sample_rate, central_freq, q, const_skirt_gain) @@ -376,7 +376,7 @@ def test_bandreject(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 return F.bandreject_biquad(tensor, sample_rate, central_freq, q) @@ -390,7 +390,7 @@ def test_band_with_noise(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 noise = True return F.band_biquad(tensor, sample_rate, central_freq, q, noise) @@ -405,7 +405,7 @@ def test_band_without_noise(self): def func(tensor): sample_rate = 44100 - central_freq = 1000. + central_freq = 1000.0 q = 0.707 noise = False return F.band_biquad(tensor, sample_rate, central_freq, q, noise) @@ -420,8 +420,8 @@ def test_treble(self): def func(tensor): sample_rate = 44100 - gain = 40. - central_freq = 1000. + gain = 40.0 + central_freq = 1000.0 q = 0.707 return F.treble_biquad(tensor, sample_rate, gain, central_freq, q) @@ -435,8 +435,8 @@ def test_bass(self): def func(tensor): sample_rate = 44100 - gain = 40. - central_freq = 1000. + gain = 40.0 + central_freq = 1000.0 q = 0.707 return F.bass_biquad(tensor, sample_rate, gain, central_freq, q) @@ -474,8 +474,8 @@ def test_equalizer(self): def func(tensor): sample_rate = 44100 - center_freq = 300. - gain = 1. + center_freq = 300.0 + gain = 1.0 q = 0.707 return F.equalizer_biquad(tensor, sample_rate, center_freq, gain, q) @@ -501,39 +501,20 @@ def func(tensor): center = False norm_vars = False a = torch.tensor( - [ - [ - -1.915875792503357, - 1.147700309753418 - ], - [ - 1.8242558240890503, - 1.3869990110397339 - ] - ], + [[-1.915875792503357, 1.147700309753418], [1.8242558240890503, 1.3869990110397339]], device=tensor.device, - dtype=tensor.dtype + dtype=tensor.dtype, ) return F.sliding_window_cmn(a, cmn_window, min_cmn_window, center, norm_vars) - b = torch.tensor( - [ - [ - -1.8701, - -0.1196 - ], - [ - 1.8701, - 0.1196 - ] - ] - ) + + b = torch.tensor([[-1.8701, -0.1196], [1.8701, 0.1196]]) self._assert_consistency(func, b) def test_contrast(self): waveform = common_utils.get_whitenoise() def func(tensor): - enhancement_amount = 80. + enhancement_amount = 80.0 return F.contrast(tensor, enhancement_amount) self._assert_consistency(func, waveform) @@ -552,8 +533,8 @@ def test_overdrive(self): waveform = common_utils.get_whitenoise() def func(tensor): - gain = 30. - colour = 50. + gain = 30.0 + colour = 50.0 return F.overdrive(tensor, gain, colour) self._assert_consistency(func, waveform) @@ -582,15 +563,24 @@ def func(tensor): regen = 3.0 width = 0.23 speed = 1.3 - phase = 60. + phase = 60.0 sample_rate = 44100 - return F.flanger(tensor, sample_rate, delay, depth, regen, width, speed, - phase, modulation='sinusoidal', interpolation='linear') + return F.flanger( + tensor, + sample_rate, + delay, + depth, + regen, + width, + speed, + phase, + modulation="sinusoidal", + interpolation="linear", + ) self._assert_consistency(func, waveform) def test_spectral_centroid(self): - def func(tensor): sample_rate = 44100 n_fft = 400 @@ -605,11 +595,11 @@ def func(tensor): @common_utils.skipIfNoKaldi def test_compute_kaldi_pitch(self): - if self.dtype != torch.float32 or self.device != torch.device('cpu'): + if self.dtype != torch.float32 or self.device != torch.device("cpu"): raise unittest.SkipTest("Only float32, cpu is supported.") def func(tensor): - sample_rate: float = 44100. + sample_rate: float = 44100.0 return F.compute_kaldi_pitch(tensor, sample_rate) tensor = common_utils.get_whitenoise(sample_rate=44100) @@ -630,7 +620,7 @@ def func(tensor): def func_beta(tensor): sr1, sr2 = 16000, 8000 - beta = 6. + beta = 6.0 return F.resample(tensor, sr1, sr2, resampling_method="kaiser_window", beta=beta) tensor = common_utils.get_whitenoise(sample_rate=16000) @@ -663,11 +653,13 @@ def func(tensor): target_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) return F.rnnt_loss(tensor, targets, logit_lengths, target_lengths) - logits = torch.tensor([[[[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.6, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.8, 0.1]], - [[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.1, 0.1], - [0.7, 0.1, 0.2, 0.1, 0.1]]]]) + logits = torch.tensor( + [ + [ + [[0.1, 0.6, 0.1, 0.1, 0.1], [0.1, 0.1, 0.6, 0.1, 0.1], [0.1, 0.1, 0.2, 0.8, 0.1]], + [[0.1, 0.6, 0.1, 0.1, 0.1], [0.1, 0.1, 0.2, 0.1, 0.1], [0.7, 0.1, 0.2, 0.1, 0.1]], + ] + ] + ) tensor = logits.to(device=self.device, dtype=torch.float32) self._assert_consistency(func, tensor) diff --git a/test/torchaudio_unittest/kaldi_io_test.py b/test/torchaudio_unittest/kaldi_io_test.py index 4ceeeebbc5..dc2a846c23 100644 --- a/test/torchaudio_unittest/kaldi_io_test.py +++ b/test/torchaudio_unittest/kaldi_io_test.py @@ -1,6 +1,5 @@ import torch import torchaudio.kaldi_io as kio - from torchaudio_unittest import common_utils @@ -9,13 +8,14 @@ class Test_KaldiIO(common_utils.TorchaudioTestCase): data2 = [[31, 32, 33], [41, 42, 43], [51, 52, 53]] def _test_helper(self, file_name, expected_data, fn, expected_dtype): - """ Takes a file_name to the input data and a function fn to extract the + """Takes a file_name to the input data and a function fn to extract the data. It compares the extracted data to the expected_data. The expected_dtype will be used to check that the extracted data is of the right type. """ test_filepath = common_utils.get_asset_path(file_name) - expected_output = {'key' + str(idx + 1): torch.tensor(val, dtype=expected_dtype) - for idx, val in enumerate(expected_data)} + expected_output = { + "key" + str(idx + 1): torch.tensor(val, dtype=expected_dtype) for idx, val in enumerate(expected_data) + } for key, vec in fn(test_filepath): self.assertTrue(key in expected_output) diff --git a/test/torchaudio_unittest/models/models_test.py b/test/torchaudio_unittest/models/models_test.py index 18f3085e53..69bc4ed37d 100644 --- a/test/torchaudio_unittest/models/models_test.py +++ b/test/torchaudio_unittest/models/models_test.py @@ -10,7 +10,6 @@ class TestWav2Letter(common_utils.TorchaudioTestCase): - def test_waveform(self): batch_size = 2 num_features = 1 @@ -39,10 +38,8 @@ def test_mfcc(self): class TestMelResNet(common_utils.TorchaudioTestCase): - def test_waveform(self): - """Validate the output dimensions of a MelResNet block. - """ + """Validate the output dimensions of a MelResNet block.""" n_batch = 2 n_time = 200 @@ -61,10 +58,8 @@ def test_waveform(self): class TestUpsampleNetwork(common_utils.TorchaudioTestCase): - def test_waveform(self): - """Validate the output dimensions of a UpsampleNetwork block. - """ + """Validate the output dimensions of a UpsampleNetwork block.""" upsample_scales = [5, 5, 8] n_batch = 2 @@ -79,12 +74,7 @@ def test_waveform(self): for upsample_scale in upsample_scales: total_scale *= upsample_scale - model = UpsampleNetwork(upsample_scales, - n_res_block, - n_freq, - n_hidden, - n_output, - kernel_size) + model = UpsampleNetwork(upsample_scales, n_res_block, n_freq, n_hidden, n_output, kernel_size) x = torch.rand(n_batch, n_freq, n_time) out1, out2 = model(x) @@ -94,10 +84,8 @@ def test_waveform(self): class TestWaveRNN(common_utils.TorchaudioTestCase): - def test_waveform(self): - """Validate the output dimensions of a WaveRNN model. - """ + """Validate the output dimensions of a WaveRNN model.""" upsample_scales = [5, 5, 8] n_rnn = 512 @@ -112,8 +100,9 @@ def test_waveform(self): n_hidden = 128 kernel_size = 5 - model = WaveRNN(upsample_scales, n_classes, hop_length, n_res_block, - n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output) + model = WaveRNN( + upsample_scales, n_classes, hop_length, n_res_block, n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output + ) x = torch.rand(n_batch, 1, hop_length * (n_time - kernel_size + 1)) mels = torch.rand(n_batch, 1, n_freq, n_time) @@ -122,8 +111,7 @@ def test_waveform(self): assert out.size() == (n_batch, 1, hop_length * (n_time - kernel_size + 1), n_classes) def test_infer_waveform(self): - """Validate the output dimensions of a WaveRNN model's infer method. - """ + """Validate the output dimensions of a WaveRNN model's infer method.""" upsample_scales = [5, 5, 8] n_rnn = 128 @@ -138,8 +126,9 @@ def test_infer_waveform(self): n_hidden = 32 kernel_size = 5 - model = WaveRNN(upsample_scales, n_classes, hop_length, n_res_block, - n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output) + model = WaveRNN( + upsample_scales, n_classes, hop_length, n_res_block, n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output + ) x = torch.rand(n_batch, n_freq, n_time) lengths = torch.tensor([n_time, n_time // 2]) @@ -165,8 +154,9 @@ def test_torchscript_infer(self): n_hidden = 32 kernel_size = 5 - model = WaveRNN(upsample_scales, n_classes, hop_length, n_res_block, - n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output) + model = WaveRNN( + upsample_scales, n_classes, hop_length, n_res_block, n_rnn, n_fc, kernel_size, n_freq, n_hidden, n_output + ) model.eval() x = torch.rand(n_batch, n_freq, n_time) torch.random.manual_seed(0) @@ -177,39 +167,43 @@ def test_torchscript_infer(self): _ConvTasNetParams = namedtuple( - '_ConvTasNetParams', + "_ConvTasNetParams", [ - 'enc_num_feats', - 'enc_kernel_size', - 'msk_num_feats', - 'msk_num_hidden_feats', - 'msk_kernel_size', - 'msk_num_layers', - 'msk_num_stacks', - ] + "enc_num_feats", + "enc_kernel_size", + "msk_num_feats", + "msk_num_hidden_feats", + "msk_kernel_size", + "msk_num_layers", + "msk_num_stacks", + ], ) class TestConvTasNet(common_utils.TorchaudioTestCase): - @parameterized.expand(list(itertools.product( - [2, 3], - [ - _ConvTasNetParams(128, 40, 128, 256, 3, 7, 2), - _ConvTasNetParams(256, 40, 128, 256, 3, 7, 2), - _ConvTasNetParams(512, 40, 128, 256, 3, 7, 2), - _ConvTasNetParams(512, 40, 128, 256, 3, 7, 2), - _ConvTasNetParams(512, 40, 128, 512, 3, 7, 2), - _ConvTasNetParams(512, 40, 128, 512, 3, 7, 2), - _ConvTasNetParams(512, 40, 256, 256, 3, 7, 2), - _ConvTasNetParams(512, 40, 256, 512, 3, 7, 2), - _ConvTasNetParams(512, 40, 256, 512, 3, 7, 2), - _ConvTasNetParams(512, 40, 128, 512, 3, 6, 4), - _ConvTasNetParams(512, 40, 128, 512, 3, 4, 6), - _ConvTasNetParams(512, 40, 128, 512, 3, 8, 3), - _ConvTasNetParams(512, 32, 128, 512, 3, 8, 3), - _ConvTasNetParams(512, 16, 128, 512, 3, 8, 3), - ], - ))) + @parameterized.expand( + list( + itertools.product( + [2, 3], + [ + _ConvTasNetParams(128, 40, 128, 256, 3, 7, 2), + _ConvTasNetParams(256, 40, 128, 256, 3, 7, 2), + _ConvTasNetParams(512, 40, 128, 256, 3, 7, 2), + _ConvTasNetParams(512, 40, 128, 256, 3, 7, 2), + _ConvTasNetParams(512, 40, 128, 512, 3, 7, 2), + _ConvTasNetParams(512, 40, 128, 512, 3, 7, 2), + _ConvTasNetParams(512, 40, 256, 256, 3, 7, 2), + _ConvTasNetParams(512, 40, 256, 512, 3, 7, 2), + _ConvTasNetParams(512, 40, 256, 512, 3, 7, 2), + _ConvTasNetParams(512, 40, 128, 512, 3, 6, 4), + _ConvTasNetParams(512, 40, 128, 512, 3, 4, 6), + _ConvTasNetParams(512, 40, 128, 512, 3, 8, 3), + _ConvTasNetParams(512, 32, 128, 512, 3, 8, 3), + _ConvTasNetParams(512, 16, 128, 512, 3, 8, 3), + ], + ) + ) + ) def test_paper_configuration(self, num_sources, model_params): """ConvTasNet model works on the valid configurations in the paper""" batch_size = 32 @@ -232,7 +226,6 @@ def test_paper_configuration(self, num_sources, model_params): class TestDeepSpeech(common_utils.TorchaudioTestCase): - def test_deepspeech(self): n_batch = 2 n_feature = 1 diff --git a/test/torchaudio_unittest/models/tacotron2/model_test_cpu_test.py b/test/torchaudio_unittest/models/tacotron2/model_test_cpu_test.py index 612699b6e1..fc6522a482 100644 --- a/test/torchaudio_unittest/models/tacotron2/model_test_cpu_test.py +++ b/test/torchaudio_unittest/models/tacotron2/model_test_cpu_test.py @@ -1,6 +1,6 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase + from .model_test_impl import ( Tacotron2EncoderTests, Tacotron2DecoderTests, diff --git a/test/torchaudio_unittest/models/tacotron2/model_test_gpu_test.py b/test/torchaudio_unittest/models/tacotron2/model_test_gpu_test.py index 7a6fdd1142..df25658e87 100644 --- a/test/torchaudio_unittest/models/tacotron2/model_test_gpu_test.py +++ b/test/torchaudio_unittest/models/tacotron2/model_test_gpu_test.py @@ -1,6 +1,6 @@ import torch - from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + from .model_test_impl import ( Tacotron2EncoderTests, Tacotron2DecoderTests, diff --git a/test/torchaudio_unittest/models/tacotron2/model_test_impl.py b/test/torchaudio_unittest/models/tacotron2/model_test_impl.py index fb43a14036..a029acef70 100644 --- a/test/torchaudio_unittest/models/tacotron2/model_test_impl.py +++ b/test/torchaudio_unittest/models/tacotron2/model_test_impl.py @@ -1,4 +1,5 @@ from typing import Tuple + import torch from torch import Tensor from torchaudio.models import Tacotron2 @@ -7,7 +8,6 @@ class Tacotron2InferenceWrapper(torch.nn.Module): - def __init__(self, model): super().__init__() self.model = model @@ -17,7 +17,6 @@ def forward(self, text: Tensor, text_lengths: Tensor) -> Tuple[Tensor, Tensor, T class Tacotron2DecoderInferenceWrapper(torch.nn.Module): - def __init__(self, model): super().__init__() self.model = model @@ -42,21 +41,18 @@ def _assert_torchscript_consistency(self, model, tensors): class Tacotron2EncoderTests(TorchscriptConsistencyMixin): - def test_tacotron2_torchscript_consistency(self): r"""Validate the torchscript consistency of a Encoder.""" n_batch, n_seq, encoder_embedding_dim = 16, 64, 512 - model = _Encoder(encoder_embedding_dim=encoder_embedding_dim, - encoder_n_convolution=3, - encoder_kernel_size=5).to(self.device).eval() - - x = torch.rand( - n_batch, encoder_embedding_dim, n_seq, device=self.device, dtype=self.dtype - ) - input_lengths = ( - torch.ones(n_batch, device=self.device, dtype=torch.int32) * n_seq + model = ( + _Encoder(encoder_embedding_dim=encoder_embedding_dim, encoder_n_convolution=3, encoder_kernel_size=5) + .to(self.device) + .eval() ) + x = torch.rand(n_batch, encoder_embedding_dim, n_seq, device=self.device, dtype=self.dtype) + input_lengths = torch.ones(n_batch, device=self.device, dtype=torch.int32) * n_seq + self._assert_torchscript_consistency(model, (x, input_lengths)) def test_encoder_output_shape(self): @@ -64,23 +60,20 @@ def test_encoder_output_shape(self): that it outputs with a tensor with expected shape. """ n_batch, n_seq, encoder_embedding_dim = 16, 64, 512 - model = _Encoder(encoder_embedding_dim=encoder_embedding_dim, - encoder_n_convolution=3, - encoder_kernel_size=5).to(self.device).eval() - - x = torch.rand( - n_batch, encoder_embedding_dim, n_seq, device=self.device, dtype=self.dtype - ) - input_lengths = ( - torch.ones(n_batch, device=self.device, dtype=torch.int32) * n_seq + model = ( + _Encoder(encoder_embedding_dim=encoder_embedding_dim, encoder_n_convolution=3, encoder_kernel_size=5) + .to(self.device) + .eval() ) + + x = torch.rand(n_batch, encoder_embedding_dim, n_seq, device=self.device, dtype=self.dtype) + input_lengths = torch.ones(n_batch, device=self.device, dtype=torch.int32) * n_seq out = model(x, input_lengths) assert out.size() == (n_batch, n_seq, encoder_embedding_dim) -def _get_decoder_model(n_mels=80, encoder_embedding_dim=512, - decoder_max_step=2000, gate_threshold=0.5): +def _get_decoder_model(n_mels=80, encoder_embedding_dim=512, decoder_max_step=2000, gate_threshold=0.5): model = _Decoder( n_mels=n_mels, n_frames_per_step=1, @@ -101,7 +94,6 @@ def _get_decoder_model(n_mels=80, encoder_embedding_dim=512, class Tacotron2DecoderTests(TorchscriptConsistencyMixin): - def test_decoder_torchscript_consistency(self): r"""Validate the torchscript consistency of a Decoder.""" n_batch = 16 @@ -113,17 +105,11 @@ def test_decoder_torchscript_consistency(self): model = _get_decoder_model(n_mels=n_mels, encoder_embedding_dim=encoder_embedding_dim) model = model.to(self.device).eval() - memory = torch.rand( - n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device - ) - decoder_inputs = torch.rand( - n_batch, n_mels, n_time_steps, dtype=self.dtype, device=self.device - ) + memory = torch.rand(n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device) + decoder_inputs = torch.rand(n_batch, n_mels, n_time_steps, dtype=self.dtype, device=self.device) memory_lengths = torch.ones(n_batch, dtype=torch.int32, device=self.device) - self._assert_torchscript_consistency( - model, (memory, decoder_inputs, memory_lengths) - ) + self._assert_torchscript_consistency(model, (memory, decoder_inputs, memory_lengths)) def test_decoder_output_shape(self): r"""Feed tensors with specific shape to Tacotron2 Decoder and validate @@ -138,17 +124,11 @@ def test_decoder_output_shape(self): model = _get_decoder_model(n_mels=n_mels, encoder_embedding_dim=encoder_embedding_dim) model = model.to(self.device).eval() - memory = torch.rand( - n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device - ) - decoder_inputs = torch.rand( - n_batch, n_mels, n_time_steps, dtype=self.dtype, device=self.device - ) + memory = torch.rand(n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device) + decoder_inputs = torch.rand(n_batch, n_mels, n_time_steps, dtype=self.dtype, device=self.device) memory_lengths = torch.ones(n_batch, dtype=torch.int32, device=self.device) - mel_specgram, gate_outputs, alignments = model( - memory, decoder_inputs, memory_lengths - ) + mel_specgram, gate_outputs, alignments = model(memory, decoder_inputs, memory_lengths) assert mel_specgram.size() == (n_batch, n_mels, n_time_steps) assert gate_outputs.size() == (n_batch, n_time_steps) @@ -171,9 +151,7 @@ def test_decoder_inference_torchscript_consistency(self): ) model = model.to(self.device).eval() - memory = torch.rand( - n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device - ) + memory = torch.rand(n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device) memory_lengths = torch.ones(n_batch, dtype=torch.int32, device=self.device) model_wrapper = Tacotron2DecoderInferenceWrapper(model) @@ -197,17 +175,16 @@ def test_decoder_inference_output_shape(self): ) model = model.to(self.device).eval() - memory = torch.rand( - n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device - ) + memory = torch.rand(n_batch, n_seq, encoder_embedding_dim, dtype=self.dtype, device=self.device) memory_lengths = torch.ones(n_batch, dtype=torch.int32, device=self.device) - mel_specgram, mel_specgram_lengths, gate_outputs, alignments = model.infer( - memory, memory_lengths - ) + mel_specgram, mel_specgram_lengths, gate_outputs, alignments = model.infer(memory, memory_lengths) assert len(mel_specgram.size()) == 3 - assert mel_specgram.size()[:-1] == (n_batch, n_mels, ) + assert mel_specgram.size()[:-1] == ( + n_batch, + n_mels, + ) assert mel_specgram.size()[2] == mel_specgram_lengths.max().item() assert len(mel_specgram_lengths.size()) == 1 assert mel_specgram_lengths.size()[0] == n_batch @@ -248,16 +225,9 @@ def _get_tacotron2_model(n_mels, decoder_max_step=2000, gate_threshold=0.5): class Tacotron2Tests(TorchscriptConsistencyMixin): - - def _get_inputs( - self, n_mels: int, n_batch: int, max_mel_specgram_length: int, max_text_length: int - ): - text = torch.randint( - 0, 148, (n_batch, max_text_length), dtype=torch.int32, device=self.device - ) - text_lengths = max_text_length * torch.ones( - (n_batch,), dtype=torch.int32, device=self.device - ) + def _get_inputs(self, n_mels: int, n_batch: int, max_mel_specgram_length: int, max_text_length: int): + text = torch.randint(0, 148, (n_batch, max_text_length), dtype=torch.int32, device=self.device) + text_lengths = max_text_length * torch.ones((n_batch,), dtype=torch.int32, device=self.device) mel_specgram = torch.rand( n_batch, n_mels, @@ -265,9 +235,7 @@ def _get_inputs( dtype=self.dtype, device=self.device, ) - mel_specgram_lengths = max_mel_specgram_length * torch.ones( - (n_batch,), dtype=torch.int32, device=self.device - ) + mel_specgram_lengths = max_mel_specgram_length * torch.ones((n_batch,), dtype=torch.int32, device=self.device) return text, text_lengths, mel_specgram, mel_specgram_lengths def test_tacotron2_torchscript_consistency(self): @@ -278,9 +246,7 @@ def test_tacotron2_torchscript_consistency(self): max_text_length = 100 model = _get_tacotron2_model(n_mels).to(self.device).eval() - inputs = self._get_inputs( - n_mels, n_batch, max_mel_specgram_length, max_text_length - ) + inputs = self._get_inputs(n_mels, n_batch, max_mel_specgram_length, max_text_length) self._assert_torchscript_consistency(model, inputs) @@ -294,9 +260,7 @@ def test_tacotron2_output_shape(self): max_text_length = 100 model = _get_tacotron2_model(n_mels).to(self.device).eval() - inputs = self._get_inputs( - n_mels, n_batch, max_mel_specgram_length, max_text_length - ) + inputs = self._get_inputs(n_mels, n_batch, max_mel_specgram_length, max_text_length) mel_out, mel_out_postnet, gate_outputs, alignments = model(*inputs) assert mel_out.size() == (n_batch, n_mels, max_mel_specgram_length) @@ -315,9 +279,7 @@ def test_tacotron2_backward(self): max_text_length = 100 model = _get_tacotron2_model(n_mels).to(self.device) - inputs = self._get_inputs( - n_mels, n_batch, max_mel_specgram_length, max_text_length - ) + inputs = self._get_inputs(n_mels, n_batch, max_mel_specgram_length, max_text_length) mel_out, mel_out_postnet, gate_outputs, _ = model(*inputs) mel_out.sum().backward(retain_graph=True) @@ -325,12 +287,8 @@ def test_tacotron2_backward(self): gate_outputs.sum().backward() def _get_inference_inputs(self, n_batch: int, max_text_length: int): - text = torch.randint( - 0, 148, (n_batch, max_text_length), dtype=torch.int32, device=self.device - ) - text_lengths = max_text_length * torch.ones( - (n_batch,), dtype=torch.int32, device=self.device - ) + text = torch.randint(0, 148, (n_batch, max_text_length), dtype=torch.int32, device=self.device) + text_lengths = max_text_length * torch.ones((n_batch,), dtype=torch.int32, device=self.device) return text, text_lengths def test_tacotron2_inference_torchscript_consistency(self): @@ -341,9 +299,11 @@ def test_tacotron2_inference_torchscript_consistency(self): decoder_max_step = 200 # make inference more efficient gate_threshold = 0.51 # if set to 0.5, the model will only run one step - model = _get_tacotron2_model( - n_mels, decoder_max_step=decoder_max_step, gate_threshold=gate_threshold - ).to(self.device).eval() + model = ( + _get_tacotron2_model(n_mels, decoder_max_step=decoder_max_step, gate_threshold=gate_threshold) + .to(self.device) + .eval() + ) inputs = self._get_inference_inputs(n_batch, max_text_length) model_wrapper = Tacotron2InferenceWrapper(model) @@ -360,9 +320,11 @@ def test_tacotron2_inference_output_shape(self): decoder_max_step = 200 # make inference more efficient gate_threshold = 0.51 # if set to 0.5, the model will only run one step - model = _get_tacotron2_model( - n_mels, decoder_max_step=decoder_max_step, gate_threshold=gate_threshold - ).to(self.device).eval() + model = ( + _get_tacotron2_model(n_mels, decoder_max_step=decoder_max_step, gate_threshold=gate_threshold) + .to(self.device) + .eval() + ) inputs = self._get_inference_inputs(n_batch, max_text_length) mel_out, mel_specgram_lengths, alignments = model.infer(*inputs) @@ -370,7 +332,10 @@ def test_tacotron2_inference_output_shape(self): # There is no guarantee on exactly what max_mel_specgram_length should be # We only know that it should be smaller than model.decoder.decoder_max_step assert len(mel_out.size()) == 3 - assert mel_out.size()[:2] == (n_batch, n_mels, ) + assert mel_out.size()[:2] == ( + n_batch, + n_mels, + ) assert mel_out.size()[2] == mel_specgram_lengths.max().item() assert len(mel_specgram_lengths.size()) == 1 assert mel_specgram_lengths.size()[0] == n_batch diff --git a/test/torchaudio_unittest/models/wav2vec2/fairseq_integration_test.py b/test/torchaudio_unittest/models/wav2vec2/fairseq_integration_test.py index 3e3b869a30..dd3dba8404 100644 --- a/test/torchaudio_unittest/models/wav2vec2/fairseq_integration_test.py +++ b/test/torchaudio_unittest/models/wav2vec2/fairseq_integration_test.py @@ -1,6 +1,7 @@ import json import torch +from parameterized import parameterized from torchaudio.models.wav2vec2 import ( wav2vec2_base, wav2vec2_large, @@ -12,8 +13,6 @@ from torchaudio.models.wav2vec2.utils import ( import_fairseq_model, ) -from parameterized import parameterized - from torchaudio_unittest.common_utils import ( get_asset_path, skipIfNoModule, @@ -22,63 +21,75 @@ def _load_config(*paths): - with open(f'{get_asset_path("wav2vec2", "fairseq", *paths)}.json', 'r') as file_: + with open(f'{get_asset_path("wav2vec2", "fairseq", *paths)}.json', "r") as file_: return json.load(file_) def _name_func(testcase_func, i, param): - return f'{testcase_func.__name__}_{i}_{param[0][1].__name__}' + return f"{testcase_func.__name__}_{i}_{param[0][1].__name__}" # Pretraining models -WAV2VEC2_BASE = _load_config('wav2vec_small') -WAV2VEC2_LARGE = _load_config('libri960_big') -WAV2VEC2_LARGE_LV60K = _load_config('wav2vec_vox_new') -WAV2VEC2_XLSR_53_56K = _load_config('xlsr_53_56k') -HUBERT_BASE = _load_config('hubert_base_ls960') -HUBERT_LARGE_LL60K = _load_config('hubert_large_ll60k') -HUBERT_XLARGE_LL60K = _load_config('hubert_xtralarge_ll60k') +WAV2VEC2_BASE = _load_config("wav2vec_small") +WAV2VEC2_LARGE = _load_config("libri960_big") +WAV2VEC2_LARGE_LV60K = _load_config("wav2vec_vox_new") +WAV2VEC2_XLSR_53_56K = _load_config("xlsr_53_56k") +HUBERT_BASE = _load_config("hubert_base_ls960") +HUBERT_LARGE_LL60K = _load_config("hubert_large_ll60k") +HUBERT_XLARGE_LL60K = _load_config("hubert_xtralarge_ll60k") # Finetuning models -WAV2VEC2_BASE_960H = _load_config('wav2vec_small_960h') -WAV2VEC2_LARGE_960H = _load_config('wav2vec_large_960h') -WAV2VEC2_LARGE_LV60K_960H = _load_config('wav2vec_large_lv60k_960h') -WAV2VEC2_LARGE_LV60K_SELF_960H = _load_config('wav2vec_large_lv60k_self_960h') -HUBERT_LARGE = _load_config('hubert_large_ll60k_finetune_ls960') -HUBERT_XLARGE = _load_config('hubert_xtralarge_ll60k_finetune_ls960') +WAV2VEC2_BASE_960H = _load_config("wav2vec_small_960h") +WAV2VEC2_LARGE_960H = _load_config("wav2vec_large_960h") +WAV2VEC2_LARGE_LV60K_960H = _load_config("wav2vec_large_lv60k_960h") +WAV2VEC2_LARGE_LV60K_SELF_960H = _load_config("wav2vec_large_lv60k_self_960h") +HUBERT_LARGE = _load_config("hubert_large_ll60k_finetune_ls960") +HUBERT_XLARGE = _load_config("hubert_xtralarge_ll60k_finetune_ls960") # Config and corresponding factory functions -WAV2VEC2_PRETRAINING_CONFIGS = parameterized.expand([ - (WAV2VEC2_BASE, wav2vec2_base), - (WAV2VEC2_LARGE, wav2vec2_large), - (WAV2VEC2_LARGE_LV60K, wav2vec2_large_lv60k), - (WAV2VEC2_XLSR_53_56K, wav2vec2_large_lv60k), -], name_func=_name_func) -HUBERT_PRETRAINING_CONFIGS = parameterized.expand([ - (HUBERT_BASE, hubert_base), - (HUBERT_LARGE_LL60K, hubert_large), - (HUBERT_XLARGE_LL60K, hubert_xlarge), -], name_func=_name_func) -ALL_PRETRAINING_CONFIGS = parameterized.expand([ - (WAV2VEC2_BASE, wav2vec2_base), - (WAV2VEC2_LARGE, wav2vec2_large), - (WAV2VEC2_LARGE_LV60K, wav2vec2_large_lv60k), - (WAV2VEC2_XLSR_53_56K, wav2vec2_large_lv60k), - (HUBERT_BASE, hubert_base), - (HUBERT_LARGE_LL60K, hubert_large), - (HUBERT_XLARGE_LL60K, hubert_xlarge), -], name_func=_name_func) -FINETUNING_CONFIGS = parameterized.expand([ - (WAV2VEC2_BASE_960H, wav2vec2_base), - (WAV2VEC2_LARGE_960H, wav2vec2_large), - (WAV2VEC2_LARGE_LV60K_960H, wav2vec2_large_lv60k), - (WAV2VEC2_LARGE_LV60K_SELF_960H, wav2vec2_large_lv60k), - (HUBERT_LARGE, hubert_large), - (HUBERT_XLARGE, hubert_xlarge), -], name_func=_name_func) - - -@skipIfNoModule('fairseq') +WAV2VEC2_PRETRAINING_CONFIGS = parameterized.expand( + [ + (WAV2VEC2_BASE, wav2vec2_base), + (WAV2VEC2_LARGE, wav2vec2_large), + (WAV2VEC2_LARGE_LV60K, wav2vec2_large_lv60k), + (WAV2VEC2_XLSR_53_56K, wav2vec2_large_lv60k), + ], + name_func=_name_func, +) +HUBERT_PRETRAINING_CONFIGS = parameterized.expand( + [ + (HUBERT_BASE, hubert_base), + (HUBERT_LARGE_LL60K, hubert_large), + (HUBERT_XLARGE_LL60K, hubert_xlarge), + ], + name_func=_name_func, +) +ALL_PRETRAINING_CONFIGS = parameterized.expand( + [ + (WAV2VEC2_BASE, wav2vec2_base), + (WAV2VEC2_LARGE, wav2vec2_large), + (WAV2VEC2_LARGE_LV60K, wav2vec2_large_lv60k), + (WAV2VEC2_XLSR_53_56K, wav2vec2_large_lv60k), + (HUBERT_BASE, hubert_base), + (HUBERT_LARGE_LL60K, hubert_large), + (HUBERT_XLARGE_LL60K, hubert_xlarge), + ], + name_func=_name_func, +) +FINETUNING_CONFIGS = parameterized.expand( + [ + (WAV2VEC2_BASE_960H, wav2vec2_base), + (WAV2VEC2_LARGE_960H, wav2vec2_large), + (WAV2VEC2_LARGE_LV60K_960H, wav2vec2_large_lv60k), + (WAV2VEC2_LARGE_LV60K_SELF_960H, wav2vec2_large_lv60k), + (HUBERT_LARGE, hubert_large), + (HUBERT_XLARGE, hubert_xlarge), + ], + name_func=_name_func, +) + + +@skipIfNoModule("fairseq") class TestFairseqIntegration(TorchaudioTestCase): """Test the process of importing the models from fairseq. @@ -86,9 +97,18 @@ class TestFairseqIntegration(TorchaudioTestCase): 1. Models loaded with fairseq cane be imported. 2. The same model can be recreated without fairseq. """ + def _get_model(self, config, num_out=None): import copy - from omegaconf import OmegaConf + + from fairseq.models.hubert.hubert import ( + HubertModel, + HubertConfig, + ) + from fairseq.models.hubert.hubert_asr import ( + HubertCtcConfig, + HubertEncoder, + ) from fairseq.models.wav2vec.wav2vec2 import ( Wav2Vec2Config, Wav2Vec2Model, @@ -97,32 +117,25 @@ def _get_model(self, config, num_out=None): Wav2VecEncoder, Wav2Vec2CtcConfig, ) - from fairseq.models.hubert.hubert_asr import ( - HubertCtcConfig, - HubertEncoder, - ) - from fairseq.models.hubert.hubert import ( - HubertModel, - HubertConfig, - ) from fairseq.tasks.hubert_pretraining import HubertPretrainingConfig + from omegaconf import OmegaConf - if config['_name'] == 'wav2vec_ctc': + if config["_name"] == "wav2vec_ctc": config = copy.deepcopy(config) - config['w2v_args'] = OmegaConf.create(config['w2v_args']) + config["w2v_args"] = OmegaConf.create(config["w2v_args"]) return Wav2VecEncoder(Wav2Vec2CtcConfig(**config), num_out) - if config['_name'] == 'wav2vec2': + if config["_name"] == "wav2vec2": return Wav2Vec2Model(Wav2Vec2Config(**config)) - if config['_name'] == 'hubert_ctc': + if config["_name"] == "hubert_ctc": config = copy.deepcopy(config) - config['w2v_args'] = OmegaConf.create(config['w2v_args']) + config["w2v_args"] = OmegaConf.create(config["w2v_args"]) ctc_cfg = HubertCtcConfig(**config) return HubertEncoder(ctc_cfg, tgt_dict=range(num_out)) - if config['_name'] == 'hubert': - dicts = [list(range(i)) for i in config['num_classes']] + if config["_name"] == "hubert": + dicts = [list(range(i)) for i in config["num_classes"]] return HubertModel( - HubertConfig(**config['model']), - HubertPretrainingConfig(**config['task']), + HubertConfig(**config["model"]), + HubertPretrainingConfig(**config["task"]), dicts, ) raise ValueError(f'Unexpected configuration: {config["_name"]}') @@ -139,7 +152,7 @@ def test_import_wave2vec2_pretraining_model(self, config, _): x = torch.randn(batch_size, num_frames) hyp, _ = imported.extract_features(x) refs = original.extract_features(x, padding_mask=torch.zeros_like(x), layer=-1) - for i, (ref, _) in enumerate(refs['layer_results']): + for i, (ref, _) in enumerate(refs["layer_results"]): self.assertEqual(hyp[i], ref.transpose(0, 1)) @HUBERT_PRETRAINING_CONFIGS @@ -177,7 +190,13 @@ def test_recreate_pretraining_model(self, config, factory_func): reloaded.eval() x = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) # Without mask ref, _ = imported(x) hyp, _ = reloaded(x) @@ -200,14 +219,20 @@ def test_import_finetuning_model(self, config, _): # Without mask x = torch.randn(batch_size, num_frames) - ref = original(x, torch.zeros_like(x))['encoder_out'].transpose(0, 1) + ref = original(x, torch.zeros_like(x))["encoder_out"].transpose(0, 1) hyp, _ = imported(x) self.assertEqual(ref, hyp) # With mask - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) mask = torch.arange(num_frames).expand(batch_size, num_frames) >= lengths[:, None] - ref = original(x, mask)['encoder_out'].transpose(0, 1) + ref = original(x, mask)["encoder_out"].transpose(0, 1) hyp, output_lengths = imported(x, lengths) for i, l in enumerate(output_lengths): self.assertEqual(ref[i, :l, ...], hyp[i, :l, ...]) @@ -233,7 +258,13 @@ def test_recreate_finetuning_model(self, config, factory_func): self.assertEqual(ref, hyp) # With mask - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) ref, ref_lengths = imported(x, lengths) hyp, hyp_lengths = reloaded(x, lengths) self.assertEqual(ref, hyp) diff --git a/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py b/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py index 49a7a88c21..0301266ff7 100644 --- a/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py +++ b/test/torchaudio_unittest/models/wav2vec2/huggingface_intergration_test.py @@ -1,14 +1,13 @@ import json import torch +from parameterized import parameterized from torchaudio.models.wav2vec2 import ( wav2vec2_base, wav2vec2_large, wav2vec2_large_lv60k, ) from torchaudio.models.wav2vec2.utils import import_huggingface_model -from parameterized import parameterized - from torchaudio_unittest.common_utils import ( get_asset_path, skipIfNoModule, @@ -17,7 +16,7 @@ def _load_config(*paths): - with open(f'{get_asset_path("wav2vec2", "huggingface", *paths)}.json', 'r') as file_: + with open(f'{get_asset_path("wav2vec2", "huggingface", *paths)}.json', "r") as file_: return json.load(file_) @@ -26,36 +25,42 @@ def _name_func(testcase_func, i, param): # Pretrained -HF_BASE = _load_config('wav2vec2-base') -HF_LARGE = _load_config('wav2vec2-large') -HF_LARGE_LV60 = _load_config('wav2vec2-large-lv60') -HF_LARGE_XLSR_53 = _load_config('wav2vec2-large-xlsr-53') -HF_BASE_10K_VOXPOPULI = _load_config('wav2vec2-base-10k-voxpopuli') +HF_BASE = _load_config("wav2vec2-base") +HF_LARGE = _load_config("wav2vec2-large") +HF_LARGE_LV60 = _load_config("wav2vec2-large-lv60") +HF_LARGE_XLSR_53 = _load_config("wav2vec2-large-xlsr-53") +HF_BASE_10K_VOXPOPULI = _load_config("wav2vec2-base-10k-voxpopuli") # Finetuned -HF_BASE_960H = _load_config('wav2vec2-base-960h') -HF_LARGE_960H = _load_config('wav2vec2-large-960h') -HF_LARGE_LV60_960H = _load_config('wav2vec2-large-960h-lv60') -HF_LARGE_LV60_SELF_960H = _load_config('wav2vec2-large-960h-lv60-self') -HF_LARGE_XLSR_DE = _load_config('wav2vec2-large-xlsr-53-german') +HF_BASE_960H = _load_config("wav2vec2-base-960h") +HF_LARGE_960H = _load_config("wav2vec2-large-960h") +HF_LARGE_LV60_960H = _load_config("wav2vec2-large-960h-lv60") +HF_LARGE_LV60_SELF_960H = _load_config("wav2vec2-large-960h-lv60-self") +HF_LARGE_XLSR_DE = _load_config("wav2vec2-large-xlsr-53-german") # Config and corresponding factory functions -PRETRAIN_CONFIGS = parameterized.expand([ - (HF_BASE, wav2vec2_base), - (HF_LARGE, wav2vec2_large), - (HF_LARGE_LV60, wav2vec2_large_lv60k), - (HF_LARGE_XLSR_53, wav2vec2_large_lv60k), - (HF_BASE_10K_VOXPOPULI, wav2vec2_base), -], name_func=_name_func) -FINETUNE_CONFIGS = parameterized.expand([ - (HF_BASE_960H, wav2vec2_base), - (HF_LARGE_960H, wav2vec2_large), - (HF_LARGE_LV60_960H, wav2vec2_large_lv60k), - (HF_LARGE_LV60_SELF_960H, wav2vec2_large_lv60k), - (HF_LARGE_XLSR_DE, wav2vec2_large_lv60k), -], name_func=_name_func) - - -@skipIfNoModule('transformers') +PRETRAIN_CONFIGS = parameterized.expand( + [ + (HF_BASE, wav2vec2_base), + (HF_LARGE, wav2vec2_large), + (HF_LARGE_LV60, wav2vec2_large_lv60k), + (HF_LARGE_XLSR_53, wav2vec2_large_lv60k), + (HF_BASE_10K_VOXPOPULI, wav2vec2_base), + ], + name_func=_name_func, +) +FINETUNE_CONFIGS = parameterized.expand( + [ + (HF_BASE_960H, wav2vec2_base), + (HF_LARGE_960H, wav2vec2_large), + (HF_LARGE_LV60_960H, wav2vec2_large_lv60k), + (HF_LARGE_LV60_SELF_960H, wav2vec2_large_lv60k), + (HF_LARGE_XLSR_DE, wav2vec2_large_lv60k), + ], + name_func=_name_func, +) + + +@skipIfNoModule("transformers") class TestHFIntegration(TorchaudioTestCase): """Test the process of importing the models from Hugging Face Transformers @@ -63,6 +68,7 @@ class TestHFIntegration(TorchaudioTestCase): 1. Models loaded with Hugging Face Transformers cane be imported. 2. The same model can be recreated without Hugging Face Transformers. """ + def _get_model(self, config): # Helper function to avoid importing transformers on module scope. # Normally, we use `is_module_available` helper function to check if @@ -75,9 +81,10 @@ def _get_model(self, config): Wav2Vec2Model, Wav2Vec2ForCTC, ) - if config['architectures'] == ['Wav2Vec2Model']: + + if config["architectures"] == ["Wav2Vec2Model"]: return Wav2Vec2Model(Wav2Vec2Config(**config)) - if config['architectures'] == ['Wav2Vec2ForCTC']: + if config["architectures"] == ["Wav2Vec2ForCTC"]: return Wav2Vec2ForCTC(Wav2Vec2Config(**config)) raise ValueError(f'Unexpected arch: {config["architectures"]}') @@ -89,12 +96,12 @@ def _test_import_pretrain(self, original, imported, config): hyp, _ = imported.feature_extractor(x, None) self.assertEqual(ref, hyp) # Feature projection - x = torch.randn(3, 10, config['conv_dim'][-1]) + x = torch.randn(3, 10, config["conv_dim"][-1]) ref = original.feature_projection(x)[0] hyp = imported.encoder.feature_projection(x) self.assertEqual(ref, hyp) # Convolutional Positional Encoder - x = torch.randn(3, 256, config['hidden_size']) + x = torch.randn(3, 256, config["hidden_size"]) ref = original.encoder.pos_conv_embed(x) hyp = imported.encoder.transformer.pos_conv_embed(x) self.assertEqual(ref, hyp) @@ -104,7 +111,7 @@ def _test_import_pretrain(self, original, imported, config): x = torch.randn(b, l, e) mask = torch.randn(b, 1, l, l) - ref, = original_(x, attention_mask=mask, output_attentions=False) + (ref,) = original_(x, attention_mask=mask, output_attentions=False) hyp = imported_(x, mask) self.assertEqual(ref, hyp) # The whole Encoder Transformer @@ -135,7 +142,13 @@ def _test_import_finetune(self, original, imported, config): # The whole model with mask batch_size, num_frames = 3, 1024 x = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) mask = torch.arange(num_frames).expand(batch_size, num_frames) < lengths[:, None] ref = original(x, attention_mask=mask).logits @@ -167,12 +180,12 @@ def _test_recreate(self, imported, reloaded, config): hyp, _ = reloaded.feature_extractor(x, None) self.assertEqual(ref, hyp) # Feature projection - x = torch.randn(3, 10, config['conv_dim'][-1]) + x = torch.randn(3, 10, config["conv_dim"][-1]) ref = imported.encoder.feature_projection(x) hyp = reloaded.encoder.feature_projection(x) self.assertEqual(ref, hyp) # Convolutional Positional Encoder - x = torch.randn(3, 256, config['hidden_size']) + x = torch.randn(3, 256, config["hidden_size"]) ref = imported.encoder.transformer.pos_conv_embed(x) hyp = reloaded.encoder.transformer.pos_conv_embed(x) self.assertEqual(ref, hyp) diff --git a/test/torchaudio_unittest/models/wav2vec2/model_test.py b/test/torchaudio_unittest/models/wav2vec2/model_test.py index 0f0757b910..7d6bcd8759 100644 --- a/test/torchaudio_unittest/models/wav2vec2/model_test.py +++ b/test/torchaudio_unittest/models/wav2vec2/model_test.py @@ -1,9 +1,9 @@ import os +from typing import Tuple import torch import torch.nn.functional as F -from typing import Tuple - +from parameterized import parameterized from torchaudio.models.wav2vec2 import ( wav2vec2_base, wav2vec2_large, @@ -18,7 +18,6 @@ skipIfNoCuda, torch_script, ) -from parameterized import parameterized TORCH_VERSION: Tuple[int, ...] = tuple(int(x) for x in torch.__version__.split(".")[:2]) if TORCH_VERSION >= (1, 10): @@ -31,14 +30,17 @@ def _name_func(testcase_func, i, param): return f"{testcase_func.__name__}_{i}_{param[0][0].__name__}" -factory_funcs = parameterized.expand([ - (wav2vec2_base, ), - (wav2vec2_large, ), - (wav2vec2_large_lv60k, ), - (hubert_base, ), - (hubert_large, ), - (hubert_xlarge, ), -], name_func=_name_func) +factory_funcs = parameterized.expand( + [ + (wav2vec2_base,), + (wav2vec2_large,), + (wav2vec2_large_lv60k,), + (hubert_base,), + (hubert_large,), + (hubert_xlarge,), + ], + name_func=_name_func, +) class TestWav2Vec2Model(TorchaudioTestCase): @@ -49,27 +51,32 @@ def _smoke_test(self, model, device, dtype): torch.manual_seed(0) batch_size, num_frames = 3, 1024 - waveforms = torch.randn( - batch_size, num_frames, device=device, dtype=dtype) + waveforms = torch.randn(batch_size, num_frames, device=device, dtype=dtype) lengths = torch.randint( - low=0, high=num_frames, size=[batch_size, ], device=device) + low=0, + high=num_frames, + size=[ + batch_size, + ], + device=device, + ) model(waveforms, lengths) - @parameterized.expand([(torch.float32, ), (torch.float64, )]) + @parameterized.expand([(torch.float32,), (torch.float64,)]) def test_cpu_smoke_test(self, dtype): model = wav2vec2_base() - self._smoke_test(model, torch.device('cpu'), dtype) + self._smoke_test(model, torch.device("cpu"), dtype) model = wav2vec2_base(aux_num_out=32) - self._smoke_test(model, torch.device('cpu'), dtype) + self._smoke_test(model, torch.device("cpu"), dtype) - @parameterized.expand([(torch.float32, ), (torch.float64, )]) + @parameterized.expand([(torch.float32,), (torch.float64,)]) @skipIfNoCuda def test_cuda_smoke_test(self, dtype): model = wav2vec2_base() - self._smoke_test(model, torch.device('cuda'), dtype) + self._smoke_test(model, torch.device("cuda"), dtype) model = wav2vec2_base(aux_num_out=32) - self._smoke_test(model, torch.device('cuda'), dtype) + self._smoke_test(model, torch.device("cuda"), dtype) def _feature_extractor_test(self, model): batch_size, num_frames = 3, 1024 @@ -79,7 +86,13 @@ def _feature_extractor_test(self, model): torch.manual_seed(0) waveforms = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) # Not providing num_layers returns all the intermediate features from # tranformer layers @@ -114,8 +127,8 @@ def _test_batch_consistency(self, model): batch_logits, output_lengths = model(waveforms, input_lengths) for i in range(batch_size): # Par-sample process without feeding length - single_logit, _ = model(waveforms[i:i + 1, :input_lengths[i]], None) - batch_logit = batch_logits[i:i + 1, :output_lengths[i]] + single_logit, _ = model(waveforms[i : i + 1, : input_lengths[i]], None) + batch_logit = batch_logits[i : i + 1, : output_lengths[i]] # Convert to probability so that it's easier to interpretate the diff single_prob = F.softmax(single_logit, dim=2) @@ -125,14 +138,12 @@ def _test_batch_consistency(self, model): @factory_funcs def test_pretrain_batch_consistency(self, factory_func): - """Results from single process and batched process should be reasonably close - """ + """Results from single process and batched process should be reasonably close""" self._test_batch_consistency(factory_func()) @factory_funcs def test_finetune_batch_consistency(self, factory_func): - """Results from single process and batched process should be reasonably close - """ + """Results from single process and batched process should be reasonably close""" self._test_batch_consistency(factory_func(aux_num_out=32)) def _test_zero_length(self, model): @@ -163,7 +174,13 @@ def _test_torchscript(self, model): torch.manual_seed(0) waveforms = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) ref_out, ref_len = model(waveforms, lengths) @@ -177,19 +194,19 @@ def _test_torchscript(self, model): @factory_funcs def test_pretrain_torchscript(self, factory_func): """Wav2Vec2Model should be scriptable""" - if factory_func is hubert_xlarge and os.name == 'nt' and os.environ.get('CI') == 'true': + if factory_func is hubert_xlarge and os.name == "nt" and os.environ.get("CI") == "true": self.skipTest( - 'hubert_xlarge is known to fail on Windows CI. ' - 'See https://github.com/pytorch/pytorch/issues/65776') + "hubert_xlarge is known to fail on Windows CI. " "See https://github.com/pytorch/pytorch/issues/65776" + ) self._test_torchscript(factory_func()) @factory_funcs def test_finetune_torchscript(self, factory_func): """Wav2Vec2Model should be scriptable""" - if factory_func is hubert_xlarge and os.name == 'nt' and os.environ.get('CI') == 'true': + if factory_func is hubert_xlarge and os.name == "nt" and os.environ.get("CI") == "true": self.skipTest( - 'hubert_xlarge is known to fail on Windows CI. ' - 'See https://github.com/pytorch/pytorch/issues/65776') + "hubert_xlarge is known to fail on Windows CI. " "See https://github.com/pytorch/pytorch/issues/65776" + ) self._test_torchscript(factory_func(aux_num_out=32)) def _test_quantize_smoke_test(self, model): @@ -198,15 +215,20 @@ def _test_quantize_smoke_test(self, model): # Remove the weight normalization forward hook model.encoder.transformer.pos_conv_embed.__prepare_scriptable__() - quantized = tq.quantize_dynamic( - model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) + quantized = tq.quantize_dynamic(model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) # A lazy way to check that Modules are different assert str(quantized) != str(model), "Dynamic quantization did not modify the module." torch.manual_seed(0) waveforms = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) _, _ = quantized(waveforms, lengths) @@ -223,15 +245,20 @@ def _test_quantize_torchscript(self, model): # Remove the weight normalization forward hook model.encoder.transformer.pos_conv_embed.__prepare_scriptable__() - quantized = tq.quantize_dynamic( - model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) + quantized = tq.quantize_dynamic(model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) # A lazy way to check that Modules are different assert str(quantized) != str(model), "Dynamic quantization did not modify the module." torch.manual_seed(0) waveforms = torch.randn(batch_size, num_frames) - lengths = torch.randint(low=0, high=num_frames, size=[batch_size, ]) + lengths = torch.randint( + low=0, + high=num_frames, + size=[ + batch_size, + ], + ) ref_out, ref_len = quantized(waveforms, lengths) diff --git a/test/torchaudio_unittest/prototype/conformer_cpu_test.py b/test/torchaudio_unittest/prototype/conformer_cpu_test.py index a566b1d697..567a6cf8d9 100644 --- a/test/torchaudio_unittest/prototype/conformer_cpu_test.py +++ b/test/torchaudio_unittest/prototype/conformer_cpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl from torchaudio_unittest.common_utils import PytorchTestCase +from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl class ConformerFloat32CPUTest(ConformerTestImpl, PytorchTestCase): diff --git a/test/torchaudio_unittest/prototype/conformer_gpu_test.py b/test/torchaudio_unittest/prototype/conformer_gpu_test.py index 85e46c6386..82c9e2cfda 100644 --- a/test/torchaudio_unittest/prototype/conformer_gpu_test.py +++ b/test/torchaudio_unittest/prototype/conformer_gpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase +from torchaudio_unittest.prototype.conformer_test_impl import ConformerTestImpl @skipIfNoCuda diff --git a/test/torchaudio_unittest/prototype/conformer_test_impl.py b/test/torchaudio_unittest/prototype/conformer_test_impl.py index cf16b704c3..98d69c4b68 100644 --- a/test/torchaudio_unittest/prototype/conformer_test_impl.py +++ b/test/torchaudio_unittest/prototype/conformer_test_impl.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.common_utils import TestBaseMixin, torch_script from torchaudio.prototype import Conformer +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script class ConformerTestImpl(TestBaseMixin): @@ -24,12 +24,8 @@ def _gen_model(self): return conformer def _gen_inputs(self, input_dim, batch_size, num_frames): - lengths = torch.randint(1, num_frames, (batch_size,)).to( - device=self.device, dtype=self.dtype - ) - input = torch.rand(batch_size, int(lengths.max()), input_dim).to( - device=self.device, dtype=self.dtype - ) + lengths = torch.randint(1, num_frames, (batch_size,)).to(device=self.device, dtype=self.dtype) + input = torch.rand(batch_size, int(lengths.max()), input_dim).to(device=self.device, dtype=self.dtype) return input, lengths def setUp(self): diff --git a/test/torchaudio_unittest/prototype/emformer_cpu_test.py b/test/torchaudio_unittest/prototype/emformer_cpu_test.py index 85f4c82afc..79dc073461 100644 --- a/test/torchaudio_unittest/prototype/emformer_cpu_test.py +++ b/test/torchaudio_unittest/prototype/emformer_cpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.emformer_test_impl import EmformerTestImpl from torchaudio_unittest.common_utils import PytorchTestCase +from torchaudio_unittest.prototype.emformer_test_impl import EmformerTestImpl class EmformerFloat32CPUTest(EmformerTestImpl, PytorchTestCase): diff --git a/test/torchaudio_unittest/prototype/emformer_gpu_test.py b/test/torchaudio_unittest/prototype/emformer_gpu_test.py index d57b115940..678f6f88b8 100644 --- a/test/torchaudio_unittest/prototype/emformer_gpu_test.py +++ b/test/torchaudio_unittest/prototype/emformer_gpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.emformer_test_impl import EmformerTestImpl from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase +from torchaudio_unittest.prototype.emformer_test_impl import EmformerTestImpl @skipIfNoCuda diff --git a/test/torchaudio_unittest/prototype/emformer_test_impl.py b/test/torchaudio_unittest/prototype/emformer_test_impl.py index 15f49e367b..a990353cff 100644 --- a/test/torchaudio_unittest/prototype/emformer_test_impl.py +++ b/test/torchaudio_unittest/prototype/emformer_test_impl.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.common_utils import TestBaseMixin, torch_script from torchaudio.prototype import Emformer +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script class EmformerTestImpl(TestBaseMixin): @@ -18,9 +18,7 @@ def _gen_model(self, input_dim, right_context_length): return emformer def _gen_inputs(self, input_dim, batch_size, num_frames, right_context_length): - input = torch.rand(batch_size, num_frames, input_dim).to( - device=self.device, dtype=self.dtype - ) + input = torch.rand(batch_size, num_frames, input_dim).to(device=self.device, dtype=self.dtype) lengths = torch.randint(1, num_frames - right_context_length, (batch_size,)).to( device=self.device, dtype=self.dtype ) @@ -34,9 +32,7 @@ def test_torchscript_consistency_forward(self): right_context_length = 1 emformer = self._gen_model(input_dim, right_context_length) - input, lengths = self._gen_inputs( - input_dim, batch_size, num_frames, right_context_length - ) + input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, right_context_length) scripted = torch_script(emformer) ref_out, ref_len = emformer(input, lengths) @@ -59,9 +55,7 @@ def test_torchscript_consistency_infer(self): for _ in range(3): input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, 0) ref_out, ref_len, ref_state = emformer.infer(input, lengths, ref_state) - scripted_out, scripted_len, scripted_state = scripted.infer( - input, lengths, scripted_state - ) + scripted_out, scripted_len, scripted_state = scripted.infer(input, lengths, scripted_state) self.assertEqual(ref_out, scripted_out) self.assertEqual(ref_len, scripted_len) self.assertEqual(ref_state, scripted_state) @@ -74,15 +68,11 @@ def test_output_shape_forward(self): right_context_length = 9 emformer = self._gen_model(input_dim, right_context_length) - input, lengths = self._gen_inputs( - input_dim, batch_size, num_frames, right_context_length - ) + input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, right_context_length) output, output_lengths = emformer(input, lengths) - self.assertEqual( - (batch_size, num_frames - right_context_length, input_dim), output.shape - ) + self.assertEqual((batch_size, num_frames - right_context_length, input_dim), output.shape) self.assertEqual((batch_size,), output_lengths.shape) def test_output_shape_infer(self): @@ -98,9 +88,7 @@ def test_output_shape_infer(self): for _ in range(3): input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, 0) output, output_lengths, state = emformer.infer(input, lengths, state) - self.assertEqual( - (batch_size, num_frames - right_context_length, input_dim), output.shape - ) + self.assertEqual((batch_size, num_frames - right_context_length, input_dim), output.shape) self.assertEqual((batch_size,), output_lengths.shape) def test_output_lengths_forward(self): @@ -111,9 +99,7 @@ def test_output_lengths_forward(self): right_context_length = 2 emformer = self._gen_model(input_dim, right_context_length) - input, lengths = self._gen_inputs( - input_dim, batch_size, num_frames, right_context_length - ) + input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, right_context_length) _, output_lengths = emformer(input, lengths) self.assertEqual(lengths, output_lengths) @@ -127,6 +113,4 @@ def test_output_lengths_infer(self): emformer = self._gen_model(input_dim, right_context_length).eval() input, lengths = self._gen_inputs(input_dim, batch_size, num_frames, 0) _, output_lengths, _ = emformer.infer(input, lengths) - self.assertEqual( - torch.clamp(lengths - right_context_length, min=0), output_lengths - ) + self.assertEqual(torch.clamp(lengths - right_context_length, min=0), output_lengths) diff --git a/test/torchaudio_unittest/prototype/rnnt_cpu_test.py b/test/torchaudio_unittest/prototype/rnnt_cpu_test.py index 93d28bacaa..0a8f8eab3a 100644 --- a/test/torchaudio_unittest/prototype/rnnt_cpu_test.py +++ b/test/torchaudio_unittest/prototype/rnnt_cpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl from torchaudio_unittest.common_utils import PytorchTestCase +from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl class RNNTFloat32CPUTest(RNNTTestImpl, PytorchTestCase): diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py b/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py index 03382386d7..f5c1e6f721 100644 --- a/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_cpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl from torchaudio_unittest.common_utils import PytorchTestCase +from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl class RNNTBeamSearchFloat32CPUTest(RNNTBeamSearchTestImpl, PytorchTestCase): diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py b/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py index 09b0264f78..fa5c21d6b8 100644 --- a/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_gpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase +from torchaudio_unittest.prototype.rnnt_decoder_test_impl import RNNTBeamSearchTestImpl @skipIfNoCuda diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py index 8845fc1687..bcc9364b97 100644 --- a/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py @@ -1,5 +1,4 @@ import torch - from torchaudio.prototype import RNNTBeamSearch, emformer_rnnt_model from torchaudio_unittest.common_utils import TestBaseMixin, torch_script @@ -42,11 +41,7 @@ def _get_model_config(self): } def _get_model(self): - return ( - emformer_rnnt_model(**self._get_model_config()) - .to(device=self.device, dtype=self.dtype) - .eval() - ) + return emformer_rnnt_model(**self._get_model_config()).to(device=self.device, dtype=self.dtype).eval() def test_torchscript_consistency_forward(self): r"""Verify that scripting RNNTBeamSearch does not change the behavior of method `forward`.""" @@ -62,12 +57,10 @@ def test_torchscript_consistency_forward(self): blank_idx = num_symbols - 1 beam_width = 5 - input = torch.rand( - batch_size, max_input_length + right_context_length, input_dim - ).to(device=self.device, dtype=self.dtype) - lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to( - device=self.device, dtype=torch.int32 + input = torch.rand(batch_size, max_input_length + right_context_length, input_dim).to( + device=self.device, dtype=self.dtype ) + lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to(device=self.device, dtype=torch.int32) model = self._get_model() beam_search = RNNTBeamSearch(model, blank_idx) @@ -91,12 +84,10 @@ def test_torchscript_consistency_infer(self): blank_idx = num_symbols - 1 beam_width = 5 - input = torch.rand( - segment_length + right_context_length, input_dim - ).to(device=self.device, dtype=self.dtype) - lengths = torch.randint( - 1, segment_length + right_context_length + 1, () - ).to(device=self.device, dtype=torch.int32) + input = torch.rand(segment_length + right_context_length, input_dim).to(device=self.device, dtype=self.dtype) + lengths = torch.randint(1, segment_length + right_context_length + 1, ()).to( + device=self.device, dtype=torch.int32 + ) model = self._get_model() @@ -107,9 +98,7 @@ def test_torchscript_consistency_infer(self): scripted = torch_script(beam_search) res = beam_search.infer(input, lengths, beam_width, state=state, hypothesis=hypo) - scripted_res = scripted.infer( - input, lengths, beam_width, state=scripted_state, hypothesis=scripted_hypo - ) + scripted_res = scripted.infer(input, lengths, beam_width, state=scripted_state, hypothesis=scripted_hypo) self.assertEqual(res, scripted_res) diff --git a/test/torchaudio_unittest/prototype/rnnt_gpu_test.py b/test/torchaudio_unittest/prototype/rnnt_gpu_test.py index 04f476e6ee..25facbecb5 100644 --- a/test/torchaudio_unittest/prototype/rnnt_gpu_test.py +++ b/test/torchaudio_unittest/prototype/rnnt_gpu_test.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase +from torchaudio_unittest.prototype.rnnt_test_impl import RNNTTestImpl @skipIfNoCuda diff --git a/test/torchaudio_unittest/prototype/rnnt_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_test_impl.py index ba68aa7b03..1357927700 100644 --- a/test/torchaudio_unittest/prototype/rnnt_test_impl.py +++ b/test/torchaudio_unittest/prototype/rnnt_test_impl.py @@ -1,6 +1,6 @@ import torch -from torchaudio_unittest.common_utils import TestBaseMixin, torch_script from torchaudio.prototype.rnnt import emformer_rnnt_model +from torchaudio_unittest.common_utils import TestBaseMixin, torch_script class RNNTTestImpl(TestBaseMixin): @@ -45,11 +45,7 @@ def _get_model_config(self): } def _get_model(self): - return ( - emformer_rnnt_model(**self._get_model_config()) - .to(device=self.device, dtype=self.dtype) - .eval() - ) + return emformer_rnnt_model(**self._get_model_config()).to(device=self.device, dtype=self.dtype).eval() def _get_transcriber_input(self): input_config = self._get_input_config() @@ -59,12 +55,10 @@ def _get_transcriber_input(self): right_context_length = input_config["right_context_length"] torch.random.manual_seed(31) - input = torch.rand( - batch_size, max_input_length + right_context_length, input_dim - ).to(device=self.device, dtype=self.dtype) - lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to( - device=self.device, dtype=torch.int32 + input = torch.rand(batch_size, max_input_length + right_context_length, input_dim).to( + device=self.device, dtype=self.dtype ) + lengths = torch.randint(1, max_input_length + 1, (batch_size,)).to(device=self.device, dtype=torch.int32) return input, lengths def _get_transcriber_streaming_input(self): @@ -75,12 +69,12 @@ def _get_transcriber_streaming_input(self): right_context_length = input_config["right_context_length"] torch.random.manual_seed(31) - input = torch.rand( - batch_size, segment_length + right_context_length, input_dim - ).to(device=self.device, dtype=self.dtype) - lengths = torch.randint( - 1, segment_length + right_context_length + 1, (batch_size,) - ).to(device=self.device, dtype=torch.int32) + input = torch.rand(batch_size, segment_length + right_context_length, input_dim).to( + device=self.device, dtype=self.dtype + ) + lengths = torch.randint(1, segment_length + right_context_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) return input, lengths def _get_predictor_input(self): @@ -90,12 +84,8 @@ def _get_predictor_input(self): max_target_length = input_config["max_target_length"] torch.random.manual_seed(31) - input = torch.randint(0, num_symbols, (batch_size, max_target_length)).to( - device=self.device, dtype=torch.int32 - ) - lengths = torch.randint(1, max_target_length + 1, (batch_size,)).to( - device=self.device, dtype=torch.int32 - ) + input = torch.randint(0, num_symbols, (batch_size, max_target_length)).to(device=self.device, dtype=torch.int32) + lengths = torch.randint(1, max_target_length + 1, (batch_size,)).to(device=self.device, dtype=torch.int32) return input, lengths def _get_joiner_input(self): @@ -106,15 +96,13 @@ def _get_joiner_input(self): input_dim = input_config["encoding_dim"] torch.random.manual_seed(31) - utterance_encodings = torch.rand( - batch_size, joiner_max_input_length, input_dim - ).to(device=self.device, dtype=self.dtype) - utterance_lengths = torch.randint( - 0, joiner_max_input_length + 1, (batch_size,) - ).to(device=self.device, dtype=torch.int32) - target_encodings = torch.rand(batch_size, max_target_length, input_dim).to( + utterance_encodings = torch.rand(batch_size, joiner_max_input_length, input_dim).to( device=self.device, dtype=self.dtype ) + utterance_lengths = torch.randint(0, joiner_max_input_length + 1, (batch_size,)).to( + device=self.device, dtype=torch.int32 + ) + target_encodings = torch.rand(batch_size, max_target_length, input_dim).to(device=self.device, dtype=self.dtype) target_lengths = torch.randint(0, max_target_length + 1, (batch_size,)).to( device=self.device, dtype=torch.int32 ) @@ -167,9 +155,7 @@ def test_torchscript_consistency_transcribe_streaming(self): ref_state, scripted_state = None, None for _ in range(2): - ref_out, ref_lengths, ref_state = rnnt.transcribe_streaming( - input, lengths, ref_state - ) + ref_out, ref_lengths, ref_state = rnnt.transcribe_streaming(input, lengths, ref_state) ( scripted_out, scripted_lengths, @@ -190,9 +176,7 @@ def test_torchscript_consistency_predict(self): ref_state, scripted_state = None, None for _ in range(2): ref_out, ref_lengths, ref_state = rnnt.predict(input, lengths, ref_state) - scripted_out, scripted_lengths, scripted_state = scripted.predict( - input, lengths, scripted_state - ) + scripted_out, scripted_lengths, scripted_state = scripted.predict(input, lengths, scripted_state) self.assertEqual(ref_out, scripted_out) self.assertEqual(ref_lengths, scripted_lengths) self.assertEqual(ref_state, scripted_state) @@ -234,9 +218,7 @@ def test_output_shape_forward(self): state = None for _ in range(2): - out, out_lengths, target_lengths, state = rnnt( - inputs, input_lengths, targets, target_lengths, state - ) + out, out_lengths, target_lengths, state = rnnt(inputs, input_lengths, targets, target_lengths, state) self.assertEqual( (batch_size, joiner_max_input_length, max_target_length, num_symbols), out.shape, diff --git a/test/torchaudio_unittest/sox_effect/common.py b/test/torchaudio_unittest/sox_effect/common.py index 00ac4a4565..9fe6ef363b 100644 --- a/test/torchaudio_unittest/sox_effect/common.py +++ b/test/torchaudio_unittest/sox_effect/common.py @@ -1,7 +1,6 @@ import json from parameterized import param - from torchaudio_unittest.common_utils import get_asset_path @@ -10,15 +9,15 @@ def name_func(func, _, params): args = "_".join([str(arg) for arg in params.args]) else: args = "_".join([str(arg) for arg in params.args[0]]) - return f'{func.__name__}_{args}' + return f"{func.__name__}_{args}" def load_params(*paths): params = [] - with open(get_asset_path(*paths), 'r') as file: + with open(get_asset_path(*paths), "r") as file: for line in file: data = json.loads(line) - for effect in data['effects']: + for effect in data["effects"]: for i, arg in enumerate(effect): if arg.startswith(""): effect[i] = arg.replace("", get_asset_path()) diff --git a/test/torchaudio_unittest/sox_effect/dataset_test.py b/test/torchaudio_unittest/sox_effect/dataset_test.py index 37ee83aa7c..5e5d2f6bb3 100644 --- a/test/torchaudio_unittest/sox_effect/dataset_test.py +++ b/test/torchaudio_unittest/sox_effect/dataset_test.py @@ -1,14 +1,13 @@ import os -import sys import platform -from unittest import skipIf -from typing import List, Tuple +import sys from concurrent.futures import ProcessPoolExecutor +from typing import List, Tuple +from unittest import skipIf import numpy as np import torch import torchaudio - from torchaudio_unittest.common_utils import ( TempDirMixin, PytorchTestCase, @@ -20,6 +19,7 @@ class RandomPerturbationFile(torch.utils.data.Dataset): """Given flist, apply random speed perturbation""" + def __init__(self, flist: List[str], sample_rate: int): super().__init__() self.flist = flist @@ -29,11 +29,11 @@ def __init__(self, flist: List[str], sample_rate: int): def __getitem__(self, index): speed = self.rng.uniform(0.5, 2.0) effects = [ - ['gain', '-n', '-10'], - ['speed', f'{speed:.5f}'], # duration of data is 0.5 ~ 2.0 seconds. - ['rate', f'{self.sample_rate}'], - ['pad', '0', '1.5'], # add 1.5 seconds silence at the end - ['trim', '0', '2'], # get the first 2 seconds + ["gain", "-n", "-10"], + ["speed", f"{speed:.5f}"], # duration of data is 0.5 ~ 2.0 seconds. + ["rate", f"{self.sample_rate}"], + ["pad", "0", "1.5"], # add 1.5 seconds silence at the end + ["trim", "0", "2"], # get the first 2 seconds ] data, _ = torchaudio.sox_effects.apply_effects_file(self.flist[index], effects) return data @@ -44,6 +44,7 @@ def __len__(self): class RandomPerturbationTensor(torch.utils.data.Dataset): """Apply speed purturbation to (synthetic) Tensor data""" + def __init__(self, signals: List[Tuple[torch.Tensor, int]], sample_rate: int): super().__init__() self.signals = signals @@ -53,11 +54,11 @@ def __init__(self, signals: List[Tuple[torch.Tensor, int]], sample_rate: int): def __getitem__(self, index): speed = self.rng.uniform(0.5, 2.0) effects = [ - ['gain', '-n', '-10'], - ['speed', f'{speed:.5f}'], # duration of data is 0.5 ~ 2.0 seconds. - ['rate', f'{self.sample_rate}'], - ['pad', '0', '1.5'], # add 1.5 seconds silence at the end - ['trim', '0', '2'], # get the first 2 seconds + ["gain", "-n", "-10"], + ["speed", f"{speed:.5f}"], # duration of data is 0.5 ~ 2.0 seconds. + ["rate", f"{self.sample_rate}"], + ["pad", "0", "1.5"], # add 1.5 seconds silence at the end + ["trim", "0", "2"], # get the first 2 seconds ] tensor, sample_rate = self.signals[index] data, _ = torchaudio.sox_effects.apply_effects_tensor(tensor, sample_rate, effects) @@ -74,11 +75,9 @@ def init_random_seed(worker_id): @skipIfNoSox @skipIf( - platform.system() == 'Darwin' and - sys.version_info.major == 3 and - sys.version_info.minor in [6, 7], - 'This test is known to get stuck for macOS with Python < 3.8. ' - 'See https://github.com/pytorch/pytorch/issues/46409' + platform.system() == "Darwin" and sys.version_info.major == 3 and sys.version_info.minor in [6, 7], + "This test is known to get stuck for macOS with Python < 3.8. " + "See https://github.com/pytorch/pytorch/issues/46409", ) class TestSoxEffectsDataset(TempDirMixin, PytorchTestCase): """Test `apply_effects_file` in multi-process dataloader setting""" @@ -87,9 +86,9 @@ def _generate_dataset(self, num_samples=128): flist = [] for i in range(num_samples): sample_rate = np.random.choice([8000, 16000, 44100]) - dtype = np.random.choice(['float32', 'int32', 'int16', 'uint8']) + dtype = np.random.choice(["float32", "int32", "int16", "uint8"]) data = get_whitenoise(n_channels=2, sample_rate=sample_rate, duration=1, dtype=dtype) - path = self.get_temp_path(f'{i:03d}_{dtype}_{sample_rate}.wav') + path = self.get_temp_path(f"{i:03d}_{dtype}_{sample_rate}.wav") save_wav(path, data, sample_rate) flist.append(path) return flist @@ -99,7 +98,9 @@ def test_apply_effects_file(self): flist = self._generate_dataset() dataset = RandomPerturbationFile(flist, sample_rate) loader = torch.utils.data.DataLoader( - dataset, batch_size=32, num_workers=16, + dataset, + batch_size=32, + num_workers=16, worker_init_fn=init_random_seed, ) for batch in loader: @@ -109,8 +110,7 @@ def _generate_signals(self, num_samples=128): signals = [] for _ in range(num_samples): sample_rate = np.random.choice([8000, 16000, 44100]) - data = get_whitenoise( - n_channels=2, sample_rate=sample_rate, duration=1, dtype='float32') + data = get_whitenoise(n_channels=2, sample_rate=sample_rate, duration=1, dtype="float32") signals.append((data, sample_rate)) return signals @@ -119,7 +119,9 @@ def test_apply_effects_tensor(self): signals = self._generate_signals() dataset = RandomPerturbationTensor(signals, sample_rate) loader = torch.utils.data.DataLoader( - dataset, batch_size=32, num_workers=16, + dataset, + batch_size=32, + num_workers=16, worker_init_fn=init_random_seed, ) for batch in loader: @@ -129,8 +131,8 @@ def test_apply_effects_tensor(self): def speed(path): wav, sample_rate = torchaudio.backend.sox_io_backend.load(path) effects = [ - ['speed', '1.03756523535464655'], - ['rate', f'{sample_rate}'], + ["speed", "1.03756523535464655"], + ["rate", f"{sample_rate}"], ] return torchaudio.sox_effects.apply_effects_tensor(wav, sample_rate, effects)[0] @@ -143,12 +145,12 @@ def setUp(self): sample_rate = 16000 self.flist = [] for i in range(10): - path = self.get_temp_path(f'{i}.wav') - data = get_whitenoise(n_channels=1, sample_rate=sample_rate, duration=1, dtype='float') + path = self.get_temp_path(f"{i}.wav") + data = get_whitenoise(n_channels=1, sample_rate=sample_rate, duration=1, dtype="float") save_wav(path, data, sample_rate) self.flist.append(path) - @skipIf(os.environ.get("CI") == 'true', "This test now hangs in CI") + @skipIf(os.environ.get("CI") == "true", "This test now hangs in CI") def test_executor(self): """Test that apply_effects_tensor with speed + rate does not crush diff --git a/test/torchaudio_unittest/sox_effect/smoke_test.py b/test/torchaudio_unittest/sox_effect/smoke_test.py index 70a6a346ea..27919b2f67 100644 --- a/test/torchaudio_unittest/sox_effect/smoke_test.py +++ b/test/torchaudio_unittest/sox_effect/smoke_test.py @@ -1,6 +1,5 @@ -from torchaudio import sox_effects from parameterized import parameterized - +from torchaudio import sox_effects from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -9,6 +8,7 @@ get_sinusoid, save_wav, ) + from .common import ( load_params, ) @@ -24,18 +24,17 @@ class SmokeTest(TempDirMixin, TorchaudioTestCase): This test suite should be able to run without any additional tools (such as sox command), however without such tools, the correctness of each function cannot be verified. """ + @parameterized.expand( load_params("sox_effect_test_args.jsonl"), name_func=lambda f, i, p: f'{f.__name__}_{i}_{p.args[0]["effects"][0][0]}', ) def test_apply_effects_tensor(self, args): """`apply_effects_tensor` should not crash""" - effects = args['effects'] + effects = args["effects"] num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) - original = get_sinusoid( - frequency=800, sample_rate=input_sr, - n_channels=num_channels, dtype='float32') + original = get_sinusoid(frequency=800, sample_rate=input_sr, n_channels=num_channels, dtype="float32") _found, _sr = sox_effects.apply_effects_tensor(original, input_sr, effects) @parameterized.expand( @@ -44,18 +43,19 @@ def test_apply_effects_tensor(self, args): ) def test_apply_effects_file(self, args): """`apply_effects_file` should return identical data as sox command""" - dtype = 'int32' + dtype = "int32" channels_first = True - effects = args['effects'] + effects = args["effects"] num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) - input_path = self.get_temp_path('input.wav') + input_path = self.get_temp_path("input.wav") data = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(input_path, data, input_sr, channels_first=channels_first) _found, _sr = sox_effects.apply_effects_file( - input_path, effects, normalize=False, channels_first=channels_first) + input_path, effects, normalize=False, channels_first=channels_first + ) @parameterized.expand( load_params("sox_effect_test_args.jsonl"), @@ -63,16 +63,17 @@ def test_apply_effects_file(self, args): ) def test_apply_effects_fileobj(self, args): """`apply_effects_file` should return identical data as sox command""" - dtype = 'int32' + dtype = "int32" channels_first = True - effects = args['effects'] + effects = args["effects"] num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) - input_path = self.get_temp_path('input.wav') + input_path = self.get_temp_path("input.wav") data = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(input_path, data, input_sr, channels_first=channels_first) - with open(input_path, 'rb') as fileobj: + with open(input_path, "rb") as fileobj: _found, _sr = sox_effects.apply_effects_file( - fileobj, effects, normalize=False, channels_first=channels_first) + fileobj, effects, normalize=False, channels_first=channels_first + ) diff --git a/test/torchaudio_unittest/sox_effect/sox_effect_test.py b/test/torchaudio_unittest/sox_effect/sox_effect_test.py index ca93e7d41b..1ea5636646 100644 --- a/test/torchaudio_unittest/sox_effect/sox_effect_test.py +++ b/test/torchaudio_unittest/sox_effect/sox_effect_test.py @@ -1,12 +1,11 @@ import io import itertools -from pathlib import Path import tarfile +from pathlib import Path from parameterized import parameterized from torchaudio import sox_effects from torchaudio._internal import module_utils as _mod_utils - from torchaudio_unittest.common_utils import ( TempDirMixin, HttpServerMixin, @@ -21,6 +20,7 @@ load_wav, sox_utils, ) + from .common import ( load_params, name_func, @@ -42,18 +42,16 @@ def test_init(self): @skipIfNoSox class TestSoxEffectsTensor(TempDirMixin, PytorchTestCase): """Test suite for `apply_effects_tensor` function""" - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2, 4, 8], - [True, False] - )), name_func=name_func) + + @parameterized.expand( + list(itertools.product(["float32", "int32", "int16", "uint8"], [8000, 16000], [1, 2, 4, 8], [True, False])), + name_func=name_func, + ) def test_apply_no_effect(self, dtype, sample_rate, num_channels, channels_first): """`apply_effects_tensor` without effects should return identical data as input""" original = get_wav_data(dtype, num_channels, channels_first=channels_first) expected = original.clone() - found, output_sample_rate = sox_effects.apply_effects_tensor( - expected, sample_rate, [], channels_first) + found, output_sample_rate = sox_effects.apply_effects_tensor(expected, sample_rate, [], channels_first) assert output_sample_rate == sample_rate # SoxEffect should not alter the input Tensor object @@ -69,20 +67,17 @@ def test_apply_no_effect(self, dtype, sample_rate, num_channels, channels_first) ) def test_apply_effects(self, args): """`apply_effects_tensor` should return identical data as sox command""" - effects = args['effects'] + effects = args["effects"] num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) output_sr = args.get("output_sample_rate") - input_path = self.get_temp_path('input.wav') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.wav") + reference_path = self.get_temp_path("reference.wav") - original = get_sinusoid( - frequency=800, sample_rate=input_sr, - n_channels=num_channels, dtype='float32') + original = get_sinusoid(frequency=800, sample_rate=input_sr, n_channels=num_channels, dtype="float32") save_wav(input_path, original, input_sr) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_sample_rate=output_sr) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_sample_rate=output_sr) expected, expected_sr = load_wav(reference_path) found, sr = sox_effects.apply_effects_tensor(original, input_sr, effects) @@ -94,20 +89,27 @@ def test_apply_effects(self, args): @skipIfNoSox class TestSoxEffectsFile(TempDirMixin, PytorchTestCase): """Test suite for `apply_effects_file` function""" - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2, 4, 8], - [False, True], - )), name_func=name_func) + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2, 4, 8], + [False, True], + ) + ), + name_func=name_func, + ) def test_apply_no_effect(self, dtype, sample_rate, num_channels, channels_first): """`apply_effects_file` without effects should return identical data as input""" - path = self.get_temp_path('input.wav') + path = self.get_temp_path("input.wav") expected = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(path, expected, sample_rate, channels_first=channels_first) found, output_sample_rate = sox_effects.apply_effects_file( - path, [], normalize=False, channels_first=channels_first) + path, [], normalize=False, channels_first=channels_first + ) assert output_sample_rate == sample_rate self.assertEqual(expected, found) @@ -118,46 +120,44 @@ def test_apply_no_effect(self, dtype, sample_rate, num_channels, channels_first) ) def test_apply_effects_str(self, args): """`apply_effects_file` should return identical data as sox command""" - dtype = 'int32' + dtype = "int32" channels_first = True - effects = args['effects'] + effects = args["effects"] num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) output_sr = args.get("output_sample_rate") - input_path = self.get_temp_path('input.wav') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.wav") + reference_path = self.get_temp_path("reference.wav") data = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(input_path, data, input_sr, channels_first=channels_first) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_sample_rate=output_sr) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_sample_rate=output_sr) expected, expected_sr = load_wav(reference_path) - found, sr = sox_effects.apply_effects_file( - input_path, effects, normalize=False, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(input_path, effects, normalize=False, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) def test_apply_effects_path(self): """`apply_effects_file` should return identical data as sox command when file path is given as a Path Object""" - dtype = 'int32' + dtype = "int32" channels_first = True effects = [["hilbert"]] num_channels = 2 input_sr = 8000 output_sr = 8000 - input_path = self.get_temp_path('input.wav') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.wav") + reference_path = self.get_temp_path("reference.wav") data = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(input_path, data, input_sr, channels_first=channels_first) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_sample_rate=output_sr) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_sample_rate=output_sr) expected, expected_sr = load_wav(reference_path) found, sr = sox_effects.apply_effects_file( - Path(input_path), effects, normalize=False, channels_first=channels_first) + Path(input_path), effects, normalize=False, channels_first=channels_first + ) assert sr == expected_sr self.assertEqual(found, expected) @@ -166,91 +166,108 @@ def test_apply_effects_path(self): @skipIfNoSox class TestFileFormats(TempDirMixin, PytorchTestCase): """`apply_effects_file` gives the same result as sox on various file formats""" - @parameterized.expand(list(itertools.product( - ['float32', 'int32', 'int16', 'uint8'], - [8000, 16000], - [1, 2], - )), name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}') + + @parameterized.expand( + list( + itertools.product( + ["float32", "int32", "int16", "uint8"], + [8000, 16000], + [1, 2], + ) + ), + name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}', + ) def test_wav(self, dtype, sample_rate, num_channels): """`apply_effects_file` works on various wav format""" channels_first = True - effects = [['band', '300', '10']] + effects = [["band", "300", "10"]] - input_path = self.get_temp_path('input.wav') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.wav") + reference_path = self.get_temp_path("reference.wav") data = get_wav_data(dtype, num_channels, channels_first=channels_first) save_wav(input_path, data, sample_rate, channels_first=channels_first) sox_utils.run_sox_effect(input_path, reference_path, effects) expected, expected_sr = load_wav(reference_path) - found, sr = sox_effects.apply_effects_file( - input_path, effects, normalize=False, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(input_path, effects, normalize=False, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - )), name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}') + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + ) + ), + name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}', + ) def test_mp3(self, sample_rate, num_channels): """`apply_effects_file` works on various mp3 format""" channels_first = True - effects = [['band', '300', '10']] + effects = [["band", "300", "10"]] - input_path = self.get_temp_path('input.mp3') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.mp3") + reference_path = self.get_temp_path("reference.wav") sox_utils.gen_audio_file(input_path, sample_rate, num_channels) sox_utils.run_sox_effect(input_path, reference_path, effects) expected, expected_sr = load_wav(reference_path) - found, sr = sox_effects.apply_effects_file( - input_path, effects, channels_first=channels_first) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(input_path, effects, channels_first=channels_first) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected, atol=1e-4, rtol=1e-8) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - )), name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}') + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + ) + ), + name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}', + ) def test_flac(self, sample_rate, num_channels): """`apply_effects_file` works on various flac format""" channels_first = True - effects = [['band', '300', '10']] + effects = [["band", "300", "10"]] - input_path = self.get_temp_path('input.flac') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.flac") + reference_path = self.get_temp_path("reference.wav") sox_utils.gen_audio_file(input_path, sample_rate, num_channels) sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) - found, sr = sox_effects.apply_effects_file( - input_path, effects, channels_first=channels_first) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(input_path, effects, channels_first=channels_first) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) - @parameterized.expand(list(itertools.product( - [8000, 16000], - [1, 2], - )), name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}') + @parameterized.expand( + list( + itertools.product( + [8000, 16000], + [1, 2], + ) + ), + name_func=lambda f, _, p: f'{f.__name__}_{"_".join(str(arg) for arg in p.args)}', + ) def test_vorbis(self, sample_rate, num_channels): """`apply_effects_file` works on various vorbis format""" channels_first = True - effects = [['band', '300', '10']] + effects = [["band", "300", "10"]] - input_path = self.get_temp_path('input.vorbis') - reference_path = self.get_temp_path('reference.wav') + input_path = self.get_temp_path("input.vorbis") + reference_path = self.get_temp_path("reference.wav") sox_utils.gen_audio_file(input_path, sample_rate, num_channels) sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) - found, sr = sox_effects.apply_effects_file( - input_path, effects, channels_first=channels_first) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(input_path, effects, channels_first=channels_first) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) @@ -268,156 +285,152 @@ def test_mp3(self): The file was generated with the following command ffmpeg -f lavfi -i "sine=frequency=1000:duration=5" -ar 16000 -f mp3 test_noext """ - effects = [['band', '300', '10']] + effects = [["band", "300", "10"]] path = get_asset_path("mp3_without_ext") _, sr = sox_effects.apply_effects_file(path, effects, format="mp3") assert sr == 16000 -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoSox class TestFileObject(TempDirMixin, PytorchTestCase): - @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), - ]) + @parameterized.expand( + [ + ("wav", None), + ("mp3", 128), + ("mp3", 320), + ("flac", 0), + ("flac", 5), + ("flac", 8), + ("vorbis", -1), + ("vorbis", 10), + ("amb", None), + ] + ) def test_fileobj(self, ext, compression): """Applying effects via file object works""" sample_rate = 16000 channels_first = True - effects = [['band', '300', '10']] - format_ = ext if ext in ['mp3'] else None - input_path = self.get_temp_path(f'input.{ext}') - reference_path = self.get_temp_path('reference.wav') - - sox_utils.gen_audio_file( - input_path, sample_rate, num_channels=2, compression=compression) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_bitdepth=32) + effects = [["band", "300", "10"]] + format_ = ext if ext in ["mp3"] else None + input_path = self.get_temp_path(f"input.{ext}") + reference_path = self.get_temp_path("reference.wav") + + sox_utils.gen_audio_file(input_path, sample_rate, num_channels=2, compression=compression) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) - with open(input_path, 'rb') as fileobj: - found, sr = sox_effects.apply_effects_file( - fileobj, effects, channels_first=channels_first, format=format_) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + with open(input_path, "rb") as fileobj: + found, sr = sox_effects.apply_effects_file(fileobj, effects, channels_first=channels_first, format=format_) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) - @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), - ]) + @parameterized.expand( + [ + ("wav", None), + ("mp3", 128), + ("mp3", 320), + ("flac", 0), + ("flac", 5), + ("flac", 8), + ("vorbis", -1), + ("vorbis", 10), + ("amb", None), + ] + ) def test_bytesio(self, ext, compression): """Applying effects via BytesIO object works""" sample_rate = 16000 channels_first = True - effects = [['band', '300', '10']] - format_ = ext if ext in ['mp3'] else None - input_path = self.get_temp_path(f'input.{ext}') - reference_path = self.get_temp_path('reference.wav') - - sox_utils.gen_audio_file( - input_path, sample_rate, num_channels=2, compression=compression) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_bitdepth=32) + effects = [["band", "300", "10"]] + format_ = ext if ext in ["mp3"] else None + input_path = self.get_temp_path(f"input.{ext}") + reference_path = self.get_temp_path("reference.wav") + + sox_utils.gen_audio_file(input_path, sample_rate, num_channels=2, compression=compression) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) - with open(input_path, 'rb') as file_: + with open(input_path, "rb") as file_: fileobj = io.BytesIO(file_.read()) - found, sr = sox_effects.apply_effects_file( - fileobj, effects, channels_first=channels_first, format=format_) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(fileobj, effects, channels_first=channels_first, format=format_) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) - @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), - ]) + @parameterized.expand( + [ + ("wav", None), + ("mp3", 128), + ("mp3", 320), + ("flac", 0), + ("flac", 5), + ("flac", 8), + ("vorbis", -1), + ("vorbis", 10), + ("amb", None), + ] + ) def test_tarfile(self, ext, compression): """Applying effects to compressed audio via file-like file works""" sample_rate = 16000 channels_first = True - effects = [['band', '300', '10']] - format_ = ext if ext in ['mp3'] else None - audio_file = f'input.{ext}' + effects = [["band", "300", "10"]] + format_ = ext if ext in ["mp3"] else None + audio_file = f"input.{ext}" input_path = self.get_temp_path(audio_file) - reference_path = self.get_temp_path('reference.wav') - archive_path = self.get_temp_path('archive.tar.gz') + reference_path = self.get_temp_path("reference.wav") + archive_path = self.get_temp_path("archive.tar.gz") - sox_utils.gen_audio_file( - input_path, sample_rate, num_channels=2, compression=compression) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_bitdepth=32) + sox_utils.gen_audio_file(input_path, sample_rate, num_channels=2, compression=compression) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) - with tarfile.TarFile(archive_path, 'w') as tarobj: + with tarfile.TarFile(archive_path, "w") as tarobj: tarobj.add(input_path, arcname=audio_file) - with tarfile.TarFile(archive_path, 'r') as tarobj: + with tarfile.TarFile(archive_path, "r") as tarobj: fileobj = tarobj.extractfile(audio_file) - found, sr = sox_effects.apply_effects_file( - fileobj, effects, channels_first=channels_first, format=format_) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(fileobj, effects, channels_first=channels_first, format=format_) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") @skipIfNoModule("requests") class TestFileObjectHttp(HttpServerMixin, PytorchTestCase): - @parameterized.expand([ - ('wav', None), - ('mp3', 128), - ('mp3', 320), - ('flac', 0), - ('flac', 5), - ('flac', 8), - ('vorbis', -1), - ('vorbis', 10), - ('amb', None), - ]) + @parameterized.expand( + [ + ("wav", None), + ("mp3", 128), + ("mp3", 320), + ("flac", 0), + ("flac", 5), + ("flac", 8), + ("vorbis", -1), + ("vorbis", 10), + ("amb", None), + ] + ) def test_requests(self, ext, compression): sample_rate = 16000 channels_first = True - effects = [['band', '300', '10']] - format_ = ext if ext in ['mp3'] else None - audio_file = f'input.{ext}' + effects = [["band", "300", "10"]] + format_ = ext if ext in ["mp3"] else None + audio_file = f"input.{ext}" input_path = self.get_temp_path(audio_file) - reference_path = self.get_temp_path('reference.wav') + reference_path = self.get_temp_path("reference.wav") - sox_utils.gen_audio_file( - input_path, sample_rate, num_channels=2, compression=compression) - sox_utils.run_sox_effect( - input_path, reference_path, effects, output_bitdepth=32) + sox_utils.gen_audio_file(input_path, sample_rate, num_channels=2, compression=compression) + sox_utils.run_sox_effect(input_path, reference_path, effects, output_bitdepth=32) expected, expected_sr = load_wav(reference_path) url = self.get_url(audio_file) with requests.get(url, stream=True) as resp: - found, sr = sox_effects.apply_effects_file( - resp.raw, effects, channels_first=channels_first, format=format_) - save_wav(self.get_temp_path('result.wav'), found, sr, channels_first=channels_first) + found, sr = sox_effects.apply_effects_file(resp.raw, effects, channels_first=channels_first, format=format_) + save_wav(self.get_temp_path("result.wav"), found, sr, channels_first=channels_first) assert sr == expected_sr self.assertEqual(found, expected) diff --git a/test/torchaudio_unittest/sox_effect/torchscript_test.py b/test/torchaudio_unittest/sox_effect/torchscript_test.py index 4bc81c2b59..6a88d3df6d 100644 --- a/test/torchaudio_unittest/sox_effect/torchscript_test.py +++ b/test/torchaudio_unittest/sox_effect/torchscript_test.py @@ -1,9 +1,8 @@ from typing import List import torch -from torchaudio import sox_effects from parameterized import parameterized - +from torchaudio import sox_effects from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -12,6 +11,7 @@ save_wav, torch_script, ) + from .common import ( load_params, ) @@ -27,8 +27,7 @@ def __init__(self, effects: List[List[str]], sample_rate: int, channels_first: b self.channels_first = channels_first def forward(self, tensor: torch.Tensor): - return sox_effects.apply_effects_tensor( - tensor, self.sample_rate, self.effects, self.channels_first) + return sox_effects.apply_effects_tensor(tensor, self.sample_rate, self.effects, self.channels_first) class SoxEffectFileTransform(torch.nn.Module): @@ -51,7 +50,7 @@ class TestTorchScript(TempDirMixin, TorchaudioTestCase): name_func=lambda f, i, p: f'{f.__name__}_{i}_{p.args[0]["effects"][0][0]}', ) def test_apply_effects_tensor(self, args): - effects = args['effects'] + effects = args["effects"] channels_first = True num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) @@ -61,11 +60,10 @@ def test_apply_effects_tensor(self, args): trans = torch_script(trans) wav = get_sinusoid( - frequency=800, sample_rate=input_sr, - n_channels=num_channels, dtype='float32', channels_first=channels_first) + frequency=800, sample_rate=input_sr, n_channels=num_channels, dtype="float32", channels_first=channels_first + ) found, sr_found = trans(wav) - expected, sr_expected = sox_effects.apply_effects_tensor( - wav, input_sr, effects, channels_first) + expected, sr_expected = sox_effects.apply_effects_tensor(wav, input_sr, effects, channels_first) assert sr_found == sr_expected self.assertEqual(expected, found) @@ -75,7 +73,7 @@ def test_apply_effects_tensor(self, args): name_func=lambda f, i, p: f'{f.__name__}_{i}_{p.args[0]["effects"][0][0]}', ) def test_apply_effects_file(self, args): - effects = args['effects'] + effects = args["effects"] channels_first = True num_channels = args.get("num_channels", 2) input_sr = args.get("input_sample_rate", 8000) @@ -83,10 +81,10 @@ def test_apply_effects_file(self, args): trans = SoxEffectFileTransform(effects, channels_first) trans = torch_script(trans) - path = self.get_temp_path('input.wav') + path = self.get_temp_path("input.wav") wav = get_sinusoid( - frequency=800, sample_rate=input_sr, - n_channels=num_channels, dtype='float32', channels_first=channels_first) + frequency=800, sample_rate=input_sr, n_channels=num_channels, dtype="float32", channels_first=channels_first + ) save_wav(path, wav, sample_rate=input_sr, channels_first=channels_first) found, sr_found = trans(path) diff --git a/test/torchaudio_unittest/transforms/autograd_cpu_test.py b/test/torchaudio_unittest/transforms/autograd_cpu_test.py index f9d4aa1994..d5878a7455 100644 --- a/test/torchaudio_unittest/transforms/autograd_cpu_test.py +++ b/test/torchaudio_unittest/transforms/autograd_cpu_test.py @@ -1,10 +1,11 @@ from torchaudio_unittest.common_utils import PytorchTestCase + from .autograd_test_impl import AutogradTestMixin, AutogradTestFloat32 class AutogradCPUTest(AutogradTestMixin, PytorchTestCase): - device = 'cpu' + device = "cpu" class AutogradRNNTCPUTest(AutogradTestFloat32, PytorchTestCase): - device = 'cpu' + device = "cpu" diff --git a/test/torchaudio_unittest/transforms/autograd_cuda_test.py b/test/torchaudio_unittest/transforms/autograd_cuda_test.py index 7565f88540..8c90bfe74f 100644 --- a/test/torchaudio_unittest/transforms/autograd_cuda_test.py +++ b/test/torchaudio_unittest/transforms/autograd_cuda_test.py @@ -2,14 +2,15 @@ PytorchTestCase, skipIfNoCuda, ) + from .autograd_test_impl import AutogradTestMixin, AutogradTestFloat32 @skipIfNoCuda class AutogradCUDATest(AutogradTestMixin, PytorchTestCase): - device = 'cuda' + device = "cuda" @skipIfNoCuda class AutogradRNNTCUDATest(AutogradTestFloat32, PytorchTestCase): - device = 'cuda' + device = "cuda" diff --git a/test/torchaudio_unittest/transforms/autograd_test_impl.py b/test/torchaudio_unittest/transforms/autograd_test_impl.py index bde7af8aee..011d63f877 100644 --- a/test/torchaudio_unittest/transforms/autograd_test_impl.py +++ b/test/torchaudio_unittest/transforms/autograd_test_impl.py @@ -1,11 +1,10 @@ -from typing import List import unittest +from typing import List -from parameterized import parameterized import torch -from torch.autograd import gradcheck, gradgradcheck import torchaudio.transforms as T - +from parameterized import parameterized +from torch.autograd import gradcheck, gradgradcheck from torchaudio_unittest.common_utils import ( TestBaseMixin, get_whitenoise, @@ -17,6 +16,7 @@ class _DeterministicWrapper(torch.nn.Module): """Helper transform wrapper to make the given transform deterministic""" + def __init__(self, transform, seed=0): super().__init__() self.seed = seed @@ -29,11 +29,11 @@ def forward(self, input: torch.Tensor): class AutogradTestMixin(TestBaseMixin): def assert_grad( - self, - transform: torch.nn.Module, - inputs: List[torch.Tensor], - *, - nondet_tol: float = 0.0, + self, + transform: torch.nn.Module, + inputs: List[torch.Tensor], + *, + nondet_tol: float = 0.0, ): transform = transform.to(dtype=torch.float64, device=self.device) @@ -42,32 +42,32 @@ def assert_grad( inputs_ = [] for i in inputs: if torch.is_tensor(i): - i = i.to( - dtype=torch.cdouble if i.is_complex() else torch.double, - device=self.device) + i = i.to(dtype=torch.cdouble if i.is_complex() else torch.double, device=self.device) i.requires_grad = True inputs_.append(i) assert gradcheck(transform, inputs_) assert gradgradcheck(transform, inputs_, nondet_tol=nondet_tol) - @parameterized.expand([ - ({'pad': 0, 'normalized': False, 'power': None, 'return_complex': True}, ), - ({'pad': 3, 'normalized': False, 'power': None, 'return_complex': True}, ), - ({'pad': 0, 'normalized': True, 'power': None, 'return_complex': True}, ), - ({'pad': 3, 'normalized': True, 'power': None, 'return_complex': True}, ), - ({'pad': 0, 'normalized': False, 'power': None}, ), - ({'pad': 3, 'normalized': False, 'power': None}, ), - ({'pad': 0, 'normalized': True, 'power': None}, ), - ({'pad': 3, 'normalized': True, 'power': None}, ), - ({'pad': 0, 'normalized': False, 'power': 1.0}, ), - ({'pad': 3, 'normalized': False, 'power': 1.0}, ), - ({'pad': 0, 'normalized': True, 'power': 1.0}, ), - ({'pad': 3, 'normalized': True, 'power': 1.0}, ), - ({'pad': 0, 'normalized': False, 'power': 2.0}, ), - ({'pad': 3, 'normalized': False, 'power': 2.0}, ), - ({'pad': 0, 'normalized': True, 'power': 2.0}, ), - ({'pad': 3, 'normalized': True, 'power': 2.0}, ), - ]) + @parameterized.expand( + [ + ({"pad": 0, "normalized": False, "power": None, "return_complex": True},), + ({"pad": 3, "normalized": False, "power": None, "return_complex": True},), + ({"pad": 0, "normalized": True, "power": None, "return_complex": True},), + ({"pad": 3, "normalized": True, "power": None, "return_complex": True},), + ({"pad": 0, "normalized": False, "power": None},), + ({"pad": 3, "normalized": False, "power": None},), + ({"pad": 0, "normalized": True, "power": None},), + ({"pad": 3, "normalized": True, "power": None},), + ({"pad": 0, "normalized": False, "power": 1.0},), + ({"pad": 3, "normalized": False, "power": 1.0},), + ({"pad": 0, "normalized": True, "power": 1.0},), + ({"pad": 3, "normalized": True, "power": 1.0},), + ({"pad": 0, "normalized": False, "power": 2.0},), + ({"pad": 3, "normalized": False, "power": 2.0},), + ({"pad": 0, "normalized": True, "power": 2.0},), + ({"pad": 3, "normalized": True, "power": 2.0},), + ] + ) def test_spectrogram(self, kwargs): # replication_pad1d_backward_cuda is not deteministic and # gives very small (~2.7756e-17) difference. @@ -105,21 +105,20 @@ def test_griffinlim(self, momentum, rand_init): power = 1 n_iter = 2 - spec = get_spectrogram( - get_whitenoise(sample_rate=8000, duration=0.01, n_channels=2), - n_fft=n_fft, power=power) + spec = get_spectrogram(get_whitenoise(sample_rate=8000, duration=0.01, n_channels=2), n_fft=n_fft, power=power) transform = _DeterministicWrapper( - T.GriffinLim(n_fft=n_fft, n_iter=n_iter, momentum=momentum, rand_init=rand_init, power=power)) + T.GriffinLim(n_fft=n_fft, n_iter=n_iter, momentum=momentum, rand_init=rand_init, power=power) + ) self.assert_grad(transform, [spec]) - @parameterized.expand([(False, ), (True, )]) + @parameterized.expand([(False,), (True,)]) def test_mfcc(self, log_mels): sample_rate = 8000 transform = T.MFCC(sample_rate=sample_rate, log_mels=log_mels) waveform = get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2) self.assert_grad(transform, [waveform]) - @parameterized.expand([(False, ), (True, )]) + @parameterized.expand([(False,), (True,)]) def test_lfcc(self, log_lf): sample_rate = 8000 transform = T.LFCC(sample_rate=sample_rate, log_lf=log_lf) @@ -137,7 +136,7 @@ def test_resample(self, orig_freq, new_freq): waveform = get_whitenoise(sample_rate=8000, duration=0.05, n_channels=2) self.assert_grad(transform, [waveform]) - @parameterized.expand([("linear", ), ("exponential", ), ("logarithmic", ), ("quarter_sine", ), ("half_sine", )]) + @parameterized.expand([("linear",), ("exponential",), ("logarithmic",), ("quarter_sine",), ("half_sine",)]) def test_fade(self, fade_shape): transform = T.Fade(fade_shape=fade_shape) waveform = get_whitenoise(sample_rate=8000, duration=0.05, n_channels=2) @@ -148,8 +147,8 @@ def test_masking(self, masking_transform): sample_rate = 8000 n_fft = 400 spectrogram = get_spectrogram( - get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2), - n_fft=n_fft, power=1) + get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2), n_fft=n_fft, power=1 + ) deterministic_transform = _DeterministicWrapper(masking_transform(400)) self.assert_grad(deterministic_transform, [spectrogram]) @@ -157,9 +156,10 @@ def test_masking(self, masking_transform): def test_masking_iid(self, masking_transform): sample_rate = 8000 n_fft = 400 - specs = [get_spectrogram( - get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2, seed=i), - n_fft=n_fft, power=1) + specs = [ + get_spectrogram( + get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2, seed=i), n_fft=n_fft, power=1 + ) for i in range(3) ] @@ -186,8 +186,8 @@ def test_melscale(self): n_mels = n_fft // 2 + 1 transform = T.MelScale(sample_rate=sample_rate, n_mels=n_mels) spec = get_spectrogram( - get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2), - n_fft=n_fft, power=1) + get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2), n_fft=n_fft, power=1 + ) self.assert_grad(transform, [spec]) @parameterized.expand([(1.5, "amplitude"), (2, "power"), (10, "db")]) @@ -197,18 +197,18 @@ def test_vol(self, gain, gain_type): waveform = get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2) self.assert_grad(transform, [waveform]) - @parameterized.expand([ - ({'cmn_window': 100, 'min_cmn_window': 50, 'center': False, 'norm_vars': False}, ), - ({'cmn_window': 100, 'min_cmn_window': 50, 'center': True, 'norm_vars': False}, ), - ({'cmn_window': 100, 'min_cmn_window': 50, 'center': False, 'norm_vars': True}, ), - ({'cmn_window': 100, 'min_cmn_window': 50, 'center': True, 'norm_vars': True}, ), - ]) + @parameterized.expand( + [ + ({"cmn_window": 100, "min_cmn_window": 50, "center": False, "norm_vars": False},), + ({"cmn_window": 100, "min_cmn_window": 50, "center": True, "norm_vars": False},), + ({"cmn_window": 100, "min_cmn_window": 50, "center": False, "norm_vars": True},), + ({"cmn_window": 100, "min_cmn_window": 50, "center": True, "norm_vars": True},), + ] + ) def test_sliding_window_cmn(self, kwargs): n_fft = 10 power = 1 - spec = get_spectrogram( - get_whitenoise(sample_rate=200, duration=0.05, n_channels=2), - n_fft=n_fft, power=power) + spec = get_spectrogram(get_whitenoise(sample_rate=200, duration=0.05, n_channels=2), n_fft=n_fft, power=power) spec_reshaped = spec.transpose(-1, -2) transform = T.SlidingWindowCmn(**kwargs) @@ -260,10 +260,12 @@ def test_psd(self): spectrogram = get_spectrogram(waveform, n_fft=400) self.assert_grad(transform, [spectrogram]) - @parameterized.expand([ - [True], - [False], - ]) + @parameterized.expand( + [ + [True], + [False], + ] + ) def test_psd_with_mask(self, multi_mask): transform = T.PSD(multi_mask=multi_mask) waveform = get_whitenoise(sample_rate=8000, duration=0.05, n_channels=2) @@ -275,12 +277,14 @@ def test_psd_with_mask(self, multi_mask): self.assert_grad(transform, [spectrogram, mask]) - @parameterized.expand([ - "ref_channel", - # stv_power and stv_evd test time too long, comment for now - # "stv_power", - # "stv_evd", - ]) + @parameterized.expand( + [ + "ref_channel", + # stv_power and stv_evd test time too long, comment for now + # "stv_power", + # "stv_evd", + ] + ) def test_mvdr(self, solution): transform = T.MVDR(solution=solution) waveform = get_whitenoise(sample_rate=8000, duration=0.05, n_channels=2) @@ -292,9 +296,9 @@ def test_mvdr(self, solution): class AutogradTestFloat32(TestBaseMixin): def assert_grad( - self, - transform: torch.nn.Module, - inputs: List[torch.Tensor], + self, + transform: torch.nn.Module, + inputs: List[torch.Tensor], ): inputs_ = [] for i in inputs: @@ -302,13 +306,15 @@ def assert_grad( i = i.to(dtype=torch.float32, device=self.device) inputs_.append(i) # gradcheck with float32 requires higher atol and epsilon - assert gradcheck(transform, inputs, eps=1e-3, atol=1e-3, nondet_tol=0.) + assert gradcheck(transform, inputs, eps=1e-3, atol=1e-3, nondet_tol=0.0) - @parameterized.expand([ - (rnnt_utils.get_B1_T10_U3_D4_data, ), - (rnnt_utils.get_B2_T4_U3_D3_data, ), - (rnnt_utils.get_B1_T2_U3_D5_data, ), - ]) + @parameterized.expand( + [ + (rnnt_utils.get_B1_T10_U3_D4_data,), + (rnnt_utils.get_B2_T4_U3_D3_data,), + (rnnt_utils.get_B1_T2_U3_D5_data,), + ] + ) def test_rnnt_loss(self, data_func): def get_data(data_func, device): data = data_func() diff --git a/test/torchaudio_unittest/transforms/batch_consistency_test.py b/test/torchaudio_unittest/transforms/batch_consistency_test.py index 3e36b1a5d3..3ae73caaae 100644 --- a/test/torchaudio_unittest/transforms/batch_consistency_test.py +++ b/test/torchaudio_unittest/transforms/batch_consistency_test.py @@ -2,25 +2,21 @@ import torch from parameterized import parameterized from torchaudio import transforms as T - from torchaudio_unittest import common_utils class TestTransforms(common_utils.TorchaudioTestCase): """Test suite for classes defined in `transforms` module""" - backend = 'default' - def assert_batch_consistency( - self, transform, batch, *args, atol=1e-8, rtol=1e-5, seed=42, - **kwargs): + backend = "default" + + def assert_batch_consistency(self, transform, batch, *args, atol=1e-8, rtol=1e-5, seed=42, **kwargs): n = batch.size(0) # Compute items separately, then batch the result torch.random.manual_seed(seed) items_input = batch.clone() - items_result = torch.stack([ - transform(items_input[i], *args, **kwargs) for i in range(n) - ]) + items_result = torch.stack([transform(items_input[i], *args, **kwargs) for i in range(n)]) # Batch the input and run torch.random.manual_seed(seed) @@ -131,11 +127,7 @@ def test_batch_TimeStretch(self): tensor = common_utils.get_whitenoise(sample_rate=8000, n_channels=batch) spec = common_utils.get_spectrogram(tensor, n_fft=num_freq) - transform = T.TimeStretch( - fixed_rate=rate, - n_freq=num_freq // 2 + 1, - hop_length=512 - ) + transform = T.TimeStretch(fixed_rate=rate, n_freq=num_freq // 2 + 1, hop_length=512) self.assert_batch_consistency(transform, spec, atol=1e-5, rtol=1e-5) @@ -197,10 +189,12 @@ def test_batch_PSD_with_mask(self): self.assertEqual(computed, expected) - @parameterized.expand([ - [True], - [False], - ]) + @parameterized.expand( + [ + [True], + [False], + ] + ) def test_MVDR(self, multi_mask): waveform = common_utils.get_whitenoise(sample_rate=8000, duration=1, n_channels=6) specgram = common_utils.get_spectrogram(waveform, n_fft=400) diff --git a/test/torchaudio_unittest/transforms/kaldi_compatibility_cpu_test.py b/test/torchaudio_unittest/transforms/kaldi_compatibility_cpu_test.py index 43be412b47..19965630d3 100644 --- a/test/torchaudio_unittest/transforms/kaldi_compatibility_cpu_test.py +++ b/test/torchaudio_unittest/transforms/kaldi_compatibility_cpu_test.py @@ -1,14 +1,14 @@ import torch - from torchaudio_unittest import common_utils + from .kaldi_compatibility_impl import Kaldi class TestKaldiFloat32(Kaldi, common_utils.PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") class TestKaldiFloat64(Kaldi, common_utils.PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/transforms/kaldi_compatibility_cuda_test.py b/test/torchaudio_unittest/transforms/kaldi_compatibility_cuda_test.py index 28adb7fce5..26b4aada14 100644 --- a/test/torchaudio_unittest/transforms/kaldi_compatibility_cuda_test.py +++ b/test/torchaudio_unittest/transforms/kaldi_compatibility_cuda_test.py @@ -1,16 +1,16 @@ import torch - from torchaudio_unittest import common_utils + from .kaldi_compatibility_impl import Kaldi @common_utils.skipIfNoCuda class TestKaldiFloat32(Kaldi, common_utils.PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") @common_utils.skipIfNoCuda class TestKaldiFloat64(Kaldi, common_utils.PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py b/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py index 9a0e6ab81d..71316c1156 100644 --- a/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py +++ b/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py @@ -1,7 +1,6 @@ """Test suites for checking numerical compatibility against Kaldi""" import torchaudio.compliance.kaldi from parameterized import parameterized - from torchaudio_unittest.common_utils import ( TestBaseMixin, TempDirMixin, @@ -21,35 +20,35 @@ def assert_equal(self, output, *, expected, rtol=None, atol=None): expected = expected.to(dtype=self.dtype, device=self.device) self.assertEqual(output, expected, rtol=rtol, atol=atol) - @parameterized.expand(load_params('kaldi_test_fbank_args.jsonl')) - @skipIfNoExec('compute-fbank-feats') + @parameterized.expand(load_params("kaldi_test_fbank_args.jsonl")) + @skipIfNoExec("compute-fbank-feats") def test_fbank(self, kwargs): """fbank should be numerically compatible with compute-fbank-feats""" - wave_file = get_asset_path('kaldi_file.wav') + wave_file = get_asset_path("kaldi_file.wav") waveform = load_wav(wave_file, normalize=False)[0].to(dtype=self.dtype, device=self.device) result = torchaudio.compliance.kaldi.fbank(waveform, **kwargs) - command = ['compute-fbank-feats'] + convert_args(**kwargs) + ['scp:-', 'ark:-'] - kaldi_result = run_kaldi(command, 'scp', wave_file) + command = ["compute-fbank-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] + kaldi_result = run_kaldi(command, "scp", wave_file) self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-8) - @parameterized.expand(load_params('kaldi_test_spectrogram_args.jsonl')) - @skipIfNoExec('compute-spectrogram-feats') + @parameterized.expand(load_params("kaldi_test_spectrogram_args.jsonl")) + @skipIfNoExec("compute-spectrogram-feats") def test_spectrogram(self, kwargs): """spectrogram should be numerically compatible with compute-spectrogram-feats""" - wave_file = get_asset_path('kaldi_file.wav') + wave_file = get_asset_path("kaldi_file.wav") waveform = load_wav(wave_file, normalize=False)[0].to(dtype=self.dtype, device=self.device) result = torchaudio.compliance.kaldi.spectrogram(waveform, **kwargs) - command = ['compute-spectrogram-feats'] + convert_args(**kwargs) + ['scp:-', 'ark:-'] - kaldi_result = run_kaldi(command, 'scp', wave_file) + command = ["compute-spectrogram-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] + kaldi_result = run_kaldi(command, "scp", wave_file) self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-8) - @parameterized.expand(load_params('kaldi_test_mfcc_args.jsonl')) - @skipIfNoExec('compute-mfcc-feats') + @parameterized.expand(load_params("kaldi_test_mfcc_args.jsonl")) + @skipIfNoExec("compute-mfcc-feats") def test_mfcc(self, kwargs): """mfcc should be numerically compatible with compute-mfcc-feats""" - wave_file = get_asset_path('kaldi_file.wav') + wave_file = get_asset_path("kaldi_file.wav") waveform = load_wav(wave_file, normalize=False)[0].to(dtype=self.dtype, device=self.device) result = torchaudio.compliance.kaldi.mfcc(waveform, **kwargs) - command = ['compute-mfcc-feats'] + convert_args(**kwargs) + ['scp:-', 'ark:-'] - kaldi_result = run_kaldi(command, 'scp', wave_file) + command = ["compute-mfcc-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] + kaldi_result = run_kaldi(command, "scp", wave_file) self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-8) diff --git a/test/torchaudio_unittest/transforms/librosa_compatibility_cpu_test.py b/test/torchaudio_unittest/transforms/librosa_compatibility_cpu_test.py index 300f4a3f71..c39bc766a6 100644 --- a/test/torchaudio_unittest/transforms/librosa_compatibility_cpu_test.py +++ b/test/torchaudio_unittest/transforms/librosa_compatibility_cpu_test.py @@ -1,9 +1,9 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase + from .librosa_compatibility_test_impl import TransformsTestBase class TestTransforms(TransformsTestBase, PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/transforms/librosa_compatibility_cuda_test.py b/test/torchaudio_unittest/transforms/librosa_compatibility_cuda_test.py index d553458aca..a82c72ab29 100644 --- a/test/torchaudio_unittest/transforms/librosa_compatibility_cuda_test.py +++ b/test/torchaudio_unittest/transforms/librosa_compatibility_cuda_test.py @@ -1,10 +1,10 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase, skipIfNoCuda + from .librosa_compatibility_test_impl import TransformsTestBase @skipIfNoCuda class TestTransforms(TransformsTestBase, PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/transforms/librosa_compatibility_test_impl.py b/test/torchaudio_unittest/transforms/librosa_compatibility_test_impl.py index 9bc8edbb20..614985163d 100644 --- a/test/torchaudio_unittest/transforms/librosa_compatibility_test_impl.py +++ b/test/torchaudio_unittest/transforms/librosa_compatibility_test_impl.py @@ -2,9 +2,8 @@ import torch import torchaudio.transforms as T -from torchaudio._internal.module_utils import is_module_available from parameterized import param, parameterized - +from torchaudio._internal.module_utils import is_module_available from torchaudio_unittest.common_utils import ( TestBaseMixin, get_whitenoise, @@ -13,7 +12,7 @@ nested_params, ) -LIBROSA_AVAILABLE = is_module_available('librosa') +LIBROSA_AVAILABLE = is_module_available("librosa") if LIBROSA_AVAILABLE: import librosa @@ -21,25 +20,28 @@ @unittest.skipIf(not LIBROSA_AVAILABLE, "Librosa not available") class TransformsTestBase(TestBaseMixin): - @parameterized.expand([ - param(n_fft=400, hop_length=200, power=2.0), - param(n_fft=600, hop_length=100, power=2.0), - param(n_fft=400, hop_length=200, power=3.0), - param(n_fft=200, hop_length=50, power=2.0), - ]) + @parameterized.expand( + [ + param(n_fft=400, hop_length=200, power=2.0), + param(n_fft=600, hop_length=100, power=2.0), + param(n_fft=400, hop_length=200, power=3.0), + param(n_fft=200, hop_length=50, power=2.0), + ] + ) def test_Spectrogram(self, n_fft, hop_length, power): sample_rate = 16000 waveform = get_whitenoise( - sample_rate=sample_rate, n_channels=1, + sample_rate=sample_rate, + n_channels=1, ).to(self.device, self.dtype) expected = librosa.core.spectrum._spectrogram( - y=waveform[0].cpu().numpy(), - n_fft=n_fft, hop_length=hop_length, power=power)[0] + y=waveform[0].cpu().numpy(), n_fft=n_fft, hop_length=hop_length, power=power + )[0] - result = T.Spectrogram( - n_fft=n_fft, hop_length=hop_length, power=power, - ).to(self.device, self.dtype)(waveform)[0] + result = T.Spectrogram(n_fft=n_fft, hop_length=hop_length, power=power,).to(self.device, self.dtype)( + waveform + )[0] self.assertEqual(result, torch.from_numpy(expected), atol=1e-5, rtol=1e-5) def test_Spectrogram_complex(self): @@ -47,16 +49,17 @@ def test_Spectrogram_complex(self): hop_length = 200 sample_rate = 16000 waveform = get_whitenoise( - sample_rate=sample_rate, n_channels=1, + sample_rate=sample_rate, + n_channels=1, ).to(self.device, self.dtype) expected = librosa.core.spectrum._spectrogram( - y=waveform[0].cpu().numpy(), - n_fft=n_fft, hop_length=hop_length, power=1)[0] + y=waveform[0].cpu().numpy(), n_fft=n_fft, hop_length=hop_length, power=1 + )[0] - result = T.Spectrogram( - n_fft=n_fft, hop_length=hop_length, power=None, return_complex=True, - ).to(self.device, self.dtype)(waveform)[0] + result = T.Spectrogram(n_fft=n_fft, hop_length=hop_length, power=None, return_complex=True,).to( + self.device, self.dtype + )(waveform)[0] self.assertEqual(result.abs(), torch.from_numpy(expected), atol=1e-5, rtol=1e-5) @nested_params( @@ -65,77 +68,95 @@ def test_Spectrogram_complex(self): param(n_fft=600, hop_length=100, n_mels=128), param(n_fft=200, hop_length=50, n_mels=32), ], - [param(norm=norm) for norm in [None, 'slaney']], - [param(mel_scale=mel_scale) for mel_scale in ['htk', 'slaney']], + [param(norm=norm) for norm in [None, "slaney"]], + [param(mel_scale=mel_scale) for mel_scale in ["htk", "slaney"]], ) def test_MelSpectrogram(self, n_fft, hop_length, n_mels, norm, mel_scale): sample_rate = 16000 waveform = get_sinusoid( - sample_rate=sample_rate, n_channels=1, + sample_rate=sample_rate, + n_channels=1, ).to(self.device, self.dtype) expected = librosa.feature.melspectrogram( y=waveform[0].cpu().numpy(), - sr=sample_rate, n_fft=n_fft, - hop_length=hop_length, n_mels=n_mels, norm=norm, - htk=mel_scale == "htk") + sr=sample_rate, + n_fft=n_fft, + hop_length=hop_length, + n_mels=n_mels, + norm=norm, + htk=mel_scale == "htk", + ) result = T.MelSpectrogram( - sample_rate=sample_rate, window_fn=torch.hann_window, - hop_length=hop_length, n_mels=n_mels, - n_fft=n_fft, norm=norm, mel_scale=mel_scale, + sample_rate=sample_rate, + window_fn=torch.hann_window, + hop_length=hop_length, + n_mels=n_mels, + n_fft=n_fft, + norm=norm, + mel_scale=mel_scale, ).to(self.device, self.dtype)(waveform)[0] self.assertEqual(result, torch.from_numpy(expected), atol=5e-4, rtol=1e-5) def test_magnitude_to_db(self): - spectrogram = get_spectrogram( - get_whitenoise(), n_fft=400, power=2).to(self.device, self.dtype) - result = T.AmplitudeToDB('magnitude', 80.).to(self.device, self.dtype)(spectrogram)[0] + spectrogram = get_spectrogram(get_whitenoise(), n_fft=400, power=2).to(self.device, self.dtype) + result = T.AmplitudeToDB("magnitude", 80.0).to(self.device, self.dtype)(spectrogram)[0] expected = librosa.core.spectrum.amplitude_to_db(spectrogram[0].cpu().numpy()) self.assertEqual(result, torch.from_numpy(expected)) def test_power_to_db(self): - spectrogram = get_spectrogram( - get_whitenoise(), n_fft=400, power=2).to(self.device, self.dtype) - result = T.AmplitudeToDB('power', 80.).to(self.device, self.dtype)(spectrogram)[0] + spectrogram = get_spectrogram(get_whitenoise(), n_fft=400, power=2).to(self.device, self.dtype) + result = T.AmplitudeToDB("power", 80.0).to(self.device, self.dtype)(spectrogram)[0] expected = librosa.core.spectrum.power_to_db(spectrogram[0].cpu().numpy()) self.assertEqual(result, torch.from_numpy(expected)) - @nested_params([ - param(n_fft=400, hop_length=200, n_mels=64, n_mfcc=40), - param(n_fft=600, hop_length=100, n_mels=128, n_mfcc=20), - param(n_fft=200, hop_length=50, n_mels=32, n_mfcc=25), - ]) + @nested_params( + [ + param(n_fft=400, hop_length=200, n_mels=64, n_mfcc=40), + param(n_fft=600, hop_length=100, n_mels=128, n_mfcc=20), + param(n_fft=200, hop_length=50, n_mels=32, n_mfcc=25), + ] + ) def test_mfcc(self, n_fft, hop_length, n_mels, n_mfcc): sample_rate = 16000 - waveform = get_whitenoise( - sample_rate=sample_rate, n_channels=1).to(self.device, self.dtype) + waveform = get_whitenoise(sample_rate=sample_rate, n_channels=1).to(self.device, self.dtype) result = T.MFCC( - sample_rate=sample_rate, n_mfcc=n_mfcc, norm='ortho', - melkwargs={'hop_length': hop_length, 'n_fft': n_fft, 'n_mels': n_mels}, + sample_rate=sample_rate, + n_mfcc=n_mfcc, + norm="ortho", + melkwargs={"hop_length": hop_length, "n_fft": n_fft, "n_mels": n_mels}, ).to(self.device, self.dtype)(waveform)[0] melspec = librosa.feature.melspectrogram( - y=waveform[0].cpu().numpy(), sr=sample_rate, n_fft=n_fft, - win_length=n_fft, hop_length=hop_length, - n_mels=n_mels, htk=True, norm=None) + y=waveform[0].cpu().numpy(), + sr=sample_rate, + n_fft=n_fft, + win_length=n_fft, + hop_length=hop_length, + n_mels=n_mels, + htk=True, + norm=None, + ) expected = librosa.feature.mfcc( - S=librosa.core.spectrum.power_to_db(melspec), - n_mfcc=n_mfcc, dct_type=2, norm='ortho') + S=librosa.core.spectrum.power_to_db(melspec), n_mfcc=n_mfcc, dct_type=2, norm="ortho" + ) self.assertEqual(result, torch.from_numpy(expected), atol=5e-4, rtol=1e-5) - @parameterized.expand([ - param(n_fft=400, hop_length=200), - param(n_fft=600, hop_length=100), - param(n_fft=200, hop_length=50), - ]) + @parameterized.expand( + [ + param(n_fft=400, hop_length=200), + param(n_fft=600, hop_length=100), + param(n_fft=200, hop_length=50), + ] + ) def test_spectral_centroid(self, n_fft, hop_length): sample_rate = 16000 - waveform = get_whitenoise( - sample_rate=sample_rate, n_channels=1).to(self.device, self.dtype) + waveform = get_whitenoise(sample_rate=sample_rate, n_channels=1).to(self.device, self.dtype) - result = T.SpectralCentroid( - sample_rate=sample_rate, n_fft=n_fft, hop_length=hop_length, - ).to(self.device, self.dtype)(waveform) + result = T.SpectralCentroid(sample_rate=sample_rate, n_fft=n_fft, hop_length=hop_length,).to( + self.device, self.dtype + )(waveform) expected = librosa.feature.spectral_centroid( - y=waveform[0].cpu().numpy(), sr=sample_rate, n_fft=n_fft, hop_length=hop_length) + y=waveform[0].cpu().numpy(), sr=sample_rate, n_fft=n_fft, hop_length=hop_length + ) self.assertEqual(result, torch.from_numpy(expected), atol=5e-4, rtol=1e-5) diff --git a/test/torchaudio_unittest/transforms/sox_compatibility_test.py b/test/torchaudio_unittest/transforms/sox_compatibility_test.py index be6c9020ab..340b311422 100644 --- a/test/torchaudio_unittest/transforms/sox_compatibility_test.py +++ b/test/torchaudio_unittest/transforms/sox_compatibility_test.py @@ -3,7 +3,6 @@ import torch import torchaudio.transforms as T from parameterized import parameterized - from torchaudio_unittest.common_utils import ( skipIfNoSox, skipIfNoExec, @@ -18,10 +17,10 @@ @skipIfNoSox -@skipIfNoExec('sox') +@skipIfNoExec("sox") class TestFunctionalFiltering(TempDirMixin, TorchaudioTestCase): def run_sox_effect(self, input_file, effect): - output_file = self.get_temp_path('expected.wav') + output_file = self.get_temp_path("expected.wav") sox_utils.run_sox_effect(input_file, output_file, [str(e) for e in effect]) return load_wav(output_file) @@ -31,39 +30,45 @@ def assert_sox_effect(self, result, input_path, effects, atol=1e-04, rtol=1e-5): def get_whitenoise(self, sample_rate=8000): noise = get_whitenoise( - sample_rate=sample_rate, duration=3, scale_factor=0.9, + sample_rate=sample_rate, + duration=3, + scale_factor=0.9, ) path = self.get_temp_path("whitenoise.wav") save_wav(path, noise, sample_rate) return noise, path - @parameterized.expand([ - ('q', 'quarter_sine'), - ('h', 'half_sine'), - ('t', 'linear'), - ]) + @parameterized.expand( + [ + ("q", "quarter_sine"), + ("h", "half_sine"), + ("t", "linear"), + ] + ) def test_fade(self, fade_shape_sox, fade_shape): fade_in_len, fade_out_len = 44100, 44100 data, path = self.get_whitenoise(sample_rate=44100) result = T.Fade(fade_in_len, fade_out_len, fade_shape)(data) - self.assert_sox_effect(result, path, ['fade', fade_shape_sox, '1', '0', '1']) + self.assert_sox_effect(result, path, ["fade", fade_shape_sox, "1", "0", "1"]) - @parameterized.expand([ - ('amplitude', 1.1), - ('db', 2), - ('power', 2), - ]) + @parameterized.expand( + [ + ("amplitude", 1.1), + ("db", 2), + ("power", 2), + ] + ) def test_vol(self, gain_type, gain): data, path = self.get_whitenoise() result = T.Vol(gain, gain_type)(data) - self.assert_sox_effect(result, path, ['vol', f'{gain}', gain_type]) + self.assert_sox_effect(result, path, ["vol", f"{gain}", gain_type]) - @parameterized.expand(['vad-go-stereo-44100.wav', 'vad-go-mono-32000.wav']) + @parameterized.expand(["vad-go-stereo-44100.wav", "vad-go-mono-32000.wav"]) def test_vad(self, filename): path = get_asset_path(filename) data, sample_rate = load_wav(path) result = T.Vad(sample_rate)(data) - self.assert_sox_effect(result, path, ['vad']) + self.assert_sox_effect(result, path, ["vad"]) def test_vad_warning(self): """vad should throw a warning if input dimension is greater than 2""" diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py b/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py index 9f16922a09..5ba0afe2b2 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_cpu_test.py @@ -1,14 +1,14 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase + from .torchscript_consistency_impl import Transforms, TransformsFloat32Only class TestTransformsFloat32(Transforms, TransformsFloat32Only, PytorchTestCase): dtype = torch.float32 - device = torch.device('cpu') + device = torch.device("cpu") class TestTransformsFloat64(Transforms, PytorchTestCase): dtype = torch.float64 - device = torch.device('cpu') + device = torch.device("cpu") diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py b/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py index b805449cd5..f2a0826a32 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_cuda_test.py @@ -1,16 +1,16 @@ import torch - from torchaudio_unittest.common_utils import skipIfNoCuda, PytorchTestCase + from .torchscript_consistency_impl import Transforms, TransformsFloat32Only @skipIfNoCuda class TestTransformsFloat32(Transforms, TransformsFloat32Only, PytorchTestCase): dtype = torch.float32 - device = torch.device('cuda') + device = torch.device("cuda") @skipIfNoCuda class TestTransformsFloat64(Transforms, PytorchTestCase): dtype = torch.float64 - device = torch.device('cuda') + device = torch.device("cuda") diff --git a/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py b/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py index c85b4a0236..9c3304911a 100644 --- a/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py +++ b/test/torchaudio_unittest/transforms/torchscript_consistency_impl.py @@ -3,7 +3,6 @@ import torch import torchaudio.transforms as T from parameterized import parameterized - from torchaudio_unittest import common_utils from torchaudio_unittest.common_utils import ( skipIfRocm, @@ -14,6 +13,7 @@ class Transforms(TestBaseMixin): """Implements test for Transforms that are performed for different devices""" + def _assert_consistency(self, transform, tensor, *args): tensor = tensor.to(device=self.device, dtype=self.dtype) transform = transform.to(device=self.device, dtype=self.dtype) @@ -139,10 +139,7 @@ def test_PitchShift(self): sample_rate = 8000 n_steps = 4 waveform = common_utils.get_whitenoise(sample_rate=sample_rate) - self._assert_consistency( - T.PitchShift(sample_rate=sample_rate, n_steps=n_steps), - waveform - ) + self._assert_consistency(T.PitchShift(sample_rate=sample_rate, n_steps=n_steps), waveform) def test_PSD(self): tensor = common_utils.get_whitenoise(sample_rate=8000, n_channels=4) @@ -157,33 +154,34 @@ def test_PSD_with_mask(self): mask = torch.rand(spectrogram.shape[-2:], device=self.device) self._assert_consistency_complex(T.PSD(), spectrogram, mask) - @parameterized.expand([ - ["ref_channel", True], - ["stv_evd", True], - ["stv_power", True], - ["ref_channel", False], - ["stv_evd", False], - ["stv_power", False], - ]) + @parameterized.expand( + [ + ["ref_channel", True], + ["stv_evd", True], + ["stv_power", True], + ["ref_channel", False], + ["stv_evd", False], + ["stv_power", False], + ] + ) def test_MVDR(self, solution, online): tensor = common_utils.get_whitenoise(sample_rate=8000, n_channels=4) spectrogram = common_utils.get_spectrogram(tensor, n_fft=400, hop_length=100) mask_s = torch.rand(spectrogram.shape[-2:], device=self.device) mask_n = torch.rand(spectrogram.shape[-2:], device=self.device) - self._assert_consistency_complex( - T.MVDR(solution=solution, online=online), - spectrogram, mask_s, mask_n - ) + self._assert_consistency_complex(T.MVDR(solution=solution, online=online), spectrogram, mask_s, mask_n) class TransformsFloat32Only(TestBaseMixin): def test_rnnt_loss(self): - logits = torch.tensor([[[[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.6, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.8, 0.1]], - [[0.1, 0.6, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.2, 0.1, 0.1], - [0.7, 0.1, 0.2, 0.1, 0.1]]]]) + logits = torch.tensor( + [ + [ + [[0.1, 0.6, 0.1, 0.1, 0.1], [0.1, 0.1, 0.6, 0.1, 0.1], [0.1, 0.1, 0.2, 0.8, 0.1]], + [[0.1, 0.6, 0.1, 0.1, 0.1], [0.1, 0.1, 0.2, 0.1, 0.1], [0.7, 0.1, 0.2, 0.1, 0.1]], + ] + ] + ) tensor = logits.to(device=self.device, dtype=torch.float32) targets = torch.tensor([[1, 2]], device=tensor.device, dtype=torch.int32) logit_lengths = torch.tensor([2], device=tensor.device, dtype=torch.int32) diff --git a/test/torchaudio_unittest/transforms/transforms_cpu_test.py b/test/torchaudio_unittest/transforms/transforms_cpu_test.py index 5177b9a455..dd76e31bd8 100644 --- a/test/torchaudio_unittest/transforms/transforms_cpu_test.py +++ b/test/torchaudio_unittest/transforms/transforms_cpu_test.py @@ -1,14 +1,14 @@ import torch - from torchaudio_unittest.common_utils import PytorchTestCase -from . transforms_test_impl import TransformsTestBase + +from .transforms_test_impl import TransformsTestBase class TransformsCPUFloat32Test(TransformsTestBase, PytorchTestCase): - device = 'cpu' + device = "cpu" dtype = torch.float32 class TransformsCPUFloat64Test(TransformsTestBase, PytorchTestCase): - device = 'cpu' + device = "cpu" dtype = torch.float64 diff --git a/test/torchaudio_unittest/transforms/transforms_cuda_test.py b/test/torchaudio_unittest/transforms/transforms_cuda_test.py index 9489660384..055aa18196 100644 --- a/test/torchaudio_unittest/transforms/transforms_cuda_test.py +++ b/test/torchaudio_unittest/transforms/transforms_cuda_test.py @@ -1,19 +1,19 @@ import torch - from torchaudio_unittest.common_utils import ( PytorchTestCase, skipIfNoCuda, ) -from . transforms_test_impl import TransformsTestBase + +from .transforms_test_impl import TransformsTestBase @skipIfNoCuda class TransformsCUDAFloat32Test(TransformsTestBase, PytorchTestCase): - device = 'cuda' + device = "cuda" dtype = torch.float32 @skipIfNoCuda class TransformsCUDAFloat64Test(TransformsTestBase, PytorchTestCase): - device = 'cuda' + device = "cuda" dtype = torch.float64 diff --git a/test/torchaudio_unittest/transforms/transforms_test.py b/test/torchaudio_unittest/transforms/transforms_test.py index 7808789cc6..821b50d67d 100644 --- a/test/torchaudio_unittest/transforms/transforms_test.py +++ b/test/torchaudio_unittest/transforms/transforms_test.py @@ -2,24 +2,23 @@ import torch import torchaudio -import torchaudio.transforms as transforms import torchaudio.functional as F - +import torchaudio.transforms as transforms from torchaudio_unittest import common_utils class Tester(common_utils.TorchaudioTestCase): - backend = 'default' + backend = "default" # create a sinewave signal for testing sample_rate = 16000 freq = 440 - volume = .3 - waveform = (torch.cos(2 * math.pi * torch.arange(0, 4 * sample_rate).float() * freq / sample_rate)) + volume = 0.3 + waveform = torch.cos(2 * math.pi * torch.arange(0, 4 * sample_rate).float() * freq / sample_rate) waveform.unsqueeze_(0) # (1, 64000) - waveform = (waveform * volume * 2**31).long() + waveform = (waveform * volume * 2 ** 31).long() - def scale(self, waveform, factor=2.0**31): + def scale(self, waveform, factor=2.0 ** 31): # scales a waveform by a factor if not waveform.is_floating_point(): waveform = waveform.to(torch.get_default_dtype()) @@ -34,20 +33,20 @@ def test_mu_law_companding(self): waveform = waveform.to(torch.get_default_dtype()) waveform /= torch.abs(waveform).max() - self.assertTrue(waveform.min() >= -1. and waveform.max() <= 1.) + self.assertTrue(waveform.min() >= -1.0 and waveform.max() <= 1.0) waveform_mu = transforms.MuLawEncoding(quantization_channels)(waveform) - self.assertTrue(waveform_mu.min() >= 0. and waveform_mu.max() <= quantization_channels) + self.assertTrue(waveform_mu.min() >= 0.0 and waveform_mu.max() <= quantization_channels) waveform_exp = transforms.MuLawDecoding(quantization_channels)(waveform_mu) - self.assertTrue(waveform_exp.min() >= -1. and waveform_exp.max() <= 1.) + self.assertTrue(waveform_exp.min() >= -1.0 and waveform_exp.max() <= 1.0) def test_AmplitudeToDB(self): - filepath = common_utils.get_asset_path('steam-train-whistle-daniel_simon.wav') + filepath = common_utils.get_asset_path("steam-train-whistle-daniel_simon.wav") waveform = common_utils.load_wav(filepath)[0] - mag_to_db_transform = transforms.AmplitudeToDB('magnitude', 80.) - power_to_db_transform = transforms.AmplitudeToDB('power', 80.) + mag_to_db_transform = transforms.AmplitudeToDB("magnitude", 80.0) + power_to_db_transform = transforms.AmplitudeToDB("power", 80.0) mag_to_db_torch = mag_to_db_transform(torch.abs(waveform)) power_to_db_torch = power_to_db_transform(torch.pow(waveform, 2)) @@ -88,8 +87,8 @@ def test_melspectrogram_load_save(self): self.assertEqual(fb, fb_copy) def test_mel2(self): - top_db = 80. - s2db = transforms.AmplitudeToDB('power', top_db) + top_db = 80.0 + s2db = transforms.AmplitudeToDB("power", top_db) waveform = self.waveform.clone() # (1, 16000) waveform_scaled = self.scale(waveform) # (1, 16000) @@ -100,20 +99,26 @@ def test_mel2(self): self.assertTrue(spectrogram_torch.ge(spectrogram_torch.max() - top_db).all()) self.assertEqual(spectrogram_torch.size(1), mel_transform.n_mels) # check correctness of filterbank conversion matrix - self.assertTrue(mel_transform.mel_scale.fb.sum(1).le(1.).all()) - self.assertTrue(mel_transform.mel_scale.fb.sum(1).ge(0.).all()) + self.assertTrue(mel_transform.mel_scale.fb.sum(1).le(1.0).all()) + self.assertTrue(mel_transform.mel_scale.fb.sum(1).ge(0.0).all()) # check options - kwargs = {'window_fn': torch.hamming_window, 'pad': 10, 'win_length': 500, - 'hop_length': 125, 'n_fft': 800, 'n_mels': 50} + kwargs = { + "window_fn": torch.hamming_window, + "pad": 10, + "win_length": 500, + "hop_length": 125, + "n_fft": 800, + "n_mels": 50, + } mel_transform2 = transforms.MelSpectrogram(**kwargs) spectrogram2_torch = s2db(mel_transform2(waveform_scaled)) # (1, 50, 513) self.assertTrue(spectrogram2_torch.dim() == 3) self.assertTrue(spectrogram_torch.ge(spectrogram_torch.max() - top_db).all()) self.assertEqual(spectrogram2_torch.size(1), mel_transform2.n_mels) - self.assertTrue(mel_transform2.mel_scale.fb.sum(1).le(1.).all()) - self.assertTrue(mel_transform2.mel_scale.fb.sum(1).ge(0.).all()) + self.assertTrue(mel_transform2.mel_scale.fb.sum(1).le(1.0).all()) + self.assertTrue(mel_transform2.mel_scale.fb.sum(1).ge(0.0).all()) # check on multi-channel audio - filepath = common_utils.get_asset_path('steam-train-whistle-daniel_simon.wav') + filepath = common_utils.get_asset_path("steam-train-whistle-daniel_simon.wav") x_stereo = common_utils.load_wav(filepath)[0] # (2, 278756), 44100 spectrogram_stereo = s2db(mel_transform(x_stereo)) # (2, 128, 1394) self.assertTrue(spectrogram_stereo.dim() == 3) @@ -121,57 +126,46 @@ def test_mel2(self): self.assertTrue(spectrogram_torch.ge(spectrogram_torch.max() - top_db).all()) self.assertEqual(spectrogram_stereo.size(1), mel_transform.n_mels) # check filterbank matrix creation - fb_matrix_transform = transforms.MelScale( - n_mels=100, sample_rate=16000, f_min=0., f_max=None, n_stft=400) - self.assertTrue(fb_matrix_transform.fb.sum(1).le(1.).all()) - self.assertTrue(fb_matrix_transform.fb.sum(1).ge(0.).all()) + fb_matrix_transform = transforms.MelScale(n_mels=100, sample_rate=16000, f_min=0.0, f_max=None, n_stft=400) + self.assertTrue(fb_matrix_transform.fb.sum(1).le(1.0).all()) + self.assertTrue(fb_matrix_transform.fb.sum(1).ge(0.0).all()) self.assertEqual(fb_matrix_transform.fb.size(), (400, 100)) def test_mfcc_defaults(self): - """Check the default configuration of the MFCC transform. - """ + """Check the default configuration of the MFCC transform.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_mfcc = 40 - mfcc_transform = torchaudio.transforms.MFCC(sample_rate=sample_rate, - n_mfcc=n_mfcc, - norm='ortho') + mfcc_transform = torchaudio.transforms.MFCC(sample_rate=sample_rate, n_mfcc=n_mfcc, norm="ortho") torch_mfcc = mfcc_transform(audio) # (1, 40, 81) self.assertEqual(torch_mfcc.dim(), 3) self.assertEqual(torch_mfcc.shape[1], n_mfcc) self.assertEqual(torch_mfcc.shape[2], 81) def test_mfcc_kwargs_passthrough(self): - """Check kwargs get correctly passed to the MelSpectrogram transform. - """ + """Check kwargs get correctly passed to the MelSpectrogram transform.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_mfcc = 40 - melkwargs = {'win_length': 200} - mfcc_transform = torchaudio.transforms.MFCC(sample_rate=sample_rate, - n_mfcc=n_mfcc, - norm='ortho', - melkwargs=melkwargs) + melkwargs = {"win_length": 200} + mfcc_transform = torchaudio.transforms.MFCC( + sample_rate=sample_rate, n_mfcc=n_mfcc, norm="ortho", melkwargs=melkwargs + ) torch_mfcc = mfcc_transform(audio) # (1, 40, 161) self.assertEqual(torch_mfcc.shape[2], 161) def test_mfcc_norms(self): - """Check if MFCC-DCT norms work correctly. - """ + """Check if MFCC-DCT norms work correctly.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_mfcc = 40 n_mels = 128 - mfcc_transform = torchaudio.transforms.MFCC(sample_rate=sample_rate, - n_mfcc=n_mfcc, - norm='ortho') + mfcc_transform = torchaudio.transforms.MFCC(sample_rate=sample_rate, n_mfcc=n_mfcc, norm="ortho") # check norms work correctly - mfcc_transform_norm_none = torchaudio.transforms.MFCC(sample_rate=sample_rate, - n_mfcc=n_mfcc, - norm=None) + mfcc_transform_norm_none = torchaudio.transforms.MFCC(sample_rate=sample_rate, n_mfcc=n_mfcc, norm=None) torch_mfcc_norm_none = mfcc_transform_norm_none(audio) # (1, 40, 81) norm_check = mfcc_transform(audio) @@ -181,56 +175,48 @@ def test_mfcc_norms(self): self.assertEqual(torch_mfcc_norm_none, norm_check) def test_lfcc_defaults(self): - """Check default settings for LFCC transform. - """ + """Check default settings for LFCC transform.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_lfcc = 40 n_filter = 128 - lfcc_transform = torchaudio.transforms.LFCC(sample_rate=sample_rate, - n_filter=n_filter, - n_lfcc=n_lfcc, - norm='ortho') + lfcc_transform = torchaudio.transforms.LFCC( + sample_rate=sample_rate, n_filter=n_filter, n_lfcc=n_lfcc, norm="ortho" + ) torch_lfcc = lfcc_transform(audio) # (1, 40, 81) self.assertEqual(torch_lfcc.dim(), 3) self.assertEqual(torch_lfcc.shape[1], n_lfcc) self.assertEqual(torch_lfcc.shape[2], 81) def test_lfcc_arg_passthrough(self): - """Check if kwargs get correctly passed to the underlying Spectrogram transform. - """ + """Check if kwargs get correctly passed to the underlying Spectrogram transform.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_lfcc = 40 n_filter = 128 - speckwargs = {'win_length': 200} - lfcc_transform = torchaudio.transforms.LFCC(sample_rate=sample_rate, - n_filter=n_filter, - n_lfcc=n_lfcc, - norm='ortho', - speckwargs=speckwargs) + speckwargs = {"win_length": 200} + lfcc_transform = torchaudio.transforms.LFCC( + sample_rate=sample_rate, n_filter=n_filter, n_lfcc=n_lfcc, norm="ortho", speckwargs=speckwargs + ) torch_lfcc = lfcc_transform(audio) # (1, 40, 161) self.assertEqual(torch_lfcc.shape[2], 161) def test_lfcc_norms(self): - """Check if LFCC-DCT norm works correctly. - """ + """Check if LFCC-DCT norm works correctly.""" sample_rate = 16000 audio = common_utils.get_whitenoise(sample_rate=sample_rate) n_lfcc = 40 n_filter = 128 - lfcc_transform = torchaudio.transforms.LFCC(sample_rate=sample_rate, - n_filter=n_filter, - n_lfcc=n_lfcc, - norm='ortho') - - lfcc_transform_norm_none = torchaudio.transforms.LFCC(sample_rate=sample_rate, - n_filter=n_filter, - n_lfcc=n_lfcc, - norm=None) + lfcc_transform = torchaudio.transforms.LFCC( + sample_rate=sample_rate, n_filter=n_filter, n_lfcc=n_lfcc, norm="ortho" + ) + + lfcc_transform_norm_none = torchaudio.transforms.LFCC( + sample_rate=sample_rate, n_filter=n_filter, n_lfcc=n_lfcc, norm=None + ) torch_lfcc_norm_none = lfcc_transform_norm_none(audio) # (1, 40, 161) norm_check = lfcc_transform(audio) # (1, 40, 161) @@ -240,26 +226,27 @@ def test_lfcc_norms(self): self.assertEqual(torch_lfcc_norm_none, norm_check) def test_resample_size(self): - input_path = common_utils.get_asset_path('sinewave.wav') + input_path = common_utils.get_asset_path("sinewave.wav") waveform, sample_rate = common_utils.load_wav(input_path) upsample_rate = sample_rate * 2 downsample_rate = sample_rate // 2 - invalid_resampling_method = 'foo' + invalid_resampling_method = "foo" with self.assertRaises(ValueError): - torchaudio.transforms.Resample(sample_rate, upsample_rate, - resampling_method=invalid_resampling_method) + torchaudio.transforms.Resample(sample_rate, upsample_rate, resampling_method=invalid_resampling_method) upsample_resample = torchaudio.transforms.Resample( - sample_rate, upsample_rate, resampling_method='sinc_interpolation') + sample_rate, upsample_rate, resampling_method="sinc_interpolation" + ) up_sampled = upsample_resample(waveform) # we expect the upsampled signal to have twice as many samples self.assertTrue(up_sampled.size(-1) == waveform.size(-1) * 2) downsample_resample = torchaudio.transforms.Resample( - sample_rate, downsample_rate, resampling_method='sinc_interpolation') + sample_rate, downsample_rate, resampling_method="sinc_interpolation" + ) down_sampled = downsample_resample(waveform) # we expect the downsampled signal to have half as many samples @@ -289,9 +276,8 @@ def test_compute_deltas_transform_same_as_functional(self, atol=1e-6, rtol=1e-8) self.assertEqual(computed_functional, computed_transform, atol=atol, rtol=rtol) def test_compute_deltas_twochannel(self): - specgram = torch.tensor([1., 2., 3., 4.]).repeat(1, 2, 1) - expected = torch.tensor([[[0.5, 1.0, 1.0, 0.5], - [0.5, 1.0, 1.0, 0.5]]]) + specgram = torch.tensor([1.0, 2.0, 3.0, 4.0]).repeat(1, 2, 1) + expected = torch.tensor([[[0.5, 1.0, 1.0, 0.5], [0.5, 1.0, 1.0, 0.5]]]) transform = transforms.ComputeDeltas(win_length=3) computed = transform(specgram) assert computed.shape == expected.shape, (computed.shape, expected.shape) @@ -299,7 +285,6 @@ def test_compute_deltas_twochannel(self): class SmokeTest(common_utils.TorchaudioTestCase): - def test_spectrogram(self): specgram = transforms.Spectrogram(center=False, pad_mode="reflect", onesided=False) self.assertEqual(specgram.center, False) diff --git a/test/torchaudio_unittest/transforms/transforms_test_impl.py b/test/torchaudio_unittest/transforms/transforms_test_impl.py index 9aa5496a18..3fe9c4c25b 100644 --- a/test/torchaudio_unittest/transforms/transforms_test_impl.py +++ b/test/torchaudio_unittest/transforms/transforms_test_impl.py @@ -38,15 +38,12 @@ def test_InverseMelScale(self): # Generate reference spectrogram and input mel-scaled spectrogram expected = get_spectrogram( - get_whitenoise(sample_rate=sample_rate, duration=1, n_channels=2), - n_fft=n_fft, power=power).to(self.device, self.dtype) - input = T.MelScale( - n_mels=n_mels, sample_rate=sample_rate, n_stft=n_stft - ).to(self.device, self.dtype)(expected) + get_whitenoise(sample_rate=sample_rate, duration=1, n_channels=2), n_fft=n_fft, power=power + ).to(self.device, self.dtype) + input = T.MelScale(n_mels=n_mels, sample_rate=sample_rate, n_stft=n_stft).to(self.device, self.dtype)(expected) # Run transform - transform = T.InverseMelScale( - n_stft, n_mels=n_mels, sample_rate=sample_rate).to(self.device, self.dtype) + transform = T.InverseMelScale(n_stft, n_mels=n_mels, sample_rate=sample_rate).to(self.device, self.dtype) torch.random.manual_seed(0) result = transform(input) @@ -55,9 +52,7 @@ def test_InverseMelScale(self): relative_diff = torch.abs((result - expected) / (expected + epsilon)) for tol in [1e-1, 1e-3, 1e-5, 1e-10]: - print( - f"Ratio of relative diff smaller than {tol:e} is " - f"{_get_ratio(relative_diff < tol)}") + print(f"Ratio of relative diff smaller than {tol:e} is " f"{_get_ratio(relative_diff < tol)}") assert _get_ratio(relative_diff < 1e-1) > 0.2 assert _get_ratio(relative_diff < 1e-3) > 5e-3 assert _get_ratio(relative_diff < 1e-5) > 1e-5 @@ -84,21 +79,23 @@ def test_resample_cache_dtype(self, resampling_method, dtype): assert transform.kernel.dtype == dtype if dtype is not None else torch.float32 - @parameterized.expand([ - param(n_fft=300, center=True, onesided=True), - param(n_fft=400, center=True, onesided=False), - param(n_fft=400, center=True, onesided=False), - param(n_fft=300, center=True, onesided=False), - param(n_fft=400, hop_length=10), - param(n_fft=800, win_length=400, hop_length=20), - param(n_fft=800, win_length=400, hop_length=20, normalized=True), - param(), - param(n_fft=400, pad=32), - # These tests do not work - cause runtime error - # See https://github.com/pytorch/pytorch/issues/62323 - # param(n_fft=400, center=False, onesided=True), - # param(n_fft=400, center=False, onesided=False), - ]) + @parameterized.expand( + [ + param(n_fft=300, center=True, onesided=True), + param(n_fft=400, center=True, onesided=False), + param(n_fft=400, center=True, onesided=False), + param(n_fft=300, center=True, onesided=False), + param(n_fft=400, hop_length=10), + param(n_fft=800, win_length=400, hop_length=20), + param(n_fft=800, win_length=400, hop_length=20, normalized=True), + param(), + param(n_fft=400, pad=32), + # These tests do not work - cause runtime error + # See https://github.com/pytorch/pytorch/issues/62323 + # param(n_fft=400, center=False, onesided=True), + # param(n_fft=400, center=False, onesided=False), + ] + ) def test_roundtrip_spectrogram(self, **args): """Test the spectrogram + inverse spectrogram results in approximate identity.""" @@ -110,12 +107,14 @@ def test_roundtrip_spectrogram(self, **args): restored = inv_s.forward(transformed, length=waveform.shape[-1]) self.assertEqual(waveform, restored, atol=1e-6, rtol=1e-6) - @parameterized.expand([ - param(0.5, 1, True, False), - param(0.5, 1, None, False), - param(1, 4, True, True), - param(1, 6, None, True), - ]) + @parameterized.expand( + [ + param(0.5, 1, True, False), + param(0.5, 1, None, False), + param(1, 4, True, True), + param(1, 6, None, True), + ] + ) def test_psd(self, duration, channel, mask, multi_mask): """Providing dtype changes the kernel cache dtype""" transform = T.PSD(multi_mask) diff --git a/test/torchaudio_unittest/utils/sox_utils_test.py b/test/torchaudio_unittest/utils/sox_utils_test.py index 7bd9018437..f2026a3699 100644 --- a/test/torchaudio_unittest/utils/sox_utils_test.py +++ b/test/torchaudio_unittest/utils/sox_utils_test.py @@ -1,5 +1,4 @@ from torchaudio.utils import sox_utils - from torchaudio_unittest.common_utils import ( PytorchTestCase, skipIfNoSox, @@ -9,6 +8,7 @@ @skipIfNoSox class TestSoxUtils(PytorchTestCase): """Smoke tests for sox_util module""" + def test_set_seed(self): """`set_seed` does not crush""" sox_utils.set_seed(0) @@ -34,16 +34,16 @@ def test_list_effects(self): """`list_effects` returns the list of available effects""" effects = sox_utils.list_effects() # We cannot infer what effects are available, so only check some of them. - assert 'highpass' in effects - assert 'phaser' in effects - assert 'gain' in effects + assert "highpass" in effects + assert "phaser" in effects + assert "gain" in effects def test_list_read_formats(self): """`list_read_formats` returns the list of supported formats""" formats = sox_utils.list_read_formats() - assert 'wav' in formats + assert "wav" in formats def test_list_write_formats(self): """`list_write_formats` returns the list of supported formats""" formats = sox_utils.list_write_formats() - assert 'opus' not in formats + assert "opus" not in formats diff --git a/tools/convert_fairseq_models.py b/tools/convert_fairseq_models.py index 8bdb7cf174..40945770e2 100644 --- a/tools/convert_fairseq_models.py +++ b/tools/convert_fairseq_models.py @@ -35,21 +35,15 @@ def _parse_args(): description=__doc__, formatter_class=argparse.RawTextHelpFormatter, ) + parser.add_argument("--input-file", required=True, help="Input model file.") + parser.add_argument("--output-file", required=False, help="Output model file.") parser.add_argument( - '--input-file', required=True, - help='Input model file.' - ) - parser.add_argument( - '--output-file', required=False, - help='Output model file.' - ) - parser.add_argument( - '--dict-dir', + "--dict-dir", help=( - 'Directory where letter vocabulary file, `dict.ltr.txt`, is found. ' - 'Required when loading wav2vec2 model. ' - 'https://dl.fbaipublicfiles.com/fairseq/wav2vec/dict.ltr.txt' - ) + "Directory where letter vocabulary file, `dict.ltr.txt`, is found. " + "Required when loading wav2vec2 model. " + "https://dl.fbaipublicfiles.com/fairseq/wav2vec/dict.ltr.txt" + ), ) return parser.parse_args() @@ -57,9 +51,10 @@ def _parse_args(): def _load_model(input_file, dict_dir): import fairseq - overrides = {} if dict_dir is None else {'data': dict_dir} + overrides = {} if dict_dir is None else {"data": dict_dir} models, _, _ = fairseq.checkpoint_utils.load_model_ensemble_and_task( - [input_file], arg_overrides=overrides, + [input_file], + arg_overrides=overrides, ) return models[0] @@ -67,7 +62,7 @@ def _load_model(input_file, dict_dir): def _import_model(model): from torchaudio.models.wav2vec2.utils import import_fairseq_model - if model.__class__.__name__ in ['HubertCtc', 'Wav2VecCtc']: + if model.__class__.__name__ in ["HubertCtc", "Wav2VecCtc"]: model = model.w2v_encoder model = import_fairseq_model(model) return model @@ -75,10 +70,11 @@ def _import_model(model): def _main(args): import torch + model = _load_model(args.input_file, args.dict_dir) model = _import_model(model) torch.save(model.state_dict(), args.output_file) -if __name__ == '__main__': +if __name__ == "__main__": _main(_parse_args()) diff --git a/tools/convert_voxpopuli_models.py b/tools/convert_voxpopuli_models.py index 02ece3c683..0be7d5e036 100755 --- a/tools/convert_voxpopuli_models.py +++ b/tools/convert_voxpopuli_models.py @@ -19,24 +19,19 @@ def _parse_args(): import argparse + parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument( - '--input-file', required=True, - help='Input checkpoint file.' - ) - parser.add_argument( - '--output-file', required=False, - help='Output model file.' - ) + parser.add_argument("--input-file", required=True, help="Input checkpoint file.") + parser.add_argument("--output-file", required=False, help="Output model file.") return parser.parse_args() def _removeprefix(s, prefix): if s.startswith(prefix): - return s[len(prefix):] + return s[len(prefix) :] return s @@ -45,13 +40,13 @@ def _load(input_file): from omegaconf import OmegaConf data = torch.load(input_file) - cfg = OmegaConf.to_container(data['cfg']) + cfg = OmegaConf.to_container(data["cfg"]) for key in list(cfg.keys()): - if key != 'model': + if key != "model": del cfg[key] - if 'w2v_args' in cfg['model']: - del cfg['model']['w2v_args'][key] - state_dict = {_removeprefix(k, 'w2v_encoder.'): v for k, v in data['model'].items()} + if "w2v_args" in cfg["model"]: + del cfg["model"]["w2v_args"][key] + state_dict = {_removeprefix(k, "w2v_encoder."): v for k, v in data["model"].items()} return cfg, state_dict @@ -75,9 +70,9 @@ def _parse_model_param(cfg, state_dict): "encoder_layerdrop": "encoder_layer_drop", } params = {} - src_dicts = [cfg['model']] - if 'w2v_args' in cfg['model']: - src_dicts.append(cfg['model']['w2v_args']['model']) + src_dicts = [cfg["model"]] + if "w2v_args" in cfg["model"]: + src_dicts.append(cfg["model"]["w2v_args"]["model"]) for src, tgt in key_mapping.items(): for model_cfg in src_dicts: @@ -89,12 +84,13 @@ def _parse_model_param(cfg, state_dict): # the following line is commented out to resolve lint warning; uncomment before running script # params["extractor_conv_layer_config"] = eval(params["extractor_conv_layer_config"]) assert len(params) == 15 - params['aux_num_out'] = state_dict['proj.bias'].numel() if 'proj.bias' in state_dict else None + params["aux_num_out"] = state_dict["proj.bias"].numel() if "proj.bias" in state_dict else None return params def _main(args): import json + import torch import torchaudio from torchaudio.models.wav2vec2.utils.import_fairseq import _convert_state_dict as _convert @@ -107,5 +103,5 @@ def _main(args): torch.save(model.state_dict(), args.output_file) -if __name__ == '__main__': +if __name__ == "__main__": _main(_parse_args()) diff --git a/tools/release_notes/retrieve_prs.py b/tools/release_notes/retrieve_prs.py index 975c52d207..2a3e66c1d9 100644 --- a/tools/release_notes/retrieve_prs.py +++ b/tools/release_notes/retrieve_prs.py @@ -24,11 +24,11 @@ def _run_cmd(cmd): - return subprocess.check_output(cmd).decode('utf-8').strip() + return subprocess.check_output(cmd).decode("utf-8").strip() def commit_title(commit_hash): - cmd = ['git', 'log', '-n', '1', '--pretty=format:%s', f'{commit_hash}'] + cmd = ["git", "log", "-n", "1", "--pretty=format:%s", f"{commit_hash}"] return _run_cmd(cmd) @@ -95,12 +95,12 @@ def get_features(commit_hash): def get_commits_between(base_version, new_version): - cmd = ['git', 'merge-base', f'{base_version}', f'{new_version}'] + cmd = ["git", "merge-base", f"{base_version}", f"{new_version}"] merge_base = _run_cmd(cmd) # Returns a list of items in the form # a7854f33 Add HuBERT model architectures (#1769) - cmd = ['git', 'log', '--reverse', '--oneline', f'{merge_base}..{new_version}'] + cmd = ["git", "log", "--reverse", "--oneline", f"{merge_base}..{new_version}"] commits = _run_cmd(cmd) log_lines = commits.split("\n") diff --git a/tools/setup_helpers/extension.py b/tools/setup_helpers/extension.py index a9d8592d4d..db5470b7de 100644 --- a/tools/setup_helpers/extension.py +++ b/tools/setup_helpers/extension.py @@ -1,60 +1,59 @@ +import distutils.sysconfig import os import platform import subprocess from pathlib import Path -import distutils.sysconfig +import torch from setuptools import Extension from setuptools.command.build_ext import build_ext -import torch __all__ = [ - 'get_ext_modules', - 'CMakeBuild', + "get_ext_modules", + "CMakeBuild", ] _THIS_DIR = Path(__file__).parent.resolve() _ROOT_DIR = _THIS_DIR.parent.parent.resolve() -_TORCHAUDIO_DIR = _ROOT_DIR / 'torchaudio' +_TORCHAUDIO_DIR = _ROOT_DIR / "torchaudio" def _get_build(var, default=False): if var not in os.environ: return default - val = os.environ.get(var, '0') - trues = ['1', 'true', 'TRUE', 'on', 'ON', 'yes', 'YES'] - falses = ['0', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO'] + val = os.environ.get(var, "0") + trues = ["1", "true", "TRUE", "on", "ON", "yes", "YES"] + falses = ["0", "false", "FALSE", "off", "OFF", "no", "NO"] if val in trues: return True if val not in falses: - print( - f'WARNING: Unexpected environment variable value `{var}={val}`. ' - f'Expected one of {trues + falses}') + print(f"WARNING: Unexpected environment variable value `{var}={val}`. " f"Expected one of {trues + falses}") return False -_BUILD_SOX = False if platform.system() == 'Windows' else _get_build("BUILD_SOX", True) -_BUILD_KALDI = False if platform.system() == 'Windows' else _get_build("BUILD_KALDI", True) +_BUILD_SOX = False if platform.system() == "Windows" else _get_build("BUILD_SOX", True) +_BUILD_KALDI = False if platform.system() == "Windows" else _get_build("BUILD_KALDI", True) _BUILD_RNNT = _get_build("BUILD_RNNT", True) -_BUILD_CTC_DECODER = False if platform.system() == 'Windows' else _get_build("BUILD_CTC_DECODER", True) +_BUILD_CTC_DECODER = False if platform.system() == "Windows" else _get_build("BUILD_CTC_DECODER", True) _USE_ROCM = _get_build("USE_ROCM", torch.cuda.is_available() and torch.version.hip is not None) _USE_CUDA = _get_build("USE_CUDA", torch.cuda.is_available() and torch.version.hip is None) -_USE_OPENMP = _get_build("USE_OPENMP", True) and \ - 'ATen parallel backend: OpenMP' in torch.__config__.parallel_info() -_TORCH_CUDA_ARCH_LIST = os.environ.get('TORCH_CUDA_ARCH_LIST', None) +_USE_OPENMP = _get_build("USE_OPENMP", True) and "ATen parallel backend: OpenMP" in torch.__config__.parallel_info() +_TORCH_CUDA_ARCH_LIST = os.environ.get("TORCH_CUDA_ARCH_LIST", None) def get_ext_modules(): modules = [ - Extension(name='torchaudio.lib.libtorchaudio', sources=[]), - Extension(name='torchaudio._torchaudio', sources=[]), + Extension(name="torchaudio.lib.libtorchaudio", sources=[]), + Extension(name="torchaudio._torchaudio", sources=[]), ] if _BUILD_CTC_DECODER: - modules.extend([ - Extension(name='torchaudio.lib.libtorchaudio_decoder', sources=[]), - Extension(name='torchaudio._torchaudio_decoder', sources=[]), - ]) + modules.extend( + [ + Extension(name="torchaudio.lib.libtorchaudio_decoder", sources=[]), + Extension(name="torchaudio._torchaudio_decoder", sources=[]), + ] + ) return modules @@ -63,7 +62,7 @@ def get_ext_modules(): class CMakeBuild(build_ext): def run(self): try: - subprocess.check_output(['cmake', '--version']) + subprocess.check_output(["cmake", "--version"]) except OSError: raise RuntimeError("CMake is not available.") from None super().run() @@ -75,11 +74,10 @@ def build_extension(self, ext): # However, the following `cmake` command will build all of them at the same time, # so, we do not need to perform `cmake` twice. # Therefore we call `cmake` only for `torchaudio._torchaudio`. - if ext.name != 'torchaudio._torchaudio': + if ext.name != "torchaudio._torchaudio": return - extdir = os.path.abspath( - os.path.dirname(self.get_ext_fullpath(ext.name))) + extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) # required for auto-detection of auxiliary "native" libs if not extdir.endswith(os.path.sep): @@ -91,7 +89,7 @@ def build_extension(self, ext): f"-DCMAKE_BUILD_TYPE={cfg}", f"-DCMAKE_PREFIX_PATH={torch.utils.cmake_prefix_path}", f"-DCMAKE_INSTALL_PREFIX={extdir}", - '-DCMAKE_VERBOSE_MAKEFILE=ON', + "-DCMAKE_VERBOSE_MAKEFILE=ON", f"-DPython_INCLUDE_DIR={distutils.sysconfig.get_python_inc()}", f"-DBUILD_SOX:BOOL={'ON' if _BUILD_SOX else 'OFF'}", f"-DBUILD_KALDI:BOOL={'ON' if _BUILD_KALDI else 'OFF'}", @@ -102,22 +100,21 @@ def build_extension(self, ext): f"-DUSE_CUDA:BOOL={'ON' if _USE_CUDA else 'OFF'}", f"-DUSE_OPENMP:BOOL={'ON' if _USE_OPENMP else 'OFF'}", ] - build_args = [ - '--target', 'install' - ] + build_args = ["--target", "install"] # Pass CUDA architecture to cmake if _TORCH_CUDA_ARCH_LIST is not None: # Convert MAJOR.MINOR[+PTX] list to new style one # defined at https://cmake.org/cmake/help/latest/prop_tgt/CUDA_ARCHITECTURES.html - _arches = _TORCH_CUDA_ARCH_LIST.replace('.', '').replace(' ', ';').split(";") + _arches = _TORCH_CUDA_ARCH_LIST.replace(".", "").replace(" ", ";").split(";") _arches = [arch[:-4] if arch.endswith("+PTX") else f"{arch}-real" for arch in _arches] cmake_args += [f"-DCMAKE_CUDA_ARCHITECTURES={';'.join(_arches)}"] # Default to Ninja - if 'CMAKE_GENERATOR' not in os.environ or platform.system() == 'Windows': + if "CMAKE_GENERATOR" not in os.environ or platform.system() == "Windows": cmake_args += ["-GNinja"] - if platform.system() == 'Windows': + if platform.system() == "Windows": import sys + python_version = sys.version_info cmake_args += [ "-DCMAKE_C_COMPILER=cl", @@ -137,14 +134,12 @@ def build_extension(self, ext): if not os.path.exists(self.build_temp): os.makedirs(self.build_temp) - subprocess.check_call( - ["cmake", str(_ROOT_DIR)] + cmake_args, cwd=self.build_temp) - subprocess.check_call( - ["cmake", "--build", "."] + build_args, cwd=self.build_temp) + subprocess.check_call(["cmake", str(_ROOT_DIR)] + cmake_args, cwd=self.build_temp) + subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=self.build_temp) def get_ext_filename(self, fullname): ext_filename = super().get_ext_filename(fullname) - ext_filename_parts = ext_filename.split('.') + ext_filename_parts = ext_filename.split(".") without_abi = ext_filename_parts[:-2] + ext_filename_parts[-1:] - ext_filename = '.'.join(without_abi) + ext_filename = ".".join(without_abi) return ext_filename diff --git a/torchaudio/__init__.py b/torchaudio/__init__.py index d492dc7fe2..f171816d64 100644 --- a/torchaudio/__init__.py +++ b/torchaudio/__init__.py @@ -10,7 +10,6 @@ sox_effects, transforms, ) - from torchaudio.backend import ( list_audio_backends, get_audio_backend, @@ -23,16 +22,16 @@ pass __all__ = [ - 'compliance', - 'datasets', - 'functional', - 'models', - 'pipelines', - 'kaldi_io', - 'utils', - 'sox_effects', - 'transforms', - 'list_audio_backends', - 'get_audio_backend', - 'set_audio_backend', + "compliance", + "datasets", + "functional", + "models", + "pipelines", + "kaldi_io", + "utils", + "sox_effects", + "transforms", + "list_audio_backends", + "get_audio_backend", + "set_audio_backend", ] diff --git a/torchaudio/_extension.py b/torchaudio/_extension.py index 0309d3ce06..0ea3c6d858 100644 --- a/torchaudio/_extension.py +++ b/torchaudio/_extension.py @@ -5,12 +5,12 @@ import torch from torchaudio._internal import module_utils as _mod_utils # noqa: F401 -_LIB_DIR = Path(__file__).parent / 'lib' +_LIB_DIR = Path(__file__).parent / "lib" def _get_lib_path(lib: str): - suffix = 'pyd' if os.name == 'nt' else 'so' - path = _LIB_DIR / f'{lib}.{suffix}' + suffix = "pyd" if os.name == "nt" else "so" + path = _LIB_DIR / f"{lib}.{suffix}" return path @@ -26,11 +26,11 @@ def _load_lib(lib: str): def _init_extension(): - if not _mod_utils.is_module_available('torchaudio._torchaudio'): - warnings.warn('torchaudio C++ extension is not available.') + if not _mod_utils.is_module_available("torchaudio._torchaudio"): + warnings.warn("torchaudio C++ extension is not available.") return - _load_lib('libtorchaudio') + _load_lib("libtorchaudio") # This import is for initializing the methods registered via PyBind11 # This has to happen after the base library is loaded from torchaudio import _torchaudio # noqa diff --git a/torchaudio/_internal/module_utils.py b/torchaudio/_internal/module_utils.py index eecc7cf6d5..d540bbbc97 100644 --- a/torchaudio/_internal/module_utils.py +++ b/torchaudio/_internal/module_utils.py @@ -1,7 +1,7 @@ -import warnings import importlib.util -from typing import Optional +import warnings from functools import wraps +from typing import Optional import torch @@ -28,14 +28,17 @@ def requires_module(*modules: str): # fall through. If all the modules are available, no need to decorate def decorator(func): return func + else: - req = f'module: {missing[0]}' if len(missing) == 1 else f'modules: {missing}' + req = f"module: {missing[0]}" if len(missing) == 1 else f"modules: {missing}" def decorator(func): @wraps(func) def wrapped(*args, **kwargs): - raise RuntimeError(f'{func.__module__}.{func.__name__} requires {req}') + raise RuntimeError(f"{func.__module__}.{func.__name__} requires {req}") + return wrapped + return decorator @@ -46,42 +49,51 @@ def deprecated(direction: str, version: Optional[str] = None): direction (str): Migration steps to be given to users. version (str or int): The version when the object will be removed """ - def decorator(func): + def decorator(func): @wraps(func) def wrapped(*args, **kwargs): message = ( - f'{func.__module__}.{func.__name__} has been deprecated ' + f"{func.__module__}.{func.__name__} has been deprecated " f'and will be removed from {"future" if version is None else version} release. ' - f'{direction}') + f"{direction}" + ) warnings.warn(message, stacklevel=2) return func(*args, **kwargs) + return wrapped + return decorator def is_kaldi_available(): - return is_module_available('torchaudio._torchaudio') and torch.ops.torchaudio.is_kaldi_available() + return is_module_available("torchaudio._torchaudio") and torch.ops.torchaudio.is_kaldi_available() def requires_kaldi(): if is_kaldi_available(): + def decorator(func): return func + else: + def decorator(func): @wraps(func) def wrapped(*args, **kwargs): - raise RuntimeError(f'{func.__module__}.{func.__name__} requires kaldi') + raise RuntimeError(f"{func.__module__}.{func.__name__} requires kaldi") + return wrapped + return decorator def _check_soundfile_importable(): - if not is_module_available('soundfile'): + if not is_module_available("soundfile"): return False try: - import soundfile # noqa: F401 + import soundfile # noqa: F401 + return True except Exception: warnings.warn("Failed to import soundfile. 'soundfile' backend is not available.") @@ -97,29 +109,39 @@ def is_soundfile_available(): def requires_soundfile(): if is_soundfile_available(): + def decorator(func): return func + else: + def decorator(func): @wraps(func) def wrapped(*args, **kwargs): - raise RuntimeError(f'{func.__module__}.{func.__name__} requires soundfile') + raise RuntimeError(f"{func.__module__}.{func.__name__} requires soundfile") + return wrapped + return decorator def is_sox_available(): - return is_module_available('torchaudio._torchaudio') and torch.ops.torchaudio.is_sox_available() + return is_module_available("torchaudio._torchaudio") and torch.ops.torchaudio.is_sox_available() def requires_sox(): if is_sox_available(): + def decorator(func): return func + else: + def decorator(func): @wraps(func) def wrapped(*args, **kwargs): - raise RuntimeError(f'{func.__module__}.{func.__name__} requires sox') + raise RuntimeError(f"{func.__module__}.{func.__name__} requires sox") + return wrapped + return decorator diff --git a/torchaudio/backend/__init__.py b/torchaudio/backend/__init__.py index c3fdf0b439..1a8be0c355 100644 --- a/torchaudio/backend/__init__.py +++ b/torchaudio/backend/__init__.py @@ -1,4 +1,4 @@ -# flake8: noqa +# flake8: noqa from . import utils from .utils import ( list_audio_backends, diff --git a/torchaudio/backend/common.py b/torchaudio/backend/common.py index 67d5cdeb5d..e6fe0b81f3 100644 --- a/torchaudio/backend/common.py +++ b/torchaudio/backend/common.py @@ -26,13 +26,14 @@ class AudioMetaData: * ``HTK``: Single channel 16-bit PCM * ``UNKNOWN`` : None of above """ + def __init__( - self, - sample_rate: int, - num_frames: int, - num_channels: int, - bits_per_sample: int, - encoding: str, + self, + sample_rate: int, + num_frames: int, + num_channels: int, + bits_per_sample: int, + encoding: str, ): self.sample_rate = sample_rate self.num_frames = num_frames diff --git a/torchaudio/backend/no_backend.py b/torchaudio/backend/no_backend.py index 7160c89f69..5ebeb38708 100644 --- a/torchaudio/backend/no_backend.py +++ b/torchaudio/backend/no_backend.py @@ -4,19 +4,21 @@ from torch import Tensor -def load(filepath: Union[str, Path], - out: Optional[Tensor] = None, - normalization: Union[bool, float, Callable] = True, - channels_first: bool = True, - num_frames: int = 0, - offset: int = 0, - filetype: Optional[str] = None) -> Tuple[Tensor, int]: - raise RuntimeError('No audio I/O backend is available.') +def load( + filepath: Union[str, Path], + out: Optional[Tensor] = None, + normalization: Union[bool, float, Callable] = True, + channels_first: bool = True, + num_frames: int = 0, + offset: int = 0, + filetype: Optional[str] = None, +) -> Tuple[Tensor, int]: + raise RuntimeError("No audio I/O backend is available.") def save(filepath: str, src: Tensor, sample_rate: int, precision: int = 16, channels_first: bool = True) -> None: - raise RuntimeError('No audio I/O backend is available.') + raise RuntimeError("No audio I/O backend is available.") def info(filepath: str) -> None: - raise RuntimeError('No audio I/O backend is available.') + raise RuntimeError("No audio I/O backend is available.") diff --git a/torchaudio/backend/soundfile_backend.py b/torchaudio/backend/soundfile_backend.py index ebf739f229..8fe76c998e 100644 --- a/torchaudio/backend/soundfile_backend.py +++ b/torchaudio/backend/soundfile_backend.py @@ -1,9 +1,10 @@ """The new soundfile backend which will become default in 0.8.0 onward""" -from typing import Tuple, Optional import warnings +from typing import Tuple, Optional import torch from torchaudio._internal import module_utils as _mod_utils + from .common import AudioMetaData @@ -19,33 +20,33 @@ # The dict is inspired from # https://github.com/bastibe/python-soundfile/blob/744efb4b01abc72498a96b09115b42a4cabd85e4/soundfile.py#L66-L94 _SUBTYPE_TO_BITS_PER_SAMPLE = { - 'PCM_S8': 8, # Signed 8 bit data - 'PCM_16': 16, # Signed 16 bit data - 'PCM_24': 24, # Signed 24 bit data - 'PCM_32': 32, # Signed 32 bit data - 'PCM_U8': 8, # Unsigned 8 bit data (WAV and RAW only) - 'FLOAT': 32, # 32 bit float data - 'DOUBLE': 64, # 64 bit float data - 'ULAW': 8, # U-Law encoded. See https://en.wikipedia.org/wiki/G.711#Types - 'ALAW': 8, # A-Law encoded. See https://en.wikipedia.org/wiki/G.711#Types - 'IMA_ADPCM': 0, # IMA ADPCM. - 'MS_ADPCM': 0, # Microsoft ADPCM. - 'GSM610': 0, # GSM 6.10 encoding. (Wikipedia says 1.625 bit depth?? https://en.wikipedia.org/wiki/Full_Rate) - 'VOX_ADPCM': 0, # OKI / Dialogix ADPCM - 'G721_32': 0, # 32kbs G721 ADPCM encoding. - 'G723_24': 0, # 24kbs G723 ADPCM encoding. - 'G723_40': 0, # 40kbs G723 ADPCM encoding. - 'DWVW_12': 12, # 12 bit Delta Width Variable Word encoding. - 'DWVW_16': 16, # 16 bit Delta Width Variable Word encoding. - 'DWVW_24': 24, # 24 bit Delta Width Variable Word encoding. - 'DWVW_N': 0, # N bit Delta Width Variable Word encoding. - 'DPCM_8': 8, # 8 bit differential PCM (XI only) - 'DPCM_16': 16, # 16 bit differential PCM (XI only) - 'VORBIS': 0, # Xiph Vorbis encoding. (lossy) - 'ALAC_16': 16, # Apple Lossless Audio Codec (16 bit). - 'ALAC_20': 20, # Apple Lossless Audio Codec (20 bit). - 'ALAC_24': 24, # Apple Lossless Audio Codec (24 bit). - 'ALAC_32': 32, # Apple Lossless Audio Codec (32 bit). + "PCM_S8": 8, # Signed 8 bit data + "PCM_16": 16, # Signed 16 bit data + "PCM_24": 24, # Signed 24 bit data + "PCM_32": 32, # Signed 32 bit data + "PCM_U8": 8, # Unsigned 8 bit data (WAV and RAW only) + "FLOAT": 32, # 32 bit float data + "DOUBLE": 64, # 64 bit float data + "ULAW": 8, # U-Law encoded. See https://en.wikipedia.org/wiki/G.711#Types + "ALAW": 8, # A-Law encoded. See https://en.wikipedia.org/wiki/G.711#Types + "IMA_ADPCM": 0, # IMA ADPCM. + "MS_ADPCM": 0, # Microsoft ADPCM. + "GSM610": 0, # GSM 6.10 encoding. (Wikipedia says 1.625 bit depth?? https://en.wikipedia.org/wiki/Full_Rate) + "VOX_ADPCM": 0, # OKI / Dialogix ADPCM + "G721_32": 0, # 32kbs G721 ADPCM encoding. + "G723_24": 0, # 24kbs G723 ADPCM encoding. + "G723_40": 0, # 40kbs G723 ADPCM encoding. + "DWVW_12": 12, # 12 bit Delta Width Variable Word encoding. + "DWVW_16": 16, # 16 bit Delta Width Variable Word encoding. + "DWVW_24": 24, # 24 bit Delta Width Variable Word encoding. + "DWVW_N": 0, # N bit Delta Width Variable Word encoding. + "DPCM_8": 8, # 8 bit differential PCM (XI only) + "DPCM_16": 16, # 16 bit differential PCM (XI only) + "VORBIS": 0, # Xiph Vorbis encoding. (lossy) + "ALAC_16": 16, # Apple Lossless Audio Codec (16 bit). + "ALAC_20": 20, # Apple Lossless Audio Codec (20 bit). + "ALAC_24": 24, # Apple Lossless Audio Codec (24 bit). + "ALAC_32": 32, # Apple Lossless Audio Codec (32 bit). } @@ -61,23 +62,23 @@ def _get_bit_depth(subtype): _SUBTYPE_TO_ENCODING = { - 'PCM_S8': 'PCM_S', - 'PCM_16': 'PCM_S', - 'PCM_24': 'PCM_S', - 'PCM_32': 'PCM_S', - 'PCM_U8': 'PCM_U', - 'FLOAT': 'PCM_F', - 'DOUBLE': 'PCM_F', - 'ULAW': 'ULAW', - 'ALAW': 'ALAW', - 'VORBIS': 'VORBIS', + "PCM_S8": "PCM_S", + "PCM_16": "PCM_S", + "PCM_24": "PCM_S", + "PCM_32": "PCM_S", + "PCM_U8": "PCM_U", + "FLOAT": "PCM_F", + "DOUBLE": "PCM_F", + "ULAW": "ULAW", + "ALAW": "ALAW", + "VORBIS": "VORBIS", } def _get_encoding(format: str, subtype: str): - if format == 'FLAC': - return 'FLAC' - return _SUBTYPE_TO_ENCODING.get(subtype, 'UNKNOWN') + if format == "FLAC": + return "FLAC" + return _SUBTYPE_TO_ENCODING.get(subtype, "UNKNOWN") @_mod_utils.requires_soundfile() @@ -211,10 +212,7 @@ def load( return waveform, sample_rate -def _get_subtype_for_wav( - dtype: torch.dtype, - encoding: str, - bits_per_sample: int): +def _get_subtype_for_wav(dtype: torch.dtype, encoding: str, bits_per_sample: int): if not encoding: if not bits_per_sample: subtype = { @@ -271,11 +269,7 @@ def _get_subtype_for_sphere(encoding: str, bits_per_sample: int): raise ValueError(f"sph does not support {encoding}.") -def _get_subtype( - dtype: torch.dtype, - format: str, - encoding: str, - bits_per_sample: int): +def _get_subtype(dtype: torch.dtype, format: str, encoding: str, bits_per_sample: int): if format == "wav": return _get_subtype_for_wav(dtype, encoding, bits_per_sample) if format == "flac": @@ -288,8 +282,7 @@ def _get_subtype( return "PCM_S8" if bits_per_sample == 8 else f"PCM_{bits_per_sample}" if format in ("ogg", "vorbis"): if encoding or bits_per_sample: - raise ValueError( - "ogg/vorbis does not support encoding/bits_per_sample.") + raise ValueError("ogg/vorbis does not support encoding/bits_per_sample.") return "VORBIS" if format == "sph": return _get_subtype_for_sphere(encoding, bits_per_sample) @@ -407,9 +400,9 @@ def save( '`save` function of "soundfile" backend does not support "compression" parameter. ' "The argument is silently ignored." ) - if hasattr(filepath, 'write'): + if hasattr(filepath, "write"): if format is None: - raise RuntimeError('`format` is required when saving to file object.') + raise RuntimeError("`format` is required when saving to file object.") ext = format.lower() else: ext = str(filepath).split(".")[-1].lower() @@ -417,8 +410,10 @@ def save( if bits_per_sample not in (None, 8, 16, 24, 32, 64): raise ValueError("Invalid bits_per_sample.") if bits_per_sample == 24: - warnings.warn("Saving audio with 24 bits per sample might warp samples near -1. " - "Using 16 bits per sample might be able to avoid this.") + warnings.warn( + "Saving audio with 24 bits per sample might warp samples near -1. " + "Using 16 bits per sample might be able to avoid this." + ) subtype = _get_subtype(src.dtype, ext, encoding, bits_per_sample) # sph is a extension used in TED-LIUM but soundfile does not recognize it as NIST format, @@ -429,6 +424,4 @@ def save( if channels_first: src = src.t() - soundfile.write( - file=filepath, data=src, samplerate=sample_rate, subtype=subtype, format=format - ) + soundfile.write(file=filepath, data=src, samplerate=sample_rate, subtype=subtype, format=format) diff --git a/torchaudio/backend/sox_io_backend.py b/torchaudio/backend/sox_io_backend.py index cf0423c192..9e7a22f045 100644 --- a/torchaudio/backend/sox_io_backend.py +++ b/torchaudio/backend/sox_io_backend.py @@ -2,18 +2,18 @@ from typing import Tuple, Optional import torch +import torchaudio from torchaudio._internal import ( module_utils as _mod_utils, ) -import torchaudio from .common import AudioMetaData @_mod_utils.requires_sox() def info( - filepath: str, - format: Optional[str] = None, + filepath: str, + format: Optional[str] = None, ) -> AudioMetaData: """Get signal information of an audio file. @@ -46,7 +46,7 @@ def info( AudioMetaData: Metadata of the given audio. """ if not torch.jit.is_scripting(): - if hasattr(filepath, 'read'): + if hasattr(filepath, "read"): sinfo = torchaudio._torchaudio.get_info_fileobj(filepath, format) return AudioMetaData(*sinfo) filepath = os.fspath(filepath) @@ -56,12 +56,12 @@ def info( @_mod_utils.requires_sox() def load( - filepath: str, - frame_offset: int = 0, - num_frames: int = -1, - normalize: bool = True, - channels_first: bool = True, - format: Optional[str] = None, + filepath: str, + frame_offset: int = 0, + num_frames: int = -1, + normalize: bool = True, + channels_first: bool = True, + format: Optional[str] = None, ) -> Tuple[torch.Tensor, int]: """Load audio data from file. @@ -145,24 +145,26 @@ def load( `[channel, time]` else `[time, channel]`. """ if not torch.jit.is_scripting(): - if hasattr(filepath, 'read'): + if hasattr(filepath, "read"): return torchaudio._torchaudio.load_audio_fileobj( - filepath, frame_offset, num_frames, normalize, channels_first, format) + filepath, frame_offset, num_frames, normalize, channels_first, format + ) filepath = os.fspath(filepath) return torch.ops.torchaudio.sox_io_load_audio_file( - filepath, frame_offset, num_frames, normalize, channels_first, format) + filepath, frame_offset, num_frames, normalize, channels_first, format + ) @_mod_utils.requires_sox() def save( - filepath: str, - src: torch.Tensor, - sample_rate: int, - channels_first: bool = True, - compression: Optional[float] = None, - format: Optional[str] = None, - encoding: Optional[str] = None, - bits_per_sample: Optional[int] = None, + filepath: str, + src: torch.Tensor, + sample_rate: int, + channels_first: bool = True, + compression: Optional[float] = None, + format: Optional[str] = None, + encoding: Optional[str] = None, + bits_per_sample: Optional[int] = None, ): """Save audio data to file. @@ -309,11 +311,12 @@ def save( or ``libmp3lame`` etc. """ if not torch.jit.is_scripting(): - if hasattr(filepath, 'write'): + if hasattr(filepath, "write"): torchaudio._torchaudio.save_audio_fileobj( - filepath, src, sample_rate, channels_first, compression, - format, encoding, bits_per_sample) + filepath, src, sample_rate, channels_first, compression, format, encoding, bits_per_sample + ) return filepath = os.fspath(filepath) torch.ops.torchaudio.sox_io_save_audio_file( - filepath, src, sample_rate, channels_first, compression, format, encoding, bits_per_sample) + filepath, src, sample_rate, channels_first, compression, format, encoding, bits_per_sample + ) diff --git a/torchaudio/backend/utils.py b/torchaudio/backend/utils.py index ab670e0c03..bf665ff677 100644 --- a/torchaudio/backend/utils.py +++ b/torchaudio/backend/utils.py @@ -4,6 +4,7 @@ import torchaudio from torchaudio._internal import module_utils as _mod_utils + from . import ( no_backend, sox_io_backend, @@ -11,9 +12,9 @@ ) __all__ = [ - 'list_audio_backends', - 'get_audio_backend', - 'set_audio_backend', + "list_audio_backends", + "get_audio_backend", + "set_audio_backend", ] @@ -24,10 +25,10 @@ def list_audio_backends() -> List[str]: List[str]: The list of available backends. """ backends = [] - if _mod_utils.is_module_available('soundfile'): - backends.append('soundfile') + if _mod_utils.is_module_available("soundfile"): + backends.append("soundfile") if _mod_utils.is_sox_available(): - backends.append('sox_io') + backends.append("sox_io") return backends @@ -40,31 +41,29 @@ def set_audio_backend(backend: Optional[str]): of the system. If ``None`` is provided the current backend is unassigned. """ if backend is not None and backend not in list_audio_backends(): - raise RuntimeError( - f'Backend "{backend}" is not one of ' - f'available backends: {list_audio_backends()}.') + raise RuntimeError(f'Backend "{backend}" is not one of ' f"available backends: {list_audio_backends()}.") if backend is None: module = no_backend - elif backend == 'sox_io': + elif backend == "sox_io": module = sox_io_backend - elif backend == 'soundfile': + elif backend == "soundfile": module = soundfile_backend else: raise NotImplementedError(f'Unexpected backend "{backend}"') - for func in ['save', 'load', 'info']: + for func in ["save", "load", "info"]: setattr(torchaudio, func, getattr(module, func)) def _init_audio_backend(): backends = list_audio_backends() - if 'sox_io' in backends: - set_audio_backend('sox_io') - elif 'soundfile' in backends: - set_audio_backend('soundfile') + if "sox_io" in backends: + set_audio_backend("sox_io") + elif "soundfile" in backends: + set_audio_backend("soundfile") else: - warnings.warn('No audio backend is available.') + warnings.warn("No audio backend is available.") set_audio_backend(None) @@ -77,7 +76,7 @@ def get_audio_backend() -> Optional[str]: if torchaudio.load == no_backend.load: return None if torchaudio.load == sox_io_backend.load: - return 'sox_io' + return "sox_io" if torchaudio.load == soundfile_backend.load: - return 'soundfile' - raise ValueError('Unknown backend.') + return "soundfile" + raise ValueError("Unknown backend.") diff --git a/torchaudio/compliance/__init__.py b/torchaudio/compliance/__init__.py index 795065dc8d..65579b4f01 100644 --- a/torchaudio/compliance/__init__.py +++ b/torchaudio/compliance/__init__.py @@ -1,5 +1,5 @@ from . import kaldi __all__ = [ - 'kaldi', + "kaldi", ] diff --git a/torchaudio/compliance/kaldi.py b/torchaudio/compliance/kaldi.py index 60442f0d60..12092d90d1 100644 --- a/torchaudio/compliance/kaldi.py +++ b/torchaudio/compliance/kaldi.py @@ -1,22 +1,21 @@ +import math from typing import Tuple -import math import torch -from torch import Tensor - import torchaudio +from torch import Tensor __all__ = [ - 'get_mel_banks', - 'inverse_mel_scale', - 'inverse_mel_scale_scalar', - 'mel_scale', - 'mel_scale_scalar', - 'spectrogram', - 'fbank', - 'mfcc', - 'vtln_warp_freq', - 'vtln_warp_mel_freq', + "get_mel_banks", + "inverse_mel_scale", + "inverse_mel_scale_scalar", + "mel_scale", + "mel_scale_scalar", + "spectrogram", + "fbank", + "mfcc", + "vtln_warp_freq", + "vtln_warp_mel_freq", ] # numeric_limits::epsilon() 1.1920928955078125e-07 @@ -25,11 +24,11 @@ MILLISECONDS_TO_SECONDS = 0.001 # window types -HAMMING = 'hamming' -HANNING = 'hanning' -POVEY = 'povey' -RECTANGULAR = 'rectangular' -BLACKMAN = 'blackman' +HAMMING = "hamming" +HANNING = "hanning" +POVEY = "povey" +RECTANGULAR = "rectangular" +BLACKMAN = "blackman" WINDOWS = [HAMMING, HANNING, POVEY, RECTANGULAR, BLACKMAN] @@ -38,8 +37,7 @@ def _get_epsilon(device, dtype): def _next_power_of_2(x: int) -> int: - r"""Returns the smallest power of 2 that is greater than x - """ + r"""Returns the smallest power of 2 that is greater than x""" return 1 if x == 0 else 2 ** (x - 1).bit_length() @@ -85,14 +83,14 @@ def _get_strided(waveform: Tensor, window_size: int, window_shift: int, snip_edg return waveform.as_strided(sizes, strides) -def _feature_window_function(window_type: str, - window_size: int, - blackman_coeff: float, - device: torch.device, - dtype: int, - ) -> Tensor: - r"""Returns a window function with the given type and size - """ +def _feature_window_function( + window_type: str, + window_size: int, + blackman_coeff: float, + device: torch.device, + dtype: int, +) -> Tensor: + r"""Returns a window function with the given type and size""" if window_type == HANNING: return torch.hann_window(window_size, periodic=False, device=device, dtype=dtype) elif window_type == HAMMING: @@ -106,64 +104,67 @@ def _feature_window_function(window_type: str, a = 2 * math.pi / (window_size - 1) window_function = torch.arange(window_size, device=device, dtype=dtype) # can't use torch.blackman_window as they use different coefficients - return (blackman_coeff - 0.5 * torch.cos(a * window_function) + - (0.5 - blackman_coeff) * torch.cos(2 * a * window_function)).to(device=device, dtype=dtype) + return ( + blackman_coeff + - 0.5 * torch.cos(a * window_function) + + (0.5 - blackman_coeff) * torch.cos(2 * a * window_function) + ).to(device=device, dtype=dtype) else: - raise Exception('Invalid window type ' + window_type) + raise Exception("Invalid window type " + window_type) -def _get_log_energy(strided_input: Tensor, - epsilon: Tensor, - energy_floor: float) -> Tensor: - r"""Returns the log energy of size (m) for a strided_input (m,*) - """ +def _get_log_energy(strided_input: Tensor, epsilon: Tensor, energy_floor: float) -> Tensor: + r"""Returns the log energy of size (m) for a strided_input (m,*)""" device, dtype = strided_input.device, strided_input.dtype log_energy = torch.max(strided_input.pow(2).sum(1), epsilon).log() # size (m) if energy_floor == 0.0: return log_energy - return torch.max( - log_energy, torch.tensor(math.log(energy_floor), device=device, dtype=dtype)) - - -def _get_waveform_and_window_properties(waveform: Tensor, - channel: int, - sample_frequency: float, - frame_shift: float, - frame_length: float, - round_to_power_of_two: bool, - preemphasis_coefficient: float) -> Tuple[Tensor, int, int, int]: - r"""Gets the waveform and window properties - """ + return torch.max(log_energy, torch.tensor(math.log(energy_floor), device=device, dtype=dtype)) + + +def _get_waveform_and_window_properties( + waveform: Tensor, + channel: int, + sample_frequency: float, + frame_shift: float, + frame_length: float, + round_to_power_of_two: bool, + preemphasis_coefficient: float, +) -> Tuple[Tensor, int, int, int]: + r"""Gets the waveform and window properties""" channel = max(channel, 0) - assert channel < waveform.size(0), ('Invalid channel {} for size {}'.format(channel, waveform.size(0))) + assert channel < waveform.size(0), "Invalid channel {} for size {}".format(channel, waveform.size(0)) waveform = waveform[channel, :] # size (n) window_shift = int(sample_frequency * frame_shift * MILLISECONDS_TO_SECONDS) window_size = int(sample_frequency * frame_length * MILLISECONDS_TO_SECONDS) padded_window_size = _next_power_of_2(window_size) if round_to_power_of_two else window_size - assert 2 <= window_size <= len( - waveform), ('choose a window size {} that is [2, {}]' - .format(window_size, len(waveform))) - assert 0 < window_shift, '`window_shift` must be greater than 0' - assert padded_window_size % 2 == 0, 'the padded `window_size` must be divisible by two.' \ - ' use `round_to_power_of_two` or change `frame_length`' - assert 0. <= preemphasis_coefficient <= 1.0, '`preemphasis_coefficient` must be between [0,1]' - assert sample_frequency > 0, '`sample_frequency` must be greater than zero' + assert 2 <= window_size <= len(waveform), "choose a window size {} that is [2, {}]".format( + window_size, len(waveform) + ) + assert 0 < window_shift, "`window_shift` must be greater than 0" + assert padded_window_size % 2 == 0, ( + "the padded `window_size` must be divisible by two." " use `round_to_power_of_two` or change `frame_length`" + ) + assert 0.0 <= preemphasis_coefficient <= 1.0, "`preemphasis_coefficient` must be between [0,1]" + assert sample_frequency > 0, "`sample_frequency` must be greater than zero" return waveform, window_shift, window_size, padded_window_size -def _get_window(waveform: Tensor, - padded_window_size: int, - window_size: int, - window_shift: int, - window_type: str, - blackman_coeff: float, - snip_edges: bool, - raw_energy: bool, - energy_floor: float, - dither: float, - remove_dc_offset: bool, - preemphasis_coefficient: float) -> Tuple[Tensor, Tensor]: +def _get_window( + waveform: Tensor, + padded_window_size: int, + window_size: int, + window_shift: int, + window_type: str, + blackman_coeff: float, + snip_edges: bool, + raw_energy: bool, + energy_floor: float, + dither: float, + remove_dc_offset: bool, + preemphasis_coefficient: float, +) -> Tuple[Tensor, Tensor]: r"""Gets a window and its log energy Returns: @@ -193,20 +194,23 @@ def _get_window(waveform: Tensor, if preemphasis_coefficient != 0.0: # strided_input[i,j] -= preemphasis_coefficient * strided_input[i, max(0, j-1)] for all i,j - offset_strided_input = torch.nn.functional.pad( - strided_input.unsqueeze(0), (1, 0), mode='replicate').squeeze(0) # size (m, window_size + 1) + offset_strided_input = torch.nn.functional.pad(strided_input.unsqueeze(0), (1, 0), mode="replicate").squeeze( + 0 + ) # size (m, window_size + 1) strided_input = strided_input - preemphasis_coefficient * offset_strided_input[:, :-1] # Apply window_function to each row/frame - window_function = _feature_window_function( - window_type, window_size, blackman_coeff, device, dtype).unsqueeze(0) # size (1, window_size) + window_function = _feature_window_function(window_type, window_size, blackman_coeff, device, dtype).unsqueeze( + 0 + ) # size (1, window_size) strided_input = strided_input * window_function # size (m, window_size) # Pad columns with zero until we reach size (m, padded_window_size) if padded_window_size != window_size: padding_right = padded_window_size - window_size strided_input = torch.nn.functional.pad( - strided_input.unsqueeze(0), (0, padding_right), mode='constant', value=0).squeeze(0) + strided_input.unsqueeze(0), (0, padding_right), mode="constant", value=0 + ).squeeze(0) # Compute energy after window function (not the raw one) if not raw_energy: @@ -224,22 +228,24 @@ def _subtract_column_mean(tensor: Tensor, subtract_mean: bool) -> Tensor: return tensor -def spectrogram(waveform: Tensor, - blackman_coeff: float = 0.42, - channel: int = -1, - dither: float = 0.0, - energy_floor: float = 1.0, - frame_length: float = 25.0, - frame_shift: float = 10.0, - min_duration: float = 0.0, - preemphasis_coefficient: float = 0.97, - raw_energy: bool = True, - remove_dc_offset: bool = True, - round_to_power_of_two: bool = True, - sample_frequency: float = 16000.0, - snip_edges: bool = True, - subtract_mean: bool = False, - window_type: str = POVEY) -> Tensor: +def spectrogram( + waveform: Tensor, + blackman_coeff: float = 0.42, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + min_duration: float = 0.0, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + window_type: str = POVEY, +) -> Tensor: r"""Create a spectrogram from a raw audio signal. This matches the input/output of Kaldi's compute-spectrogram-feats. @@ -278,21 +284,33 @@ def spectrogram(waveform: Tensor, epsilon = _get_epsilon(device, dtype) waveform, window_shift, window_size, padded_window_size = _get_waveform_and_window_properties( - waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient) + waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient + ) if len(waveform) < min_duration * sample_frequency: # signal is too short return torch.empty(0) strided_input, signal_log_energy = _get_window( - waveform, padded_window_size, window_size, window_shift, window_type, blackman_coeff, - snip_edges, raw_energy, energy_floor, dither, remove_dc_offset, preemphasis_coefficient) + waveform, + padded_window_size, + window_size, + window_shift, + window_type, + blackman_coeff, + snip_edges, + raw_energy, + energy_floor, + dither, + remove_dc_offset, + preemphasis_coefficient, + ) # size (m, padded_window_size // 2 + 1, 2) fft = torch.fft.rfft(strided_input) # Convert the FFT into a power spectrum - power_spectrum = torch.max(fft.abs().pow(2.), epsilon).log() # size (m, padded_window_size // 2 + 1) + power_spectrum = torch.max(fft.abs().pow(2.0), epsilon).log() # size (m, padded_window_size // 2 + 1) power_spectrum[:, 0] = signal_log_energy power_spectrum = _subtract_column_mean(power_spectrum, subtract_mean) @@ -315,12 +333,14 @@ def mel_scale(freq: Tensor) -> Tensor: return 1127.0 * (1.0 + freq / 700.0).log() -def vtln_warp_freq(vtln_low_cutoff: float, - vtln_high_cutoff: float, - low_freq: float, - high_freq: float, - vtln_warp_factor: float, - freq: Tensor) -> Tensor: +def vtln_warp_freq( + vtln_low_cutoff: float, + vtln_high_cutoff: float, + low_freq: float, + high_freq: float, + vtln_warp_factor: float, + freq: Tensor, +) -> Tensor: r"""This computes a VTLN warping function that is not the same as HTK's one, but has similar inputs (this function has the advantage of never producing empty bins). @@ -357,8 +377,8 @@ def vtln_warp_freq(vtln_low_cutoff: float, Returns: Tensor: Freq after vtln warp """ - assert vtln_low_cutoff > low_freq, 'be sure to set the vtln_low option higher than low_freq' - assert vtln_high_cutoff < high_freq, 'be sure to set the vtln_high option lower than high_freq [or negative]' + assert vtln_low_cutoff > low_freq, "be sure to set the vtln_low option higher than low_freq" + assert vtln_high_cutoff < high_freq, "be sure to set the vtln_high option lower than high_freq [or negative]" l = vtln_low_cutoff * max(1.0, vtln_warp_factor) h = vtln_high_cutoff * min(1.0, vtln_warp_factor) scale = 1.0 / vtln_warp_factor @@ -388,11 +408,14 @@ def vtln_warp_freq(vtln_low_cutoff: float, return res -def vtln_warp_mel_freq(vtln_low_cutoff: float, - vtln_high_cutoff: float, - low_freq, high_freq: float, - vtln_warp_factor: float, - mel_freq: Tensor) -> Tensor: +def vtln_warp_mel_freq( + vtln_low_cutoff: float, + vtln_high_cutoff: float, + low_freq, + high_freq: float, + vtln_warp_factor: float, + mel_freq: Tensor, +) -> Tensor: r""" Args: vtln_low_cutoff (float): Lower frequency cutoffs for VTLN @@ -405,25 +428,30 @@ def vtln_warp_mel_freq(vtln_low_cutoff: float, Returns: Tensor: ``mel_freq`` after vtln warp """ - return mel_scale(vtln_warp_freq(vtln_low_cutoff, vtln_high_cutoff, low_freq, high_freq, - vtln_warp_factor, inverse_mel_scale(mel_freq))) - - -def get_mel_banks(num_bins: int, - window_length_padded: int, - sample_freq: float, - low_freq: float, - high_freq: float, - vtln_low: float, - vtln_high: float, - vtln_warp_factor: float) -> Tuple[Tensor, Tensor]: + return mel_scale( + vtln_warp_freq( + vtln_low_cutoff, vtln_high_cutoff, low_freq, high_freq, vtln_warp_factor, inverse_mel_scale(mel_freq) + ) + ) + + +def get_mel_banks( + num_bins: int, + window_length_padded: int, + sample_freq: float, + low_freq: float, + high_freq: float, + vtln_low: float, + vtln_high: float, + vtln_warp_factor: float, +) -> Tuple[Tensor, Tensor]: """ Returns: (Tensor, Tensor): The tuple consists of ``bins`` (which is melbank of size (``num_bins``, ``num_fft_bins``)) and ``center_freqs`` (which is center frequencies of bins of size (``num_bins``)). """ - assert num_bins > 3, 'Must have at least 3 mel bins' + assert num_bins > 3, "Must have at least 3 mel bins" assert window_length_padded % 2 == 0 num_fft_bins = window_length_padded / 2 nyquist = 0.5 * sample_freq @@ -431,8 +459,9 @@ def get_mel_banks(num_bins: int, if high_freq <= 0.0: high_freq += nyquist - assert (0.0 <= low_freq < nyquist) and (0.0 < high_freq <= nyquist) and (low_freq < high_freq), \ - ('Bad values in options: low-freq {} and high-freq {} vs. nyquist {}'.format(low_freq, high_freq, nyquist)) + assert ( + (0.0 <= low_freq < nyquist) and (0.0 < high_freq <= nyquist) and (low_freq < high_freq) + ), "Bad values in options: low-freq {} and high-freq {} vs. nyquist {}".format(low_freq, high_freq, nyquist) # fft-bin width [think of it as Nyquist-freq / half-window-length] fft_bin_width = sample_freq / window_length_padded @@ -446,10 +475,11 @@ def get_mel_banks(num_bins: int, if vtln_high < 0.0: vtln_high += nyquist - assert vtln_warp_factor == 1.0 or ((low_freq < vtln_low < high_freq) and - (0.0 < vtln_high < high_freq) and (vtln_low < vtln_high)), \ - ('Bad values in options: vtln-low {} and vtln-high {}, versus ' - 'low-freq {} and high-freq {}'.format(vtln_low, vtln_high, low_freq, high_freq)) + assert vtln_warp_factor == 1.0 or ( + (low_freq < vtln_low < high_freq) and (0.0 < vtln_high < high_freq) and (vtln_low < vtln_high) + ), "Bad values in options: vtln-low {} and vtln-high {}, versus " "low-freq {} and high-freq {}".format( + vtln_low, vtln_high, low_freq, high_freq + ) bin = torch.arange(num_bins).unsqueeze(1) left_mel = mel_low_freq + bin * mel_freq_delta # size(num_bins, 1) @@ -483,32 +513,34 @@ def get_mel_banks(num_bins: int, return bins, center_freqs -def fbank(waveform: Tensor, - blackman_coeff: float = 0.42, - channel: int = -1, - dither: float = 0.0, - energy_floor: float = 1.0, - frame_length: float = 25.0, - frame_shift: float = 10.0, - high_freq: float = 0.0, - htk_compat: bool = False, - low_freq: float = 20.0, - min_duration: float = 0.0, - num_mel_bins: int = 23, - preemphasis_coefficient: float = 0.97, - raw_energy: bool = True, - remove_dc_offset: bool = True, - round_to_power_of_two: bool = True, - sample_frequency: float = 16000.0, - snip_edges: bool = True, - subtract_mean: bool = False, - use_energy: bool = False, - use_log_fbank: bool = True, - use_power: bool = True, - vtln_high: float = -500.0, - vtln_low: float = 100.0, - vtln_warp: float = 1.0, - window_type: str = POVEY) -> Tensor: +def fbank( + waveform: Tensor, + blackman_coeff: float = 0.42, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + high_freq: float = 0.0, + htk_compat: bool = False, + low_freq: float = 20.0, + min_duration: float = 0.0, + num_mel_bins: int = 23, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + use_energy: bool = False, + use_log_fbank: bool = True, + use_power: bool = True, + vtln_high: float = -500.0, + vtln_low: float = 100.0, + vtln_warp: float = 1.0, + window_type: str = POVEY, +) -> Tensor: r"""Create a fbank from a raw audio signal. This matches the input/output of Kaldi's compute-fbank-feats. @@ -559,7 +591,8 @@ def fbank(waveform: Tensor, device, dtype = waveform.device, waveform.dtype waveform, window_shift, window_size, padded_window_size = _get_waveform_and_window_properties( - waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient) + waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient + ) if len(waveform) < min_duration * sample_frequency: # signal is too short @@ -567,21 +600,33 @@ def fbank(waveform: Tensor, # strided_input, size (m, padded_window_size) and signal_log_energy, size (m) strided_input, signal_log_energy = _get_window( - waveform, padded_window_size, window_size, window_shift, window_type, blackman_coeff, - snip_edges, raw_energy, energy_floor, dither, remove_dc_offset, preemphasis_coefficient) + waveform, + padded_window_size, + window_size, + window_shift, + window_type, + blackman_coeff, + snip_edges, + raw_energy, + energy_floor, + dither, + remove_dc_offset, + preemphasis_coefficient, + ) # size (m, padded_window_size // 2 + 1) spectrum = torch.fft.rfft(strided_input).abs() if use_power: - spectrum = spectrum.pow(2.) + spectrum = spectrum.pow(2.0) # size (num_mel_bins, padded_window_size // 2) - mel_energies, _ = get_mel_banks(num_mel_bins, padded_window_size, sample_frequency, - low_freq, high_freq, vtln_low, vtln_high, vtln_warp) + mel_energies, _ = get_mel_banks( + num_mel_bins, padded_window_size, sample_frequency, low_freq, high_freq, vtln_low, vtln_high, vtln_warp + ) mel_energies = mel_energies.to(device=device, dtype=dtype) # pad right column with zeros and add dimension, size (num_mel_bins, padded_window_size // 2 + 1) - mel_energies = torch.nn.functional.pad(mel_energies, (0, 1), mode='constant', value=0) + mel_energies = torch.nn.functional.pad(mel_energies, (0, 1), mode="constant", value=0) # sum with mel fiterbanks over the power spectrum, size (m, num_mel_bins) mel_energies = torch.mm(spectrum, mel_energies.T) @@ -605,7 +650,7 @@ def fbank(waveform: Tensor, def _get_dct_matrix(num_ceps: int, num_mel_bins: int) -> Tensor: # returns a dct matrix of size (num_mel_bins, num_ceps) # size (num_mel_bins, num_mel_bins) - dct_matrix = torchaudio.functional.create_dct(num_mel_bins, num_mel_bins, 'ortho') + dct_matrix = torchaudio.functional.create_dct(num_mel_bins, num_mel_bins, "ortho") # kaldi expects the first cepstral to be weighted sum of factor sqrt(1/num_mel_bins) # this would be the first column in the dct_matrix for torchaudio as it expects a # right multiply (which would be the first column of the kaldi's dct_matrix as kaldi @@ -624,32 +669,33 @@ def _get_lifter_coeffs(num_ceps: int, cepstral_lifter: float) -> Tensor: def mfcc( - waveform: Tensor, - blackman_coeff: float = 0.42, - cepstral_lifter: float = 22.0, - channel: int = -1, - dither: float = 0.0, - energy_floor: float = 1.0, - frame_length: float = 25.0, - frame_shift: float = 10.0, - high_freq: float = 0.0, - htk_compat: bool = False, - low_freq: float = 20.0, - num_ceps: int = 13, - min_duration: float = 0.0, - num_mel_bins: int = 23, - preemphasis_coefficient: float = 0.97, - raw_energy: bool = True, - remove_dc_offset: bool = True, - round_to_power_of_two: bool = True, - sample_frequency: float = 16000.0, - snip_edges: bool = True, - subtract_mean: bool = False, - use_energy: bool = False, - vtln_high: float = -500.0, - vtln_low: float = 100.0, - vtln_warp: float = 1.0, - window_type: str = POVEY) -> Tensor: + waveform: Tensor, + blackman_coeff: float = 0.42, + cepstral_lifter: float = 22.0, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + high_freq: float = 0.0, + htk_compat: bool = False, + low_freq: float = 20.0, + num_ceps: int = 13, + min_duration: float = 0.0, + num_mel_bins: int = 23, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + use_energy: bool = False, + vtln_high: float = -500.0, + vtln_low: float = 100.0, + vtln_warp: float = 1.0, + window_type: str = POVEY, +) -> Tensor: r"""Create a mfcc from a raw audio signal. This matches the input/output of Kaldi's compute-mfcc-feats. @@ -697,29 +743,48 @@ def mfcc( Tensor: A mfcc identical to what Kaldi would output. The shape is (m, ``num_ceps``) where m is calculated in _get_strided """ - assert num_ceps <= num_mel_bins, 'num_ceps cannot be larger than num_mel_bins: %d vs %d' % (num_ceps, num_mel_bins) + assert num_ceps <= num_mel_bins, "num_ceps cannot be larger than num_mel_bins: %d vs %d" % (num_ceps, num_mel_bins) device, dtype = waveform.device, waveform.dtype # The mel_energies should not be squared (use_power=True), not have mean subtracted # (subtract_mean=False), and use log (use_log_fbank=True). # size (m, num_mel_bins + use_energy) - feature = fbank(waveform=waveform, blackman_coeff=blackman_coeff, channel=channel, - dither=dither, energy_floor=energy_floor, frame_length=frame_length, - frame_shift=frame_shift, high_freq=high_freq, htk_compat=htk_compat, - low_freq=low_freq, min_duration=min_duration, num_mel_bins=num_mel_bins, - preemphasis_coefficient=preemphasis_coefficient, raw_energy=raw_energy, - remove_dc_offset=remove_dc_offset, round_to_power_of_two=round_to_power_of_two, - sample_frequency=sample_frequency, snip_edges=snip_edges, subtract_mean=False, - use_energy=use_energy, use_log_fbank=True, use_power=True, - vtln_high=vtln_high, vtln_low=vtln_low, vtln_warp=vtln_warp, window_type=window_type) + feature = fbank( + waveform=waveform, + blackman_coeff=blackman_coeff, + channel=channel, + dither=dither, + energy_floor=energy_floor, + frame_length=frame_length, + frame_shift=frame_shift, + high_freq=high_freq, + htk_compat=htk_compat, + low_freq=low_freq, + min_duration=min_duration, + num_mel_bins=num_mel_bins, + preemphasis_coefficient=preemphasis_coefficient, + raw_energy=raw_energy, + remove_dc_offset=remove_dc_offset, + round_to_power_of_two=round_to_power_of_two, + sample_frequency=sample_frequency, + snip_edges=snip_edges, + subtract_mean=False, + use_energy=use_energy, + use_log_fbank=True, + use_power=True, + vtln_high=vtln_high, + vtln_low=vtln_low, + vtln_warp=vtln_warp, + window_type=window_type, + ) if use_energy: # size (m) signal_log_energy = feature[:, num_mel_bins if htk_compat else 0] # offset is 0 if htk_compat==True else 1 mel_offset = int(not htk_compat) - feature = feature[:, mel_offset:(num_mel_bins + mel_offset)] + feature = feature[:, mel_offset : (num_mel_bins + mel_offset)] # size (num_mel_bins, num_ceps) dct_matrix = _get_dct_matrix(num_ceps, num_mel_bins).to(dtype=dtype, device=device) diff --git a/torchaudio/datasets/__init__.py b/torchaudio/datasets/__init__.py index 6b3efb5cd0..cf41d72d14 100644 --- a/torchaudio/datasets/__init__.py +++ b/torchaudio/datasets/__init__.py @@ -1,16 +1,16 @@ +from .cmuarctic import CMUARCTIC +from .cmudict import CMUDict from .commonvoice import COMMONVOICE -from .librispeech import LIBRISPEECH -from .speechcommands import SPEECHCOMMANDS -from .vctk import VCTK_092 from .dr_vctk import DR_VCTK from .gtzan import GTZAN -from .yesno import YESNO -from .ljspeech import LJSPEECH -from .cmuarctic import CMUARCTIC -from .cmudict import CMUDict from .librimix import LibriMix +from .librispeech import LIBRISPEECH from .libritts import LIBRITTS +from .ljspeech import LJSPEECH +from .speechcommands import SPEECHCOMMANDS from .tedlium import TEDLIUM +from .vctk import VCTK_092 +from .yesno import YESNO __all__ = [ diff --git a/torchaudio/datasets/cmuarctic.py b/torchaudio/datasets/cmuarctic.py index 49dbed338f..0b8e2f8440 100644 --- a/torchaudio/datasets/cmuarctic.py +++ b/torchaudio/datasets/cmuarctic.py @@ -1,12 +1,12 @@ -import os import csv +import os from pathlib import Path from typing import Tuple, Union import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -14,49 +14,28 @@ URL = "aew" FOLDER_IN_ARCHIVE = "ARCTIC" _CHECKSUMS = { - "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": - "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", - "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": - "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", - "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": - "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", - "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": - "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", - "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": - "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", - "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": - "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", - "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": - "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", - "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": - "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", - "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": - "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", - "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": - "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", - "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": - "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", - "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": - "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", - "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": - "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", - "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": - "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", - "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": - "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", - "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": - "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", - "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": - "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", - "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": - "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", + "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", + "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", + "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", + "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", + "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", + "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", + "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", + "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", + "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", + "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", + "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", + "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", + "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", + "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", + "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", + "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", + "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", + "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", } -def load_cmuarctic_item(line: str, - path: str, - folder_audio: str, - ext_audio: str) -> Tuple[Tensor, int, str, str]: +def load_cmuarctic_item(line: str, path: str, folder_audio: str, ext_audio: str) -> Tuple[Tensor, int, str, str]: utterance_id, transcript = line[0].strip().split(" ", 2)[1:] @@ -68,12 +47,7 @@ def load_cmuarctic_item(line: str, # Load audio waveform, sample_rate = torchaudio.load(file_audio) - return ( - waveform, - sample_rate, - transcript, - utterance_id.split("_")[1] - ) + return (waveform, sample_rate, transcript, utterance_id.split("_")[1]) class CMUARCTIC(Dataset): @@ -98,11 +72,9 @@ class CMUARCTIC(Dataset): _ext_audio = ".wav" _folder_audio = "wav" - def __init__(self, - root: Union[str, Path], - url: str = URL, - folder_in_archive: str = FOLDER_IN_ARCHIVE, - download: bool = False) -> None: + def __init__( + self, root: Union[str, Path], url: str = URL, folder_in_archive: str = FOLDER_IN_ARCHIVE, download: bool = False + ) -> None: if url in [ "aew", @@ -122,7 +94,7 @@ def __init__(self, "rms", "rxr", "slp", - "slt" + "slt", ]: url = "cmu_us_" + url + "_arctic" diff --git a/torchaudio/datasets/cmudict.py b/torchaudio/datasets/cmudict.py index df0a7ce51c..598538da9c 100644 --- a/torchaudio/datasets/cmudict.py +++ b/torchaudio/datasets/cmudict.py @@ -3,83 +3,83 @@ from pathlib import Path from typing import Iterable, Tuple, Union, List -from torch.utils.data import Dataset from torch.hub import download_url_to_file +from torch.utils.data import Dataset _CHECKSUMS = { - "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": - "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", - "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": - "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", } -_PUNCTUATIONS = set([ - "!EXCLAMATION-POINT", - "\"CLOSE-QUOTE", - "\"DOUBLE-QUOTE", - "\"END-OF-QUOTE", - "\"END-QUOTE", - "\"IN-QUOTES", - "\"QUOTE", - "\"UNQUOTE", - "#HASH-MARK", - "#POUND-SIGN", - "#SHARP-SIGN", - "%PERCENT", - "&ERSAND", - "'END-INNER-QUOTE", - "'END-QUOTE", - "'INNER-QUOTE", - "'QUOTE", - "'SINGLE-QUOTE", - "(BEGIN-PARENS", - "(IN-PARENTHESES", - "(LEFT-PAREN", - "(OPEN-PARENTHESES", - "(PAREN", - "(PARENS", - "(PARENTHESES", - ")CLOSE-PAREN", - ")CLOSE-PARENTHESES", - ")END-PAREN", - ")END-PARENS", - ")END-PARENTHESES", - ")END-THE-PAREN", - ")PAREN", - ")PARENS", - ")RIGHT-PAREN", - ")UN-PARENTHESES", - "+PLUS", - ",COMMA", - "--DASH", - "-DASH", - "-HYPHEN", - "...ELLIPSIS", - ".DECIMAL", - ".DOT", - ".FULL-STOP", - ".PERIOD", - ".POINT", - "/SLASH", - ":COLON", - ";SEMI-COLON", - ";SEMI-COLON(1)", - "?QUESTION-MARK", - "{BRACE", - "{LEFT-BRACE", - "{OPEN-BRACE", - "}CLOSE-BRACE", - "}RIGHT-BRACE", -]) +_PUNCTUATIONS = set( + [ + "!EXCLAMATION-POINT", + '"CLOSE-QUOTE', + '"DOUBLE-QUOTE', + '"END-OF-QUOTE', + '"END-QUOTE', + '"IN-QUOTES', + '"QUOTE', + '"UNQUOTE', + "#HASH-MARK", + "#POUND-SIGN", + "#SHARP-SIGN", + "%PERCENT", + "&ERSAND", + "'END-INNER-QUOTE", + "'END-QUOTE", + "'INNER-QUOTE", + "'QUOTE", + "'SINGLE-QUOTE", + "(BEGIN-PARENS", + "(IN-PARENTHESES", + "(LEFT-PAREN", + "(OPEN-PARENTHESES", + "(PAREN", + "(PARENS", + "(PARENTHESES", + ")CLOSE-PAREN", + ")CLOSE-PARENTHESES", + ")END-PAREN", + ")END-PARENS", + ")END-PARENTHESES", + ")END-THE-PAREN", + ")PAREN", + ")PARENS", + ")RIGHT-PAREN", + ")UN-PARENTHESES", + "+PLUS", + ",COMMA", + "--DASH", + "-DASH", + "-HYPHEN", + "...ELLIPSIS", + ".DECIMAL", + ".DOT", + ".FULL-STOP", + ".PERIOD", + ".POINT", + "/SLASH", + ":COLON", + ";SEMI-COLON", + ";SEMI-COLON(1)", + "?QUESTION-MARK", + "{BRACE", + "{LEFT-BRACE", + "{OPEN-BRACE", + "}CLOSE-BRACE", + "}RIGHT-BRACE", + ] +) def _parse_dictionary(lines: Iterable[str], exclude_punctuations: bool) -> List[str]: - _alt_re = re.compile(r'\([0-9]+\)') + _alt_re = re.compile(r"\([0-9]+\)") cmudict: List[Tuple[str, List[str]]] = list() for line in lines: - if not line or line.startswith(';;;'): # ignore comments + if not line or line.startswith(";;;"): # ignore comments continue - word, phones = line.strip().split(' ') + word, phones = line.strip().split(" ") if word in _PUNCTUATIONS: if exclude_punctuations: continue @@ -96,7 +96,7 @@ def _parse_dictionary(lines: Iterable[str], exclude_punctuations: bool) -> List[ # if a word have multiple pronunciations, there will be (number) appended to it # for example, DATAPOINTS and DATAPOINTS(1), # the regular expression `_alt_re` removes the '(1)' and change the word DATAPOINTS(1) to DATAPOINTS - word = re.sub(_alt_re, '', word) + word = re.sub(_alt_re, "", word) phones = phones.split(" ") cmudict.append((word, phones)) @@ -121,44 +121,46 @@ class CMUDict(Dataset): (default: ``"http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols"``) """ - def __init__(self, - root: Union[str, Path], - exclude_punctuations: bool = True, - *, - download: bool = False, - url: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b", - url_symbols: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols", - ) -> None: + def __init__( + self, + root: Union[str, Path], + exclude_punctuations: bool = True, + *, + download: bool = False, + url: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b", + url_symbols: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols", + ) -> None: self.exclude_punctuations = exclude_punctuations self._root_path = Path(root) if not os.path.isdir(self._root_path): - raise RuntimeError(f'The root directory does not exist; {root}') + raise RuntimeError(f"The root directory does not exist; {root}") dict_file = self._root_path / os.path.basename(url) symbol_file = self._root_path / os.path.basename(url_symbols) if not os.path.exists(dict_file): if not download: raise RuntimeError( - 'The dictionary file is not found in the following location. ' - f'Set `download=True` to download it. {dict_file}') + "The dictionary file is not found in the following location. " + f"Set `download=True` to download it. {dict_file}" + ) checksum = _CHECKSUMS.get(url, None) download_url_to_file(url, dict_file, checksum) if not os.path.exists(symbol_file): if not download: raise RuntimeError( - 'The symbol file is not found in the following location. ' - f'Set `download=True` to download it. {symbol_file}') + "The symbol file is not found in the following location. " + f"Set `download=True` to download it. {symbol_file}" + ) checksum = _CHECKSUMS.get(url_symbols, None) download_url_to_file(url_symbols, symbol_file, checksum) with open(symbol_file, "r") as text: self._symbols = [line.strip() for line in text.readlines()] - with open(dict_file, "r", encoding='latin-1') as text: - self._dictionary = _parse_dictionary( - text.readlines(), exclude_punctuations=self.exclude_punctuations) + with open(dict_file, "r", encoding="latin-1") as text: + self._dictionary = _parse_dictionary(text.readlines(), exclude_punctuations=self.exclude_punctuations) def __getitem__(self, n: int) -> Tuple[str, List[str]]: """Load the n-th sample from the dataset. @@ -177,6 +179,5 @@ def __len__(self) -> int: @property def symbols(self) -> List[str]: - """list[str]: A list of phonemes symbols, such as `AA`, `AE`, `AH`. - """ + """list[str]: A list of phonemes symbols, such as `AA`, `AE`, `AH`.""" return self._symbols.copy() diff --git a/torchaudio/datasets/commonvoice.py b/torchaudio/datasets/commonvoice.py index 20f9234f89..8218bdf54e 100644 --- a/torchaudio/datasets/commonvoice.py +++ b/torchaudio/datasets/commonvoice.py @@ -3,17 +3,14 @@ from pathlib import Path from typing import List, Dict, Tuple, Union +import torchaudio from torch import Tensor from torch.utils.data import Dataset -import torchaudio - -def load_commonvoice_item(line: List[str], - header: List[str], - path: str, - folder_audio: str, - ext_audio: str) -> Tuple[Tensor, int, Dict[str, str]]: +def load_commonvoice_item( + line: List[str], header: List[str], path: str, folder_audio: str, ext_audio: str +) -> Tuple[Tensor, int, Dict[str, str]]: # Each line as the following data: # client_id, path, sentence, up_votes, down_votes, age, gender, accent @@ -45,9 +42,7 @@ class COMMONVOICE(Dataset): _ext_audio = ".mp3" _folder_audio = "clips" - def __init__(self, - root: Union[str, Path], - tsv: str = "train.tsv") -> None: + def __init__(self, root: Union[str, Path], tsv: str = "train.tsv") -> None: # Get string representation of 'root' in case Path object is passed self._path = os.fspath(root) diff --git a/torchaudio/datasets/dr_vctk.py b/torchaudio/datasets/dr_vctk.py index 8bd5409e00..bd48f1a273 100644 --- a/torchaudio/datasets/dr_vctk.py +++ b/torchaudio/datasets/dr_vctk.py @@ -1,11 +1,10 @@ from pathlib import Path from typing import Dict, Tuple, Union +import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file - -import torchaudio +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) diff --git a/torchaudio/datasets/gtzan.py b/torchaudio/datasets/gtzan.py index d98d827bfb..b7b62c54f5 100644 --- a/torchaudio/datasets/gtzan.py +++ b/torchaudio/datasets/gtzan.py @@ -4,8 +4,8 @@ import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -1039,8 +1039,7 @@ def __init__( self.subset = subset assert subset is None or subset in ["training", "validation", "testing"], ( - "When `subset` not None, it must take a value from " - + "{'training', 'validation', 'testing'}." + "When `subset` not None, it must take a value from " + "{'training', 'validation', 'testing'}." ) archive = os.path.basename(url) @@ -1055,9 +1054,7 @@ def __init__( extract_archive(archive) if not os.path.isdir(self._path): - raise RuntimeError( - "Dataset not found. Please use `download=True` to download it." - ) + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") if self.subset is None: # Check every subdirectory under dataset root diff --git a/torchaudio/datasets/librimix.py b/torchaudio/datasets/librimix.py index ab3b3877dc..c8b38324e9 100644 --- a/torchaudio/datasets/librimix.py +++ b/torchaudio/datasets/librimix.py @@ -2,9 +2,8 @@ from typing import Union, Tuple, List import torch -from torch.utils.data import Dataset - import torchaudio +from torch.utils.data import Dataset SampleType = Tuple[int, torch.Tensor, List[torch.Tensor]] @@ -30,6 +29,7 @@ class LibriMix(Dataset): Note: The LibriMix dataset needs to be manually generated. Please check https://github.com/JorisCos/LibriMix """ + def __init__( self, root: Union[str, Path], @@ -44,9 +44,7 @@ def __init__( elif sample_rate == 16000: self.root = self.root / "wav16k/min" / subset else: - raise ValueError( - f"Unsupported sample rate. Found {sample_rate}." - ) + raise ValueError(f"Unsupported sample rate. Found {sample_rate}.") self.sample_rate = sample_rate self.task = task self.mix_dir = (self.root / f"mix_{task.split('_')[1]}").resolve() @@ -70,9 +68,7 @@ def _load_sample(self, filename) -> SampleType: for i, dir_ in enumerate(self.src_dirs): src = self._load_audio(str(dir_ / filename)) if mixed.shape != src.shape: - raise ValueError( - f"Different waveform shapes. mixed: {mixed.shape}, src[{i}]: {src.shape}" - ) + raise ValueError(f"Different waveform shapes. mixed: {mixed.shape}, src[{i}]: {src.shape}") srcs.append(src) return self.sample_rate, mixed, srcs diff --git a/torchaudio/datasets/librispeech.py b/torchaudio/datasets/librispeech.py index 4aea9fac5b..74f1521c76 100644 --- a/torchaudio/datasets/librispeech.py +++ b/torchaudio/datasets/librispeech.py @@ -1,12 +1,11 @@ import os -from typing import Tuple, Union from pathlib import Path +from typing import Tuple, Union import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file - +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -14,27 +13,19 @@ URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriSpeech" _CHECKSUMS = { - "http://www.openslr.org/resources/12/dev-clean.tar.gz": - "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", - "http://www.openslr.org/resources/12/dev-other.tar.gz": - "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", - "http://www.openslr.org/resources/12/test-clean.tar.gz": - "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", - "http://www.openslr.org/resources/12/test-other.tar.gz": - "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", - "http://www.openslr.org/resources/12/train-clean-100.tar.gz": - "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", - "http://www.openslr.org/resources/12/train-clean-360.tar.gz": - "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", - "http://www.openslr.org/resources/12/train-other-500.tar.gz": - "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2" + "http://www.openslr.org/resources/12/dev-clean.tar.gz": "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", + "http://www.openslr.org/resources/12/dev-other.tar.gz": "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", + "http://www.openslr.org/resources/12/test-clean.tar.gz": "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", + "http://www.openslr.org/resources/12/test-other.tar.gz": "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", + "http://www.openslr.org/resources/12/train-clean-100.tar.gz": "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", + "http://www.openslr.org/resources/12/train-clean-360.tar.gz": "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", + "http://www.openslr.org/resources/12/train-other-500.tar.gz": "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2", } -def load_librispeech_item(fileid: str, - path: str, - ext_audio: str, - ext_txt: str) -> Tuple[Tensor, int, str, int, int, int]: +def load_librispeech_item( + fileid: str, path: str, ext_audio: str, ext_txt: str +) -> Tuple[Tensor, int, str, int, int, int]: speaker_id, chapter_id, utterance_id = fileid.split("-") file_text = speaker_id + "-" + chapter_id + ext_txt @@ -86,11 +77,9 @@ class LIBRISPEECH(Dataset): _ext_txt = ".trans.txt" _ext_audio = ".flac" - def __init__(self, - root: Union[str, Path], - url: str = URL, - folder_in_archive: str = FOLDER_IN_ARCHIVE, - download: bool = False) -> None: + def __init__( + self, root: Union[str, Path], url: str = URL, folder_in_archive: str = FOLDER_IN_ARCHIVE, download: bool = False + ) -> None: if url in [ "dev-clean", @@ -125,7 +114,7 @@ def __init__(self, download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) - self._walker = sorted(str(p.stem) for p in Path(self._path).glob('*/*/*' + self._ext_audio)) + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*/*/*" + self._ext_audio)) def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, int, int]: """Load the n-th sample from the dataset. diff --git a/torchaudio/datasets/libritts.py b/torchaudio/datasets/libritts.py index 6913383ba0..c6c8d1df7b 100644 --- a/torchaudio/datasets/libritts.py +++ b/torchaudio/datasets/libritts.py @@ -1,11 +1,11 @@ import os -from typing import Tuple, Union from pathlib import Path +from typing import Tuple, Union import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -13,20 +13,13 @@ URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriTTS" _CHECKSUMS = { - "http://www.openslr.org/resources/60/dev-clean.tar.gz": - "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", - "http://www.openslr.org/resources/60/dev-other.tar.gz": - "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", - "http://www.openslr.org/resources/60/test-clean.tar.gz": - "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", - "http://www.openslr.org/resources/60/test-other.tar.gz": - "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", - "http://www.openslr.org/resources/60/train-clean-100.tar.gz": - "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", - "http://www.openslr.org/resources/60/train-clean-360.tar.gz": - "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", - "http://www.openslr.org/resources/60/train-other-500.tar.gz": - "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", + "http://www.openslr.org/resources/60/dev-clean.tar.gz": "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", + "http://www.openslr.org/resources/60/dev-other.tar.gz": "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", + "http://www.openslr.org/resources/60/test-clean.tar.gz": "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", + "http://www.openslr.org/resources/60/test-other.tar.gz": "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", + "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", + "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", + "http://www.openslr.org/resources/60/train-other-500.tar.gz": "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", } @@ -132,7 +125,7 @@ def __init__( download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) - self._walker = sorted(str(p.stem) for p in Path(self._path).glob('*/*/*' + self._ext_audio)) + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*/*/*" + self._ext_audio)) def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str, int, int, str]: """Load the n-th sample from the dataset. diff --git a/torchaudio/datasets/ljspeech.py b/torchaudio/datasets/ljspeech.py index e2fd0f0cb9..11ce44d4d8 100644 --- a/torchaudio/datasets/ljspeech.py +++ b/torchaudio/datasets/ljspeech.py @@ -1,13 +1,13 @@ -import os import csv -from typing import Tuple, Union +import os from pathlib import Path +from typing import Tuple, Union import torchaudio -from torchaudio.datasets.utils import extract_archive from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file +from torch.utils.data import Dataset +from torchaudio.datasets.utils import extract_archive _RELEASE_CONFIGS = { @@ -32,11 +32,13 @@ class LJSPEECH(Dataset): Whether to download the dataset if it is not found at root path. (default: ``False``). """ - def __init__(self, - root: Union[str, Path], - url: str = _RELEASE_CONFIGS["release1"]["url"], - folder_in_archive: str = _RELEASE_CONFIGS["release1"]["folder_in_archive"], - download: bool = False) -> None: + def __init__( + self, + root: Union[str, Path], + url: str = _RELEASE_CONFIGS["release1"]["url"], + folder_in_archive: str = _RELEASE_CONFIGS["release1"]["folder_in_archive"], + download: bool = False, + ) -> None: self._parse_filesystem(root, url, folder_in_archive, download) @@ -50,7 +52,7 @@ def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, downloa folder_in_archive = basename / folder_in_archive self._path = root / folder_in_archive - self._metadata_path = root / basename / 'metadata.csv' + self._metadata_path = root / basename / "metadata.csv" if download: if not os.path.isdir(self._path): @@ -59,7 +61,7 @@ def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, downloa download_url_to_file(url, archive, hash_prefix=checksum) extract_archive(archive) - with open(self._metadata_path, "r", newline='') as metadata: + with open(self._metadata_path, "r", newline="") as metadata: flist = csv.reader(metadata, delimiter="|", quoting=csv.QUOTE_NONE) self._flist = list(flist) diff --git a/torchaudio/datasets/speechcommands.py b/torchaudio/datasets/speechcommands.py index 47522cbdef..dd5fe82c17 100644 --- a/torchaudio/datasets/speechcommands.py +++ b/torchaudio/datasets/speechcommands.py @@ -1,12 +1,11 @@ import os -from typing import Tuple, Optional, Union from pathlib import Path +from typing import Tuple, Optional, Union import torchaudio -from torch.utils.data import Dataset from torch import Tensor from torch.hub import download_url_to_file - +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -16,10 +15,8 @@ HASH_DIVIDER = "_nohash_" EXCEPT_FOLDER = "_background_noise_" _CHECKSUMS = { - "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz": - "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", - "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.02.tar.gz": - "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", + "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz": "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", + "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.02.tar.gz": "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", } @@ -75,17 +72,17 @@ class SPEECHCOMMANDS(Dataset): original paper can be found `here `_. (Default: ``None``) """ - def __init__(self, - root: Union[str, Path], - url: str = URL, - folder_in_archive: str = FOLDER_IN_ARCHIVE, - download: bool = False, - subset: Optional[str] = None, - ) -> None: + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + subset: Optional[str] = None, + ) -> None: assert subset is None or subset in ["training", "validation", "testing"], ( - "When `subset` not None, it must take a value from " - + "{'training', 'validation', 'testing'}." + "When `subset` not None, it must take a value from " + "{'training', 'validation', 'testing'}." ) if url in [ @@ -121,15 +118,14 @@ def __init__(self, self._walker = _load_list(self._path, "testing_list.txt") elif subset == "training": excludes = set(_load_list(self._path, "validation_list.txt", "testing_list.txt")) - walker = sorted(str(p) for p in Path(self._path).glob('*/*.wav')) + walker = sorted(str(p) for p in Path(self._path).glob("*/*.wav")) self._walker = [ - w for w in walker - if HASH_DIVIDER in w - and EXCEPT_FOLDER not in w - and os.path.normpath(w) not in excludes + w + for w in walker + if HASH_DIVIDER in w and EXCEPT_FOLDER not in w and os.path.normpath(w) not in excludes ] else: - walker = sorted(str(p) for p in Path(self._path).glob('*/*.wav')) + walker = sorted(str(p) for p in Path(self._path).glob("*/*.wav")) self._walker = [w for w in walker if HASH_DIVIDER in w and EXCEPT_FOLDER not in w] def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str, int]: diff --git a/torchaudio/datasets/tedlium.py b/torchaudio/datasets/tedlium.py index 5f4dbb499d..7a496401b7 100644 --- a/torchaudio/datasets/tedlium.py +++ b/torchaudio/datasets/tedlium.py @@ -1,12 +1,11 @@ import os -from typing import Tuple, Union from pathlib import Path +from typing import Tuple, Union import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file - +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -58,13 +57,14 @@ class TEDLIUM(Dataset): Whether to download the dataset if it is not found at root path. (default: ``False``). audio_ext (str, optional): extension for audio file (default: ``"audio_ext"``) """ + def __init__( self, root: Union[str, Path], release: str = "release1", subset: str = None, download: bool = False, - audio_ext: str = ".sph" + audio_ext: str = ".sph", ) -> None: self._ext_audio = audio_ext if release in _RELEASE_CONFIGS.keys(): @@ -75,14 +75,16 @@ def __init__( # Raise warning raise RuntimeError( "The release {} does not match any of the supported tedlium releases{} ".format( - release, _RELEASE_CONFIGS.keys(), + release, + _RELEASE_CONFIGS.keys(), ) ) if subset not in _RELEASE_CONFIGS[release]["supported_subsets"]: # Raise warning raise RuntimeError( "The subset {} does not match any of the supported tedlium subsets{} ".format( - subset, _RELEASE_CONFIGS[release]["supported_subsets"], + subset, + _RELEASE_CONFIGS[release]["supported_subsets"], ) ) diff --git a/torchaudio/datasets/utils.py b/torchaudio/datasets/utils.py index ea75f642fd..e9976c4eee 100644 --- a/torchaudio/datasets/utils.py +++ b/torchaudio/datasets/utils.py @@ -4,17 +4,16 @@ import tarfile import urllib import urllib.request -import zipfile import warnings +import zipfile from typing import Any, Iterable, List, Optional from torch.utils.model_zoo import tqdm -def stream_url(url: str, - start_byte: Optional[int] = None, - block_size: int = 32 * 1024, - progress_bar: bool = True) -> Iterable: +def stream_url( + url: str, start_byte: Optional[int] = None, block_size: int = 32 * 1024, progress_bar: bool = True +) -> Iterable: """Stream url by chunk Args: @@ -36,11 +35,11 @@ def stream_url(url: str, req.headers["Range"] = "bytes={}-".format(start_byte) with urllib.request.urlopen(req) as upointer, tqdm( - unit="B", - unit_scale=True, - unit_divisor=1024, - total=url_size, - disable=not progress_bar, + unit="B", + unit_scale=True, + unit_divisor=1024, + total=url_size, + disable=not progress_bar, ) as pbar: num_bytes = 0 @@ -53,13 +52,15 @@ def stream_url(url: str, pbar.update(len(chunk)) -def download_url(url: str, - download_folder: str, - filename: Optional[str] = None, - hash_value: Optional[str] = None, - hash_type: str = "sha256", - progress_bar: bool = True, - resume: bool = False) -> None: +def download_url( + url: str, + download_folder: str, + filename: Optional[str] = None, + hash_value: Optional[str] = None, + hash_type: str = "sha256", + progress_bar: bool = True, + resume: bool = False, +) -> None: """Download file to disk. Args: @@ -84,9 +85,7 @@ def download_url(url: str, local_size: Optional[int] = os.path.getsize(filepath) elif not resume and os.path.exists(filepath): - raise RuntimeError( - "{} already exists. Delete the file manually and retry.".format(filepath) - ) + raise RuntimeError("{} already exists. Delete the file manually and retry.".format(filepath)) else: mode = "wb" local_size = None @@ -95,11 +94,7 @@ def download_url(url: str, with open(filepath, "rb") as file_obj: if validate_file(file_obj, hash_value, hash_type): return - raise RuntimeError( - "The hash of {} does not match. Delete the file manually and retry.".format( - filepath - ) - ) + raise RuntimeError("The hash of {} does not match. Delete the file manually and retry.".format(filepath)) with open(filepath, mode) as fpointer: for chunk in stream_url(url, start_byte=local_size, progress_bar=progress_bar): @@ -107,11 +102,7 @@ def download_url(url: str, with open(filepath, "rb") as file_obj: if hash_value and not validate_file(file_obj, hash_value, hash_type): - raise RuntimeError( - "The hash of {} does not match. Delete the file manually and retry.".format( - filepath - ) - ) + raise RuntimeError("The hash of {} does not match. Delete the file manually and retry.".format(filepath)) def validate_file(file_obj: Any, hash_value: str, hash_type: str = "sha256") -> bool: diff --git a/torchaudio/datasets/vctk.py b/torchaudio/datasets/vctk.py index 43f2a93b87..6442f2c9ba 100644 --- a/torchaudio/datasets/vctk.py +++ b/torchaudio/datasets/vctk.py @@ -1,20 +1,17 @@ import os from typing import Tuple +import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file - - -import torchaudio +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) URL = "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip" _CHECKSUMS = { - "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": - "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" + "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" } @@ -41,17 +38,15 @@ class VCTK_092(Dataset): """ def __init__( - self, - root: str, - mic_id: str = "mic2", - download: bool = False, - url: str = URL, - audio_ext=".flac", + self, + root: str, + mic_id: str = "mic2", + download: bool = False, + url: str = URL, + audio_ext=".flac", ): if mic_id not in ["mic1", "mic2"]: - raise RuntimeError( - f'`mic_id` has to be either "mic1" or "mic2". Found: {mic_id}' - ) + raise RuntimeError(f'`mic_id` has to be either "mic1" or "mic2". Found: {mic_id}') archive = os.path.join(root, "VCTK-Corpus-0.92.zip") @@ -69,9 +64,7 @@ def __init__( extract_archive(archive, self._path) if not os.path.isdir(self._path): - raise RuntimeError( - "Dataset not found. Please use `download=True` to download it." - ) + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") # Extracting speaker IDs from the folder structure self._speaker_ids = sorted(os.listdir(self._txt_dir)) @@ -91,9 +84,7 @@ def __init__( if speaker_id == "p280" and mic_id == "mic2": continue utterance_dir = os.path.join(self._txt_dir, speaker_id) - for utterance_file in sorted( - f for f in os.listdir(utterance_dir) if f.endswith(".txt") - ): + for utterance_file in sorted(f for f in os.listdir(utterance_dir) if f.endswith(".txt")): utterance_id = os.path.splitext(utterance_file)[0] audio_path_mic = os.path.join( self._audio_dir, @@ -112,9 +103,7 @@ def _load_audio(self, file_path) -> Tuple[Tensor, int]: return torchaudio.load(file_path) def _load_sample(self, speaker_id: str, utterance_id: str, mic_id: str) -> SampleType: - transcript_path = os.path.join( - self._txt_dir, speaker_id, f"{speaker_id}_{utterance_id}.txt" - ) + transcript_path = os.path.join(self._txt_dir, speaker_id, f"{speaker_id}_{utterance_id}.txt") audio_path = os.path.join( self._audio_dir, speaker_id, diff --git a/torchaudio/datasets/yesno.py b/torchaudio/datasets/yesno.py index 095c01abd5..b9709735c3 100644 --- a/torchaudio/datasets/yesno.py +++ b/torchaudio/datasets/yesno.py @@ -2,11 +2,10 @@ from pathlib import Path from typing import List, Tuple, Union +import torchaudio from torch import Tensor -from torch.utils.data import Dataset from torch.hub import download_url_to_file - -import torchaudio +from torch.utils.data import Dataset from torchaudio.datasets.utils import ( extract_archive, ) @@ -39,7 +38,7 @@ def __init__( root: Union[str, Path], url: str = _RELEASE_CONFIGS["release1"]["url"], folder_in_archive: str = _RELEASE_CONFIGS["release1"]["folder_in_archive"], - download: bool = False + download: bool = False, ) -> None: self._parse_filesystem(root, url, folder_in_archive, download) @@ -58,9 +57,7 @@ def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, downloa extract_archive(archive) if not os.path.isdir(self._path): - raise RuntimeError( - "Dataset not found. Please use `download=True` to download it." - ) + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*.wav")) diff --git a/torchaudio/functional/__init__.py b/torchaudio/functional/__init__.py index 174bbd533a..1d8187f33a 100644 --- a/torchaudio/functional/__init__.py +++ b/torchaudio/functional/__init__.py @@ -1,3 +1,27 @@ +from .filtering import ( + allpass_biquad, + band_biquad, + bandpass_biquad, + bandreject_biquad, + bass_biquad, + biquad, + contrast, + dither, + dcshift, + deemph_biquad, + equalizer_biquad, + filtfilt, + flanger, + gain, + highpass_biquad, + lfilter, + lowpass_biquad, + overdrive, + phaser, + riaa_biquad, + treble_biquad, + vad, +) from .functional import ( amplitude_to_DB, compute_deltas, @@ -23,75 +47,51 @@ pitch_shift, rnnt_loss, ) -from .filtering import ( - allpass_biquad, - band_biquad, - bandpass_biquad, - bandreject_biquad, - bass_biquad, - biquad, - contrast, - dither, - dcshift, - deemph_biquad, - equalizer_biquad, - filtfilt, - flanger, - gain, - highpass_biquad, - lfilter, - lowpass_biquad, - overdrive, - phaser, - riaa_biquad, - treble_biquad, - vad, -) __all__ = [ - 'amplitude_to_DB', - 'compute_deltas', - 'compute_kaldi_pitch', - 'create_dct', - 'melscale_fbanks', - 'linear_fbanks', - 'DB_to_amplitude', - 'detect_pitch_frequency', - 'griffinlim', - 'mask_along_axis', - 'mask_along_axis_iid', - 'mu_law_encoding', - 'mu_law_decoding', - 'phase_vocoder', - 'sliding_window_cmn', - 'spectrogram', - 'inverse_spectrogram', - 'spectral_centroid', - 'allpass_biquad', - 'band_biquad', - 'bandpass_biquad', - 'bandreject_biquad', - 'bass_biquad', - 'biquad', - 'contrast', - 'dither', - 'dcshift', - 'deemph_biquad', - 'equalizer_biquad', - 'filtfilt', - 'flanger', - 'gain', - 'highpass_biquad', - 'lfilter', - 'lowpass_biquad', - 'overdrive', - 'phaser', - 'riaa_biquad', - 'treble_biquad', - 'vad', - 'apply_codec', - 'resample', - 'edit_distance', - 'pitch_shift', - 'rnnt_loss', + "amplitude_to_DB", + "compute_deltas", + "compute_kaldi_pitch", + "create_dct", + "melscale_fbanks", + "linear_fbanks", + "DB_to_amplitude", + "detect_pitch_frequency", + "griffinlim", + "mask_along_axis", + "mask_along_axis_iid", + "mu_law_encoding", + "mu_law_decoding", + "phase_vocoder", + "sliding_window_cmn", + "spectrogram", + "inverse_spectrogram", + "spectral_centroid", + "allpass_biquad", + "band_biquad", + "bandpass_biquad", + "bandreject_biquad", + "bass_biquad", + "biquad", + "contrast", + "dither", + "dcshift", + "deemph_biquad", + "equalizer_biquad", + "filtfilt", + "flanger", + "gain", + "highpass_biquad", + "lfilter", + "lowpass_biquad", + "overdrive", + "phaser", + "riaa_biquad", + "treble_biquad", + "vad", + "apply_codec", + "resample", + "edit_distance", + "pitch_shift", + "rnnt_loss", ] diff --git a/torchaudio/functional/filtering.py b/torchaudio/functional/filtering.py index 0f5d24f67e..fd49337229 100644 --- a/torchaudio/functional/filtering.py +++ b/torchaudio/functional/filtering.py @@ -45,7 +45,7 @@ def _generate_wave_table( d = (torch.sin(point.to(torch.float64) / table_size * 2 * math.pi) + 1) / 2 elif wave_type == "TRIANGLE": d = point.to(torch.float64) * 2 / table_size - value = torch.div(4 * point, table_size, rounding_mode='floor') + value = torch.div(4 * point, table_size, rounding_mode="floor") d[value == 0] = d[value == 0] + 0.5 d[value == 1] = 1.5 - d[value == 1] d[value == 2] = 1.5 - d[value == 2] @@ -64,9 +64,7 @@ def _generate_wave_table( return d -def allpass_biquad( - waveform: Tensor, sample_rate: int, central_freq: float, Q: float = 0.707 -) -> Tensor: +def allpass_biquad(waveform: Tensor, sample_rate: int, central_freq: float, Q: float = 0.707) -> Tensor: r"""Design two-pole all-pass filter. Similar to SoX implementation. Args: @@ -191,9 +189,7 @@ def bandpass_biquad( return biquad(waveform, b0, b1, b2, a0, a1, a2) -def bandreject_biquad( - waveform: Tensor, sample_rate: int, central_freq: float, Q: float = 0.707 -) -> Tensor: +def bandreject_biquad(waveform: Tensor, sample_rate: int, central_freq: float, Q: float = 0.707) -> Tensor: r"""Design two-pole band-reject filter. Similar to SoX implementation. Args: @@ -273,9 +269,7 @@ def bass_biquad( return biquad(waveform, b0 / a0, b1 / a0, b2 / a0, a0 / a0, a1 / a0, a2 / a0) -def biquad( - waveform: Tensor, b0: float, b1: float, b2: float, a0: float, a1: float, a2: float -) -> Tensor: +def biquad(waveform: Tensor, b0: float, b1: float, b2: float, a0: float, a1: float, a2: float) -> Tensor: r"""Perform a biquad filter of input tensor. Initial conditions set to 0. https://en.wikipedia.org/wiki/Digital_biquad_filter @@ -339,9 +333,7 @@ def contrast(waveform: Tensor, enhancement_amount: float = 75.0) -> Tensor: return output_waveform -def dcshift( - waveform: Tensor, shift: float, limiter_gain: Optional[float] = None -) -> Tensor: +def dcshift(waveform: Tensor, shift: float, limiter_gain: Optional[float] = None) -> Tensor: r"""Apply a DC shift to the audio. Similar to SoX implementation. This can be useful to remove a DC offset (caused perhaps by a hardware problem in the recording chain) from the audio @@ -367,25 +359,13 @@ def dcshift( if limiter_gain is not None and shift > 0: mask = waveform > limiter_threshold - temp = ( - (waveform[mask] - limiter_threshold) - * limiter_gain - / (1 - limiter_threshold) - ) - output_waveform[mask] = (temp + limiter_threshold + shift).clamp( - max=limiter_threshold - ) + temp = (waveform[mask] - limiter_threshold) * limiter_gain / (1 - limiter_threshold) + output_waveform[mask] = (temp + limiter_threshold + shift).clamp(max=limiter_threshold) output_waveform[~mask] = (waveform[~mask] + shift).clamp(min=-1, max=1) elif limiter_gain is not None and shift < 0: mask = waveform < -limiter_threshold - temp = ( - (waveform[mask] + limiter_threshold) - * limiter_gain - / (1 - limiter_threshold) - ) - output_waveform[mask] = (temp - limiter_threshold + shift).clamp( - min=-limiter_threshold - ) + temp = (waveform[mask] + limiter_threshold) * limiter_gain / (1 - limiter_threshold) + output_waveform[mask] = (temp - limiter_threshold + shift).clamp(min=-limiter_threshold) output_waveform[~mask] = (waveform[~mask] + shift).clamp(min=-1, max=1) else: output_waveform = (waveform + shift).clamp(min=-1, max=1) @@ -461,9 +441,7 @@ def _add_noise_shaping(dithered_waveform: Tensor, waveform: Tensor) -> Tensor: return noise_shaped.reshape(dithered_shape[:-1] + noise_shaped.shape[-1:]) -def _apply_probability_distribution( - waveform: Tensor, density_function: str = "TPDF" -) -> Tensor: +def _apply_probability_distribution(waveform: Tensor, density_function: str = "TPDF") -> Tensor: r"""Apply a probability distribution function on a waveform. Triangular probability density function (TPDF) dither noise has a @@ -561,9 +539,7 @@ def _apply_probability_distribution( signal_scaled_dis = signal_scaled + gaussian else: # dtype needed for https://github.com/pytorch/pytorch/issues/32358 - TPDF = torch.bartlett_window( - time_size + 1, dtype=signal_scaled.dtype, device=signal_scaled.device - ) + TPDF = torch.bartlett_window(time_size + 1, dtype=signal_scaled.dtype, device=signal_scaled.device) TPDF = TPDF.repeat((channel_size + 1), 1) signal_scaled_dis = signal_scaled + TPDF @@ -574,9 +550,7 @@ def _apply_probability_distribution( return quantised_signal.reshape(shape[:-1] + quantised_signal.shape[-1:]) -def dither( - waveform: Tensor, density_function: str = "TPDF", noise_shaping: bool = False -) -> Tensor: +def dither(waveform: Tensor, density_function: str = "TPDF", noise_shaping: bool = False) -> Tensor: r"""Dither increases the perceived dynamic range of audio stored at a particular bit-depth by eliminating nonlinear truncation distortion (i.e. adding minimally perceived noise to mask distortion caused by quantization). @@ -594,9 +568,7 @@ def dither( Returns: Tensor: waveform dithered """ - dithered = _apply_probability_distribution( - waveform, density_function=density_function - ) + dithered = _apply_probability_distribution(waveform, density_function=density_function) if noise_shaping: return _add_noise_shaping(dithered, waveform) @@ -643,7 +615,10 @@ def equalizer_biquad( def filtfilt( - waveform: Tensor, a_coeffs: Tensor, b_coeffs: Tensor, clamp: bool = True, + waveform: Tensor, + a_coeffs: Tensor, + b_coeffs: Tensor, + clamp: bool = True, ) -> Tensor: r"""Apply an IIR filter forward and backward to a waveform. @@ -667,7 +642,11 @@ def filtfilt( """ forward_filtered = lfilter(waveform, a_coeffs, b_coeffs, clamp=False, batching=True) backward_filtered = lfilter( - forward_filtered.flip(-1), a_coeffs, b_coeffs, clamp=clamp, batching=True, + forward_filtered.flip(-1), + a_coeffs, + b_coeffs, + clamp=clamp, + batching=True, ).flip(-1) return backward_filtered @@ -757,9 +736,7 @@ def flanger( delay_buf_length = int((delay_min + delay_depth) * sample_rate + 0.5) delay_buf_length = delay_buf_length + 2 - delay_bufs = torch.zeros( - waveform.shape[0], n_channels, delay_buf_length, dtype=dtype, device=device - ) + delay_bufs = torch.zeros(waveform.shape[0], n_channels, delay_buf_length, dtype=dtype, device=device) delay_last = torch.zeros(waveform.shape[0], n_channels, dtype=dtype, device=device) lfo_length = int(sample_rate / speed) @@ -787,9 +764,7 @@ def flanger( delay_buf_pos = (delay_buf_pos + delay_buf_length - 1) % delay_buf_length - cur_channel_phase = (channel_idxs * lfo_length * channel_phase + 0.5).to( - torch.int64 - ) + cur_channel_phase = (channel_idxs * lfo_length * channel_phase + 0.5).to(torch.int64) delay_tensor = lfo[(lfo_pos + cur_channel_phase) % lfo_length] frac_delay = torch.frac(delay_tensor) delay_tensor = torch.floor(delay_tensor) @@ -800,24 +775,18 @@ def flanger( delay_bufs[:, :, delay_buf_pos] = temp + delay_last * feedback_gain - delayed_0 = delay_bufs[ - :, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length - ] + delayed_0 = delay_bufs[:, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length] int_delay = int_delay + 1 - delayed_1 = delay_bufs[ - :, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length - ] + delayed_1 = delay_bufs[:, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length] int_delay = int_delay + 1 if interpolation == "linear": delayed = delayed_0 + (delayed_1 - delayed_0) * frac_delay else: - delayed_2 = delay_bufs[ - :, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length - ] + delayed_2 = delay_bufs[:, channel_idxs, (delay_buf_pos + int_delay) % delay_buf_length] int_delay = int_delay + 1 @@ -854,9 +823,7 @@ def gain(waveform: Tensor, gain_db: float = 1.0) -> Tensor: return waveform * ratio -def highpass_biquad( - waveform: Tensor, sample_rate: int, cutoff_freq: float, Q: float = 0.707 -) -> Tensor: +def highpass_biquad(waveform: Tensor, sample_rate: int, cutoff_freq: float, Q: float = 0.707) -> Tensor: r"""Design biquad highpass filter and perform filtering. Similar to SoX implementation. Args: @@ -889,9 +856,7 @@ def _lfilter_core_generic_loop(input_signal_windows: Tensor, a_coeffs_flipped: T n_order = a_coeffs_flipped.size(1) a_coeffs_flipped = a_coeffs_flipped.unsqueeze(2) for i_sample, o0 in enumerate(input_signal_windows.permute(2, 0, 1)): - windowed_output_signal = padded_output_waveform[ - :, :, i_sample:i_sample + n_order - ] + windowed_output_signal = padded_output_waveform[:, :, i_sample : i_sample + n_order] o0 -= (windowed_output_signal.transpose(0, 1) @ a_coeffs_flipped)[..., 0].t() padded_output_waveform[:, :, i_sample + n_order - 1] = o0 @@ -899,7 +864,7 @@ def _lfilter_core_generic_loop(input_signal_windows: Tensor, a_coeffs_flipped: T try: _lfilter_core_cpu_loop = torch.ops.torchaudio._lfilter_core_loop except RuntimeError as err: - assert str(err) == 'No such operator torchaudio::_lfilter_core_loop' + assert str(err) == "No such operator torchaudio::_lfilter_core_loop" _lfilter_core_cpu_loop = _lfilter_core_generic_loop @@ -929,40 +894,32 @@ def _lfilter_core( b_coeffs_flipped = b_coeffs.flip(1) # calculate windowed_input_signal in parallel using convolution - input_signal_windows = torch.nn.functional.conv1d( - padded_waveform, - b_coeffs_flipped.unsqueeze(1), - groups=n_channel - ) + input_signal_windows = torch.nn.functional.conv1d(padded_waveform, b_coeffs_flipped.unsqueeze(1), groups=n_channel) input_signal_windows.div_(a_coeffs[:, :1]) a_coeffs_flipped.div_(a_coeffs[:, :1]) - if input_signal_windows.device == torch.device('cpu') and\ - a_coeffs_flipped.device == torch.device('cpu') and\ - padded_output_waveform.device == torch.device('cpu'): + if ( + input_signal_windows.device == torch.device("cpu") + and a_coeffs_flipped.device == torch.device("cpu") + and padded_output_waveform.device == torch.device("cpu") + ): _lfilter_core_cpu_loop(input_signal_windows, a_coeffs_flipped, padded_output_waveform) else: _lfilter_core_generic_loop(input_signal_windows, a_coeffs_flipped, padded_output_waveform) - output = padded_output_waveform[:, :, n_order - 1:] + output = padded_output_waveform[:, :, n_order - 1 :] return output try: _lfilter = torch.ops.torchaudio._lfilter except RuntimeError as err: - assert str(err) == 'No such operator torchaudio::_lfilter' + assert str(err) == "No such operator torchaudio::_lfilter" _lfilter = _lfilter_core -def lfilter( - waveform: Tensor, - a_coeffs: Tensor, - b_coeffs: Tensor, - clamp: bool = True, - batching: bool = True -) -> Tensor: +def lfilter(waveform: Tensor, a_coeffs: Tensor, b_coeffs: Tensor, clamp: bool = True, batching: bool = True) -> Tensor: r"""Perform an IIR filter by evaluating difference equation. Note: @@ -1016,9 +973,7 @@ def lfilter( return output -def lowpass_biquad( - waveform: Tensor, sample_rate: int, cutoff_freq: float, Q: float = 0.707 -) -> Tensor: +def lowpass_biquad(waveform: Tensor, sample_rate: int, cutoff_freq: float, Q: float = 0.707) -> Tensor: r"""Design biquad lowpass filter and perform filtering. Similar to SoX implementation. Args: @@ -1048,11 +1003,7 @@ def lowpass_biquad( def _overdrive_core_loop_generic( - waveform: Tensor, - temp: Tensor, - last_in: Tensor, - last_out: Tensor, - output_waveform: Tensor + waveform: Tensor, temp: Tensor, last_in: Tensor, last_out: Tensor, output_waveform: Tensor ): for i in range(waveform.shape[-1]): last_out = temp[:, i] - last_in + 0.995 * last_out @@ -1063,7 +1014,7 @@ def _overdrive_core_loop_generic( try: _overdrive_core_loop_cpu = torch.ops.torchaudio._overdrive_core_loop except RuntimeError as err: - assert str(err) == 'No such operator torchaudio::_overdrive_core_loop' + assert str(err) == "No such operator torchaudio::_overdrive_core_loop" _overdrive_core_loop_cpu = _overdrive_core_loop_generic @@ -1110,7 +1061,7 @@ def overdrive(waveform: Tensor, gain: float = 20, colour: float = 20) -> Tensor: output_waveform = torch.zeros_like(waveform, dtype=dtype, device=device) # Uses CPU optimized loop function if available for CPU device - if device == torch.device('cpu'): + if device == torch.device("cpu"): _overdrive_core_loop_cpu(waveform, temp, last_in, last_out, output_waveform) else: _overdrive_core_loop_generic(waveform, temp, last_in, last_out, output_waveform) @@ -1164,9 +1115,7 @@ def phaser( waveform = waveform.view(-1, actual_shape[-1]) delay_buf_len = int((delay_ms * 0.001 * sample_rate) + 0.5) - delay_buf = torch.zeros( - waveform.shape[0], delay_buf_len, dtype=dtype, device=device - ) + delay_buf = torch.zeros(waveform.shape[0], delay_buf_len, dtype=dtype, device=device) mod_buf_len = int(sample_rate / mod_speed + 0.5) @@ -1203,9 +1152,7 @@ def phaser( delay_buf_list[delay_pos] = temp * decay output_waveform_pre_gain_list.append(temp) - output_waveform = torch.stack(output_waveform_pre_gain_list, dim=1).to( - dtype=dtype, device=device - ) + output_waveform = torch.stack(output_waveform_pre_gain_list, dim=1).to(dtype=dtype, device=device) output_waveform.mul_(gain_out) return output_waveform.clamp(min=-1, max=1).view(actual_shape) @@ -1344,9 +1291,7 @@ def _measure( dftBuf = torch.zeros(dft_len_ws) - _index_ns = torch.tensor( - [index_ns] + [(index_ns + i) % samplesLen_ns for i in range(1, measure_len_ws)] - ) + _index_ns = torch.tensor([index_ns] + [(index_ns + i) % samplesLen_ns for i in range(1, measure_len_ws)]) dftBuf[:measure_len_ws] = samples[_index_ns] * spectrum_window[:measure_len_ws] # memset(c->dftBuf + i, 0, (p->dft_len_ws - i) * sizeof(*c->dftBuf)); @@ -1358,9 +1303,7 @@ def _measure( # memset(c->dftBuf, 0, p->spectrum_start * sizeof(*c->dftBuf)); _dftBuf[:spectrum_start].zero_() - mult: float = ( - boot_count / (1.0 + boot_count) if boot_count >= 0 else measure_smooth_time_mult - ) + mult: float = boot_count / (1.0 + boot_count) if boot_count >= 0 else measure_smooth_time_mult _d = _dftBuf[spectrum_start:spectrum_end].abs() spectrum[spectrum_start:spectrum_end].mul_(mult).add_(_d * (1 - mult)) @@ -1387,17 +1330,13 @@ def _measure( _cepstrum_Buf: Tensor = torch.zeros(dft_len_ws >> 1) _cepstrum_Buf[spectrum_start:spectrum_end] = _d * cepstrum_window - _cepstrum_Buf[spectrum_end:dft_len_ws >> 1].zero_() + _cepstrum_Buf[spectrum_end : dft_len_ws >> 1].zero_() # lsx_safe_rdft((int)p->dft_len_ws >> 1, 1, c->dftBuf); _cepstrum_Buf = torch.fft.rfft(_cepstrum_Buf) - result: float = float( - torch.sum(_cepstrum_Buf[cepstrum_start:cepstrum_end].abs().pow(2)) - ) - result = ( - math.log(result / (cepstrum_end - cepstrum_start)) if result > 0 else -math.inf - ) + result: float = float(torch.sum(_cepstrum_Buf[cepstrum_start:cepstrum_end].abs().pow(2))) + result = math.log(result / (cepstrum_end - cepstrum_start)) if result > 0 else -math.inf return max(0, 21 + result) @@ -1489,9 +1428,7 @@ def vad( " and https://github.com/pytorch/audio/issues/1468." ) - measure_duration: float = ( - 2.0 / measure_freq if measure_duration is None else measure_duration - ) + measure_duration: float = 2.0 / measure_freq if measure_duration is None else measure_duration measure_len_ws = int(sample_rate * measure_duration + 0.5) measure_len_ns = measure_len_ws @@ -1506,9 +1443,7 @@ def vad( gap_len = int(allowed_gap * measure_freq + 0.5) fixed_pre_trigger_len_ns = int(pre_trigger_time * sample_rate + 0.5) - samplesLen_ns = ( - fixed_pre_trigger_len_ns + search_pre_trigger_len_ns + measure_len_ns - ) + samplesLen_ns = fixed_pre_trigger_len_ns + search_pre_trigger_len_ns + measure_len_ns spectrum_window = torch.zeros(measure_len_ws) for i in range(measure_len_ws): @@ -1526,9 +1461,7 @@ def vad( for i in range(spectrum_end - spectrum_start): cepstrum_window[i] = 2.0 / math.sqrt(float(spectrum_end) - spectrum_start) # lsx_apply_hann(cepstrum_window,(int)(spectrum_end - spectrum_start)); - cepstrum_window *= torch.hann_window( - spectrum_end - spectrum_start, dtype=torch.float - ) + cepstrum_window *= torch.hann_window(spectrum_end - spectrum_start, dtype=torch.float) cepstrum_start = math.ceil(sample_rate * 0.5 / lp_lifter_freq) cepstrum_end = math.floor(sample_rate * 0.5 / hp_lifter_freq) @@ -1567,9 +1500,7 @@ def vad( samples[i, samplesIndex_ns] = waveform[i, pos] # if (!p->measure_timer_ns) { if measure_timer_ns == 0: - index_ns: int = ( - samplesIndex_ns + samplesLen_ns - measure_len_ns - ) % samplesLen_ns + index_ns: int = (samplesIndex_ns + samplesLen_ns - measure_len_ns) % samplesLen_ns meas: float = _measure( measure_len_ws=measure_len_ws, samples=samples[i], @@ -1589,9 +1520,7 @@ def vad( boot_count=boot_count, ) measures[i, measures_index] = meas - mean_meas[i] = mean_meas[i] * trigger_meas_time_mult + meas * ( - 1.0 - trigger_meas_time_mult - ) + mean_meas[i] = mean_meas[i] * trigger_meas_time_mult + meas * (1.0 - trigger_meas_time_mult) has_triggered = has_triggered or (mean_meas[i] >= trigger_level) if has_triggered: @@ -1602,9 +1531,7 @@ def vad( j: int = 0 for j in range(n): - if (measures[i, k] >= trigger_level) and ( - j <= jTrigger + gap_len - ): + if (measures[i, k] >= trigger_level) and (j <= jTrigger + gap_len): jZero = jTrigger = j elif (measures[i, k] == 0) and (jTrigger >= jZero): jZero = j @@ -1631,6 +1558,6 @@ def vad( flushedLen_ns = (measures_len - num_measures_to_flush) * measure_period_ns samplesIndex_ns = (samplesIndex_ns + flushedLen_ns) % samplesLen_ns - res = waveform[:, pos - samplesLen_ns + flushedLen_ns:] + res = waveform[:, pos - samplesLen_ns + flushedLen_ns :] # unpack batch return res.view(shape[:-1] + res.shape[-1:]) diff --git a/torchaudio/functional/functional.py b/torchaudio/functional/functional.py index 74ea1fd2ea..81b70ad1c0 100644 --- a/torchaudio/functional/functional.py +++ b/torchaudio/functional/functional.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -from collections.abc import Sequence import io import math import warnings +from collections.abc import Sequence from typing import Optional, Tuple import torch +import torchaudio from torch import Tensor from torchaudio._internal import module_utils as _mod_utils -import torchaudio __all__ = [ "spectrogram", @@ -28,9 +28,9 @@ "mu_law_encoding", "mu_law_decoding", "phase_vocoder", - 'mask_along_axis', - 'mask_along_axis_iid', - 'sliding_window_cmn', + "mask_along_axis", + "mask_along_axis_iid", + "sliding_window_cmn", "spectral_centroid", "apply_codec", "resample", @@ -41,18 +41,18 @@ def spectrogram( - waveform: Tensor, - pad: int, - window: Tensor, - n_fft: int, - hop_length: int, - win_length: int, - power: Optional[float], - normalized: bool, - center: bool = True, - pad_mode: str = "reflect", - onesided: bool = True, - return_complex: Optional[bool] = None, + waveform: Tensor, + pad: int, + window: Tensor, + n_fft: int, + hop_length: int, + win_length: int, + power: Optional[float], + normalized: bool, + center: bool = True, + pad_mode: str = "reflect", + onesided: bool = True, + return_complex: Optional[bool] = None, ) -> Tensor: r"""Create a spectrogram or a batch of spectrograms from a raw audio signal. The spectrogram can be either magnitude-only or complex. @@ -116,7 +116,7 @@ def spectrogram( spec_f = spec_f.reshape(shape[:-1] + spec_f.shape[-2:]) if normalized: - spec_f /= window.pow(2.).sum().sqrt() + spec_f /= window.pow(2.0).sum().sqrt() if power is not None: if power == 1.0: return spec_f.abs() @@ -125,17 +125,17 @@ def spectrogram( def inverse_spectrogram( - spectrogram: Tensor, - length: Optional[int], - pad: int, - window: Tensor, - n_fft: int, - hop_length: int, - win_length: int, - normalized: bool, - center: bool = True, - pad_mode: str = "reflect", - onesided: bool = True, + spectrogram: Tensor, + length: Optional[int], + pad: int, + window: Tensor, + n_fft: int, + hop_length: int, + win_length: int, + normalized: bool, + center: bool = True, + pad_mode: str = "reflect", + onesided: bool = True, ) -> Tensor: r"""Create an inverse spectrogram or a batch of inverse spectrograms from the provided complex-valued spectrogram. @@ -166,7 +166,7 @@ def inverse_spectrogram( raise ValueError("Expected `spectrogram` to be complex dtype.") if normalized: - spectrogram = spectrogram * window.pow(2.).sum().sqrt() + spectrogram = spectrogram * window.pow(2.0).sum().sqrt() # pack batch shape = spectrogram.size() @@ -203,20 +203,20 @@ def _get_complex_dtype(real_dtype: torch.dtype): return torch.cfloat if real_dtype == torch.half: return torch.complex32 - raise ValueError(f'Unexpected dtype {real_dtype}') + raise ValueError(f"Unexpected dtype {real_dtype}") def griffinlim( - specgram: Tensor, - window: Tensor, - n_fft: int, - hop_length: int, - win_length: int, - power: float, - n_iter: int, - momentum: float, - length: Optional[int], - rand_init: bool + specgram: Tensor, + window: Tensor, + n_fft: int, + hop_length: int, + win_length: int, + power: float, + n_iter: int, + momentum: float, + length: Optional[int], + rand_init: bool, ) -> Tensor: r"""Compute waveform from a linear scale magnitude spectrogram using the Griffin-Lim transformation. @@ -244,8 +244,8 @@ def griffinlim( Returns: Tensor: waveform of `(..., time)`, where time equals the ``length`` parameter if given. """ - assert momentum < 1, 'momentum={} > 1 can be unstable'.format(momentum) - assert momentum >= 0, 'momentum={} < 0'.format(momentum) + assert momentum < 1, "momentum={} > 1 can be unstable".format(momentum) + assert momentum >= 0, "momentum={} < 0".format(momentum) # pack batch shape = specgram.size() @@ -255,24 +255,17 @@ def griffinlim( # initialize the phase if rand_init: - angles = torch.rand( - specgram.size(), - dtype=_get_complex_dtype(specgram.dtype), device=specgram.device) + angles = torch.rand(specgram.size(), dtype=_get_complex_dtype(specgram.dtype), device=specgram.device) else: - angles = torch.full( - specgram.size(), 1, - dtype=_get_complex_dtype(specgram.dtype), device=specgram.device) + angles = torch.full(specgram.size(), 1, dtype=_get_complex_dtype(specgram.dtype), device=specgram.device) # And initialize the previous iterate to 0 - tprev = torch.tensor(0., dtype=specgram.dtype, device=specgram.device) + tprev = torch.tensor(0.0, dtype=specgram.dtype, device=specgram.device) for _ in range(n_iter): # Invert with our current estimate of the phases - inverse = torch.istft(specgram * angles, - n_fft=n_fft, - hop_length=hop_length, - win_length=win_length, - window=window, - length=length) + inverse = torch.istft( + specgram * angles, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, length=length + ) # Rebuild the spectrogram rebuilt = torch.stft( @@ -282,7 +275,7 @@ def griffinlim( win_length=win_length, window=window, center=True, - pad_mode='reflect', + pad_mode="reflect", normalized=False, onesided=True, return_complex=True, @@ -298,12 +291,9 @@ def griffinlim( tprev = rebuilt # Return the final phase estimates - waveform = torch.istft(specgram * angles, - n_fft=n_fft, - hop_length=hop_length, - win_length=win_length, - window=window, - length=length) + waveform = torch.istft( + specgram * angles, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, length=length + ) # unpack batch waveform = waveform.reshape(shape[:-2] + waveform.shape[-1:]) @@ -312,11 +302,7 @@ def griffinlim( def amplitude_to_DB( - x: Tensor, - multiplier: float, - amin: float, - db_multiplier: float, - top_db: Optional[float] = None + x: Tensor, multiplier: float, amin: float, db_multiplier: float, top_db: Optional[float] = None ) -> Tensor: r"""Turn a spectrogram from the power/amplitude scale to the decibel scale. @@ -354,11 +340,7 @@ def amplitude_to_DB( return x_db -def DB_to_amplitude( - x: Tensor, - ref: float, - power: float -) -> Tensor: +def DB_to_amplitude(x: Tensor, ref: float, power: float) -> Tensor: r"""Turn a tensor from the decibel scale to the power/amplitude scale. Args: @@ -383,7 +365,7 @@ def _hz_to_mel(freq: float, mel_scale: str = "htk") -> float: mels (float): Frequency in Mels """ - if mel_scale not in ['slaney', 'htk']: + if mel_scale not in ["slaney", "htk"]: raise ValueError('mel_scale should be one of "htk" or "slaney".') if mel_scale == "htk": @@ -417,11 +399,11 @@ def _mel_to_hz(mels: Tensor, mel_scale: str = "htk") -> Tensor: freqs (Tensor): Mels converted in Hz """ - if mel_scale not in ['slaney', 'htk']: + if mel_scale not in ["slaney", "htk"]: raise ValueError('mel_scale should be one of "htk" or "slaney".') if mel_scale == "htk": - return 700.0 * (10.0**(mels / 2595.0) - 1.0) + return 700.0 * (10.0 ** (mels / 2595.0) - 1.0) # Fill in the linear scale f_min = 0.0 @@ -433,15 +415,15 @@ def _mel_to_hz(mels: Tensor, mel_scale: str = "htk") -> Tensor: min_log_mel = (min_log_hz - f_min) / f_sp logstep = math.log(6.4) / 27.0 - log_t = (mels >= min_log_mel) + log_t = mels >= min_log_mel freqs[log_t] = min_log_hz * torch.exp(logstep * (mels[log_t] - min_log_mel)) return freqs def _create_triangular_filterbank( - all_freqs: Tensor, - f_pts: Tensor, + all_freqs: Tensor, + f_pts: Tensor, ) -> Tensor: """Create a triangular filter bank. @@ -466,13 +448,13 @@ def _create_triangular_filterbank( def melscale_fbanks( - n_freqs: int, - f_min: float, - f_max: float, - n_mels: int, - sample_rate: int, - norm: Optional[str] = None, - mel_scale: str = "htk", + n_freqs: int, + f_min: float, + f_max: float, + n_mels: int, + sample_rate: int, + norm: Optional[str] = None, + mel_scale: str = "htk", ) -> Tensor: r"""Create a frequency bin conversion matrix. @@ -520,10 +502,10 @@ def melscale_fbanks( if norm is not None and norm == "slaney": # Slaney-style mel is scaled to be approx constant energy per channel - enorm = 2.0 / (f_pts[2:n_mels + 2] - f_pts[:n_mels]) + enorm = 2.0 / (f_pts[2 : n_mels + 2] - f_pts[:n_mels]) fb *= enorm.unsqueeze(0) - if (fb.max(dim=0).values == 0.).any(): + if (fb.max(dim=0).values == 0.0).any(): warnings.warn( "At least one mel filterbank has all zero values. " f"The value for `n_mels` ({n_mels}) may be set too high. " @@ -534,11 +516,11 @@ def melscale_fbanks( def linear_fbanks( - n_freqs: int, - f_min: float, - f_max: float, - n_filter: int, - sample_rate: int, + n_freqs: int, + f_min: float, + f_max: float, + n_filter: int, + sample_rate: int, ) -> Tensor: r"""Creates a linear triangular filterbank. @@ -575,11 +557,7 @@ def linear_fbanks( return fb -def create_dct( - n_mfcc: int, - n_mels: int, - norm: Optional[str] -) -> Tensor: +def create_dct(n_mfcc: int, n_mels: int, norm: Optional[str]) -> Tensor: r"""Create a DCT transformation matrix with shape (``n_mels``, ``n_mfcc``), normalized depending on norm. @@ -605,10 +583,7 @@ def create_dct( return dct.t() -def mu_law_encoding( - x: Tensor, - quantization_channels: int -) -> Tensor: +def mu_law_encoding(x: Tensor, quantization_channels: int) -> Tensor: r"""Encode signal based on mu-law companding. For more info see the `Wikipedia Entry `_ @@ -624,8 +599,10 @@ def mu_law_encoding( """ mu = quantization_channels - 1.0 if not x.is_floating_point(): - warnings.warn("The input Tensor must be of floating type. \ - This will be an error in the v0.12 release.") + warnings.warn( + "The input Tensor must be of floating type. \ + This will be an error in the v0.12 release." + ) x = x.to(torch.float) mu = torch.tensor(mu, dtype=x.dtype) x_mu = torch.sign(x) * torch.log1p(mu * torch.abs(x)) / torch.log1p(mu) @@ -633,10 +610,7 @@ def mu_law_encoding( return x_mu -def mu_law_decoding( - x_mu: Tensor, - quantization_channels: int -) -> Tensor: +def mu_law_decoding(x_mu: Tensor, quantization_channels: int) -> Tensor: r"""Decode mu-law encoded signal. For more info see the `Wikipedia Entry `_ @@ -659,11 +633,7 @@ def mu_law_decoding( return x -def phase_vocoder( - complex_specgrams: Tensor, - rate: float, - phase_advance: Tensor -) -> Tensor: +def phase_vocoder(complex_specgrams: Tensor, rate: float, phase_advance: Tensor) -> Tensor: r"""Given a STFT tensor, speed up in time without modifying pitch by a factor of ``rate``. @@ -699,12 +669,7 @@ def phase_vocoder( # Figures out the corresponding real dtype, i.e. complex128 -> float64, complex64 -> float32 # Note torch.real is a view so it does not incur any memory copy. real_dtype = torch.real(complex_specgrams).dtype - time_steps = torch.arange( - 0, - complex_specgrams.size(-1), - rate, - device=complex_specgrams.device, - dtype=real_dtype) + time_steps = torch.arange(0, complex_specgrams.size(-1), rate, device=complex_specgrams.device, dtype=real_dtype) alphas = time_steps % 1.0 phase_0 = complex_specgrams[..., :1].angle() @@ -739,12 +704,7 @@ def phase_vocoder( return complex_specgrams_stretch -def mask_along_axis_iid( - specgrams: Tensor, - mask_param: int, - mask_value: float, - axis: int -) -> Tensor: +def mask_along_axis_iid(specgrams: Tensor, mask_param: int, mask_value: float, axis: int) -> Tensor: r""" Apply a mask along ``axis``. Mask will be applied from indices ``[v_0, v_0 + v)``, where ``v`` is sampled from ``uniform(0, mask_param)``, and ``v_0`` from ``uniform(0, max_v - v)``. @@ -760,7 +720,7 @@ def mask_along_axis_iid( """ if axis not in [2, 3]: - raise ValueError('Only Frequency and Time masking are supported') + raise ValueError("Only Frequency and Time masking are supported") device = specgrams.device dtype = specgrams.dtype @@ -781,12 +741,7 @@ def mask_along_axis_iid( return specgrams -def mask_along_axis( - specgram: Tensor, - mask_param: int, - mask_value: float, - axis: int -) -> Tensor: +def mask_along_axis(specgram: Tensor, mask_param: int, mask_value: float, axis: int) -> Tensor: r""" Apply a mask along ``axis``. Mask will be applied from indices ``[v_0, v_0 + v)``, where ``v`` is sampled from ``uniform(0, mask_param)``, and ``v_0`` from ``uniform(0, max_v - v)``. @@ -802,7 +757,7 @@ def mask_along_axis( Tensor: Masked spectrogram of dimensions `(channel, freq, time)` """ if axis not in [1, 2]: - raise ValueError('Only Frequency and Time masking are supported') + raise ValueError("Only Frequency and Time masking are supported") # pack batch shape = specgram.size() @@ -827,11 +782,7 @@ def mask_along_axis( return specgram -def compute_deltas( - specgram: Tensor, - win_length: int = 5, - mode: str = "replicate" -) -> Tensor: +def compute_deltas(specgram: Tensor, win_length: int = 5, mode: str = "replicate") -> Tensor: r"""Compute delta coefficients of a tensor, usually a spectrogram: .. math:: @@ -880,12 +831,7 @@ def compute_deltas( return output -def _compute_nccf( - waveform: Tensor, - sample_rate: int, - frame_time: float, - freq_low: int -) -> Tensor: +def _compute_nccf(waveform: Tensor, sample_rate: int, frame_time: float, freq_low: int) -> Tensor: r""" Compute Normalized Cross-Correlation Function (NCCF). @@ -932,25 +878,17 @@ def _compute_nccf( return nccf -def _combine_max( - a: Tuple[Tensor, Tensor], - b: Tuple[Tensor, Tensor], - thresh: float = 0.99 -) -> Tuple[Tensor, Tensor]: +def _combine_max(a: Tuple[Tensor, Tensor], b: Tuple[Tensor, Tensor], thresh: float = 0.99) -> Tuple[Tensor, Tensor]: """ Take value from first if bigger than a multiplicative factor of the second, elementwise. """ - mask = (a[0] > thresh * b[0]) + mask = a[0] > thresh * b[0] values = mask * a[0] + ~mask * b[0] indices = mask * a[1] + ~mask * b[1] return values, indices -def _find_max_per_frame( - nccf: Tensor, - sample_rate: int, - freq_high: int -) -> Tensor: +def _find_max_per_frame(nccf: Tensor, sample_rate: int, freq_high: int) -> Tensor: r""" For each frame, take the highest value of NCCF, apply centered median smoothing, and convert to frequency. @@ -979,10 +917,7 @@ def _find_max_per_frame( return indices -def _median_smoothing( - indices: Tensor, - win_length: int -) -> Tensor: +def _median_smoothing(indices: Tensor, win_length: int) -> Tensor: r""" Apply median smoothing to the 1D tensor over the given window. """ @@ -991,9 +926,7 @@ def _median_smoothing( pad_length = (win_length - 1) // 2 # "replicate" padding in any dimension - indices = torch.nn.functional.pad( - indices, (pad_length, 0), mode="constant", value=0. - ) + indices = torch.nn.functional.pad(indices, (pad_length, 0), mode="constant", value=0.0) indices[..., :pad_length] = torch.cat(pad_length * [indices[..., pad_length].unsqueeze(-1)], dim=-1) roll = indices.unfold(-1, win_length, 1) @@ -1003,12 +936,12 @@ def _median_smoothing( def detect_pitch_frequency( - waveform: Tensor, - sample_rate: int, - frame_time: float = 10 ** (-2), - win_length: int = 30, - freq_low: int = 85, - freq_high: int = 3400, + waveform: Tensor, + sample_rate: int, + frame_time: float = 10 ** (-2), + win_length: int = 30, + freq_low: int = 85, + freq_high: int = 3400, ) -> Tensor: r"""Detect pitch frequency. @@ -1075,8 +1008,7 @@ def sliding_window_cmn( last_window_start = last_window_end = -1 cur_sum = torch.zeros(num_channels, num_feats, dtype=dtype, device=device) cur_sumsq = torch.zeros(num_channels, num_feats, dtype=dtype, device=device) - cmn_specgram = torch.zeros( - num_channels, num_frames, num_feats, dtype=dtype, device=device) + cmn_specgram = torch.zeros(num_channels, num_frames, num_feats, dtype=dtype, device=device) for t in range(num_frames): window_start = 0 window_end = 0 @@ -1093,12 +1025,12 @@ def sliding_window_cmn( if window_end > t: window_end = max(t + 1, min_cmn_window) if window_end > num_frames: - window_start -= (window_end - num_frames) + window_start -= window_end - num_frames window_end = num_frames if window_start < 0: window_start = 0 if last_window_start == -1: - input_part = specgram[:, window_start: window_end - window_start, :] + input_part = specgram[:, window_start : window_end - window_start, :] cur_sum += torch.sum(input_part, 1) if norm_vars: cur_sumsq += torch.cumsum(input_part ** 2, 1)[:, -1, :] @@ -1107,24 +1039,23 @@ def sliding_window_cmn( frame_to_remove = specgram[:, last_window_start, :] cur_sum -= frame_to_remove if norm_vars: - cur_sumsq -= (frame_to_remove ** 2) + cur_sumsq -= frame_to_remove ** 2 if window_end > last_window_end: frame_to_add = specgram[:, last_window_end, :] cur_sum += frame_to_add if norm_vars: - cur_sumsq += (frame_to_add ** 2) + cur_sumsq += frame_to_add ** 2 window_frames = window_end - window_start last_window_start = window_start last_window_end = window_end cmn_specgram[:, t, :] = specgram[:, t, :] - cur_sum / window_frames if norm_vars: if window_frames == 1: - cmn_specgram[:, t, :] = torch.zeros( - num_channels, num_feats, dtype=dtype, device=device) + cmn_specgram[:, t, :] = torch.zeros(num_channels, num_feats, dtype=dtype, device=device) else: variance = cur_sumsq variance = variance / window_frames - variance -= ((cur_sum ** 2) / (window_frames ** 2)) + variance -= (cur_sum ** 2) / (window_frames ** 2) variance = torch.pow(variance, -0.5) cmn_specgram[:, t, :] *= variance @@ -1135,13 +1066,13 @@ def sliding_window_cmn( def spectral_centroid( - waveform: Tensor, - sample_rate: int, - pad: int, - window: Tensor, - n_fft: int, - hop_length: int, - win_length: int, + waveform: Tensor, + sample_rate: int, + pad: int, + window: Tensor, + n_fft: int, + hop_length: int, + win_length: int, ) -> Tensor: r""" Compute the spectral centroid for each channel along the time axis. @@ -1161,10 +1092,17 @@ def spectral_centroid( Returns: Tensor: Dimension `(..., time)` """ - specgram = spectrogram(waveform, pad=pad, window=window, n_fft=n_fft, hop_length=hop_length, - win_length=win_length, power=1., normalized=False) - freqs = torch.linspace(0, sample_rate // 2, steps=1 + n_fft // 2, - device=specgram.device).reshape((-1, 1)) + specgram = spectrogram( + waveform, + pad=pad, + window=window, + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + power=1.0, + normalized=False, + ) + freqs = torch.linspace(0, sample_rate // 2, steps=1 + n_fft // 2, device=specgram.device).reshape((-1, 1)) freq_dim = -2 return (freqs * specgram).sum(dim=freq_dim) / specgram.sum(dim=freq_dim) @@ -1201,42 +1139,37 @@ def apply_codec( If ``channels_first=True``, it has `(channel, time)` else `(time, channel)`. """ bytes = io.BytesIO() - torchaudio.backend.sox_io_backend.save(bytes, - waveform, - sample_rate, - channels_first, - compression, - format, - encoding, - bits_per_sample - ) + torchaudio.backend.sox_io_backend.save( + bytes, waveform, sample_rate, channels_first, compression, format, encoding, bits_per_sample + ) bytes.seek(0) augmented, _ = torchaudio.sox_effects.sox_effects.apply_effects_file( - bytes, effects=[["rate", f"{sample_rate}"]], channels_first=channels_first, format=format) + bytes, effects=[["rate", f"{sample_rate}"]], channels_first=channels_first, format=format + ) return augmented @_mod_utils.requires_kaldi() def compute_kaldi_pitch( - waveform: torch.Tensor, - sample_rate: float, - frame_length: float = 25.0, - frame_shift: float = 10.0, - min_f0: float = 50, - max_f0: float = 400, - soft_min_f0: float = 10.0, - penalty_factor: float = 0.1, - lowpass_cutoff: float = 1000, - resample_frequency: float = 4000, - delta_pitch: float = 0.005, - nccf_ballast: float = 7000, - lowpass_filter_width: int = 1, - upsample_filter_width: int = 5, - max_frames_latency: int = 0, - frames_per_chunk: int = 0, - simulate_first_pass_online: bool = False, - recompute_frame: int = 500, - snip_edges: bool = True, + waveform: torch.Tensor, + sample_rate: float, + frame_length: float = 25.0, + frame_shift: float = 10.0, + min_f0: float = 50, + max_f0: float = 400, + soft_min_f0: float = 10.0, + penalty_factor: float = 0.1, + lowpass_cutoff: float = 1000, + resample_frequency: float = 4000, + delta_pitch: float = 0.005, + nccf_ballast: float = 7000, + lowpass_filter_width: int = 1, + upsample_filter_width: int = 5, + max_frames_latency: int = 0, + frames_per_chunk: int = 0, + simulate_first_pass_online: bool = False, + recompute_frame: int = 500, + snip_edges: bool = True, ) -> torch.Tensor: """Extract pitch based on method described in *A pitch extraction algorithm tuned for automatic speech recognition* [:footcite:`6854049`]. @@ -1302,11 +1235,24 @@ def compute_kaldi_pitch( shape = waveform.shape waveform = waveform.reshape(-1, shape[-1]) result = torch.ops.torchaudio.kaldi_ComputeKaldiPitch( - waveform, sample_rate, frame_length, frame_shift, - min_f0, max_f0, soft_min_f0, penalty_factor, lowpass_cutoff, - resample_frequency, delta_pitch, nccf_ballast, - lowpass_filter_width, upsample_filter_width, max_frames_latency, - frames_per_chunk, simulate_first_pass_online, recompute_frame, + waveform, + sample_rate, + frame_length, + frame_shift, + min_f0, + max_f0, + soft_min_f0, + penalty_factor, + lowpass_cutoff, + resample_frequency, + delta_pitch, + nccf_ballast, + lowpass_filter_width, + upsample_filter_width, + max_frames_latency, + frames_per_chunk, + simulate_first_pass_online, + recompute_frame, snip_edges, ) result = result.reshape(shape[:-1] + result.shape[-2:]) @@ -1314,15 +1260,16 @@ def compute_kaldi_pitch( def _get_sinc_resample_kernel( - orig_freq: int, - new_freq: int, - gcd: int, - lowpass_filter_width: int, - rolloff: float, - resampling_method: str, - beta: Optional[float], - device: torch.device = torch.device("cpu"), - dtype: Optional[torch.dtype] = None): + orig_freq: int, + new_freq: int, + gcd: int, + lowpass_filter_width: int, + rolloff: float, + resampling_method: str, + beta: Optional[float], + device: torch.device = torch.device("cpu"), + dtype: Optional[torch.dtype] = None, +): if not (int(orig_freq) == orig_freq and int(new_freq) == new_freq): raise Exception( @@ -1334,8 +1281,8 @@ def _get_sinc_resample_kernel( "For more information, please refer to https://github.com/pytorch/audio/issues/1487." ) - if resampling_method not in ['sinc_interpolation', 'kaiser_window']: - raise ValueError('Invalid resampling method: {}'.format(resampling_method)) + if resampling_method not in ["sinc_interpolation", "kaiser_window"]: + raise ValueError("Invalid resampling method: {}".format(resampling_method)) orig_freq = int(orig_freq) // gcd new_freq = int(new_freq) // gcd @@ -1381,7 +1328,7 @@ def _get_sinc_resample_kernel( # we do not use built in torch windows here as we need to evaluate the window # at specific positions, not over a regular grid. if resampling_method == "sinc_interpolation": - window = torch.cos(t * math.pi / lowpass_filter_width / 2)**2 + window = torch.cos(t * math.pi / lowpass_filter_width / 2) ** 2 else: # kaiser_window if beta is None: @@ -1389,7 +1336,7 @@ def _get_sinc_resample_kernel( beta_tensor = torch.tensor(float(beta)) window = torch.i0(beta_tensor * torch.sqrt(1 - (t / lowpass_filter_width) ** 2)) / torch.i0(beta_tensor) t *= math.pi - kernel = torch.where(t == 0, torch.tensor(1.).to(t), torch.sin(t) / t) + kernel = torch.where(t == 0, torch.tensor(1.0).to(t), torch.sin(t) / t) kernel.mul_(window) kernels.append(kernel) @@ -1401,12 +1348,12 @@ def _get_sinc_resample_kernel( def _apply_sinc_resample_kernel( - waveform: Tensor, - orig_freq: int, - new_freq: int, - gcd: int, - kernel: Tensor, - width: int, + waveform: Tensor, + orig_freq: int, + new_freq: int, + gcd: int, + kernel: Tensor, + width: int, ): orig_freq = int(orig_freq) // gcd new_freq = int(new_freq) // gcd @@ -1428,13 +1375,13 @@ def _apply_sinc_resample_kernel( def resample( - waveform: Tensor, - orig_freq: int, - new_freq: int, - lowpass_filter_width: int = 6, - rolloff: float = 0.99, - resampling_method: str = "sinc_interpolation", - beta: Optional[float] = None, + waveform: Tensor, + orig_freq: int, + new_freq: int, + lowpass_filter_width: int = 6, + rolloff: float = 0.99, + resampling_method: str = "sinc_interpolation", + beta: Optional[float] = None, ) -> Tensor: r"""Resamples the waveform at the new frequency using bandlimited interpolation. @@ -1467,8 +1414,17 @@ def resample( gcd = math.gcd(int(orig_freq), int(new_freq)) - kernel, width = _get_sinc_resample_kernel(orig_freq, new_freq, gcd, lowpass_filter_width, rolloff, - resampling_method, beta, waveform.device, waveform.dtype) + kernel, width = _get_sinc_resample_kernel( + orig_freq, + new_freq, + gcd, + lowpass_filter_width, + rolloff, + resampling_method, + beta, + waveform.device, + waveform.dtype, + ) resampled = _apply_sinc_resample_kernel(waveform, orig_freq, new_freq, gcd, kernel, width) return resampled @@ -1557,25 +1513,24 @@ def pitch_shift( ori_len = shape[-1] rate = 2.0 ** (-float(n_steps) / bins_per_octave) - spec_f = torch.stft(input=waveform, - n_fft=n_fft, - hop_length=hop_length, - win_length=win_length, - window=window, - center=True, - pad_mode='reflect', - normalized=False, - onesided=True, - return_complex=True) + spec_f = torch.stft( + input=waveform, + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + window=window, + center=True, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) phase_advance = torch.linspace(0, math.pi * hop_length, spec_f.shape[-2], device=spec_f.device)[..., None] spec_stretch = phase_vocoder(spec_f, rate, phase_advance) len_stretch = int(round(ori_len / rate)) - waveform_stretch = torch.istft(spec_stretch, - n_fft=n_fft, - hop_length=hop_length, - win_length=win_length, - window=window, - length=len_stretch) + waveform_stretch = torch.istft( + spec_stretch, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, length=len_stretch + ) waveform_shift = resample(waveform_stretch, int(sample_rate / rate), sample_rate) shift_len = waveform_shift.size()[-1] if shift_len > ori_len: @@ -1617,7 +1572,7 @@ def rnnt_loss( Tensor: Loss with the reduction option applied. If ``reduction`` is ``'none'``, then size `(batch)`, otherwise scalar. """ - if reduction not in ['none', 'mean', 'sum']: + if reduction not in ["none", "mean", "sum"]: raise ValueError("reduction should be one of 'none', 'mean', or 'sum'") if blank < 0: # reinterpret blank index if blank < 0. @@ -1632,9 +1587,9 @@ def rnnt_loss( clamp=clamp, ) - if reduction == 'mean': + if reduction == "mean": return costs.mean() - elif reduction == 'sum': + elif reduction == "sum": return costs.sum() return costs diff --git a/torchaudio/kaldi_io.py b/torchaudio/kaldi_io.py index ba1689da2b..9cc69f9649 100644 --- a/torchaudio/kaldi_io.py +++ b/torchaudio/kaldi_io.py @@ -7,23 +7,23 @@ from torch import Tensor from torchaudio._internal import module_utils as _mod_utils -if _mod_utils.is_module_available('kaldi_io', 'numpy'): - import numpy as np +if _mod_utils.is_module_available("kaldi_io", "numpy"): import kaldi_io + import numpy as np __all__ = [ - 'read_vec_int_ark', - 'read_vec_flt_scp', - 'read_vec_flt_ark', - 'read_mat_scp', - 'read_mat_ark', + "read_vec_int_ark", + "read_vec_flt_scp", + "read_vec_flt_ark", + "read_mat_scp", + "read_mat_ark", ] -def _convert_method_output_to_tensor(file_or_fd: Any, - fn: Callable, - convert_contiguous: bool = False) -> Iterable[Tuple[str, Tensor]]: +def _convert_method_output_to_tensor( + file_or_fd: Any, fn: Callable, convert_contiguous: bool = False +) -> Iterable[Tuple[str, Tensor]]: r"""Takes a method invokes it. The output is converted to a tensor. Args: @@ -42,7 +42,7 @@ def _convert_method_output_to_tensor(file_or_fd: Any, yield key, torch.from_numpy(np_arr) -@_mod_utils.requires_module('kaldi_io', 'numpy') +@_mod_utils.requires_module("kaldi_io", "numpy") def read_vec_int_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: r"""Create generator of (key,vector) tuples, which reads from the ark file/stream. @@ -62,7 +62,7 @@ def read_vec_int_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_int_ark, convert_contiguous=True) -@_mod_utils.requires_module('kaldi_io', 'numpy') +@_mod_utils.requires_module("kaldi_io", "numpy") def read_vec_flt_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: r"""Create generator of (key,vector) tuples, read according to Kaldi scp. @@ -79,7 +79,7 @@ def read_vec_flt_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_flt_scp) -@_mod_utils.requires_module('kaldi_io', 'numpy') +@_mod_utils.requires_module("kaldi_io", "numpy") def read_vec_flt_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: r"""Create generator of (key,vector) tuples, which reads from the ark file/stream. @@ -96,7 +96,7 @@ def read_vec_flt_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_flt_ark) -@_mod_utils.requires_module('kaldi_io', 'numpy') +@_mod_utils.requires_module("kaldi_io", "numpy") def read_mat_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: r"""Create generator of (key,matrix) tuples, read according to Kaldi scp. @@ -113,7 +113,7 @@ def read_mat_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_mat_scp) -@_mod_utils.requires_module('kaldi_io', 'numpy') +@_mod_utils.requires_module("kaldi_io", "numpy") def read_mat_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: r"""Create generator of (key,matrix) tuples, which reads from the ark file/stream. diff --git a/torchaudio/models/__init__.py b/torchaudio/models/__init__.py index 956fc82e2f..78f746e32e 100644 --- a/torchaudio/models/__init__.py +++ b/torchaudio/models/__init__.py @@ -1,8 +1,7 @@ -from .wav2letter import Wav2Letter -from .wavernn import WaveRNN from .conv_tasnet import ConvTasNet from .deepspeech import DeepSpeech from .tacotron2 import Tacotron2 +from .wav2letter import Wav2Letter from .wav2vec2 import ( Wav2Vec2Model, wav2vec2_model, @@ -13,19 +12,20 @@ hubert_large, hubert_xlarge, ) +from .wavernn import WaveRNN __all__ = [ - 'Wav2Letter', - 'WaveRNN', - 'ConvTasNet', - 'DeepSpeech', - 'Wav2Vec2Model', - 'wav2vec2_model', - 'wav2vec2_base', - 'wav2vec2_large', - 'wav2vec2_large_lv60k', - 'hubert_base', - 'hubert_large', - 'hubert_xlarge', - 'Tacotron2', + "Wav2Letter", + "WaveRNN", + "ConvTasNet", + "DeepSpeech", + "Wav2Vec2Model", + "wav2vec2_model", + "wav2vec2_base", + "wav2vec2_large", + "wav2vec2_large_lv60k", + "hubert_base", + "hubert_large", + "hubert_xlarge", + "Tacotron2", ] diff --git a/torchaudio/models/conv_tasnet.py b/torchaudio/models/conv_tasnet.py index 39d3dae02f..767ef036ab 100644 --- a/torchaudio/models/conv_tasnet.py +++ b/torchaudio/models/conv_tasnet.py @@ -35,9 +35,7 @@ def __init__( super().__init__() self.conv_layers = torch.nn.Sequential( - torch.nn.Conv1d( - in_channels=io_channels, out_channels=hidden_channels, kernel_size=1 - ), + torch.nn.Conv1d(in_channels=io_channels, out_channels=hidden_channels, kernel_size=1), torch.nn.PReLU(), torch.nn.GroupNorm(num_groups=1, num_channels=hidden_channels, eps=1e-08), torch.nn.Conv1d( @@ -55,17 +53,11 @@ def __init__( self.res_out = ( None if no_residual - else torch.nn.Conv1d( - in_channels=hidden_channels, out_channels=io_channels, kernel_size=1 - ) - ) - self.skip_out = torch.nn.Conv1d( - in_channels=hidden_channels, out_channels=io_channels, kernel_size=1 + else torch.nn.Conv1d(in_channels=hidden_channels, out_channels=io_channels, kernel_size=1) ) + self.skip_out = torch.nn.Conv1d(in_channels=hidden_channels, out_channels=io_channels, kernel_size=1) - def forward( - self, input: torch.Tensor - ) -> Tuple[Optional[torch.Tensor], torch.Tensor]: + def forward(self, input: torch.Tensor) -> Tuple[Optional[torch.Tensor], torch.Tensor]: feature = self.conv_layers(input) if self.res_out is None: residual = None @@ -110,12 +102,8 @@ def __init__( self.input_dim = input_dim self.num_sources = num_sources - self.input_norm = torch.nn.GroupNorm( - num_groups=1, num_channels=input_dim, eps=1e-8 - ) - self.input_conv = torch.nn.Conv1d( - in_channels=input_dim, out_channels=num_feats, kernel_size=1 - ) + self.input_norm = torch.nn.GroupNorm(num_groups=1, num_channels=input_dim, eps=1e-8) + self.input_conv = torch.nn.Conv1d(in_channels=input_dim, out_channels=num_feats, kernel_size=1) self.receptive_field = 0 self.conv_layers = torch.nn.ModuleList([]) @@ -133,12 +121,12 @@ def __init__( no_residual=(l == (num_layers - 1) and s == (num_stacks - 1)), ) ) - self.receptive_field += ( - kernel_size if s == 0 and l == 0 else (kernel_size - 1) * multi - ) + self.receptive_field += kernel_size if s == 0 and l == 0 else (kernel_size - 1) * multi self.output_prelu = torch.nn.PReLU() self.output_conv = torch.nn.Conv1d( - in_channels=num_feats, out_channels=input_dim * num_sources, kernel_size=1, + in_channels=num_feats, + out_channels=input_dim * num_sources, + kernel_size=1, ) if msk_activate == "sigmoid": self.mask_activate = torch.nn.Sigmoid() @@ -239,9 +227,7 @@ def __init__( bias=False, ) - def _align_num_frames_with_strides( - self, input: torch.Tensor - ) -> Tuple[torch.Tensor, int]: + def _align_num_frames_with_strides(self, input: torch.Tensor) -> Tuple[torch.Tensor, int]: """Pad input Tensor so that the end of the input tensor corresponds with 1. (if kernel size is odd) the center of the last convolution kernel @@ -294,9 +280,7 @@ def forward(self, input: torch.Tensor) -> torch.Tensor: Tensor: 3D Tensor with shape [batch, channel==num_sources, frames] """ if input.ndim != 3 or input.shape[1] != 1: - raise ValueError( - f"Expected 3D tensor (batch, channel==1, frames). Found: {input.shape}" - ) + raise ValueError(f"Expected 3D tensor (batch, channel==1, frames). Found: {input.shape}") # B: batch size # L: input frame length @@ -309,13 +293,9 @@ def forward(self, input: torch.Tensor) -> torch.Tensor: batch_size, num_padded_frames = padded.shape[0], padded.shape[2] feats = self.encoder(padded) # B, F, M masked = self.mask_generator(feats) * feats.unsqueeze(1) # B, S, F, M - masked = masked.view( - batch_size * self.num_sources, self.enc_num_feats, -1 - ) # B*S, F, M + masked = masked.view(batch_size * self.num_sources, self.enc_num_feats, -1) # B*S, F, M decoded = self.decoder(masked) # B*S, 1, L' - output = decoded.view( - batch_size, self.num_sources, num_padded_frames - ) # B, S, L' + output = decoded.view(batch_size, self.num_sources, num_padded_frames) # B, S, L' if num_pads > 0: output = output[..., :-num_pads] # B, S, L return output diff --git a/torchaudio/models/deepspeech.py b/torchaudio/models/deepspeech.py index 41efc07d9e..e279498e49 100644 --- a/torchaudio/models/deepspeech.py +++ b/torchaudio/models/deepspeech.py @@ -10,11 +10,7 @@ class FullyConnected(torch.nn.Module): n_hidden: Internal hidden unit size. """ - def __init__(self, - n_feature: int, - n_hidden: int, - dropout: float, - relu_max_clip: int = 20) -> None: + def __init__(self, n_feature: int, n_hidden: int, dropout: float, relu_max_clip: int = 20) -> None: super(FullyConnected, self).__init__() self.fc = torch.nn.Linear(n_feature, n_hidden, bias=True) self.relu_max_clip = relu_max_clip @@ -52,9 +48,7 @@ def __init__( self.fc1 = FullyConnected(n_feature, n_hidden, dropout) self.fc2 = FullyConnected(n_hidden, n_hidden, dropout) self.fc3 = FullyConnected(n_hidden, n_hidden, dropout) - self.bi_rnn = torch.nn.RNN( - n_hidden, n_hidden, num_layers=1, nonlinearity="relu", bidirectional=True - ) + self.bi_rnn = torch.nn.RNN(n_hidden, n_hidden, num_layers=1, nonlinearity="relu", bidirectional=True) self.fc4 = FullyConnected(n_hidden, n_hidden, dropout) self.out = torch.nn.Linear(n_hidden, n_class) @@ -78,7 +72,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # T x N x H x, _ = self.bi_rnn(x) # The fifth (non-recurrent) layer takes both the forward and backward units as inputs - x = x[:, :, :self.n_hidden] + x[:, :, self.n_hidden:] + x = x[:, :, : self.n_hidden] + x[:, :, self.n_hidden :] # T x N x H x = self.fc4(x) # T x N x H diff --git a/torchaudio/models/tacotron2.py b/torchaudio/models/tacotron2.py index bd0b863ff3..fae472d33e 100644 --- a/torchaudio/models/tacotron2.py +++ b/torchaudio/models/tacotron2.py @@ -29,8 +29,8 @@ from typing import Tuple, List, Optional, Union import torch -from torch import nn from torch import Tensor +from torch import nn from torch.nn import functional as F @@ -39,9 +39,7 @@ ] -def _get_linear_layer( - in_dim: int, out_dim: int, bias: bool = True, w_init_gain: str = "linear" -) -> torch.nn.Linear: +def _get_linear_layer(in_dim: int, out_dim: int, bias: bool = True, w_init_gain: str = "linear") -> torch.nn.Linear: r"""Linear layer with xavier uniform initialization. Args: @@ -55,9 +53,7 @@ def _get_linear_layer( (torch.nn.Linear): The corresponding linear layer. """ linear = torch.nn.Linear(in_dim, out_dim, bias=bias) - torch.nn.init.xavier_uniform_( - linear.weight, gain=torch.nn.init.calculate_gain(w_init_gain) - ) + torch.nn.init.xavier_uniform_(linear.weight, gain=torch.nn.init.calculate_gain(w_init_gain)) return linear @@ -101,9 +97,7 @@ def _get_conv1d_layer( bias=bias, ) - torch.nn.init.xavier_uniform_( - conv1d.weight, gain=torch.nn.init.calculate_gain(w_init_gain) - ) + torch.nn.init.xavier_uniform_(conv1d.weight, gain=torch.nn.init.calculate_gain(w_init_gain)) return conv1d @@ -194,9 +188,7 @@ def __init__( attention_location_kernel_size: int, ) -> None: super().__init__() - self.query_layer = _get_linear_layer( - attention_rnn_dim, attention_hidden_dim, bias=False, w_init_gain="tanh" - ) + self.query_layer = _get_linear_layer(attention_rnn_dim, attention_hidden_dim, bias=False, w_init_gain="tanh") self.memory_layer = _get_linear_layer( encoder_embedding_dim, attention_hidden_dim, bias=False, w_init_gain="tanh" ) @@ -208,9 +200,7 @@ def __init__( ) self.score_mask_value = -float("inf") - def _get_alignment_energies( - self, query: Tensor, processed_memory: Tensor, attention_weights_cat: Tensor - ) -> Tensor: + def _get_alignment_energies(self, query: Tensor, processed_memory: Tensor, attention_weights_cat: Tensor) -> Tensor: r"""Get the alignment vector. Args: @@ -226,9 +216,7 @@ def _get_alignment_energies( processed_query = self.query_layer(query.unsqueeze(1)) processed_attention_weights = self.location_layer(attention_weights_cat) - energies = self.v( - torch.tanh(processed_query + processed_attention_weights + processed_memory) - ) + energies = self.v(torch.tanh(processed_query + processed_attention_weights + processed_memory)) alignment = energies.squeeze(2) return alignment @@ -256,9 +244,7 @@ def forward( attention_context (Tensor): Context vector with shape (n_batch, ``encoder_embedding_dim``). attention_weights (Tensor): Attention weights with shape (n_batch, max of ``text_lengths``). """ - alignment = self._get_alignment_energies( - attention_hidden_state, processed_memory, attention_weights_cat - ) + alignment = self._get_alignment_energies(attention_hidden_state, processed_memory, attention_weights_cat) alignment = alignment.masked_fill(mask, self.score_mask_value) @@ -281,10 +267,7 @@ def __init__(self, in_dim: int, out_sizes: List[int]) -> None: super().__init__() in_sizes = [in_dim] + out_sizes[:-1] self.layers = nn.ModuleList( - [ - _get_linear_layer(in_size, out_size, bias=False) - for (in_size, out_size) in zip(in_sizes, out_sizes) - ] + [_get_linear_layer(in_size, out_size, bias=False) for (in_size, out_size) in zip(in_sizes, out_sizes)] ) def forward(self, x: Tensor) -> Tensor: @@ -488,9 +471,7 @@ def __init__( self.prenet = _Prenet(n_mels * n_frames_per_step, [prenet_dim, prenet_dim]) - self.attention_rnn = nn.LSTMCell( - prenet_dim + encoder_embedding_dim, attention_rnn_dim - ) + self.attention_rnn = nn.LSTMCell(prenet_dim + encoder_embedding_dim, attention_rnn_dim) self.attention_layer = _Attention( attention_rnn_dim, @@ -500,13 +481,9 @@ def __init__( attention_location_kernel_size, ) - self.decoder_rnn = nn.LSTMCell( - attention_rnn_dim + encoder_embedding_dim, decoder_rnn_dim, True - ) + self.decoder_rnn = nn.LSTMCell(attention_rnn_dim + encoder_embedding_dim, decoder_rnn_dim, True) - self.linear_projection = _get_linear_layer( - decoder_rnn_dim + encoder_embedding_dim, n_mels * n_frames_per_step - ) + self.linear_projection = _get_linear_layer(decoder_rnn_dim + encoder_embedding_dim, n_mels * n_frames_per_step) self.gate_layer = _get_linear_layer( decoder_rnn_dim + encoder_embedding_dim, 1, bias=True, w_init_gain="sigmoid" @@ -526,9 +503,7 @@ def _get_initial_frame(self, memory: Tensor) -> Tensor: n_batch = memory.size(0) dtype = memory.dtype device = memory.device - decoder_input = torch.zeros( - n_batch, self.n_mels * self.n_frames_per_step, dtype=dtype, device=device - ) + decoder_input = torch.zeros(n_batch, self.n_mels * self.n_frames_per_step, dtype=dtype, device=device) return decoder_input def _initialize_decoder_states( @@ -557,27 +532,15 @@ def _initialize_decoder_states( dtype = memory.dtype device = memory.device - attention_hidden = torch.zeros( - n_batch, self.attention_rnn_dim, dtype=dtype, device=device - ) - attention_cell = torch.zeros( - n_batch, self.attention_rnn_dim, dtype=dtype, device=device - ) + attention_hidden = torch.zeros(n_batch, self.attention_rnn_dim, dtype=dtype, device=device) + attention_cell = torch.zeros(n_batch, self.attention_rnn_dim, dtype=dtype, device=device) - decoder_hidden = torch.zeros( - n_batch, self.decoder_rnn_dim, dtype=dtype, device=device - ) - decoder_cell = torch.zeros( - n_batch, self.decoder_rnn_dim, dtype=dtype, device=device - ) + decoder_hidden = torch.zeros(n_batch, self.decoder_rnn_dim, dtype=dtype, device=device) + decoder_cell = torch.zeros(n_batch, self.decoder_rnn_dim, dtype=dtype, device=device) attention_weights = torch.zeros(n_batch, max_time, dtype=dtype, device=device) - attention_weights_cum = torch.zeros( - n_batch, max_time, dtype=dtype, device=device - ) - attention_context = torch.zeros( - n_batch, self.encoder_embedding_dim, dtype=dtype, device=device - ) + attention_weights_cum = torch.zeros(n_batch, max_time, dtype=dtype, device=device) + attention_context = torch.zeros(n_batch, self.encoder_embedding_dim, dtype=dtype, device=device) processed_memory = self.attention_layer.memory_layer(memory) @@ -688,16 +651,10 @@ def decode( """ cell_input = torch.cat((decoder_input, attention_context), -1) - attention_hidden, attention_cell = self.attention_rnn( - cell_input, (attention_hidden, attention_cell) - ) - attention_hidden = F.dropout( - attention_hidden, self.attention_dropout, self.training - ) + attention_hidden, attention_cell = self.attention_rnn(cell_input, (attention_hidden, attention_cell)) + attention_hidden = F.dropout(attention_hidden, self.attention_dropout, self.training) - attention_weights_cat = torch.cat( - (attention_weights.unsqueeze(1), attention_weights_cum.unsqueeze(1)), dim=1 - ) + attention_weights_cat = torch.cat((attention_weights.unsqueeze(1), attention_weights_cum.unsqueeze(1)), dim=1) attention_context, attention_weights = self.attention_layer( attention_hidden, memory, processed_memory, attention_weights_cat, mask ) @@ -705,14 +662,10 @@ def decode( attention_weights_cum += attention_weights decoder_input = torch.cat((attention_hidden, attention_context), -1) - decoder_hidden, decoder_cell = self.decoder_rnn( - decoder_input, (decoder_hidden, decoder_cell) - ) + decoder_hidden, decoder_cell = self.decoder_rnn(decoder_input, (decoder_hidden, decoder_cell)) decoder_hidden = F.dropout(decoder_hidden, self.decoder_dropout, self.training) - decoder_hidden_attention_context = torch.cat( - (decoder_hidden, attention_context), dim=1 - ) + decoder_hidden_attention_context = torch.cat((decoder_hidden, attention_context), dim=1) decoder_output = self.linear_projection(decoder_hidden_attention_context) gate_prediction = self.gate_layer(decoder_hidden_attention_context) @@ -819,15 +772,11 @@ def _get_go_frame(self, memory: Tensor) -> Tensor: n_batch = memory.size(0) dtype = memory.dtype device = memory.device - decoder_input = torch.zeros( - n_batch, self.n_mels * self.n_frames_per_step, dtype=dtype, device=device - ) + decoder_input = torch.zeros(n_batch, self.n_mels * self.n_frames_per_step, dtype=dtype, device=device) return decoder_input @torch.jit.export - def infer(self, - memory: Tensor, - memory_lengths: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor]: + def infer(self, memory: Tensor, memory_lengths: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor]: """Decoder inference Args: @@ -905,16 +854,14 @@ def infer(self, if len(mel_specgrams) == self.decoder_max_step: warnings.warn( - "Reached max decoder steps. The generated spectrogram might not cover " - "the whole transcript.") + "Reached max decoder steps. The generated spectrogram might not cover " "the whole transcript." + ) mel_specgrams = torch.cat(mel_specgrams, dim=0) gate_outputs = torch.cat(gate_outputs, dim=0) alignments = torch.cat(alignments, dim=0) - mel_specgrams, gate_outputs, alignments = self._parse_decoder_outputs( - mel_specgrams, gate_outputs, alignments - ) + mel_specgrams, gate_outputs, alignments = self._parse_decoder_outputs(mel_specgrams, gate_outputs, alignments) return mel_specgrams, mel_specgram_lengths, gate_outputs, alignments @@ -984,9 +931,7 @@ def __init__( self.n_frames_per_step = n_frames_per_step self.embedding = nn.Embedding(n_symbol, symbol_embedding_dim) torch.nn.init.xavier_uniform_(self.embedding.weight) - self.encoder = _Encoder( - encoder_embedding_dim, encoder_n_convolution, encoder_kernel_size - ) + self.encoder = _Encoder(encoder_embedding_dim, encoder_n_convolution, encoder_kernel_size) self.decoder = _Decoder( n_mels, n_frames_per_step, @@ -1003,9 +948,7 @@ def __init__( prenet_dim, gate_threshold, ) - self.postnet = _Postnet( - n_mels, postnet_embedding_dim, postnet_kernel_size, postnet_n_convolution - ) + self.postnet = _Postnet(n_mels, postnet_embedding_dim, postnet_kernel_size, postnet_n_convolution) def forward( self, @@ -1094,9 +1037,7 @@ def infer(self, tokens: Tensor, lengths: Optional[Tensor] = None) -> Tuple[Tenso embedded_inputs = self.embedding(tokens).transpose(1, 2) encoder_outputs = self.encoder(embedded_inputs, lengths) - mel_specgram, mel_specgram_lengths, _, alignments = self.decoder.infer( - encoder_outputs, lengths - ) + mel_specgram, mel_specgram_lengths, _, alignments = self.decoder.infer(encoder_outputs, lengths) mel_outputs_postnet = self.postnet(mel_specgram) mel_outputs_postnet = mel_specgram + mel_outputs_postnet diff --git a/torchaudio/models/wav2letter.py b/torchaudio/models/wav2letter.py index 4d93e74392..0e88fe5b17 100644 --- a/torchaudio/models/wav2letter.py +++ b/torchaudio/models/wav2letter.py @@ -19,9 +19,7 @@ class Wav2Letter(nn.Module): num_features (int, optional): Number of input features that the network will receive (Default: ``1``). """ - def __init__(self, num_classes: int = 40, - input_type: str = "waveform", - num_features: int = 1) -> None: + def __init__(self, num_classes: int = 40, input_type: str = "waveform", num_features: int = 1) -> None: super(Wav2Letter, self).__init__() acoustic_num_features = 250 if input_type == "waveform" else num_features @@ -47,13 +45,13 @@ def __init__(self, num_classes: int = 40, nn.Conv1d(in_channels=2000, out_channels=2000, kernel_size=1, stride=1, padding=0), nn.ReLU(inplace=True), nn.Conv1d(in_channels=2000, out_channels=num_classes, kernel_size=1, stride=1, padding=0), - nn.ReLU(inplace=True) + nn.ReLU(inplace=True), ) if input_type == "waveform": waveform_model = nn.Sequential( nn.Conv1d(in_channels=num_features, out_channels=250, kernel_size=250, stride=160, padding=45), - nn.ReLU(inplace=True) + nn.ReLU(inplace=True), ) self.acoustic_model = nn.Sequential(waveform_model, acoustic_model) diff --git a/torchaudio/models/wav2vec2/__init__.py b/torchaudio/models/wav2vec2/__init__.py index f0538a4a3f..c9f443d092 100644 --- a/torchaudio/models/wav2vec2/__init__.py +++ b/torchaudio/models/wav2vec2/__init__.py @@ -1,3 +1,4 @@ +from . import utils from .model import ( Wav2Vec2Model, wav2vec2_model, @@ -8,16 +9,15 @@ hubert_large, hubert_xlarge, ) -from . import utils __all__ = [ - 'Wav2Vec2Model', - 'wav2vec2_model', - 'wav2vec2_base', - 'wav2vec2_large', - 'wav2vec2_large_lv60k', - 'hubert_base', - 'hubert_large', - 'hubert_xlarge', - 'utils', + "Wav2Vec2Model", + "wav2vec2_model", + "wav2vec2_base", + "wav2vec2_large", + "wav2vec2_large_lv60k", + "hubert_base", + "hubert_large", + "hubert_xlarge", + "utils", ] diff --git a/torchaudio/models/wav2vec2/components.py b/torchaudio/models/wav2vec2/components.py index 564a1428ba..dda95be06d 100644 --- a/torchaudio/models/wav2vec2/components.py +++ b/torchaudio/models/wav2vec2/components.py @@ -10,24 +10,25 @@ class LayerNorm(nn.LayerNorm): """Layer norm with transpose""" + def forward(self, input: Tensor) -> Tensor: x = input.transpose(-2, -1) - x = nn.functional.layer_norm( - x, self.normalized_shape, self.weight, self.bias, self.eps) + x = nn.functional.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps) x = x.transpose(-2, -1) return x class ConvLayerBlock(Module): """Convolution unit of FeatureExtractor""" + def __init__( - self, - in_channels: int, - out_channels: int, - kernel_size: int, - stride: int, - bias: bool, - layer_norm: Optional[Module], + self, + in_channels: int, + out_channels: int, + kernel_size: int, + stride: int, + bias: bool, + layer_norm: Optional[Module], ): super().__init__() self.kernel_size = kernel_size @@ -42,9 +43,9 @@ def __init__( ) def forward( - self, - x: Tensor, - length: Optional[Tensor], + self, + x: Tensor, + length: Optional[Tensor], ) -> Tuple[Tensor, Optional[Tensor]]: """ Args: @@ -60,7 +61,7 @@ def forward( x = nn.functional.gelu(x) if length is not None: - length = torch.div(length - self.kernel_size, self.stride, rounding_mode='floor') + 1 + length = torch.div(length - self.kernel_size, self.stride, rounding_mode="floor") + 1 # When input length is 0, the resulting length can be negative. So fix it here. length = torch.max(torch.zeros_like(length), length) return x, length @@ -73,17 +74,18 @@ class FeatureExtractor(Module): conv_layers (nn.ModuleList): convolution layers """ + def __init__( - self, - conv_layers: nn.ModuleList, + self, + conv_layers: nn.ModuleList, ): super().__init__() self.conv_layers = conv_layers def forward( - self, - x: Tensor, - length: Optional[Tensor], + self, + x: Tensor, + length: Optional[Tensor], ) -> Tuple[Tensor, Optional[Tensor]]: """ Args: @@ -100,9 +102,7 @@ def forward( Valid length of each output sample. shape: ``[batch, ]``. """ if x.ndim != 2: - raise ValueError( - "Expected the input Tensor to be 2D (batch, time), " - "but received {list(x.shape)}") + raise ValueError("Expected the input Tensor to be 2D (batch, time), " "but received {list(x.shape)}") x = x.unsqueeze(1) # (batch, channel==1, frame) for layer in self.conv_layers: @@ -121,15 +121,19 @@ class FeatureProjection(Module): out_features (int): Output feature dim. dropout (float): Dropout probability. """ + def __init__( - self, - in_features: int, - out_features: int, - dropout: float, + self, + in_features: int, + out_features: int, + dropout: float, ): super().__init__() self.layer_norm = nn.LayerNorm(in_features) - self.projection = nn.Linear(in_features, out_features,) + self.projection = nn.Linear( + in_features, + out_features, + ) self.dropout = nn.Dropout(dropout) def forward(self, x): @@ -154,11 +158,12 @@ class ConvolutionalPositionalEmbedding(Module): kernel_size (int): The number of frames to be use. groups (int): The number of groups in feature dimensions. """ + def __init__( - self, - embed_dim: int, - kernel_size: int, - groups: int, + self, + embed_dim: int, + kernel_size: int, + groups: int, ): super().__init__() self.embed_dim = embed_dim @@ -178,11 +183,8 @@ def __prepare_scriptable__(self): # normally we would do `if isinstance(...)` but this class is not accessible # because of shadowing, so we check the module name directly. # https://github.com/pytorch/pytorch/blob/be0ca00c5ce260eb5bcec3237357f7a30cc08983/torch/nn/utils/__init__.py#L3 - if ( - hook.__module__ == 'torch.nn.utils.weight_norm' and - hook.__class__.__name__ == 'WeightNorm' - ): - _LG.warning('Removing weight_norm from %s', self.__class__.__name__) + if hook.__module__ == "torch.nn.utils.weight_norm" and hook.__class__.__name__ == "WeightNorm": + _LG.warning("Removing weight_norm from %s", self.__class__.__name__) torch.nn.utils.remove_weight_norm(self.conv) return self @@ -197,7 +199,7 @@ def forward(self, x): x = x.transpose(-2, -1) x = self.conv(x) if self.num_remove > 0: - x = x[..., :-self.num_remove] + x = x[..., : -self.num_remove] x = torch.nn.functional.gelu(x) x = x.transpose(-2, -1) return x @@ -212,11 +214,12 @@ class SelfAttention(Module): dropout (float, optional): Dropout probabiliry on attn_output_weights. Default: ``0.0`` """ + def __init__( - self, - embed_dim: int, - num_heads: int, - dropout: float = 0.0, + self, + embed_dim: int, + num_heads: int, + dropout: float = 0.0, ): super().__init__() head_dim = embed_dim // num_heads @@ -236,9 +239,9 @@ def __init__( self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True) def forward( - self, - x: Tensor, - attention_mask: Optional[Tensor] = None, + self, + x: Tensor, + attention_mask: Optional[Tensor] = None, ) -> Tensor: """ Args: @@ -251,17 +254,13 @@ def forward( """ if x.ndim != 3 or x.shape[2] != self.embed_dim: raise ValueError( - f"The expected input shape is (batch, sequence, embed_dim=={self.embed_dim}). " - f"Found {x.shape}." + f"The expected input shape is (batch, sequence, embed_dim=={self.embed_dim}). " f"Found {x.shape}." ) batch_size, length, embed_dim = x.size() if attention_mask is not None: shape_ = (batch_size, 1, length, length) if attention_mask.size() != shape_: - raise ValueError( - f"The expected attention mask shape is {shape_}. " - f"Found {attention_mask.size()}." - ) + raise ValueError(f"The expected attention mask shape is {shape_}. " f"Found {attention_mask.size()}.") shape = (batch_size, length, self.num_heads, self.head_dim) q = self.q_proj(x).view(*shape).transpose(2, 1) # B, nH, L, Hd @@ -283,14 +282,14 @@ def forward( class FeedForward(Module): - """Layer that follows attention layer in encoder layer. - """ + """Layer that follows attention layer in encoder layer.""" + def __init__( - self, - io_features: int, - intermediate_features: int, - intermediate_dropout: float, - output_dropout: float, + self, + io_features: int, + intermediate_features: int, + intermediate_dropout: float, + output_dropout: float, ): super().__init__() self.intermediate_dense = nn.Linear(io_features, intermediate_features) @@ -315,14 +314,14 @@ def forward(self, x): class EncoderLayer(Module): - """A layer unit in encoder. Combines multihead self attention and feed forward. - """ + """A layer unit in encoder. Combines multihead self attention and feed forward.""" + def __init__( - self, - attention: Module, - dropout: float, - layer_norm_first: bool, - feed_forward: Module, + self, + attention: Module, + dropout: float, + layer_norm_first: bool, + feed_forward: Module, ): super().__init__() self.attention = attention @@ -333,9 +332,9 @@ def __init__( self.final_layer_norm = nn.LayerNorm(attention.embed_dim) def forward( - self, - x: Tensor, - attention_mask: Optional[Tensor] = None, + self, + x: Tensor, + attention_mask: Optional[Tensor] = None, ): """ Args: @@ -362,12 +361,12 @@ def forward( class Transformer(Module): def __init__( - self, - pos_conv_embed: Module, - dropout: float, - layers: Module, - layer_norm_first: bool, - layer_drop: float, + self, + pos_conv_embed: Module, + dropout: float, + layers: Module, + layer_norm_first: bool, + layer_drop: float, ): super().__init__() self.pos_conv_embed = pos_conv_embed @@ -387,9 +386,9 @@ def _preprocess(self, x: Tensor): return x def forward( - self, - x: Tensor, - attention_mask: Optional[Tensor] = None, + self, + x: Tensor, + attention_mask: Optional[Tensor] = None, ): x = self._preprocess(x) for layer in self.layers: @@ -402,14 +401,14 @@ def forward( return x def get_intermediate_outputs( - self, - x: Tensor, - attention_mask: Optional[Tensor] = None, - num_layers: Optional[int] = None, + self, + x: Tensor, + attention_mask: Optional[Tensor] = None, + num_layers: Optional[int] = None, ) -> List[Tensor]: if num_layers is not None: if not 0 < num_layers <= len(self.layers): - raise ValueError(f'`num_layers` must be between [1, {len(self.layers)}]') + raise ValueError(f"`num_layers` must be between [1, {len(self.layers)}]") ret: List[Tensor] = [] x = self._preprocess(x) @@ -423,18 +422,18 @@ def get_intermediate_outputs( class Encoder(Module): def __init__( - self, - feature_projection: Module, - transformer: Module, + self, + feature_projection: Module, + transformer: Module, ): super().__init__() self.feature_projection = feature_projection self.transformer = transformer def _preprocess( - self, - features: Tensor, - lengths: Optional[Tensor] = None, + self, + features: Tensor, + lengths: Optional[Tensor] = None, ) -> Tuple[Tensor, Optional[Tensor]]: x = self.feature_projection(features) @@ -450,30 +449,29 @@ def _preprocess( return x, mask def forward( - self, - features: Tensor, - lengths: Optional[Tensor] = None, + self, + features: Tensor, + lengths: Optional[Tensor] = None, ) -> Tensor: x, mask = self._preprocess(features, lengths) x = self.transformer(x, attention_mask=mask) return x def extract_features( - self, - features: Tensor, - lengths: Optional[Tensor] = None, - num_layers: Optional[int] = None, + self, + features: Tensor, + lengths: Optional[Tensor] = None, + num_layers: Optional[int] = None, ) -> List[Tensor]: x, masks = self._preprocess(features, lengths) - return self.transformer.get_intermediate_outputs( - x, attention_mask=masks, num_layers=num_layers) + return self.transformer.get_intermediate_outputs(x, attention_mask=masks, num_layers=num_layers) ################################################################################ def _get_feature_extractor( - norm_mode: str, - shapes: List[Tuple[int, int, int]], - bias: bool, + norm_mode: str, + shapes: List[Tuple[int, int, int]], + bias: bool, ) -> FeatureExtractor: """ Args: @@ -545,19 +543,19 @@ def _get_feature_extractor( def _get_encoder( - in_features: int, - embed_dim: int, - dropout_input: float, - pos_conv_kernel: int, - pos_conv_groups: int, - num_layers: int, - num_heads: int, - attention_dropout: float, - ff_interm_features: int, - ff_interm_dropout: float, - dropout: float, - layer_norm_first: bool, - layer_drop: float, + in_features: int, + embed_dim: int, + dropout_input: float, + pos_conv_kernel: int, + pos_conv_groups: int, + num_layers: int, + num_heads: int, + attention_dropout: float, + ff_interm_features: int, + ff_interm_dropout: float, + dropout: float, + layer_norm_first: bool, + layer_drop: float, ) -> Encoder: """ Args: diff --git a/torchaudio/models/wav2vec2/model.py b/torchaudio/models/wav2vec2/model.py index a28742a239..a4aa74853f 100644 --- a/torchaudio/models/wav2vec2/model.py +++ b/torchaudio/models/wav2vec2/model.py @@ -26,11 +26,12 @@ class Wav2Vec2Model(Module): aux (torch.nn.Module or None, optional): Auxiliary module. If provided, the output from encoder is passed to this module. """ # noqa: E501 + def __init__( - self, - feature_extractor: Module, - encoder: Module, - aux: Optional[Module] = None, + self, + feature_extractor: Module, + encoder: Module, + aux: Optional[Module] = None, ): super().__init__() self.feature_extractor = feature_extractor @@ -39,10 +40,10 @@ def __init__( @torch.jit.export def extract_features( - self, - waveforms: Tensor, - lengths: Optional[Tensor] = None, - num_layers: Optional[int] = None, + self, + waveforms: Tensor, + lengths: Optional[Tensor] = None, + num_layers: Optional[int] = None, ) -> Tuple[List[Tensor], Optional[Tensor]]: """Extract feature vectors from raw waveforms @@ -81,9 +82,9 @@ def extract_features( return x, lengths def forward( - self, - waveforms: Tensor, - lengths: Optional[Tensor] = None, + self, + waveforms: Tensor, + lengths: Optional[Tensor] = None, ) -> Tuple[Tensor, Optional[Tensor]]: """Compute the sequence of probability distribution over labels. @@ -117,22 +118,22 @@ def forward( def wav2vec2_model( - extractor_mode: str, - extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], - extractor_conv_bias: bool, - encoder_embed_dim: int, - encoder_projection_dropout: float, - encoder_pos_conv_kernel: int, - encoder_pos_conv_groups: int, - encoder_num_layers: int, - encoder_num_heads: int, - encoder_attention_dropout: float, - encoder_ff_interm_features: int, - encoder_ff_interm_dropout: float, - encoder_dropout: float, - encoder_layer_norm_first: bool, - encoder_layer_drop: float, - aux_num_out: Optional[int], + extractor_mode: str, + extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], + extractor_conv_bias: bool, + encoder_embed_dim: int, + encoder_projection_dropout: float, + encoder_pos_conv_kernel: int, + encoder_pos_conv_groups: int, + encoder_num_layers: int, + encoder_num_heads: int, + encoder_attention_dropout: float, + encoder_ff_interm_features: int, + encoder_ff_interm_dropout: float, + encoder_dropout: float, + encoder_layer_norm_first: bool, + encoder_layer_drop: float, + aux_num_out: Optional[int], ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """wav2vec2_model(extractor_mode: str, extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], extractor_conv_bias: bool, encoder_embed_dim: int, encoder_projection_dropout: float, encoder_pos_conv_kernel: int, encoder_pos_conv_groups: int, encoder_num_layers: int, encoder_num_heads: int, encoder_attention_dropout: float, encoder_ff_interm_features: int, encoder_ff_interm_dropout: float, encoder_dropout: float, encoder_layer_norm_first: bool, encoder_layer_drop: float, aux_num_out: Optional[int]) -> torchaudio.models.Wav2Vec2Model @@ -262,7 +263,8 @@ def wav2vec2_model( extractor_conv_layer_config = [(512, 10, 5)] + [(512, 3, 2)] * 4 + [(512, 2, 2)] * 2 feature_extractor = components._get_feature_extractor( - extractor_mode, extractor_conv_layer_config, extractor_conv_bias) + extractor_mode, extractor_conv_layer_config, extractor_conv_bias + ) encoder = components._get_encoder( in_features=extractor_conv_layer_config[-1][0], embed_dim=encoder_embed_dim, @@ -285,12 +287,12 @@ def wav2vec2_model( def wav2vec2_base( - encoder_projection_dropout: float = 0.1, - encoder_attention_dropout: float = 0.1, - encoder_ff_interm_dropout: float = 0.1, - encoder_dropout: float = 0.1, - encoder_layer_drop: float = 0.1, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.1, + encoder_attention_dropout: float = 0.1, + encoder_ff_interm_dropout: float = 0.1, + encoder_dropout: float = 0.1, + encoder_layer_drop: float = 0.1, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """wav2vec2_base(encoder_projection_dropout: float = 0.1, encoder_attention_dropout: float = 0.1, encoder_ff_interm_dropout: float = 0.1, encoder_dropout: float = 0.1, encoder_layer_drop: float = 0.1, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -336,12 +338,12 @@ def wav2vec2_base( def wav2vec2_large( - encoder_projection_dropout: float = 0.1, - encoder_attention_dropout: float = 0.1, - encoder_ff_interm_dropout: float = 0.1, - encoder_dropout: float = 0.1, - encoder_layer_drop: float = 0.1, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.1, + encoder_attention_dropout: float = 0.1, + encoder_ff_interm_dropout: float = 0.1, + encoder_dropout: float = 0.1, + encoder_layer_drop: float = 0.1, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """wav2vec2_large(encoder_projection_dropout: float = 0.1, encoder_attention_dropout: float = 0.1, encoder_ff_interm_dropout: float = 0.1, encoder_dropout: float = 0.1, encoder_layer_drop: float = 0.1, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -387,12 +389,12 @@ def wav2vec2_large( def wav2vec2_large_lv60k( - encoder_projection_dropout: float = 0.1, - encoder_attention_dropout: float = 0.0, - encoder_ff_interm_dropout: float = 0.1, - encoder_dropout: float = 0.0, - encoder_layer_drop: float = 0.1, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.1, + encoder_attention_dropout: float = 0.0, + encoder_ff_interm_dropout: float = 0.1, + encoder_dropout: float = 0.0, + encoder_layer_drop: float = 0.1, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """wav2vec2_large_lv60k( encoder_projection_dropout: float = 0.1, encoder_attention_dropout: float = 0.0, encoder_ff_interm_dropout: float = 0.1, encoder_dropout: float = 0.0, encoder_layer_drop: float = 0.1, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -438,12 +440,12 @@ def wav2vec2_large_lv60k( def hubert_base( - encoder_projection_dropout: float = 0.1, - encoder_attention_dropout: float = 0.1, - encoder_ff_interm_dropout: float = 0.0, - encoder_dropout: float = 0.1, - encoder_layer_drop: float = 0.05, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.1, + encoder_attention_dropout: float = 0.1, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.1, + encoder_layer_drop: float = 0.05, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """hubert_base(encoder_projection_dropout: float = 0.1, encoder_attention_dropout: float = 0.1, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.1, encoder_layer_drop: float = 0.05, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -469,7 +471,7 @@ def hubert_base( The resulting model. """ # noqa: E501 return wav2vec2_model( - extractor_mode='group_norm', + extractor_mode="group_norm", extractor_conv_layer_config=None, extractor_conv_bias=False, encoder_embed_dim=768, @@ -489,12 +491,12 @@ def hubert_base( def hubert_large( - encoder_projection_dropout: float = 0.0, - encoder_attention_dropout: float = 0.0, - encoder_ff_interm_dropout: float = 0.0, - encoder_dropout: float = 0.0, - encoder_layer_drop: float = 0.0, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.0, + encoder_attention_dropout: float = 0.0, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.0, + encoder_layer_drop: float = 0.0, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """hubert_large(encoder_projection_dropout: float = 0.0, encoder_attention_dropout: float = 0.0, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.0, encoder_layer_drop: float = 0.0, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -520,7 +522,7 @@ def hubert_large( The resulting model. """ # noqa: E501 return wav2vec2_model( - extractor_mode='layer_norm', + extractor_mode="layer_norm", extractor_conv_layer_config=None, extractor_conv_bias=False, encoder_embed_dim=1024, @@ -540,12 +542,12 @@ def hubert_large( def hubert_xlarge( - encoder_projection_dropout: float = 0.0, - encoder_attention_dropout: float = 0.0, - encoder_ff_interm_dropout: float = 0.0, - encoder_dropout: float = 0.0, - encoder_layer_drop: float = 0.0, - aux_num_out: Optional[int] = None, + encoder_projection_dropout: float = 0.0, + encoder_attention_dropout: float = 0.0, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.0, + encoder_layer_drop: float = 0.0, + aux_num_out: Optional[int] = None, ) -> Wav2Vec2Model: # Overriding the signature so that the return type is correct on Sphinx """hubert_xlarge(encoder_projection_dropout: float = 0.0, encoder_attention_dropout: float = 0.0, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.0, encoder_layer_drop: float = 0.0, aux_num_out: Optional[int] = None) -> torchaudio.models.Wav2Vec2Model @@ -571,7 +573,7 @@ def hubert_xlarge( The resulting model. """ # noqa: E501 return wav2vec2_model( - extractor_mode='layer_norm', + extractor_mode="layer_norm", extractor_conv_layer_config=None, extractor_conv_bias=False, encoder_embed_dim=1280, diff --git a/torchaudio/models/wav2vec2/utils/__init__.py b/torchaudio/models/wav2vec2/utils/__init__.py index 9d9fb218bb..0457b5dd70 100644 --- a/torchaudio/models/wav2vec2/utils/__init__.py +++ b/torchaudio/models/wav2vec2/utils/__init__.py @@ -1,7 +1,7 @@ -from .import_huggingface import import_huggingface_model from .import_fairseq import import_fairseq_model +from .import_huggingface import import_huggingface_model __all__ = [ - 'import_huggingface_model', - 'import_fairseq_model', + "import_huggingface_model", + "import_fairseq_model", ] diff --git a/torchaudio/models/wav2vec2/utils/import_fairseq.py b/torchaudio/models/wav2vec2/utils/import_fairseq.py index e285d3d1e1..46ffac79e7 100644 --- a/torchaudio/models/wav2vec2/utils/import_fairseq.py +++ b/torchaudio/models/wav2vec2/utils/import_fairseq.py @@ -13,11 +13,11 @@ def _parse_config(w2v_model): encoder = w2v_model.encoder conv_layers = w2v_model.feature_extractor.conv_layers - extractor_mode = 'layer_norm' - if 'GroupNorm' in conv_layers[0][2].__class__.__name__: - extractor_mode = 'group_norm' + extractor_mode = "layer_norm" + if "GroupNorm" in conv_layers[0][2].__class__.__name__: + extractor_mode = "group_norm" else: - extractor_mode = 'layer_norm' + extractor_mode = "layer_norm" conv_layer_config = [(l[0].out_channels, l[0].kernel_size[0], l[0].stride[0]) for l in conv_layers] @@ -26,53 +26,52 @@ def _parse_config(w2v_model): elif all(l[0].bias is not None for l in conv_layers): conv_bias = True else: - raise ValueError( - 'Either all the convolutions layers have bias term or none of them should.') + raise ValueError("Either all the convolutions layers have bias term or none of them should.") config = { - 'extractor_mode': extractor_mode, - 'extractor_conv_layer_config': conv_layer_config, - 'extractor_conv_bias': conv_bias, - 'encoder_embed_dim': w2v_model.post_extract_proj.out_features, - 'encoder_projection_dropout': w2v_model.dropout_input.p, - 'encoder_pos_conv_kernel': encoder.pos_conv[0].kernel_size[0], - 'encoder_pos_conv_groups': encoder.pos_conv[0].groups, - 'encoder_num_layers': len(encoder.layers), - 'encoder_num_heads': encoder.layers[0].self_attn.num_heads, - 'encoder_attention_dropout': encoder.layers[0].self_attn.dropout_module.p, - 'encoder_ff_interm_features': encoder.layers[0].fc1.out_features, - 'encoder_ff_interm_dropout': encoder.layers[0].dropout2.p, - 'encoder_dropout': encoder.layers[0].dropout3.p, - 'encoder_layer_norm_first': encoder.layer_norm_first, - 'encoder_layer_drop': encoder.layerdrop, + "extractor_mode": extractor_mode, + "extractor_conv_layer_config": conv_layer_config, + "extractor_conv_bias": conv_bias, + "encoder_embed_dim": w2v_model.post_extract_proj.out_features, + "encoder_projection_dropout": w2v_model.dropout_input.p, + "encoder_pos_conv_kernel": encoder.pos_conv[0].kernel_size[0], + "encoder_pos_conv_groups": encoder.pos_conv[0].groups, + "encoder_num_layers": len(encoder.layers), + "encoder_num_heads": encoder.layers[0].self_attn.num_heads, + "encoder_attention_dropout": encoder.layers[0].self_attn.dropout_module.p, + "encoder_ff_interm_features": encoder.layers[0].fc1.out_features, + "encoder_ff_interm_dropout": encoder.layers[0].dropout2.p, + "encoder_dropout": encoder.layers[0].dropout3.p, + "encoder_layer_norm_first": encoder.layer_norm_first, + "encoder_layer_drop": encoder.layerdrop, } return config def _map_key(key): key_ = key - if key.startswith('w2v_model.'): - key = key.replace('w2v_model.', '') - if re.match(r'(mask_emb|quantizer|project_q|final_proj|mask_emb)', key): + if key.startswith("w2v_model."): + key = key.replace("w2v_model.", "") + if re.match(r"(mask_emb|quantizer|project_q|final_proj|mask_emb)", key): return None # Feature Extractor # Group norm when "extractor_mode" is "default". # (Only the first layer) # "conv_layers.0.2.weight" -> "conv_layers.0.layer_norm.weight" # "conv_layers.0.2.bias" -> "conv_layers.0.layer_norm.bias" - match = re.match(r'feature_extractor\.conv_layers\.0\.2\.(weight|bias)', key) + match = re.match(r"feature_extractor\.conv_layers\.0\.2\.(weight|bias)", key) if match: return f"feature_extractor.conv_layers.0.layer_norm.{match.group(1)}" # Convolutions # "conv_layers.X.0.weight" -> "conv_layers.X.conv.weight" # "conv_layers.X.0.bias" -> "conv_layers.X.conv.bias" - match = re.match(r'feature_extractor\.conv_layers\.(\d+)\.0\.(weight|bias)', key) + match = re.match(r"feature_extractor\.conv_layers\.(\d+)\.0\.(weight|bias)", key) if match: return f"feature_extractor.conv_layers.{match.group(1)}.conv.{match.group(2)}" # Layer norm when "extractor_mode" is "layer_norm". # "conv_layers.X.2.1.weight" -> "conv_layers.X.layer_norm.weight" # "conv_layers.X.2.1.bias" -> "conv_layers.X.layer_norm.bias" - match = re.match(r'feature_extractor\.conv_layers\.(\d+)\.2\.1\.(weight|bias)', key) + match = re.match(r"feature_extractor\.conv_layers\.(\d+)\.2\.1\.(weight|bias)", key) if match: return f"feature_extractor.conv_layers.{match.group(1)}.layer_norm.{match.group(2)}" match = re.match(r"post_extract_proj\.(weight|bias)", key) @@ -111,9 +110,9 @@ def _map_key(key): if match: return f"aux.{match.group(1)}" # HuBERT Extension - if key in ['label_embs_concat']: + if key in ["label_embs_concat"]: return key - raise ValueError(f'Unexpected key: {key_}') + raise ValueError(f"Unexpected key: {key_}") def _convert_state_dict(state_dict): @@ -179,16 +178,15 @@ def import_fairseq_model(original: Module) -> Wav2Vec2Model: .. _fairseq: https://github.com/pytorch/fairseq """ class_ = original.__class__.__name__ - if class_ == 'Wav2Vec2Model': + if class_ == "Wav2Vec2Model": return _import_wav2vec2_pretraining(original) - if class_ == 'Wav2VecEncoder': + if class_ == "Wav2VecEncoder": return _import_wav2vec2_finetuning(original) - if class_ == 'HubertModel': + if class_ == "HubertModel": return _import_hubert_pretraining(original) - if class_ == 'HubertEncoder': + if class_ == "HubertEncoder": return _import_hubert_finetuning(original) - raise ValueError( - f'Expected an instance of `Wav2Vec2Model` or `Wav2VecEncoder`. Found: {class_}') + raise ValueError(f"Expected an instance of `Wav2Vec2Model` or `Wav2VecEncoder`. Found: {class_}") def _import_wav2vec2_finetuning(original: Module) -> Wav2Vec2Model: diff --git a/torchaudio/models/wav2vec2/utils/import_huggingface.py b/torchaudio/models/wav2vec2/utils/import_huggingface.py index c1a5c4133c..540a99a8dc 100644 --- a/torchaudio/models/wav2vec2/utils/import_huggingface.py +++ b/torchaudio/models/wav2vec2/utils/import_huggingface.py @@ -11,40 +11,38 @@ def _get_config(cfg): config = { - 'extractor_mode': f'{cfg.feat_extract_norm}_norm', - 'extractor_conv_layer_config': list(zip(cfg.conv_dim, cfg.conv_kernel, cfg.conv_stride)), - 'extractor_conv_bias': cfg.conv_bias, - 'encoder_embed_dim': cfg.hidden_size, - 'encoder_projection_dropout': cfg.feat_proj_dropout, - 'encoder_pos_conv_kernel': cfg.num_conv_pos_embeddings, - 'encoder_pos_conv_groups': cfg.num_conv_pos_embedding_groups, - 'encoder_num_layers': cfg.num_hidden_layers, - 'encoder_num_heads': cfg.num_attention_heads, - 'encoder_attention_dropout': cfg.attention_dropout, - 'encoder_ff_interm_features': cfg.intermediate_size, - 'encoder_ff_interm_dropout': cfg.activation_dropout, - 'encoder_dropout': cfg.hidden_dropout, - 'encoder_layer_norm_first': cfg.do_stable_layer_norm, - 'encoder_layer_drop': cfg.layerdrop, + "extractor_mode": f"{cfg.feat_extract_norm}_norm", + "extractor_conv_layer_config": list(zip(cfg.conv_dim, cfg.conv_kernel, cfg.conv_stride)), + "extractor_conv_bias": cfg.conv_bias, + "encoder_embed_dim": cfg.hidden_size, + "encoder_projection_dropout": cfg.feat_proj_dropout, + "encoder_pos_conv_kernel": cfg.num_conv_pos_embeddings, + "encoder_pos_conv_groups": cfg.num_conv_pos_embedding_groups, + "encoder_num_layers": cfg.num_hidden_layers, + "encoder_num_heads": cfg.num_attention_heads, + "encoder_attention_dropout": cfg.attention_dropout, + "encoder_ff_interm_features": cfg.intermediate_size, + "encoder_ff_interm_dropout": cfg.activation_dropout, + "encoder_dropout": cfg.hidden_dropout, + "encoder_layer_norm_first": cfg.do_stable_layer_norm, + "encoder_layer_drop": cfg.layerdrop, } return config def _build(config, original): - if original.__class__.__name__ == 'Wav2Vec2ForCTC': + if original.__class__.__name__ == "Wav2Vec2ForCTC": aux_num_out = original.config.vocab_size wav2vec2 = original.wav2vec2 else: - _LG.warning( - 'The model is not an instance of Wav2Vec2ForCTC. ' - '"lm_head" module is not imported.') + _LG.warning("The model is not an instance of Wav2Vec2ForCTC. " '"lm_head" module is not imported.') aux_num_out = None wav2vec2 = original imported = wav2vec2_model(**config, aux_num_out=aux_num_out) imported.feature_extractor.load_state_dict(wav2vec2.feature_extractor.state_dict()) imported.encoder.feature_projection.load_state_dict(wav2vec2.feature_projection.state_dict()) imported.encoder.transformer.load_state_dict(wav2vec2.encoder.state_dict()) - if original.__class__.__name__ == 'Wav2Vec2ForCTC': + if original.__class__.__name__ == "Wav2Vec2ForCTC": imported.aux.load_state_dict(original.lm_head.state_dict()) return imported @@ -71,10 +69,10 @@ def import_huggingface_model(original: Module) -> Wav2Vec2Model: .. _Transformers: https://huggingface.co/transformers/ """ - _LG.info('Importing model.') - _LG.info('Loading model configuration.') + _LG.info("Importing model.") + _LG.info("Loading model configuration.") config = _get_config(original.config) - _LG.debug(' - config: %s', config) - _LG.info('Building model.') + _LG.debug(" - config: %s", config) + _LG.info("Building model.") imported = _build(config, original) return imported diff --git a/torchaudio/models/wavernn.py b/torchaudio/models/wavernn.py index 0a41a2aa96..447f2f3a85 100644 --- a/torchaudio/models/wavernn.py +++ b/torchaudio/models/wavernn.py @@ -1,10 +1,10 @@ -from typing import List, Tuple, Optional import math +from typing import List, Tuple, Optional import torch +import torch.nn.functional as F from torch import Tensor from torch import nn -import torch.nn.functional as F __all__ = [ "ResBlock", @@ -35,7 +35,7 @@ def __init__(self, n_freq: int = 128) -> None: nn.BatchNorm1d(n_freq), nn.ReLU(inplace=True), nn.Conv1d(in_channels=n_freq, out_channels=n_freq, kernel_size=1, bias=False), - nn.BatchNorm1d(n_freq) + nn.BatchNorm1d(n_freq), ) def forward(self, specgram: Tensor) -> Tensor: @@ -66,12 +66,9 @@ class MelResNet(nn.Module): >>> output = melresnet(input) # shape: (10, 128, 508) """ - def __init__(self, - n_res_block: int = 10, - n_freq: int = 128, - n_hidden: int = 128, - n_output: int = 128, - kernel_size: int = 5) -> None: + def __init__( + self, n_res_block: int = 10, n_freq: int = 128, n_hidden: int = 128, n_output: int = 128, kernel_size: int = 5 + ) -> None: super().__init__() ResBlocks = [ResBlock(n_hidden) for _ in range(n_res_block)] @@ -81,7 +78,7 @@ def __init__(self, nn.BatchNorm1d(n_hidden), nn.ReLU(inplace=True), *ResBlocks, - nn.Conv1d(in_channels=n_hidden, out_channels=n_output, kernel_size=1) + nn.Conv1d(in_channels=n_hidden, out_channels=n_output, kernel_size=1), ) def forward(self, specgram: Tensor) -> Tensor: @@ -110,9 +107,7 @@ class Stretch2d(nn.Module): >>> output = stretch2d(input) # shape: (10, 500, 5120) """ - def __init__(self, - time_scale: int, - freq_scale: int) -> None: + def __init__(self, time_scale: int, freq_scale: int) -> None: super().__init__() self.freq_scale = freq_scale @@ -148,13 +143,15 @@ class UpsampleNetwork(nn.Module): >>> output = upsamplenetwork(input) # shape: (10, 1536, 128), (10, 1536, 128) """ - def __init__(self, - upsample_scales: List[int], - n_res_block: int = 10, - n_freq: int = 128, - n_hidden: int = 128, - n_output: int = 128, - kernel_size: int = 5) -> None: + def __init__( + self, + upsample_scales: List[int], + n_res_block: int = 10, + n_freq: int = 128, + n_hidden: int = 128, + n_output: int = 128, + kernel_size: int = 5, + ) -> None: super().__init__() total_scale = 1 @@ -169,12 +166,10 @@ def __init__(self, up_layers = [] for scale in upsample_scales: stretch = Stretch2d(scale, 1) - conv = nn.Conv2d(in_channels=1, - out_channels=1, - kernel_size=(1, scale * 2 + 1), - padding=(0, scale), - bias=False) - torch.nn.init.constant_(conv.weight, 1. / (scale * 2 + 1)) + conv = nn.Conv2d( + in_channels=1, out_channels=1, kernel_size=(1, scale * 2 + 1), padding=(0, scale), bias=False + ) + torch.nn.init.constant_(conv.weight, 1.0 / (scale * 2 + 1)) up_layers.append(stretch) up_layers.append(conv) self.upsample_layers = nn.Sequential(*up_layers) @@ -197,7 +192,7 @@ def forward(self, specgram: Tensor) -> Tuple[Tensor, Tensor]: specgram = specgram.unsqueeze(1) upsampling_output = self.upsample_layers(specgram) - upsampling_output = upsampling_output.squeeze(1)[:, :, self.indent:-self.indent] + upsampling_output = upsampling_output.squeeze(1)[:, :, self.indent : -self.indent] return upsampling_output, resnet_output @@ -230,17 +225,19 @@ class WaveRNN(nn.Module): >>> # output shape: (n_batch, n_channel, (n_time - kernel_size + 1) * hop_length, n_classes) """ - def __init__(self, - upsample_scales: List[int], - n_classes: int, - hop_length: int, - n_res_block: int = 10, - n_rnn: int = 512, - n_fc: int = 512, - kernel_size: int = 5, - n_freq: int = 128, - n_hidden: int = 128, - n_output: int = 128) -> None: + def __init__( + self, + upsample_scales: List[int], + n_classes: int, + hop_length: int, + n_res_block: int = 10, + n_rnn: int = 512, + n_fc: int = 512, + kernel_size: int = 5, + n_freq: int = 128, + n_hidden: int = 128, + n_output: int = 128, + ) -> None: super().__init__() self.kernel_size = kernel_size @@ -257,12 +254,7 @@ def __init__(self, if total_scale != self.hop_length: raise ValueError(f"Expected: total_scale == hop_length, but found {total_scale} != {hop_length}") - self.upsample = UpsampleNetwork(upsample_scales, - n_res_block, - n_freq, - n_hidden, - n_output, - kernel_size) + self.upsample = UpsampleNetwork(upsample_scales, n_res_block, n_freq, n_hidden, n_output, kernel_size) self.fc = nn.Linear(n_freq + self.n_aux + 1, n_rnn) self.rnn1 = nn.GRU(n_rnn, n_rnn, batch_first=True) @@ -286,8 +278,8 @@ def forward(self, waveform: Tensor, specgram: Tensor) -> Tensor: Tensor: shape (n_batch, 1, (n_time - kernel_size + 1) * hop_length, n_classes) """ - assert waveform.size(1) == 1, 'Require the input channel of waveform is 1' - assert specgram.size(1) == 1, 'Require the input channel of specgram is 1' + assert waveform.size(1) == 1, "Require the input channel of waveform is 1" + assert specgram.size(1) == 1, "Require the input channel of specgram is 1" # remove channel dimension until the end waveform, specgram = waveform.squeeze(1), specgram.squeeze(1) @@ -302,10 +294,10 @@ def forward(self, waveform: Tensor, specgram: Tensor) -> Tensor: aux = aux.transpose(1, 2) aux_idx = [self.n_aux * i for i in range(5)] - a1 = aux[:, :, aux_idx[0]:aux_idx[1]] - a2 = aux[:, :, aux_idx[1]:aux_idx[2]] - a3 = aux[:, :, aux_idx[2]:aux_idx[3]] - a4 = aux[:, :, aux_idx[3]:aux_idx[4]] + a1 = aux[:, :, aux_idx[0] : aux_idx[1]] + a2 = aux[:, :, aux_idx[1] : aux_idx[2]] + a3 = aux[:, :, aux_idx[2] : aux_idx[3]] + a4 = aux[:, :, aux_idx[3] : aux_idx[4]] x = torch.cat([waveform.unsqueeze(-1), specgram, a1], dim=-1) x = self.fc(x) @@ -375,7 +367,7 @@ def infer(self, specgram: Tensor, lengths: Optional[Tensor] = None) -> Tuple[Ten h2 = torch.zeros((1, b_size, self.n_rnn), device=device, dtype=dtype) x = torch.zeros((b_size, 1), device=device, dtype=dtype) - aux_split = [aux[:, self.n_aux * i: self.n_aux * (i + 1), :] for i in range(4)] + aux_split = [aux[:, self.n_aux * i : self.n_aux * (i + 1), :] for i in range(4)] for i in range(seq_len): diff --git a/torchaudio/pipelines/__init__.py b/torchaudio/pipelines/__init__.py index d0eaa609fb..60b654a371 100644 --- a/torchaudio/pipelines/__init__.py +++ b/torchaudio/pipelines/__init__.py @@ -1,3 +1,10 @@ +from ._tts import ( + Tacotron2TTSBundle, + TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH, + TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH, + TACOTRON2_WAVERNN_CHAR_LJSPEECH, + TACOTRON2_WAVERNN_PHONE_LJSPEECH, +) from ._wav2vec2.impl import ( Wav2Vec2Bundle, Wav2Vec2ASRBundle, @@ -25,43 +32,36 @@ HUBERT_ASR_LARGE, HUBERT_ASR_XLARGE, ) -from ._tts import ( - Tacotron2TTSBundle, - TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH, - TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH, - TACOTRON2_WAVERNN_CHAR_LJSPEECH, - TACOTRON2_WAVERNN_PHONE_LJSPEECH, -) __all__ = [ - 'Wav2Vec2Bundle', - 'Wav2Vec2ASRBundle', - 'WAV2VEC2_BASE', - 'WAV2VEC2_LARGE', - 'WAV2VEC2_LARGE_LV60K', - 'WAV2VEC2_ASR_BASE_10M', - 'WAV2VEC2_ASR_BASE_100H', - 'WAV2VEC2_ASR_BASE_960H', - 'WAV2VEC2_ASR_LARGE_10M', - 'WAV2VEC2_ASR_LARGE_100H', - 'WAV2VEC2_ASR_LARGE_960H', - 'WAV2VEC2_ASR_LARGE_LV60K_10M', - 'WAV2VEC2_ASR_LARGE_LV60K_100H', - 'WAV2VEC2_ASR_LARGE_LV60K_960H', - 'WAV2VEC2_XLSR53', - 'VOXPOPULI_ASR_BASE_10K_EN', - 'VOXPOPULI_ASR_BASE_10K_ES', - 'VOXPOPULI_ASR_BASE_10K_DE', - 'VOXPOPULI_ASR_BASE_10K_FR', - 'VOXPOPULI_ASR_BASE_10K_IT', - 'HUBERT_BASE', - 'HUBERT_LARGE', - 'HUBERT_XLARGE', - 'HUBERT_ASR_LARGE', - 'HUBERT_ASR_XLARGE', - 'Tacotron2TTSBundle', - 'TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH', - 'TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH', - 'TACOTRON2_WAVERNN_CHAR_LJSPEECH', - 'TACOTRON2_WAVERNN_PHONE_LJSPEECH', + "Wav2Vec2Bundle", + "Wav2Vec2ASRBundle", + "WAV2VEC2_BASE", + "WAV2VEC2_LARGE", + "WAV2VEC2_LARGE_LV60K", + "WAV2VEC2_ASR_BASE_10M", + "WAV2VEC2_ASR_BASE_100H", + "WAV2VEC2_ASR_BASE_960H", + "WAV2VEC2_ASR_LARGE_10M", + "WAV2VEC2_ASR_LARGE_100H", + "WAV2VEC2_ASR_LARGE_960H", + "WAV2VEC2_ASR_LARGE_LV60K_10M", + "WAV2VEC2_ASR_LARGE_LV60K_100H", + "WAV2VEC2_ASR_LARGE_LV60K_960H", + "WAV2VEC2_XLSR53", + "VOXPOPULI_ASR_BASE_10K_EN", + "VOXPOPULI_ASR_BASE_10K_ES", + "VOXPOPULI_ASR_BASE_10K_DE", + "VOXPOPULI_ASR_BASE_10K_FR", + "VOXPOPULI_ASR_BASE_10K_IT", + "HUBERT_BASE", + "HUBERT_LARGE", + "HUBERT_XLARGE", + "HUBERT_ASR_LARGE", + "HUBERT_ASR_XLARGE", + "Tacotron2TTSBundle", + "TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH", + "TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH", + "TACOTRON2_WAVERNN_CHAR_LJSPEECH", + "TACOTRON2_WAVERNN_PHONE_LJSPEECH", ] diff --git a/torchaudio/pipelines/_tts/__init__.py b/torchaudio/pipelines/_tts/__init__.py index c361707c5b..02851f596c 100644 --- a/torchaudio/pipelines/_tts/__init__.py +++ b/torchaudio/pipelines/_tts/__init__.py @@ -1,16 +1,16 @@ -from .interface import Tacotron2TTSBundle from .impl import ( TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH, TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH, TACOTRON2_WAVERNN_CHAR_LJSPEECH, TACOTRON2_WAVERNN_PHONE_LJSPEECH, ) +from .interface import Tacotron2TTSBundle __all__ = [ - 'Tacotron2TTSBundle', - 'TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH', - 'TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH', - 'TACOTRON2_WAVERNN_CHAR_LJSPEECH', - 'TACOTRON2_WAVERNN_PHONE_LJSPEECH', + "Tacotron2TTSBundle", + "TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH", + "TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH", + "TACOTRON2_WAVERNN_CHAR_LJSPEECH", + "TACOTRON2_WAVERNN_PHONE_LJSPEECH", ] diff --git a/torchaudio/pipelines/_tts/impl.py b/torchaudio/pipelines/_tts/impl.py index 20138172b3..bd8fc9755d 100644 --- a/torchaudio/pipelines/_tts/impl.py +++ b/torchaudio/pipelines/_tts/impl.py @@ -1,20 +1,20 @@ -from dataclasses import dataclass import re +from dataclasses import dataclass from typing import Union, Optional, Dict, Any, Tuple, List import torch from torch import Tensor - from torchaudio._internal import load_state_dict_from_url -from torchaudio.models import Tacotron2, WaveRNN from torchaudio.functional import mu_law_decoding +from torchaudio.models import Tacotron2, WaveRNN from torchaudio.transforms import InverseMelScale, GriffinLim + from . import utils from .interface import Tacotron2TTSBundle __all__ = [] -_BASE_URL = 'https://download.pytorch.org/torchaudio/models' +_BASE_URL = "https://download.pytorch.org/torchaudio/models" ################################################################################ @@ -44,8 +44,7 @@ def __init__(self, *, dl_kwargs=None): super().__init__() self._tokens = utils._get_phones() self._mapping = {p: i for i, p in enumerate(self._tokens)} - self._phonemizer = utils._load_phonemizer( - 'en_us_cmudict_forward.pt', dl_kwargs=dl_kwargs) + self._phonemizer = utils._load_phonemizer("en_us_cmudict_forward.pt", dl_kwargs=dl_kwargs) self._pattern = r"(\[[A-Z]+?\]|[_!'(),.:;? -])" @property @@ -57,9 +56,9 @@ def __call__(self, texts: Union[str, List[str]]) -> Tuple[Tensor, Tensor]: texts = [texts] indices = [] - for phones in self._phonemizer(texts, lang='en_us'): + for phones in self._phonemizer(texts, lang="en_us"): # '[F][UW][B][AA][R]!' -> ['F', 'UW', 'B', 'AA', 'R', '!'] - ret = [re.sub(r'[\[\]]', '', r) for r in re.findall(self._pattern, phones)] + ret = [re.sub(r"[\[\]]", "", r) for r in re.findall(self._pattern, phones)] indices.append([self._mapping[p] for p in ret]) return utils._to_tensor(indices) @@ -68,12 +67,9 @@ def __call__(self, texts: Union[str, List[str]]) -> Tuple[Tensor, Tensor]: # Pipeline implementation - Vocoder ################################################################################ + class _WaveRNNVocoder(torch.nn.Module, Tacotron2TTSBundle.Vocoder): - def __init__( - self, - model: WaveRNN, - min_level_db: Optional[float] = -100 - ): + def __init__(self, model: WaveRNN, min_level_db: Optional[float] = -100): super().__init__() self._sample_rate = 22050 self._model = model @@ -104,10 +100,10 @@ def __init__(self): n_stft=(1024 // 2 + 1), n_mels=80, sample_rate=self.sample_rate, - f_min=0., - f_max=8000., + f_min=0.0, + f_max=8000.0, mel_scale="slaney", - norm='slaney', + norm="slaney", ) self._griffin_lim = GriffinLim( n_fft=1024, @@ -151,7 +147,7 @@ class _Tacotron2Mixin: def get_tacotron2(self, *, dl_kwargs=None) -> Tacotron2: model = Tacotron2(**self._tacotron2_params) - url = f'{_BASE_URL}/{self._tacotron2_path}' + url = f"{_BASE_URL}/{self._tacotron2_path}" dl_kwargs = {} if dl_kwargs is None else dl_kwargs state_dict = load_state_dict_from_url(url, **dl_kwargs) model.load_state_dict(state_dict) @@ -170,7 +166,7 @@ def get_vocoder(self, *, dl_kwargs=None): def _get_wavernn(self, *, dl_kwargs=None): model = WaveRNN(**self._wavernn_params) - url = f'{_BASE_URL}/{self._wavernn_path}' + url = f"{_BASE_URL}/{self._wavernn_path}" dl_kwargs = {} if dl_kwargs is None else dl_kwargs state_dict = load_state_dict_from_url(url, **dl_kwargs) model.load_state_dict(state_dict) @@ -214,11 +210,10 @@ class _Tacotron2GriffinLimPhoneBundle(_GriffinLimMixin, _Tacotron2Mixin, _PhoneM TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH = _Tacotron2GriffinLimCharBundle( - _tacotron2_path='tacotron2_english_characters_1500_epochs_ljspeech.pth', + _tacotron2_path="tacotron2_english_characters_1500_epochs_ljspeech.pth", _tacotron2_params=utils._get_taco_params(n_symbols=38), ) -TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH.__doc__ = ( - '''Character-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and +TACOTRON2_GRIFFINLIM_CHAR_LJSPEECH.__doc__ = """Character-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and :py:class:`torchaudio.transforms.GriffinLim`. The text processor encodes the input texts character-by-character. @@ -254,14 +249,13 @@ class _Tacotron2GriffinLimPhoneBundle(_GriffinLimMixin, _Tacotron2Mixin, _PhoneM Your browser does not support the audio element. -''') # noqa: E501 +""" # noqa: E501 TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH = _Tacotron2GriffinLimPhoneBundle( - _tacotron2_path='tacotron2_english_phonemes_1500_epochs_ljspeech.pth', + _tacotron2_path="tacotron2_english_phonemes_1500_epochs_ljspeech.pth", _tacotron2_params=utils._get_taco_params(n_symbols=96), ) -TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH.__doc__ = ( - '''Phoneme-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and +TACOTRON2_GRIFFINLIM_PHONE_LJSPEECH.__doc__ = """Phoneme-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and :py:class:`torchaudio.transforms.GriffinLim`. The text processor encodes the input texts based on phoneme. @@ -302,16 +296,15 @@ class _Tacotron2GriffinLimPhoneBundle(_GriffinLimMixin, _Tacotron2Mixin, _PhoneM Your browser does not support the audio element. -''') # noqa: E501 +""" # noqa: E501 TACOTRON2_WAVERNN_CHAR_LJSPEECH = _Tacotron2WaveRNNCharBundle( - _tacotron2_path='tacotron2_english_characters_1500_epochs_wavernn_ljspeech.pth', + _tacotron2_path="tacotron2_english_characters_1500_epochs_wavernn_ljspeech.pth", _tacotron2_params=utils._get_taco_params(n_symbols=38), - _wavernn_path='wavernn_10k_epochs_8bits_ljspeech.pth', + _wavernn_path="wavernn_10k_epochs_8bits_ljspeech.pth", _wavernn_params=utils._get_wrnn_params(), ) -TACOTRON2_WAVERNN_CHAR_LJSPEECH.__doc__ = ( - '''Character-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and +TACOTRON2_WAVERNN_CHAR_LJSPEECH.__doc__ = """Character-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and :py:class:`torchaudio.models.WaveRNN`. The text processor encodes the input texts character-by-character. @@ -350,16 +343,15 @@ class _Tacotron2GriffinLimPhoneBundle(_GriffinLimMixin, _Tacotron2Mixin, _PhoneM Your browser does not support the audio element. -''') # noqa: E501 +""" # noqa: E501 TACOTRON2_WAVERNN_PHONE_LJSPEECH = _Tacotron2WaveRNNPhoneBundle( - _tacotron2_path='tacotron2_english_phonemes_1500_epochs_wavernn_ljspeech.pth', + _tacotron2_path="tacotron2_english_phonemes_1500_epochs_wavernn_ljspeech.pth", _tacotron2_params=utils._get_taco_params(n_symbols=96), - _wavernn_path='wavernn_10k_epochs_8bits_ljspeech.pth', + _wavernn_path="wavernn_10k_epochs_8bits_ljspeech.pth", _wavernn_params=utils._get_wrnn_params(), ) -TACOTRON2_WAVERNN_PHONE_LJSPEECH.__doc__ = ( - '''Phoneme-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and +TACOTRON2_WAVERNN_PHONE_LJSPEECH.__doc__ = """Phoneme-based TTS pipeline with :py:class:`torchaudio.models.Tacotron2` and :py:class:`torchaudio.models.WaveRNN`. The text processor encodes the input texts based on phoneme. @@ -403,4 +395,4 @@ class _Tacotron2GriffinLimPhoneBundle(_GriffinLimMixin, _Tacotron2Mixin, _PhoneM Your browser does not support the audio element. -''') # noqa: E501 +""" # noqa: E501 diff --git a/torchaudio/pipelines/_tts/interface.py b/torchaudio/pipelines/_tts/interface.py index bb725ba4fe..ec201c43b0 100644 --- a/torchaudio/pipelines/_tts/interface.py +++ b/torchaudio/pipelines/_tts/interface.py @@ -2,7 +2,6 @@ from typing import Union, List, Tuple, Optional from torch import Tensor - from torchaudio.models import Tacotron2 diff --git a/torchaudio/pipelines/_tts/utils.py b/torchaudio/pipelines/_tts/utils.py index 2b4c14b48c..9416dddd9a 100644 --- a/torchaudio/pipelines/_tts/utils.py +++ b/torchaudio/pipelines/_tts/utils.py @@ -1,8 +1,7 @@ -import os import logging +import os import torch - from torchaudio._internal import ( download_url_to_file, module_utils as _mod_utils, @@ -11,44 +10,44 @@ def _get_chars(): return ( - '_', - '-', - '!', + "_", + "-", + "!", "'", - '(', - ')', - ',', - '.', - ':', - ';', - '?', - ' ', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'm', - 'n', - 'o', - 'p', - 'q', - 'r', - 's', - 't', - 'u', - 'v', - 'w', - 'x', - 'y', - 'z', + "(", + ")", + ",", + ".", + ":", + ";", + "?", + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", ) @@ -149,7 +148,7 @@ def _get_phones(): "W", "Y", "Z", - "ZH" + "ZH", ) @@ -161,18 +160,18 @@ def _to_tensor(indices): def _load_phonemizer(file, dl_kwargs): - if not _mod_utils.is_module_available('dp'): - raise RuntimeError('DeepPhonemizer is not installed. Please install it.') + if not _mod_utils.is_module_available("dp"): + raise RuntimeError("DeepPhonemizer is not installed. Please install it.") from dp.phonemizer import Phonemizer # By default, dp issues DEBUG level log. - logger = logging.getLogger('dp') + logger = logging.getLogger("dp") orig_level = logger.level logger.setLevel(logging.INFO) try: - url = f'https://public-asai-dl-models.s3.eu-central-1.amazonaws.com/DeepPhonemizer/{file}' - directory = os.path.join(torch.hub.get_dir(), 'checkpoints') + url = f"https://public-asai-dl-models.s3.eu-central-1.amazonaws.com/DeepPhonemizer/{file}" + directory = os.path.join(torch.hub.get_dir(), "checkpoints") os.makedirs(directory, exist_ok=True) path = os.path.join(directory, file) if not os.path.exists(path): @@ -192,41 +191,41 @@ def _unnormalize_waveform(waveform: torch.Tensor, bits: int) -> torch.Tensor: def _get_taco_params(n_symbols): return { - 'mask_padding': False, - 'n_mels': 80, - 'n_frames_per_step': 1, - 'symbol_embedding_dim': 512, - 'encoder_embedding_dim': 512, - 'encoder_n_convolution': 3, - 'encoder_kernel_size': 5, - 'decoder_rnn_dim': 1024, - 'decoder_max_step': 2000, - 'decoder_dropout': 0.1, - 'decoder_early_stopping': True, - 'attention_rnn_dim': 1024, - 'attention_hidden_dim': 128, - 'attention_location_n_filter': 32, - 'attention_location_kernel_size': 31, - 'attention_dropout': 0.1, - 'prenet_dim': 256, - 'postnet_n_convolution': 5, - 'postnet_kernel_size': 5, - 'postnet_embedding_dim': 512, - 'gate_threshold': 0.5, - 'n_symbol': n_symbols, + "mask_padding": False, + "n_mels": 80, + "n_frames_per_step": 1, + "symbol_embedding_dim": 512, + "encoder_embedding_dim": 512, + "encoder_n_convolution": 3, + "encoder_kernel_size": 5, + "decoder_rnn_dim": 1024, + "decoder_max_step": 2000, + "decoder_dropout": 0.1, + "decoder_early_stopping": True, + "attention_rnn_dim": 1024, + "attention_hidden_dim": 128, + "attention_location_n_filter": 32, + "attention_location_kernel_size": 31, + "attention_dropout": 0.1, + "prenet_dim": 256, + "postnet_n_convolution": 5, + "postnet_kernel_size": 5, + "postnet_embedding_dim": 512, + "gate_threshold": 0.5, + "n_symbol": n_symbols, } def _get_wrnn_params(): return { - 'upsample_scales': [5, 5, 11], - 'n_classes': 2 ** 8, # n_bits = 8 - 'hop_length': 275, - 'n_res_block': 10, - 'n_rnn': 512, - 'n_fc': 512, - 'kernel_size': 5, - 'n_freq': 80, - 'n_hidden': 128, - 'n_output': 128 + "upsample_scales": [5, 5, 11], + "n_classes": 2 ** 8, # n_bits = 8 + "hop_length": 275, + "n_res_block": 10, + "n_rnn": 512, + "n_fc": 512, + "kernel_size": 5, + "n_freq": 80, + "n_hidden": 128, + "n_output": 128, } diff --git a/torchaudio/pipelines/_wav2vec2/impl.py b/torchaudio/pipelines/_wav2vec2/impl.py index 01624e62ac..4ea7e043b7 100644 --- a/torchaudio/pipelines/_wav2vec2/impl.py +++ b/torchaudio/pipelines/_wav2vec2/impl.py @@ -2,9 +2,9 @@ from typing import Dict, Tuple, Any import torch - from torchaudio._internal import load_state_dict_from_url from torchaudio.models import wav2vec2_model, Wav2Vec2Model + from . import utils @@ -43,6 +43,7 @@ class Wav2Vec2Bundle: >>> # Extract acoustic features >>> features, _ = model.extract_features(waveform) """ # noqa: E501 + _path: str _params: Dict[str, Any] _sample_rate: float @@ -56,7 +57,7 @@ def sample_rate(self) -> float: return self._sample_rate def _get_state_dict(self, dl_kwargs): - url = f'https://download.pytorch.org/torchaudio/models/{self._path}' + url = f"https://download.pytorch.org/torchaudio/models/{self._path}" dl_kwargs = {} if dl_kwargs is None else dl_kwargs state_dict = load_state_dict_from_url(url, **dl_kwargs) return state_dict @@ -120,13 +121,14 @@ class Wav2Vec2ASRBundle(Wav2Vec2Bundle): >>> # `ctc_decode` is for illustration purpose only >>> transcripts = ctc_decode(emissions, labels) """ # noqa: E501 + _labels: Tuple[str] _remove_aux_axis: Tuple[int] = (1, 2, 3) def get_labels( - self, - *, - blank: str = '-', + self, + *, + blank: str = "-", ) -> Tuple[str]: """The output class labels (only applicable to fine-tuned bundles) @@ -161,17 +163,17 @@ def _get_state_dict(self, dl_kwargs): # that resembles mistake. # The label `1` shows up in the training dataset of German (1 out of 16M), # English (1 / 28M), Spanish (1 / 9.4M), Romanian (1 / 4.7M) and Polish (6 / 5.8M) - for key in ['aux.weight', 'aux.bias']: + for key in ["aux.weight", "aux.bias"]: t = state_dict[key] state_dict[key] = torch.stack([t[i] for i in range(t.size(0)) if i not in self._remove_aux_axis]) return state_dict WAV2VEC2_BASE = Wav2Vec2Bundle( - _path='wav2vec2_fairseq_base_ls960.pth', + _path="wav2vec2_fairseq_base_ls960.pth", _params={ - 'extractor_mode': 'group_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "group_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -180,19 +182,19 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 768, - 'encoder_projection_dropout': 0.1, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 12, - 'encoder_num_heads': 12, - 'encoder_attention_dropout': 0.1, - 'encoder_ff_interm_features': 3072, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.1, - 'encoder_layer_norm_first': False, - 'encoder_layer_drop': 0.05, + "extractor_conv_bias": False, + "encoder_embed_dim": 768, + "encoder_projection_dropout": 0.1, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 12, + "encoder_num_heads": 12, + "encoder_attention_dropout": 0.1, + "encoder_ff_interm_features": 3072, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.1, + "encoder_layer_norm_first": False, + "encoder_layer_drop": 0.05, "aux_num_out": None, }, _sample_rate=16000, @@ -212,10 +214,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_BASE_10M = Wav2Vec2ASRBundle( - _path='wav2vec2_fairseq_base_ls960_asr_ll10m.pth', + _path="wav2vec2_fairseq_base_ls960_asr_ll10m.pth", _params={ - 'extractor_mode': 'group_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "group_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -224,19 +226,19 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 768, - 'encoder_projection_dropout': 0.1, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 12, - 'encoder_num_heads': 12, - 'encoder_attention_dropout': 0.1, - 'encoder_ff_interm_features': 3072, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.1, - 'encoder_layer_norm_first': False, - 'encoder_layer_drop': 0.05, + "extractor_conv_bias": False, + "encoder_embed_dim": 768, + "encoder_projection_dropout": 0.1, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 12, + "encoder_num_heads": 12, + "encoder_attention_dropout": 0.1, + "encoder_ff_interm_features": 3072, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.1, + "encoder_layer_norm_first": False, + "encoder_layer_drop": 0.05, "aux_num_out": 29, }, _labels=utils._get_en_labels(), @@ -258,10 +260,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_BASE_100H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_base_ls960_asr_ls100.pth', + "wav2vec2_fairseq_base_ls960_asr_ls100.pth", { - 'extractor_mode': 'group_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "group_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -270,19 +272,19 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 768, - 'encoder_projection_dropout': 0.1, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 12, - 'encoder_num_heads': 12, - 'encoder_attention_dropout': 0.1, - 'encoder_ff_interm_features': 3072, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.1, - 'encoder_layer_norm_first': False, - 'encoder_layer_drop': 0.05, + "extractor_conv_bias": False, + "encoder_embed_dim": 768, + "encoder_projection_dropout": 0.1, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 12, + "encoder_num_heads": 12, + "encoder_attention_dropout": 0.1, + "encoder_ff_interm_features": 3072, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.1, + "encoder_layer_norm_first": False, + "encoder_layer_drop": 0.05, "aux_num_out": 29, }, _labels=utils._get_en_labels(), @@ -304,7 +306,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_BASE_960H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_base_ls960_asr_ls960.pth', + "wav2vec2_fairseq_base_ls960_asr_ls960.pth", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -349,7 +351,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_LARGE = Wav2Vec2Bundle( - 'wav2vec2_fairseq_large_ls960.pth', + "wav2vec2_fairseq_large_ls960.pth", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -393,7 +395,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_10M = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_ls960_asr_ll10m.pth', + "wav2vec2_fairseq_large_ls960_asr_ll10m.pth", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -439,7 +441,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_100H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_ls960_asr_ls100.pth', + "wav2vec2_fairseq_large_ls960_asr_ls100.pth", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -485,7 +487,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_960H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_ls960_asr_ls960.pth', + "wav2vec2_fairseq_large_ls960_asr_ls960.pth", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -530,7 +532,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_LARGE_LV60K = Wav2Vec2Bundle( - 'wav2vec2_fairseq_large_lv60k.pth', + "wav2vec2_fairseq_large_lv60k.pth", { "extractor_mode": "layer_norm", "extractor_conv_layer_config": [ @@ -574,7 +576,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_LV60K_10M = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_lv60k_asr_ll10m.pth', + "wav2vec2_fairseq_large_lv60k_asr_ll10m.pth", { "extractor_mode": "layer_norm", "extractor_conv_layer_config": [ @@ -620,7 +622,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_LV60K_100H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_lv60k_asr_ls100.pth', + "wav2vec2_fairseq_large_lv60k_asr_ls100.pth", { "extractor_mode": "layer_norm", "extractor_conv_layer_config": [ @@ -666,7 +668,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_ASR_LARGE_LV60K_960H = Wav2Vec2ASRBundle( - 'wav2vec2_fairseq_large_lv60k_asr_ls960.pth', + "wav2vec2_fairseq_large_lv60k_asr_ls960.pth", { "extractor_mode": "layer_norm", "extractor_conv_layer_config": [ @@ -713,7 +715,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 WAV2VEC2_XLSR53 = Wav2Vec2Bundle( - 'wav2vec2_fairseq_large_xlsr53.pth', + "wav2vec2_fairseq_large_xlsr53.pth", { "extractor_mode": "layer_norm", "extractor_conv_layer_config": [ @@ -760,10 +762,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 HUBERT_BASE = Wav2Vec2Bundle( - 'hubert_fairseq_base_ls960.pth', + "hubert_fairseq_base_ls960.pth", { - 'extractor_mode': 'group_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "group_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -772,20 +774,20 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 768, - 'encoder_projection_dropout': 0.1, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 12, - 'encoder_num_heads': 12, - 'encoder_attention_dropout': 0.1, - 'encoder_ff_interm_features': 3072, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.1, - 'encoder_layer_norm_first': False, - 'encoder_layer_drop': 0.05, - 'aux_num_out': None, + "extractor_conv_bias": False, + "encoder_embed_dim": 768, + "encoder_projection_dropout": 0.1, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 12, + "encoder_num_heads": 12, + "encoder_attention_dropout": 0.1, + "encoder_ff_interm_features": 3072, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.1, + "encoder_layer_norm_first": False, + "encoder_layer_drop": 0.05, + "aux_num_out": None, }, _sample_rate=16000, ) @@ -804,10 +806,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 HUBERT_LARGE = Wav2Vec2Bundle( - 'hubert_fairseq_large_ll60k.pth', + "hubert_fairseq_large_ll60k.pth", { - 'extractor_mode': 'layer_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "layer_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -816,20 +818,20 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 1024, - 'encoder_projection_dropout': 0.0, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 24, - 'encoder_num_heads': 16, - 'encoder_attention_dropout': 0.0, - 'encoder_ff_interm_features': 4096, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.0, - 'encoder_layer_norm_first': True, - 'encoder_layer_drop': 0.0, - 'aux_num_out': None, + "extractor_conv_bias": False, + "encoder_embed_dim": 1024, + "encoder_projection_dropout": 0.0, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 24, + "encoder_num_heads": 16, + "encoder_attention_dropout": 0.0, + "encoder_ff_interm_features": 4096, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.0, + "encoder_layer_norm_first": True, + "encoder_layer_drop": 0.0, + "aux_num_out": None, }, _sample_rate=16000, ) @@ -848,10 +850,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 HUBERT_XLARGE = Wav2Vec2Bundle( - 'hubert_fairseq_xlarge_ll60k.pth', + "hubert_fairseq_xlarge_ll60k.pth", { - 'extractor_mode': 'layer_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "layer_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -860,20 +862,20 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 1280, - 'encoder_projection_dropout': 0.0, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 48, - 'encoder_num_heads': 16, - 'encoder_attention_dropout': 0.0, - 'encoder_ff_interm_features': 5120, - 'encoder_ff_interm_dropout': 0.0, - 'encoder_dropout': 0.0, - 'encoder_layer_norm_first': True, - 'encoder_layer_drop': 0.0, - 'aux_num_out': None, + "extractor_conv_bias": False, + "encoder_embed_dim": 1280, + "encoder_projection_dropout": 0.0, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 48, + "encoder_num_heads": 16, + "encoder_attention_dropout": 0.0, + "encoder_ff_interm_features": 5120, + "encoder_ff_interm_dropout": 0.0, + "encoder_dropout": 0.0, + "encoder_layer_norm_first": True, + "encoder_layer_drop": 0.0, + "aux_num_out": None, }, _sample_rate=16000, ) @@ -892,10 +894,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 HUBERT_ASR_LARGE = Wav2Vec2ASRBundle( - 'hubert_fairseq_large_ll60k_asr_ls960.pth', + "hubert_fairseq_large_ll60k_asr_ls960.pth", { - 'extractor_mode': 'layer_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "layer_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -904,20 +906,20 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 1024, - 'encoder_projection_dropout': 0.0, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 24, - 'encoder_num_heads': 16, - 'encoder_attention_dropout': 0.0, - 'encoder_ff_interm_features': 4096, - 'encoder_ff_interm_dropout': 0.1, - 'encoder_dropout': 0.0, - 'encoder_layer_norm_first': True, - 'encoder_layer_drop': 0.1, - 'aux_num_out': 29, + "extractor_conv_bias": False, + "encoder_embed_dim": 1024, + "encoder_projection_dropout": 0.0, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 24, + "encoder_num_heads": 16, + "encoder_attention_dropout": 0.0, + "encoder_ff_interm_features": 4096, + "encoder_ff_interm_dropout": 0.1, + "encoder_dropout": 0.0, + "encoder_layer_norm_first": True, + "encoder_layer_drop": 0.1, + "aux_num_out": 29, }, _labels=utils._get_en_labels(), _sample_rate=16000, @@ -939,10 +941,10 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 HUBERT_ASR_XLARGE = Wav2Vec2ASRBundle( - 'hubert_fairseq_xlarge_ll60k_asr_ls960.pth', + "hubert_fairseq_xlarge_ll60k_asr_ls960.pth", { - 'extractor_mode': 'layer_norm', - 'extractor_conv_layer_config': [ + "extractor_mode": "layer_norm", + "extractor_conv_layer_config": [ (512, 10, 5), (512, 3, 2), (512, 3, 2), @@ -951,20 +953,20 @@ def _get_state_dict(self, dl_kwargs): (512, 2, 2), (512, 2, 2), ], - 'extractor_conv_bias': False, - 'encoder_embed_dim': 1280, - 'encoder_projection_dropout': 0.0, - 'encoder_pos_conv_kernel': 128, - 'encoder_pos_conv_groups': 16, - 'encoder_num_layers': 48, - 'encoder_num_heads': 16, - 'encoder_attention_dropout': 0.0, - 'encoder_ff_interm_features': 5120, - 'encoder_ff_interm_dropout': 0.1, - 'encoder_dropout': 0.0, - 'encoder_layer_norm_first': True, - 'encoder_layer_drop': 0.1, - 'aux_num_out': 29, + "extractor_conv_bias": False, + "encoder_embed_dim": 1280, + "encoder_projection_dropout": 0.0, + "encoder_pos_conv_kernel": 128, + "encoder_pos_conv_groups": 16, + "encoder_num_layers": 48, + "encoder_num_heads": 16, + "encoder_attention_dropout": 0.0, + "encoder_ff_interm_features": 5120, + "encoder_ff_interm_dropout": 0.1, + "encoder_dropout": 0.0, + "encoder_layer_norm_first": True, + "encoder_layer_drop": 0.1, + "aux_num_out": 29, }, _labels=utils._get_en_labels(), _sample_rate=16000, @@ -987,7 +989,7 @@ def _get_state_dict(self, dl_kwargs): VOXPOPULI_ASR_BASE_10K_DE = Wav2Vec2ASRBundle( - 'wav2vec2_voxpopuli_base_10k_asr_de.pt', + "wav2vec2_voxpopuli_base_10k_asr_de.pt", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -1034,7 +1036,7 @@ def _get_state_dict(self, dl_kwargs): VOXPOPULI_ASR_BASE_10K_EN = Wav2Vec2ASRBundle( - 'wav2vec2_voxpopuli_base_10k_asr_en.pt', + "wav2vec2_voxpopuli_base_10k_asr_en.pt", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -1059,7 +1061,7 @@ def _get_state_dict(self, dl_kwargs): "encoder_dropout": 0.0, "encoder_layer_norm_first": False, "encoder_layer_drop": 0.1, - "aux_num_out": 28 + "aux_num_out": 28, }, _labels=utils._get_vp_en_labels(), _sample_rate=16000, @@ -1081,7 +1083,7 @@ def _get_state_dict(self, dl_kwargs): VOXPOPULI_ASR_BASE_10K_ES = Wav2Vec2ASRBundle( - 'wav2vec2_voxpopuli_base_10k_asr_es.pt', + "wav2vec2_voxpopuli_base_10k_asr_es.pt", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -1106,7 +1108,7 @@ def _get_state_dict(self, dl_kwargs): "encoder_dropout": 0.0, "encoder_layer_norm_first": False, "encoder_layer_drop": 0.1, - "aux_num_out": 35 + "aux_num_out": 35, }, _labels=utils._get_es_labels(), _sample_rate=16000, @@ -1127,7 +1129,7 @@ def _get_state_dict(self, dl_kwargs): """ # noqa: E501 VOXPOPULI_ASR_BASE_10K_FR = Wav2Vec2ASRBundle( - 'wav2vec2_voxpopuli_base_10k_asr_fr.pt', + "wav2vec2_voxpopuli_base_10k_asr_fr.pt", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ @@ -1152,7 +1154,7 @@ def _get_state_dict(self, dl_kwargs): "encoder_dropout": 0.0, "encoder_layer_norm_first": False, "encoder_layer_drop": 0.1, - "aux_num_out": 43 + "aux_num_out": 43, }, _labels=utils._get_fr_labels(), _sample_rate=16000, @@ -1173,7 +1175,7 @@ def _get_state_dict(self, dl_kwargs): VOXPOPULI_ASR_BASE_10K_IT = Wav2Vec2ASRBundle( - 'wav2vec2_voxpopuli_base_10k_asr_it.pt', + "wav2vec2_voxpopuli_base_10k_asr_it.pt", { "extractor_mode": "group_norm", "extractor_conv_layer_config": [ diff --git a/torchaudio/pipelines/_wav2vec2/utils.py b/torchaudio/pipelines/_wav2vec2/utils.py index ca48f798a2..60f76b8007 100644 --- a/torchaudio/pipelines/_wav2vec2/utils.py +++ b/torchaudio/pipelines/_wav2vec2/utils.py @@ -1,33 +1,33 @@ def _get_en_labels(): return ( - '|', - 'E', - 'T', - 'A', - 'O', - 'N', - 'I', - 'H', - 'S', - 'R', - 'D', - 'L', - 'U', - 'M', - 'W', - 'C', - 'F', - 'G', - 'Y', - 'P', - 'B', - 'V', - 'K', + "|", + "E", + "T", + "A", + "O", + "N", + "I", + "H", + "S", + "R", + "D", + "L", + "U", + "M", + "W", + "C", + "F", + "G", + "Y", + "P", + "B", + "V", + "K", "'", - 'X', - 'J', - 'Q', - 'Z', + "X", + "J", + "Q", + "Z", ) diff --git a/torchaudio/prototype/conformer.py b/torchaudio/prototype/conformer.py index f02e16ce31..5efb665ee2 100644 --- a/torchaudio/prototype/conformer.py +++ b/torchaudio/prototype/conformer.py @@ -1,7 +1,8 @@ import math -import torch from typing import List, Optional, Tuple +import torch + __all__ = ["Conformer"] @@ -12,9 +13,9 @@ def _lengths_to_padding_mask(lengths: torch.Tensor) -> torch.Tensor: batch_size = lengths.shape[0] max_length = int(torch.max(lengths).item()) - padding_mask = torch.arange( - max_length, device=lengths.device, dtype=lengths.dtype - ).expand(batch_size, max_length) >= lengths.unsqueeze(1) + padding_mask = torch.arange(max_length, device=lengths.device, dtype=lengths.dtype).expand( + batch_size, max_length + ) >= lengths.unsqueeze(1) return padding_mask @@ -31,12 +32,8 @@ def _get_sinusoidal_embeddings( from the description in Section 3.5 of "Attention Is All You Need". """ half_dim = embedding_dim // 2 - t = ( - torch.arange(half_dim, dtype=torch.float) * -math.log(10000) / (half_dim - 1) - ).exp() - embedding_t = torch.arange(num_embeddings, dtype=torch.float).unsqueeze( - 1 - ) * t.unsqueeze(0) + t = (torch.arange(half_dim, dtype=torch.float) * -math.log(10000) / (half_dim - 1)).exp() + embedding_t = torch.arange(num_embeddings, dtype=torch.float).unsqueeze(1) * t.unsqueeze(0) embeddings = torch.cat([embedding_t.sin(), embedding_t.cos()], dim=1) if embedding_dim % 2 == 1: embeddings = torch.cat([embeddings, torch.zeros(num_embeddings, 1)], dim=1) @@ -64,13 +61,16 @@ def __init__( dropout: float = 0.0, ) -> None: super().__init__() - assert ( - depthwise_kernel_size - 1 - ) % 2 == 0, "depthwise_kernel_size must be odd to achieve 'SAME' padding." + assert (depthwise_kernel_size - 1) % 2 == 0, "depthwise_kernel_size must be odd to achieve 'SAME' padding." self.layer_norm = torch.nn.LayerNorm(input_dim) self.sequential = torch.nn.Sequential( torch.nn.Conv1d( - input_dim, 2 * num_channels, 1, stride=1, padding=0, bias=bias, + input_dim, + 2 * num_channels, + 1, + stride=1, + padding=0, + bias=bias, ), torch.nn.GLU(dim=1), torch.nn.Conv1d( @@ -85,7 +85,12 @@ def __init__( torch.nn.BatchNorm1d(num_channels), torch.nn.SiLU(), torch.nn.Conv1d( - num_channels, input_dim, kernel_size=1, stride=1, padding=0, bias=bias, + num_channels, + input_dim, + kernel_size=1, + stride=1, + padding=0, + bias=bias, ), torch.nn.Dropout(dropout), ) @@ -159,9 +164,7 @@ def __init__( self.ffn1 = FeedForwardModule(input_dim, ffn_dim, dropout=dropout) self.self_attn_layer_norm = torch.nn.LayerNorm(input_dim) - self.self_attn = torch.nn.MultiheadAttention( - input_dim, num_attention_heads, dropout=dropout - ) + self.self_attn = torch.nn.MultiheadAttention(input_dim, num_attention_heads, dropout=dropout) self.self_attn_dropout = torch.nn.Dropout(dropout) self.conv_module = ConvolutionModule( @@ -173,9 +176,7 @@ def __init__( self.ffn2 = FeedForwardModule(input_dim, ffn_dim, dropout=dropout) self.final_layer_norm = torch.nn.LayerNorm(input_dim) - def forward( - self, input: torch.Tensor, key_padding_mask: Optional[torch.Tensor] - ) -> torch.Tensor: + def forward(self, input: torch.Tensor, key_padding_mask: Optional[torch.Tensor]) -> torch.Tensor: r""" Args: input (torch.Tensor): input, with shape `(T, B, D)`. @@ -256,9 +257,7 @@ def _get_output_lengths(self, lengths: torch.Tensor) -> torch.Tensor: out = ((out.float() - 1) / 2 + 1).floor().long() return out.to(torch.int32) - def forward( - self, input: torch.Tensor, lengths: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: r""" Args: input (torch.Tensor): input frames, with shape `(B, T_in, in_channels)`. @@ -289,15 +288,11 @@ class SinusoidalPositionalEmbedding(torch.nn.Module): init_size (int, optional): initial embedding count. (Default: 1024) """ - def __init__( - self, embedding_dim: int, padding_idx: int = 0, init_size: int = 1024 - ) -> None: + def __init__(self, embedding_dim: int, padding_idx: int = 0, init_size: int = 1024) -> None: super().__init__() self.embedding_dim = embedding_dim self.padding_idx = padding_idx - self.embeddings = _get_sinusoidal_embeddings( - init_size, embedding_dim, padding_idx - ) + self.embeddings = _get_sinusoidal_embeddings(init_size, embedding_dim, padding_idx) def forward(self, input: torch.Tensor) -> torch.Tensor: r""" @@ -310,14 +305,10 @@ def forward(self, input: torch.Tensor) -> torch.Tensor: B, T = input.shape max_pos = self.padding_idx + 1 + T if max_pos > self.embeddings.size(0): - self.embeddings = _get_sinusoidal_embeddings( - max_pos, self.embedding_dim, self.padding_idx - ) + self.embeddings = _get_sinusoidal_embeddings(max_pos, self.embedding_dim, self.padding_idx) self.embeddings = self.embeddings.to(input) positions = _make_positions(input, self.padding_idx) - return ( - self.embeddings.index_select(0, positions.view(-1)).view(B, T, -1).detach() - ) + return self.embeddings.index_select(0, positions.view(-1)).view(B, T, -1).detach() class Conformer(torch.nn.Module): @@ -370,16 +361,17 @@ def __init__( super().__init__() self.subsample = Conv1dSubsampler( - input_dim, conv_channels, conformer_layer_input_dim, conv_kernel_sizes, + input_dim, + conv_channels, + conformer_layer_input_dim, + conv_kernel_sizes, ) self.position_embedding = SinusoidalPositionalEmbedding( conformer_layer_input_dim, padding_idx=PADDING_IDX, init_size=max_source_positions + PADDING_IDX + 1, ) - self.linear = torch.nn.Linear( - conformer_layer_input_dim, conformer_layer_input_dim - ) + self.linear = torch.nn.Linear(conformer_layer_input_dim, conformer_layer_input_dim) self.dropout = torch.nn.Dropout(dropout) self.conformer_layers = torch.nn.ModuleList( [ @@ -394,9 +386,7 @@ def __init__( ] ) - def forward( - self, input: torch.Tensor, lengths: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: r""" Args: input (torch.Tensor): with shape `(B, T_in, input_dim)`. diff --git a/torchaudio/prototype/emformer.py b/torchaudio/prototype/emformer.py index 2d7c66f98a..2ce2339c42 100644 --- a/torchaudio/prototype/emformer.py +++ b/torchaudio/prototype/emformer.py @@ -10,9 +10,9 @@ def _lengths_to_padding_mask(lengths: torch.Tensor) -> torch.Tensor: batch_size = lengths.shape[0] max_length = int(torch.max(lengths).item()) - padding_mask = torch.arange( - max_length, device=lengths.device, dtype=lengths.dtype - ).expand(batch_size, max_length) >= lengths.unsqueeze(1) + padding_mask = torch.arange(max_length, device=lengths.device, dtype=lengths.dtype).expand( + batch_size, max_length + ) >= lengths.unsqueeze(1) return padding_mask @@ -30,15 +30,8 @@ def _gen_padding_mask( padding_mask = None else: right_context_blocks_length = T - torch.max(lengths).int() - summary.size(0) - left_context_blocks_length = ( - left_context_key.size(0) if left_context_key is not None else 0 - ) - klengths = ( - lengths - + mems.size(0) - + right_context_blocks_length - + left_context_blocks_length - ) + left_context_blocks_length = left_context_key.size(0) if left_context_key is not None else 0 + klengths = lengths + mems.size(0) + right_context_blocks_length + left_context_blocks_length padding_mask = _lengths_to_padding_mask(lengths=klengths) return padding_mask @@ -54,9 +47,7 @@ def _get_activation_module(activation: str) -> torch.nn.Module: raise ValueError(f"Unsupported activation {activation}") -def _get_weight_init_gains( - weight_init_scale_strategy: Optional[str], num_layers: int -) -> List[Optional[float]]: +def _get_weight_init_gains(weight_init_scale_strategy: Optional[str], num_layers: int) -> List[Optional[float]]: if weight_init_scale_strategy is None: return [None for _ in range(num_layers)] elif weight_init_scale_strategy == "depthwise": @@ -64,17 +55,13 @@ def _get_weight_init_gains( elif weight_init_scale_strategy == "constant": return [1.0 / math.sqrt(2) for layer_idx in range(num_layers)] else: - raise ValueError( - f"Unsupported weight_init_scale_strategy value {weight_init_scale_strategy}" - ) + raise ValueError(f"Unsupported weight_init_scale_strategy value {weight_init_scale_strategy}") def _gen_attention_mask_block( col_widths: List[int], col_mask: List[bool], num_rows: int, device: torch.device ) -> torch.Tensor: - assert len(col_widths) == len( - col_mask - ), "Length of col_widths must match that of col_mask" + assert len(col_widths) == len(col_mask), "Length of col_widths must match that of col_mask" mask_block = [ torch.ones(num_rows, col_width, device=device) @@ -110,9 +97,7 @@ def __init__( super().__init__() if input_dim % num_heads != 0: - raise ValueError( - f"input_dim ({input_dim}) is not a multiple of num_heads ({num_heads})." - ) + raise ValueError(f"input_dim ({input_dim}) is not a multiple of num_heads ({num_heads}).") self.input_dim = input_dim self.num_heads = num_heads @@ -127,23 +112,15 @@ def __init__( self.out_proj = torch.nn.Linear(input_dim, input_dim, bias=True) if weight_init_gain: - torch.nn.init.xavier_uniform_( - self.emb_to_key_value.weight, gain=weight_init_gain - ) - torch.nn.init.xavier_uniform_( - self.emb_to_query.weight, gain=weight_init_gain - ) + torch.nn.init.xavier_uniform_(self.emb_to_key_value.weight, gain=weight_init_gain) + torch.nn.init.xavier_uniform_(self.emb_to_query.weight, gain=weight_init_gain) - def _gen_key_value( - self, input: torch.Tensor, mems: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def _gen_key_value(self, input: torch.Tensor, mems: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: T, _, _ = input.shape summary_length = mems.size(0) + 1 right_ctx_utterance_block = input[: T - summary_length] mems_right_ctx_utterance_block = torch.cat([mems, right_ctx_utterance_block]) - key, value = self.emb_to_key_value(mems_right_ctx_utterance_block).chunk( - chunks=2, dim=2 - ) + key, value = self.emb_to_key_value(mems_right_ctx_utterance_block).chunk(chunks=2, dim=2) return key, value def _gen_attention_probs( @@ -153,27 +130,17 @@ def _gen_attention_probs( padding_mask: Optional[torch.Tensor], ) -> torch.Tensor: attention_weights_float = attention_weights.float() - attention_weights_float = attention_weights_float.masked_fill( - attention_mask.unsqueeze(0), self.negative_inf - ) + attention_weights_float = attention_weights_float.masked_fill(attention_mask.unsqueeze(0), self.negative_inf) T = attention_weights.size(1) B = attention_weights.size(0) // self.num_heads if padding_mask is not None: - attention_weights_float = attention_weights_float.view( - B, self.num_heads, T, -1 - ) + attention_weights_float = attention_weights_float.view(B, self.num_heads, T, -1) attention_weights_float = attention_weights_float.masked_fill( padding_mask.unsqueeze(1).unsqueeze(2).to(torch.bool), self.negative_inf ) - attention_weights_float = attention_weights_float.view( - B * self.num_heads, T, -1 - ) - attention_probs = torch.nn.functional.softmax( - attention_weights_float, dim=-1 - ).type_as(attention_weights) - return torch.nn.functional.dropout( - attention_probs, p=float(self.dropout), training=self.training - ) + attention_weights_float = attention_weights_float.view(B * self.num_heads, T, -1) + attention_probs = torch.nn.functional.softmax(attention_weights_float, dim=-1).type_as(attention_weights) + return torch.nn.functional.dropout(attention_probs, p=float(self.dropout), training=self.training) def _forward_impl( self, @@ -193,9 +160,7 @@ def _forward_impl( query = self.emb_to_query(torch.cat([right_context, utterance, summary])) # Compute key and value with [mems, right context, utterance]. - key, value = self.emb_to_key_value( - torch.cat([mems, right_context, utterance]) - ).chunk(chunks=2, dim=2) + key, value = self.emb_to_key_value(torch.cat([mems, right_context, utterance])).chunk(chunks=2, dim=2) if left_context_key is not None and left_context_val is not None: right_context_blocks_length = T - torch.max(lengths).int() - summary.size(0) @@ -203,37 +168,29 @@ def _forward_impl( [ key[: mems.size(0) + right_context_blocks_length], left_context_key, - key[mems.size(0) + right_context_blocks_length:], + key[mems.size(0) + right_context_blocks_length :], ], ) value = torch.cat( [ value[: mems.size(0) + right_context_blocks_length], left_context_val, - value[mems.size(0) + right_context_blocks_length:], + value[mems.size(0) + right_context_blocks_length :], ], ) # Compute attention weights from query, key, and value. reshaped_query, reshaped_key, reshaped_value = [ - tensor.contiguous() - .view(-1, B * self.num_heads, self.input_dim // self.num_heads) - .transpose(0, 1) + tensor.contiguous().view(-1, B * self.num_heads, self.input_dim // self.num_heads).transpose(0, 1) for tensor in [query, key, value] ] - attention_weights = torch.bmm( - reshaped_query * self.scaling, reshaped_key.transpose(1, 2) - ) + attention_weights = torch.bmm(reshaped_query * self.scaling, reshaped_key.transpose(1, 2)) # Compute padding mask. - padding_mask = _gen_padding_mask( - utterance, right_context, summary, lengths, mems, left_context_key - ) + padding_mask = _gen_padding_mask(utterance, right_context, summary, lengths, mems, left_context_key) # Compute attention probabilities. - attention_probs = self._gen_attention_probs( - attention_weights, attention_mask, padding_mask - ) + attention_probs = self._gen_attention_probs(attention_weights, attention_mask, padding_mask) # Compute attention. attention = torch.bmm(attention_probs, reshaped_value) @@ -249,7 +206,7 @@ def _forward_impl( summary_length = summary.size(0) output_right_context = output_right_context_mems[: T - summary_length] - output_mems = output_right_context_mems[T - summary_length:] + output_mems = output_right_context_mems[T - summary_length :] if self.tanh_on_mem: output_mems = torch.tanh(output_mems) else: @@ -291,9 +248,7 @@ def forward( Tensor updated memory elements, with shape `(M, B, D)`. """ - output, output_mems, _, _ = self._forward_impl( - utterance, lengths, right_context, summary, mems, attention_mask - ) + output, output_mems, _, _ = self._forward_impl(utterance, lengths, right_context, summary, mems, attention_mask) return output, output_mems[:-1] @torch.jit.export @@ -338,15 +293,8 @@ def infer( attention value computed for left context and utterance. """ query_dim = right_context.size(0) + utterance.size(0) + summary.size(0) - key_dim = ( - right_context.size(0) - + utterance.size(0) - + mems.size(0) - + left_context_key.size(0) - ) - attention_mask = torch.zeros(query_dim, key_dim).to( - dtype=torch.bool, device=utterance.device - ) + key_dim = right_context.size(0) + utterance.size(0) + mems.size(0) + left_context_key.size(0) + attention_mask = torch.zeros(query_dim, key_dim).to(dtype=torch.bool, device=utterance.device) attention_mask[-1, : mems.size(0)] = True output, output_mems, key, value = self._forward_impl( utterance, @@ -361,8 +309,8 @@ def infer( return ( output, output_mems, - key[mems.size(0) + right_context.size(0):], - value[mems.size(0) + right_context.size(0):], + key[mems.size(0) + right_context.size(0) :], + value[mems.size(0) + right_context.size(0) :], ) @@ -410,9 +358,7 @@ def __init__( negative_inf=negative_inf, ) self.dropout = torch.nn.Dropout(dropout) - self.memory_op = torch.nn.AvgPool1d( - kernel_size=segment_length, stride=segment_length, ceil_mode=True - ) + self.memory_op = torch.nn.AvgPool1d(kernel_size=segment_length, stride=segment_length, ceil_mode=True) activation_module = _get_activation_module(activation) self.pos_ff = torch.nn.Sequential( @@ -433,18 +379,10 @@ def __init__( self.use_mem = max_memory_size > 0 - def _init_state( - self, batch_size: int, device: Optional[torch.device] - ) -> List[torch.Tensor]: - empty_memory = torch.zeros( - self.max_memory_size, batch_size, self.input_dim, device=device - ) - left_context_key = torch.zeros( - self.left_context_length, batch_size, self.input_dim, device=device - ) - left_context_val = torch.zeros( - self.left_context_length, batch_size, self.input_dim, device=device - ) + def _init_state(self, batch_size: int, device: Optional[torch.device]) -> List[torch.Tensor]: + empty_memory = torch.zeros(self.max_memory_size, batch_size, self.input_dim, device=device) + left_context_key = torch.zeros(self.left_context_length, batch_size, self.input_dim, device=device) + left_context_val = torch.zeros(self.left_context_length, batch_size, self.input_dim, device=device) past_length = torch.zeros(1, batch_size, dtype=torch.int32, device=device) return [empty_memory, left_context_key, left_context_val, past_length] @@ -453,12 +391,10 @@ def _unpack_state( ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: past_length = state[3][0][0].item() past_left_context_length = min(self.left_context_length, past_length) - past_mem_length = min( - self.max_memory_size, math.ceil(past_length / self.segment_length) - ) - pre_mems = state[0][self.max_memory_size - past_mem_length:] - lc_key = state[1][self.left_context_length - past_left_context_length:] - lc_val = state[2][self.left_context_length - past_left_context_length:] + past_mem_length = min(self.max_memory_size, math.ceil(past_length / self.segment_length)) + pre_mems = state[0][self.max_memory_size - past_mem_length :] + lc_key = state[1][self.left_context_length - past_left_context_length :] + lc_val = state[2][self.left_context_length - past_left_context_length :] return pre_mems, lc_key, lc_val def _pack_state( @@ -471,9 +407,9 @@ def _pack_state( ) -> List[torch.Tensor]: new_k = torch.cat([state[1], next_k]) new_v = torch.cat([state[2], next_v]) - state[0] = torch.cat([state[0], mems])[-self.max_memory_size:] - state[1] = new_k[new_k.shape[0] - self.left_context_length:] - state[2] = new_v[new_v.shape[0] - self.left_context_length:] + state[0] = torch.cat([state[0], mems])[-self.max_memory_size :] + state[1] = new_k[new_k.shape[0] - self.left_context_length :] + state[2] = new_v[new_v.shape[0] - self.left_context_length :] state[3] = state[3] + update_length return state @@ -493,7 +429,7 @@ def _apply_pre_attention_layer_norm( ) -> Tuple[torch.Tensor, torch.Tensor]: layer_norm_input = self.layer_norm_input(torch.cat([right_context, utterance])) return ( - layer_norm_input[right_context.size(0):], + layer_norm_input[right_context.size(0) :], layer_norm_input[: right_context.size(0)], ) @@ -501,7 +437,7 @@ def _apply_post_attention_ffn( self, rc_output: torch.Tensor, utterance: torch.Tensor, right_context: torch.Tensor ) -> Tuple[torch.Tensor, torch.Tensor]: rc_output = self._process_attention_output(rc_output, utterance, right_context) - return rc_output[right_context.size(0):], rc_output[: right_context.size(0)] + return rc_output[right_context.size(0) :], rc_output[: right_context.size(0)] def _apply_attention_forward( self, @@ -512,9 +448,7 @@ def _apply_attention_forward( attention_mask: Optional[torch.Tensor], ) -> Tuple[torch.Tensor, torch.Tensor]: if attention_mask is None: - raise ValueError( - "attention_mask must be not None when for_inference is False" - ) + raise ValueError("attention_mask must be not None when for_inference is False") if self.use_mem: summary = self.memory_op(utterance.permute(1, 2, 0)).permute(2, 0, 1) @@ -602,9 +536,7 @@ def forward( mems, attention_mask, ) - output_utterance, output_right_context = self._apply_post_attention_ffn( - rc_output, utterance, right_context - ) + output_utterance, output_right_context = self._apply_post_attention_ffn(rc_output, utterance, right_context) return output_utterance, output_right_context, output_mems @torch.jit.export @@ -652,9 +584,7 @@ def infer( rc_output, output_mems, output_state = self._apply_attention_infer( layer_norm_utterance, lengths, layer_norm_right_context, mems, state ) - output_utterance, output_right_context = self._apply_post_attention_ffn( - rc_output, utterance, right_context - ) + output_utterance, output_right_context = self._apply_post_attention_ffn(rc_output, utterance, right_context) return output_utterance, output_right_context, output_state, output_mems @@ -708,12 +638,12 @@ def __init__( self.use_mem = max_memory_size > 0 self.memory_op = torch.nn.AvgPool1d( - kernel_size=segment_length, stride=segment_length, ceil_mode=True, + kernel_size=segment_length, + stride=segment_length, + ceil_mode=True, ) - weight_init_gains = _get_weight_init_gains( - weight_init_scale_strategy, num_layers - ) + weight_init_gains = _get_weight_init_gains(weight_init_scale_strategy, num_layers) self.emformer_layers = torch.nn.ModuleList( [ _EmformerLayer( @@ -747,12 +677,10 @@ def _gen_right_context(self, input: torch.Tensor) -> torch.Tensor: start = (seg_idx + 1) * self.segment_length end = start + self.right_context_length right_context_blocks.append(input[start:end]) - right_context_blocks.append(input[T - self.right_context_length:]) + right_context_blocks.append(input[T - self.right_context_length :]) return torch.cat(right_context_blocks) - def _gen_attention_mask_col_widths( - self, seg_idx: int, utterance_length: int - ) -> List[int]: + def _gen_attention_mask_col_widths(self, seg_idx: int, utterance_length: int) -> List[int]: num_segs = math.ceil(utterance_length / self.segment_length) rc = self.right_context_length lc = self.left_context_length @@ -830,19 +758,13 @@ def _gen_attention_mask(self, input: torch.Tensor) -> torch.Tensor: query_mask.append(query_mask_block) if s_cols_mask is not None: - summary_mask_block = _gen_attention_mask_block( - col_widths, s_cols_mask, 1, input.device - ) + summary_mask_block = _gen_attention_mask_block(col_widths, s_cols_mask, 1, input.device) summary_mask.append(summary_mask_block) - attention_mask = ( - 1 - torch.cat([torch.cat(mask) for mask in masks_to_concat]) - ).to(torch.bool) + attention_mask = (1 - torch.cat([torch.cat(mask) for mask in masks_to_concat])).to(torch.bool) return attention_mask - def forward( - self, input: torch.Tensor, lengths: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: r"""Forward pass for training. B: batch size; @@ -874,9 +796,7 @@ def forward( ) output = utterance for layer in self.emformer_layers: - output, right_context, mems = layer( - output, lengths, right_context, mems, attention_mask - ) + output, right_context, mems = layer(output, lengths, right_context, mems, attention_mask) return output.permute(1, 0, 2), lengths @torch.jit.export diff --git a/torchaudio/prototype/rnnt.py b/torchaudio/prototype/rnnt.py index 9bcf88f6aa..71e516c7dc 100644 --- a/torchaudio/prototype/rnnt.py +++ b/torchaudio/prototype/rnnt.py @@ -15,13 +15,12 @@ class _TimeReduction(torch.nn.Module): Args: stride (int): number of frames to merge for each output frame. """ + def __init__(self, stride: int) -> None: super().__init__() self.stride = stride - def forward( - self, input: torch.Tensor, lengths: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: r"""Forward pass. B: batch size; @@ -64,6 +63,7 @@ class _CustomLSTM(torch.nn.Module): layer_norm_epsilon (float, optional): value of epsilon to use in layer normalization layers (Default: 1e-5) """ + def __init__( self, input_dim: int, @@ -179,7 +179,9 @@ def __init__( ) -> None: super().__init__() self.input_linear = torch.nn.Linear( - input_dim, time_reduction_input_dim, bias=False, + input_dim, + time_reduction_input_dim, + bias=False, ) self.time_reduction = _TimeReduction(time_reduction_stride) transformer_input_dim = time_reduction_input_dim * time_reduction_stride @@ -200,9 +202,7 @@ def __init__( self.output_linear = torch.nn.Linear(transformer_input_dim, output_dim) self.layer_norm = torch.nn.LayerNorm(output_dim) - def forward( - self, input: torch.Tensor, lengths: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: r"""Forward pass for training. B: batch size; @@ -225,12 +225,8 @@ def forward( number of valid elements for i-th batch element in output frame sequences. """ input_linear_out = self.input_linear(input) - time_reduction_out, time_reduction_lengths = self.time_reduction( - input_linear_out, lengths - ) - transformer_out, transformer_lengths = self.transformer( - time_reduction_out, time_reduction_lengths - ) + time_reduction_out, time_reduction_lengths = self.time_reduction(input_linear_out, lengths) + transformer_out, transformer_lengths = self.transformer(time_reduction_out, time_reduction_lengths) output_linear_out = self.output_linear(transformer_out) layer_norm_out = self.layer_norm(output_linear_out) return layer_norm_out, transformer_lengths @@ -271,9 +267,7 @@ def infer( of ``infer``. """ input_linear_out = self.input_linear(input) - time_reduction_out, time_reduction_lengths = self.time_reduction( - input_linear_out, lengths - ) + time_reduction_out, time_reduction_lengths = self.time_reduction(input_linear_out, lengths) ( transformer_out, transformer_lengths, @@ -299,6 +293,7 @@ class _Predictor(torch.nn.Module): lstm_dropout (float, optional): LSTM dropout probability. (Default: 0.0) """ + def __init__( self, num_symbols: int, @@ -368,9 +363,7 @@ def forward( lstm_out = input_layer_norm_out state_out: List[List[torch.Tensor]] = [] for layer_idx, lstm in enumerate(self.lstm_layers): - lstm_out, lstm_state_out = lstm( - lstm_out, None if state is None else state[layer_idx] - ) + lstm_out, lstm_state_out = lstm(lstm_out, None if state is None else state[layer_idx]) lstm_out = self.dropout(lstm_out) state_out.append(lstm_state_out) @@ -426,10 +419,7 @@ def forward( output target lengths, with shape `(B,)` and i-th element representing number of valid elements along dim 2 for i-th batch element in joint network output. """ - joint_encodings = ( - source_encodings.unsqueeze(2).contiguous() - + target_encodings.unsqueeze(1).contiguous() - ) + joint_encodings = source_encodings.unsqueeze(2).contiguous() + target_encodings.unsqueeze(1).contiguous() relu_out = self.relu(joint_encodings) output = self.linear(relu_out) return output, source_lengths, target_lengths @@ -447,9 +437,7 @@ class RNNT(torch.nn.Module): joiner (torch.nn.Module): joint network. """ - def __init__( - self, transcriber: _Transcriber, predictor: _Predictor, joiner: _Joiner - ) -> None: + def __init__(self, transcriber: _Transcriber, predictor: _Predictor, joiner: _Joiner) -> None: super().__init__() self.transcriber = transcriber self.predictor = predictor @@ -500,10 +488,13 @@ def forward( of ``forward``. """ source_encodings, source_lengths = self.transcriber( - input=sources, lengths=source_lengths, + input=sources, + lengths=source_lengths, ) target_encodings, target_lengths, predictor_state = self.predictor( - input=targets, lengths=target_lengths, state=predictor_state, + input=targets, + lengths=target_lengths, + state=predictor_state, ) output, source_lengths, target_lengths = self.joiner( source_encodings=source_encodings, @@ -558,7 +549,9 @@ def transcribe_streaming( @torch.jit.export def transcribe( - self, sources: torch.Tensor, source_lengths: torch.Tensor, + self, + sources: torch.Tensor, + source_lengths: torch.Tensor, ) -> Tuple[torch.Tensor, torch.Tensor]: r"""Applies transcription network to sources in non-streaming mode. diff --git a/torchaudio/prototype/rnnt_decoder.py b/torchaudio/prototype/rnnt_decoder.py index 378dd60b01..8d006ee515 100644 --- a/torchaudio/prototype/rnnt_decoder.py +++ b/torchaudio/prototype/rnnt_decoder.py @@ -35,21 +35,14 @@ def _batch_state(hypos: List[Hypothesis]) -> List[List[torch.Tensor]]: for i in range(len(hypos[0].state)): batched_state_components: List[torch.Tensor] = [] for j in range(len(hypos[0].state[i])): - batched_state_components.append( - torch.cat([hypo.state[i][j] for hypo in hypos]) - ) + batched_state_components.append(torch.cat([hypo.state[i][j] for hypo in hypos])) states.append(batched_state_components) return states -def _slice_state( - states: List[List[torch.Tensor]], idx: int, device: torch.device -) -> List[List[torch.Tensor]]: +def _slice_state(states: List[List[torch.Tensor]], idx: int, device: torch.device) -> List[List[torch.Tensor]]: idx_tensor = torch.tensor([idx], device=device) - return [ - [state.index_select(0, idx_tensor) for state in state_tuple] - for state_tuple in states - ] + return [[state.index_select(0, idx_tensor) for state in state_tuple] for state_tuple in states] def _default_hypo_sort_key(hypo: Hypothesis) -> float: @@ -57,18 +50,14 @@ def _default_hypo_sort_key(hypo: Hypothesis) -> float: def _compute_updated_scores( - hypos: List[Hypothesis], next_token_probs: torch.Tensor, beam_width: int, + hypos: List[Hypothesis], + next_token_probs: torch.Tensor, + beam_width: int, ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: hypo_scores = torch.tensor([h.score for h in hypos]).unsqueeze(1) - nonblank_scores = ( - hypo_scores + next_token_probs[:, :-1] - ) # [beam_width, num_tokens - 1] - nonblank_nbest_scores, nonblank_nbest_idx = nonblank_scores.reshape(-1).topk( - beam_width - ) - nonblank_nbest_hypo_idx = nonblank_nbest_idx.div( - nonblank_scores.shape[1], rounding_mode="trunc" - ) + nonblank_scores = hypo_scores + next_token_probs[:, :-1] # [beam_width, num_tokens - 1] + nonblank_nbest_scores, nonblank_nbest_idx = nonblank_scores.reshape(-1).topk(beam_width) + nonblank_nbest_hypo_idx = nonblank_nbest_idx.div(nonblank_scores.shape[1], rounding_mode="trunc") nonblank_nbest_token = nonblank_nbest_idx % nonblank_scores.shape[1] return nonblank_nbest_scores, nonblank_nbest_hypo_idx, nonblank_nbest_token @@ -114,9 +103,7 @@ def __init__( self.step_max_tokens = step_max_tokens - def _init_b_hypos( - self, hypo: Optional[Hypothesis], device: torch.device - ) -> List[Hypothesis]: + def _init_b_hypos(self, hypo: Optional[Hypothesis], device: torch.device) -> List[Hypothesis]: if hypo is not None: token = hypo.tokens[-1] state = hypo.state @@ -125,9 +112,7 @@ def _init_b_hypos( state = None one_tensor = torch.tensor([1], device=device) - pred_out, _, pred_state = self.model.predict( - torch.tensor([[token]], device=device), one_tensor, state - ) + pred_out, _, pred_state = self.model.predict(torch.tensor([[token]], device=device), one_tensor, state) init_hypo = Hypothesis( tokens=[token], predictor_out=pred_out[0].detach(), @@ -150,9 +135,7 @@ def _gen_next_token_probs( predictor_out, torch.tensor([1] * len(hypos), device=device), ) # [beam_width, 1, 1, num_tokens] - joined_out = torch.nn.functional.log_softmax( - joined_out / self.temperature, dim=3 - ) + joined_out = torch.nn.functional.log_softmax(joined_out / self.temperature, dim=3) joined_out[:, :, :, :4].add_(-99999) # blank out invalid tokens return joined_out[:, 0, 0] @@ -220,9 +203,7 @@ def _gen_a_hypos( new_scores.append(score) if base_hypos: - new_hypos = self._gen_new_hypos( - base_hypos, new_tokens, new_scores, t, device - ) + new_hypos = self._gen_new_hypos(base_hypos, new_tokens, new_scores, t, device) else: new_hypos: List[Hypothesis] = [] @@ -239,7 +220,9 @@ def _gen_new_hypos( tgt_tokens = torch.tensor([[token] for token in tokens], device=device) states = _batch_state(base_hypos) pred_out, _, pred_states = self.model.predict( - tgt_tokens, torch.tensor([1] * len(base_hypos), device=device), states, + tgt_tokens, + torch.tensor([1] * len(base_hypos), device=device), + states, ) new_hypos: List[Hypothesis] = [] for i, h_a in enumerate(base_hypos): @@ -258,7 +241,10 @@ def _gen_new_hypos( return new_hypos def _search( - self, enc_out: torch.Tensor, hypo: Optional[Hypothesis], beam_width: int, + self, + enc_out: torch.Tensor, + hypo: Optional[Hypothesis], + beam_width: int, ) -> List[Hypothesis]: n_time_steps = enc_out.shape[1] device = enc_out.device @@ -272,33 +258,35 @@ def _search( symbols_current_t = 0 while a_hypos: - next_token_probs = self._gen_next_token_probs( - enc_out[:, t: t + 1], a_hypos, device - ) + next_token_probs = self._gen_next_token_probs(enc_out[:, t : t + 1], a_hypos, device) next_token_probs = next_token_probs.cpu() b_hypos = self._gen_b_hypos( - b_hypos, a_hypos, next_token_probs, key_to_b_hypo, + b_hypos, + a_hypos, + next_token_probs, + key_to_b_hypo, ) if symbols_current_t == self.step_max_tokens: break a_hypos = self._gen_a_hypos( - a_hypos, b_hypos, next_token_probs, t, beam_width, device, + a_hypos, + b_hypos, + next_token_probs, + t, + beam_width, + device, ) if a_hypos: symbols_current_t += 1 - _, sorted_idx = torch.tensor( - [self.hypo_sort_key(hypo) for hypo in b_hypos] - ).topk(beam_width) + _, sorted_idx = torch.tensor([self.hypo_sort_key(hypo) for hypo in b_hypos]).topk(beam_width) b_hypos = [b_hypos[idx] for idx in sorted_idx] return b_hypos - def forward( - self, input: torch.Tensor, length: torch.Tensor, beam_width: int - ) -> List[Hypothesis]: + def forward(self, input: torch.Tensor, length: torch.Tensor, beam_width: int) -> List[Hypothesis]: r"""Performs beam search for the given input sequence. T: number of frames; @@ -319,9 +307,7 @@ def forward( if input.dim() == 2: input = input.unsqueeze(0) - assert length.shape == () or length.shape == ( - 1, - ), "length must be of shape () or (1,)" + assert length.shape == () or length.shape == (1,), "length must be of shape () or (1,)" if input.dim() == 0: input = input.unsqueeze(0) @@ -367,9 +353,7 @@ def infer( if input.dim() == 2: input = input.unsqueeze(0) - assert length.shape == () or length.shape == ( - 1, - ), "length must be of shape () or (1,)" + assert length.shape == () or length.shape == (1,), "length must be of shape () or (1,)" if input.dim() == 0: input = input.unsqueeze(0) diff --git a/torchaudio/sox_effects/__init__.py b/torchaudio/sox_effects/__init__.py index 3c46cebdd2..ff0ede5d78 100644 --- a/torchaudio/sox_effects/__init__.py +++ b/torchaudio/sox_effects/__init__.py @@ -1,4 +1,5 @@ from torchaudio._internal import module_utils as _mod_utils + from .sox_effects import ( init_sox_effects, shutdown_sox_effects, @@ -10,13 +11,14 @@ if _mod_utils.is_sox_available(): import atexit + init_sox_effects() atexit.register(shutdown_sox_effects) __all__ = [ - 'init_sox_effects', - 'shutdown_sox_effects', - 'effect_names', - 'apply_effects_tensor', - 'apply_effects_file', + "init_sox_effects", + "shutdown_sox_effects", + "effect_names", + "apply_effects_tensor", + "apply_effects_file", ] diff --git a/torchaudio/sox_effects/sox_effects.py b/torchaudio/sox_effects/sox_effects.py index c17c356859..0d94e50621 100644 --- a/torchaudio/sox_effects/sox_effects.py +++ b/torchaudio/sox_effects/sox_effects.py @@ -2,7 +2,6 @@ from typing import List, Tuple, Optional import torch - import torchaudio from torchaudio._internal import module_utils as _mod_utils from torchaudio.utils.sox_utils import list_effects @@ -53,10 +52,10 @@ def effect_names() -> List[str]: @_mod_utils.requires_sox() def apply_effects_tensor( - tensor: torch.Tensor, - sample_rate: int, - effects: List[List[str]], - channels_first: bool = True, + tensor: torch.Tensor, + sample_rate: int, + effects: List[List[str]], + channels_first: bool = True, ) -> Tuple[torch.Tensor, int]: """Apply sox effects to given Tensor @@ -149,17 +148,16 @@ def apply_effects_tensor( >>> waveform, sample_rate = transform(waveform, input_sample_rate) >>> assert sample_rate == 8000 """ - return torch.ops.torchaudio.sox_effects_apply_effects_tensor( - tensor, sample_rate, effects, channels_first) + return torch.ops.torchaudio.sox_effects_apply_effects_tensor(tensor, sample_rate, effects, channels_first) @_mod_utils.requires_sox() def apply_effects_file( - path: str, - effects: List[List[str]], - normalize: bool = True, - channels_first: bool = True, - format: Optional[str] = None, + path: str, + effects: List[List[str]], + normalize: bool = True, + channels_first: bool = True, + format: Optional[str] = None, ) -> Tuple[torch.Tensor, int]: """Apply sox effects to the audio file and load the resulting data as Tensor @@ -265,9 +263,7 @@ def apply_effects_file( >>> pass """ if not torch.jit.is_scripting(): - if hasattr(path, 'read'): - return torchaudio._torchaudio.apply_effects_fileobj( - path, effects, normalize, channels_first, format) + if hasattr(path, "read"): + return torchaudio._torchaudio.apply_effects_fileobj(path, effects, normalize, channels_first, format) path = os.fspath(path) - return torch.ops.torchaudio.sox_effects_apply_effects_file( - path, effects, normalize, channels_first, format) + return torch.ops.torchaudio.sox_effects_apply_effects_file(path, effects, normalize, channels_first, format) diff --git a/torchaudio/transforms.py b/torchaudio/transforms.py index bc2065962f..5c8ae79505 100644 --- a/torchaudio/transforms.py +++ b/torchaudio/transforms.py @@ -14,31 +14,31 @@ ) __all__ = [ - 'Spectrogram', - 'InverseSpectrogram', - 'GriffinLim', - 'AmplitudeToDB', - 'MelScale', - 'InverseMelScale', - 'MelSpectrogram', - 'MFCC', - 'LFCC', - 'MuLawEncoding', - 'MuLawDecoding', - 'Resample', - 'TimeStretch', - 'Fade', - 'FrequencyMasking', - 'TimeMasking', - 'SlidingWindowCmn', - 'Vad', - 'SpectralCentroid', - 'Vol', - 'ComputeDeltas', - 'PitchShift', - 'RNNTLoss', - 'PSD', - 'MVDR', + "Spectrogram", + "InverseSpectrogram", + "GriffinLim", + "AmplitudeToDB", + "MelScale", + "InverseMelScale", + "MelSpectrogram", + "MFCC", + "LFCC", + "MuLawEncoding", + "MuLawDecoding", + "Resample", + "TimeStretch", + "Fade", + "FrequencyMasking", + "TimeMasking", + "SlidingWindowCmn", + "Vad", + "SpectralCentroid", + "Vol", + "ComputeDeltas", + "PitchShift", + "RNNTLoss", + "PSD", + "MVDR", ] @@ -73,21 +73,23 @@ class Spectrogram(torch.nn.Module): >>> spectrogram = transform(waveform) """ - __constants__ = ['n_fft', 'win_length', 'hop_length', 'pad', 'power', 'normalized'] - - def __init__(self, - n_fft: int = 400, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - pad: int = 0, - window_fn: Callable[..., Tensor] = torch.hann_window, - power: Optional[float] = 2., - normalized: bool = False, - wkwargs: Optional[dict] = None, - center: bool = True, - pad_mode: str = "reflect", - onesided: bool = True, - return_complex: Optional[bool] = None) -> None: + __constants__ = ["n_fft", "win_length", "hop_length", "pad", "power", "normalized"] + + def __init__( + self, + n_fft: int = 400, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + pad: int = 0, + window_fn: Callable[..., Tensor] = torch.hann_window, + power: Optional[float] = 2.0, + normalized: bool = False, + wkwargs: Optional[dict] = None, + center: bool = True, + pad_mode: str = "reflect", + onesided: bool = True, + return_complex: Optional[bool] = None, + ) -> None: super(Spectrogram, self).__init__() self.n_fft = n_fft # number of FFT bins. the returned STFT result will have n_fft // 2 + 1 @@ -95,7 +97,7 @@ def __init__(self, self.win_length = win_length if win_length is not None else n_fft self.hop_length = hop_length if hop_length is not None else self.win_length // 2 window = window_fn(self.win_length) if wkwargs is None else window_fn(self.win_length, **wkwargs) - self.register_buffer('window', window) + self.register_buffer("window", window) self.pad = pad self.power = power self.normalized = normalized @@ -162,19 +164,21 @@ class InverseSpectrogram(torch.nn.Module): >>> transform = transforms.InverseSpectrogram(n_fft=512) >>> waveform = transform(spectrogram, length) """ - __constants__ = ['n_fft', 'win_length', 'hop_length', 'pad', 'power', 'normalized'] - - def __init__(self, - n_fft: int = 400, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - pad: int = 0, - window_fn: Callable[..., Tensor] = torch.hann_window, - normalized: bool = False, - wkwargs: Optional[dict] = None, - center: bool = True, - pad_mode: str = "reflect", - onesided: bool = True) -> None: + __constants__ = ["n_fft", "win_length", "hop_length", "pad", "power", "normalized"] + + def __init__( + self, + n_fft: int = 400, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + pad: int = 0, + window_fn: Callable[..., Tensor] = torch.hann_window, + normalized: bool = False, + wkwargs: Optional[dict] = None, + center: bool = True, + pad_mode: str = "reflect", + onesided: bool = True, + ) -> None: super(InverseSpectrogram, self).__init__() self.n_fft = n_fft # number of FFT bins. the returned STFT result will have n_fft // 2 + 1 @@ -182,7 +186,7 @@ def __init__(self, self.win_length = win_length if win_length is not None else n_fft self.hop_length = hop_length if hop_length is not None else self.win_length // 2 window = window_fn(self.win_length) if wkwargs is None else window_fn(self.win_length, **wkwargs) - self.register_buffer('window', window) + self.register_buffer("window", window) self.pad = pad self.normalized = normalized self.center = center @@ -242,31 +246,32 @@ class GriffinLim(torch.nn.Module): >>> transform = transforms.GriffinLim(n_fft=512) >>> waveform = transform(spectrogram) """ - __constants__ = ['n_fft', 'n_iter', 'win_length', 'hop_length', 'power', - 'length', 'momentum', 'rand_init'] - - def __init__(self, - n_fft: int = 400, - n_iter: int = 32, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - window_fn: Callable[..., Tensor] = torch.hann_window, - power: float = 2., - wkwargs: Optional[dict] = None, - momentum: float = 0.99, - length: Optional[int] = None, - rand_init: bool = True) -> None: + __constants__ = ["n_fft", "n_iter", "win_length", "hop_length", "power", "length", "momentum", "rand_init"] + + def __init__( + self, + n_fft: int = 400, + n_iter: int = 32, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + window_fn: Callable[..., Tensor] = torch.hann_window, + power: float = 2.0, + wkwargs: Optional[dict] = None, + momentum: float = 0.99, + length: Optional[int] = None, + rand_init: bool = True, + ) -> None: super(GriffinLim, self).__init__() - assert momentum < 1, 'momentum={} > 1 can be unstable'.format(momentum) - assert momentum >= 0, 'momentum={} < 0'.format(momentum) + assert momentum < 1, "momentum={} > 1 can be unstable".format(momentum) + assert momentum >= 0, "momentum={} < 0".format(momentum) self.n_fft = n_fft self.n_iter = n_iter self.win_length = win_length if win_length is not None else n_fft self.hop_length = hop_length if hop_length is not None else self.win_length // 2 window = window_fn(self.win_length) if wkwargs is None else window_fn(self.win_length, **wkwargs) - self.register_buffer('window', window) + self.register_buffer("window", window) self.length = length self.power = power self.momentum = momentum / (1 + momentum) @@ -282,8 +287,18 @@ def forward(self, specgram: Tensor) -> Tensor: Returns: Tensor: waveform of (..., time), where time equals the ``length`` parameter if given. """ - return F.griffinlim(specgram, self.window, self.n_fft, self.hop_length, self.win_length, self.power, - self.n_iter, self.momentum, self.length, self.rand_init) + return F.griffinlim( + specgram, + self.window, + self.n_fft, + self.hop_length, + self.win_length, + self.power, + self.n_iter, + self.momentum, + self.length, + self.rand_init, + ) class AmplitudeToDB(torch.nn.Module): @@ -299,15 +314,15 @@ class AmplitudeToDB(torch.nn.Module): top_db (float or None, optional): minimum negative cut-off in decibels. A reasonable number is 80. (Default: ``None``) """ - __constants__ = ['multiplier', 'amin', 'ref_value', 'db_multiplier'] + __constants__ = ["multiplier", "amin", "ref_value", "db_multiplier"] - def __init__(self, stype: str = 'power', top_db: Optional[float] = None) -> None: + def __init__(self, stype: str = "power", top_db: Optional[float] = None) -> None: super(AmplitudeToDB, self).__init__() self.stype = stype if top_db is not None and top_db < 0: - raise ValueError('top_db must be positive value') + raise ValueError("top_db must be positive value") self.top_db = top_db - self.multiplier = 10.0 if stype == 'power' else 20.0 + self.multiplier = 10.0 if stype == "power" else 20.0 self.amin = 1e-10 self.ref_value = 1.0 self.db_multiplier = math.log10(max(self.amin, self.ref_value)) @@ -344,16 +359,18 @@ class MelScale(torch.nn.Module): :py:func:`torchaudio.functional.melscale_fbanks` - The function used to generate the filter banks. """ - __constants__ = ['n_mels', 'sample_rate', 'f_min', 'f_max'] - - def __init__(self, - n_mels: int = 128, - sample_rate: int = 16000, - f_min: float = 0., - f_max: Optional[float] = None, - n_stft: int = 201, - norm: Optional[str] = None, - mel_scale: str = "htk") -> None: + __constants__ = ["n_mels", "sample_rate", "f_min", "f_max"] + + def __init__( + self, + n_mels: int = 128, + sample_rate: int = 16000, + f_min: float = 0.0, + f_max: Optional[float] = None, + n_stft: int = 201, + norm: Optional[str] = None, + mel_scale: str = "htk", + ) -> None: super(MelScale, self).__init__() self.n_mels = n_mels self.sample_rate = sample_rate @@ -362,11 +379,9 @@ def __init__(self, self.norm = norm self.mel_scale = mel_scale - assert f_min <= self.f_max, 'Require f_min: {} < f_max: {}'.format(f_min, self.f_max) - fb = F.melscale_fbanks( - n_stft, self.f_min, self.f_max, self.n_mels, self.sample_rate, self.norm, - self.mel_scale) - self.register_buffer('fb', fb) + assert f_min <= self.f_max, "Require f_min: {} < f_max: {}".format(f_min, self.f_max) + fb = F.melscale_fbanks(n_stft, self.f_min, self.f_max, self.n_mels, self.sample_rate, self.norm, self.mel_scale) + self.register_buffer("fb", fb) def forward(self, specgram: Tensor) -> Tensor: r""" @@ -404,21 +419,32 @@ class InverseMelScale(torch.nn.Module): (area normalization). (Default: ``None``) mel_scale (str, optional): Scale to use: ``htk`` or ``slaney``. (Default: ``htk``) """ - __constants__ = ['n_stft', 'n_mels', 'sample_rate', 'f_min', 'f_max', 'max_iter', 'tolerance_loss', - 'tolerance_change', 'sgdargs'] - - def __init__(self, - n_stft: int, - n_mels: int = 128, - sample_rate: int = 16000, - f_min: float = 0., - f_max: Optional[float] = None, - max_iter: int = 100000, - tolerance_loss: float = 1e-5, - tolerance_change: float = 1e-8, - sgdargs: Optional[dict] = None, - norm: Optional[str] = None, - mel_scale: str = "htk") -> None: + __constants__ = [ + "n_stft", + "n_mels", + "sample_rate", + "f_min", + "f_max", + "max_iter", + "tolerance_loss", + "tolerance_change", + "sgdargs", + ] + + def __init__( + self, + n_stft: int, + n_mels: int = 128, + sample_rate: int = 16000, + f_min: float = 0.0, + f_max: Optional[float] = None, + max_iter: int = 100000, + tolerance_loss: float = 1e-5, + tolerance_change: float = 1e-8, + sgdargs: Optional[dict] = None, + norm: Optional[str] = None, + mel_scale: str = "htk", + ) -> None: super(InverseMelScale, self).__init__() self.n_mels = n_mels self.sample_rate = sample_rate @@ -427,13 +453,12 @@ def __init__(self, self.max_iter = max_iter self.tolerance_loss = tolerance_loss self.tolerance_change = tolerance_change - self.sgdargs = sgdargs or {'lr': 0.1, 'momentum': 0.9} + self.sgdargs = sgdargs or {"lr": 0.1, "momentum": 0.9} - assert f_min <= self.f_max, 'Require f_min: {} < f_max: {}'.format(f_min, self.f_max) + assert f_min <= self.f_max, "Require f_min: {} < f_max: {}".format(f_min, self.f_max) - fb = F.melscale_fbanks(n_stft, self.f_min, self.f_max, self.n_mels, self.sample_rate, - norm, mel_scale) - self.register_buffer('fb', fb) + fb = F.melscale_fbanks(n_stft, self.f_min, self.f_max, self.n_mels, self.sample_rate, norm, mel_scale) + self.register_buffer("fb", fb) def forward(self, melspec: Tensor) -> Tensor: r""" @@ -452,12 +477,13 @@ def forward(self, melspec: Tensor) -> Tensor: melspec = melspec.transpose(-1, -2) assert self.n_mels == n_mels - specgram = torch.rand(melspec.size()[0], time, freq, requires_grad=True, - dtype=melspec.dtype, device=melspec.device) + specgram = torch.rand( + melspec.size()[0], time, freq, requires_grad=True, dtype=melspec.dtype, device=melspec.device + ) optim = torch.optim.SGD([specgram], **self.sgdargs) - loss = float('inf') + loss = float("inf") for _ in range(self.max_iter): optim.zero_grad() diff = melspec - specgram.matmul(self.fb) @@ -527,26 +553,28 @@ class MelSpectrogram(torch.nn.Module): :py:func:`torchaudio.functional.melscale_fbanks` - The function used to generate the filter banks. """ - __constants__ = ['sample_rate', 'n_fft', 'win_length', 'hop_length', 'pad', 'n_mels', 'f_min'] - - def __init__(self, - sample_rate: int = 16000, - n_fft: int = 400, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - f_min: float = 0., - f_max: Optional[float] = None, - pad: int = 0, - n_mels: int = 128, - window_fn: Callable[..., Tensor] = torch.hann_window, - power: float = 2., - normalized: bool = False, - wkwargs: Optional[dict] = None, - center: bool = True, - pad_mode: str = "reflect", - onesided: bool = True, - norm: Optional[str] = None, - mel_scale: str = "htk") -> None: + __constants__ = ["sample_rate", "n_fft", "win_length", "hop_length", "pad", "n_mels", "f_min"] + + def __init__( + self, + sample_rate: int = 16000, + n_fft: int = 400, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + f_min: float = 0.0, + f_max: Optional[float] = None, + pad: int = 0, + n_mels: int = 128, + window_fn: Callable[..., Tensor] = torch.hann_window, + power: float = 2.0, + normalized: bool = False, + wkwargs: Optional[dict] = None, + center: bool = True, + pad_mode: str = "reflect", + onesided: bool = True, + norm: Optional[str] = None, + mel_scale: str = "htk", + ) -> None: super(MelSpectrogram, self).__init__() self.sample_rate = sample_rate self.n_fft = n_fft @@ -558,19 +586,21 @@ def __init__(self, self.n_mels = n_mels # number of mel frequency bins self.f_max = f_max self.f_min = f_min - self.spectrogram = Spectrogram(n_fft=self.n_fft, win_length=self.win_length, - hop_length=self.hop_length, - pad=self.pad, window_fn=window_fn, power=self.power, - normalized=self.normalized, wkwargs=wkwargs, - center=center, pad_mode=pad_mode, onesided=onesided) + self.spectrogram = Spectrogram( + n_fft=self.n_fft, + win_length=self.win_length, + hop_length=self.hop_length, + pad=self.pad, + window_fn=window_fn, + power=self.power, + normalized=self.normalized, + wkwargs=wkwargs, + center=center, + pad_mode=pad_mode, + onesided=onesided, + ) self.mel_scale = MelScale( - self.n_mels, - self.sample_rate, - self.f_min, - self.f_max, - self.n_fft // 2 + 1, - norm, - mel_scale + self.n_mels, self.sample_rate, self.f_min, self.f_max, self.n_fft // 2 + 1, norm, mel_scale ) def forward(self, waveform: Tensor) -> Tensor: @@ -609,33 +639,35 @@ class MFCC(torch.nn.Module): :py:func:`torchaudio.functional.melscale_fbanks` - The function used to generate the filter banks. """ - __constants__ = ['sample_rate', 'n_mfcc', 'dct_type', 'top_db', 'log_mels'] - - def __init__(self, - sample_rate: int = 16000, - n_mfcc: int = 40, - dct_type: int = 2, - norm: str = 'ortho', - log_mels: bool = False, - melkwargs: Optional[dict] = None) -> None: + __constants__ = ["sample_rate", "n_mfcc", "dct_type", "top_db", "log_mels"] + + def __init__( + self, + sample_rate: int = 16000, + n_mfcc: int = 40, + dct_type: int = 2, + norm: str = "ortho", + log_mels: bool = False, + melkwargs: Optional[dict] = None, + ) -> None: super(MFCC, self).__init__() supported_dct_types = [2] if dct_type not in supported_dct_types: - raise ValueError('DCT type not supported: {}'.format(dct_type)) + raise ValueError("DCT type not supported: {}".format(dct_type)) self.sample_rate = sample_rate self.n_mfcc = n_mfcc self.dct_type = dct_type self.norm = norm self.top_db = 80.0 - self.amplitude_to_DB = AmplitudeToDB('power', self.top_db) + self.amplitude_to_DB = AmplitudeToDB("power", self.top_db) melkwargs = melkwargs or {} self.MelSpectrogram = MelSpectrogram(sample_rate=self.sample_rate, **melkwargs) if self.n_mfcc > self.MelSpectrogram.n_mels: - raise ValueError('Cannot select more MFCC coefficients than # mel bins') + raise ValueError("Cannot select more MFCC coefficients than # mel bins") dct_mat = F.create_dct(self.n_mfcc, self.MelSpectrogram.n_mels, self.norm) - self.register_buffer('dct_mat', dct_mat) + self.register_buffer("dct_mat", dct_mat) self.log_mels = log_mels def forward(self, waveform: Tensor) -> Tensor: @@ -685,22 +717,24 @@ class LFCC(torch.nn.Module): :py:func:`torchaudio.functional.linear_fbanks` - The function used to generate the filter banks. """ - __constants__ = ['sample_rate', 'n_filter', 'n_lfcc', 'dct_type', 'top_db', 'log_lf'] - - def __init__(self, - sample_rate: int = 16000, - n_filter: int = 128, - f_min: float = 0., - f_max: Optional[float] = None, - n_lfcc: int = 40, - dct_type: int = 2, - norm: str = 'ortho', - log_lf: bool = False, - speckwargs: Optional[dict] = None) -> None: + __constants__ = ["sample_rate", "n_filter", "n_lfcc", "dct_type", "top_db", "log_lf"] + + def __init__( + self, + sample_rate: int = 16000, + n_filter: int = 128, + f_min: float = 0.0, + f_max: Optional[float] = None, + n_lfcc: int = 40, + dct_type: int = 2, + norm: str = "ortho", + log_lf: bool = False, + speckwargs: Optional[dict] = None, + ) -> None: super(LFCC, self).__init__() supported_dct_types = [2] if dct_type not in supported_dct_types: - raise ValueError('DCT type not supported: {}'.format(dct_type)) + raise ValueError("DCT type not supported: {}".format(dct_type)) self.sample_rate = sample_rate self.f_min = f_min self.f_max = f_max if f_max is not None else float(sample_rate // 2) @@ -709,13 +743,13 @@ def __init__(self, self.dct_type = dct_type self.norm = norm self.top_db = 80.0 - self.amplitude_to_DB = AmplitudeToDB('power', self.top_db) + self.amplitude_to_DB = AmplitudeToDB("power", self.top_db) speckwargs = speckwargs or {} self.Spectrogram = Spectrogram(**speckwargs) if self.n_lfcc > self.Spectrogram.n_fft: - raise ValueError('Cannot select more LFCC coefficients than # fft bins') + raise ValueError("Cannot select more LFCC coefficients than # fft bins") filter_mat = F.linear_fbanks( n_freqs=self.Spectrogram.n_fft // 2 + 1, @@ -727,7 +761,7 @@ def __init__(self, self.register_buffer("filter_mat", filter_mat) dct_mat = F.create_dct(self.n_lfcc, self.n_filter, self.norm) - self.register_buffer('dct_mat', dct_mat) + self.register_buffer("dct_mat", dct_mat) self.log_lf = log_lf def forward(self, waveform: Tensor) -> Tensor: @@ -770,7 +804,7 @@ class MuLawEncoding(torch.nn.Module): >>> mulawtrans = transform(waveform) """ - __constants__ = ['quantization_channels'] + __constants__ = ["quantization_channels"] def __init__(self, quantization_channels: int = 256) -> None: super(MuLawEncoding, self).__init__() @@ -802,7 +836,7 @@ class MuLawDecoding(torch.nn.Module): >>> transform = torchaudio.transforms.MuLawDecoding(quantization_channels=512) >>> mulawtrans = transform(waveform) """ - __constants__ = ['quantization_channels'] + __constants__ = ["quantization_channels"] def __init__(self, quantization_channels: int = 256) -> None: super(MuLawDecoding, self).__init__() @@ -853,15 +887,15 @@ class Resample(torch.nn.Module): """ def __init__( - self, - orig_freq: int = 16000, - new_freq: int = 16000, - resampling_method: str = 'sinc_interpolation', - lowpass_filter_width: int = 6, - rolloff: float = 0.99, - beta: Optional[float] = None, - *, - dtype: Optional[torch.dtype] = None, + self, + orig_freq: int = 16000, + new_freq: int = 16000, + resampling_method: str = "sinc_interpolation", + lowpass_filter_width: int = 6, + rolloff: float = 0.99, + beta: Optional[float] = None, + *, + dtype: Optional[torch.dtype] = None, ) -> None: super().__init__() @@ -875,10 +909,16 @@ def __init__( if self.orig_freq != self.new_freq: kernel, self.width = _get_sinc_resample_kernel( - self.orig_freq, self.new_freq, self.gcd, - self.lowpass_filter_width, self.rolloff, - self.resampling_method, beta, dtype=dtype) - self.register_buffer('kernel', kernel) + self.orig_freq, + self.new_freq, + self.gcd, + self.lowpass_filter_width, + self.rolloff, + self.resampling_method, + beta, + dtype=dtype, + ) + self.register_buffer("kernel", kernel) def forward(self, waveform: Tensor) -> Tensor: r""" @@ -890,9 +930,7 @@ def forward(self, waveform: Tensor) -> Tensor: """ if self.orig_freq == self.new_freq: return waveform - return _apply_sinc_resample_kernel( - waveform, self.orig_freq, self.new_freq, self.gcd, - self.kernel, self.width) + return _apply_sinc_resample_kernel(waveform, self.orig_freq, self.new_freq, self.gcd, self.kernel, self.width) class ComputeDeltas(torch.nn.Module): @@ -904,7 +942,7 @@ class ComputeDeltas(torch.nn.Module): win_length (int, optional): The window length used for computing delta. (Default: ``5``) mode (str, optional): Mode parameter passed to padding. (Default: ``'replicate'``) """ - __constants__ = ['win_length'] + __constants__ = ["win_length"] def __init__(self, win_length: int = 5, mode: str = "replicate") -> None: super(ComputeDeltas, self).__init__() @@ -954,19 +992,16 @@ class TimeStretch(torch.nn.Module): :alt: Spectrogram streched by 0.9 """ - __constants__ = ['fixed_rate'] + __constants__ = ["fixed_rate"] - def __init__(self, - hop_length: Optional[int] = None, - n_freq: int = 201, - fixed_rate: Optional[float] = None) -> None: + def __init__(self, hop_length: Optional[int] = None, n_freq: int = 201, fixed_rate: Optional[float] = None) -> None: super(TimeStretch, self).__init__() self.fixed_rate = fixed_rate n_fft = (n_freq - 1) * 2 hop_length = hop_length if hop_length is not None else n_fft // 2 - self.register_buffer('phase_advance', torch.linspace(0, math.pi * hop_length, n_freq)[..., None]) + self.register_buffer("phase_advance", torch.linspace(0, math.pi * hop_length, n_freq)[..., None]) def forward(self, complex_specgrams: Tensor, overriding_rate: Optional[float] = None) -> Tensor: r""" @@ -983,8 +1018,7 @@ def forward(self, complex_specgrams: Tensor, overriding_rate: Optional[float] = """ if overriding_rate is None: if self.fixed_rate is None: - raise ValueError( - "If no fixed_rate is specified, must pass a valid rate to the forward method.") + raise ValueError("If no fixed_rate is specified, must pass a valid rate to the forward method.") rate = self.fixed_rate else: rate = overriding_rate @@ -1007,10 +1041,7 @@ class Fade(torch.nn.Module): >>> faded_waveform = transform(waveform) """ - def __init__(self, - fade_in_len: int = 0, - fade_out_len: int = 0, - fade_shape: str = "linear") -> None: + def __init__(self, fade_in_len: int = 0, fade_out_len: int = 0, fade_shape: str = "linear") -> None: super(Fade, self).__init__() self.fade_in_len = fade_in_len self.fade_out_len = fade_out_len @@ -1026,11 +1057,7 @@ def forward(self, waveform: Tensor) -> Tensor: """ waveform_length = waveform.size()[-1] device = waveform.device - return ( - self._fade_in(waveform_length, device) - * self._fade_out(waveform_length, device) - * waveform - ) + return self._fade_in(waveform_length, device) * self._fade_out(waveform_length, device) * waveform def _fade_in(self, waveform_length: int, device: torch.device) -> Tensor: fade = torch.linspace(0, 1, self.fade_in_len, device=device) @@ -1043,7 +1070,7 @@ def _fade_in(self, waveform_length: int, device: torch.device) -> Tensor: fade = torch.pow(2, (fade - 1)) * fade if self.fade_shape == "logarithmic": - fade = torch.log10(.1 + fade) + 1 + fade = torch.log10(0.1 + fade) + 1 if self.fade_shape == "quarter_sine": fade = torch.sin(fade * math.pi / 2) @@ -1058,10 +1085,10 @@ def _fade_out(self, waveform_length: int, device: torch.device) -> Tensor: ones = torch.ones(waveform_length - self.fade_out_len, device=device) if self.fade_shape == "linear": - fade = - fade + 1 + fade = -fade + 1 if self.fade_shape == "exponential": - fade = torch.pow(2, - fade) * (1 - fade) + fade = torch.pow(2, -fade) * (1 - fade) if self.fade_shape == "logarithmic": fade = torch.log10(1.1 - fade) + 1 @@ -1084,7 +1111,7 @@ class _AxisMasking(torch.nn.Module): iid_masks (bool): Applies iid masks to each of the examples in the batch dimension. This option is applicable only when the input tensor is 4D. """ - __constants__ = ['mask_param', 'axis', 'iid_masks'] + __constants__ = ["mask_param", "axis", "iid_masks"] def __init__(self, mask_param: int, axis: int, iid_masks: bool) -> None: @@ -1093,7 +1120,7 @@ def __init__(self, mask_param: int, axis: int, iid_masks: bool) -> None: self.axis = axis self.iid_masks = iid_masks - def forward(self, specgram: Tensor, mask_value: float = 0.) -> Tensor: + def forward(self, specgram: Tensor, mask_value: float = 0.0) -> Tensor: r""" Args: specgram (Tensor): Tensor of dimension `(..., freq, time)`. @@ -1180,12 +1207,12 @@ class Vol(torch.nn.Module): gain_type (str, optional): Type of gain. One of: ``amplitude``, ``power``, ``db`` (Default: ``amplitude``) """ - def __init__(self, gain: float, gain_type: str = 'amplitude'): + def __init__(self, gain: float, gain_type: str = "amplitude"): super(Vol, self).__init__() self.gain = gain self.gain_type = gain_type - if gain_type in ['amplitude', 'power'] and gain < 0: + if gain_type in ["amplitude", "power"] and gain < 0: raise ValueError("If gain_type = amplitude or power, gain must be positive.") def forward(self, waveform: Tensor) -> Tensor: @@ -1221,11 +1248,9 @@ class SlidingWindowCmn(torch.nn.Module): norm_vars (bool, optional): If true, normalize variance to one. (bool, default = false) """ - def __init__(self, - cmn_window: int = 600, - min_cmn_window: int = 100, - center: bool = False, - norm_vars: bool = False) -> None: + def __init__( + self, cmn_window: int = 600, min_cmn_window: int = 100, center: bool = False, norm_vars: bool = False + ) -> None: super().__init__() self.cmn_window = cmn_window self.min_cmn_window = min_cmn_window @@ -1240,8 +1265,7 @@ def forward(self, specgram: Tensor) -> Tensor: Returns: Tensor: Tensor of spectrogram of dimension `(..., time, freq)`. """ - cmn_specgram = F.sliding_window_cmn( - specgram, self.cmn_window, self.min_cmn_window, self.center, self.norm_vars) + cmn_specgram = F.sliding_window_cmn(specgram, self.cmn_window, self.min_cmn_window, self.center, self.norm_vars) return cmn_specgram @@ -1297,24 +1321,26 @@ class Vad(torch.nn.Module): - http://sox.sourceforge.net/sox.html """ - def __init__(self, - sample_rate: int, - trigger_level: float = 7.0, - trigger_time: float = 0.25, - search_time: float = 1.0, - allowed_gap: float = 0.25, - pre_trigger_time: float = 0.0, - boot_time: float = .35, - noise_up_time: float = .1, - noise_down_time: float = .01, - noise_reduction_amount: float = 1.35, - measure_freq: float = 20.0, - measure_duration: Optional[float] = None, - measure_smooth_time: float = .4, - hp_filter_freq: float = 50., - lp_filter_freq: float = 6000., - hp_lifter_freq: float = 150., - lp_lifter_freq: float = 2000.) -> None: + def __init__( + self, + sample_rate: int, + trigger_level: float = 7.0, + trigger_time: float = 0.25, + search_time: float = 1.0, + allowed_gap: float = 0.25, + pre_trigger_time: float = 0.0, + boot_time: float = 0.35, + noise_up_time: float = 0.1, + noise_down_time: float = 0.01, + noise_reduction_amount: float = 1.35, + measure_freq: float = 20.0, + measure_duration: Optional[float] = None, + measure_smooth_time: float = 0.4, + hp_filter_freq: float = 50.0, + lp_filter_freq: float = 6000.0, + hp_lifter_freq: float = 150.0, + lp_lifter_freq: float = 2000.0, + ) -> None: super().__init__() self.sample_rate = sample_rate @@ -1386,23 +1412,25 @@ class SpectralCentroid(torch.nn.Module): >>> transform = transforms.SpectralCentroid(sample_rate) >>> spectral_centroid = transform(waveform) # (channel, time) """ - __constants__ = ['sample_rate', 'n_fft', 'win_length', 'hop_length', 'pad'] - - def __init__(self, - sample_rate: int, - n_fft: int = 400, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - pad: int = 0, - window_fn: Callable[..., Tensor] = torch.hann_window, - wkwargs: Optional[dict] = None) -> None: + __constants__ = ["sample_rate", "n_fft", "win_length", "hop_length", "pad"] + + def __init__( + self, + sample_rate: int, + n_fft: int = 400, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + pad: int = 0, + window_fn: Callable[..., Tensor] = torch.hann_window, + wkwargs: Optional[dict] = None, + ) -> None: super(SpectralCentroid, self).__init__() self.sample_rate = sample_rate self.n_fft = n_fft self.win_length = win_length if win_length is not None else n_fft self.hop_length = hop_length if hop_length is not None else self.win_length // 2 window = window_fn(self.win_length) if wkwargs is None else window_fn(self.win_length, **wkwargs) - self.register_buffer('window', window) + self.register_buffer("window", window) self.pad = pad def forward(self, waveform: Tensor) -> Tensor: @@ -1414,8 +1442,9 @@ def forward(self, waveform: Tensor) -> Tensor: Tensor: Spectral Centroid of size `(..., time)`. """ - return F.spectral_centroid(waveform, self.sample_rate, self.pad, self.window, self.n_fft, self.hop_length, - self.win_length) + return F.spectral_centroid( + waveform, self.sample_rate, self.pad, self.window, self.n_fft, self.hop_length, self.win_length + ) class PitchShift(torch.nn.Module): @@ -1438,17 +1467,19 @@ class PitchShift(torch.nn.Module): >>> transform = transforms.PitchShift(sample_rate, 4) >>> waveform_shift = transform(waveform) # (channel, time) """ - __constants__ = ['sample_rate', 'n_steps', 'bins_per_octave', 'n_fft', 'win_length', 'hop_length'] - - def __init__(self, - sample_rate: int, - n_steps: int, - bins_per_octave: int = 12, - n_fft: int = 512, - win_length: Optional[int] = None, - hop_length: Optional[int] = None, - window_fn: Callable[..., Tensor] = torch.hann_window, - wkwargs: Optional[dict] = None) -> None: + __constants__ = ["sample_rate", "n_steps", "bins_per_octave", "n_fft", "win_length", "hop_length"] + + def __init__( + self, + sample_rate: int, + n_steps: int, + bins_per_octave: int = 12, + n_fft: int = 512, + win_length: Optional[int] = None, + hop_length: Optional[int] = None, + window_fn: Callable[..., Tensor] = torch.hann_window, + wkwargs: Optional[dict] = None, + ) -> None: super(PitchShift, self).__init__() self.n_steps = n_steps self.bins_per_octave = bins_per_octave @@ -1457,7 +1488,7 @@ def __init__(self, self.win_length = win_length if win_length is not None else n_fft self.hop_length = hop_length if hop_length is not None else self.win_length // 4 window = window_fn(self.win_length) if wkwargs is None else window_fn(self.win_length, **wkwargs) - self.register_buffer('window', window) + self.register_buffer("window", window) def forward(self, waveform: Tensor) -> Tensor: r""" @@ -1468,8 +1499,16 @@ def forward(self, waveform: Tensor) -> Tensor: Tensor: The pitch-shifted audio of shape `(..., time)`. """ - return F.pitch_shift(waveform, self.sample_rate, self.n_steps, self.bins_per_octave, self.n_fft, - self.win_length, self.hop_length, self.window) + return F.pitch_shift( + waveform, + self.sample_rate, + self.n_steps, + self.bins_per_octave, + self.n_fft, + self.win_length, + self.hop_length, + self.window, + ) class RNNTLoss(torch.nn.Module): @@ -1506,7 +1545,7 @@ class RNNTLoss(torch.nn.Module): def __init__( self, blank: int = -1, - clamp: float = -1., + clamp: float = -1.0, reduction: str = "mean", ): super().__init__() @@ -1532,15 +1571,7 @@ def forward( Tensor: Loss with the reduction option applied. If ``reduction`` is ``'none'``, then size (batch), otherwise scalar. """ - return F.rnnt_loss( - logits, - targets, - logit_lengths, - target_lengths, - self.blank, - self.clamp, - self.reduction - ) + return F.rnnt_loss(logits, targets, logit_lengths, target_lengths, self.blank, self.clamp, self.reduction) def _get_mat_trace(input: torch.Tensor, dim1: int = -1, dim2: int = -2) -> torch.Tensor: @@ -1557,8 +1588,7 @@ def _get_mat_trace(input: torch.Tensor, dim1: int = -1, dim2: int = -2) -> torch torch.Tensor: trace of the input Tensor """ assert input.ndim >= 2, "The dimension of the tensor must be at least 2." - assert input.shape[dim1] == input.shape[dim2],\ - "The size of ``dim1`` and ``dim2`` must be the same." + assert input.shape[dim1] == input.shape[dim2], "The size of ``dim1`` and ``dim2`` must be the same." input = torch.diagonal(input, 0, dim1=dim1, dim2=dim2) return input.sum(dim=-1) @@ -1684,8 +1714,11 @@ def __init__( online: bool = False, ): super().__init__() - assert solution in ["ref_channel", "stv_evd", "stv_power"],\ - "Unknown solution provided. Must be one of [``ref_channel``, ``stv_evd``, ``stv_power``]." + assert solution in [ + "ref_channel", + "stv_evd", + "stv_power", + ], "Unknown solution provided. Must be one of [``ref_channel``, ``stv_evd``, ``stv_power``]." self.ref_channel = ref_channel self.solution = solution self.multi_mask = multi_mask @@ -1698,10 +1731,10 @@ def __init__( psd_n: torch.Tensor = torch.zeros(1) mask_sum_s: torch.Tensor = torch.zeros(1) mask_sum_n: torch.Tensor = torch.zeros(1) - self.register_buffer('psd_s', psd_s) - self.register_buffer('psd_n', psd_n) - self.register_buffer('mask_sum_s', mask_sum_s) - self.register_buffer('mask_sum_n', mask_sum_n) + self.register_buffer("psd_s", psd_s) + self.register_buffer("psd_n", psd_n) + self.register_buffer("mask_sum_s", mask_sum_s) + self.register_buffer("mask_sum_n", mask_sum_n) def _get_updated_mvdr_vector( self, @@ -1710,7 +1743,7 @@ def _get_updated_mvdr_vector( mask_s: torch.Tensor, mask_n: torch.Tensor, reference_vector: torch.Tensor, - solution: str = 'ref_channel', + solution: str = "ref_channel", diagonal_loading: bool = True, diag_eps: float = 1e-7, eps: float = 1e-8, @@ -1788,7 +1821,7 @@ def _get_mvdr_vector( psd_s: torch.Tensor, psd_n: torch.Tensor, reference_vector: torch.Tensor, - solution: str = 'ref_channel', + solution: str = "ref_channel", diagonal_loading: bool = True, diag_eps: float = 1e-7, eps: float = 1e-8, @@ -1851,10 +1884,7 @@ def _get_steering_vector_evd(self, psd_s: torch.Tensor) -> torch.Tensor: return stv def _get_steering_vector_power( - self, - psd_s: torch.Tensor, - psd_n: torch.Tensor, - reference_vector: torch.Tensor + self, psd_s: torch.Tensor, psd_n: torch.Tensor, reference_vector: torch.Tensor ) -> torch.Tensor: r"""Estimate the steering vector by the power method. @@ -1876,11 +1906,7 @@ def _get_steering_vector_power( stv = torch.matmul(psd_s, stv) return stv - def _apply_beamforming_vector( - self, - specgram: torch.Tensor, - beamform_vector: torch.Tensor - ) -> torch.Tensor: + def _apply_beamforming_vector(self, specgram: torch.Tensor, beamform_vector: torch.Tensor) -> torch.Tensor: r"""Apply the beamforming weight to the noisy STFT Args: specgram (torch.tensor): multi-channel noisy STFT @@ -1896,12 +1922,7 @@ def _apply_beamforming_vector( specgram_enhanced = torch.einsum("...fc,...cft->...ft", [beamform_vector.conj(), specgram]) return specgram_enhanced - def _tik_reg( - self, - mat: torch.Tensor, - reg: float = 1e-7, - eps: float = 1e-8 - ) -> torch.Tensor: + def _tik_reg(self, mat: torch.Tensor, reg: float = 1e-7, eps: float = 1e-8) -> torch.Tensor: """Perform Tikhonov regularization (only modifying real part). Args: mat (torch.Tensor): input matrix (..., channel, channel) @@ -1922,10 +1943,7 @@ def _tik_reg( return mat def forward( - self, - specgram: torch.Tensor, - mask_s: torch.Tensor, - mask_n: Optional[torch.Tensor] = None + self, specgram: torch.Tensor, mask_s: torch.Tensor, mask_n: Optional[torch.Tensor] = None ) -> torch.Tensor: """Perform MVDR beamforming. @@ -1946,9 +1964,7 @@ def forward( """ dtype = specgram.dtype if specgram.ndim < 3: - raise ValueError( - f"Expected at least 3D tensor (..., channel, freq, time). Found: {specgram.shape}" - ) + raise ValueError(f"Expected at least 3D tensor (..., channel, freq, time). Found: {specgram.shape}") if not specgram.is_complex(): raise ValueError( f"The type of ``specgram`` tensor must be ``torch.cfloat`` or ``torch.cdouble``.\ @@ -1958,9 +1974,7 @@ def forward( specgram = specgram.cdouble() # Convert specgram to ``torch.cdouble``. if mask_n is None: - warnings.warn( - "``mask_n`` is not provided, use ``1 - mask_s`` as ``mask_n``." - ) + warnings.warn("``mask_n`` is not provided, use ``1 - mask_s`` as ``mask_n``.") mask_n = 1 - mask_s shape = specgram.size() @@ -1977,33 +1991,15 @@ def forward( psd_s = self.psd(specgram, mask_s) # (..., freq, time, channel, channel) psd_n = self.psd(specgram, mask_n) # (..., freq, time, channel, channel) - u = torch.zeros( - specgram.size()[:-2], - device=specgram.device, - dtype=torch.cdouble - ) # (..., channel) + u = torch.zeros(specgram.size()[:-2], device=specgram.device, dtype=torch.cdouble) # (..., channel) u[..., self.ref_channel].fill_(1) if self.online: w_mvdr = self._get_updated_mvdr_vector( - psd_s, - psd_n, - mask_s, - mask_n, - u, - self.solution, - self.diag_loading, - self.diag_eps + psd_s, psd_n, mask_s, mask_n, u, self.solution, self.diag_loading, self.diag_eps ) else: - w_mvdr = self._get_mvdr_vector( - psd_s, - psd_n, - u, - self.solution, - self.diag_loading, - self.diag_eps - ) + w_mvdr = self._get_mvdr_vector(psd_s, psd_n, u, self.solution, self.diag_loading, self.diag_eps) specgram_enhanced = self._apply_beamforming_vector(specgram, w_mvdr) diff --git a/torchaudio/utils/__init__.py b/torchaudio/utils/__init__.py index 578a51925a..c2330906cd 100644 --- a/torchaudio/utils/__init__.py +++ b/torchaudio/utils/__init__.py @@ -1,7 +1,8 @@ +from torchaudio._internal import module_utils as _mod_utils + from . import ( sox_utils, ) -from torchaudio._internal import module_utils as _mod_utils if _mod_utils.is_sox_available(): From a76b0066d4b3fbe303b92e8bb903514a11195fab Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Thu, 23 Dec 2021 14:41:08 -0800 Subject: [PATCH 0053/1144] Add Python CTC decoder API (#2089) Summary: Part of https://github.com/pytorch/audio/issues/2072 -- splitting up PR for easier review This PR adds Python decoder API and basic README Pull Request resolved: https://github.com/pytorch/audio/pull/2089 Reviewed By: mthrok Differential Revision: D33299818 Pulled By: carolineechen fbshipit-source-id: 778ec3692331e95258d3734f0d4ab60b6618ddbc --- docs/source/prototype.rst | 20 ++ .../assets/decoder/kenlm.arpa | 35 +++ .../assets/decoder/lexicon.txt | 3 + .../assets/decoder/tokens.txt | 7 + .../common_utils/__init__.py | 2 + .../common_utils/case_utils.py | 2 + .../common_utils/ctc_decoder_utils.py | 7 + .../prototype/ctc_decoder_test.py | 47 ++++ torchaudio/csrc/decoder/README.md | 36 +++ torchaudio/prototype/ctc_decoder/__init__.py | 16 ++ .../prototype/ctc_decoder/ctc_decoder.py | 218 ++++++++++++++++++ 11 files changed, 393 insertions(+) create mode 100644 test/torchaudio_unittest/assets/decoder/kenlm.arpa create mode 100644 test/torchaudio_unittest/assets/decoder/lexicon.txt create mode 100644 test/torchaudio_unittest/assets/decoder/tokens.txt create mode 100644 test/torchaudio_unittest/common_utils/ctc_decoder_utils.py create mode 100644 test/torchaudio_unittest/prototype/ctc_decoder_test.py create mode 100644 torchaudio/csrc/decoder/README.md create mode 100644 torchaudio/prototype/ctc_decoder/__init__.py create mode 100644 torchaudio/prototype/ctc_decoder/ctc_decoder.py diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index e42376a9c0..6a26981e90 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -73,6 +73,26 @@ Hypothesis .. autoclass:: Hypothesis +KenLMLexiconDecoder +~~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: torchaudio.prototype.ctc_decoder + +.. autoclass:: KenLMLexiconDecoder + + .. automethod:: __call__ + + .. automethod:: idxs_to_tokens + + +kenlm_lexicon_decoder +~~~~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: torchaudio.prototype.ctc_decoder + +.. autoclass:: kenlm_lexicon_decoder + + References ~~~~~~~~~~ diff --git a/test/torchaudio_unittest/assets/decoder/kenlm.arpa b/test/torchaudio_unittest/assets/decoder/kenlm.arpa new file mode 100644 index 0000000000..234f85af3a --- /dev/null +++ b/test/torchaudio_unittest/assets/decoder/kenlm.arpa @@ -0,0 +1,35 @@ +\data\ +ngram 1=6 +ngram 2=9 +ngram 3=8 + +\1-grams: +-0.8515802 0 +0 -0.30103 +-0.8515802 0 +-0.8515802 foo -0.30103 +-0.44013768 bar -0.30103 +-0.6679358 foobar -0.30103 + +\2-grams: +-0.7091413 foo 0 +-0.6251838 bar 0 +-0.24384303 foobar 0 +-0.6251838 foo -0.30103 +-0.49434766 foo foo -0.30103 +-0.39393726 bar foo -0.30103 +-0.4582359 bar -0.30103 +-0.51359576 foo bar -0.30103 +-0.56213206 foobar -0.30103 + +\3-grams: +-0.45881382 bar foo +-0.43354067 foo bar +-0.105027884 foobar +-0.18033421 foo foo +-0.38702002 bar foo foo +-0.15375455 bar foo +-0.34500393 foo bar foo +-0.18492673 foo foo bar + +\end\ diff --git a/test/torchaudio_unittest/assets/decoder/lexicon.txt b/test/torchaudio_unittest/assets/decoder/lexicon.txt new file mode 100644 index 0000000000..5918cd3ab1 --- /dev/null +++ b/test/torchaudio_unittest/assets/decoder/lexicon.txt @@ -0,0 +1,3 @@ +foo f o o | +bar b a r | +foobar f o o b a r | diff --git a/test/torchaudio_unittest/assets/decoder/tokens.txt b/test/torchaudio_unittest/assets/decoder/tokens.txt new file mode 100644 index 0000000000..239cf8e21a --- /dev/null +++ b/test/torchaudio_unittest/assets/decoder/tokens.txt @@ -0,0 +1,7 @@ +- +| +f +o +b +a +r diff --git a/test/torchaudio_unittest/common_utils/__init__.py b/test/torchaudio_unittest/common_utils/__init__.py index 2dedf52f02..e53489cf04 100644 --- a/test/torchaudio_unittest/common_utils/__init__.py +++ b/test/torchaudio_unittest/common_utils/__init__.py @@ -7,6 +7,7 @@ TestBaseMixin, PytorchTestCase, TorchaudioTestCase, + skipIfNoCtcDecoder, skipIfNoCuda, skipIfNoExec, skipIfNoModule, @@ -42,6 +43,7 @@ "TestBaseMixin", "PytorchTestCase", "TorchaudioTestCase", + "skipIfNoCtcDecoder", "skipIfNoCuda", "skipIfNoExec", "skipIfNoModule", diff --git a/test/torchaudio_unittest/common_utils/case_utils.py b/test/torchaudio_unittest/common_utils/case_utils.py index 840227c75c..329d364cf2 100644 --- a/test/torchaudio_unittest/common_utils/case_utils.py +++ b/test/torchaudio_unittest/common_utils/case_utils.py @@ -10,6 +10,7 @@ from torchaudio._internal.module_utils import is_module_available, is_sox_available, is_kaldi_available from .backend_utils import set_audio_backend +from .ctc_decoder_utils import is_ctc_decoder_available class TempDirMixin: @@ -115,6 +116,7 @@ def skipIfNoCuda(test_item): skipIfNoSox = unittest.skipIf(not is_sox_available(), reason="Sox not available") skipIfNoKaldi = unittest.skipIf(not is_kaldi_available(), reason="Kaldi not available") +skipIfNoCtcDecoder = unittest.skipIf(not is_ctc_decoder_available(), reason="CTC decoder not available") skipIfRocm = unittest.skipIf( os.getenv("TORCHAUDIO_TEST_WITH_ROCM", "0") == "1", reason="test doesn't currently work on the ROCm stack" ) diff --git a/test/torchaudio_unittest/common_utils/ctc_decoder_utils.py b/test/torchaudio_unittest/common_utils/ctc_decoder_utils.py new file mode 100644 index 0000000000..bdeb17c53c --- /dev/null +++ b/test/torchaudio_unittest/common_utils/ctc_decoder_utils.py @@ -0,0 +1,7 @@ +def is_ctc_decoder_available(): + try: + import torchaudio.prototype.ctc_decoder # noqa: F401 + + return True + except ImportError: + return False diff --git a/test/torchaudio_unittest/prototype/ctc_decoder_test.py b/test/torchaudio_unittest/prototype/ctc_decoder_test.py new file mode 100644 index 0000000000..a308b04e85 --- /dev/null +++ b/test/torchaudio_unittest/prototype/ctc_decoder_test.py @@ -0,0 +1,47 @@ +import torch +from torchaudio_unittest.common_utils import ( + TempDirMixin, + TorchaudioTestCase, + get_asset_path, + skipIfNoCtcDecoder, +) + + +@skipIfNoCtcDecoder +class CTCDecoderTest(TempDirMixin, TorchaudioTestCase): + def _get_decoder(self): + from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder + + lexicon_file = get_asset_path("decoder/lexicon.txt") + tokens_file = get_asset_path("decoder/tokens.txt") + kenlm_file = get_asset_path("decoder/kenlm.arpa") + + return kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens_file, + kenlm=kenlm_file, + ) + + def test_construct_decoder(self): + self._get_decoder() + + def test_shape(self): + B, T, N = 4, 15, 10 + + torch.manual_seed(0) + emissions = torch.rand(B, T, N) + + decoder = self._get_decoder() + results = decoder(emissions) + + self.assertEqual(len(results), B) + + def test_index_to_tokens(self): + # decoder tokens: '-' '|' 'f' 'o' 'b' 'a' 'r' + decoder = self._get_decoder() + + idxs = torch.LongTensor((1, 2, 1, 3, 5)) + tokens = decoder.idxs_to_tokens(idxs) + + expected_tokens = ["|", "f", "|", "o", "a"] + self.assertEqual(tokens, expected_tokens) diff --git a/torchaudio/csrc/decoder/README.md b/torchaudio/csrc/decoder/README.md new file mode 100644 index 0000000000..4e7e017e61 --- /dev/null +++ b/torchaudio/csrc/decoder/README.md @@ -0,0 +1,36 @@ +# Flashlight Decoder Binding +CTC Decoder with KenLM and lexicon support based on [flashlight](https://github.com/flashlight/flashlight) decoder implementation +and fairseq [KenLMDecoder](https://github.com/pytorch/fairseq/blob/fcca32258c8e8bcc9f9890bf4714fa2f96b6b3e1/examples/speech_recognition/new/decoders/flashlight_decoder.py#L53) +Python wrapper + +## Setup +### Build torchaudio with decoder support +``` +BUILD_CTC_DECODER=1 python setup.py develop +``` + +## Usage +```py +from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder +decoder = kenlm_lexicon_decoder(args...) +results = decoder(emissions) # dim (B, nbest) of dictionary of "tokens", "score", "words" keys +best_transcripts = [" ".join(results[i][0].words).strip() for i in range(B)] +``` + +## Required Files +- tokens: tokens for which the acoustic model generates probabilities for +- lexicon: mapping between words and its corresponding spelling +- language model: n-gram KenLM model + +## Experiment Results +LibriSpeech dev-other and test-other results using pretrained [Wav2Vec2](https://arxiv.org/pdf/2006.11477.pdf) models of +BASE configuration. + +| Model | Decoder | dev-other | test-other | beam search params | +| ----------- | ---------- | ----------- | ---------- |-------------------------------------------- | +| BASE_10M | Greedy | 51.6 | 51 | | +| | 4-gram LM | 15.95 | 15.9 | LM weight=3.23, word score=-0.26, beam=1500 | +| BASE_100H | Greedy | 13.6 | 13.3 | | +| | 4-gram LM | 8.5 | 8.8 | LM weight=2.15, word score=-0.52, beam=50 | +| BASE_960H | Greedy | 8.9 | 8.4 | | +| | 4-gram LM | 6.3 | 6.4 | LM weight=1.74, word score=0.52, beam=50 | diff --git a/torchaudio/prototype/ctc_decoder/__init__.py b/torchaudio/prototype/ctc_decoder/__init__.py new file mode 100644 index 0000000000..9ee466e256 --- /dev/null +++ b/torchaudio/prototype/ctc_decoder/__init__.py @@ -0,0 +1,16 @@ +import torchaudio + +try: + torchaudio._extension._load_lib("libtorchaudio_decoder") + from .ctc_decoder import KenLMLexiconDecoder, kenlm_lexicon_decoder +except ImportError as err: + raise ImportError( + "flashlight decoder bindings are required to use this functionality. " + "Please set BUILD_CTC_DECODER=1 when building from source." + ) from err + + +__all__ = [ + "KenLMLexiconDecoder", + "kenlm_lexicon_decoder", +] diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py new file mode 100644 index 0000000000..f1464c41e9 --- /dev/null +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -0,0 +1,218 @@ +import itertools as it +from collections import namedtuple +from typing import List, Optional + +import torch +from torchaudio._torchaudio_decoder import ( + _CriterionType, + _KenLM, + _LexiconDecoder, + _LexiconDecoderOptions, + _SmearingMode, + _Trie, + _Dictionary, + _create_word_dict, + _load_words, +) +from typing import Dict + + +__all__ = ["KenLMLexiconDecoder", "kenlm_lexicon_decoder"] + + +Hypothesis = namedtuple("Hypothesis", ["tokens", "words", "score"]) + + +class KenLMLexiconDecoder: + def __init__( + self, + nbest: int, + lexicon: Dict, + word_dict: _Dictionary, + tokens_dict: _Dictionary, + kenlm: _KenLM, + decoder_options: _LexiconDecoderOptions, + blank_token: str, + sil_token: str, + unk_word: str, + ) -> None: + """ + KenLM CTC Decoder with Lexicon constraint. + + Note: + To build the decoder, please use the factory function kenlm_lexicon_decoder. + + Args: + nbest (int): number of best decodings to return + lexicon (Dict): lexicon mapping of words to spellings + word_dict (_Dictionary): dictionary of words + tokens_dict (_Dictionary): dictionary of tokens + kenlm (_KenLM): n-gram KenLM language model + decoder_options (_LexiconDecoderOptions): parameters used for beam search decoding + blank_token (str): token corresopnding to blank + sil_token (str): token corresponding to silence + unk_word (str): word corresponding to unknown + """ + + self.nbest = nbest + self.word_dict = word_dict + self.tokens_dict = tokens_dict + + unk_word = word_dict.get_index(unk_word) + self.blank = self.tokens_dict.get_index(blank_token) + silence = self.tokens_dict.get_index(sil_token) + + vocab_size = self.tokens_dict.index_size() + trie = _Trie(vocab_size, silence) + start_state = kenlm.start(False) + + for word, spellings in lexicon.items(): + word_idx = self.word_dict.get_index(word) + _, score = kenlm.score(start_state, word_idx) + for spelling in spellings: + spelling_idx = [self.tokens_dict.get_index(token) for token in spelling] + trie.insert(spelling_idx, word_idx, score) + trie.smear(_SmearingMode.MAX) + + self.decoder = _LexiconDecoder( + decoder_options, + trie, + kenlm, + silence, + self.blank, + unk_word, + [], + False, # word level LM + ) + + def _get_tokens(self, idxs: torch.IntTensor) -> torch.LongTensor: + idxs = (g[0] for g in it.groupby(idxs)) + idxs = filter(lambda x: x != self.blank, idxs) + return torch.LongTensor(list(idxs)) + + def __call__(self, emissions: torch.FloatTensor, lengths: Optional[torch.Tensor] = None) -> List[List[Hypothesis]]: + """ + Args: + emissions (torch.FloatTensor): tensor of shape `(batch, frame, num_tokens)` storing sequences of + probability distribution over labels; output of acoustic model + lengths (Tensor or None, optional): tensor of shape `(batch, )` storing the valid length of + in time axis of the output Tensor in each batch + + Returns: + List[List[Hypothesis]]: + List of sorted best hypotheses for each audio sequence in the batch. + + Each hypothesis is named tuple with the following fields: + tokens: torch.LongTensor of raw token IDs + score: hypothesis score + words: list of decoded words + """ + assert emissions.dtype == torch.float32 + + B, T, N = emissions.size() + if lengths is None: + lengths = torch.full((B,), T) + + float_bytes = 4 + hypos = [] + for b in range(B): + emissions_ptr = emissions.data_ptr() + float_bytes * b * emissions.stride(0) + + results = self.decoder.decode(emissions_ptr, lengths[b], N) + + nbest_results = results[: self.nbest] + hypos.append( + [ + Hypothesis( + self._get_tokens(result.tokens), # token ids + [self.word_dict.get_entry(x) for x in result.words if x >= 0], # words + result.score, # score + ) + for result in nbest_results + ] + ) + + return hypos + + def idxs_to_tokens(self, idxs: torch.LongTensor) -> List: + """ + Map raw token IDs into corresponding tokens + + Args: + idxs (LongTensor): raw token IDs generated from decoder + + Returns: + List: tokens corresponding to the input IDs + """ + return [self.tokens_dict.get_entry(idx.item()) for idx in idxs] + + +def kenlm_lexicon_decoder( + lexicon: str, + tokens: str, + kenlm: str, + nbest: int = 1, + beam_size: int = 50, + beam_size_token: Optional[int] = None, + beam_threshold: float = 50, + lm_weight: float = 2, + word_score: float = 0, + unk_score: float = float("-inf"), + sil_score: float = 0, + log_add: bool = False, + blank_token: str = "-", + sil_token: str = "|", + unk_word: str = "", +) -> KenLMLexiconDecoder: + """ + Builds Ken LM CTC Lexicon Decoder with given parameters + + Args: + lexicon (str): lexicon file containing the possible words + tokens (str): file containing valid tokens + kenlm (str): file containing languge model + nbest (int, optional): number of best decodings to return (Default: 1) + beam_size (int, optional): max number of hypos to hold after each decode step (Default: 50) + beam_size_token (int, optional): max number of tokens to consider at each decode step. + If None, it is set to the total number of tokens (Default: None) + beam_threshold (float, optional): threshold for pruning hypothesis (Default: 50) + lm_weight (float, optional): weight of lm (Default: 2) + word_score (float, optional): word insertion score (Default: 0) + unk_score (float, optional): unknown word insertion score (Default: -inf) + sil_score (float, optional): silence insertion score (Default: 0) + log_add (bool, optional): whether or not to use logadd when merging hypotheses (Default: False) + blank_token (str, optional): token corresponding to blank (Default: "-") + sil_token (str, optional): token corresponding to silence (Default: "|") + unk_word (str, optional): word corresponding to unknown (Default: "") + + Returns: + KenLMLexiconDecoder: decoder + """ + lexicon = _load_words(lexicon) + word_dict = _create_word_dict(lexicon) + kenlm = _KenLM(kenlm, word_dict) + tokens_dict = _Dictionary(tokens) + + decoder_options = _LexiconDecoderOptions( + beam_size=beam_size, + beam_size_token=beam_size_token or tokens_dict.index_size(), + beam_threshold=beam_threshold, + lm_weight=lm_weight, + word_score=word_score, + unk_score=unk_score, + sil_score=sil_score, + log_add=log_add, + criterion_type=_CriterionType.CTC, + ) + + return KenLMLexiconDecoder( + nbest=nbest, + lexicon=lexicon, + word_dict=word_dict, + tokens_dict=tokens_dict, + kenlm=kenlm, + decoder_options=decoder_options, + blank_token=blank_token, + sil_token=sil_token, + unk_word=unk_word, + ) From cd52d008c42bff71672bb5a8fbdcaf05c6e464ae Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 23 Dec 2021 14:50:33 -0800 Subject: [PATCH 0054/1144] Add FilterGraph class (#2043) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add FilterGraph class that is responsible for handling AVFilterGraph structure and the application of filters. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2042. Pull Request resolved: https://github.com/pytorch/audio/pull/2043 Reviewed By: carolineechen Differential Revision: D32940535 Pulled By: mthrok fbshipit-source-id: 231e3ad17df2d67b6c7b323e5c89e718a3d48d0d --- torchaudio/csrc/ffmpeg/filter_graph.cpp | 181 ++++++++++++++++++++++++ torchaudio/csrc/ffmpeg/filter_graph.h | 52 +++++++ 2 files changed, 233 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/filter_graph.cpp create mode 100644 torchaudio/csrc/ffmpeg/filter_graph.h diff --git a/torchaudio/csrc/ffmpeg/filter_graph.cpp b/torchaudio/csrc/ffmpeg/filter_graph.cpp new file mode 100644 index 0000000000..9d5155158b --- /dev/null +++ b/torchaudio/csrc/ffmpeg/filter_graph.cpp @@ -0,0 +1,181 @@ +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +FilterGraph::FilterGraph( + AVRational time_base, + AVCodecParameters* codecpar, + std::string filter_description) + : filter_description(filter_description) { + add_src(time_base, codecpar); + add_sink(); + add_process(); + create_filter(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Configuration methods +//////////////////////////////////////////////////////////////////////////////// +namespace { +std::string get_audio_src_args( + AVRational time_base, + AVCodecParameters* codecpar) { + char args[512]; + std::snprintf( + args, + sizeof(args), + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64, + time_base.num, + time_base.den, + codecpar->sample_rate, + av_get_sample_fmt_name(static_cast(codecpar->format)), + codecpar->channel_layout); + return std::string(args); +} + +std::string get_video_src_args( + AVRational time_base, + AVCodecParameters* codecpar) { + char args[512]; + std::snprintf( + args, + sizeof(args), + "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", + codecpar->width, + codecpar->height, + static_cast(codecpar->format), + time_base.num, + time_base.den, + codecpar->sample_aspect_ratio.num, + codecpar->sample_aspect_ratio.den); + return std::string(args); +} + +} // namespace + +void FilterGraph::add_src(AVRational time_base, AVCodecParameters* codecpar) { + if (media_type != AVMEDIA_TYPE_UNKNOWN) { + throw std::runtime_error("Source buffer is already allocated."); + } + media_type = codecpar->codec_type; + std::string args; + switch (media_type) { + case AVMEDIA_TYPE_AUDIO: + args = get_audio_src_args(time_base, codecpar); + break; + case AVMEDIA_TYPE_VIDEO: + args = get_video_src_args(time_base, codecpar); + break; + default: + throw std::runtime_error("Only audio/video are supported."); + } + + const AVFilter* buffersrc = avfilter_get_by_name( + media_type == AVMEDIA_TYPE_AUDIO ? "abuffer" : "buffer"); + int ret = avfilter_graph_create_filter( + &buffersrc_ctx, buffersrc, "in", args.c_str(), NULL, pFilterGraph); + if (ret < 0) { + throw std::runtime_error("Failed to create input filter: \"" + args + "\""); + } +} + +void FilterGraph::add_sink() { + if (media_type == AVMEDIA_TYPE_UNKNOWN) { + throw std::runtime_error("Source buffer is not allocated."); + } + if (buffersink_ctx) { + throw std::runtime_error("Sink buffer is already allocated."); + } + const AVFilter* buffersink = avfilter_get_by_name( + media_type == AVMEDIA_TYPE_AUDIO ? "abuffersink" : "buffersink"); + // Note + // Originally, the code here followed the example + // https://ffmpeg.org/doxygen/4.1/filtering_audio_8c-example.html + // which sets option for `abuffersink`, which caused an issue where the + // `abuffersink` parameters set for the first time survive across multiple + // fitler generations. + // According to the other example + // https://ffmpeg.org/doxygen/4.1/filter_audio_8c-example.html + // `abuffersink` should not take options, and this resolved issue. + int ret = avfilter_graph_create_filter( + &buffersink_ctx, buffersink, "out", nullptr, nullptr, pFilterGraph); + if (ret < 0) { + throw std::runtime_error("Failed to create output filter."); + } +} + +namespace { + +// Encapsulating AVFilterInOut* with handy methods since +// we need to deal with multiple of them at the same time. +class InOuts { + AVFilterInOut* p = nullptr; + // Disable copy constructor/assignment just in case. + InOuts(const InOuts&) = delete; + InOuts& operator=(const InOuts&) = delete; + + public: + InOuts(const char* name, AVFilterContext* pCtx) { + p = avfilter_inout_alloc(); + if (!p) { + throw std::runtime_error("Failed to allocate AVFilterInOut."); + } + p->name = av_strdup(name); + p->filter_ctx = pCtx; + p->pad_idx = 0; + p->next = nullptr; + } + ~InOuts() { + avfilter_inout_free(&p); + } + operator AVFilterInOut**() { + return &p; + } +}; + +} // namespace + +void FilterGraph::add_process() { + // Note + // The official example and other derived codes out there use + // https://ffmpeg.org/doxygen/4.1/filtering_audio_8c-example.html#_a37 + // variable name `in` for "out"/buffersink, and `out` for "in"/buffersrc. + // If you are debugging this part of the code, you might get confused. + InOuts in{"in", buffersrc_ctx}, out{"out", buffersink_ctx}; + + std::string desc = filter_description.empty() + ? (media_type == AVMEDIA_TYPE_AUDIO) ? "anull" : "null" + : filter_description; + + int ret = + avfilter_graph_parse_ptr(pFilterGraph, desc.c_str(), out, in, nullptr); + + if (ret < 0) { + throw std::runtime_error("Failed to create the filter."); + } +} + +void FilterGraph::create_filter() { + if (avfilter_graph_config(pFilterGraph, nullptr) < 0) + throw std::runtime_error("Failed to configure the graph."); + // char* desc = avfilter_graph_dump(pFilterGraph.get(), NULL); + // std::cerr << "Filter created:\n" << desc << std::endl; + // av_free(static_cast(desc)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Streaming process +////////////////////////////////////////////////////////////////////////////// +int FilterGraph::add_frame(AVFrame* pInputFrame) { + return av_buffersrc_add_frame_flags( + buffersrc_ctx, pInputFrame, AV_BUFFERSRC_FLAG_KEEP_REF); +} + +int FilterGraph::get_frame(AVFrame* pOutputFrame) { + return av_buffersink_get_frame(buffersink_ctx, pOutputFrame); +} + +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/filter_graph.h b/torchaudio/csrc/ffmpeg/filter_graph.h new file mode 100644 index 0000000000..d601f63b26 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/filter_graph.h @@ -0,0 +1,52 @@ +#pragma once + +#include +namespace torchaudio { +namespace ffmpeg { + +class FilterGraph { + AVMediaType media_type = AVMEDIA_TYPE_UNKNOWN; + AVFilterGraphPtr pFilterGraph; + // AVFilterContext is freed as a part of AVFilterGraph + // so we do not manage the resource. + AVFilterContext* buffersrc_ctx = nullptr; + AVFilterContext* buffersink_ctx = nullptr; + + public: + const std::string filter_description; + + FilterGraph( + AVRational time_base, + AVCodecParameters* codecpar, + std::string filter_desc); + // Custom destructor to release AVFilterGraph* + ~FilterGraph() = default; + // Non-copyable + FilterGraph(const FilterGraph&) = delete; + FilterGraph& operator=(const FilterGraph&) = delete; + // Movable + FilterGraph(FilterGraph&&) = default; + FilterGraph& operator=(FilterGraph&&) = default; + + ////////////////////////////////////////////////////////////////////////////// + // Configuration methods + ////////////////////////////////////////////////////////////////////////////// + private: + void add_src(AVRational time_base, AVCodecParameters* codecpar); + + void add_sink(); + + void add_process(); + + void create_filter(); + + ////////////////////////////////////////////////////////////////////////////// + // Streaming process + ////////////////////////////////////////////////////////////////////////////// + public: + int add_frame(AVFrame* pInputFrame); + int get_frame(AVFrame* pOutputFrame); +}; + +} // namespace ffmpeg +} // namespace torchaudio From c6de2a194bcfa4d0a50fa2058d195851b8aa1a47 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 23 Dec 2021 17:15:11 -0800 Subject: [PATCH 0055/1144] Add Buffer class (#2044) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add Buffer class that is responsible for converting `AVFrame` to `Tensor`. Note: The API to retrieve the buffered Tensors is tentative. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2043. Pull Request resolved: https://github.com/pytorch/audio/pull/2044 Reviewed By: carolineechen Differential Revision: D32940553 Pulled By: mthrok fbshipit-source-id: 8b8b2222ad7b47edc17e9139420e8a71c00d726a --- torchaudio/csrc/ffmpeg/buffer.cpp | 163 ++++++++++++++++++++++++++++++ torchaudio/csrc/ffmpeg/buffer.h | 24 +++++ 2 files changed, 187 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/buffer.cpp create mode 100644 torchaudio/csrc/ffmpeg/buffer.h diff --git a/torchaudio/csrc/ffmpeg/buffer.cpp b/torchaudio/csrc/ffmpeg/buffer.cpp new file mode 100644 index 0000000000..72fd6c8118 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/buffer.cpp @@ -0,0 +1,163 @@ +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +Buffer::Buffer(AVMediaType type) : media_type(type) {} + +namespace { +torch::Tensor convert_audio_tensor(AVFrame* pFrame) { + // ref: https://ffmpeg.org/doxygen/4.1/filter__audio_8c_source.html#l00215 + AVSampleFormat format = static_cast(pFrame->format); + int num_channels = pFrame->channels; + int bps = av_get_bytes_per_sample(format); + + // Note + // FFMpeg's `nb_samples` represnts the number of samples par channel. + // This corresponds to `num_frames` in torchaudio's notation. + // Also torchaudio uses `num_samples` as the number of samples + // across channels. + int num_frames = pFrame->nb_samples; + + int is_planar = av_sample_fmt_is_planar(format); + int num_planes = is_planar ? num_channels : 1; + int plane_size = bps * num_frames * (is_planar ? 1 : num_channels); + std::vector shape = is_planar + ? std::vector{num_channels, num_frames} + : std::vector{num_frames, num_channels}; + + torch::Tensor t; + uint8_t* ptr = NULL; + switch (format) { + case AV_SAMPLE_FMT_U8: + case AV_SAMPLE_FMT_U8P: { + t = torch::empty(shape, torch::kUInt8); + ptr = t.data_ptr(); + break; + } + case AV_SAMPLE_FMT_S16: + case AV_SAMPLE_FMT_S16P: { + t = torch::empty(shape, torch::kInt16); + ptr = reinterpret_cast(t.data_ptr()); + break; + } + case AV_SAMPLE_FMT_S32: + case AV_SAMPLE_FMT_S32P: { + t = torch::empty(shape, torch::kInt32); + ptr = reinterpret_cast(t.data_ptr()); + break; + } + case AV_SAMPLE_FMT_S64: + case AV_SAMPLE_FMT_S64P: { + t = torch::empty(shape, torch::kInt64); + ptr = reinterpret_cast(t.data_ptr()); + break; + } + case AV_SAMPLE_FMT_FLT: + case AV_SAMPLE_FMT_FLTP: { + t = torch::empty(shape, torch::kFloat32); + ptr = reinterpret_cast(t.data_ptr()); + break; + } + case AV_SAMPLE_FMT_DBL: + case AV_SAMPLE_FMT_DBLP: { + t = torch::empty(shape, torch::kFloat64); + ptr = reinterpret_cast(t.data_ptr()); + break; + } + default: + throw std::runtime_error( + "Unsupported audio format: " + + std::string(av_get_sample_fmt_name(format))); + } + for (int i = 0; i < num_planes; ++i) { + memcpy(ptr, pFrame->extended_data[i], plane_size); + ptr += plane_size; + } + if (is_planar) + t = t.t(); + return t; +} +} // namespace + +void Buffer::push_audio_frame(AVFrame* pFrame) { + chunks.push_back(convert_audio_tensor(pFrame)); +} + +namespace { +torch::Tensor convert_image_tensor(AVFrame* pFrame) { + // ref: + // https://ffmpeg.org/doxygen/4.1/filtering__video_8c_source.html#l00179 + // https://ffmpeg.org/doxygen/4.1/decode__video_8c_source.html#l00038 + AVPixelFormat format = static_cast(pFrame->format); + int width = pFrame->width; + int height = pFrame->height; + uint8_t* buf = pFrame->data[0]; + int linesize = pFrame->linesize[0]; + + int channel; + switch (format) { + case AV_PIX_FMT_RGB24: + case AV_PIX_FMT_BGR24: + channel = 3; + break; + case AV_PIX_FMT_ARGB: + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_ABGR: + case AV_PIX_FMT_BGRA: + channel = 4; + break; + case AV_PIX_FMT_GRAY8: + channel = 1; + break; + default: + throw std::runtime_error( + "Unexpected format: " + std::string(av_get_pix_fmt_name(format))); + } + + torch::Tensor t; + t = torch::empty({1, height, width, channel}, torch::kUInt8); + auto ptr = t.data_ptr(); + int stride = width * channel; + for (int i = 0; i < height; ++i) { + memcpy(ptr, buf, stride); + buf += linesize; + ptr += stride; + } + return t.permute({0, 3, 1, 2}); +} +} // namespace + +void Buffer::push_video_frame(AVFrame* pFrame) { + chunks.push_back(convert_image_tensor(pFrame)); +} + +torch::Tensor Buffer::pop_all() { + if (!chunks.size()) + return torch::empty({}); + + std::vector tmp; + while (chunks.size()) { + tmp.push_back(chunks.front()); + chunks.pop_front(); + } + return torch::cat(tmp, 0); +} + +void Buffer::push_frame(AVFrame* frame) { + switch (media_type) { + case AVMEDIA_TYPE_AUDIO: + push_audio_frame(frame); + break; + case AVMEDIA_TYPE_VIDEO: + push_video_frame(frame); + break; + default: + throw std::runtime_error( + "Unexpected media type. Only audio/video is supported."); + } +} +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/buffer.h b/torchaudio/csrc/ffmpeg/buffer.h new file mode 100644 index 0000000000..8e570c2069 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/buffer.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +class Buffer { + std::deque chunks; + AVMediaType media_type; + + void push_audio_frame(AVFrame* pFrame); + void push_video_frame(AVFrame* pFrame); + + public: + Buffer(AVMediaType type); + + void push_frame(AVFrame* pFrame); + torch::Tensor pop_all(); +}; + +} // namespace ffmpeg +} // namespace torchaudio From 9397cd5b9c4389baa8b3891b3ba12aa2ae07100f Mon Sep 17 00:00:00 2001 From: CodemodService Bot <> Date: Fri, 24 Dec 2021 05:38:50 -0800 Subject: [PATCH 0056/1144] [Codemod][FBSourceBlackLinter] Daily `arc lint --take BLACK` Reviewed By: zertosh Differential Revision: D33307283 fbshipit-source-id: 55a95689b8c20b17b7c882070bc3e24706c44444 --- torchaudio/prototype/ctc_decoder/ctc_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py index f1464c41e9..2a119d15c0 100644 --- a/torchaudio/prototype/ctc_decoder/ctc_decoder.py +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -1,5 +1,6 @@ import itertools as it from collections import namedtuple +from typing import Dict from typing import List, Optional import torch @@ -14,7 +15,6 @@ _create_word_dict, _load_words, ) -from typing import Dict __all__ = ["KenLMLexiconDecoder", "kenlm_lexicon_decoder"] From a4bc8a86f5fc67ffff77bf1239a829c0af8d2bf2 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 28 Dec 2021 08:55:50 -0800 Subject: [PATCH 0057/1144] Show lint diff with color (#2102) Summary: *Before* Screen Shot 2021-12-24 at 12 34 14 PM *After* https://app.circleci.com/pipelines/github/pytorch/audio/8870/workflows/0445f1ac-ad48-412f-8045-2400d0cef4f4/jobs/482060 Screen Shot 2021-12-24 at 12 33 32 PM Pull Request resolved: https://github.com/pytorch/audio/pull/2102 Reviewed By: carolineechen Differential Revision: D33311253 Pulled By: mthrok fbshipit-source-id: 6944921a8be58a2062b66a7dfd2c7ffe8c0866c3 --- .circleci/config.yml | 2 +- .circleci/config.yml.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 468fcbdec6..e466ad2b86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,7 +129,7 @@ jobs: - run: name: Required lint modifications when: always - command: git --no-pager diff + command: git --no-pager diff --color=always download_third_parties_nix: docker: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 4b8aaabcbe..762431e85e 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -129,7 +129,7 @@ jobs: - run: name: Required lint modifications when: always - command: git --no-pager diff + command: git --no-pager diff --color=always download_third_parties_nix: docker: From eb8e8dc84fa6b3e3174bfdc82b19035a624f7c3d Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 28 Dec 2021 09:09:24 -0800 Subject: [PATCH 0058/1144] Add Sphinx gallery automatically (#2101) Summary: This commit updates the documentation configuration so that if an API (function or class) is used in tutorials, then it automatically add the links to the tutorials. It also adds `py:func:` so that it's easy to jump from tutorials to API reference. Note: the use of `py:func:` is not required to be recognized by Shpinx-gallery. * https://482162-90321822-gh.circle-artifacts.com/0/docs/transforms.html#feature-extractions Screen Shot 2021-12-24 at 12 41 43 PM * https://482162-90321822-gh.circle-artifacts.com/0/docs/transforms.html#mvdr Screen Shot 2021-12-24 at 12 42 31 PM Pull Request resolved: https://github.com/pytorch/audio/pull/2101 Reviewed By: hwangjeff Differential Revision: D33311283 Pulled By: mthrok fbshipit-source-id: e0c124d2a761e0f8d81c3d14c4ffc836ffffe288 --- docs/source/conf.py | 21 +++++++++++++++++++ docs/source/pipelines.rst | 8 ------- docs/source/torchaudio.rst | 3 ++- examples/tutorials/audio_datasets_tutorial.py | 3 ++- .../audio_feature_augmentation_tutorial.py | 13 +++++++----- .../audio_feature_extractions_tutorial.py | 11 +++++----- examples/tutorials/audio_io_tutorial.py | 13 ++++++------ .../tutorials/audio_resampling_tutorial.py | 12 +++++++---- examples/tutorials/mvdr_tutorial.py | 3 ++- .../speech_recognition_pipeline_tutorial.py | 2 +- 10 files changed, 57 insertions(+), 32 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ffe8c61c22..fdf37ac144 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -311,3 +311,24 @@ def handle_item(fieldarg, content): TypedField.make_field = patched_make_field + + +# Based off of +# https://github.com/sphinx-gallery/sphinx-gallery/blob/5b21962284f865beeaeb79cca50c8c394fa60cba/sphinx_gallery/directives.py#L66-L70 +def _has_backref(obj): + this_dir = os.path.dirname(__file__) + path = os.path.join(this_dir, "gen_modules", "backreferences", f"{obj}.examples") + return os.path.isfile(path) and os.path.getsize(path) > 0 + + +# Based off of +# https://github.com/pytorch/vision/blob/5335006be7ef01c9f6cb700fe793d7c645e83e84/docs/source/conf.py#L262 +def inject_minigalleries(app, what, name, obj, options, lines): + if what in ("class", "function") and _has_backref(name): + lines.append(f"Tutorials using ``{name.split('.')[-1]}``:") + lines.append(f" .. minigallery:: {name}") + lines.append("\n") + + +def setup(app): + app.connect("autodoc-process-docstring", inject_minigalleries) diff --git a/docs/source/pipelines.rst b/docs/source/pipelines.rst index 31b855d1a4..05628a1178 100644 --- a/docs/source/pipelines.rst +++ b/docs/source/pipelines.rst @@ -85,10 +85,6 @@ Wav2Vec2ASRBundle .. automethod:: get_labels -.. minigallery:: torchaudio.pipelines.WAV2VEC2_ASR_BASE_960H - :add-heading: Examples using ``Wav2Vec2ASRBundle`` - :heading-level: ~ - WAV2VEC2_ASR_BASE_10M ~~~~~~~~~~~~~~~~~~~~~ @@ -231,10 +227,6 @@ Tacotron2TTSBundle .. automethod:: get_vocoder -.. minigallery:: torchaudio.pipelines.Tacotron2TTSBundle - :add-heading: Examples using ``Tacotron2TTSBundle`` - :heading-level: ~ - Tacotron2TTSBundle - TextProcessor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/torchaudio.rst b/docs/source/torchaudio.rst index cb616d01aa..ef565320b4 100644 --- a/docs/source/torchaudio.rst +++ b/docs/source/torchaudio.rst @@ -6,7 +6,8 @@ I/O functionalities Audio I/O functions are implemented in :ref:`torchaudio.backend` module, but for the ease of use, the following functions are made available on :mod:`torchaudio` module. There are different backends available and you can switch backends with :func:`set_audio_backend`. -Refer to :ref:`backend` for the detail. + +Please refer to :ref:`backend` for the detail, and the :doc:`Audio I/O tutorial <../tutorials/audio_io_tutorial>` for the usage. .. function:: torchaudio.info(filepath: str, ...) diff --git a/examples/tutorials/audio_datasets_tutorial.py b/examples/tutorials/audio_datasets_tutorial.py index 263d9bd4c8..4756fad814 100644 --- a/examples/tutorials/audio_datasets_tutorial.py +++ b/examples/tutorials/audio_datasets_tutorial.py @@ -84,7 +84,8 @@ def play_audio(waveform, sample_rate): ###################################################################### -# Here, we show how to use the ``YESNO`` dataset. +# Here, we show how to use the +# :py:func:`torchaudio.datasets.YESNO` dataset. # diff --git a/examples/tutorials/audio_feature_augmentation_tutorial.py b/examples/tutorials/audio_feature_augmentation_tutorial.py index a46f603be6..3961dafbc7 100644 --- a/examples/tutorials/audio_feature_augmentation_tutorial.py +++ b/examples/tutorials/audio_feature_augmentation_tutorial.py @@ -112,11 +112,14 @@ def plot_spectrogram(spec, title=None, ylabel="freq_bin", aspect="auto", xmax=No # `SpecAugment `__ # is a popular spectrogram augmentation technique. # -# ``torchaudio`` implements ``TimeStretch``, ``TimeMasking`` and -# ``FrequencyMasking``. +# ``torchaudio`` implements :py:func:`torchaudio.transforms.TimeStretch`, +# :py:func:`torchaudio.transforms.TimeMasking` and +# :py:func:`torchaudio.transforms.FrequencyMasking`. # + +###################################################################### # TimeStretch -# ~~~~~~~~~~~ +# ----------- # @@ -135,7 +138,7 @@ def plot_spectrogram(spec, title=None, ylabel="freq_bin", aspect="auto", xmax=No ###################################################################### # TimeMasking -# ~~~~~~~~~~~ +# ----------- # torch.random.manual_seed(4) @@ -150,7 +153,7 @@ def plot_spectrogram(spec, title=None, ylabel="freq_bin", aspect="auto", xmax=No ###################################################################### # FrequencyMasking -# ~~~~~~~~~~~~~~~~ +# ---------------- # diff --git a/examples/tutorials/audio_feature_extractions_tutorial.py b/examples/tutorials/audio_feature_extractions_tutorial.py index fb1ae74254..638d729d9f 100644 --- a/examples/tutorials/audio_feature_extractions_tutorial.py +++ b/examples/tutorials/audio_feature_extractions_tutorial.py @@ -212,7 +212,7 @@ def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc): # ----------- # # To get the frequency make-up of an audio signal as it varies with time, -# you can use ``Spectrogram``. +# you can use :py:func:`torchaudio.functional.Spectrogram`. # @@ -274,11 +274,11 @@ def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc): # Mel Filter Bank # --------------- # -# ``torchaudio.functional.melscale_fbanks`` generates the filter bank +# :py:func:`torchaudio.functional.melscale_fbanks` generates the filter bank # for converting frequency bins to mel-scale bins. # # Since this function does not require input audio/features, there is no -# equivalent transform in ``torchaudio.transforms``. +# equivalent transform in :py:func:`torchaudio.transforms`. # @@ -325,7 +325,8 @@ def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc): # -------------- # # Generating a mel-scale spectrogram involves generating a spectrogram -# and performing mel-scale conversion. In ``torchaudio``, ``MelSpectrogram`` provides +# and performing mel-scale conversion. In ``torchaudio``, +# :py:func:`torchaudio.transforms.MelSpectrogram` provides # this functionality. # @@ -456,7 +457,7 @@ def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc): # # Kaldi Pitch feature [1] is a pitch detection mechanism tuned for automatic # speech recognition (ASR) applications. This is a beta feature in ``torchaudio``, -# and it is available only in ``functional``. +# and it is available as :py:func:`torchaudio.functional.compute_kaldi_pitch`. # # 1. A pitch extraction algorithm tuned for automatic speech recognition # diff --git a/examples/tutorials/audio_io_tutorial.py b/examples/tutorials/audio_io_tutorial.py index af4acc0cca..ddc8e06109 100644 --- a/examples/tutorials/audio_io_tutorial.py +++ b/examples/tutorials/audio_io_tutorial.py @@ -178,8 +178,8 @@ def inspect_file(path): # Querying audio metadata # ----------------------- # -# Function ``torchaudio.info`` fetches audio metadata. You can provide -# a path-like object or file-like object. +# Function :py:func:`torchaudio.info` fetches audio metadata. +# You can provide a path-like object or file-like object. # @@ -237,7 +237,7 @@ def inspect_file(path): # Querying file-like object # ~~~~~~~~~~~~~~~~~~~~~~~~~ # -# ``info`` works on file-like objects. +# :py:func:`torchaudio.info` works on file-like objects. # print("Source:", SAMPLE_WAV_URL) @@ -268,7 +268,7 @@ def inspect_file(path): # Loading audio data into Tensor # ------------------------------ # -# To load audio data, you can use ``torchaudio.load``. +# To load audio data, you can use :py:func:`torchaudio.load`. # # This function accepts a path-like object or file-like object as input. # @@ -366,7 +366,7 @@ def inspect_file(path): # -------------------- # # To save audio data in formats interpretable by common applications, -# you can use ``torchaudio.save``. +# you can use :py:func:`torchaudio.save`. # # This function accepts a path-like object or file-like object. # @@ -404,7 +404,8 @@ def inspect_file(path): ###################################################################### -# ``torchaudio.save`` can also handle other formats. To name a few: +# :py:func`torchaudio.save` can also handle other formats. +# To name a few: # waveform, sample_rate = get_sample(resample=8000) diff --git a/examples/tutorials/audio_resampling_tutorial.py b/examples/tutorials/audio_resampling_tutorial.py index 32b54201a8..31db5a7c6f 100644 --- a/examples/tutorials/audio_resampling_tutorial.py +++ b/examples/tutorials/audio_resampling_tutorial.py @@ -196,11 +196,15 @@ def benchmark_resample( ###################################################################### +# Resampling Overview +# ------------------- +# # To resample an audio waveform from one freqeuncy to another, you can use -# ``transforms.Resample`` or ``functional.resample``. -# ``transforms.Resample`` precomputes and caches the kernel used for -# resampling, while ``functional.resample`` computes it on the fly, so -# using ``transforms.Resample`` will result in a speedup when resampling +# :py:func:`torchaudio.transforms.Resample` or +# :py:func:`torchaudio.functional.resample`. +# ``transforms.Resample`` precomputes and caches the kernel used for resampling, +# while ``functional.resample`` computes it on the fly, so using +# ``torchaudio.transforms.Resample`` will result in a speedup when resampling # multiple waveforms using the same parameters (see Benchmarking section). # # Both resampling methods use `bandlimited sinc diff --git a/examples/tutorials/mvdr_tutorial.py b/examples/tutorials/mvdr_tutorial.py index b05d5ec9ff..a1922f574d 100644 --- a/examples/tutorials/mvdr_tutorial.py +++ b/examples/tutorials/mvdr_tutorial.py @@ -10,7 +10,8 @@ # Overview # -------- # -# This is a tutorial on how to apply MVDR beamforming by using `torchaudio `__. +# This is a tutorial on how to apply MVDR beamforming with +# :py:func:`torchaudio.transforms.MVDR`. # # Steps # diff --git a/examples/tutorials/speech_recognition_pipeline_tutorial.py b/examples/tutorials/speech_recognition_pipeline_tutorial.py index 33518ae3c5..f03445ec88 100644 --- a/examples/tutorials/speech_recognition_pipeline_tutorial.py +++ b/examples/tutorials/speech_recognition_pipeline_tutorial.py @@ -26,7 +26,7 @@ # Torchaudio provides easy access to the pre-trained weights and # associated information, such as the expected sample rate and class # labels. They are bundled together and available under -# ``torchaudio.pipelines`` module. +# :py:func:`torchaudio.pipelines` module. # From 7bf04d1e9e366c90918a00003c7efa64ca8b70ae Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 28 Dec 2021 09:20:48 -0800 Subject: [PATCH 0059/1144] Disable matplotlib warning in tutorial rendering (#2107) Summary: *Before:* https://pytorch.org/audio/main/tutorials/audio_data_augmentation_tutorial.html#effects-applied Screen Shot 2021-12-28 at 11 25 08 AM *After:* https://484994-90321822-gh.circle-artifacts.com/0/docs/tutorials/audio_data_augmentation_tutorial.html#effects-applied Screen Shot 2021-12-28 at 11 25 57 AM Pull Request resolved: https://github.com/pytorch/audio/pull/2107 Reviewed By: carolineechen Differential Revision: D33337164 Pulled By: mthrok fbshipit-source-id: 20e3309f0d11d46619f516dc46d967b34f22ec95 --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index fdf37ac144..b6087219b0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,11 +22,15 @@ # sys.path.insert(0, os.path.abspath('.')) import os import re +import warnings import pytorch_sphinx_theme # -- General configuration ------------------------------------------------ +warnings.filterwarnings("ignore", module="matplotlib\..*") + + # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = "1.6" From 37a2555fec692a4bf7e78d7cbf5faaf8e4e443e2 Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Tue, 28 Dec 2021 13:27:32 -0800 Subject: [PATCH 0060/1144] Add HuBERT pretrain model to enable training from scratch (#2064) Summary: - Add three factory functions:`hubert_pretrain_base`, `hubert_pretrain_large`, and `hubert_pretrain_xlarge`, to enable the HuBERT model to train from scratch. - Add `num_classes` argument to `hubert_pretrain_base` factory function because the base model has two iterations of training, the first iteration the `num_cluster` is 100, in the second iteration `num_cluster` is 500. - The model takes `waveforms`, `labels`, and `lengths` as inputs - The model generates the last layer of transformer embedding, `logit_m`, `logit_u` as the outputs. Pull Request resolved: https://github.com/pytorch/audio/pull/2064 Reviewed By: hwangjeff, mthrok Differential Revision: D33338587 Pulled By: nateanl fbshipit-source-id: 534bc17c576c5f344043d8ba098204b8da6e630a --- docs/source/models.rst | 27 ++ torchaudio/models/__init__.py | 10 + torchaudio/models/wav2vec2/__init__.py | 10 + torchaudio/models/wav2vec2/components.py | 326 ++++++++++++- torchaudio/models/wav2vec2/model.py | 556 +++++++++++++++++++++++ 5 files changed, 928 insertions(+), 1 deletion(-) diff --git a/docs/source/models.rst b/docs/source/models.rst index 2d65e5b73d..43b202b2d0 100644 --- a/docs/source/models.rst +++ b/docs/source/models.rst @@ -59,6 +59,13 @@ Wav2Vec2Model .. automethod:: forward +HuBERTPretrainModel +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: HuBERTPretrainModel + + .. automethod:: forward + Factory Functions ----------------- @@ -98,6 +105,26 @@ hubert_xlarge .. autofunction:: hubert_xlarge +hubert_pretrain_model +^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: hubert_pretrain_model + +hubert_pretrain_base +^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: hubert_pretrain_base + +hubert_pretrain_large +^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: hubert_pretrain_large + +hubert_pretrain_xlarge +^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: hubert_pretrain_xlarge + Utility Functions ----------------- diff --git a/torchaudio/models/__init__.py b/torchaudio/models/__init__.py index 78f746e32e..6a592534a8 100644 --- a/torchaudio/models/__init__.py +++ b/torchaudio/models/__init__.py @@ -4,6 +4,7 @@ from .wav2letter import Wav2Letter from .wav2vec2 import ( Wav2Vec2Model, + HuBERTPretrainModel, wav2vec2_model, wav2vec2_base, wav2vec2_large, @@ -11,6 +12,10 @@ hubert_base, hubert_large, hubert_xlarge, + hubert_pretrain_model, + hubert_pretrain_base, + hubert_pretrain_large, + hubert_pretrain_xlarge, ) from .wavernn import WaveRNN @@ -20,6 +25,7 @@ "ConvTasNet", "DeepSpeech", "Wav2Vec2Model", + "HuBERTPretrainModel", "wav2vec2_model", "wav2vec2_base", "wav2vec2_large", @@ -27,5 +33,9 @@ "hubert_base", "hubert_large", "hubert_xlarge", + "hubert_pretrain_model", + "hubert_pretrain_base", + "hubert_pretrain_large", + "hubert_pretrain_xlarge", "Tacotron2", ] diff --git a/torchaudio/models/wav2vec2/__init__.py b/torchaudio/models/wav2vec2/__init__.py index c9f443d092..5c007d4b96 100644 --- a/torchaudio/models/wav2vec2/__init__.py +++ b/torchaudio/models/wav2vec2/__init__.py @@ -1,6 +1,7 @@ from . import utils from .model import ( Wav2Vec2Model, + HuBERTPretrainModel, wav2vec2_model, wav2vec2_base, wav2vec2_large, @@ -8,10 +9,15 @@ hubert_base, hubert_large, hubert_xlarge, + hubert_pretrain_model, + hubert_pretrain_base, + hubert_pretrain_large, + hubert_pretrain_xlarge, ) __all__ = [ "Wav2Vec2Model", + "HuBERTPretrainModel", "wav2vec2_model", "wav2vec2_base", "wav2vec2_large", @@ -19,5 +25,9 @@ "hubert_base", "hubert_large", "hubert_xlarge", + "hubert_pretrain_model", + "hubert_pretrain_base", + "hubert_pretrain_large", + "hubert_pretrain_xlarge", "utils", ] diff --git a/torchaudio/models/wav2vec2/components.py b/torchaudio/models/wav2vec2/components.py index dda95be06d..30066b9114 100644 --- a/torchaudio/models/wav2vec2/components.py +++ b/torchaudio/models/wav2vec2/components.py @@ -3,7 +3,7 @@ import torch from torch import Tensor, nn -from torch.nn import Module +from torch.nn import Module, Parameter _LG = logging.getLogger(__name__) @@ -713,3 +713,327 @@ def _get_encoder( layer_drop=layer_drop, ) return Encoder(feature_projection, transformer) + + +def _compute_mask_indices( + shape: Tuple[int, int], + padding_mask: Optional[Tensor], + mask_prob: float, + mask_length: int, + mask_type: str = "static", + mask_other: float = 0.0, + min_masks: int = 0, + no_overlap: bool = False, + min_space: int = 0, +) -> Tensor: + """Computes random mask spans for a given shape. + Args: + shape (int, int): The shape for which to compute masks. + The first element is batch size and second is the number of frames. + padding_mask (Tensor or None): The padding mask of the same dimension as shape, + which will prevent masking padded elements. + mask_prob (float): Probability for each token to be chosen as start of the span to be masked. + This will be multiplied by number of timesteps divided by length of mask span to mask + approximately this percentage of all elements. However due to overlaps, the actual number + will be smaller (unless no_overlap is True). + mask_type (str): How to compute mask lengths. Options: [``static``, ``uniform``, ``normal``, ``poisson``]. + ``static``: Fixed size + ``uniform``: Sample from uniform distribution [mask_other, mask_length*2] + ``normal``: Sample from normal distribution with mean ``mask_length`` and stdev ``mask_other``. + ``poisson``: Sample from possion distribution with lambda = ``mask_length``. + min_masks (int): Minimum number of masked spans. + no_overlap (bool): If false, will switch to an alternative recursive algorithm + that prevents spans from overlapping. + min_space (int): How many frames to keep unmasked between spans (Only used if no_overlap is True). + + Returns: + (Tensor): The mask indices of dimension `[batch, frame]`. + """ + + batch_size, frame = shape + mask = torch.full((batch_size, frame), False) + # add a random number for probabilistic rounding + all_num_mask = int(mask_prob * frame / float(mask_length) + torch.rand(1)) + + all_num_mask = max(min_masks, all_num_mask) + + mask_idcs = [] + for i in range(batch_size): + if padding_mask is not None: + sz = frame - padding_mask[i].long().sum().item() + # add a random number for probabilistic rounding + num_mask = int(mask_prob * sz / float(mask_length) + torch.rand(1)) + num_mask = max(min_masks, num_mask) + else: + sz = frame + num_mask = all_num_mask + + if mask_type == "static": + lengths = torch.full((num_mask,), mask_length) + elif mask_type == "uniform": + lengths = torch.randint(mask_other, mask_length * 2 + 1, size=(num_mask,)) + elif mask_type == "normal": + lengths = torch.normal(mask_length, mask_other, size=(num_mask,)) + lengths = torch.maximum(torch.ones(1), torch.round(lengths)).int() + elif mask_type == "poisson": + lengths = torch.poisson(mask_length, size=(num_mask,)) + lengths = torch.round(lengths).int() + else: + raise Exception(f"unknown mask selection: {mask_type}") + + if sum(lengths) == 0: + lengths[0] = min(mask_length, sz - 1) + + if no_overlap: + mask_idc = [] + + def arrange(s, e, length, keep_length): + span_start = torch.randint(s, e - length, size=(1,)) + mask_idc.extend(span_start + i for i in range(length)) + + new_parts = [] + if span_start - s - min_space >= keep_length: + new_parts.append((s, span_start - min_space + 1)) + if e - span_start - keep_length - min_space > keep_length: + new_parts.append((span_start + length + min_space, e)) + return new_parts + + parts = [(0, sz)] + min_length = min(lengths) + for length in sorted(lengths, reverse=True): + lens = torch.tensor([e - s for s, e in parts], dtype=torch.int) + lens[lens < length + min_space] = 0 + l_sum = lens.sum() + if l_sum == 0: + break + probs = lens / l_sum + c = torch.distributions.categorical.Categorical(probs).sample() + s, e = parts.pop(c) + parts.extend(arrange(s, e, length, min_length)) + mask_idc = torch.tensor(mask_idc) + else: + min_len = min(lengths) + if sz - min_len <= num_mask: + min_len = sz - num_mask - 1 + + mask_idc = torch.multinomial(torch.ones((sz - min_len,)), num_samples=num_mask, replacement=False) + + mask_idc = torch.tensor( + [mask_idc[j] + offset for j in range(len(mask_idc)) for offset in range(lengths[j])] + ) + + mask_idcs.append(torch.unique(mask_idc[mask_idc < sz])) + + min_len = min([len(m) for m in mask_idcs]) + for i, mask_idc in enumerate(mask_idcs): + if len(mask_idc) > min_len: + mask_idc = torch.index_select( + mask_idc, + 0, + torch.multinomial( + torch.ones((mask_idc.shape[0],)), + num_samples=min_len, + replacement=False, + ), + ) + mask[i, mask_idc] = True + + return mask + + +def _get_padding_mask(input: Tensor, lengths: Tensor) -> Tensor: + """Generate the padding mask given the padded input and the lengths Tensors. + Args: + input (Tensor): The padded Tensor of dimension `[batch, max_len, frequency]`. + lengths (Tensor): The lengths Tensor of dimension `[batch,]`. + + Returns: + (Tensor): The padding mask. + """ + batch_size, max_len, _ = input.shape + mask = torch.arange(max_len, device=lengths.device).expand(batch_size, max_len) >= lengths[:, None] + return mask + + +class MaskGenerator(Module): + """Generate the masks for masked prediction. + Args: + encoder_embed_dim (int): The dimension of the transformer embedding output. + mask_prob (float): Probability for each token to be chosen as start of the span to be masked. + This will be multiplied by number of timesteps divided by length of mask span to mask + approximately this percentage of all elements. However due to overlaps, the actual number + will be smaller (unless no_overlap is True). + mask_selection (str): How to choose the mask length. + Options: [``static``, ``uniform``, ``normal``, ``poisson``]. + mask_other (float): Secondary mask argument (used for more complex distributions). + mask_length (int): The lengths of the mask. + no_mask_overlap (bool): Whether to allow masks to overlap. + mask_min_space (int): Minimum space between spans (if no overlap is enabled). + mask_channel_prob (float): The probability of replacing a feature with 0. + mask_channel_selection (str): How to choose the mask length for channel masking. + Options: [``static``, ``uniform``, ``normal``, ``poisson``]. + mask_channel_other (float): Secondary mask argument for channel masking(used for more complex distributions). + mask_channel_length (int): Minimum space between spans (if no overlap is enabled) for channel masking. + no_mask_channel_overlap (bool): Whether to allow channel masks to overlap. + mask_channel_min_space (int): Minimum space between spans for channel masking(if no overlap is enabled). + """ + + def __init__( + self, + encoder_embed_dim: int, + mask_prob: float, + mask_selection: str, + mask_other: float, + mask_length: int, + no_mask_overlap: bool, + mask_min_space: int, + mask_channel_prob: float, + mask_channel_selection: str, + mask_channel_other: float, + mask_channel_length: int, + no_mask_channel_overlap: bool, + mask_channel_min_space: int, + ): + super().__init__() + self.mask_prob = mask_prob + self.mask_selection = mask_selection + self.mask_other = mask_other + self.mask_length = mask_length + self.no_mask_overlap = no_mask_overlap + self.mask_min_space = mask_min_space + self.mask_channel_prob = mask_channel_prob + self.mask_channel_selection = mask_channel_selection + self.mask_channel_other = mask_channel_other + self.mask_channel_length = mask_channel_length + self.no_mask_channel_overlap = no_mask_channel_overlap + self.mask_channel_min_space = mask_channel_min_space + self.mask_embedding = Parameter(torch.FloatTensor(encoder_embed_dim)) + torch.nn.init.uniform_(self.mask_embedding) + + def forward(self, x: Tensor, padding_mask: Optional[Tensor]) -> Tensor: + """ + Args: + x (Tensor): The encoded representations after feature extraction module. + padding_mask (Tensor or None): The padding mask of the same dimension as shape, + which will prevent masking padded elements. + + Returns: + Tensor: The feature representations after masking. + Tensor: The generated mask indices. + """ + B, T, C = x.shape + if self.mask_prob > 0: + mask_indices = _compute_mask_indices( + (B, T), + padding_mask, + self.mask_prob, + self.mask_length, + self.mask_selection, + self.mask_other, + min_masks=2, + no_overlap=self.no_mask_overlap, + min_space=self.mask_min_space, + ) + mask_indices = mask_indices.to(x.device) + x[mask_indices] = self.mask_embedding + else: + mask_indices = None + + if self.mask_channel_prob > 0: + mask_channel_indices = _compute_mask_indices( + (B, C), + None, + self.mask_channel_prob, + self.mask_channel_length, + self.mask_channel_selection, + self.mask_channel_other, + no_overlap=self.no_mask_channel_overlap, + min_space=self.mask_channel_min_space, + ) + mask_channel_indices = mask_channel_indices.to(x.device).unsqueeze(1).expand(-1, T, -1) + x[mask_channel_indices] = 0 + + return x, mask_indices + + +def _compute_logits( + proj_x: Tensor, + target: Tensor, + label_embeddings: Parameter, +) -> Tensor: + """Compute the logits of the embeddings. + Args: + proj_x (Tensor): The projected masked representations of dimension `[batch, frame, final_dim]`. + target (Tensor): The target Tensor of dimension `[batch, frame, final_dim]`. + label_embeddings (Parameter): The trainable embeddings of target of dimension `[num_class, final_dim]`. + + Returns: + (Tensor): The logits of the inputs. + """ + logit_temp = 0.1 + pos = torch.index_select(label_embeddings, 0, target.long()) + negs = label_embeddings.unsqueeze(1).expand(-1, proj_x.size(0), -1) + neg_is_pos = (pos == negs).all(-1) + pos = pos.unsqueeze(0) + targets = torch.cat([pos, negs], dim=0) + + logits = torch.cosine_similarity(proj_x.float(), targets.float(), dim=-1).type_as(proj_x) + logits /= logit_temp + if neg_is_pos.any(): + logits[1:][neg_is_pos] = float("-inf") + logits = logits.transpose(0, 1) # (num_x, num_cls+1) + return logits + + +class LogitGenerator(Module): + """Generate the logits of masked and unmasked inputs. + Args: + encoder_embed_dim (int): The dimension of the transformer embedding output. + num_classes (int): The number of classes in the labels. + final_dim (int): Project final representations and targets to `final_dim`. + skip_masked (bool): If True, skip computing losses over masked frames. + skip_nomask (bool): If True, skip computing losses over unmasked frames. + """ + + def __init__( + self, + encoder_embed_dim: int, + num_classes: int, + final_dim: int, + skip_masked: bool, + skip_nomask: bool, + ): + super().__init__() + self.label_embeddings = Parameter(torch.FloatTensor(num_classes, final_dim)) + torch.nn.init.uniform_(self.label_embeddings) + self.final_proj = torch.nn.Linear(encoder_embed_dim, final_dim) + self.skip_masked = skip_masked + self.skip_nomask = skip_nomask + + def forward(self, x: Tensor, label: Tensor, mask_m: Tensor, mask_u: Tensor) -> Tuple[Tensor, Tensor]: + """ + Args: + x (Tensor): The feature representation of the last transformer layer. + label (Tensor): The label Tensor of dimension `[batch, frame]`. + mask_m (Tensor): The masked indices of dimension `[batch, frame]`. + mask_u (Tensor): The unmasked indices of dimension `[batch, frame]`. + + Returns: + Tensor: The logits of masked frames. Tensor of dimension `[masked_frame, final_dim]`. + Tensor: The logits of unmasked frames. Tensor of dimension `[unmasked_frame, final_dim]`. + """ + proj_x = self.final_proj(x) + if self.skip_masked: + logit_m = None + else: + proj_x_m = proj_x[mask_m] + label_m = label[mask_m] + logit_m = _compute_logits(proj_x_m, label_m, self.label_embeddings) + + if self.skip_nomask: + logit_u = None + else: + proj_x_u = proj_x[mask_u] + label_u = label[mask_u] + logit_u = _compute_logits(proj_x_u, label_u, self.label_embeddings) + return logit_m, logit_u diff --git a/torchaudio/models/wav2vec2/model.py b/torchaudio/models/wav2vec2/model.py index a4aa74853f..89986584f6 100644 --- a/torchaudio/models/wav2vec2/model.py +++ b/torchaudio/models/wav2vec2/model.py @@ -117,6 +117,89 @@ def forward( return x, lengths +class HuBERTPretrainModel(Module): + """HuBERT pre-train model for training from scratch. + + Note: + To build the model, please use one of the factory functions in + `[hubert_pretrain_base, hubert_pretrain_large, hubert_pretrain_xlarge]`. + + Args: + feature_extractor (torch.nn.Module): + Feature extractor that extracts feature vectors from raw audio Tensor. + + encoder (torch.nn.Module): + Encoder that converts the audio features into the sequence of probability + distribution (in negative log-likelihood) over labels. + + mask_generator (torch.nn.Module): + Mask generator that generates the mask for masked prediction during the training. + + logit_generator (torch.nn.Module): + Logit generator that predicts the logits of the masked and unmasked inputs. + """ + + def __init__( + self, + wav2vec2: Wav2Vec2Model, + mask_generator: Module, + logit_generator: Module, + ): + super().__init__() + self.wav2vec2 = wav2vec2 + self.mask_generator = mask_generator + self.logit_generator = logit_generator + + def forward( + self, + waveforms: Tensor, + labels: Tensor, + audio_lengths: Optional[Tensor] = None, + ) -> Tuple[Tensor, Optional[Tensor]]: + """Compute the sequence of probability distribution over labels. + + Args: + waveforms (Tensor): Audio tensor of dimension `[batch, frames]`. + labels (Tensor): Label for pre-training. A Tensor of dimension `[batch, frames]`. + audio_lengths (Tensor or None, optional): + Indicates the valid length of each audio in the batch. + Shape: `[batch, ]`. + When the ``waveforms`` contains audios with different durations, + by providing ``lengths`` argument, the model will compute + the corresponding valid output lengths and apply proper mask in + transformer attention layer. + If ``None``, it is assumed that all the audio in ``waveforms`` + have valid length. Default: ``None``. + + Returns: + (Tensor, Tensor): + Tensor + The masked sequences of probability distribution (in logit). + Shape: `(masked_frames, num labels)`. + Tensor + The unmasked sequence of probability distribution (in logit). + Shape: `(unmasked_frames, num labels)`. + """ + x, lengths = self.wav2vec2.feature_extractor(waveforms, audio_lengths) + if lengths is not None: + padding_mask = components._get_padding_mask(x, lengths) + else: + padding_mask = None + x, attention_mask = self.wav2vec2.encoder._preprocess(x, lengths) + x, mask = self.mask_generator(x, padding_mask) + x = self.wav2vec2.encoder.transformer(x, attention_mask=attention_mask) + if padding_mask: + mask_m = torch.logical_and(~padding_mask, mask) + mask_u = torch.logical_and(~padding_mask, ~mask_m) + else: + mask_m = mask + mask_u = ~mask_m + + logit_m, logit_u = self.logit_generator(x, labels, mask_m, mask_u) + + return logit_m, logit_u + + def wav2vec2_model( extractor_mode: str, extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], @@ -590,3 +673,476 @@ def hubert_xlarge( encoder_layer_drop=encoder_layer_drop, aux_num_out=aux_num_out, ) + + +def hubert_pretrain_model( + extractor_mode: str, + extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], + extractor_conv_bias: bool, + encoder_embed_dim: int, + encoder_projection_dropout: float, + encoder_pos_conv_kernel: int, + encoder_pos_conv_groups: int, + encoder_num_layers: int, + encoder_num_heads: int, + encoder_attention_dropout: float, + encoder_ff_interm_features: int, + encoder_ff_interm_dropout: float, + encoder_dropout: float, + encoder_layer_norm_first: bool, + encoder_layer_drop: float, + mask_prob: float, + mask_selection: str, + mask_other: float, + mask_length: int, + no_mask_overlap: bool, + mask_min_space: int, + mask_channel_prob: float, + mask_channel_selection: str, + mask_channel_other: float, + mask_channel_length: int, + no_mask_channel_overlap: bool, + mask_channel_min_space: int, + skip_masked: bool, + skip_nomask: bool, + num_classes: int, + final_dim: int, +) -> HuBERTPretrainModel: + # Overriding the signature so that the return type is correct on Sphinx + """hubert_pretrain_model(extractor_mode: str, extractor_conv_layer_config: Optional[List[Tuple[int, int, int]]], extractor_conv_bias: bool, encoder_embed_dim: int, encoder_projection_dropout: float, encoder_pos_conv_kernel: int, encoder_pos_conv_groups: int, encoder_num_layers: int, encoder_num_heads: int, encoder_attention_dropout: float, encoder_ff_interm_features: int, encoder_ff_interm_dropout: float, encoder_dropout: float, encoder_layer_norm_first: bool, encoder_layer_drop: float, mask_prob: float, mask_selection: str, mask_other: float, mask_length: int, no_mask_overlap: bool, mask_min_space: int, mask_channel_prob: float, mask_channel_selection: str, mask_channel_other: float, mask_channel_length: int, no_mask_channel_overlap: bool, mask_channel_min_space: int, skip_masked: bool, skip_nomask: bool, num_classes: int, final_dim: int) -> torchaudio.models.HuBERTPretrainModel + + Build a custom HuBERTPretrainModel for training from scratch + + Note: + The "feature extractor" below corresponds to + `ConvFeatureExtractionModel `__ + in the original ``fairseq`` implementation. + This is referred as "(convolutional) feature encoder" in the *wav2vec 2.0* + [:footcite:`baevski2020wav2vec`] paper. + + The "encoder" below corresponds to `TransformerEncoder `__, + and this is referred as "Transformer" in the paper. + + Args: + extractor_mode (str): Operation mode of feature extractor. + Valid values are ``"group_norm"`` or ``"layer_norm"``. + If ``"group_norm"``, then a single normalization is applied + in the first convolution block. Otherwise, all the convolution + blocks will have layer normalization. + + This option corresponds to ``extractor_mode`` from ``fairseq``. + + extractor_conv_layer_config (list of integer tuples or None): + Configuration of convolution layers in feature extractor. + List of convolution configuration, + i.e. ``[(output_channel, kernel_size, stride), ...]`` + + If ``None`` is provided, then the following default value is used. + + .. code-block:: python + + [ + (512, 10, 5), + (512, 3, 2), + (512, 3, 2), + (512, 3, 2), + (512, 3, 2), + (512, 2, 2), + (512, 2, 2), + ] + + This option corresponds to ``conv_feature_layers`` from ``fairseq``. + + extractor_conv_bias (bool): + Whether to include bias term to each convolution operation. + + This option corresponds to ``conv_bias`` from ``fairseq``. + + encoder_embed_dim (int): + The dimension of embedding in encoder. + + This option corresponds to ``encoder_embed_dim`` from ``fairseq``. + + encoder_projection_dropout (float): + The dropout probability applied after the input feature is projected + to ``encoder_embed_dim``. + + This option corresponds to ``dropout_input`` from ``fairseq``. + + encoder_pos_conv_kernel (int): + The kernel size of convolutional positional embeddings. + + This option corresponds to ``conv_pos`` from ``fairseq``. + + encoder_pos_conv_groups (int): + The number of groups of convolutional positional embeddings. + + This option corresponds to ``conv_pos_groups`` from ``fairseq``. + + encoder_num_layers (int): + The number of self attention layers in transformer block. + + This option corresponds to ``encoder_layers`` from ``fairseq``. + + encoder_num_heads (int): + The number of heads in self attention layers. + + This option corresponds to ``encoder_attention_heads`` from ``fairseq``. + + encoder_attention_dropout (float): + The dropout probability applied after softmax in self-attention layer. + + This option corresponds to ``attention_dropout`` from ``fairseq``. + + encoder_ff_interm_features (int): + The dimension of hidden features in feed forward layer. + + This option corresponds to ``encoder_ffn_embed_dim`` from ``fairseq``. + + encoder_ff_interm_dropout (float): + The dropout probability applied in feedforward layer. + + This option correspinds to ``activation_dropout`` from ``fairseq``. + + encoder_dropout (float): + The dropout probability applied at the end of feed forward layer. + + This option corresponds to ``dropout`` from ``fairseq``. + + encoder_layer_norm_first (bool): + Control the order of layer norm in transformer layer and each encoder layer. + If True, in transformer layer, layer norm is applied before features are fed + to encoder layers. In encoder layer, two layer norms are applied before and after + self attention. + If False, in transformer layer, layer norm is applied after features are fed + to encoder layers. In encoder layer, two layer norms are applied after self + attention, before and after feed forward. + + This option corresponds to ``layer_norm_first`` from ``fairseq``. + + encoder_layer_drop (float): + Probability to drop each encoder layer during training. + + This option corresponds to ``layerdrop`` from ``fairseq``. + + mask_prob (float): + Probability for each token to be chosen as start of the span to be masked. this will be multiplied by + number of timesteps divided by length of mask span to mask approximately this percentage of all elements. + However due to overlaps, the actual number will be smaller (unless no_overlap is True). + + This option corresponds to ``mask_prob`` from ``fairseq``. + + mask_selection (str): + How to choose the mask length. Options: [``static``, ``uniform``, ``normal``, ``poisson``]. + + This option corresponds to ``mask_selection`` from ``fairseq``. + + mask_other (float): + Secondary mask argument (used for more complex distributions). + + This option corresponds to ``mask_other`` from ``fairseq``. + + mask_length (int): + The lengths of the mask. + + This option corresponds to ``mask_length`` from ``fairseq``. + + no_mask_overlap (bool): + Whether to allow masks to overlap. + + This option corresponds to ``no_mask_overlap`` from ``fairseq``. + + mask_min_space (int): + Minimum space between spans (if no overlap is enabled). + + This option corresponds to ``mask_min_space`` from ``fairseq``. + + mask_channel_prob: (float): + The probability of replacing a feature with 0. + + This option corresponds to ``mask_channel_prob`` from ``fairseq``. + + mask_channel_selection (str): + How to choose the mask length for channel masking. Options: [``static``, ``uniform``, ``normal``, ``poisson``]. + + This option corresponds to ``mask_channel_selection`` from ``fairseq``. + + mask_channel_other (float): + Secondary mask argument for channel masking(used for more complex distributions). + + This option corresponds to ``mask_channel_other`` from ``fairseq``. + + mask_channel_length (int): + Minimum space between spans (if no overlap is enabled) for channel masking. + + This option corresponds to ``mask_channel_length`` from ``fairseq``. + + no_mask_channel_overlap (bool): + Whether to allow channel masks to overlap. + + This option corresponds to ``no_mask_channel_overlap`` from ``fairseq``. + + mask_channel_min_space (int): + Minimum space between spans for channel masking(if no overlap is enabled). + + This option corresponds to ``mask_channel_min_space`` from ``fairseq``. + + skip_masked (bool): + If True, skip computing losses over masked frames. + + This option corresponds to ``skip_masked`` from ``fairseq``. + + skip_nomask (bool): + If True, skip computing losses over unmasked frames. + + This option corresponds to ``skip_nomask`` from ``fairseq``. + + num_classes (int): + The number of classes in the labels. + + final_dim (int): + Project final representations and targets to `final_dim`. + + This option corresponds to ``final_dim`` from ``fairseq``. + + Returns: + HuBERTPretrainModel: + The resulting model. + """ # noqa: E501 + if extractor_conv_layer_config is None: + extractor_conv_layer_config = [(512, 10, 5)] + [(512, 3, 2)] * 4 + [(512, 2, 2)] * 2 + + feature_extractor = components._get_feature_extractor( + extractor_mode, extractor_conv_layer_config, extractor_conv_bias + ) + encoder = components._get_encoder( + in_features=extractor_conv_layer_config[-1][0], + embed_dim=encoder_embed_dim, + dropout_input=encoder_projection_dropout, + pos_conv_kernel=encoder_pos_conv_kernel, + pos_conv_groups=encoder_pos_conv_groups, + num_layers=encoder_num_layers, + num_heads=encoder_num_heads, + attention_dropout=encoder_attention_dropout, + ff_interm_features=encoder_ff_interm_features, + ff_interm_dropout=encoder_ff_interm_dropout, + dropout=encoder_dropout, + layer_norm_first=encoder_layer_norm_first, + layer_drop=encoder_layer_drop, + ) + wav2vec2 = Wav2Vec2Model(feature_extractor, encoder) + mask_generator = components.MaskGenerator( + encoder_embed_dim, + mask_prob, + mask_selection, + mask_other, + mask_length, + no_mask_overlap, + mask_min_space, + mask_channel_prob, + mask_channel_selection, + mask_channel_other, + mask_channel_length, + no_mask_channel_overlap, + mask_channel_min_space, + ) + logit_generator = components.LogitGenerator( + encoder_embed_dim, + num_classes, + final_dim, + skip_masked, + skip_nomask, + ) + return HuBERTPretrainModel(wav2vec2=wav2vec2, mask_generator=mask_generator, logit_generator=logit_generator) + + +def hubert_pretrain_base( + encoder_projection_dropout: float = 0.1, + encoder_attention_dropout: float = 0.1, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.1, + encoder_layer_drop: float = 0.05, + num_classes: int = 100, +) -> HuBERTPretrainModel: + # Overriding the signature so that the return type is correct on Sphinx + """hubert_pretrain_base(encoder_projection_dropout: float = 0.1, encoder_attention_dropout: float = 0.1, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.1, encoder_layer_drop: float = 0.05, num_classes: int = 100) -> torchaudio.models.HuBERTPretrainModel + + Build HuBERTPretrainModel model with "base" architecture from *HuBERT* [:footcite:`hsu2021hubert`] + + Args: + encoder_projection_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_attention_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_ff_interm_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_layer_drop (float): + See :py:func:`hubert_pretrain_model`. + num_classes (int, optional): + See :py:func:`hubert_pretrain_model`. + + Returns: + HuBERTPretrainModel: + The resulting model. + """ # noqa: E501 + return hubert_pretrain_model( + extractor_mode="group_norm", + extractor_conv_layer_config=None, + extractor_conv_bias=False, + encoder_embed_dim=768, + encoder_projection_dropout=encoder_projection_dropout, + encoder_pos_conv_kernel=128, + encoder_pos_conv_groups=16, + encoder_num_layers=12, + encoder_num_heads=12, + encoder_attention_dropout=encoder_attention_dropout, + encoder_ff_interm_features=3072, + encoder_ff_interm_dropout=encoder_ff_interm_dropout, + encoder_dropout=encoder_dropout, + encoder_layer_norm_first=False, + encoder_layer_drop=encoder_layer_drop, + mask_prob=0.80, + mask_selection="static", + mask_other=0.0, + mask_length=10, + no_mask_overlap=False, + mask_min_space=1, + mask_channel_prob=0.0, + mask_channel_selection="static", + mask_channel_other=0.0, + mask_channel_length=10, + no_mask_channel_overlap=False, + mask_channel_min_space=1, + skip_masked=False, + skip_nomask=False, + num_classes=num_classes, + final_dim=256, + ) + + +def hubert_pretrain_large( + encoder_projection_dropout: float = 0.0, + encoder_attention_dropout: float = 0.0, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.0, + encoder_layer_drop: float = 0.0, +) -> HuBERTPretrainModel: + # Overriding the signature so that the return type is correct on Sphinx + """hubert_pretrain_large(encoder_projection_dropout: float = 0.0, encoder_attention_dropout: float = 0.0, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.0, encoder_layer_drop: float = 0.0) -> torchaudio.models.HuBERTPretrainModel + + Build HuBERTPretrainModel model for pre-training with "large" architecture from *HuBERT* [:footcite:`hsu2021hubert`] + + Args: + encoder_projection_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_attention_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_ff_interm_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_layer_drop (float): + See :py:func:`hubert_pretrain_model`. + + Returns: + HuBERTPretrainModel: + The resulting model. + """ # noqa: E501 + return hubert_pretrain_model( + extractor_mode="layer_norm", + extractor_conv_layer_config=None, + extractor_conv_bias=False, + encoder_embed_dim=1024, + encoder_projection_dropout=encoder_projection_dropout, + encoder_pos_conv_kernel=128, + encoder_pos_conv_groups=16, + encoder_num_layers=24, + encoder_num_heads=16, + encoder_attention_dropout=encoder_attention_dropout, + encoder_ff_interm_features=4096, + encoder_ff_interm_dropout=encoder_ff_interm_dropout, + encoder_dropout=encoder_dropout, + encoder_layer_norm_first=True, + encoder_layer_drop=encoder_layer_drop, + mask_prob=0.80, + mask_selection="static", + mask_other=0.0, + mask_length=10, + no_mask_overlap=False, + mask_min_space=1, + mask_channel_prob=0.0, + mask_channel_selection="static", + mask_channel_other=0.0, + mask_channel_length=10, + no_mask_channel_overlap=False, + mask_channel_min_space=1, + skip_masked=False, + skip_nomask=False, + num_classes=500, + final_dim=768, + ) + + +def hubert_pretrain_xlarge( + encoder_projection_dropout: float = 0.0, + encoder_attention_dropout: float = 0.0, + encoder_ff_interm_dropout: float = 0.0, + encoder_dropout: float = 0.0, + encoder_layer_drop: float = 0.0, +) -> HuBERTPretrainModel: + # Overriding the signature so that the return type is correct on Sphinx + """hubert_pretrain_xlarge(encoder_projection_dropout: float = 0.0, encoder_attention_dropout: float = 0.0, encoder_ff_interm_dropout: float = 0.0, encoder_dropout: float = 0.0, encoder_layer_drop: float = 0.0) -> torchaudio.models.HuBERTPretrainModel + + Build HuBERTPretrainModel model for pre-training with "extra large" architecture from *HuBERT* [:footcite:`hsu2021hubert`] + + Args: + encoder_projection_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_attention_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_ff_interm_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_dropout (float): + See :py:func:`hubert_pretrain_model`. + encoder_layer_drop (float): + See :py:func:`hubert_pretrain_model`. + + Returns: + HuBERTPretrainModel: + The resulting model. + """ # noqa: E501 + return hubert_pretrain_model( + extractor_mode="layer_norm", + extractor_conv_layer_config=None, + extractor_conv_bias=False, + encoder_embed_dim=1280, + encoder_projection_dropout=encoder_projection_dropout, + encoder_pos_conv_kernel=128, + encoder_pos_conv_groups=16, + encoder_num_layers=48, + encoder_num_heads=16, + encoder_attention_dropout=encoder_attention_dropout, + encoder_ff_interm_features=5120, + encoder_ff_interm_dropout=encoder_ff_interm_dropout, + encoder_dropout=encoder_dropout, + encoder_layer_norm_first=True, + encoder_layer_drop=encoder_layer_drop, + mask_prob=0.80, + mask_selection="static", + mask_other=0.0, + mask_length=10, + no_mask_overlap=False, + mask_min_space=1, + mask_channel_prob=0.0, + mask_channel_selection="static", + mask_channel_other=0.0, + mask_channel_length=10, + no_mask_channel_overlap=False, + mask_channel_min_space=1, + skip_masked=False, + skip_nomask=False, + num_classes=500, + final_dim=1024, + ) From 133d00654cd52a43158e235c2003ed7f3322d70d Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Tue, 28 Dec 2021 14:00:50 -0800 Subject: [PATCH 0061/1144] Add ASR CTC inference tutorial (#2106) Summary: demonstrate usage of the CTC beam search decoder w/ lexicon constraint and KenLM support, on a LibriSpeech sample and using a pretrained wav2vec2 model rendered: https://485200-90321822-gh.circle-artifacts.com/0/docs/tutorials/asr_inference_with_ctc_decoder_tutorial.html follow-ups: - incorporate `nbest` - demonstrate customizability of different beam search parameters Pull Request resolved: https://github.com/pytorch/audio/pull/2106 Reviewed By: mthrok Differential Revision: D33340946 Pulled By: carolineechen fbshipit-source-id: 0ab838375d96a035d54ed5b5bd9ab4dc8d19adb7 --- docs/source/index.rst | 1 + ...asr_inference_with_ctc_decoder_tutorial.py | 250 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py diff --git a/docs/source/index.rst b/docs/source/index.rst index d0015f3c69..58c54a1501 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -75,6 +75,7 @@ Advanced Usages tutorials/forced_alignment_tutorial tutorials/tacotron2_pipeline_tutorial tutorials/mvdr_tutorial + tutorials/asr_inference_with_ctc_decoder_tutorial Citing torchaudio ----------------- diff --git a/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py new file mode 100644 index 0000000000..60836c3d85 --- /dev/null +++ b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py @@ -0,0 +1,250 @@ +""" +ASR Inference with CTC Decoder +============================== + +**Author**: `Caroline Chen `__ + +This tutorial shows how to perform speech recognition inference using a +CTC beam search decoder with lexicon constraint and KenLM language model +support. We demonstrate this on a pretrained wav2vec 2.0 model trained +using CTC loss. + +""" + + +###################################################################### +# Overview +# -------- +# +# Running ASR inference using a CTC Beam Search decoder with a KenLM +# language model and lexicon constraint requires the following components +# +# - Acoustic Model: model predicting phonetics from audio waveforms +# - Tokens: the possible predicted tokens from the acoustic model +# - Lexicon: mapping between possible words and their corresponding +# tokens sequence +# - KenLM: n-gram language model trained with the `KenLM +# library `__ +# + + +###################################################################### +# Preparation +# ----------- +# +# First we import the necessary utilities and fetch the data that we are +# working with +# + +import os + +import IPython +import torch +import torchaudio + + +###################################################################### +# Acoustic Model and Data +# ~~~~~~~~~~~~~~~~~~~~~~~ +# +# We use the pretrained `Wav2Vec 2.0 `__ +# Base model that is finetuned on 10 min of the `LibriSpeech +# dataset `__, which can be loaded in using +# ``torchaudio.pipelines``. For more detail on running Wav2Vec 2.0 speech +# recognition pipelines in torchaudio, please refer to `this +# tutorial `__. +# + +bundle = torchaudio.pipelines.WAV2VEC2_ASR_BASE_10M +acoustic_model = bundle.get_model() + + +###################################################################### +# We will load a sample from the LibriSpeech test-other dataset. +# + +hub_dir = torch.hub.get_dir() + +speech_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/8461-258277-0000.wav" +speech_file = f"{hub_dir}/speech.wav" + +torch.hub.download_url_to_file(speech_url, speech_file) + +IPython.display.Audio(speech_file) + + +###################################################################### +# The transcript corresponding to this audio file is +# ``"when it was the seven hundred and eighteenth night"`` +# + +waveform, sample_rate = torchaudio.load(speech_file) + +if sample_rate != bundle.sample_rate: + waveform = torchaudio.functional.resample(waveform, sample_rate, bundle.sample_rate) + + +###################################################################### +# Files for Decoder +# ~~~~~~~~~~~~~~~~~ +# +# Next, we load in our token, lexicon, and KenLM data, which are used by +# the decoder to predict words from the acoustic model output. +# +# Note: this cell may take a couple of minutes to run, as the language +# model can be large +# + + +###################################################################### +# Tokens +# ^^^^^^ +# +# The tokens are the possible symbols that the acoustic model can predict, +# including the blank and silent symbols. +# +# :: +# +# # tokens.txt +# _ +# | +# e +# t +# ... +# + +token_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/tokens-w2v2.txt" +token_file = f"{hub_dir}/token.txt" +torch.hub.download_url_to_file(token_url, token_file) + + +###################################################################### +# Lexicon +# ^^^^^^^ +# +# The lexicon is a mapping from words to their corresponding tokens +# sequence, and is used to restrict the search space of the decoder to +# only words from the lexicon. The expected format of the lexicon file is +# a line per word, with a word followed by its space-split tokens. +# +# :: +# +# # lexcion.txt +# a a | +# able a b l e | +# about a b o u t | +# ... +# ... +# + +lexicon_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/lexicon-librispeech.txt" +lexicon_file = f"{hub_dir}/lexicon.txt" +torch.hub.download_url_to_file(lexicon_url, lexicon_file) + + +###################################################################### +# KenLM +# ^^^^^ +# +# This is an n-gram language model trained with the `KenLM +# library `__. Both the ``.arpa`` or +# the binarized ``.bin`` LM can be used, but the binary format is +# recommended for faster loading. +# + +kenlm_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/4-gram-librispeech.bin" +kenlm_file = f"{hub_dir}/kenlm.bin" +torch.hub.download_url_to_file(kenlm_url, kenlm_file) + + +###################################################################### +# Construct Beam Search Decoder +# ----------------------------- +# +# The decoder can be constructed using the ``kenlm_lexicon_decoder`` +# factory function from ``torchaudio.prototype.ctc_decoder``. In addition +# to the previously mentioned components, it also takes in various beam +# search decoding parameters and token/word parameters. The full list of +# parameters can be found +# `here `__. +# + +from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder + +beam_search_decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=token_file, + kenlm=kenlm_file, + nbest=1, + beam_size=1500, + beam_size_token=50, + lm_weight=3.23, + word_score=-1.39, + unk_score=float("-inf"), + sil_score=0, +) + + +###################################################################### +# Greedy Decoder +# -------------- +# +# For comparison against the beam search decoder, we also construct a +# basic greedy decoder.\ **bold text** +# + + +class GreedyCTCDecoder(torch.nn.Module): + def __init__(self, labels, blank=0): + super().__init__() + self.labels = labels + self.blank = blank + + def forward(self, emission: torch.Tensor) -> str: + """Given a sequence emission over labels, get the best path string + Args: + emission (Tensor): Logit tensors. Shape `[num_seq, num_label]`. + + Returns: + str: The resulting transcript + """ + indices = torch.argmax(emission, dim=-1) # [num_seq,] + indices = torch.unique_consecutive(indices, dim=-1) + indices = [i for i in indices if i != self.blank] + return "".join([self.labels[i] for i in indices]) + + +greedy_decoder = GreedyCTCDecoder(labels=bundle.get_labels()) + + +###################################################################### +# Run Inference +# ------------- +# +# Now that we have the data, acoustic model, and decoder, we can perform +# inference. Recall the transcript corresponding to the waveform is +# ``"when it was the seven hundred and eighteenth night"`` +# + +emission, _ = acoustic_model(waveform) + +###################################################################### +# Using the beam search decoder: + +beam_search_result = beam_search_decoder(emission) +beam_search_transcript = " ".join(beam_search_result[0][0].words).lower().strip() +print(beam_search_transcript) + +###################################################################### +# Using the greedy decoder: + +greedy_result = greedy_decoder(emission[0]) +greedy_transcript = greedy_result.replace("|", " ").lower().strip() +print(greedy_transcript) + + +###################################################################### +# We see that the transcript with the lexicon-constrained beam search +# decoder consists of real words, while the greedy decoder can predict +# incorrectly spelled words like “hundrad”. +# From 340ec8916a9257e1efd741a266c64b05c55e52c1 Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Tue, 28 Dec 2021 14:11:20 -0800 Subject: [PATCH 0062/1144] Remove the MVDR tutorial in examples (#2109) Summary: Remove it as it's already introduced in the [gallery](https://github.com/pytorch/audio/blob/main/examples/tutorials/mvdr_tutorial.py). Pull Request resolved: https://github.com/pytorch/audio/pull/2109 Reviewed By: carolineechen Differential Revision: D33341574 Pulled By: nateanl fbshipit-source-id: e5c1c8537063b9453947dc3ecafa70e9b6c74146 --- examples/beamforming/MVDR_tutorial.ipynb | 578 ----------------------- 1 file changed, 578 deletions(-) delete mode 100644 examples/beamforming/MVDR_tutorial.ipynb diff --git a/examples/beamforming/MVDR_tutorial.ipynb b/examples/beamforming/MVDR_tutorial.ipynb deleted file mode 100644 index 1cd69ee1f8..0000000000 --- a/examples/beamforming/MVDR_tutorial.ipynb +++ /dev/null @@ -1,578 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 2, - "metadata": { - "colab": { - "name": "Copy of Copy of torchaudio_MVDR_tutorial.ipynb", - "provenance": [], - "collapsed_sections": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3.9.6 64-bit ('dev': conda)" - }, - "language_info": { - "name": "python", - "version": "3.9.6", - "mimetype": "text/x-python", - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "pygments_lexer": "ipython3", - "nbconvert_exporter": "python", - "file_extension": ".py" - }, - "interpreter": { - "hash": "6a702c257b9a40163843ba760790c17a6ddd2abeef8febce55475eea4b92c28c" - } - }, - "cells": [ - { - "cell_type": "markdown", - "source": [ - "\"Open" - ], - "metadata": { - "id": "xheYDPUcYGbp" - } - }, - { - "cell_type": "markdown", - "source": [ - "This is a tutorial on how to apply MVDR beamforming by using [torchaudio](https://github.com/pytorch/audio)\n", - "-----------\n", - "\n", - "The multi-channel audio example is selected from [ConferencingSpeech](https://github.com/ConferencingSpeech/ConferencingSpeech2021) dataset. \n", - "\n", - "```\n", - "original filename: SSB07200001\\#noise-sound-bible-0038\\#7.86_6.16_3.00_3.14_4.84_134.5285_191.7899_0.4735\\#15217\\#25.16333303751458\\#0.2101221178590021.wav\n", - "```\n", - "\n", - "Note:\n", - "- You need to use the nightly torchaudio in order to use the MVDR and InverseSpectrogram modules.\n", - "\n", - "\n", - "Steps\n", - "\n", - "- Ideal Ratio Mask (IRM) is generated by dividing the clean/noise magnitude by the mixture magnitude.\n", - "- We test all three solutions (``ref_channel``, ``stv_evd``, ``stv_power``) of torchaudio's MVDR module.\n", - "- We test the single-channel and multi-channel masks for MVDR beamforming. The multi-channel mask is averaged along channel dimension when computing the covariance matrices of speech and noise, respectively." - ], - "metadata": { - "id": "L6R0MXe5Wr19" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "!pip install --pre torchaudio -f https://download.pytorch.org/whl/nightly/torch_nightly.html --force" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "juO6PE9XLctD", - "outputId": "8777ba14-da99-4c18-d80f-b070ad9861af" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "import torch\n", - "import torchaudio\n", - "import IPython.display as ipd" - ], - "outputs": [], - "metadata": { - "id": "T4u4unhFMMBG" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Load audios of mixture, reverberated clean speech, and dry clean speech." - ], - "metadata": { - "id": "bDILVXkeg2s3" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "!curl -LJO https://github.com/nateanl/torchaudio_mvdr_tutorial/raw/main/wavs/mix.wav\n", - "!curl -LJO https://github.com/nateanl/torchaudio_mvdr_tutorial/raw/main/wavs/reverb_clean.wav\n", - "!curl -LJO https://github.com/nateanl/torchaudio_mvdr_tutorial/raw/main/wavs/clean.wav" - ], - "outputs": [], - "metadata": { - "id": "2XIyMa_VKv0c", - "colab": { - "base_uri": "https://localhost:8080/" - }, - "outputId": "404f46a6-e70c-4f80-af8d-d356408a9f18" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "mix, sr = torchaudio.load('mix.wav')\n", - "reverb_clean, sr2 = torchaudio.load('reverb_clean.wav')\n", - "clean, sr3 = torchaudio.load('clean.wav')\n", - "assert sr == sr2\n", - "noise = mix - reverb_clean" - ], - "outputs": [], - "metadata": { - "id": "iErB6UhQPtD3" - } - }, - { - "cell_type": "markdown", - "source": [ - "## Note: The MVDR Module requires ``torch.cdouble`` dtype for noisy STFT. We need to convert the dtype of the waveforms to ``torch.double``" - ], - "metadata": { - "id": "Aq-x_fo5VkwL" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "mix = mix.to(torch.double)\n", - "noise = noise.to(torch.double)\n", - "clean = clean.to(torch.double)\n", - "reverb_clean = reverb_clean.to(torch.double)" - ], - "outputs": [], - "metadata": { - "id": "5c66pHcQV0P9" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Initilize the Spectrogram and InverseSpectrogram modules" - ], - "metadata": { - "id": "05D26we0V4P-" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "stft = torchaudio.transforms.Spectrogram(n_fft=1024, hop_length=256, return_complex=True, power=None)\n", - "istft = torchaudio.transforms.InverseSpectrogram(n_fft=1024, hop_length=256)" - ], - "outputs": [], - "metadata": { - "id": "NcGhD7_TUKd1" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Compute the complex-valued STFT of mixture, clean speech, and noise" - ], - "metadata": { - "id": "-dlJcuSNUCgA" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "spec_mix = stft(mix)\n", - "spec_clean = stft(clean)\n", - "spec_reverb_clean = stft(reverb_clean)\n", - "spec_noise = stft(noise)" - ], - "outputs": [], - "metadata": { - "id": "w1vO7w1BUKt4" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Generate the Ideal Ratio Mask (IRM)\n", - "Note: we found using the mask directly peforms better than using the square root of it. This is slightly different from the definition of IRM." - ], - "metadata": { - "id": "8SBchrDhURK1" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "def get_irms(spec_clean, spec_noise, spec_mix):\n", - " mag_mix = spec_mix.abs() ** 2\n", - " mag_clean = spec_clean.abs() ** 2\n", - " mag_noise = spec_noise.abs() ** 2\n", - " irm_speech = mag_clean / (mag_clean + mag_noise)\n", - " irm_noise = mag_noise / (mag_clean + mag_noise)\n", - "\n", - " return irm_speech, irm_noise" - ], - "outputs": [], - "metadata": { - "id": "2gB63BoWUmHZ" - } - }, - { - "cell_type": "markdown", - "source": [ - "## Note: We use reverberant clean speech as the target here, you can also set it to dry clean speech" - ], - "metadata": { - "id": "reGMDyNCaE7L" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "irm_speech, irm_noise = get_irms(spec_reverb_clean, spec_noise, spec_mix)" - ], - "outputs": [], - "metadata": { - "id": "HSTCGy_5Uqzx" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Apply MVDR beamforming by using multi-channel masks" - ], - "metadata": { - "id": "1R5I_TmSUbS0" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "results_multi = {}\n", - "for solution in ['ref_channel', 'stv_evd', 'stv_power']:\n", - " mvdr = torchaudio.transforms.MVDR(ref_channel=0, solution=solution, multi_mask=True)\n", - " stft_est = mvdr(spec_mix, irm_speech, irm_noise)\n", - " est = istft(stft_est, length=mix.shape[-1])\n", - " results_multi[solution] = est" - ], - "outputs": [], - "metadata": { - "id": "SiWFZgCbadz7" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Apply MVDR beamforming by using single-channel masks \n", - "(We use the 1st channel as an example. The channel selection may depend on the design of the microphone array)" - ], - "metadata": { - "id": "Ukez6_lcUfna" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "results_single = {}\n", - "for solution in ['ref_channel', 'stv_evd', 'stv_power']:\n", - " mvdr = torchaudio.transforms.MVDR(ref_channel=0, solution=solution, multi_mask=False)\n", - " stft_est = mvdr(spec_mix, irm_speech[0], irm_noise[0])\n", - " est = istft(stft_est, length=mix.shape[-1])\n", - " results_single[solution] = est" - ], - "outputs": [], - "metadata": { - "id": "kLeNKsk-VLm5" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Compute Si-SDR scores" - ], - "metadata": { - "id": "uJjJNdYiUnf0" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "def si_sdr(estimate, reference, epsilon=1e-8):\n", - " estimate = estimate - estimate.mean()\n", - " reference = reference - reference.mean()\n", - " reference_pow = reference.pow(2).mean(axis=1, keepdim=True)\n", - " mix_pow = (estimate * reference).mean(axis=1, keepdim=True)\n", - " scale = mix_pow / (reference_pow + epsilon)\n", - "\n", - " reference = scale * reference\n", - " error = estimate - reference\n", - "\n", - " reference_pow = reference.pow(2)\n", - " error_pow = error.pow(2)\n", - "\n", - " reference_pow = reference_pow.mean(axis=1)\n", - " error_pow = error_pow.mean(axis=1)\n", - "\n", - " sisdr = 10 * torch.log10(reference_pow) - 10 * torch.log10(error_pow)\n", - " return sisdr.item()" - ], - "outputs": [], - "metadata": { - "id": "MgmAJcyiU-FU" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Single-channel mask results" - ], - "metadata": { - "id": "3TCJEwTOUxci" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "for solution in results_single:\n", - " print(solution+\": \", si_sdr(results_single[solution][None,...], reverb_clean[0:1]))" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "NrUXXj98VVY7", - "outputId": "bc113347-70e3-47a9-8479-8aeeeca80abf" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Multi-channel mask results" - ], - "metadata": { - "id": "-7AnjM-gU3c8" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "for solution in results_multi:\n", - " print(solution+\": \", si_sdr(results_multi[solution][None,...], reverb_clean[0:1]))" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "S_VINTnlXobM", - "outputId": "234b5615-63e7-44d8-f816-a6cc05999e52" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Display the mixture audio" - ], - "metadata": { - "id": "_vOK8vgmU_UP" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"Mixture speech\")\n", - "ipd.Audio(mix[0], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "QaKauQIHYctE", - "outputId": "674c7f9b-62a3-4298-81ac-d3ab1ee43cd7" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Display the noise" - ], - "metadata": { - "id": "R-QGGm87VFQI" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"Noise\")\n", - "ipd.Audio(noise[0], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "l1WgzxIZYhlk", - "outputId": "7b100679-b4a0-47ff-b30b-9f4cb9dca3d1" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Display the clean speech" - ], - "metadata": { - "id": "P3kB-jzpVKKu" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"Clean speech\")\n", - "ipd.Audio(clean[0], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "pwAWvlRAVJkT", - "outputId": "5e173a1b-2ba8-4797-8f3a-e41cbf05ac2b" - } - }, - { - "cell_type": "markdown", - "source": [ - "### Display the enhanced audios¶" - ], - "metadata": { - "id": "RIlyzL1wVTnr" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"multi-channel mask, ref_channel solution\")\n", - "ipd.Audio(results_multi['ref_channel'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "M3YQsledVIQ5", - "outputId": "43d9ee34-6933-401b-baf9-e4cdb7d79b63" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"multi-channel mask, stv_evd solution\")\n", - "ipd.Audio(results_multi['stv_evd'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "UhYOHLvCVWBN", - "outputId": "761468ec-ebf9-4b31-ad71-bfa2e15fed37" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"multi-channel mask, stv_power solution\")\n", - "ipd.Audio(results_multi['stv_power'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "9dv8VDtCVXzd", - "outputId": "1ae61ea3-d3c4-479f-faad-7439f942aac1" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"single-channel mask, ref_channel solution\")\n", - "ipd.Audio(results_single['ref_channel'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "jCFUN890VZdh", - "outputId": "c0d2a928-5dd0-4584-b277-7838ac4a9e6b" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"single-channel mask, stv_evd solution\")\n", - "ipd.Audio(results_single['stv_evd'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "hzlzagsKVbAv", - "outputId": "96af9e37-82ca-4544-9c08-421fe222bde4" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "print(\"single-channel mask, stv_power solution\")\n", - "ipd.Audio(results_single['stv_power'], rate=16000)" - ], - "outputs": [], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "A4igQpTnVctG", - "outputId": "cf968089-9274-4c1c-a1a5-32b220de0bf9" - } - } - ] -} \ No newline at end of file From 72a98a86c6bc17573cbe61a26061689b3830e5af Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Tue, 28 Dec 2021 16:18:28 -0800 Subject: [PATCH 0063/1144] Add pretrained Emformer RNN-T streaming ASR inference pipeline (#2093) Summary: Adds pretrained Emformer RNN-T inference pipeline that's capable of performing streaming and non-streaming ASR. Includes demo script that uses pipeline to alternately perform streaming and non-streaming ASR on LibriSpeech test samples (video below). https://user-images.githubusercontent.com/8345689/147590753-d5126557-d575-4551-8dfe-5977276cb4ad.mov Pull Request resolved: https://github.com/pytorch/audio/pull/2093 Reviewed By: mthrok Differential Revision: D33340776 Pulled By: hwangjeff fbshipit-source-id: fbb3b1d471b4e9f1b93fa9dea9c464154537a8ac --- docs/source/prototype.rst | 28 ++ .../pipeline_demo.py | 54 +++ torchaudio/prototype/__init__.py | 3 + torchaudio/prototype/rnnt_pipeline.py | 386 ++++++++++++++++++ 4 files changed, 471 insertions(+) create mode 100644 examples/asr/librispeech_emformer_rnnt/pipeline_demo.py create mode 100644 torchaudio/prototype/rnnt_pipeline.py diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index 6a26981e90..afbde5a3cf 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -73,6 +73,34 @@ Hypothesis .. autoclass:: Hypothesis +RNNTBundle +~~~~~~~~~~ + +.. autoclass:: RNNTBundle + :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length + + .. automethod:: get_decoder + + .. automethod:: get_feature_extractor + + .. automethod:: get_streaming_feature_extractor + + .. automethod:: get_token_processor + + .. autoclass:: torchaudio.prototype::RNNTBundle.FeatureExtractor + :special-members: __call__ + + .. autoclass:: torchaudio.prototype::RNNTBundle.TokenProcessor + :special-members: __call__ + + +EMFORMER_RNNT_BASE_LIBRISPEECH +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH + :no-value: + + KenLMLexiconDecoder ~~~~~~~~~~~~~~~~~~~ diff --git a/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py new file mode 100644 index 0000000000..dd65c7936d --- /dev/null +++ b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py @@ -0,0 +1,54 @@ +from argparse import ArgumentParser +import pathlib +import torch +import torchaudio +from torchaudio.prototype.rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH + + +def cli_main(): + parser = ArgumentParser() + parser.add_argument( + "--librispeech_path", type=pathlib.Path, required=True, help="Path to LibriSpeech datasets.", + ) + args = parser.parse_args() + + dataset = torchaudio.datasets.LIBRISPEECH(args.librispeech_path, url="test-clean") + decoder = EMFORMER_RNNT_BASE_LIBRISPEECH.get_decoder() + token_processor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_token_processor() + feature_extractor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_feature_extractor() + streaming_feature_extractor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_streaming_feature_extractor() + + hop_length = EMFORMER_RNNT_BASE_LIBRISPEECH.hop_length + num_samples_segment = EMFORMER_RNNT_BASE_LIBRISPEECH.segment_length * hop_length + num_samples_segment_right_context = ( + num_samples_segment + EMFORMER_RNNT_BASE_LIBRISPEECH.right_context_length * hop_length + ) + + for idx in range(10): + sample = dataset[idx] + waveform = sample[0].squeeze() + + # Streaming decode. + state, hypothesis = None, None + for idx in range(0, len(waveform), num_samples_segment): + segment = waveform[idx: idx + num_samples_segment_right_context] + segment = torch.nn.functional.pad(segment, (0, num_samples_segment_right_context - len(segment))) + with torch.no_grad(): + features, length = streaming_feature_extractor(segment) + hypos, state = decoder.infer(features, length, 10, state=state, hypothesis=hypothesis) + hypothesis = hypos[0] + transcript = token_processor(hypothesis.tokens) + if transcript: + print(transcript, end=" ", flush=True) + print() + + # Non-streaming decode. + with torch.no_grad(): + features, length = feature_extractor(waveform) + hypos = decoder(features, length, 10) + print(token_processor(hypos[0].tokens)) + print() + + +if __name__ == "__main__": + cli_main() diff --git a/torchaudio/prototype/__init__.py b/torchaudio/prototype/__init__.py index 872e31d291..8856fe46fd 100644 --- a/torchaudio/prototype/__init__.py +++ b/torchaudio/prototype/__init__.py @@ -2,6 +2,7 @@ from .emformer import Emformer from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model from .rnnt_decoder import Hypothesis, RNNTBeamSearch +from .rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH, RNNTBundle __all__ = [ @@ -12,4 +13,6 @@ "RNNTBeamSearch", "emformer_rnnt_base", "emformer_rnnt_model", + "EMFORMER_RNNT_BASE_LIBRISPEECH", + "RNNTBundle", ] diff --git a/torchaudio/prototype/rnnt_pipeline.py b/torchaudio/prototype/rnnt_pipeline.py new file mode 100644 index 0000000000..9e42f2c3eb --- /dev/null +++ b/torchaudio/prototype/rnnt_pipeline.py @@ -0,0 +1,386 @@ +from abc import ABC, abstractmethod +from typing import Callable, List, Tuple +from dataclasses import dataclass +import json +import math +import pathlib +import torch + +import torchaudio +from torchaudio._internal import download_url_to_file, load_state_dict_from_url, module_utils +from torchaudio.prototype import RNNT, RNNTBeamSearch, emformer_rnnt_base + + +__all__ = [] + + +_BASE_MODELS_URL = "https://download.pytorch.org/torchaudio/models" +_BASE_PIPELINES_URL = "https://download.pytorch.org/torchaudio/pipeline-assets" + + +def _download_asset(asset_path: str): + dst_path = pathlib.Path(torch.hub.get_dir()) / "_assets" / asset_path + if not dst_path.exists(): + dst_path.parent.mkdir(exist_ok=True) + download_url_to_file(f"{_BASE_PIPELINES_URL}/{asset_path}", dst_path) + return str(dst_path) + + +_decibel = 2 * 20 * math.log10(torch.iinfo(torch.int16).max) +_gain = pow(10, 0.05 * _decibel) + + +def _piecewise_linear_log(x): + x[x > math.e] = torch.log(x[x > math.e]) + x[x <= math.e] = x[x <= math.e] / math.e + return x + + +class _FunctionalModule(torch.nn.Module): + def __init__(self, functional): + super().__init__() + self.functional = functional + + def forward(self, input): + return self.functional(input) + + +class _GlobalStatsNormalization(torch.nn.Module): + def __init__(self, global_stats_path): + super().__init__() + + with open(global_stats_path) as f: + blob = json.loads(f.read()) + + self.mean = torch.tensor(blob["mean"]) + self.invstddev = torch.tensor(blob["invstddev"]) + + def forward(self, input): + return (input - self.mean) * self.invstddev + + +class _FeatureExtractor(ABC): + @abstractmethod + def __call__(self, input: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """Generates features and length output from the given input tensor. + + Args: + input (torch.Tensor): input tensor. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor: + Features, with shape `(length, *)`. + torch.Tensor: + Length, with shape `(1,)`. + """ + + +class _TokenProcessor(ABC): + @abstractmethod + def __call__(self, tokens: List[int]) -> str: + """Decodes given list of tokens to text sequence. + + Args: + tokens (List[int]): list of tokens to decode. + + Returns: + str: + Decoded text sequence. + """ + + +class _ModuleFeatureExtractor(torch.nn.Module, _FeatureExtractor): + """``torch.nn.Module``-based feature extraction pipeline. + + Args: + pipeline (torch.nn.Module): module that implements feature extraction logic. + """ + + def __init__(self, pipeline: torch.nn.Module) -> None: + super().__init__() + self.pipeline = pipeline + + def forward(self, input: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """Generates features and length output from the given input tensor. + + Args: + input (torch.Tensor): input tensor. + + Returns: + (torch.Tensor, torch.Tensor): + torch.Tensor: + Features, with shape `(length, *)`. + torch.Tensor: + Length, with shape `(1,)`. + """ + features = self.pipeline(input) + length = torch.tensor([features.shape[0]]) + return features, length + + +class _SentencePieceTokenProcessor(_TokenProcessor): + """SentencePiece-model-based token processor. + + Args: + sp_model_path (str): path to SentencePiece model. + """ + + def __init__(self, sp_model_path: str) -> None: + if not module_utils.is_module_available("sentencepiece"): + raise RuntimeError("SentencePiece is not available. Please install it.") + + import sentencepiece as spm + + self.sp_model = spm.SentencePieceProcessor(model_file=sp_model_path) + self.post_process_remove_list = { + self.sp_model.unk_id(), + self.sp_model.eos_id(), + self.sp_model.pad_id(), + } + + def __call__(self, tokens: List[int]) -> str: + """Decodes given list of tokens to text sequence. + + Args: + tokens (List[int]): list of tokens to decode. + + Returns: + str: + Decoded text sequence. + """ + filtered_hypo_tokens = [ + token_index for token_index in tokens[1:] if token_index not in self.post_process_remove_list + ] + return self.sp_model.decode(filtered_hypo_tokens) + + +@dataclass +class RNNTBundle: + """torchaudio.prototype.rnnt_pipeline.RNNTBundle() + + Dataclass that bundles components for performing automatic speech recognition (ASR, speech-to-text) + inference with an RNN-T model. + + More specifically, the class provides methods that produce the featurization pipeline, + decoder wrapping the specified RNN-T model, and output token post-processor that together + constitute a complete end-to-end ASR inference pipeline that produces a text sequence + given a raw waveform. + + It can support non-streaming (full-context) inference as well as streaming inference. + + Users should not directly instantiate objects of this class; rather, users should use the + instances (representing pre-trained models) that exist within the module, + e.g. :py:obj:`EMFORMER_RNNT_BASE_LIBRISPEECH`. + + Example + >>> import torchaudio + >>> from torchaudio.prototype.rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH + >>> import torch + >>> + >>> # Non-streaming inference. + >>> # Build feature extractor, decoder with RNN-T model, and token processor. + >>> feature_extractor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_feature_extractor() + 100%|███████████████████████████████| 3.81k/3.81k [00:00<00:00, 4.22MB/s] + >>> decoder = EMFORMER_RNNT_BASE_LIBRISPEECH.get_decoder() + Downloading: "https://download.pytorch.org/torchaudio/models/emformer_rnnt_base_librispeech.pt" + 100%|███████████████████████████████| 293M/293M [00:07<00:00, 42.1MB/s] + >>> token_processor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_token_processor() + 100%|███████████████████████████████| 295k/295k [00:00<00:00, 25.4MB/s] + >>> + >>> # Instantiate LibriSpeech dataset; retrieve waveform for first sample. + >>> dataset = torchaudio.datasets.LIBRISPEECH("/home/librispeech", url="test-clean") + >>> waveform = next(iter(dataset))[0].squeeze() + >>> + >>> with torch.no_grad(): + >>> # Produce mel-scale spectrogram features. + >>> features, length = feature_extractor(waveform) + >>> + >>> # Generate top-10 hypotheses. + >>> hypotheses = decoder(features, length, 10) + >>> + >>> # For top hypothesis, convert predicted tokens to text. + >>> text = token_processor(hypotheses[0].tokens) + >>> print(text) + he hoped there would be stew for dinner turnips and carrots and bruised potatoes and fat mutton pieces to [...] + >>> + >>> + >>> # Streaming inference. + >>> hop_length = EMFORMER_RNNT_BASE_LIBRISPEECH.hop_length + >>> num_samples_segment = EMFORMER_RNNT_BASE_LIBRISPEECH.segment_length * hop_length + >>> num_samples_segment_right_context = ( + >>> num_samples_segment + EMFORMER_RNNT_BASE_LIBRISPEECH.right_context_length * hop_length + >>> ) + >>> + >>> # Build streaming inference feature extractor. + >>> streaming_feature_extractor = EMFORMER_RNNT_BASE_LIBRISPEECH.get_streaming_feature_extractor() + >>> + >>> # Process same waveform as before, this time sequentially across overlapping segments + >>> # to simulate streaming inference. Note the usage of ``streaming_feature_extractor`` and ``decoder.infer``. + >>> state, hypothesis = None, None + >>> for idx in range(0, len(waveform), num_samples_segment): + >>> segment = waveform[idx: idx + num_samples_segment_right_context] + >>> segment = torch.nn.functional.pad(segment, (0, num_samples_segment_right_context - len(segment))) + >>> with torch.no_grad(): + >>> features, length = streaming_feature_extractor(segment) + >>> hypotheses, state = decoder.infer(features, length, 10, state=state, hypothesis=hypothesis) + >>> hypothesis = hypotheses[0] + >>> transcript = token_processor(hypothesis.tokens) + >>> if transcript: + >>> print(transcript, end=" ", flush=True) + he hoped there would be stew for dinner turn ips and car rots and bru 'd oes and fat mut ton pieces to [...] + """ + + class FeatureExtractor(_FeatureExtractor): + pass + + class TokenProcessor(_TokenProcessor): + pass + + _rnnt_path: str + _rnnt_factory_func: Callable[[], RNNT] + _global_stats_path: str + _sp_model_path: str + _right_padding: int + _blank: int + _sample_rate: int + _n_fft: int + _n_mels: int + _hop_length: int + _segment_length: int + _right_context_length: int + + def _get_model(self) -> RNNT: + model = self._rnnt_factory_func() + url = f"{_BASE_MODELS_URL}/{self._rnnt_path}" + state_dict = load_state_dict_from_url(url) + model.load_state_dict(state_dict) + model.eval() + return model + + @property + def sample_rate(self) -> int: + """Sample rate (in cycles per second) of input waveforms. + + :type: int + """ + return self._sample_rate + + @property + def n_fft(self) -> int: + """Size of FFT window to use. + + :type: int + """ + return self._n_fft + + @property + def n_mels(self) -> int: + """Number of mel spectrogram features to extract from input waveforms. + + :type: int + """ + return self._n_mels + + @property + def hop_length(self) -> int: + """Number of samples between successive frames in input expected by model. + + :type: int + """ + return self._hop_length + + @property + def segment_length(self) -> int: + """Number of frames in segment in input expected by model. + + :type: int + """ + return self._segment_length + + @property + def right_context_length(self) -> int: + """Number of frames in right contextual block in input expected by model. + + :type: int + """ + return self._right_context_length + + def get_decoder(self) -> RNNTBeamSearch: + """Constructs RNN-T decoder. + + Returns: + RNNTBeamSearch + """ + model = self._get_model() + return RNNTBeamSearch(model, self._blank) + + def get_feature_extractor(self) -> FeatureExtractor: + """Constructs feature extractor for non-streaming (full-context) ASR. + + Returns: + FeatureExtractor + """ + local_path = _download_asset(self._global_stats_path) + return _ModuleFeatureExtractor( + torch.nn.Sequential( + torchaudio.transforms.MelSpectrogram( + sample_rate=self.sample_rate, n_fft=self.n_fft, n_mels=self.n_mels, hop_length=self.hop_length + ), + _FunctionalModule(lambda x: x.transpose(1, 0)), + _FunctionalModule(lambda x: _piecewise_linear_log(x * _gain)), + _GlobalStatsNormalization(local_path), + _FunctionalModule(lambda x: torch.nn.functional.pad(x, (0, 0, 0, self._right_padding))), + ) + ) + + def get_streaming_feature_extractor(self) -> FeatureExtractor: + """Constructs feature extractor for streaming (simultaneous) ASR. + + Returns: + FeatureExtractor + """ + local_path = _download_asset(self._global_stats_path) + return _ModuleFeatureExtractor( + torch.nn.Sequential( + torchaudio.transforms.MelSpectrogram( + sample_rate=self.sample_rate, n_fft=self.n_fft, n_mels=self.n_mels, hop_length=self.hop_length + ), + _FunctionalModule(lambda x: x.transpose(1, 0)), + _FunctionalModule(lambda x: _piecewise_linear_log(x * _gain)), + _GlobalStatsNormalization(local_path), + ) + ) + + def get_token_processor(self) -> TokenProcessor: + """Constructs token processor. + + Returns: + TokenProcessor + """ + local_path = _download_asset(self._sp_model_path) + return _SentencePieceTokenProcessor(local_path) + + +EMFORMER_RNNT_BASE_LIBRISPEECH = RNNTBundle( + _rnnt_path="emformer_rnnt_base_librispeech.pt", + _rnnt_factory_func=emformer_rnnt_base, + _global_stats_path="global_stats_rnnt_librispeech.json", + _sp_model_path="spm_bpe_4096_librispeech.model", + _right_padding=4, + _blank=4096, + _sample_rate=16000, + _n_fft=400, + _n_mels=80, + _hop_length=160, + _segment_length=16, + _right_context_length=4, +) +EMFORMER_RNNT_BASE_LIBRISPEECH.__doc__ = """Pre-trained Emformer-RNNT-based ASR pipeline capable of performing both streaming and non-streaming inference. + + The underlying model is constructed by :py:func:`torchaudio.prototypes.emformer_rnnt_base` + and utilizes weights trained on LibriSpeech using training script ``train.py`` + `here `__ with default arguments. + + Please refer to :py:class:`RNNTBundle` for usage instructions. + """ From 10cce19841db4953fadee4e46416dd8a32b92582 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 28 Dec 2021 18:50:13 -0800 Subject: [PATCH 0064/1144] Update prototype documentations (#2108) Summary: ### Change list * Split the documentation of prototypes * Add a new API reference section dedicated for prototypes. * Hide the signature of KenLMLexiconDecoder constructor. (cc carolineechen ) * https://489516-90321822-gh.circle-artifacts.com/0/docs/prototype.ctc_decoder.html#torchaudio.prototype.ctc_decoder.KenLMLexiconDecoder * Hide the signature of RNNT constructor. (cc hwangjeff ) * https://489516-90321822-gh.circle-artifacts.com/0/docs/prototype.rnnt.html#torchaudio.prototype.RNNT * Tweak CTC tutorial * Replace hyperlinks to API reference with backlinks * Add `progress=False` to download ### Follow-up RNNT decoder and CTC decode returns their own `Hypothesis` classes. When I tried to add Hypothesis of CTC decode to the documentation, the build process complains that it's ambiguous. I think the Hypothesis classes can be put inside of each decoder. (if TorchScript supports it) or make the name different, but in that case the interface of each Hypothesis has to be generic enough. ### Before https://pytorch.org/audio/main/prototype.html Screen Shot 2021-12-28 at 1 05 53 PM ### After https://489516-90321822-gh.circle-artifacts.com/0/docs/prototype.html Screen Shot 2021-12-28 at 8 37 35 PM https://489516-90321822-gh.circle-artifacts.com/0/docs/prototype.rnnt.html Screen Shot 2021-12-28 at 8 38 27 PM https://489516-90321822-gh.circle-artifacts.com/0/docs/prototype.ctc_decoder.html Screen Shot 2021-12-28 at 8 39 04 PM Pull Request resolved: https://github.com/pytorch/audio/pull/2108 Reviewed By: hwangjeff, carolineechen, nateanl Differential Revision: D33340816 Pulled By: mthrok fbshipit-source-id: 870edfadbe41d6f8abaf78fdb7017b3980dfe187 --- docs/source/index.rst | 10 ++ docs/source/prototype.ctc_decoder.rst | 25 ++++ docs/source/prototype.rnnt.rst | 110 +++++++++++++++ docs/source/prototype.rst | 130 ++---------------- ...asr_inference_with_ctc_decoder_tutorial.py | 15 +- .../prototype/ctc_decoder/ctc_decoder.py | 8 ++ torchaudio/prototype/rnnt.py | 4 +- 7 files changed, 175 insertions(+), 127 deletions(-) create mode 100644 docs/source/prototype.ctc_decoder.rst create mode 100644 docs/source/prototype.rnnt.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 58c54a1501..a4582ca60b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -48,7 +48,17 @@ API References compliance.kaldi kaldi_io utils + +Prototype API References +------------------------ + +.. toctree:: + :maxdepth: 1 + :caption: Prototype API Reference + prototype + prototype.rnnt + prototype.ctc_decoder Getting Started --------------- diff --git a/docs/source/prototype.ctc_decoder.rst b/docs/source/prototype.ctc_decoder.rst new file mode 100644 index 0000000000..6c381d7ccc --- /dev/null +++ b/docs/source/prototype.ctc_decoder.rst @@ -0,0 +1,25 @@ +torchaudio.prototype.ctc_decoder +================================ + +.. currentmodule:: torchaudio.prototype.ctc_decoder + +Decoder Class +------------- + +KenLMLexiconDecoder +~~~~~~~~~~~~~~~~~~~ + + +.. autoclass:: KenLMLexiconDecoder + + .. automethod:: __call__ + + .. automethod:: idxs_to_tokens + +Factory Function +---------------- + +kenlm_lexicon_decoder +~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: kenlm_lexicon_decoder diff --git a/docs/source/prototype.rnnt.rst b/docs/source/prototype.rnnt.rst new file mode 100644 index 0000000000..e4639e010d --- /dev/null +++ b/docs/source/prototype.rnnt.rst @@ -0,0 +1,110 @@ +torchaudio.prototype.rnnt +========================= + +.. py:module:: torchaudio.prototype + +.. currentmodule:: torchaudio.prototype + +Model Classes +------------- + +Conformer +~~~~~~~~~ + +.. autoclass:: Conformer + + .. automethod:: forward + + +Emformer +~~~~~~~~ + +.. autoclass:: Emformer + + .. automethod:: forward + + .. automethod:: infer + + +RNNT +~~~~ + +.. autoclass:: RNNT + + .. automethod:: forward + + .. automethod:: transcribe_streaming + + .. automethod:: transcribe + + .. automethod:: predict + + .. automethod:: join + +Model Factory Functions +----------------------- + +emformer_rnnt_base +~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_base + +emformer_rnnt_model +~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_model + +Decoder Classes +--------------- + +RNNTBeamSearch +~~~~~~~~~~~~ + +.. autoclass:: RNNTBeamSearch + + .. automethod:: forward + + .. automethod:: infer + + +Hypothesis +~~~~~~~~~~ + +.. autoclass:: Hypothesis + +Pipeline Primitives (Pre-trained Models) +---------------------------------------- + +RNNTBundle +~~~~~~~~~~ + +.. autoclass:: RNNTBundle + :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length + + .. automethod:: get_decoder + + .. automethod:: get_feature_extractor + + .. automethod:: get_streaming_feature_extractor + + .. automethod:: get_token_processor + + .. autoclass:: torchaudio.prototype::RNNTBundle.FeatureExtractor + :special-members: __call__ + + .. autoclass:: torchaudio.prototype::RNNTBundle.TokenProcessor + :special-members: __call__ + + +EMFORMER_RNNT_BASE_LIBRISPEECH +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. container:: py attribute + + .. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH + :no-value: + +References +---------- + +.. footbibliography:: diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index afbde5a3cf..f8f90a7082 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -1,127 +1,21 @@ -.. role:: hidden - :class: hidden-section - torchaudio.prototype ==================== -.. py:module:: torchaudio.prototype - -.. currentmodule:: torchaudio.prototype - ``torchaudio.prototype`` provides prototype features; -see `here `_ for more information on prototype features. -The module is available only within nightly builds and must be imported -explicitly, e.g. ``import torchaudio.prototype``. - -Conformer -~~~~~~~~~ - -.. autoclass:: Conformer - - .. automethod:: forward - - -Emformer -~~~~~~~~ - -.. autoclass:: Emformer - - .. automethod:: forward - - .. automethod:: infer - - -RNNT -~~~~ - -.. autoclass:: RNNT - - .. automethod:: forward - - .. automethod:: transcribe_streaming - - .. automethod:: transcribe - - .. automethod:: predict - - .. automethod:: join - -emformer_rnnt_base -~~~~~~~~~~~~~~~~~~ - -.. autofunction:: emformer_rnnt_base - -emformer_rnnt_model -~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: emformer_rnnt_model - - -RNNTBeamSearch -~~~~~~~~~~~~~~ - -.. autoclass:: RNNTBeamSearch - - .. automethod:: forward - - .. automethod:: infer - - -Hypothesis -~~~~~~~~~~ - -.. autoclass:: Hypothesis - - -RNNTBundle -~~~~~~~~~~ - -.. autoclass:: RNNTBundle - :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length - - .. automethod:: get_decoder - - .. automethod:: get_feature_extractor - - .. automethod:: get_streaming_feature_extractor - - .. automethod:: get_token_processor - - .. autoclass:: torchaudio.prototype::RNNTBundle.FeatureExtractor - :special-members: __call__ - - .. autoclass:: torchaudio.prototype::RNNTBundle.TokenProcessor - :special-members: __call__ - - -EMFORMER_RNNT_BASE_LIBRISPEECH -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH - :no-value: - - -KenLMLexiconDecoder -~~~~~~~~~~~~~~~~~~~ - -.. currentmodule:: torchaudio.prototype.ctc_decoder - -.. autoclass:: KenLMLexiconDecoder - - .. automethod:: __call__ - - .. automethod:: idxs_to_tokens - - -kenlm_lexicon_decoder -~~~~~~~~~~~~~~~~~~~~~ +they are at an early stage for feedback and testing. +Their interfaces might be changed without prior notice. -.. currentmodule:: torchaudio.prototype.ctc_decoder +Most modules of prototypes are excluded from release. +Please refer to `here `_ for +more information on prototype features. -.. autoclass:: kenlm_lexicon_decoder +The modules under ``torchaudio.prototype`` must be +imported explicitly, e.g. +.. code-block:: python -References -~~~~~~~~~~ + import torchaudio.prototype.rnnt -.. footbibliography:: +.. toctree:: + prototype.rnnt + prototype.ctc_decoder diff --git a/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py index 60836c3d85..9ad2f4a0f0 100644 --- a/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py +++ b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py @@ -50,7 +50,7 @@ # We use the pretrained `Wav2Vec 2.0 `__ # Base model that is finetuned on 10 min of the `LibriSpeech # dataset `__, which can be loaded in using -# ``torchaudio.pipelines``. For more detail on running Wav2Vec 2.0 speech +# py:func:`torchaudio.pipelines`. For more detail on running Wav2Vec 2.0 speech # recognition pipelines in torchaudio, please refer to `this # tutorial `__. # @@ -161,12 +161,11 @@ # Construct Beam Search Decoder # ----------------------------- # -# The decoder can be constructed using the ``kenlm_lexicon_decoder`` -# factory function from ``torchaudio.prototype.ctc_decoder``. In addition -# to the previously mentioned components, it also takes in various beam -# search decoding parameters and token/word parameters. The full list of -# parameters can be found -# `here `__. +# The decoder can be constructed using the +# :py:func:`torchaudio.prototype.ctc_decoder.kenlm_lexicon_decoder` +# factory function. +# In addition to the previously mentioned components, it also takes in +# various beam search decoding parameters and token/word parameters. # from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder @@ -190,7 +189,7 @@ # -------------- # # For comparison against the beam search decoder, we also construct a -# basic greedy decoder.\ **bold text** +# basic greedy decoder. # diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py index 2a119d15c0..a02a70db17 100644 --- a/torchaudio/prototype/ctc_decoder/ctc_decoder.py +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -24,6 +24,14 @@ class KenLMLexiconDecoder: + """torchaudio.prototype.ctc_decoder.KenLMLexiconDecoder() + + Note: + To build the decoder, please use factory function + :py:func:`kenlm_lexicon_decoder`. + + """ + def __init__( self, nbest: int, diff --git a/torchaudio/prototype/rnnt.py b/torchaudio/prototype/rnnt.py index 71e516c7dc..1099297093 100644 --- a/torchaudio/prototype/rnnt.py +++ b/torchaudio/prototype/rnnt.py @@ -426,7 +426,9 @@ def forward( class RNNT(torch.nn.Module): - r"""Recurrent neural network transducer (RNN-T) model. + r"""torchaudio.prototype.rnnt.RNNT() + + Recurrent neural network transducer (RNN-T) model. Note: To build the model, please use one of the factory functions. From 697f92f1cdec1d717b9ee6fd71634eb89ac13cbe Mon Sep 17 00:00:00 2001 From: CodemodService Bot <> Date: Wed, 29 Dec 2021 04:05:48 -0800 Subject: [PATCH 0065/1144] [Codemod][FBSourceBlackLinter] Daily `arc lint --take BLACK` Reviewed By: zertosh Differential Revision: D33347867 fbshipit-source-id: 7672f65392e363c0359de2d86e745782a09cf9dc --- .../asr/librispeech_emformer_rnnt/pipeline_demo.py | 10 +++++++--- torchaudio/prototype/rnnt_pipeline.py | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py index dd65c7936d..4e76e803e4 100644 --- a/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py +++ b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py @@ -1,5 +1,6 @@ -from argparse import ArgumentParser import pathlib +from argparse import ArgumentParser + import torch import torchaudio from torchaudio.prototype.rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH @@ -8,7 +9,10 @@ def cli_main(): parser = ArgumentParser() parser.add_argument( - "--librispeech_path", type=pathlib.Path, required=True, help="Path to LibriSpeech datasets.", + "--librispeech_path", + type=pathlib.Path, + required=True, + help="Path to LibriSpeech datasets.", ) args = parser.parse_args() @@ -31,7 +35,7 @@ def cli_main(): # Streaming decode. state, hypothesis = None, None for idx in range(0, len(waveform), num_samples_segment): - segment = waveform[idx: idx + num_samples_segment_right_context] + segment = waveform[idx : idx + num_samples_segment_right_context] segment = torch.nn.functional.pad(segment, (0, num_samples_segment_right_context - len(segment))) with torch.no_grad(): features, length = streaming_feature_extractor(segment) diff --git a/torchaudio/prototype/rnnt_pipeline.py b/torchaudio/prototype/rnnt_pipeline.py index 9e42f2c3eb..9444cf694e 100644 --- a/torchaudio/prototype/rnnt_pipeline.py +++ b/torchaudio/prototype/rnnt_pipeline.py @@ -1,11 +1,11 @@ -from abc import ABC, abstractmethod -from typing import Callable, List, Tuple -from dataclasses import dataclass import json import math import pathlib -import torch +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Callable, List, Tuple +import torch import torchaudio from torchaudio._internal import download_url_to_file, load_state_dict_from_url, module_utils from torchaudio.prototype import RNNT, RNNTBeamSearch, emformer_rnnt_base From 5cc4765abc9ce1c8ee1dade4f4a812a63fd520f7 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 29 Dec 2021 11:13:06 -0800 Subject: [PATCH 0066/1144] Add Sink class (#2111) Summary: Add Sink class that bundles FilterGraph and Buffer. Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Pull Request resolved: https://github.com/pytorch/audio/pull/2111 Reviewed By: carolineechen Differential Revision: D33350388 Pulled By: mthrok fbshipit-source-id: 8f42c5fe4be39ef2432c51fc0d0ac72ba3f06a26 --- torchaudio/csrc/ffmpeg/sink.cpp | 58 +++++++++++++++++++++++++++++++++ torchaudio/csrc/ffmpeg/sink.h | 30 +++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/sink.cpp create mode 100644 torchaudio/csrc/ffmpeg/sink.h diff --git a/torchaudio/csrc/ffmpeg/sink.cpp b/torchaudio/csrc/ffmpeg/sink.cpp new file mode 100644 index 0000000000..5be77578df --- /dev/null +++ b/torchaudio/csrc/ffmpeg/sink.cpp @@ -0,0 +1,58 @@ +#include + +namespace torchaudio { +namespace ffmpeg { + +namespace { +std::unique_ptr get_buffer( + AVMediaType type, + int frames_per_chunk, + int num_chunks) { + switch (type) { + case AVMEDIA_TYPE_AUDIO: + return std::unique_ptr( + new AudioBuffer(frames_per_chunk, num_chunks)); + case AVMEDIA_TYPE_VIDEO: + return std::unique_ptr( + new VideoBuffer(frames_per_chunk, num_chunks)); + default: + throw std::runtime_error( + std::string("Unsupported media type: ") + + av_get_media_type_string(type)); + } +} +} // namespace + +Sink::Sink( + AVRational input_time_base, + AVCodecParameters* codecpar, + int frames_per_chunk, + int num_chunks, + double output_time_base, + std::string filter_description) + : filter(input_time_base, codecpar, filter_description), + buffer(get_buffer(codecpar->codec_type, frames_per_chunk, num_chunks)), + time_base(output_time_base) {} + +// 0: some kind of success +// <0: Some error happened +int Sink::process_frame(AVFrame* pFrame) { + int ret = filter.add_frame(pFrame); + while (ret >= 0) { + ret = filter.get_frame(frame); + // AVERROR(EAGAIN) means that new input data is required to return new + // output. + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + return 0; + if (ret >= 0) + buffer->push_frame(frame); + av_frame_unref(frame); + } + return ret; +} + +bool Sink::is_buffer_ready() const { + return buffer->is_ready(); +} +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/sink.h b/torchaudio/csrc/ffmpeg/sink.h new file mode 100644 index 0000000000..db45ebba78 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/sink.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +class Sink { + AVFramePtr frame; + + public: + FilterGraph filter; + std::unique_ptr buffer; + double time_base; + Sink( + AVRational input_time_base, + AVCodecParameters* codecpar, + int frames_per_chunk, + int num_chunks, + double output_time_base, + std::string filter_description); + + int process_frame(AVFrame* frame); + bool is_buffer_ready() const; +}; + +} // namespace ffmpeg +} // namespace torchaudio From 572cd2e227fd0f1107d088676942ae7d39e676ce Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:13:04 -0800 Subject: [PATCH 0067/1144] Add StreamProcessor class (#2045) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add StreamProcessor class that bundles `Buffer`, `FilterGraph` and `Decoder`. Note: The API to retrieve the buffered Tensors is tentative. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2044. Pull Request resolved: https://github.com/pytorch/audio/pull/2045 Reviewed By: carolineechen Differential Revision: D33299858 Pulled By: mthrok fbshipit-source-id: d85bececed475f45622743f137dd59cb1390ceed --- torchaudio/csrc/ffmpeg/stream_processor.cpp | 129 ++++++++++++++++++++ torchaudio/csrc/ffmpeg/stream_processor.h | 85 +++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/stream_processor.cpp create mode 100644 torchaudio/csrc/ffmpeg/stream_processor.h diff --git a/torchaudio/csrc/ffmpeg/stream_processor.cpp b/torchaudio/csrc/ffmpeg/stream_processor.cpp new file mode 100644 index 0000000000..93bd0d50fd --- /dev/null +++ b/torchaudio/csrc/ffmpeg/stream_processor.cpp @@ -0,0 +1,129 @@ +#include +#include "libavutil/frame.h" + +namespace torchaudio { +namespace ffmpeg { + +using KeyType = StreamProcessor::KeyType; + +StreamProcessor::StreamProcessor(AVCodecParameters* codecpar) + : media_type(codecpar->codec_type), decoder(codecpar) {} + +//////////////////////////////////////////////////////////////////////////////// +// Configurations +//////////////////////////////////////////////////////////////////////////////// +KeyType StreamProcessor::add_stream( + AVRational input_time_base, + AVCodecParameters* codecpar, + int frames_per_chunk, + int num_chunks, + double output_rate, + std::string filter_description) { + switch (codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + case AVMEDIA_TYPE_VIDEO: + break; + default: + throw std::runtime_error("Only Audio and Video are supported"); + } + KeyType key = current_key++; + sinks.emplace( + std::piecewise_construct, + std::forward_as_tuple(key), + std::forward_as_tuple( + input_time_base, + codecpar, + frames_per_chunk, + num_chunks, + (output_rate > 0) ? 1 / output_rate : av_q2d(input_time_base), + std::move(filter_description))); + decoder_time_base = av_q2d(input_time_base); + return key; +} + +void StreamProcessor::remove_stream(KeyType key) { + sinks.erase(key); +} + +//////////////////////////////////////////////////////////////////////////////// +// Query methods +//////////////////////////////////////////////////////////////////////////////// +std::string StreamProcessor::get_filter_description(KeyType key) const { + return sinks.at(key).filter.get_description(); +} + +bool StreamProcessor::is_buffer_ready() const { + for (const auto& it : sinks) { + if (!it.second.is_buffer_ready()) { + return false; + } + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// The streaming process +//////////////////////////////////////////////////////////////////////////////// +namespace { +void debug_print_frame(AVFrame* pFrame, double time_rate) { + if (pFrame->sample_rate) + std::cerr << " ---- format: " + << av_get_sample_fmt_name( + static_cast(pFrame->format)) + << ", num_frames: " << pFrame->nb_samples + << ", num_channels: " << pFrame->channels + << ", num_samples: " << pFrame->nb_samples * pFrame->channels + << ", sample_rate: " << pFrame->sample_rate + << ", pts: " << pFrame->pts << ", pts/sample_rate: " + << pFrame->pts / (double)pFrame->sample_rate + << ", time: " << pFrame->pts * time_rate << std::endl; + else + std::cerr << " -------- format: " + << av_get_pix_fmt_name(static_cast(pFrame->format)) + << ", width: " << pFrame->width << ", height: " << pFrame->height + << ", pts: " << pFrame->pts + << ", time: " << pFrame->pts * time_rate << std::endl; +} +} // namespace + +// 0: some kind of success +// <0: Some error happened +int StreamProcessor::process_packet(AVPacket* packet) { + int ret = decoder.process_packet(packet); + while (ret >= 0) { + ret = decoder.get_frame(pFrame1); + // AVERROR(EAGAIN) means that new input data is required to return new + // output. + if (ret == AVERROR(EAGAIN)) + return 0; + if (ret == AVERROR_EOF) + return send_frame(NULL); + if (ret < 0) + return ret; + send_frame(pFrame1); + av_frame_unref(pFrame1); + } + return ret; +} + +// 0: some kind of success +// <0: Some error happened +int StreamProcessor::send_frame(AVFrame* pFrame) { + int ret = 0; + for (auto& ite : sinks) { + int ret2 = ite.second.process_frame(pFrame); + if (ret2 < 0) + ret = ret2; + } + return ret; +} + +//////////////////////////////////////////////////////////////////////////////// +// Retrieval +//////////////////////////////////////////////////////////////////////////////// +c10::optional StreamProcessor::pop_chunk(KeyType key) { + return sinks.at(key).buffer->pop_chunk(); +} + +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/stream_processor.h b/torchaudio/csrc/ffmpeg/stream_processor.h new file mode 100644 index 0000000000..94e6141081 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/stream_processor.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +class StreamProcessor { + public: + using KeyType = int; + + private: + AVMediaType media_type = AVMEDIA_TYPE_UNKNOWN; + AVFramePtr pFrame1; + AVFramePtr pFrame2; + + // Components for decoding source media + double decoder_time_base; // for debug + Decoder decoder; + + KeyType current_key = 0; + std::map sinks; + + public: + StreamProcessor(AVCodecParameters* codecpar); + ~StreamProcessor() = default; + // Non-copyable + StreamProcessor(const StreamProcessor&) = delete; + StreamProcessor& operator=(const StreamProcessor&) = delete; + // Movable + StreamProcessor(StreamProcessor&&) = default; + StreamProcessor& operator=(StreamProcessor&&) = default; + + ////////////////////////////////////////////////////////////////////////////// + // Configurations + ////////////////////////////////////////////////////////////////////////////// + // 1. Initialize decoder (if not initialized yet) + // 2. Configure a new audio/video filter. + // If the custom parameter is provided, then perform resize, resample etc.. + // otherwise, the filter only converts the sample type. + // 3. Configure a buffer. + // 4. Return filter ID. + KeyType add_stream( + AVRational input_time_base, + AVCodecParameters* codecpar, + int frames_per_chunk, + int num_chunks, + double output_rate, + std::string filter_description); + + // 1. Remove the stream + void remove_stream(KeyType key); + + ////////////////////////////////////////////////////////////////////////////// + // Query methods + ////////////////////////////////////////////////////////////////////////////// + std::string get_filter_description(KeyType key) const; + bool is_buffer_ready() const; + + ////////////////////////////////////////////////////////////////////////////// + // The streaming process + ////////////////////////////////////////////////////////////////////////////// + // 1. decode the input frame + // 2. pass the decoded data to filters + // 3. each filter store the result to the corresponding buffer + // - Sending NULL will drain (flush) the internal + int process_packet(AVPacket* packet); + + private: + int send_frame(AVFrame* pFrame); + + ////////////////////////////////////////////////////////////////////////////// + // Retrieval + ////////////////////////////////////////////////////////////////////////////// + public: + // Get the chunk from the given filter result + c10::optional pop_chunk(KeyType key); +}; + +} // namespace ffmpeg +} // namespace torchaudio From 67cdf8828f9bed16fe0a0c93fccd7cb63e9f10df Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Wed, 29 Dec 2021 13:29:41 -0800 Subject: [PATCH 0068/1144] Reorganize RNN-T components in prototype module (#2110) Summary: Regroup RNN-T components under `torchaudio.prototype.models` and `torchaudio.prototype.pipelines`. Updated docs: https://492321-90321822-gh.circle-artifacts.com/0/docs/prototype.html Pull Request resolved: https://github.com/pytorch/audio/pull/2110 Reviewed By: carolineechen, mthrok Differential Revision: D33354116 Pulled By: hwangjeff fbshipit-source-id: 9cf4afed548cb173d56211c16d31bcfa25a8e4cb --- docs/source/index.rst | 3 +- docs/source/prototype.models.rst | 81 +++++++++++++ docs/source/prototype.pipelines.rst | 40 +++++++ docs/source/prototype.rnnt.rst | 110 ------------------ docs/source/prototype.rst | 5 +- .../librispeech_emformer_rnnt/lightning.py | 3 +- .../pipeline_demo.py | 2 +- .../prototype/conformer_test_impl.py | 2 +- .../prototype/emformer_test_impl.py | 2 +- .../prototype/rnnt_decoder_test_impl.py | 2 +- .../prototype/rnnt_test_impl.py | 2 +- torchaudio/prototype/__init__.py | 18 --- torchaudio/prototype/models/__init__.py | 15 +++ .../prototype/{ => models}/conformer.py | 0 torchaudio/prototype/{ => models}/emformer.py | 0 torchaudio/prototype/{ => models}/rnnt.py | 4 +- .../prototype/{ => models}/rnnt_decoder.py | 2 +- torchaudio/prototype/pipelines/__init__.py | 7 ++ .../{ => pipelines}/rnnt_pipeline.py | 8 +- 19 files changed, 161 insertions(+), 145 deletions(-) create mode 100644 docs/source/prototype.models.rst create mode 100644 docs/source/prototype.pipelines.rst delete mode 100644 docs/source/prototype.rnnt.rst create mode 100644 torchaudio/prototype/models/__init__.py rename torchaudio/prototype/{ => models}/conformer.py (100%) rename torchaudio/prototype/{ => models}/emformer.py (100%) rename torchaudio/prototype/{ => models}/rnnt.py (99%) rename torchaudio/prototype/{ => models}/rnnt_decoder.py (99%) create mode 100644 torchaudio/prototype/pipelines/__init__.py rename torchaudio/prototype/{ => pipelines}/rnnt_pipeline.py (98%) diff --git a/docs/source/index.rst b/docs/source/index.rst index a4582ca60b..9694f936d3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -57,8 +57,9 @@ Prototype API References :caption: Prototype API Reference prototype - prototype.rnnt prototype.ctc_decoder + prototype.models + prototype.pipelines Getting Started --------------- diff --git a/docs/source/prototype.models.rst b/docs/source/prototype.models.rst new file mode 100644 index 0000000000..0a814ecb84 --- /dev/null +++ b/docs/source/prototype.models.rst @@ -0,0 +1,81 @@ +torchaudio.prototype.models +=========================== + +.. py:module:: torchaudio.prototype.models + +.. currentmodule:: torchaudio.prototype.models + +The models subpackage contains definitions of models and components for addressing common audio tasks. + + +Model Classes +------------- + +Conformer +~~~~~~~~~ + +.. autoclass:: Conformer + + .. automethod:: forward + +Emformer +~~~~~~~~ + +.. autoclass:: Emformer + + .. automethod:: forward + + .. automethod:: infer + +RNNT +~~~~ + +.. autoclass:: RNNT + + .. automethod:: forward + + .. automethod:: transcribe_streaming + + .. automethod:: transcribe + + .. automethod:: predict + + .. automethod:: join + + +Model Factory Functions +----------------------- + +emformer_rnnt_model +~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_model + +emformer_rnnt_base +~~~~~~~~~~~~~~~~~~ + +.. autofunction:: emformer_rnnt_base + + +Decoder Classes +--------------- + +RNNTBeamSearch +~~~~~~~~~~~~~~ + +.. autoclass:: RNNTBeamSearch + + .. automethod:: forward + + .. automethod:: infer + +Hypothesis +~~~~~~~~~~ + +.. autoclass:: Hypothesis + + +References +---------- + +.. footbibliography:: diff --git a/docs/source/prototype.pipelines.rst b/docs/source/prototype.pipelines.rst new file mode 100644 index 0000000000..3221cfdfae --- /dev/null +++ b/docs/source/prototype.pipelines.rst @@ -0,0 +1,40 @@ +torchaudio.prototype.pipelines +============================== + +.. py:module:: torchaudio.prototype.pipelines + +.. currentmodule:: torchaudio.prototype.pipelines + +The pipelines subpackage contains APIs to models with pretrained weights and relevant utilities. + +RNN-T Streaming/Non-Streaming ASR +--------------------------------- + +RNNTBundle +~~~~~~~~~~ + +.. autoclass:: RNNTBundle + :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length + + .. automethod:: get_decoder + + .. automethod:: get_feature_extractor + + .. automethod:: get_streaming_feature_extractor + + .. automethod:: get_token_processor + + .. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.FeatureExtractor + :special-members: __call__ + + .. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.TokenProcessor + :special-members: __call__ + + +EMFORMER_RNNT_BASE_LIBRISPEECH +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. container:: py attribute + + .. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH + :no-value: \ No newline at end of file diff --git a/docs/source/prototype.rnnt.rst b/docs/source/prototype.rnnt.rst deleted file mode 100644 index e4639e010d..0000000000 --- a/docs/source/prototype.rnnt.rst +++ /dev/null @@ -1,110 +0,0 @@ -torchaudio.prototype.rnnt -========================= - -.. py:module:: torchaudio.prototype - -.. currentmodule:: torchaudio.prototype - -Model Classes -------------- - -Conformer -~~~~~~~~~ - -.. autoclass:: Conformer - - .. automethod:: forward - - -Emformer -~~~~~~~~ - -.. autoclass:: Emformer - - .. automethod:: forward - - .. automethod:: infer - - -RNNT -~~~~ - -.. autoclass:: RNNT - - .. automethod:: forward - - .. automethod:: transcribe_streaming - - .. automethod:: transcribe - - .. automethod:: predict - - .. automethod:: join - -Model Factory Functions ------------------------ - -emformer_rnnt_base -~~~~~~~~~~~~~~~~~~ - -.. autofunction:: emformer_rnnt_base - -emformer_rnnt_model -~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: emformer_rnnt_model - -Decoder Classes ---------------- - -RNNTBeamSearch -~~~~~~~~~~~~ - -.. autoclass:: RNNTBeamSearch - - .. automethod:: forward - - .. automethod:: infer - - -Hypothesis -~~~~~~~~~~ - -.. autoclass:: Hypothesis - -Pipeline Primitives (Pre-trained Models) ----------------------------------------- - -RNNTBundle -~~~~~~~~~~ - -.. autoclass:: RNNTBundle - :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length - - .. automethod:: get_decoder - - .. automethod:: get_feature_extractor - - .. automethod:: get_streaming_feature_extractor - - .. automethod:: get_token_processor - - .. autoclass:: torchaudio.prototype::RNNTBundle.FeatureExtractor - :special-members: __call__ - - .. autoclass:: torchaudio.prototype::RNNTBundle.TokenProcessor - :special-members: __call__ - - -EMFORMER_RNNT_BASE_LIBRISPEECH -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. container:: py attribute - - .. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH - :no-value: - -References ----------- - -.. footbibliography:: diff --git a/docs/source/prototype.rst b/docs/source/prototype.rst index f8f90a7082..382af2a8b9 100644 --- a/docs/source/prototype.rst +++ b/docs/source/prototype.rst @@ -14,8 +14,9 @@ imported explicitly, e.g. .. code-block:: python - import torchaudio.prototype.rnnt + import torchaudio.prototype.models .. toctree:: - prototype.rnnt prototype.ctc_decoder + prototype.models + prototype.pipelines diff --git a/examples/asr/librispeech_emformer_rnnt/lightning.py b/examples/asr/librispeech_emformer_rnnt/lightning.py index 0bf5cb2895..285a4ed7a7 100644 --- a/examples/asr/librispeech_emformer_rnnt/lightning.py +++ b/examples/asr/librispeech_emformer_rnnt/lightning.py @@ -9,8 +9,7 @@ import torchaudio import torchaudio.functional as F from pytorch_lightning import LightningModule -from torchaudio.prototype.rnnt import emformer_rnnt_base -from torchaudio.prototype.rnnt_decoder import Hypothesis, RNNTBeamSearch +from torchaudio.prototype.models import Hypothesis, RNNTBeamSearch, emformer_rnnt_base Batch = namedtuple("Batch", ["features", "feature_lengths", "targets", "target_lengths"]) diff --git a/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py index 4e76e803e4..aedd57bf1e 100644 --- a/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py +++ b/examples/asr/librispeech_emformer_rnnt/pipeline_demo.py @@ -3,7 +3,7 @@ import torch import torchaudio -from torchaudio.prototype.rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH +from torchaudio.prototype.pipelines import EMFORMER_RNNT_BASE_LIBRISPEECH def cli_main(): diff --git a/test/torchaudio_unittest/prototype/conformer_test_impl.py b/test/torchaudio_unittest/prototype/conformer_test_impl.py index 98d69c4b68..bda9b2264d 100644 --- a/test/torchaudio_unittest/prototype/conformer_test_impl.py +++ b/test/torchaudio_unittest/prototype/conformer_test_impl.py @@ -1,5 +1,5 @@ import torch -from torchaudio.prototype import Conformer +from torchaudio.prototype.models import Conformer from torchaudio_unittest.common_utils import TestBaseMixin, torch_script diff --git a/test/torchaudio_unittest/prototype/emformer_test_impl.py b/test/torchaudio_unittest/prototype/emformer_test_impl.py index a990353cff..d380af4b4e 100644 --- a/test/torchaudio_unittest/prototype/emformer_test_impl.py +++ b/test/torchaudio_unittest/prototype/emformer_test_impl.py @@ -1,5 +1,5 @@ import torch -from torchaudio.prototype import Emformer +from torchaudio.prototype.models import Emformer from torchaudio_unittest.common_utils import TestBaseMixin, torch_script diff --git a/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py index bcc9364b97..af4c8e6851 100644 --- a/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py +++ b/test/torchaudio_unittest/prototype/rnnt_decoder_test_impl.py @@ -1,5 +1,5 @@ import torch -from torchaudio.prototype import RNNTBeamSearch, emformer_rnnt_model +from torchaudio.prototype.models import RNNTBeamSearch, emformer_rnnt_model from torchaudio_unittest.common_utils import TestBaseMixin, torch_script diff --git a/test/torchaudio_unittest/prototype/rnnt_test_impl.py b/test/torchaudio_unittest/prototype/rnnt_test_impl.py index 1357927700..fcfb8ab6fd 100644 --- a/test/torchaudio_unittest/prototype/rnnt_test_impl.py +++ b/test/torchaudio_unittest/prototype/rnnt_test_impl.py @@ -1,5 +1,5 @@ import torch -from torchaudio.prototype.rnnt import emformer_rnnt_model +from torchaudio.prototype.models import emformer_rnnt_model from torchaudio_unittest.common_utils import TestBaseMixin, torch_script diff --git a/torchaudio/prototype/__init__.py b/torchaudio/prototype/__init__.py index 8856fe46fd..e69de29bb2 100644 --- a/torchaudio/prototype/__init__.py +++ b/torchaudio/prototype/__init__.py @@ -1,18 +0,0 @@ -from .conformer import Conformer -from .emformer import Emformer -from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model -from .rnnt_decoder import Hypothesis, RNNTBeamSearch -from .rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH, RNNTBundle - - -__all__ = [ - "Conformer", - "Emformer", - "Hypothesis", - "RNNT", - "RNNTBeamSearch", - "emformer_rnnt_base", - "emformer_rnnt_model", - "EMFORMER_RNNT_BASE_LIBRISPEECH", - "RNNTBundle", -] diff --git a/torchaudio/prototype/models/__init__.py b/torchaudio/prototype/models/__init__.py new file mode 100644 index 0000000000..872e31d291 --- /dev/null +++ b/torchaudio/prototype/models/__init__.py @@ -0,0 +1,15 @@ +from .conformer import Conformer +from .emformer import Emformer +from .rnnt import RNNT, emformer_rnnt_base, emformer_rnnt_model +from .rnnt_decoder import Hypothesis, RNNTBeamSearch + + +__all__ = [ + "Conformer", + "Emformer", + "Hypothesis", + "RNNT", + "RNNTBeamSearch", + "emformer_rnnt_base", + "emformer_rnnt_model", +] diff --git a/torchaudio/prototype/conformer.py b/torchaudio/prototype/models/conformer.py similarity index 100% rename from torchaudio/prototype/conformer.py rename to torchaudio/prototype/models/conformer.py diff --git a/torchaudio/prototype/emformer.py b/torchaudio/prototype/models/emformer.py similarity index 100% rename from torchaudio/prototype/emformer.py rename to torchaudio/prototype/models/emformer.py diff --git a/torchaudio/prototype/rnnt.py b/torchaudio/prototype/models/rnnt.py similarity index 99% rename from torchaudio/prototype/rnnt.py rename to torchaudio/prototype/models/rnnt.py index 1099297093..c35025f8a9 100644 --- a/torchaudio/prototype/rnnt.py +++ b/torchaudio/prototype/models/rnnt.py @@ -2,7 +2,7 @@ import torch -from .emformer import Emformer +from torchaudio.prototype.models import Emformer __all__ = ["RNNT", "emformer_rnnt_base", "emformer_rnnt_model"] @@ -426,7 +426,7 @@ def forward( class RNNT(torch.nn.Module): - r"""torchaudio.prototype.rnnt.RNNT() + r"""torchaudio.prototype.models.RNNT() Recurrent neural network transducer (RNN-T) model. diff --git a/torchaudio/prototype/rnnt_decoder.py b/torchaudio/prototype/models/rnnt_decoder.py similarity index 99% rename from torchaudio/prototype/rnnt_decoder.py rename to torchaudio/prototype/models/rnnt_decoder.py index 8d006ee515..1f19c06679 100644 --- a/torchaudio/prototype/rnnt_decoder.py +++ b/torchaudio/prototype/models/rnnt_decoder.py @@ -2,7 +2,7 @@ import torch -from .rnnt import RNNT +from torchaudio.prototype.models import RNNT __all__ = ["Hypothesis", "RNNTBeamSearch"] diff --git a/torchaudio/prototype/pipelines/__init__.py b/torchaudio/prototype/pipelines/__init__.py new file mode 100644 index 0000000000..f6c0bc20db --- /dev/null +++ b/torchaudio/prototype/pipelines/__init__.py @@ -0,0 +1,7 @@ +from .rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH, RNNTBundle + + +__all__ = [ + "EMFORMER_RNNT_BASE_LIBRISPEECH", + "RNNTBundle", +] diff --git a/torchaudio/prototype/rnnt_pipeline.py b/torchaudio/prototype/pipelines/rnnt_pipeline.py similarity index 98% rename from torchaudio/prototype/rnnt_pipeline.py rename to torchaudio/prototype/pipelines/rnnt_pipeline.py index 9444cf694e..9e07a5fdd9 100644 --- a/torchaudio/prototype/rnnt_pipeline.py +++ b/torchaudio/prototype/pipelines/rnnt_pipeline.py @@ -8,7 +8,7 @@ import torch import torchaudio from torchaudio._internal import download_url_to_file, load_state_dict_from_url, module_utils -from torchaudio.prototype import RNNT, RNNTBeamSearch, emformer_rnnt_base +from torchaudio.prototype.models import RNNT, RNNTBeamSearch, emformer_rnnt_base __all__ = [] @@ -157,7 +157,7 @@ def __call__(self, tokens: List[int]) -> str: @dataclass class RNNTBundle: - """torchaudio.prototype.rnnt_pipeline.RNNTBundle() + """torchaudio.prototype.pipelines.RNNTBundle() Dataclass that bundles components for performing automatic speech recognition (ASR, speech-to-text) inference with an RNN-T model. @@ -175,7 +175,7 @@ class RNNTBundle: Example >>> import torchaudio - >>> from torchaudio.prototype.rnnt_pipeline import EMFORMER_RNNT_BASE_LIBRISPEECH + >>> from torchaudio.prototype.pipelines import EMFORMER_RNNT_BASE_LIBRISPEECH >>> import torch >>> >>> # Non-streaming inference. @@ -378,7 +378,7 @@ def get_token_processor(self) -> TokenProcessor: ) EMFORMER_RNNT_BASE_LIBRISPEECH.__doc__ = """Pre-trained Emformer-RNNT-based ASR pipeline capable of performing both streaming and non-streaming inference. - The underlying model is constructed by :py:func:`torchaudio.prototypes.emformer_rnnt_base` + The underlying model is constructed by :py:func:`torchaudio.prototype.models.emformer_rnnt_base` and utilizes weights trained on LibriSpeech using training script ``train.py`` `here `__ with default arguments. From bb528d7eae3bfe3fcdf03401f36c0bf20a499ff6 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:52:59 -0800 Subject: [PATCH 0069/1144] Add Streamer class (#2046) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add `Streamer` class that bundles `StreamProcessor` and handle input. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2045. Pull Request resolved: https://github.com/pytorch/audio/pull/2046 Reviewed By: carolineechen Differential Revision: D33299863 Pulled By: mthrok fbshipit-source-id: 6470cbe061057c8cb970ce7bb5692be04efb5fe9 --- torchaudio/csrc/ffmpeg/streamer.cpp | 260 ++++++++++++++++++++++++++++ torchaudio/csrc/ffmpeg/streamer.h | 102 +++++++++++ 2 files changed, 362 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/streamer.cpp create mode 100644 torchaudio/csrc/ffmpeg/streamer.h diff --git a/torchaudio/csrc/ffmpeg/streamer.cpp b/torchaudio/csrc/ffmpeg/streamer.cpp new file mode 100644 index 0000000000..cd59eca3cb --- /dev/null +++ b/torchaudio/csrc/ffmpeg/streamer.cpp @@ -0,0 +1,260 @@ +#include +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +using KeyType = StreamProcessor::KeyType; + +////////////////////////////////////////////////////////////////////////////// +// Helper methods +////////////////////////////////////////////////////////////////////////////// +void Streamer::validate_open_stream() const { + if (!pFormatContext) + throw std::runtime_error("Stream is not open."); +} + +void Streamer::validate_src_stream_index(int i) const { + validate_open_stream(); + if (i < 0 || i >= static_cast(pFormatContext->nb_streams)) + throw std::out_of_range("Source stream index out of range"); +} + +void Streamer::validate_output_stream_index(int i) const { + if (i < 0 || i >= static_cast(stream_indices.size())) + throw std::out_of_range("Output stream index out of range"); +} + +void Streamer::validate_src_stream_type(int i, AVMediaType type) { + validate_src_stream_index(i); + if (pFormatContext->streams[i]->codecpar->codec_type != type) { + std::ostringstream oss; + oss << "Stream " << i << " is not " << av_get_media_type_string(type) + << " stream."; + throw std::runtime_error(oss.str()); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Initialization / resource allocations +////////////////////////////////////////////////////////////////////////////// +Streamer::Streamer( + const std::string& src, + const std::string& device, + const std::map& option) + : pFormatContext(src, device, option) { + processors = + std::vector>(pFormatContext->nb_streams); + for (int i = 0; i < pFormatContext->nb_streams; ++i) { + switch (pFormatContext->streams[i]->codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + case AVMEDIA_TYPE_VIDEO: + break; + default: + pFormatContext->streams[i]->discard = AVDISCARD_ALL; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Query methods +//////////////////////////////////////////////////////////////////////////////// +int Streamer::num_src_streams() const { + return pFormatContext->nb_streams; +} + +SrcStreamInfo Streamer::get_src_stream_info(int i) const { + validate_src_stream_index(i); + AVStream* stream = pFormatContext->streams[i]; + AVCodecParameters* codecpar = stream->codecpar; + + SrcStreamInfo ret; + ret.media_type = codecpar->codec_type; + ret.bit_rate = codecpar->bit_rate; + const AVCodecDescriptor* desc = avcodec_descriptor_get(codecpar->codec_id); + if (desc) { + ret.codec_name = desc->name; + ret.codec_long_name = desc->long_name; + } + switch (codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + ret.fmt_name = + av_get_sample_fmt_name(static_cast(codecpar->format)); + ret.sample_rate = static_cast(codecpar->sample_rate); + ret.num_channels = codecpar->channels; + break; + case AVMEDIA_TYPE_VIDEO: + ret.fmt_name = + av_get_pix_fmt_name(static_cast(codecpar->format)); + ret.width = codecpar->width; + ret.height = codecpar->height; + ret.frame_rate = av_q2d(stream->r_frame_rate); + break; + default:; + } + return ret; +} + +int Streamer::num_out_streams() const { + return stream_indices.size(); +} + +OutputStreamInfo Streamer::get_out_stream_info(int i) const { + validate_output_stream_index(i); + OutputStreamInfo ret; + int i_src = stream_indices[i].first; + KeyType key = stream_indices[i].second; + ret.source_index = i_src; + ret.filter_description = processors[i_src]->get_filter_description(key); + return ret; +} + +int Streamer::find_best_audio_stream() const { + return av_find_best_stream( + pFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); +} + +int Streamer::find_best_video_stream() const { + return av_find_best_stream( + pFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); +} + +bool Streamer::is_buffer_ready() const { + for (const auto& it : processors) { + if (it && !it->is_buffer_ready()) { + return false; + } + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// Configure methods +//////////////////////////////////////////////////////////////////////////////// +void Streamer::add_audio_stream( + int i, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc) { + add_stream( + i, + AVMEDIA_TYPE_AUDIO, + frames_per_chunk, + num_chunks, + rate, + std::move(filter_desc)); +} + +void Streamer::add_video_stream( + int i, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc) { + add_stream( + i, + AVMEDIA_TYPE_VIDEO, + frames_per_chunk, + num_chunks, + rate, + std::move(filter_desc)); +} + +void Streamer::add_stream( + int i, + AVMediaType media_type, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc) { + validate_src_stream_type(i, media_type); + AVStream* stream = pFormatContext->streams[i]; + stream->discard = AVDISCARD_DEFAULT; + if (!processors[i]) + processors[i] = std::make_unique(stream->codecpar); + int key = processors[i]->add_stream( + stream->time_base, + stream->codecpar, + frames_per_chunk, + num_chunks, + rate, + std::move(filter_desc)); + stream_indices.push_back(std::make_pair<>(i, key)); +} + +void Streamer::remove_stream(int i) { + validate_output_stream_index(i); + auto it = stream_indices.begin() + i; + int iP = it->first; + processors[iP]->remove_stream(it->second); + stream_indices.erase(it); + + // Check if the processor is still refered and if not, disable the processor + bool still_used = false; + for (auto& p : stream_indices) { + still_used |= (iP == p.first); + if (still_used) + break; + } + if (!still_used) + processors[iP].reset(NULL); +} + +//////////////////////////////////////////////////////////////////////////////// +// Stream methods +//////////////////////////////////////////////////////////////////////////////// +// Note +// return value (to be finalized) +// 0: caller should keep calling this function +// 1: It's done, caller should stop calling +// <0: Some error happened +int Streamer::process_packet() { + int ret = av_read_frame(pFormatContext, pPacket); + if (ret == AVERROR_EOF) { + ret = drain(); + return (ret < 0) ? ret : 1; + } + if (ret < 0) + return ret; + AutoPacketUnref packet{pPacket}; + auto& processor = processors[pPacket->stream_index]; + if (!processor) + return 0; + ret = processor->process_packet(packet); + return (ret < 0) ? ret : 0; +} + +// <0: Some error happened. +int Streamer::drain() { + int ret = 0, tmp = 0; + for (auto& p : processors) { + if (p) { + tmp = p->process_packet(NULL); + if (tmp < 0) + ret = tmp; + } + } + return ret; +} + +int Streamer::process_all_packets() { + int ret = 0; + do { + ret = process_packet(); + } while (!ret); + return ret; +} + +std::vector> Streamer::pop_chunks() { + std::vector> ret; + for (auto& i : stream_indices) { + ret.push_back(processors[i.first]->pop_chunk(i.second)); + } + return ret; +} + +} // namespace ffmpeg +} // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/streamer.h b/torchaudio/csrc/ffmpeg/streamer.h new file mode 100644 index 0000000000..612048ef85 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/streamer.h @@ -0,0 +1,102 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +class Streamer { + AVFormatContextPtr pFormatContext; + AVPacketPtr pPacket; + + std::vector> processors; + // Mapping from user-facing stream index to internal index. + // The first one is processor index, + // the second is the map key inside of processor. + std::vector> stream_indices; + + public: + // Open the input and allocate the resource + Streamer( + const std::string& src, + const std::string& device, + const std::map& option); + ~Streamer() = default; + // Non-copyable + Streamer(const Streamer&) = delete; + Streamer& operator=(const Streamer&) = delete; + // Movable + Streamer(Streamer&&) = default; + Streamer& operator=(Streamer&&) = default; + + ////////////////////////////////////////////////////////////////////////////// + // Helper methods + ////////////////////////////////////////////////////////////////////////////// + private: + void validate_open_stream() const; + void validate_src_stream_index(int i) const; + void validate_output_stream_index(int i) const; + void validate_src_stream_type(int i, AVMediaType type); + + ////////////////////////////////////////////////////////////////////////////// + // Query methods + ////////////////////////////////////////////////////////////////////////////// + public: + // Find a suitable audio/video streams using heuristics from ffmpeg + int find_best_audio_stream() const; + int find_best_video_stream() const; + // Fetch information about source streams + int num_src_streams() const; + SrcStreamInfo get_src_stream_info(int i) const; + // Fetch information about output streams + int num_out_streams() const; + OutputStreamInfo get_out_stream_info(int i) const; + // Check if all the buffers of the output streams are ready. + bool is_buffer_ready() const; + + ////////////////////////////////////////////////////////////////////////////// + // Configure methods + ////////////////////////////////////////////////////////////////////////////// + void add_audio_stream( + int i, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc); + void add_video_stream( + int i, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc); + void remove_stream(int i); + + private: + void add_stream( + int i, + AVMediaType media_type, + int frames_per_chunk, + int num_chunks, + double rate, + std::string filter_desc); + + public: + ////////////////////////////////////////////////////////////////////////////// + // Stream methods + ////////////////////////////////////////////////////////////////////////////// + int process_packet(); + int process_all_packets(); + + int drain(); + + ////////////////////////////////////////////////////////////////////////////// + // Retrieval + ////////////////////////////////////////////////////////////////////////////// + std::vector> pop_chunks(); +}; + +} // namespace ffmpeg +} // namespace torchaudio From 896ade04a3837c27964f83f4e0f61c00506c5d43 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Wed, 29 Dec 2021 14:14:20 -0800 Subject: [PATCH 0070/1144] Allow token list as CTC decoder input (#2112) Summary: Additionally accept list of tokens as CTC decoder input. This makes it possible to directly pass in something like `bundles.get_labels()` into the decoder factory function instead of requiring a separate tokens file. Pull Request resolved: https://github.com/pytorch/audio/pull/2112 Reviewed By: hwangjeff, nateanl, mthrok Differential Revision: D33352909 Pulled By: carolineechen fbshipit-source-id: 6d22072e34f6cd7c6f931ce4eaf294ae4cf0c5cc --- .../prototype/ctc_decoder_test.py | 19 ++++++++++++------- torchaudio/csrc/decoder/bindings/pybind.cpp | 1 + .../decoder/src/dictionary/Dictionary.cpp | 9 +++++++++ .../csrc/decoder/src/dictionary/Dictionary.h | 2 ++ .../prototype/ctc_decoder/ctc_decoder.py | 6 +++--- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/test/torchaudio_unittest/prototype/ctc_decoder_test.py b/test/torchaudio_unittest/prototype/ctc_decoder_test.py index a308b04e85..4de2ddab7d 100644 --- a/test/torchaudio_unittest/prototype/ctc_decoder_test.py +++ b/test/torchaudio_unittest/prototype/ctc_decoder_test.py @@ -1,4 +1,5 @@ import torch +from parameterized import parameterized from torchaudio_unittest.common_utils import ( TempDirMixin, TorchaudioTestCase, @@ -9,21 +10,24 @@ @skipIfNoCtcDecoder class CTCDecoderTest(TempDirMixin, TorchaudioTestCase): - def _get_decoder(self): + def _get_decoder(self, tokens=None): from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder lexicon_file = get_asset_path("decoder/lexicon.txt") - tokens_file = get_asset_path("decoder/tokens.txt") kenlm_file = get_asset_path("decoder/kenlm.arpa") + if tokens is None: + tokens = get_asset_path("decoder/tokens.txt") + return kenlm_lexicon_decoder( lexicon=lexicon_file, - tokens=tokens_file, + tokens=tokens, kenlm=kenlm_file, ) - def test_construct_decoder(self): - self._get_decoder() + @parameterized.expand([(get_asset_path("decoder/tokens.txt"),), (["-", "|", "f", "o", "b", "a", "r"],)]) + def test_construct_decoder(self, tokens): + self._get_decoder(tokens) def test_shape(self): B, T, N = 4, 15, 10 @@ -36,9 +40,10 @@ def test_shape(self): self.assertEqual(len(results), B) - def test_index_to_tokens(self): + @parameterized.expand([(get_asset_path("decoder/tokens.txt"),), (["-", "|", "f", "o", "b", "a", "r"],)]) + def test_index_to_tokens(self, tokens): # decoder tokens: '-' '|' 'f' 'o' 'b' 'a' 'r' - decoder = self._get_decoder() + decoder = self._get_decoder(tokens) idxs = torch.LongTensor((1, 2, 1, 3, 5)) tokens = decoder.idxs_to_tokens(idxs) diff --git a/torchaudio/csrc/decoder/bindings/pybind.cpp b/torchaudio/csrc/decoder/bindings/pybind.cpp index 9203797cf1..b49ff31211 100644 --- a/torchaudio/csrc/decoder/bindings/pybind.cpp +++ b/torchaudio/csrc/decoder/bindings/pybind.cpp @@ -225,6 +225,7 @@ PYBIND11_MODULE(_torchaudio_decoder, m) { py::class_(m, "_Dictionary") .def(py::init<>()) + .def(py::init&>(), "tkns"_a) .def(py::init(), "filename"_a) .def("entry_size", &Dictionary::entrySize) .def("index_size", &Dictionary::indexSize) diff --git a/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp b/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp index fe533cb2c4..78d6978ed3 100644 --- a/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp +++ b/torchaudio/csrc/decoder/src/dictionary/Dictionary.cpp @@ -26,6 +26,15 @@ Dictionary::Dictionary(const std::string& filename) { createFromStream(stream); } +Dictionary::Dictionary(const std::vector& tkns) { + for (const auto& tkn : tkns) { + addEntry(tkn); + } + if (!isContiguous()) { + throw std::runtime_error("Invalid dictionary format - not contiguous"); + } +} + void Dictionary::createFromStream(std::istream& stream) { if (!stream) { throw std::runtime_error("Unable to open dictionary input stream."); diff --git a/torchaudio/csrc/decoder/src/dictionary/Dictionary.h b/torchaudio/csrc/decoder/src/dictionary/Dictionary.h index d473694725..74ecf1cf0c 100644 --- a/torchaudio/csrc/decoder/src/dictionary/Dictionary.h +++ b/torchaudio/csrc/decoder/src/dictionary/Dictionary.h @@ -26,6 +26,8 @@ class Dictionary { explicit Dictionary(const std::string& filename); + explicit Dictionary(const std::vector& tkns); + size_t entrySize() const; size_t indexSize() const; diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py index a02a70db17..19aa2d883c 100644 --- a/torchaudio/prototype/ctc_decoder/ctc_decoder.py +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -1,7 +1,7 @@ import itertools as it from collections import namedtuple from typing import Dict -from typing import List, Optional +from typing import List, Optional, Union import torch from torchaudio._torchaudio_decoder import ( @@ -157,7 +157,7 @@ def idxs_to_tokens(self, idxs: torch.LongTensor) -> List: def kenlm_lexicon_decoder( lexicon: str, - tokens: str, + tokens: Union[str, List[str]], kenlm: str, nbest: int = 1, beam_size: int = 50, @@ -177,7 +177,7 @@ def kenlm_lexicon_decoder( Args: lexicon (str): lexicon file containing the possible words - tokens (str): file containing valid tokens + tokens (str or List[str]): file or list containing valid tokens kenlm (str): file containing languge model nbest (int, optional): number of best decodings to return (Default: 1) beam_size (int, optional): max number of hypos to hold after each decode step (Default: 50) From 1ec7ff73d48af07403a74613ed5756b14a2630b6 Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Wed, 29 Dec 2021 15:20:38 -0800 Subject: [PATCH 0071/1144] Add parameter p to TimeMasking (#2090) Summary: Adds parameter `p` to `TimeMasking` to allow for enforcing an upper bound on the proportion of time steps that it can mask. This behavior is consistent with the specifications provided in the SpecAugment paper (https://arxiv.org/abs/1904.08779). Pull Request resolved: https://github.com/pytorch/audio/pull/2090 Reviewed By: carolineechen Differential Revision: D33344772 Pulled By: hwangjeff fbshipit-source-id: 6ff65f5304e489fa1c23e15c3d96b9946229fdcf --- .../functional/functional_impl.py | 27 +++++++--- .../transforms/autograd_test_impl.py | 10 ++++ torchaudio/functional/functional.py | 52 ++++++++++++++++--- torchaudio/transforms.py | 18 ++++--- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/test/torchaudio_unittest/functional/functional_impl.py b/test/torchaudio_unittest/functional/functional_impl.py index 4d088b44e5..553a7cf93c 100644 --- a/test/torchaudio_unittest/functional/functional_impl.py +++ b/test/torchaudio_unittest/functional/functional_impl.py @@ -328,11 +328,17 @@ def test_amplitude_to_DB_top_db_clamp(self, shape): close_to_limit = decibels < 6.0207 assert close_to_limit.any(), f"No values were close to the limit. Did it over-clamp?\n{decibels}" - @parameterized.expand(list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0.0, 30.0], [1, 2]))) - def test_mask_along_axis(self, shape, mask_param, mask_value, axis): + @parameterized.expand( + list(itertools.product([(2, 1025, 400), (1, 201, 100)], [100], [0.0, 30.0], [1, 2], [0.33, 1.0])) + ) + def test_mask_along_axis(self, shape, mask_param, mask_value, axis, p): torch.random.manual_seed(42) specgram = torch.randn(*shape, dtype=self.dtype, device=self.device) - mask_specgram = F.mask_along_axis(specgram, mask_param, mask_value, axis) + + if p != 1.0: + mask_specgram = F.mask_along_axis(specgram, mask_param, mask_value, axis, p=p) + else: + mask_specgram = F.mask_along_axis(specgram, mask_param, mask_value, axis) other_axis = 1 if axis == 2 else 2 @@ -340,21 +346,30 @@ def test_mask_along_axis(self, shape, mask_param, mask_value, axis): num_masked_columns = (masked_columns == mask_specgram.size(other_axis)).sum() num_masked_columns = torch.div(num_masked_columns, mask_specgram.size(0), rounding_mode="floor") + if p != 1.0: + mask_param = min(mask_param, int(specgram.shape[axis] * p)) + assert mask_specgram.size() == specgram.size() assert num_masked_columns < mask_param - @parameterized.expand(list(itertools.product([100], [0.0, 30.0], [2, 3]))) - def test_mask_along_axis_iid(self, mask_param, mask_value, axis): + @parameterized.expand(list(itertools.product([100], [0.0, 30.0], [2, 3], [0.2, 1.0]))) + def test_mask_along_axis_iid(self, mask_param, mask_value, axis, p): torch.random.manual_seed(42) specgrams = torch.randn(4, 2, 1025, 400, dtype=self.dtype, device=self.device) - mask_specgrams = F.mask_along_axis_iid(specgrams, mask_param, mask_value, axis) + if p != 1.0: + mask_specgrams = F.mask_along_axis_iid(specgrams, mask_param, mask_value, axis, p=p) + else: + mask_specgrams = F.mask_along_axis_iid(specgrams, mask_param, mask_value, axis) other_axis = 2 if axis == 3 else 3 masked_columns = (mask_specgrams == mask_value).sum(other_axis) num_masked_columns = (masked_columns == mask_specgrams.size(other_axis)).sum(-1) + if p != 1.0: + mask_param = min(mask_param, int(specgrams.shape[axis] * p)) + assert mask_specgrams.size() == specgrams.size() assert (num_masked_columns < mask_param).sum() == num_masked_columns.numel() diff --git a/test/torchaudio_unittest/transforms/autograd_test_impl.py b/test/torchaudio_unittest/transforms/autograd_test_impl.py index 011d63f877..cafcd3f581 100644 --- a/test/torchaudio_unittest/transforms/autograd_test_impl.py +++ b/test/torchaudio_unittest/transforms/autograd_test_impl.py @@ -168,6 +168,16 @@ def test_masking_iid(self, masking_transform): deterministic_transform = _DeterministicWrapper(masking_transform(400, True)) self.assert_grad(deterministic_transform, [batch]) + def test_time_masking_p(self): + sample_rate = 8000 + n_fft = 400 + spectrogram = get_spectrogram( + get_whitenoise(sample_rate=sample_rate, duration=0.05, n_channels=2), n_fft=n_fft, power=1 + ) + time_mask = T.TimeMasking(400, iid_masks=False, p=0.1) + deterministic_transform = _DeterministicWrapper(time_mask) + self.assert_grad(deterministic_transform, [spectrogram]) + def test_spectral_centroid(self): sample_rate = 8000 transform = T.SpectralCentroid(sample_rate=sample_rate) diff --git a/torchaudio/functional/functional.py b/torchaudio/functional/functional.py index 81b70ad1c0..3b3ef4aa71 100644 --- a/torchaudio/functional/functional.py +++ b/torchaudio/functional/functional.py @@ -704,16 +704,32 @@ def phase_vocoder(complex_specgrams: Tensor, rate: float, phase_advance: Tensor) return complex_specgrams_stretch -def mask_along_axis_iid(specgrams: Tensor, mask_param: int, mask_value: float, axis: int) -> Tensor: +def _get_mask_param(mask_param: int, p: float, axis_length: int) -> int: + if p == 1.0: + return mask_param + else: + return min(mask_param, int(axis_length * p)) + + +def mask_along_axis_iid( + specgrams: Tensor, + mask_param: int, + mask_value: float, + axis: int, + p: float = 1.0, +) -> Tensor: r""" Apply a mask along ``axis``. Mask will be applied from indices ``[v_0, v_0 + v)``, where - ``v`` is sampled from ``uniform(0, mask_param)``, and ``v_0`` from ``uniform(0, max_v - v)``. + ``v`` is sampled from ``uniform(0, max_v)`` and ``v_0`` from ``uniform(0, specgrams.size(axis) - v)``, with + ``max_v = mask_param`` when ``p = 1.0`` and ``max_v = min(mask_param, floor(specgrams.size(axis) * p))`` + otherwise. Args: specgrams (Tensor): Real spectrograms `(batch, channel, freq, time)` mask_param (int): Number of columns to be masked will be uniformly sampled from [0, mask_param] mask_value (float): Value to assign to the masked columns axis (int): Axis to apply masking on (2 -> frequency, 3 -> time) + p (float, optional): maximum proportion of columns that can be masked. (Default: 1.0) Returns: Tensor: Masked spectrograms of dimensions `(batch, channel, freq, time)` @@ -722,6 +738,13 @@ def mask_along_axis_iid(specgrams: Tensor, mask_param: int, mask_value: float, a if axis not in [2, 3]: raise ValueError("Only Frequency and Time masking are supported") + if not 0.0 <= p <= 1.0: + raise ValueError(f"The value of p must be between 0.0 and 1.0 ({p} given).") + + mask_param = _get_mask_param(mask_param, p, specgrams.shape[axis]) + if mask_param < 1: + return specgrams + device = specgrams.device dtype = specgrams.dtype @@ -729,8 +752,8 @@ def mask_along_axis_iid(specgrams: Tensor, mask_param: int, mask_value: float, a min_value = torch.rand(specgrams.shape[:2], device=device, dtype=dtype) * (specgrams.size(axis) - value) # Create broadcastable mask - mask_start = min_value[..., None, None] - mask_end = (min_value + value)[..., None, None] + mask_start = min_value.long()[..., None, None] + mask_end = (min_value.long() + value.long())[..., None, None] mask = torch.arange(0, specgrams.size(axis), device=device, dtype=dtype) # Per batch example masking @@ -741,17 +764,25 @@ def mask_along_axis_iid(specgrams: Tensor, mask_param: int, mask_value: float, a return specgrams -def mask_along_axis(specgram: Tensor, mask_param: int, mask_value: float, axis: int) -> Tensor: +def mask_along_axis( + specgram: Tensor, + mask_param: int, + mask_value: float, + axis: int, + p: float = 1.0, +) -> Tensor: r""" Apply a mask along ``axis``. Mask will be applied from indices ``[v_0, v_0 + v)``, where - ``v`` is sampled from ``uniform(0, mask_param)``, and ``v_0`` from ``uniform(0, max_v - v)``. - All examples will have the same mask interval. + ``v`` is sampled from ``uniform(0, max_v)`` and ``v_0`` from ``uniform(0, specgrams.size(axis) - v)``, with + ``max_v = mask_param`` when ``p = 1.0`` and ``max_v = min(mask_param, floor(specgrams.size(axis) * p))`` + otherwise. All examples will have the same mask interval. Args: specgram (Tensor): Real spectrogram `(channel, freq, time)` mask_param (int): Number of columns to be masked will be uniformly sampled from [0, mask_param] mask_value (float): Value to assign to the masked columns axis (int): Axis to apply masking on (1 -> frequency, 2 -> time) + p (float, optional): maximum proportion of columns that can be masked. (Default: 1.0) Returns: Tensor: Masked spectrogram of dimensions `(channel, freq, time)` @@ -759,6 +790,13 @@ def mask_along_axis(specgram: Tensor, mask_param: int, mask_value: float, axis: if axis not in [1, 2]: raise ValueError("Only Frequency and Time masking are supported") + if not 0.0 <= p <= 1.0: + raise ValueError(f"The value of p must be between 0.0 and 1.0 ({p} given).") + + mask_param = _get_mask_param(mask_param, p, specgram.shape[axis]) + if mask_param < 1: + return specgram + # pack batch shape = specgram.size() specgram = specgram.reshape([-1] + list(shape[-2:])) diff --git a/torchaudio/transforms.py b/torchaudio/transforms.py index 5c8ae79505..81b409e408 100644 --- a/torchaudio/transforms.py +++ b/torchaudio/transforms.py @@ -1110,15 +1110,17 @@ class _AxisMasking(torch.nn.Module): axis (int): What dimension the mask is applied on. iid_masks (bool): Applies iid masks to each of the examples in the batch dimension. This option is applicable only when the input tensor is 4D. + p (float, optional): maximum proportion of columns that can be masked. (Default: 1.0) """ - __constants__ = ["mask_param", "axis", "iid_masks"] + __constants__ = ["mask_param", "axis", "iid_masks", "p"] - def __init__(self, mask_param: int, axis: int, iid_masks: bool) -> None: + def __init__(self, mask_param: int, axis: int, iid_masks: bool, p: float = 1.0) -> None: super(_AxisMasking, self).__init__() self.mask_param = mask_param self.axis = axis self.iid_masks = iid_masks + self.p = p def forward(self, specgram: Tensor, mask_value: float = 0.0) -> Tensor: r""" @@ -1131,9 +1133,9 @@ def forward(self, specgram: Tensor, mask_value: float = 0.0) -> Tensor: """ # if iid_masks flag marked and specgram has a batch dimension if self.iid_masks and specgram.dim() == 4: - return F.mask_along_axis_iid(specgram, self.mask_param, mask_value, self.axis + 1) + return F.mask_along_axis_iid(specgram, self.mask_param, mask_value, self.axis + 1, p=self.p) else: - return F.mask_along_axis(specgram, self.mask_param, mask_value, self.axis) + return F.mask_along_axis(specgram, self.mask_param, mask_value, self.axis, p=self.p) class FrequencyMasking(_AxisMasking): @@ -1177,6 +1179,8 @@ class TimeMasking(_AxisMasking): iid_masks (bool, optional): whether to apply different masks to each example/channel in the batch. (Default: ``False``) This option is applicable only when the input tensor is 4D. + p (float, optional): maximum proportion of time steps that can be masked. + Must be within range [0.0, 1.0]. (Default: 1.0) Example >>> spectrogram = torchaudio.transforms.Spectrogram() @@ -1192,8 +1196,10 @@ class TimeMasking(_AxisMasking): :alt: The spectrogram masked along time axis """ - def __init__(self, time_mask_param: int, iid_masks: bool = False) -> None: - super(TimeMasking, self).__init__(time_mask_param, 2, iid_masks) + def __init__(self, time_mask_param: int, iid_masks: bool = False, p: float = 1.0) -> None: + if not 0.0 <= p <= 1.0: + raise ValueError(f"The value of p must be between 0.0 and 1.0 ({p} given).") + super(TimeMasking, self).__init__(time_mask_param, 2, iid_masks, p=p) class Vol(torch.nn.Module): From 3de7892e4c73495198ec2c23caf4eb866a97e1e9 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:52:49 -0800 Subject: [PATCH 0072/1144] Add ffmpeg prototype bindings (#2047) Summary: Part of https://github.com/pytorch/audio/issues/1986. Splitting the PR for easier review. Add `Streamer` TorchBind. For the overall architecture, see https://github.com/mthrok/audio/blob/ffmpeg/torchaudio/csrc/ffmpeg/README.md. Note: Without a change to build process, the code added here won't be compiled. The build process will be updated later. Needs to be imported after https://github.com/pytorch/audio/issues/2046. Pull Request resolved: https://github.com/pytorch/audio/pull/2047 Reviewed By: hwangjeff Differential Revision: D33355190 Pulled By: mthrok fbshipit-source-id: a3ad4c2822ed3a7ddc19b1aaca9dddabd59ce2f8 --- torchaudio/csrc/ffmpeg/prototype.cpp | 329 +++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/prototype.cpp diff --git a/torchaudio/csrc/ffmpeg/prototype.cpp b/torchaudio/csrc/ffmpeg/prototype.cpp new file mode 100644 index 0000000000..e1c94b32ab --- /dev/null +++ b/torchaudio/csrc/ffmpeg/prototype.cpp @@ -0,0 +1,329 @@ +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +namespace { + +std::map convert_dict( + const c10::optional>& option) { + std::map opts; + if (option) { + for (auto& it : option.value()) { + opts[it.key()] = it.value(); + } + } + return opts; +} + +struct StreamerHolder : torch::CustomClassHolder { + Streamer s; + StreamerHolder( + const std::string& src, + c10::optional device, + c10::optional> option) + : s(src, device.value_or(""), convert_dict(option)) {} +}; + +using S = c10::intrusive_ptr; + +S init( + const std::string& src, + c10::optional device, + c10::optional> option) { + return c10::make_intrusive(src, device, option); +} + +using SrcInfo = std::tuple< + std::string, // media_type + std::string, // codec name + std::string, // codec long name + std::string, // format name + int64_t, // bit_rate + // Audio + double, // sample_rate + int64_t, // num_channels + // Video + int64_t, // width + int64_t, // height + double // frame_rate + >; + +SrcInfo convert(SrcStreamInfo ssi) { + return SrcInfo(std::forward_as_tuple( + av_get_media_type_string(ssi.media_type), + ssi.codec_name, + ssi.codec_long_name, + ssi.fmt_name, + ssi.bit_rate, + ssi.sample_rate, + ssi.num_channels, + ssi.width, + ssi.height, + ssi.frame_rate)); +} + +SrcInfo get_src_stream_info(S s, int64_t i) { + return convert(s->s.get_src_stream_info(i)); +} + +using OutInfo = std::tuple< + int64_t, // source index + std::string // filter description + >; + +OutInfo convert(OutputStreamInfo osi) { + return OutInfo( + std::forward_as_tuple(osi.source_index, osi.filter_description)); +} + +OutInfo get_out_stream_info(S s, int64_t i) { + return convert(s->s.get_out_stream_info(i)); +} + +int64_t num_src_streams(S s) { + return s->s.num_src_streams(); +} + +int64_t num_out_streams(S s) { + return s->s.num_out_streams(); +} + +int64_t find_best_audio_stream(S s) { + return s->s.find_best_audio_stream(); +} + +int64_t find_best_video_stream(S s) { + return s->s.find_best_video_stream(); +} + +template +std::string string_format(const std::string& format, Args... args) { + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), format.c_str(), args...); + return std::string(buffer); +} + +std::string join( + const std::vector& components, + const std::string& delim) { + std::ostringstream s; + for (int i = 0; i < components.size(); ++i) { + if (i) + s << delim; + s << components[i]; + } + return s.str(); +} +std::string get_afilter_desc( + const c10::optional& sample_rate, + const c10::optional& dtype) { + std::vector components; + if (sample_rate) { + // TODO: test float sample rate + components.emplace_back( + string_format("aresample=%d", static_cast(sample_rate.value()))); + } + if (dtype) { + AVSampleFormat fmt = [&]() { + switch (dtype.value()) { + case c10::ScalarType::Byte: + return AV_SAMPLE_FMT_U8P; + case c10::ScalarType::Short: + return AV_SAMPLE_FMT_S16P; + case c10::ScalarType::Int: + return AV_SAMPLE_FMT_S32P; + case c10::ScalarType::Long: + return AV_SAMPLE_FMT_S64P; + case c10::ScalarType::Float: + return AV_SAMPLE_FMT_FLTP; + case c10::ScalarType::Double: + return AV_SAMPLE_FMT_DBLP; + default: + throw std::runtime_error("Unexpected dtype."); + } + }(); + components.emplace_back( + string_format("aformat=sample_fmts=%s", av_get_sample_fmt_name(fmt))); + } + return join(components, ","); +} +std::string get_vfilter_desc( + const c10::optional& frame_rate, + const c10::optional& width, + const c10::optional& height, + const c10::optional& format) { + // TODO: + // - Add `flags` for different scale algorithm + // https://ffmpeg.org/ffmpeg-filters.html#scale + // - Consider `framerate` as well + // https://ffmpeg.org/ffmpeg-filters.html#framerate + + // - scale + // https://ffmpeg.org/ffmpeg-filters.html#scale-1 + // https://ffmpeg.org/ffmpeg-scaler.html#toc-Scaler-Options + // - framerate + // https://ffmpeg.org/ffmpeg-filters.html#framerate + + // TODO: + // - format + // https://ffmpeg.org/ffmpeg-filters.html#toc-format-1 + // - fps + // https://ffmpeg.org/ffmpeg-filters.html#fps-1 + std::vector components; + if (frame_rate) + components.emplace_back(string_format("fps=%lf", frame_rate.value())); + + std::vector scale_components; + if (width) + scale_components.emplace_back(string_format("width=%d", width.value())); + if (height) + scale_components.emplace_back(string_format("height=%d", height.value())); + if (scale_components.size()) + components.emplace_back( + string_format("scale=%s", join(scale_components, ":").c_str())); + if (format) { + // TODO: + // Check other useful formats + // https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes + AVPixelFormat fmt = [&]() { + std::string val = format.value(); + if (val == "RGB") + return AV_PIX_FMT_RGB24; + if (val == "BGR") + return AV_PIX_FMT_BGR24; + if (val == "GRAY") + return AV_PIX_FMT_GRAY8; + throw std::runtime_error("Unexpected format: " + val); + }(); + components.emplace_back( + string_format("format=pix_fmts=%s", av_get_pix_fmt_name(fmt))); + } + return join(components, ","); +}; + +void add_basic_audio_stream( + S s, + int64_t i, + int64_t frames_per_chunk, + int64_t num_chunks, + const c10::optional& sample_rate, + const c10::optional& dtype) { + std::string filter_desc = get_afilter_desc(sample_rate, dtype); + s->s.add_audio_stream( + i, frames_per_chunk, num_chunks, sample_rate.value_or(-1), filter_desc); +} + +void add_basic_video_stream( + S s, + int64_t i, + int64_t frames_per_chunk, + int64_t num_chunks, + const c10::optional& frame_rate, + const c10::optional& width, + const c10::optional& height, + const c10::optional& format) { + std::string filter_desc = get_vfilter_desc(frame_rate, width, height, format); + s->s.add_video_stream( + i, frames_per_chunk, num_chunks, frame_rate.value_or(-1), filter_desc); +} + +void add_audio_stream( + S s, + int64_t i, + int64_t frames_per_chunk, + int64_t num_chunks, + const c10::optional& sample_rate, + const c10::optional& filter_desc) { + s->s.add_audio_stream( + i, + frames_per_chunk, + num_chunks, + sample_rate.value_or(-1), + filter_desc.value_or("")); +} + +void add_video_stream( + S s, + int64_t i, + int64_t frames_per_chunk, + int64_t num_chunks, + const c10::optional& frame_rate, + const c10::optional& filter_desc) { + s->s.add_video_stream( + i, + frames_per_chunk, + num_chunks, + frame_rate.value_or(-1), + filter_desc.value_or("")); +} + +void remove_stream(S s, int64_t i) { + s->s.remove_stream(i); +} + +int64_t process_packet(S s) { + return s->s.process_packet(); +} + +int64_t process_all_packets(S s) { + return s->s.process_all_packets(); +} + +bool is_buffer_ready(S s) { + return s->s.is_buffer_ready(); +} + +std::vector> pop_chunks(S s) { + return s->s.pop_chunks(); +} + +std::tuple, int64_t> load(const std::string& src) { + Streamer s{src, "", {}}; + int i = s.find_best_audio_stream(); + auto sinfo = s.get_src_stream_info(i); + int64_t sample_rate = static_cast(sinfo.sample_rate); + s.add_audio_stream(i, -1, -1, 1, ""); + s.process_all_packets(); + auto tensors = s.pop_chunks(); + return std::make_tuple<>(tensors[0], sample_rate); +} + +TORCH_LIBRARY_FRAGMENT(torchaudio, m) { + m.def("torchaudio::ffmpeg_init", []() { + avdevice_register_all(); + if (av_log_get_level() == AV_LOG_INFO) + av_log_set_level(AV_LOG_ERROR); + }); + m.def("torchaudio::ffmpeg_load", load); + m.class_("ffmpeg_Streamer"); + m.def("torchaudio::ffmpeg_streamer_init", init); + m.def("torchaudio::ffmpeg_streamer_num_src_streams", num_src_streams); + m.def("torchaudio::ffmpeg_streamer_num_out_streams", num_out_streams); + m.def("torchaudio::ffmpeg_streamer_get_src_stream_info", get_src_stream_info); + m.def("torchaudio::ffmpeg_streamer_get_out_stream_info", get_out_stream_info); + m.def( + "torchaudio::ffmpeg_streamer_find_best_audio_stream", + find_best_audio_stream); + m.def( + "torchaudio::ffmpeg_streamer_find_best_video_stream", + find_best_video_stream); + m.def( + "torchaudio::ffmpeg_streamer_add_basic_audio_stream", + add_basic_audio_stream); + m.def( + "torchaudio::ffmpeg_streamer_add_basic_video_stream", + add_basic_video_stream); + m.def("torchaudio::ffmpeg_streamer_add_audio_stream", add_audio_stream); + m.def("torchaudio::ffmpeg_streamer_add_video_stream", add_video_stream); + m.def("torchaudio::ffmpeg_streamer_remove_stream", remove_stream); + m.def("torchaudio::ffmpeg_streamer_process_packet", process_packet); + m.def("torchaudio::ffmpeg_streamer_process_all_packets", process_all_packets); + m.def("torchaudio::ffmpeg_streamer_is_buffer_ready", is_buffer_ready); + m.def("torchaudio::ffmpeg_streamer_pop_chunks", pop_chunks); +} + +} // namespace +} // namespace ffmpeg +} // namespace torchaudio From fd3c957399d85c003d5d80859f8153b8e8a7f0c1 Mon Sep 17 00:00:00 2001 From: CodemodService Bot <> Date: Thu, 30 Dec 2021 03:47:15 -0800 Subject: [PATCH 0073/1144] [Codemod][FBSourceBlackLinter] Daily `arc lint --take BLACK` Reviewed By: zertosh Differential Revision: D33361077 fbshipit-source-id: 007db010bd38c28f597ea66f68f97b13309e878c --- torchaudio/prototype/models/rnnt.py | 1 - torchaudio/prototype/models/rnnt_decoder.py | 1 - 2 files changed, 2 deletions(-) diff --git a/torchaudio/prototype/models/rnnt.py b/torchaudio/prototype/models/rnnt.py index c35025f8a9..59e9969479 100644 --- a/torchaudio/prototype/models/rnnt.py +++ b/torchaudio/prototype/models/rnnt.py @@ -1,7 +1,6 @@ from typing import List, Optional, Tuple import torch - from torchaudio.prototype.models import Emformer diff --git a/torchaudio/prototype/models/rnnt_decoder.py b/torchaudio/prototype/models/rnnt_decoder.py index 1f19c06679..d9478eddd2 100644 --- a/torchaudio/prototype/models/rnnt_decoder.py +++ b/torchaudio/prototype/models/rnnt_decoder.py @@ -1,7 +1,6 @@ from typing import Callable, Dict, List, Optional, NamedTuple, Tuple import torch - from torchaudio.prototype.models import RNNT From 9cb75e7410b4497e9a15f66d84b8a541ecbe9803 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 30 Dec 2021 06:34:18 -0800 Subject: [PATCH 0074/1144] Update and fill the rest of ffmpeg-integration C++ code (#2113) Summary: - Introduce AudioBuffer and VideoBuffer for different way of handling frames - Update the way option dictionary is passed - Remove unused AutoFrameUnref - Add SrcStreamInfo/OutputStreamInfo classes Pull Request resolved: https://github.com/pytorch/audio/pull/2113 Reviewed By: nateanl Differential Revision: D33356144 Pulled By: mthrok fbshipit-source-id: e837e84fae48baa7befd5c70599bcd2cbb61514d --- torchaudio/csrc/ffmpeg/buffer.cpp | 164 ++++++++++++++++++++---- torchaudio/csrc/ffmpeg/buffer.h | 74 ++++++++++- torchaudio/csrc/ffmpeg/ffmpeg.cpp | 25 ++-- torchaudio/csrc/ffmpeg/ffmpeg.h | 15 +-- torchaudio/csrc/ffmpeg/filter_graph.cpp | 7 + torchaudio/csrc/ffmpeg/filter_graph.h | 8 +- torchaudio/csrc/ffmpeg/prototype.cpp | 5 + torchaudio/csrc/ffmpeg/streamer.cpp | 8 ++ torchaudio/csrc/ffmpeg/streamer.h | 2 + torchaudio/csrc/ffmpeg/typedefs.h | 32 +++++ 10 files changed, 283 insertions(+), 57 deletions(-) create mode 100644 torchaudio/csrc/ffmpeg/typedefs.h diff --git a/torchaudio/csrc/ffmpeg/buffer.cpp b/torchaudio/csrc/ffmpeg/buffer.cpp index 72fd6c8118..1564985327 100644 --- a/torchaudio/csrc/ffmpeg/buffer.cpp +++ b/torchaudio/csrc/ffmpeg/buffer.cpp @@ -5,8 +5,27 @@ namespace torchaudio { namespace ffmpeg { -Buffer::Buffer(AVMediaType type) : media_type(type) {} +Buffer::Buffer(int frames_per_chunk, int num_chunks) + : frames_per_chunk(frames_per_chunk), num_chunks(num_chunks) {} +AudioBuffer::AudioBuffer(int frames_per_chunk, int num_chunks) + : Buffer(frames_per_chunk, num_chunks) {} + +VideoBuffer::VideoBuffer(int frames_per_chunk, int num_chunks) + : Buffer(frames_per_chunk, num_chunks) {} + +//////////////////////////////////////////////////////////////////////////////// +// Query +//////////////////////////////////////////////////////////////////////////////// +bool Buffer::is_ready() const { + if (frames_per_chunk < 0) + return num_buffered_frames > 0; + return num_buffered_frames >= frames_per_chunk; +} + +//////////////////////////////////////////////////////////////////////////////// +// Modifiers - Push Audio +//////////////////////////////////////////////////////////////////////////////// namespace { torch::Tensor convert_audio_tensor(AVFrame* pFrame) { // ref: https://ffmpeg.org/doxygen/4.1/filter__audio_8c_source.html#l00215 @@ -82,10 +101,64 @@ torch::Tensor convert_audio_tensor(AVFrame* pFrame) { } } // namespace -void Buffer::push_audio_frame(AVFrame* pFrame) { - chunks.push_back(convert_audio_tensor(pFrame)); +void AudioBuffer::push_tensor(torch::Tensor t) { + // If frames_per_chunk < 0, users want to fetch all frames. + // Just push back to chunks and that's it. + if (frames_per_chunk < 0) { + chunks.push_back(t); + num_buffered_frames += t.size(0); + return; + } + + // Push + // Note: + // For audio, the incoming tensor contains multiple of samples. + // For small `frames_per_chunk` value, it might be more than `max_frames`. + // If we push the tensor as-is, then, the whole frame might be popped at + // trimming stage, resulting buffer always empty. So we slice push the + // incoming Tensor. + + // Check the last inserted Tensor and if the numbe of frames is not + // frame_per_chunk, reprocess it again with the incomping tensor + if (num_buffered_frames % frames_per_chunk) { + torch::Tensor prev = chunks.back(); + chunks.pop_back(); + num_buffered_frames -= prev.size(0); + t = torch::cat({prev, t}, 0); + } + + while (true) { + int num_input_frames = t.size(0); + if (num_input_frames <= frames_per_chunk) { + chunks.push_back(t); + num_buffered_frames += num_input_frames; + break; + } + // The input tensor contains more frames than frames_per_chunk + auto splits = torch::tensor_split(t, {frames_per_chunk, num_input_frames}); + chunks.push_back(splits[0]); + num_buffered_frames += frames_per_chunk; + t = splits[1]; + } + + // Trim + // If frames_per_chunk > 0, we only retain the following number of frames and + // Discard older frames. + int max_frames = num_chunks * frames_per_chunk; + while (num_buffered_frames > max_frames) { + torch::Tensor& t = chunks.front(); + num_buffered_frames -= t.size(0); + chunks.pop_front(); + } +} + +void AudioBuffer::push_frame(AVFrame* frame) { + push_tensor(convert_audio_tensor(frame)); } +//////////////////////////////////////////////////////////////////////////////// +// Modifiers - Push Video +//////////////////////////////////////////////////////////////////////////////// namespace { torch::Tensor convert_image_tensor(AVFrame* pFrame) { // ref: @@ -130,34 +203,79 @@ torch::Tensor convert_image_tensor(AVFrame* pFrame) { } } // namespace -void Buffer::push_video_frame(AVFrame* pFrame) { - chunks.push_back(convert_image_tensor(pFrame)); +void VideoBuffer::push_tensor(torch::Tensor t) { + // the video frames is expected to contain only one frame + chunks.push_back(t); + num_buffered_frames += t.size(0); + + if (frames_per_chunk < 0) { + return; + } + + // Trim + int max_frames = num_chunks * frames_per_chunk; + if (num_buffered_frames > max_frames) { + torch::Tensor& t = chunks.front(); + num_buffered_frames -= t.size(0); + chunks.pop_front(); + } } -torch::Tensor Buffer::pop_all() { - if (!chunks.size()) - return torch::empty({}); +void VideoBuffer::push_frame(AVFrame* frame) { + push_tensor(convert_image_tensor(frame)); +} - std::vector tmp; - while (chunks.size()) { - tmp.push_back(chunks.front()); +//////////////////////////////////////////////////////////////////////////////// +// Modifiers - Pop +//////////////////////////////////////////////////////////////////////////////// + +using namespace torch::indexing; + +c10::optional Buffer::pop_chunk() { + if (!num_buffered_frames) { + return c10::optional{}; + } + if (frames_per_chunk < 0) { + return c10::optional{pop_all()}; + } + return c10::optional{pop_one_chunk()}; +} + +torch::Tensor AudioBuffer::pop_one_chunk() { + // Audio deque are aligned with `frames_per_chunk` + torch::Tensor ret = chunks.front(); + chunks.pop_front(); + num_buffered_frames -= ret.size(0); + return ret; +} + +torch::Tensor VideoBuffer::pop_one_chunk() { + // Video deque contains one frame par one tensor + std::vector ret; + while (num_buffered_frames > 0 && ret.size() < frames_per_chunk) { + torch::Tensor& t = chunks.front(); + ret.push_back(t); chunks.pop_front(); + num_buffered_frames -= 1; } - return torch::cat(tmp, 0); + return torch::cat(ret, 0); } -void Buffer::push_frame(AVFrame* frame) { - switch (media_type) { - case AVMEDIA_TYPE_AUDIO: - push_audio_frame(frame); - break; - case AVMEDIA_TYPE_VIDEO: - push_video_frame(frame); - break; - default: - throw std::runtime_error( - "Unexpected media type. Only audio/video is supported."); +torch::Tensor Buffer::pop_all() { + // Note: + // This method is common to audio/video. + // In audio case, each Tensor contains multiple frames + // In video case, each Tensor contains one frame, + std::vector ret; + while (chunks.size()) { + torch::Tensor& t = chunks.front(); + int n_frames = t.size(0); + ret.push_back(t); + chunks.pop_front(); + num_buffered_frames -= n_frames; } + return torch::cat(ret, 0); } + } // namespace ffmpeg } // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/buffer.h b/torchaudio/csrc/ffmpeg/buffer.h index 8e570c2069..3e286b5f15 100644 --- a/torchaudio/csrc/ffmpeg/buffer.h +++ b/torchaudio/csrc/ffmpeg/buffer.h @@ -7,18 +7,82 @@ namespace torchaudio { namespace ffmpeg { class Buffer { + protected: + // Each AVFrame is converted to a Tensor and stored here. std::deque chunks; - AVMediaType media_type; - void push_audio_frame(AVFrame* pFrame); - void push_video_frame(AVFrame* pFrame); + // The number of frames to return as a chunk + // If <0, then user wants to receive all the frames + const int frames_per_chunk; + // The numbe of chunks to retain + const int num_chunks; + // The number of currently stored chunks + // For video, one Tensor corresponds to one frame, but for audio, + // one Tensor contains multiple samples, so we track here. + int num_buffered_frames = 0; public: - Buffer(AVMediaType type); + Buffer(int frames_per_chunk, int num_chunks); + virtual ~Buffer() = default; - void push_frame(AVFrame* pFrame); + ////////////////////////////////////////////////////////////////////////////// + // Query + ////////////////////////////////////////////////////////////////////////////// + // Check if buffeer has enoough number of frames for a chunk + // If frame_per_chunk <0, returns true if there is >0 frames. + // Otherwise, returns if num_frames >= frame_per_chunk. + bool is_ready() const; + + ////////////////////////////////////////////////////////////////////////////// + // Modifiers + ////////////////////////////////////////////////////////////////////////////// + virtual void push_frame(AVFrame* frame) = 0; + + c10::optional pop_chunk(); + + private: + virtual torch::Tensor pop_one_chunk() = 0; torch::Tensor pop_all(); }; +// Specialization of the handling around push/pop for audio/video. + +//////////////////////////////////////////////////////////////////////////////// +// AudioBuffer specialization +//////////////////////////////////////////////////////////////////////////////// +// For audio, input AVFrame contains multiple frames. +// When popping the buffered frames chunk-by-chunk, it is easier if they are +// organized by chunk when pushed to deque object. +// Therefore, audio implements pushing mechanism that makes sure that +// each Tensor in deque consists Tensors with `frames_per_chunk` frames. +class AudioBuffer : public Buffer { + public: + AudioBuffer(int frames_per_chunk, int num_chunks); + + void push_frame(AVFrame* frame); + + private: + void push_tensor(torch::Tensor tensor); + torch::Tensor pop_one_chunk(); +}; + +//////////////////////////////////////////////////////////////////////////////// +// VideoBuffer specialization +//////////////////////////////////////////////////////////////////////////////// +// For video, input AVFrame contains one frame. +// Contraty to audio, it is simple to push one frame each time to deque. +// But this mean that chunks consisting of multiple frames have to be created +// at popping time. +class VideoBuffer : public Buffer { + public: + VideoBuffer(int frames_per_chunk, int num_chunks); + + void push_frame(AVFrame* frame); + + private: + void push_tensor(torch::Tensor tensor); + torch::Tensor pop_one_chunk(); +}; + } // namespace ffmpeg } // namespace torchaudio diff --git a/torchaudio/csrc/ffmpeg/ffmpeg.cpp b/torchaudio/csrc/ffmpeg/ffmpeg.cpp index ed434dc0aa..a679346e3a 100644 --- a/torchaudio/csrc/ffmpeg/ffmpeg.cpp +++ b/torchaudio/csrc/ffmpeg/ffmpeg.cpp @@ -14,12 +14,20 @@ namespace { AVFormatContext* get_format_context( const std::string& src, const std::string& device, - AVDictionary** option) { + const std::map& option) { AVFormatContext* pFormat = NULL; AVInputFormat* pInput = device.empty() ? NULL : av_find_input_format(device.c_str()); - if (avformat_open_input(&pFormat, src.c_str(), pInput, option) < 0) + AVDictionary* dict = NULL; + for (auto& it : option) { + av_dict_set(&dict, it.first.c_str(), it.second.c_str(), 0); + } + + int ret = avformat_open_input(&pFormat, src.c_str(), pInput, &dict); + av_dict_free(&dict); + + if (ret < 0) throw std::runtime_error("Failed to open the input: " + src); return pFormat; } @@ -28,7 +36,7 @@ AVFormatContext* get_format_context( AVFormatContextPtr::AVFormatContextPtr( const std::string& src, const std::string& device, - AVDictionary** option) + const std::map& option) : Wrapper( get_format_context(src, device, option)) { if (avformat_find_stream_info(ptr.get(), NULL) < 0) @@ -82,17 +90,6 @@ AVFrame* get_av_frame() { AVFramePtr::AVFramePtr() : Wrapper(get_av_frame()) {} -/////////////////////////////////////////////////////////////////////////////// -// AVFrame - buffer unref -//////////////////////////////////////////////////////////////////////////////// -AutoFrameUnref::AutoFrameUnref(AVFramePtr& p) : p_(p){}; -AutoFrameUnref::~AutoFrameUnref() { - av_frame_unref(p_); -} -AutoFrameUnref::operator AVFrame*() const { - return p_; -} - //////////////////////////////////////////////////////////////////////////////// // AVCodecContext //////////////////////////////////////////////////////////////////////////////// diff --git a/torchaudio/csrc/ffmpeg/ffmpeg.h b/torchaudio/csrc/ffmpeg/ffmpeg.h index da058e33c8..ba03f5c560 100644 --- a/torchaudio/csrc/ffmpeg/ffmpeg.h +++ b/torchaudio/csrc/ffmpeg/ffmpeg.h @@ -1,6 +1,7 @@ // One stop header for all ffmepg needs #pragma once #include +#include #include #include @@ -58,7 +59,7 @@ struct AVFormatContextPtr AVFormatContextPtr( const std::string& src, const std::string& device, - AVDictionary** option); + const std::map& option); }; //////////////////////////////////////////////////////////////////////////////// @@ -101,18 +102,6 @@ struct AVFramePtr : public Wrapper { AVFramePtr(); }; -//////////////////////////////////////////////////////////////////////////////// -// AVFrame - buffer unref -//////////////////////////////////////////////////////////////////////////////// -// Similar to `AutoPacketUnref`, this structure will release the memory -// allocated for frame content. -struct AutoFrameUnref { - AVFramePtr& p_; - AutoFrameUnref(AVFramePtr& p); - ~AutoFrameUnref(); - operator AVFrame*() const; -}; - //////////////////////////////////////////////////////////////////////////////// // AVCodecContext //////////////////////////////////////////////////////////////////////////////// diff --git a/torchaudio/csrc/ffmpeg/filter_graph.cpp b/torchaudio/csrc/ffmpeg/filter_graph.cpp index 9d5155158b..cd3251fcf6 100644 --- a/torchaudio/csrc/ffmpeg/filter_graph.cpp +++ b/torchaudio/csrc/ffmpeg/filter_graph.cpp @@ -15,6 +15,13 @@ FilterGraph::FilterGraph( create_filter(); } +//////////////////////////////////////////////////////////////////////////////// +// Query method +//////////////////////////////////////////////////////////////////////////////// +std::string FilterGraph::get_description() const { + return filter_description; +}; + //////////////////////////////////////////////////////////////////////////////// // Configuration methods //////////////////////////////////////////////////////////////////////////////// diff --git a/torchaudio/csrc/ffmpeg/filter_graph.h b/torchaudio/csrc/ffmpeg/filter_graph.h index d601f63b26..065cee3f4e 100644 --- a/torchaudio/csrc/ffmpeg/filter_graph.h +++ b/torchaudio/csrc/ffmpeg/filter_graph.h @@ -11,10 +11,9 @@ class FilterGraph { // so we do not manage the resource. AVFilterContext* buffersrc_ctx = nullptr; AVFilterContext* buffersink_ctx = nullptr; - - public: const std::string filter_description; + public: FilterGraph( AVRational time_base, AVCodecParameters* codecpar, @@ -28,6 +27,11 @@ class FilterGraph { FilterGraph(FilterGraph&&) = default; FilterGraph& operator=(FilterGraph&&) = default; + ////////////////////////////////////////////////////////////////////////////// + // Query method + ////////////////////////////////////////////////////////////////////////////// + std::string get_description() const; + ////////////////////////////////////////////////////////////////////////////// // Configuration methods ////////////////////////////////////////////////////////////////////////////// diff --git a/torchaudio/csrc/ffmpeg/prototype.cpp b/torchaudio/csrc/ffmpeg/prototype.cpp index e1c94b32ab..7ff9415c71 100644 --- a/torchaudio/csrc/ffmpeg/prototype.cpp +++ b/torchaudio/csrc/ffmpeg/prototype.cpp @@ -98,6 +98,10 @@ int64_t find_best_video_stream(S s) { return s->s.find_best_video_stream(); } +void seek(S s, int64_t timestamp) { + s->s.seek(timestamp); +} + template std::string string_format(const std::string& format, Args... args) { char buffer[512]; @@ -309,6 +313,7 @@ TORCH_LIBRARY_FRAGMENT(torchaudio, m) { m.def( "torchaudio::ffmpeg_streamer_find_best_video_stream", find_best_video_stream); + m.def("torchaudio::ffmpeg_streamer_seek", seek); m.def( "torchaudio::ffmpeg_streamer_add_basic_audio_stream", add_basic_audio_stream); diff --git a/torchaudio/csrc/ffmpeg/streamer.cpp b/torchaudio/csrc/ffmpeg/streamer.cpp index cd59eca3cb..836ec2b3f9 100644 --- a/torchaudio/csrc/ffmpeg/streamer.cpp +++ b/torchaudio/csrc/ffmpeg/streamer.cpp @@ -133,6 +133,14 @@ bool Streamer::is_buffer_ready() const { //////////////////////////////////////////////////////////////////////////////// // Configure methods //////////////////////////////////////////////////////////////////////////////// +void Streamer::seek(double timestamp) { + int64_t ts = static_cast(timestamp * AV_TIME_BASE); + int ret = avformat_seek_file(pFormatContext, -1, INT64_MIN, ts, INT64_MAX, 0); + if (ret < 0) { + throw std::runtime_error(std::string("Failed to seek: ") + av_err2str(ret)); + } +} + void Streamer::add_audio_stream( int i, int frames_per_chunk, diff --git a/torchaudio/csrc/ffmpeg/streamer.h b/torchaudio/csrc/ffmpeg/streamer.h index 612048ef85..bb8eb5d398 100644 --- a/torchaudio/csrc/ffmpeg/streamer.h +++ b/torchaudio/csrc/ffmpeg/streamer.h @@ -60,6 +60,8 @@ class Streamer { ////////////////////////////////////////////////////////////////////////////// // Configure methods ////////////////////////////////////////////////////////////////////////////// + void seek(double timestamp); + void add_audio_stream( int i, int frames_per_chunk, diff --git a/torchaudio/csrc/ffmpeg/typedefs.h b/torchaudio/csrc/ffmpeg/typedefs.h new file mode 100644 index 0000000000..7fcf663ca4 --- /dev/null +++ b/torchaudio/csrc/ffmpeg/typedefs.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace torchaudio { +namespace ffmpeg { + +struct SrcStreamInfo { + AVMediaType media_type; + const char* codec_name = NULL; + const char* codec_long_name = NULL; + const char* fmt_name = NULL; + int bit_rate = 0; + // Audio + double sample_rate = 0; + int num_channels = 0; + // Video + int width = 0; + int height = 0; + double frame_rate = 0; +}; + +struct OutputStreamInfo { + int source_index; + std::string filter_description; + double rate; + OutputStreamInfo() = default; +}; + +} // namespace ffmpeg +} // namespace torchaudio From ece03edc3fc28a1ce2c28ef438d2898ed0a78d3f Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 30 Dec 2021 08:31:19 -0800 Subject: [PATCH 0075/1144] Add a switch to build ffmpeg binding (#2048) Summary: This PR adds `BUILD_FFMPEG` switch to torchaudio build process so that features related to ffmpeg are built. The flag is false by default, so no CI jobs or development flow are affected. This is because handling the dependencies around ffmpeg is a bit tricky. Currently, the CMake file uses `pkg-config` to find an ffmpeg installation in the system. This works fine for both conda-based installation and system-managed installation (like `apt`). In subsequent PRs, I will find a solution that works for local development and binary distributions. Pull Request resolved: https://github.com/pytorch/audio/pull/2048 Reviewed By: hwangjeff, nateanl Differential Revision: D33367260 Pulled By: mthrok fbshipit-source-id: 94517acecb62bd6d4e96d4b7cbc3ab3c2a25706c --- CMakeLists.txt | 1 + third_party/CMakeLists.txt | 20 ++++++++++++++++++++ tools/setup_helpers/extension.py | 4 ++++ torchaudio/csrc/CMakeLists.txt | 24 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 89c8bd66b8..2249cdd094 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ endif() # Options option(BUILD_SOX "Build libsox statically" ON) +option(BUILD_FFMPEG "Enable ffmpeg-based features" OFF) option(BUILD_KALDI "Build kaldi statically" ON) option(BUILD_RNNT "Enable RNN transducer" ON) option(BUILD_CTC_DECODER "Build Flashlight CTC decoder" ON) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 197e35db9b..f3b30fc8ed 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -13,6 +13,26 @@ if (BUILD_SOX) list(APPEND TORCHAUDIO_THIRD_PARTIES libsox) endif() +################################################################################ +# ffmpeg +################################################################################ +if (BUILD_FFMPEG) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + # requires ffmpeg>=4.1 + libavdevice>=58 + libavfilter>=7 + libavformat>=58 + libavcodec>=58 + libswresample>=3 + libswscale>=3 + libavutil>=56 + ) + add_library(ffmpeg INTERFACE) + target_include_directories(ffmpeg INTERFACE ${LIBAV_INCLUDE_DIRS}) + target_link_libraries(ffmpeg INTERFACE ${LIBAV_LINK_LIBRARIES}) +endif() + ################################################################################ # kaldi ################################################################################ diff --git a/tools/setup_helpers/extension.py b/tools/setup_helpers/extension.py index db5470b7de..310df1a364 100644 --- a/tools/setup_helpers/extension.py +++ b/tools/setup_helpers/extension.py @@ -36,6 +36,7 @@ def _get_build(var, default=False): _BUILD_KALDI = False if platform.system() == "Windows" else _get_build("BUILD_KALDI", True) _BUILD_RNNT = _get_build("BUILD_RNNT", True) _BUILD_CTC_DECODER = False if platform.system() == "Windows" else _get_build("BUILD_CTC_DECODER", True) +_BUILD_FFMPEG = _get_build("BUILD_FFMPEG", False) _USE_ROCM = _get_build("USE_ROCM", torch.cuda.is_available() and torch.version.hip is not None) _USE_CUDA = _get_build("USE_CUDA", torch.cuda.is_available() and torch.version.hip is None) _USE_OPENMP = _get_build("USE_OPENMP", True) and "ATen parallel backend: OpenMP" in torch.__config__.parallel_info() @@ -54,6 +55,8 @@ def get_ext_modules(): Extension(name="torchaudio._torchaudio_decoder", sources=[]), ] ) + if _BUILD_FFMPEG: + modules.append(Extension(name="torchaudio.lib.libtorchaudio_ffmpeg", sources=[])) return modules @@ -92,6 +95,7 @@ def build_extension(self, ext): "-DCMAKE_VERBOSE_MAKEFILE=ON", f"-DPython_INCLUDE_DIR={distutils.sysconfig.get_python_inc()}", f"-DBUILD_SOX:BOOL={'ON' if _BUILD_SOX else 'OFF'}", + f"-DBUILD_FFMPEG:BOOL={'ON' if _BUILD_FFMPEG else 'OFF'}", f"-DBUILD_KALDI:BOOL={'ON' if _BUILD_KALDI else 'OFF'}", f"-DBUILD_RNNT:BOOL={'ON' if _BUILD_RNNT else 'OFF'}", f"-DBUILD_CTC_DECODER:BOOL={'ON' if _BUILD_CTC_DECODER else 'OFF'}", diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index 9acdc0fe07..586d288e61 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -166,6 +166,30 @@ else() set(TORCHAUDIO_LIBRARY -Wl,--no-as-needed libtorchaudio -Wl,--as-needed CACHE INTERNAL "") endif() +################################################################################ +# libtorchaudio_ffmpeg +################################################################################ +if(BUILD_FFMPEG) + set( + LIBTORCHAUDIO_FFMPEG_SOURCES + ffmpeg/prototype.cpp + ffmpeg/decoder.cpp + ffmpeg/ffmpeg.cpp + ffmpeg/filter_graph.cpp + ffmpeg/buffer.cpp + ffmpeg/sink.cpp + ffmpeg/stream_processor.cpp + ffmpeg/streamer.cpp + ) +define_library( + libtorchaudio_ffmpeg + "${LIBTORCHAUDIO_FFMPEG_SOURCES}" + "${LIBTORCHAUDIO_INCLUDE_DIRS}" + "libtorchaudio;ffmpeg" + "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" + ) +endif() + ################################################################################ # _torchaudio.so ################################################################################ From 4c8fd7608e564ae5c4d5c72432262a1bcf8e54f3 Mon Sep 17 00:00:00 2001 From: hwangjeff Date: Thu, 30 Dec 2021 13:07:58 -0800 Subject: [PATCH 0076/1144] Clean up Emformer module (#2091) Summary: * Removes redundant declaration `right_context_blocks = []`, as flagged by kobenaxie. * Adds random seed to tests, as flagged by carolineechen in other PRs. Pull Request resolved: https://github.com/pytorch/audio/pull/2091 Reviewed By: mthrok Differential Revision: D33340964 Pulled By: hwangjeff fbshipit-source-id: a9de43e28d1bae7bd4806b280717b0d822bb42fc --- test/torchaudio_unittest/prototype/emformer_test_impl.py | 4 ++++ torchaudio/prototype/models/emformer.py | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/torchaudio_unittest/prototype/emformer_test_impl.py b/test/torchaudio_unittest/prototype/emformer_test_impl.py index d380af4b4e..16adfa13ac 100644 --- a/test/torchaudio_unittest/prototype/emformer_test_impl.py +++ b/test/torchaudio_unittest/prototype/emformer_test_impl.py @@ -24,6 +24,10 @@ def _gen_inputs(self, input_dim, batch_size, num_frames, right_context_length): ) return input, lengths + def setUp(self): + super().setUp() + torch.random.manual_seed(29) + def test_torchscript_consistency_forward(self): r"""Verify that scripting Emformer does not change the behavior of method `forward`.""" input_dim = 128 diff --git a/torchaudio/prototype/models/emformer.py b/torchaudio/prototype/models/emformer.py index 2ce2339c42..3463371807 100644 --- a/torchaudio/prototype/models/emformer.py +++ b/torchaudio/prototype/models/emformer.py @@ -669,8 +669,7 @@ def __init__( self.max_memory_size = max_memory_size def _gen_right_context(self, input: torch.Tensor) -> torch.Tensor: - right_context_blocks = [] - T, B, D = input.shape + T = input.shape[0] num_segs = math.ceil((T - self.right_context_length) / self.segment_length) right_context_blocks = [] for seg_idx in range(num_segs - 1): @@ -765,7 +764,7 @@ def _gen_attention_mask(self, input: torch.Tensor) -> torch.Tensor: return attention_mask def forward(self, input: torch.Tensor, lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - r"""Forward pass for training. + r"""Forward pass for training and non-streaming inference. B: batch size; T: number of frames; @@ -806,7 +805,7 @@ def infer( lengths: torch.Tensor, states: Optional[List[List[torch.Tensor]]] = None, ) -> Tuple[torch.Tensor, torch.Tensor, List[List[torch.Tensor]]]: - r"""Forward pass for inference. + r"""Forward pass for streaming inference. B: batch size; T: number of frames; From 8ed1478276b9d505c36b6b39876242a51b5b93a5 Mon Sep 17 00:00:00 2001 From: Joao Gomes Date: Thu, 30 Dec 2021 13:33:56 -0800 Subject: [PATCH 0077/1144] Enforce lint checks and fix/mute lint errors (#2116) Summary: cc mthrok Pull Request resolved: https://github.com/pytorch/audio/pull/2116 Reviewed By: mthrok Differential Revision: D33368453 Pulled By: jdsgomes fbshipit-source-id: 09cf3fe5ed6f771c2f16505633c0e59b0c27453c --- .circleci/config.yml | 2 +- .circleci/config.yml.in | 2 +- .flake8 | 2 +- docs/source/conf.py | 2 +- docs/source/prototype.pipelines.rst | 2 +- .../global_stats.json | 2 +- examples/libtorchaudio/augmentation/README.md | 1 - packaging/vs2019/install_activate.bat | 1 - .../wav2vec2_pipeline_test.py | 14 ++++---- .../datasets/commonvoice_test.py | 2 +- third_party/kaldi/README.md | 2 +- torchaudio/datasets/cmuarctic.py | 36 +++++++++---------- torchaudio/datasets/cmudict.py | 4 +-- torchaudio/datasets/librispeech.py | 14 ++++---- torchaudio/datasets/libritts.py | 14 ++++---- torchaudio/datasets/speechcommands.py | 4 +-- torchaudio/datasets/vctk.py | 2 +- 17 files changed, 52 insertions(+), 54 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e466ad2b86..5e82b603d6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,7 +125,7 @@ jobs: command: pre-commit install-hooks - run: name: Lint Python code and config files - command: pre-commit run --all-files || true + command: pre-commit run --all-files - run: name: Required lint modifications when: always diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 762431e85e..5a52df7cc4 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -125,7 +125,7 @@ jobs: command: pre-commit install-hooks - run: name: Lint Python code and config files - command: pre-commit run --all-files || true + command: pre-commit run --all-files - run: name: Required lint modifications when: always diff --git a/.flake8 b/.flake8 index 5b50ffd4a8..b4e3878df3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 120 -ignore = E305,E402,E721,E741,F405,W503,W504,F999 +ignore = E203,E305,E402,E721,E741,F405,W503,W504,F999 exclude = build,docs/source,_ext,third_party,examples/tutorials diff --git a/docs/source/conf.py b/docs/source/conf.py index b6087219b0..46ebe8f6f9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,7 @@ # -- General configuration ------------------------------------------------ -warnings.filterwarnings("ignore", module="matplotlib\..*") +warnings.filterwarnings("ignore", module=r"matplotlib\..*") # If your documentation needs a minimal Sphinx version, state it here. diff --git a/docs/source/prototype.pipelines.rst b/docs/source/prototype.pipelines.rst index 3221cfdfae..f5495ec5f3 100644 --- a/docs/source/prototype.pipelines.rst +++ b/docs/source/prototype.pipelines.rst @@ -37,4 +37,4 @@ EMFORMER_RNNT_BASE_LIBRISPEECH .. container:: py attribute .. autodata:: EMFORMER_RNNT_BASE_LIBRISPEECH - :no-value: \ No newline at end of file + :no-value: diff --git a/examples/asr/librispeech_emformer_rnnt/global_stats.json b/examples/asr/librispeech_emformer_rnnt/global_stats.json index 9ceb496bf1..017d9cead2 100644 --- a/examples/asr/librispeech_emformer_rnnt/global_stats.json +++ b/examples/asr/librispeech_emformer_rnnt/global_stats.json @@ -163,4 +163,4 @@ 0.22755587298227964, 0.24719513536827162 ] -} \ No newline at end of file +} diff --git a/examples/libtorchaudio/augmentation/README.md b/examples/libtorchaudio/augmentation/README.md index b78248af6b..81c58b3bd6 100644 --- a/examples/libtorchaudio/augmentation/README.md +++ b/examples/libtorchaudio/augmentation/README.md @@ -33,4 +33,3 @@ input_audio_file="./data/input.wav" ``` When you give a clean speech file, the output audio sounds like it's a phone conversation. - diff --git a/packaging/vs2019/install_activate.bat b/packaging/vs2019/install_activate.bat index 84adb2dd95..378f7efb95 100644 --- a/packaging/vs2019/install_activate.bat +++ b/packaging/vs2019/install_activate.bat @@ -25,4 +25,3 @@ IF "%cross_compiler_target_platform%" == "win-64" ( echo CALL "VC\Auxiliary\Build\vcvars32.bat" >> "%PREFIX%\etc\conda\activate.d\vs%YEAR%_compiler_vars.bat" echo popd ) - diff --git a/test/integration_tests/wav2vec2_pipeline_test.py b/test/integration_tests/wav2vec2_pipeline_test.py index e5e7117a31..adbb825819 100644 --- a/test/integration_tests/wav2vec2_pipeline_test.py +++ b/test/integration_tests/wav2vec2_pipeline_test.py @@ -61,24 +61,24 @@ def test_pretraining_models(bundle): ( VOXPOPULI_ASR_BASE_10K_EN, "en2", - "i|hope|that|we|will|see|a|ddrasstic|decrease|of|funding|for|the|failed|eu|project|and|that|more|money|will|come|back|to|the|taxpayers", - ), # noqa: E501 + "i|hope|that|we|will|see|a|ddrasstic|decrease|of|funding|for|the|failed|eu|project|and|that|more|money|will|come|back|to|the|taxpayers", # noqa: E501 + ), ( VOXPOPULI_ASR_BASE_10K_ES, "es", - "la|primera|que|es|imprescindible|pensar|a|pequeña|a|escala|para|implicar|y|complementar|así|la|actuación|global", - ), # noqa: E501 + "la|primera|que|es|imprescindible|pensar|a|pequeña|a|escala|para|implicar|y|complementar|así|la|actuación|global", # noqa: E501 + ), (VOXPOPULI_ASR_BASE_10K_DE, "de", "dabei|spielt|auch|eine|sorgfältige|berichterstattung|eine|wichtige|rolle"), ( VOXPOPULI_ASR_BASE_10K_FR, "fr", - "la|commission|va|faire|des|propositions|sur|ce|sujet|comment|mettre|en|place|cette|capacité|fiscale|et|le|conseil|européen|y|reviendra|sour|les|sujets|au|moins|de|mars", - ), # noqa: E501 + "la|commission|va|faire|des|propositions|sur|ce|sujet|comment|mettre|en|place|cette|capacité|fiscale|et|le|conseil|européen|y|reviendra|sour|les|sujets|au|moins|de|mars", # noqa: E501 + ), ( VOXPOPULI_ASR_BASE_10K_IT, "it", "credo|che|illatino|non|sia|contemplato|tra|le|traduzioni|e|quindi|mi|attengo|allitaliano", - ), # noqa: E501 + ), ], ) def test_finetune_asr_model( diff --git a/test/torchaudio_unittest/datasets/commonvoice_test.py b/test/torchaudio_unittest/datasets/commonvoice_test.py index c250c2a844..5b82f1f7cb 100644 --- a/test/torchaudio_unittest/datasets/commonvoice_test.py +++ b/test/torchaudio_unittest/datasets/commonvoice_test.py @@ -15,7 +15,7 @@ _ORIGINAL_EXT_AUDIO = COMMONVOICE._ext_audio _SAMPLE_RATE = 48000 -_HEADERS = [u"client_ids", u"path", u"sentence", u"up_votes", u"down_votes", u"age", u"gender", u"accent"] +_HEADERS = ["client_ids", "path", "sentence", "up_votes", "down_votes", "age", "gender", "accent"] _EN_TRAIN_CSV_CONTENTS = [ [ "9d16c5d980247861130e0480e2719f448be73d86a496c36d01a477cbdecd8cfd1399403d7a77bf458d211a70711b2da0845c", diff --git a/third_party/kaldi/README.md b/third_party/kaldi/README.md index 58c48747a8..f981674601 100644 --- a/third_party/kaldi/README.md +++ b/third_party/kaldi/README.md @@ -3,4 +3,4 @@ This directory contains original Kaldi repository (as submodule), [the custom implementation of Kaldi's vector/matrix](./src) and the build script. We use the custom build process so that the resulting library only contains what torchaudio needs. -We use the custom vector/matrix implementation so that we can use the same BLAS library that PyTorch is compiled with, and so that we can (hopefully, in future) take advantage of other PyTorch features (such as differentiability and GPU support). The down side of this approach is that it adds a lot of overhead compared to the original Kaldi (operator dispatch and element-wise processing, which PyTorch is not efficient at). We can improve this gradually, and if you are interested in helping, please let us know by opening an issue. \ No newline at end of file +We use the custom vector/matrix implementation so that we can use the same BLAS library that PyTorch is compiled with, and so that we can (hopefully, in future) take advantage of other PyTorch features (such as differentiability and GPU support). The down side of this approach is that it adds a lot of overhead compared to the original Kaldi (operator dispatch and element-wise processing, which PyTorch is not efficient at). We can improve this gradually, and if you are interested in helping, please let us know by opening an issue. diff --git a/torchaudio/datasets/cmuarctic.py b/torchaudio/datasets/cmuarctic.py index 0b8e2f8440..977de720fa 100644 --- a/torchaudio/datasets/cmuarctic.py +++ b/torchaudio/datasets/cmuarctic.py @@ -14,24 +14,24 @@ URL = "aew" FOLDER_IN_ARCHIVE = "ARCTIC" _CHECKSUMS = { - "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", - "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", - "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", - "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", - "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", - "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", - "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", - "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", - "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", - "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", - "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", - "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", - "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", - "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", - "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", - "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", - "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", - "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", + "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", # noqa: E501 } diff --git a/torchaudio/datasets/cmudict.py b/torchaudio/datasets/cmudict.py index 598538da9c..7e5c132354 100644 --- a/torchaudio/datasets/cmudict.py +++ b/torchaudio/datasets/cmudict.py @@ -7,8 +7,8 @@ from torch.utils.data import Dataset _CHECKSUMS = { - "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", - "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", # noqa: E501 + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", # noqa: E501 } _PUNCTUATIONS = set( [ diff --git a/torchaudio/datasets/librispeech.py b/torchaudio/datasets/librispeech.py index 74f1521c76..76c777df11 100644 --- a/torchaudio/datasets/librispeech.py +++ b/torchaudio/datasets/librispeech.py @@ -13,13 +13,13 @@ URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriSpeech" _CHECKSUMS = { - "http://www.openslr.org/resources/12/dev-clean.tar.gz": "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", - "http://www.openslr.org/resources/12/dev-other.tar.gz": "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", - "http://www.openslr.org/resources/12/test-clean.tar.gz": "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", - "http://www.openslr.org/resources/12/test-other.tar.gz": "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", - "http://www.openslr.org/resources/12/train-clean-100.tar.gz": "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", - "http://www.openslr.org/resources/12/train-clean-360.tar.gz": "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", - "http://www.openslr.org/resources/12/train-other-500.tar.gz": "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2", + "http://www.openslr.org/resources/12/dev-clean.tar.gz": "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", # noqa: E501 + "http://www.openslr.org/resources/12/dev-other.tar.gz": "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", # noqa: E501 + "http://www.openslr.org/resources/12/test-clean.tar.gz": "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", # noqa: E501 + "http://www.openslr.org/resources/12/test-other.tar.gz": "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-100.tar.gz": "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-360.tar.gz": "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", # noqa: E501 + "http://www.openslr.org/resources/12/train-other-500.tar.gz": "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2", # noqa: E501 } diff --git a/torchaudio/datasets/libritts.py b/torchaudio/datasets/libritts.py index c6c8d1df7b..ae2fc9b7c8 100644 --- a/torchaudio/datasets/libritts.py +++ b/torchaudio/datasets/libritts.py @@ -13,13 +13,13 @@ URL = "train-clean-100" FOLDER_IN_ARCHIVE = "LibriTTS" _CHECKSUMS = { - "http://www.openslr.org/resources/60/dev-clean.tar.gz": "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", - "http://www.openslr.org/resources/60/dev-other.tar.gz": "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", - "http://www.openslr.org/resources/60/test-clean.tar.gz": "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", - "http://www.openslr.org/resources/60/test-other.tar.gz": "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", - "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", - "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", - "http://www.openslr.org/resources/60/train-other-500.tar.gz": "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", + "http://www.openslr.org/resources/60/dev-clean.tar.gz": "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", # noqa: E501 + "http://www.openslr.org/resources/60/dev-other.tar.gz": "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", # noqa: E501 + "http://www.openslr.org/resources/60/test-clean.tar.gz": "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", # noqa: E501 + "http://www.openslr.org/resources/60/test-other.tar.gz": "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", # noqa: E501 + "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", # noqa: E501 + "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", # noqa: E501 + "http://www.openslr.org/resources/60/train-other-500.tar.gz": "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", # noqa: E501 } diff --git a/torchaudio/datasets/speechcommands.py b/torchaudio/datasets/speechcommands.py index dd5fe82c17..a5b3eb99ad 100644 --- a/torchaudio/datasets/speechcommands.py +++ b/torchaudio/datasets/speechcommands.py @@ -15,8 +15,8 @@ HASH_DIVIDER = "_nohash_" EXCEPT_FOLDER = "_background_noise_" _CHECKSUMS = { - "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz": "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", - "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.02.tar.gz": "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", + "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz": "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", # noqa: E501 + "https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.02.tar.gz": "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", # noqa: E501 } diff --git a/torchaudio/datasets/vctk.py b/torchaudio/datasets/vctk.py index 6442f2c9ba..7620d5ce36 100644 --- a/torchaudio/datasets/vctk.py +++ b/torchaudio/datasets/vctk.py @@ -11,7 +11,7 @@ URL = "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip" _CHECKSUMS = { - "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" + "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" # noqa: E501 } From 524d554046f3725ea86d2032642edc479e53d0d5 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 30 Dec 2021 13:57:43 -0800 Subject: [PATCH 0078/1144] Add note about ffmpeg code (#2115) Summary: Pull Request resolved: https://github.com/pytorch/audio/pull/2115 Reviewed By: carolineechen Differential Revision: D33370700 Pulled By: mthrok fbshipit-source-id: 591b67870247f69cc542f649cd62444ee3c45934 --- torchaudio/csrc/ffmpeg/README.md | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 torchaudio/csrc/ffmpeg/README.md diff --git a/torchaudio/csrc/ffmpeg/README.md b/torchaudio/csrc/ffmpeg/README.md new file mode 100644 index 0000000000..fd6920a28c --- /dev/null +++ b/torchaudio/csrc/ffmpeg/README.md @@ -0,0 +1,132 @@ +# FFMpeg binding dev note + +The ffmpeg binding is based on ver 4.1. + +## Learning material + +For understanding the concept of stream processing, some tutorials are useful. + +https://github.com/leandromoreira/ffmpeg-libav-tutorial + +The best way to learn how to use ffmpeg is to look at the official examples. +Practically all the code is re-organization of examples; + +https://ffmpeg.org/doxygen/4.1/examples.html + +## Streamer Architecture + +The top level class is `Streamer` class. This class handles the input (via `AVFormatContext*`), and manages `StreamProcessor`s for each stream in the input. + +The `Streamer` object slices the input data into a series of `AVPacket` objects and it feeds the objects to corresponding `StreamProcessor`s. + +``` + Streamer +┌─────────────────────────────────────────────────┐ +│ │ +│ AVFormatContext* ┌──► StreamProcessor[0] │ +│ │ │ │ +│ └─────────────┼──► StreamProcessor[1] │ +│ AVPacket* │ │ +│ └──► ... │ +│ │ +└─────────────────────────────────────────────────┘ +``` + +The `StreamProcessor` class is composed of one `Decoder` and multiple of `Sink` objects. + +`Sink` objects correspond to output streams that users set. +`Sink` class is a wrapper `FilterGraph` and `Buffer` classes. + +The `AVPacket*` passed to `StreamProcessor` is first passed to `Decoder`. +`Decoder` generates audio / video frames (`AVFrame`) and pass it to `Sink`s. + +Firstly `Sink` class passes the incoming frame to `FilterGraph`. + +`FilterGraph` is a class based on [`AVFilterGraph` structure](https://ffmpeg.org/doxygen/4.1/structAVFilterGraph.html), +and it can apply various filters. +At minimum, it performs format conversion so that the resuling data is suitable for Tensor representation, +such as YUV to RGB. + +The output `AVFrame` from `FilterGraph` is passed to `Buffer` class, which converts it to Tensor. + +``` + StreamProcessor +┌─────────────────────────────────────────────────────────┐ +│ AVPacket* │ +│ │ │ +│ │ AVFrame* AVFrame* │ +│ └► Decoder ──┬─► FilterGraph ─────► Buffer ───► Tensor │ +│ │ │ +│ ├─► FilterGraph ─────► Buffer ───► Tensor │ +│ │ │ +│ └─► ... │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Implementation guideline + +### Memory management and object lifecycle + +Ffmpeg uses raw pointers, which needs to be allocated and freed with dedicated functions. +In the binding code, these pointers are encapsulated in a class with RAII semantic and +`std::unique_ptr<>` to guarantee sole ownership. + +**Decoder lifecycle** + +```c++ +// Default construction (no memory allocation) +decoder = Decoder(...); +// Decode +decoder.process_packet(pPacket); +// Retrieve result +decoder.get_frame(pFrame); +// Release resources +decoder::~Decoder(); +``` + +**FilterGraph lifecycle** + +```c++ +// Default construction (no memory allocation) +filter_graph = FilterGraph(); +// Filter configuration +... +filter_graph.create_filter(); +// Apply filter +fitler_graph.add_frame(pFrame); +// Retrieve result +filter_graph.get_frame(pFrame); +// Release resources +filter_graph::~FilterGraph(); +``` + +**StreamProcessor lifecycle** + +```c++ +// Default construction (no memory allocation) +processor = Processor(...); +// Define the process stream +processor.add_audio_stream(...); +processor.add_audio_stream(...); +// Process the packet +processor.process_packet(pPacket); +// Retrieve result +tensor = processor.get_chunk(...); +// Release resources +processor::~Processor(); +``` + +### ON/OFF semantic and `std::unique_ptr<>` + +Since we want to make some components (such as stream processors and filters) +separately configurable, we introduce states for ON/OFF. +To make the code simple, we use `std::unique_ptr<>`. +`nullptr` means the component is turned off. +This pattern applies to `StreamProcessor` (output streams). + +### Exception and return value + +To report the error during the configuration and initialization of objects, +we use `Exception`. However, throwing errors is expensive during the streaming, +so we use return value for that. From 9f14fa635383ecab2529f388a28253a79207cd47 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 30 Dec 2021 14:40:15 -0800 Subject: [PATCH 0079/1144] Build ffmpeg-features in Linux/macOS unittests (#2114) Summary: Preparation to land Python front-end of ffmpeg-related features. - Set BUILD_FFMPEG=1 in Linux/macOS unit test jobs - Install ffmpeg and pkg-config from conda-forge - Add note about Windows build process - Temporarily avoid `av_err2str` Pull Request resolved: https://github.com/pytorch/audio/pull/2114 Reviewed By: hwangjeff Differential Revision: D33371346 Pulled By: mthrok fbshipit-source-id: b0e16a35959a49a2166109068f3e0cbbb836e888 --- .circleci/unittest/linux/scripts/install.sh | 2 +- .circleci/unittest/linux/scripts/setup_env.sh | 1 + third_party/CMakeLists.txt | 10 ++++++++++ torchaudio/csrc/ffmpeg/streamer.cpp | 9 ++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.circleci/unittest/linux/scripts/install.sh b/.circleci/unittest/linux/scripts/install.sh index 869799da8d..cbd17b7bfe 100755 --- a/.circleci/unittest/linux/scripts/install.sh +++ b/.circleci/unittest/linux/scripts/install.sh @@ -52,7 +52,7 @@ printf "Installing PyTorch with %s\n" "${cudatoolkit}" # 2. Install torchaudio printf "* Installing torchaudio\n" -python setup.py install +BUILD_FFMPEG=1 python setup.py install # 3. Install Test tools printf "* Installing test tools\n" diff --git a/.circleci/unittest/linux/scripts/setup_env.sh b/.circleci/unittest/linux/scripts/setup_env.sh index 25a9eed3d7..541330a4e5 100755 --- a/.circleci/unittest/linux/scripts/setup_env.sh +++ b/.circleci/unittest/linux/scripts/setup_env.sh @@ -40,3 +40,4 @@ conda activate "${env_dir}" # 3. Install minimal build tools pip --quiet install cmake ninja +conda install --quiet -y -c conda-forge 'ffmpeg>4.1' pkg-config diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index f3b30fc8ed..f121c287c3 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -17,6 +17,16 @@ endif() # ffmpeg ################################################################################ if (BUILD_FFMPEG) + if(WIN32) + message(FATAL_ERROR "FFmpeg integration is not supported on Windows.") + # TODO + # Since pkg-config is not well-supported on Windows (for example, ffmpeg + # installed from conda-forge does not include `.pc` medata file), + # to support windows, we need to fill interface manually. + # + # Something like this might work + # https://github.com/microsoft/vcpkg/issues/1379#issuecomment-312483740 + endif() find_package(PkgConfig REQUIRED) pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET # requires ffmpeg>=4.1 diff --git a/torchaudio/csrc/ffmpeg/streamer.cpp b/torchaudio/csrc/ffmpeg/streamer.cpp index 836ec2b3f9..10dfbed4e1 100644 --- a/torchaudio/csrc/ffmpeg/streamer.cpp +++ b/torchaudio/csrc/ffmpeg/streamer.cpp @@ -137,7 +137,14 @@ void Streamer::seek(double timestamp) { int64_t ts = static_cast(timestamp * AV_TIME_BASE); int ret = avformat_seek_file(pFormatContext, -1, INT64_MIN, ts, INT64_MAX, 0); if (ret < 0) { - throw std::runtime_error(std::string("Failed to seek: ") + av_err2str(ret)); + // Temporarily removing `av_err2str` function as it causes + // `error: taking address of temporary array` on GCC. + // TODO: + // Workaround with `av_err2string` function from + // https://github.com/joncampbell123/composite-video-simulator/issues/5#issuecomment-611885908 + throw std::runtime_error(std::string("Failed to seek.")); + // throw std::runtime_error(std::string("Failed to seek: ") + + // av_err2str(ret)); } } From 64c7e065e5cf8e217aa1312c02be1165b58d1663 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Thu, 30 Dec 2021 16:17:58 -0800 Subject: [PATCH 0080/1144] Update CTC Hypothesis docs (#2117) Summary: add documentaion for CTC decoder `Hypothesis` and include it in docs Pull Request resolved: https://github.com/pytorch/audio/pull/2117 Reviewed By: mthrok Differential Revision: D33370381 Pulled By: carolineechen fbshipit-source-id: cf6501a499e5303cda0410f733f0fab4e1c39aff --- docs/source/prototype.ctc_decoder.rst | 5 +++ torchaudio/prototype/ctc_decoder/__init__.py | 3 +- .../prototype/ctc_decoder/ctc_decoder.py | 33 +++++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/source/prototype.ctc_decoder.rst b/docs/source/prototype.ctc_decoder.rst index 6c381d7ccc..5207565919 100644 --- a/docs/source/prototype.ctc_decoder.rst +++ b/docs/source/prototype.ctc_decoder.rst @@ -16,6 +16,11 @@ KenLMLexiconDecoder .. automethod:: idxs_to_tokens +Hypothesis +~~~~~~~~~~ + +.. autoclass:: Hypothesis + Factory Function ---------------- diff --git a/torchaudio/prototype/ctc_decoder/__init__.py b/torchaudio/prototype/ctc_decoder/__init__.py index 9ee466e256..18e36e74fe 100644 --- a/torchaudio/prototype/ctc_decoder/__init__.py +++ b/torchaudio/prototype/ctc_decoder/__init__.py @@ -2,7 +2,7 @@ try: torchaudio._extension._load_lib("libtorchaudio_decoder") - from .ctc_decoder import KenLMLexiconDecoder, kenlm_lexicon_decoder + from .ctc_decoder import Hypothesis, KenLMLexiconDecoder, kenlm_lexicon_decoder except ImportError as err: raise ImportError( "flashlight decoder bindings are required to use this functionality. " @@ -11,6 +11,7 @@ __all__ = [ + "Hypothesis", "KenLMLexiconDecoder", "kenlm_lexicon_decoder", ] diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py index 19aa2d883c..1af27c5631 100644 --- a/torchaudio/prototype/ctc_decoder/ctc_decoder.py +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -1,7 +1,5 @@ import itertools as it -from collections import namedtuple -from typing import Dict -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union, NamedTuple import torch from torchaudio._torchaudio_decoder import ( @@ -17,10 +15,19 @@ ) -__all__ = ["KenLMLexiconDecoder", "kenlm_lexicon_decoder"] +__all__ = ["Hypothesis", "KenLMLexiconDecoder", "kenlm_lexicon_decoder"] -Hypothesis = namedtuple("Hypothesis", ["tokens", "words", "score"]) +class Hypothesis(NamedTuple): + r"""Represents hypothesis generated by CTC beam search decoder :py:func`KenLMLexiconDecoder`. + + :ivar torch.LongTensor tokens: Predicted sequence of token IDs + :ivar List[str] words: List of predicted words + :ivar float score: Score corresponding to hypothesis + """ + tokens: torch.LongTensor + words: List[str] + score: float class KenLMLexiconDecoder: @@ -99,7 +106,10 @@ def _get_tokens(self, idxs: torch.IntTensor) -> torch.LongTensor: return torch.LongTensor(list(idxs)) def __call__(self, emissions: torch.FloatTensor, lengths: Optional[torch.Tensor] = None) -> List[List[Hypothesis]]: - """ + # Overriding the signature so that the return type is correct on Sphinx + """__call__(self, emissions: torch.FloatTensor, lengths: Optional[torch.Tensor] = None) -> \ + List[List[torchaudio.prototype.ctc_decoder.Hypothesis]] + Args: emissions (torch.FloatTensor): tensor of shape `(batch, frame, num_tokens)` storing sequences of probability distribution over labels; output of acoustic model @@ -109,11 +119,6 @@ def __call__(self, emissions: torch.FloatTensor, lengths: Optional[torch.Tensor] Returns: List[List[Hypothesis]]: List of sorted best hypotheses for each audio sequence in the batch. - - Each hypothesis is named tuple with the following fields: - tokens: torch.LongTensor of raw token IDs - score: hypothesis score - words: list of decoded words """ assert emissions.dtype == torch.float32 @@ -132,9 +137,9 @@ def __call__(self, emissions: torch.FloatTensor, lengths: Optional[torch.Tensor] hypos.append( [ Hypothesis( - self._get_tokens(result.tokens), # token ids - [self.word_dict.get_entry(x) for x in result.words if x >= 0], # words - result.score, # score + tokens=self._get_tokens(result.tokens), # token ids + words=[self.word_dict.get_entry(x) for x in result.words if x >= 0], # words + score=result.score, # score ) for result in nbest_results ] From da5c80bca55c7d45bae2fd57e7306667a347ef04 Mon Sep 17 00:00:00 2001 From: Werner Chao Date: Fri, 31 Dec 2021 13:11:14 -0800 Subject: [PATCH 0081/1144] Drop support for python3.6 as per task T109096383. Item 2 on issue 2051. (#2119) Summary: As per item 2 on [issue 2051](https://github.com/pytorch/audio/issues/2051), dropping support for python 3.6. Removed 3.6 from test matrix and ran `.circleci/regenerate.py `. Pull Request resolved: https://github.com/pytorch/audio/pull/2119 Reviewed By: mthrok Differential Revision: D33379542 Pulled By: wernerchao fbshipit-source-id: 6d0fb51b18c2fa7c8cf4eeee4a7f19c4a5210fac --- .circleci/config.yml | 819 +--------------------------------------- .circleci/regenerate.py | 2 +- 2 files changed, 11 insertions(+), 810 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e82b603d6..fe8a86c4d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -669,47 +669,6 @@ workflows: - circleci_consistency - download_third_parties_nix: name: download_third_parties_nix - - binary_linux_wheel: - cuda_version: cpu - name: binary_linux_wheel_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_linux_wheel: - cuda_version: cu102 - name: binary_linux_wheel_py3.6_cu102 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda102 - - binary_linux_wheel: - cuda_version: cu111 - name: binary_linux_wheel_py3.6_cu111 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda111 - - binary_linux_wheel: - cuda_version: cu113 - name: binary_linux_wheel_py3.6_cu113 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda113 - - binary_linux_wheel: - cuda_version: cu115 - name: binary_linux_wheel_py3.6_cu115 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda115 - - binary_linux_wheel: - cuda_version: rocm4.1 - name: binary_linux_wheel_py3.6_rocm4.1 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-rocm:4.1 - binary_linux_wheel: cuda_version: cpu name: binary_linux_wheel_py3.7_cpu @@ -839,12 +798,6 @@ workflows: requires: - download_third_parties_nix wheel_docker_image: pytorch/manylinux-rocm:4.1 - - binary_macos_wheel: - cuda_version: cpu - name: binary_macos_wheel_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - binary_macos_wheel: cuda_version: cpu name: binary_macos_wheel_py3.7_cpu @@ -863,20 +816,6 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix - - binary_windows_wheel: - cuda_version: cpu - name: binary_windows_wheel_py3.6_cpu - python_version: '3.6' - - binary_windows_wheel: - cuda_version: cu113 - name: binary_windows_wheel_py3.6_cu113 - python_version: '3.6' - wheel_docker_image: pytorch/manylinux-cuda113 - - binary_windows_wheel: - cuda_version: cu115 - name: binary_windows_wheel_py3.6_cu115 - python_version: '3.6' - wheel_docker_image: pytorch/manylinux-cuda115 - binary_windows_wheel: cuda_version: cpu name: binary_windows_wheel_py3.7_cpu @@ -919,41 +858,6 @@ workflows: name: binary_windows_wheel_py3.9_cu115 python_version: '3.9' wheel_docker_image: pytorch/manylinux-cuda115 - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - name: binary_linux_conda_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda102 - cuda_version: cu102 - name: binary_linux_conda_py3.6_cu102 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda111 - cuda_version: cu111 - name: binary_linux_conda_py3.6_cu111 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda113 - cuda_version: cu113 - name: binary_linux_conda_py3.6_cu113 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda115 - cuda_version: cu115 - name: binary_linux_conda_py3.6_cu115 - python_version: '3.6' - requires: - - download_third_parties_nix - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1059,13 +963,6 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix - - binary_macos_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - name: binary_macos_conda_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - binary_macos_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1087,21 +984,6 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - name: binary_windows_conda_py3.6_cpu - python_version: '3.6' - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cuda113 - cuda_version: cu113 - name: binary_windows_conda_py3.6_cu113 - python_version: '3.6' - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cuda115 - cuda_version: cu115 - name: binary_windows_conda_py3.6_cu115 - python_version: '3.6' - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -1181,20 +1063,14 @@ workflows: name: download_third_parties_nix - unittest_linux_cpu: cuda_version: cpu - name: unittest_linux_cpu_py3.6 - python_version: '3.6' + name: unittest_linux_cpu_py3.7 + python_version: '3.7' requires: - download_third_parties_nix - stylecheck: cuda_version: cpu - name: stylecheck_py3.6 - python_version: '3.6' - - unittest_linux_cpu: - cuda_version: cpu - name: unittest_linux_cpu_py3.7 + name: stylecheck_py3.7 python_version: '3.7' - requires: - - download_third_parties_nix - unittest_linux_cpu: cuda_version: cpu name: unittest_linux_cpu_py3.8 @@ -1207,12 +1083,6 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix - - unittest_linux_gpu: - cuda_version: cu113 - name: unittest_linux_gpu_py3.6 - python_version: '3.6' - requires: - - download_third_parties_nix - unittest_linux_gpu: cuda_version: cu113 name: unittest_linux_gpu_py3.7 @@ -1231,10 +1101,6 @@ workflows: python_version: '3.9' requires: - download_third_parties_nix - - unittest_windows_cpu: - cuda_version: cpu - name: unittest_windows_cpu_py3.6 - python_version: '3.6' - unittest_windows_cpu: cuda_version: cpu name: unittest_windows_cpu_py3.7 @@ -1247,10 +1113,6 @@ workflows: cuda_version: cpu name: unittest_windows_cpu_py3.9 python_version: '3.9' - - unittest_windows_gpu: - cuda_version: cu113 - name: unittest_windows_gpu_py3.6 - python_version: '3.6' - unittest_windows_gpu: cuda_version: cu113 name: unittest_windows_gpu_py3.7 @@ -1263,12 +1125,6 @@ workflows: cuda_version: cu113 name: unittest_windows_gpu_py3.9 python_version: '3.9' - - unittest_macos_cpu: - cuda_version: cpu - name: unittest_macos_cpu_py3.6 - python_version: '3.6' - requires: - - download_third_parties_nix - unittest_macos_cpu: cuda_version: cpu name: unittest_macos_cpu_py3.7 @@ -1301,227 +1157,6 @@ workflows: tags: only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ name: download_third_parties_nix - - binary_linux_wheel: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cpu_upload - requires: - - nightly_binary_linux_wheel_py3.6_cpu - subfolder: cpu/ - - smoke_test_linux_pip: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cpu_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_cpu_upload - - binary_linux_wheel: - cuda_version: cu102 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu102 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda102 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu102_upload - requires: - - nightly_binary_linux_wheel_py3.6_cu102 - subfolder: cu102/ - - smoke_test_linux_pip: - cuda_version: cu102 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu102_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_cu102_upload - - binary_linux_wheel: - cuda_version: cu111 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu111 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda111 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu111_upload - requires: - - nightly_binary_linux_wheel_py3.6_cu111 - subfolder: cu111/ - - smoke_test_linux_pip: - cuda_version: cu111 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu111_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_cu111_upload - - binary_linux_wheel: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu113 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda113 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu113_upload - requires: - - nightly_binary_linux_wheel_py3.6_cu113 - subfolder: cu113/ - - smoke_test_linux_pip: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu113_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_cu113_upload - - binary_linux_wheel: - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu115 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-cuda115 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu115_upload - requires: - - nightly_binary_linux_wheel_py3.6_cu115 - subfolder: cu115/ - - smoke_test_linux_pip: - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_cu115_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_cu115_upload - - binary_linux_wheel: - cuda_version: rocm4.1 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_rocm4.1 - python_version: '3.6' - requires: - - download_third_parties_nix - wheel_docker_image: pytorch/manylinux-rocm:4.1 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_rocm4.1_upload - requires: - - nightly_binary_linux_wheel_py3.6_rocm4.1 - subfolder: rocm4.1/ - - smoke_test_linux_pip: - cuda_version: rocm4.1 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_wheel_py3.6_rocm4.1_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_linux_wheel_py3.6_rocm4.1_upload - binary_linux_wheel: cuda_version: cpu filters: @@ -2185,30 +1820,6 @@ workflows: python_version: '3.9' requires: - nightly_binary_linux_wheel_py3.9_rocm4.1_upload - - binary_macos_wheel: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_macos_wheel_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_macos_wheel_py3.6_cpu_upload - requires: - - nightly_binary_macos_wheel_py3.6_cpu - subfolder: '' - binary_macos_wheel: cuda_version: cpu filters: @@ -2266,125 +1877,21 @@ workflows: tags: only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ name: nightly_binary_macos_wheel_py3.9_cpu - python_version: '3.9' - requires: - - download_third_parties_nix - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_macos_wheel_py3.9_cpu_upload - requires: - - nightly_binary_macos_wheel_py3.9_cpu - subfolder: '' - - binary_windows_wheel: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cpu - python_version: '3.6' - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cpu_upload - requires: - - nightly_binary_windows_wheel_py3.6_cpu - subfolder: cpu/ - - smoke_test_windows_pip: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cpu_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_windows_wheel_py3.6_cpu_upload - - binary_windows_wheel: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu113 - python_version: '3.6' - wheel_docker_image: pytorch/manylinux-cuda113 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu113_upload - requires: - - nightly_binary_windows_wheel_py3.6_cu113 - subfolder: cu113/ - - smoke_test_windows_pip: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu113_smoke_test_pip - python_version: '3.6' - requires: - - nightly_binary_windows_wheel_py3.6_cu113_upload - - binary_windows_wheel: - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu115 - python_version: '3.6' - wheel_docker_image: pytorch/manylinux-cuda115 - - binary_wheel_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu115_upload + python_version: '3.9' requires: - - nightly_binary_windows_wheel_py3.6_cu115 - subfolder: cu115/ - - smoke_test_windows_pip: - cuda_version: cu115 + - download_third_parties_nix + - binary_wheel_upload: + context: org-member filters: branches: only: - nightly tags: only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_wheel_py3.6_cu115_smoke_test_pip - python_version: '3.6' + name: nightly_binary_macos_wheel_py3.9_cpu_upload requires: - - nightly_binary_windows_wheel_py3.6_cu115_upload + - nightly_binary_macos_wheel_py3.9_cpu + subfolder: '' - binary_windows_wheel: cuda_version: cpu filters: @@ -2697,186 +2204,6 @@ workflows: python_version: '3.9' requires: - nightly_binary_windows_wheel_py3.9_cu115_upload - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cpu_upload - requires: - - nightly_binary_linux_conda_py3.6_cpu - - smoke_test_linux_conda: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cpu_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_linux_conda_py3.6_cpu_upload - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda102 - cuda_version: cu102 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu102 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu102_upload - requires: - - nightly_binary_linux_conda_py3.6_cu102 - - smoke_test_linux_conda_gpu: - cuda_version: cu102 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu102_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_linux_conda_py3.6_cu102_upload - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda111 - cuda_version: cu111 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu111 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu111_upload - requires: - - nightly_binary_linux_conda_py3.6_cu111 - - smoke_test_linux_conda_gpu: - cuda_version: cu111 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu111_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_linux_conda_py3.6_cu111_upload - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda113 - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu113 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu113_upload - requires: - - nightly_binary_linux_conda_py3.6_cu113 - - smoke_test_linux_conda_gpu: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu113_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_linux_conda_py3.6_cu113_upload - - binary_linux_conda: - conda_docker_image: pytorch/conda-builder:cuda115 - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu115 - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu115_upload - requires: - - nightly_binary_linux_conda_py3.6_cu115 - - smoke_test_linux_conda_gpu: - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_linux_conda_py3.6_cu115_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_linux_conda_py3.6_cu115_upload - binary_linux_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3417,30 +2744,6 @@ workflows: python_version: '3.9' requires: - nightly_binary_linux_conda_py3.9_cu115_upload - - binary_macos_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_macos_conda_py3.6_cpu - python_version: '3.6' - requires: - - download_third_parties_nix - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_macos_conda_py3.6_cpu_upload - requires: - - nightly_binary_macos_conda_py3.6_cpu - binary_macos_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu @@ -3513,108 +2816,6 @@ workflows: name: nightly_binary_macos_conda_py3.9_cpu_upload requires: - nightly_binary_macos_conda_py3.9_cpu - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cpu - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cpu - python_version: '3.6' - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cpu_upload - requires: - - nightly_binary_windows_conda_py3.6_cpu - - smoke_test_windows_conda: - cuda_version: cpu - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cpu_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_windows_conda_py3.6_cpu_upload - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cuda113 - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu113 - python_version: '3.6' - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu113_upload - requires: - - nightly_binary_windows_conda_py3.6_cu113 - - smoke_test_windows_conda: - cuda_version: cu113 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu113_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_windows_conda_py3.6_cu113_upload - - binary_windows_conda: - conda_docker_image: pytorch/conda-builder:cuda115 - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu115 - python_version: '3.6' - - binary_conda_upload: - context: org-member - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu115_upload - requires: - - nightly_binary_windows_conda_py3.6_cu115 - - smoke_test_windows_conda: - cuda_version: cu115 - filters: - branches: - only: - - nightly - tags: - only: /v[0-9]+(\.[0-9]+)*-rc[0-9]+/ - name: nightly_binary_windows_conda_py3.6_cu115_smoke_test_conda - python_version: '3.6' - requires: - - nightly_binary_windows_conda_py3.6_cu115_upload - binary_windows_conda: conda_docker_image: pytorch/conda-builder:cpu cuda_version: cpu diff --git a/.circleci/regenerate.py b/.circleci/regenerate.py index 91515f15fe..0dbe1fa336 100755 --- a/.circleci/regenerate.py +++ b/.circleci/regenerate.py @@ -21,7 +21,7 @@ from jinja2 import select_autoescape -PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +PYTHON_VERSIONS = ["3.7", "3.8", "3.9"] CU_VERSIONS_DICT = { "linux": ["cpu", "cu102", "cu111", "cu113", "cu115", "rocm4.1"], "windows": ["cpu", "cu113", "cu115"], From 8b9be42136ae0979341aa936e5412c5c37bb2908 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Sun, 2 Jan 2022 10:36:26 -0800 Subject: [PATCH 0082/1144] Make decoder/ffmpeg libs not depend on libtorchaudio (#2121) Summary: `libtorchaudio_decoder`, `torchaudio_decoder` and `libtorchaudio_ffmpeg` need not to be linked against `libtorchaudio`. ![dependency graph](https://user-images.githubusercontent.com/855818/147840753-5354bc43-19a5-4a24-a357-6dd3b6ba658e.png) Pull Request resolved: https://github.com/pytorch/audio/pull/2121 Reviewed By: carolineechen Differential Revision: D33389197 Pulled By: mthrok fbshipit-source-id: ebb051e894e17519a87094b8056d26e7a1cd3281 --- torchaudio/csrc/CMakeLists.txt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/torchaudio/csrc/CMakeLists.txt b/torchaudio/csrc/CMakeLists.txt index 586d288e61..b3bf7d3d81 100644 --- a/torchaudio/csrc/CMakeLists.txt +++ b/torchaudio/csrc/CMakeLists.txt @@ -146,15 +146,11 @@ if (BUILD_CTC_DECODER) LIBTORCHAUDIO_DECODER_DEFINITIONS BUILD_CTC_DECODER ) - set( - LIBTORCHAUDIO_DECODER_DEPS - libtorchaudio - kenlm) define_library( libtorchaudio_decoder "${LIBTORCHAUDIO_DECODER_SOURCES}" "${PROJECT_SOURCE_DIR}" - "${LIBTORCHAUDIO_DECODER_DEPS}" + "torch;kenlm" "${LIBTORCHAUDIO_COMPILE_DEFINITIONS};${LIBTORCHAUDIO_DECODER_DEFINITIONS}" ) endif() @@ -185,7 +181,7 @@ define_library( libtorchaudio_ffmpeg "${LIBTORCHAUDIO_FFMPEG_SOURCES}" "${LIBTORCHAUDIO_INCLUDE_DIRS}" - "libtorchaudio;ffmpeg" + "torch;ffmpeg" "${LIBTORCHAUDIO_COMPILE_DEFINITIONS}" ) endif() @@ -261,7 +257,7 @@ if (BUILD_TORCHAUDIO_PYTHON_EXTENSION) define_extension( _torchaudio_decoder "${DECODER_EXTENSION_SOURCES}" - "libtorchaudio;libtorchaudio_decoder" + "libtorchaudio_decoder" "${LIBTORCHAUDIO_DECODER_DEFINITIONS}" ) endif() From 10dcfc704463e63c9fcec14207d16df3c1a6b1c1 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Mon, 3 Jan 2022 10:50:28 -0800 Subject: [PATCH 0083/1144] Build ffmpeg-features on Conda binary dist (#2120) Summary: This commit enable ffmpeg-feature build on conda-based binary distribution on Linux and macOS. It adds `ffmpeg` as build-time dependencies and enable the build with `BUILD_FFMPEG=1`. Windows binaries, wheel-based binaries on Linux/macOS are not changed. Pull Request resolved: https://github.com/pytorch/audio/pull/2120 Reviewed By: nateanl Differential Revision: D33397473 Pulled By: mthrok fbshipit-source-id: 67a23a40c0614c56fee60cc06a45f3265037f6df --- packaging/torchaudio/build.sh | 2 +- packaging/torchaudio/meta.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/torchaudio/build.sh b/packaging/torchaudio/build.sh index d1fdb4cbf4..d0a1f30852 100644 --- a/packaging/torchaudio/build.sh +++ b/packaging/torchaudio/build.sh @@ -14,5 +14,5 @@ if [ "${USE_CUDA}" == "1" ] ; then fi fi shopt -u nocasematch - +export BUILD_FFMPEG=1 python setup.py install --single-version-externally-managed --record=record.txt diff --git a/packaging/torchaudio/meta.yaml b/packaging/torchaudio/meta.yaml index ef6d2b0269..0a79cd3d64 100644 --- a/packaging/torchaudio/meta.yaml +++ b/packaging/torchaudio/meta.yaml @@ -21,6 +21,7 @@ requirements: {{ environ.get('CONDA_PYTORCH_BUILD_CONSTRAINT', 'pytorch') }} {{ environ.get('CONDA_EXTRA_BUILD_CONSTRAINT', '') }} {{ environ.get('CONDA_CUDATOOLKIT_CONSTRAINT', '') }} + - ffmpeg >=4.1 # [not win] run: - python From df0175e84b758a9723989518544552b7f1ba5a44 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 4 Jan 2022 08:19:13 -0800 Subject: [PATCH 0084/1144] [CI] Install tools from conda instead of brew (#1873) Summary: Currently, macOS CI jobs install `pkg-config` and `wget` with `brew`. This is problematic as brew takes a long time with auto-update, and disabling the auto-update is not an ideal solution. Conda also distributes these packages, so switching to conda. Example issues with brew installation. https://app.circleci.com/pipelines/github/pytorch/audio/7825/workflows/53965bcf-6ddf-4e42-ad52-83fd1bbab717 This commit removes the use of `brew` by 1. Replacing the use of `wget` with `curl` (pre-installed in most distro) 2. Install `pkg-condig` from conda. Note: All the macOS jobs, including binary build jobs, uses conda. Using `pkg-config` from Conda makes it easy to discover the packages installed from conda. (like `ffmpeg` in https://github.com/pytorch/audio/issues/2122) 3. Add `pkg-config` to conda build-time dependency 4. Make sure that the availability of `pkg-config` is explicitly checked when `sox` is being configured. (otherwise, it will fail at somewhere in the middle of build process with somewhat unintuitve error message) Pull Request resolved: https://github.com/pytorch/audio/pull/1873 Reviewed By: carolineechen, nateanl Differential Revision: D33404975 Pulled By: mthrok fbshipit-source-id: ae512d3a3a422ebfe3b46c492bed44deecc36e72 --- .circleci/config.yml | 10 ---------- .circleci/config.yml.in | 10 ---------- .circleci/unittest/linux/scripts/setup_env.sh | 4 ++-- packaging/pkg_helpers.bash | 3 +++ packaging/torchaudio/meta.yaml | 1 + third_party/sox/CMakeLists.txt | 2 ++ 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fe8a86c4d5..53213e9a2c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,13 +38,6 @@ commands: our_upload_channel=test fi echo "export UPLOAD_CHANNEL=${our_upload_channel}" >> ${BASH_ENV} - install_build_tools_macos: - description: "installs tools required to build torchaudio" - steps: - - run: - name: Install build tools - command: HOMEBREW_NO_AUTO_UPDATE=1 brew install pkg-config wget - # Disable brew auto update which is very slow load_conda_channel_flags: description: "Determines whether we need extra conda channels" steps: @@ -199,7 +192,6 @@ jobs: xcode: "12.0" steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party @@ -225,7 +217,6 @@ jobs: xcode: "12.0" steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party @@ -560,7 +551,6 @@ jobs: resource_class: large steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 5a52df7cc4..70dac97c5b 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -38,13 +38,6 @@ commands: our_upload_channel=test fi echo "export UPLOAD_CHANNEL=${our_upload_channel}" >> ${BASH_ENV} - install_build_tools_macos: - description: "installs tools required to build torchaudio" - steps: - - run: - name: Install build tools - command: HOMEBREW_NO_AUTO_UPDATE=1 brew install pkg-config wget - # Disable brew auto update which is very slow load_conda_channel_flags: description: "Determines whether we need extra conda channels" steps: @@ -199,7 +192,6 @@ jobs: xcode: "12.0" steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party @@ -225,7 +217,6 @@ jobs: xcode: "12.0" steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party @@ -560,7 +551,6 @@ jobs: resource_class: large steps: - checkout - - install_build_tools_macos - load_conda_channel_flags - attach_workspace: at: third_party diff --git a/.circleci/unittest/linux/scripts/setup_env.sh b/.circleci/unittest/linux/scripts/setup_env.sh index 541330a4e5..c8d577ddf9 100755 --- a/.circleci/unittest/linux/scripts/setup_env.sh +++ b/.circleci/unittest/linux/scripts/setup_env.sh @@ -21,7 +21,7 @@ esac # 1. Install conda at ./conda if [ ! -d "${conda_dir}" ]; then printf "* Installing conda\n" - wget --quiet -O miniconda.sh "http://repo.continuum.io/miniconda/Miniconda3-latest-${os}-x86_64.sh" + curl --silent -L -o miniconda.sh "http://repo.continuum.io/miniconda/Miniconda3-latest-${os}-x86_64.sh" bash ./miniconda.sh -b -f -p "${conda_dir}" eval "$("${conda_dir}/bin/conda" shell.bash hook)" printf "* Updating the base Python version to %s\n" "${PYTHON_VERSION}" @@ -40,4 +40,4 @@ conda activate "${env_dir}" # 3. Install minimal build tools pip --quiet install cmake ninja -conda install --quiet -y -c conda-forge 'ffmpeg>4.1' pkg-config +conda install --quiet -y 'ffmpeg>=4.1' pkg-config diff --git a/packaging/pkg_helpers.bash b/packaging/pkg_helpers.bash index 0fc639c96b..204c38ea63 100644 --- a/packaging/pkg_helpers.bash +++ b/packaging/pkg_helpers.bash @@ -182,6 +182,9 @@ setup_wheel_python() { conda env remove -n "env$PYTHON_VERSION" || true conda create -yn "env$PYTHON_VERSION" python="$PYTHON_VERSION" conda activate "env$PYTHON_VERSION" + if [[ "$(uname)" == Darwin ]]; then + conda install --quiet -y pkg-config + fi else case "$PYTHON_VERSION" in 2.7) diff --git a/packaging/torchaudio/meta.yaml b/packaging/torchaudio/meta.yaml index 0a79cd3d64..3aef2c694f 100644 --- a/packaging/torchaudio/meta.yaml +++ b/packaging/torchaudio/meta.yaml @@ -14,6 +14,7 @@ requirements: host: - python - setuptools + - pkg-config # [not win] - cmake - ninja - defaults::numpy >=1.11 diff --git a/third_party/sox/CMakeLists.txt b/third_party/sox/CMakeLists.txt index ff35e70bff..46fa5806d3 100644 --- a/third_party/sox/CMakeLists.txt +++ b/third_party/sox/CMakeLists.txt @@ -1,3 +1,5 @@ +find_package(PkgConfig REQUIRED) + include(ExternalProject) set(INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../install) From 614a7dac4050b39ab0f072660a7f30bed0adc9b7 Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Tue, 4 Jan 2022 08:23:06 -0800 Subject: [PATCH 0085/1144] Add feature mean square value to HuBERT Pretrain model output (#2128) Summary: In [Fairseq](https://github.com/pytorch/fairseq/blob/main/examples/hubert/config/pretrain/hubert_base_librispeech.yaml#L48), the training applies additional penalty loss besides the cross-entropy losses. This PR adds the feature's mean square value to the model output to support such penalty loss. Pull Request resolved: https://github.com/pytorch/audio/pull/2128 Reviewed By: mthrok Differential Revision: D33403972 Pulled By: nateanl fbshipit-source-id: f08fefa2c975a847c6075171b310f57c1980309d --- torchaudio/models/wav2vec2/model.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/torchaudio/models/wav2vec2/model.py b/torchaudio/models/wav2vec2/model.py index 89986584f6..8380e3b35e 100644 --- a/torchaudio/models/wav2vec2/model.py +++ b/torchaudio/models/wav2vec2/model.py @@ -172,15 +172,19 @@ def forward( have valid length. Default: ``None``. Returns: - (Tensor, Tensor): + (Tensor, Tensor, Tensor): Tensor The masked sequences of probability distribution (in logit). Shape: `(masked_frames, num labels)`. Tensor The unmasked sequence of probability distribution (in logit). Shape: `(unmasked_frames, num labels)`. + Tensor + The feature mean value for additional penalty loss. + Shape: `(1,)`. """ x, lengths = self.wav2vec2.feature_extractor(waveforms, audio_lengths) + features_pen = x.float().pow(2).mean() if lengths is not None: padding_mask = components._get_padding_mask(x, lengths) else: @@ -188,7 +192,8 @@ def forward( x, attention_mask = self.wav2vec2.encoder._preprocess(x, lengths) x, mask = self.mask_generator(x, padding_mask) x = self.wav2vec2.encoder.transformer(x, attention_mask=attention_mask) - if padding_mask: + assert x.shape[1] == labels.shape[1], "The length of label must match that of HuBERT model output" + if padding_mask is not None: mask_m = torch.logical_and(~padding_mask, mask) mask_u = torch.logical_and(~padding_mask, ~mask_m) else: @@ -197,7 +202,7 @@ def forward( logit_m, logit_u = self.logit_generator(x, labels, mask_m, mask_u) - return logit_m, logit_u + return logit_m, logit_u, features_pen def wav2vec2_model( From 6a470649d44eb11318c4f50d0ce2c4052d7c0671 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 4 Jan 2022 10:27:48 -0800 Subject: [PATCH 0086/1144] Adopt FindFFMPEG.cmake (#2125) Summary: Preparation for updating the build process of ffmpeg-related feature to support Windows. Checking-in FindFFMPEG.cmake from Kitware/VTK repo without modification so that later it's easy to follow our modification on it with git tool. Pull Request resolved: https://github.com/pytorch/audio/pull/2125 Reviewed By: carolineechen Differential Revision: D33405408 Pulled By: mthrok fbshipit-source-id: 5faea8940d2dfcf0b2f647eda3754f713d21fcd1 --- cmake/FindFFMPEG.cmake | 236 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 cmake/FindFFMPEG.cmake diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake new file mode 100644 index 0000000000..c5a5f6c646 --- /dev/null +++ b/cmake/FindFFMPEG.cmake @@ -0,0 +1,236 @@ +#[==[ + +Originally taken from: https://github.com/Kitware/VTK/blob/8485477f9aa41f3c33094c3beb201e747abf5541/CMake/FindFFMPEG.cmake +License: https://github.com/Kitware/VTK/blob/8485477f9aa41f3c33094c3beb201e747abf5541/Copyright.txt + +/*========================================================================= + + Program: Visualization Toolkit + Module: Copyright.txt + +Copyright (c) 1993-2015 Ken Martin, Will Schroeder, Bill Lorensen +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither name of Ken Martin, Will Schroeder, or Bill Lorensen nor the names + of any contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + +========================================================================= + +Provides the following variables: + + * `FFMPEG_INCLUDE_DIRS`: Include directories necessary to use FFMPEG. + * `FFMPEG_LIBRARIES`: Libraries necessary to use FFMPEG. Note that this only + includes libraries for the components requested. + * `FFMPEG_VERSION`: The version of FFMPEG found. + +The following components are supported: + + * `avcodec` + * `avdevice` + * `avfilter` + * `avformat` + * `avresample` + * `avutil` + * `swresample` + * `swscale` + +For each component, the following are provided: + + * `FFMPEG__FOUND`: Libraries for the component. + * `FFMPEG__INCLUDE_DIRS`: Include directories for + the component. + * `FFMPEG__LIBRARIES`: Libraries for the component. + * `FFMPEG::`: A target to use with `target_link_libraries`. + +Note that only components requested with `COMPONENTS` or `OPTIONAL_COMPONENTS` +are guaranteed to set these variables or provide targets. +#]==] + +function (_ffmpeg_find component headername) + find_path("FFMPEG_${component}_INCLUDE_DIR" + NAMES + "lib${component}/${headername}" + PATHS + "${FFMPEG_ROOT}/include" + ~/Library/Frameworks + /Library/Frameworks + /usr/local/include + /usr/include + /sw/include # Fink + /opt/local/include # DarwinPorts + /opt/csw/include # Blastwave + /opt/include + /usr/freeware/include + PATH_SUFFIXES + ffmpeg + DOC "FFMPEG's ${component} include directory") + mark_as_advanced("FFMPEG_${component}_INCLUDE_DIR") + + # On Windows, static FFMPEG is sometimes built as `lib.a`. + if (WIN32) + list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".lib") + list(APPEND CMAKE_FIND_LIBRARY_PREFIXES "" "lib") + endif () + + find_library("FFMPEG_${component}_LIBRARY" + NAMES + "${component}" + PATHS + "${FFMPEG_ROOT}/lib" + ~/Library/Frameworks + /Library/Frameworks + /usr/local/lib + /usr/local/lib64 + /usr/lib + /usr/lib64 + /sw/lib + /opt/local/lib + /opt/csw/lib + /opt/lib + /usr/freeware/lib64 + "${FFMPEG_ROOT}/bin" + DOC "FFMPEG's ${component} library") + mark_as_advanced("FFMPEG_${component}_LIBRARY") + + if (FFMPEG_${component}_LIBRARY AND FFMPEG_${component}_INCLUDE_DIR) + set(_deps_found TRUE) + set(_deps_link) + foreach (_ffmpeg_dep IN LISTS ARGN) + if (TARGET "FFMPEG::${_ffmpeg_dep}") + list(APPEND _deps_link "FFMPEG::${_ffmpeg_dep}") + else () + set(_deps_found FALSE) + endif () + endforeach () + if (_deps_found) + if (NOT TARGET "FFMPEG::${component}") + add_library("FFMPEG::${component}" UNKNOWN IMPORTED) + set_target_properties("FFMPEG::${component}" PROPERTIES + IMPORTED_LOCATION "${FFMPEG_${component}_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIR}" + IMPORTED_LINK_INTERFACE_LIBRARIES "${_deps_link}") + endif () + set("FFMPEG_${component}_FOUND" 1 + PARENT_SCOPE) + + set(version_header_path "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version.h") + if (EXISTS "${version_header_path}") + string(TOUPPER "${component}" component_upper) + file(STRINGS "${version_header_path}" version + REGEX "#define *LIB${component_upper}_VERSION_(MAJOR|MINOR|MICRO) ") + string(REGEX REPLACE ".*_MAJOR *\([0-9]*\).*" "\\1" major "${version}") + string(REGEX REPLACE ".*_MINOR *\([0-9]*\).*" "\\1" minor "${version}") + string(REGEX REPLACE ".*_MICRO *\([0-9]*\).*" "\\1" micro "${version}") + if (NOT major STREQUAL "" AND + NOT minor STREQUAL "" AND + NOT micro STREQUAL "") + set("FFMPEG_${component}_VERSION" "${major}.${minor}.${micro}" + PARENT_SCOPE) + endif () + endif () + else () + set("FFMPEG_${component}_FOUND" 0 + PARENT_SCOPE) + set(what) + if (NOT FFMPEG_${component}_LIBRARY) + set(what "library") + endif () + if (NOT FFMPEG_${component}_INCLUDE_DIR) + if (what) + string(APPEND what " or headers") + else () + set(what "headers") + endif () + endif () + set("FFMPEG_${component}_NOT_FOUND_MESSAGE" + "Could not find the ${what} for ${component}." + PARENT_SCOPE) + endif () + endif () +endfunction () + +_ffmpeg_find(avutil avutil.h) +_ffmpeg_find(avresample avresample.h + avutil) +_ffmpeg_find(swresample swresample.h + avutil) +_ffmpeg_find(swscale swscale.h + avutil) +_ffmpeg_find(avcodec avcodec.h + avutil) +_ffmpeg_find(avformat avformat.h + avcodec avutil) +_ffmpeg_find(avfilter avfilter.h + avutil) +_ffmpeg_find(avdevice avdevice.h + avformat avutil) + +if (TARGET FFMPEG::avutil) + set(_ffmpeg_version_header_path "${FFMPEG_avutil_INCLUDE_DIR}/libavutil/ffversion.h") + if (EXISTS "${_ffmpeg_version_header_path}") + file(STRINGS "${_ffmpeg_version_header_path}" _ffmpeg_version + REGEX "FFMPEG_VERSION") + string(REGEX REPLACE ".*\"n?\(.*\)\"" "\\1" FFMPEG_VERSION "${_ffmpeg_version}") + unset(_ffmpeg_version) + else () + set(FFMPEG_VERSION FFMPEG_VERSION-NOTFOUND) + endif () + unset(_ffmpeg_version_header_path) +endif () + +set(FFMPEG_INCLUDE_DIRS) +set(FFMPEG_LIBRARIES) +set(_ffmpeg_required_vars) +foreach (_ffmpeg_component IN LISTS FFMPEG_FIND_COMPONENTS) + if (TARGET "FFMPEG::${_ffmpeg_component}") + set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS + "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") + set(FFMPEG_${_ffmpeg_component}_LIBRARIES + "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + list(APPEND FFMPEG_INCLUDE_DIRS + "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS}") + list(APPEND FFMPEG_LIBRARIES + "${FFMPEG_${_ffmpeg_component}_LIBRARIES}") + if (FFMEG_FIND_REQUIRED_${_ffmpeg_component}) + list(APPEND _ffmpeg_required_vars + "FFMPEG_${_ffmpeg_required_vars}_INCLUDE_DIRS" + "FFMPEG_${_ffmpeg_required_vars}_LIBRARIES") + endif () + endif () +endforeach () +unset(_ffmpeg_component) + +if (FFMPEG_INCLUDE_DIRS) + list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS) +endif () + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(FFMPEG + REQUIRED_VARS FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES ${_ffmpeg_required_vars} + VERSION_VAR FFMPEG_VERSION + HANDLE_COMPONENTS) +unset(_ffmpeg_required_vars) From 203305d3ee1d0b070fdd8cb195800d044f8d2822 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Tue, 4 Jan 2022 12:24:07 -0800 Subject: [PATCH 0087/1144] Update third party submodule setup (#2132) Summary: update the logic for initializing and updating submodule in setup, now that we have more than one submodule Pull Request resolved: https://github.com/pytorch/audio/pull/2132 Reviewed By: mthrok Differential Revision: D33410039 Pulled By: carolineechen fbshipit-source-id: 6e18ec2904a01efb2f753f65345de063218c0b9b --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 439e3c12eb..73a95d34d3 100644 --- a/setup.py +++ b/setup.py @@ -123,8 +123,7 @@ def _fetch_archives(src): def _fetch_third_party_libraries(): - if not (ROOT_DIR / "third_party" / "kaldi" / "submodule" / "CMakeLists.txt").exists(): - _init_submodule() + _init_submodule() if os.name != "nt": _fetch_archives(_parse_sources()) From 832f055a9b44c31c70aed39f72abf5e3eb6d7584 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 4 Jan 2022 12:36:35 -0800 Subject: [PATCH 0088/1144] Add custom CSS to make signatures appear in multi-line (#2123) Summary: * Before https://pytorch.org/audio/main/models.html Screen Shot 2022-01-04 at 11 00 12 AM *After https://503135-90321822-gh.circle-artifacts.com/0/docs/models.html Screen Shot 2022-01-04 at 10 59 40 AM Pull Request resolved: https://github.com/pytorch/audio/pull/2123 Reviewed By: carolineechen Differential Revision: D33409661 Pulled By: mthrok fbshipit-source-id: bb2dffea25ccc4356d257b2ab4a6e88f7f4e2bb3 --- docs/source/_static/css/custom.css | 11 +++++++++++ docs/source/conf.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/source/_static/css/custom.css diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css new file mode 100644 index 0000000000..a5eab9b625 --- /dev/null +++ b/docs/source/_static/css/custom.css @@ -0,0 +1,11 @@ +/* https://github.com/sphinx-doc/sphinx/issues/1514#issuecomment-742703082 */ +/* Newlines (\a) and spaces (\20) before each parameter */ +dt > em.sig-param:before { + content: "\a\20\20\20\20"; + white-space: pre; +} +/* Newline after the last parameter (so the closing bracket is on a new line) */ +dt > em.sig-param:last-of-type::after { + content: "\a"; + white-space: pre; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 46ebe8f6f9..63f6635326 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -199,7 +199,7 @@ def _get_pattern(): # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -html_css_files = ["https://cdn.jsdelivr.net/npm/katex@0.10.0-beta/dist/katex.min.css"] +html_css_files = ["https://cdn.jsdelivr.net/npm/katex@0.10.0-beta/dist/katex.min.css", "css/custom.css"] # -- Options for HTMLHelp output ------------------------------------------ From 601acce16e9677058bb6a4126da4f78dee29f548 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 4 Jan 2022 12:50:08 -0800 Subject: [PATCH 0089/1144] Build ffmpeg-features on macOS wheel binary dist (#2122) Summary: This commit enable ffmpeg-feature build on wheel-based binary distribution on macOS. For macOS, since the underlying Python env is Conda, it installs `ffmpeg` from conda. Depends on https://github.com/pytorch/audio/issues/1873 Pull Request resolved: https://github.com/pytorch/audio/pull/2122 Reviewed By: carolineechen, nateanl Differential Revision: D33409113 Pulled By: mthrok fbshipit-source-id: a73839087548010353422109b33e89e262c12a57 --- .circleci/config.yml | 2 ++ .circleci/config.yml.in | 2 ++ packaging/pkg_helpers.bash | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 53213e9a2c..755cc8e126 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -204,6 +204,8 @@ jobs: sh conda.sh -b source $HOME/miniconda3/bin/activate packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 70dac97c5b..b7cacecc9a 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -204,6 +204,8 @@ jobs: sh conda.sh -b source $HOME/miniconda3/bin/activate packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: diff --git a/packaging/pkg_helpers.bash b/packaging/pkg_helpers.bash index 204c38ea63..c86b496632 100644 --- a/packaging/pkg_helpers.bash +++ b/packaging/pkg_helpers.bash @@ -183,7 +183,7 @@ setup_wheel_python() { conda create -yn "env$PYTHON_VERSION" python="$PYTHON_VERSION" conda activate "env$PYTHON_VERSION" if [[ "$(uname)" == Darwin ]]; then - conda install --quiet -y pkg-config + conda install --quiet -y pkg-config "ffmpeg>=4.1" fi else case "$PYTHON_VERSION" in From 352aeb7a70b49b1402fd285f58132ab4a6ad4c98 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:51:29 -0800 Subject: [PATCH 0090/1144] Fix header include (#2135) Summary: MSVS is strict on header and would not allow `std::runtime_error` without ``. Also this commit removes the stray `"libavutil/frame.h"`, inserted by IDE, which I missed. Ref https://github.com/pytorch/audio/issues/2124 Pull Request resolved: https://github.com/pytorch/audio/pull/2135 Reviewed By: carolineechen Differential Revision: D33419120 Pulled By: mthrok fbshipit-source-id: 328e723b70a3608133d9ddef9fc4a95e5d90e61d --- torchaudio/csrc/ffmpeg/ffmpeg.cpp | 1 + torchaudio/csrc/ffmpeg/prototype.cpp | 1 + torchaudio/csrc/ffmpeg/sink.cpp | 1 + torchaudio/csrc/ffmpeg/stream_processor.cpp | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/torchaudio/csrc/ffmpeg/ffmpeg.cpp b/torchaudio/csrc/ffmpeg/ffmpeg.cpp index a679346e3a..51bf631a63 100644 --- a/torchaudio/csrc/ffmpeg/ffmpeg.cpp +++ b/torchaudio/csrc/ffmpeg/ffmpeg.cpp @@ -1,4 +1,5 @@ #include +#include namespace torchaudio { namespace ffmpeg { diff --git a/torchaudio/csrc/ffmpeg/prototype.cpp b/torchaudio/csrc/ffmpeg/prototype.cpp index 7ff9415c71..0df16ffdcc 100644 --- a/torchaudio/csrc/ffmpeg/prototype.cpp +++ b/torchaudio/csrc/ffmpeg/prototype.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace torchaudio { namespace ffmpeg { diff --git a/torchaudio/csrc/ffmpeg/sink.cpp b/torchaudio/csrc/ffmpeg/sink.cpp index 5be77578df..1cfcef6d83 100644 --- a/torchaudio/csrc/ffmpeg/sink.cpp +++ b/torchaudio/csrc/ffmpeg/sink.cpp @@ -1,4 +1,5 @@ #include +#include namespace torchaudio { namespace ffmpeg { diff --git a/torchaudio/csrc/ffmpeg/stream_processor.cpp b/torchaudio/csrc/ffmpeg/stream_processor.cpp index 93bd0d50fd..12ee581d05 100644 --- a/torchaudio/csrc/ffmpeg/stream_processor.cpp +++ b/torchaudio/csrc/ffmpeg/stream_processor.cpp @@ -1,5 +1,5 @@ #include -#include "libavutil/frame.h" +#include namespace torchaudio { namespace ffmpeg { From 5c4c61b23cefae9c7231a5fd877d4653ac79cbfc Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Tue, 4 Jan 2022 20:48:55 -0800 Subject: [PATCH 0091/1144] Add librispeech inference script (#2130) Summary: add script for running CTC beam search decoder on librispeech dataset with torchaudio pretrained wav2vec2 models Pull Request resolved: https://github.com/pytorch/audio/pull/2130 Reviewed By: mthrok Differential Revision: D33419436 Pulled By: carolineechen fbshipit-source-id: 0a0d00f4c17ecdbb497c9eda78673aa939d73c57 --- .../asr/librispeech_ctc_decoder/README.md | 27 ++++ .../asr/librispeech_ctc_decoder/inference.py | 134 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 examples/asr/librispeech_ctc_decoder/README.md create mode 100644 examples/asr/librispeech_ctc_decoder/inference.py diff --git a/examples/asr/librispeech_ctc_decoder/README.md b/examples/asr/librispeech_ctc_decoder/README.md new file mode 100644 index 0000000000..d86f6b6ad2 --- /dev/null +++ b/examples/asr/librispeech_ctc_decoder/README.md @@ -0,0 +1,27 @@ +# Speech Recognition Inference with CTC Beam Search Decoder + +This is an example inference script for running decoding on the LibriSpeech dataset and wav2vec 2.0 models, using a CTC beam search decoder that supports lexicon constraint and language model integration. The language model used is a 4-gram KenLM trained on the LibriSpeech dataset. + +## Usage +Additional command line parameters and information can is available with the `--help` option. + +Sample command + +``` +python inference.py \ + --librispeech_path ./librispeech/ \ + --split test-other \ + --model WAV2VEC2_ASR_BASE_960H \ + --beam-size 1500 \ + --lm-weight 1.74 \ + --word-score 0.52 +``` + +## Results +The table below contains WER results for various pretrained models on the LibriSpeech test-other split, using a beam size of 1500, and language model weight and word insertion scores taken from Table 7 of [wav2vec 2.0](https://arxiv.org/pdf/2006.11477.pdf). + +| Model | WER | +|:----------------------------------------------------------------------------------------------:|--------:| +| [WAV2VEC2_ASR_BASE_10M](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-10m) | 0.1591| +| [WAV2VEC2_ASR_BASE_100H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-100h) | 0.0807| +| [WAV2VEC2_ASR_BASE_960H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-960h) | 0.0615| diff --git a/examples/asr/librispeech_ctc_decoder/inference.py b/examples/asr/librispeech_ctc_decoder/inference.py new file mode 100644 index 0000000000..93d1c5cd37 --- /dev/null +++ b/examples/asr/librispeech_ctc_decoder/inference.py @@ -0,0 +1,134 @@ +import argparse +import logging +from typing import Optional + +import torch +import torchaudio +from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder + + +logger = logging.getLogger(__name__) + + +def _download_files(lexicon_file, kenlm_file): + torch.hub.download_url_to_file( + "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/lexicon-librispeech.txt", lexicon_file + ) + torch.hub.download_url_to_file( + "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/4-gram-librispeech.bin", kenlm_file + ) + + +def run_inference(args): + # get pretrained wav2vec2.0 model + bundle = getattr(torchaudio.pipelines, args.model) + model = bundle.get_model() + tokens = [label.lower() for label in bundle.get_labels()] + + # get decoder files + hub_dir = torch.hub.get_dir() + lexicon_file = f"{hub_dir}/lexicon.txt" + kenlm_file = f"{hub_dir}/kenlm.bin" + _download_files(lexicon_file, kenlm_file) + + decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens, + kenlm=kenlm_file, + nbest=1, + beam_size=1500, + beam_size_token=None, + beam_threshold=50, + lm_weight=args.lm_weight, + word_score=args.word_score, + unk_score=float("-inf"), + sil_score=0, + log_add=False, + ) + + dataset = torchaudio.datasets.LIBRISPEECH(args.librispeech_path, url=args.split, download=False) + + total_edit_distance = 0 + total_length = 0 + for idx, sample in enumerate(dataset): + waveform, _, transcript, _, _, _ = sample + transcript = transcript.strip().lower().strip() + + with torch.inference_mode(): + emission, _ = model(waveform) + results = decoder(emission) + + total_edit_distance += torchaudio.functional.edit_distance(transcript.split(), results[0][0].words) + total_length += len(transcript.split()) + + if idx % 100 == 0: + logger.info(f"Processed elem {idx}; WER: {total_edit_distance / total_length}") + logger.info(f"Final WER: {total_edit_distance / total_length}") + + +def _parse_args(): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "--librispeech_path", + type=str, + help="folder where LibriSpeech is stored", + ) + parser.add_argument( + "--split", + type=str, + help="LibriSpeech dataset split", + choices=["dev-clean", "dev-other", "test-clean", "test-other"], + default="test-other", + ) + parser.add_argument( + "--model", + type=str, + default="WAV2VEC2_ASR_BASE_960H", + help="pretrained Wav2Vec2 model from torchaudio.pipelines", + ) + parser.add_argument("--nbest", type=int, default=1, help="number of best hypotheses to return") + parser.add_argument( + "--beam-size", type=int, default=500, help="beam size for determining number of hypotheses to store" + ) + parser.add_argument( + "--beam-size-token", + type=Optional[int], + default=None, + help="number of tokens to consider at each beam search step", + ) + parser.add_argument("--beam-threshold", type=int, default=50, help="beam threshold for pruning hypotheses") + parser.add_argument( + "--lm-weight", + type=float, + default=1.74, + help="languge model weight", + ) + parser.add_argument( + "--word-score", + type=float, + default=0.52, + help="word insertion score", + ) + parser.add_argument("--unk_score", type=float, default=float("-inf"), help="unknown word insertion score") + parser.add_argument("--sil_score", type=float, default=0, help="silence insertion score") + parser.add_argument("--debug", action="store_true", help="whether to use debug level for logging") + return parser.parse_args() + + +def _init_logger(debug): + fmt = "%(asctime)s %(message)s" if debug else "%(message)s" + level = logging.DEBUG if debug else logging.INFO + logging.basicConfig(format=fmt, level=level, datefmt="%Y-%m-%d %H:%M:%S") + + +def _main(): + args = _parse_args() + _init_logger(args.debug) + run_inference(args) + + +if __name__ == "__main__": + _main() From 6854eedfc73a1160b2f7f48473c4ff2af8477a96 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Wed, 5 Jan 2022 00:41:45 -0800 Subject: [PATCH 0092/1144] Update CTC decoder docs (#2136) Summary: after addition of tutorial and librispeech example with [WER results](https://github.com/pytorch/audio/pull/2130/files#diff-5f82be20f11a10a4cb411007df965b84eaab98fcc7ed49b42be1dd9916203193R20), we can remove README from decoder csrc directory and move the rest of the information to the corresponding documentation/main README Pull Request resolved: https://github.com/pytorch/audio/pull/2136 Reviewed By: mthrok Differential Revision: D33419449 Pulled By: carolineechen fbshipit-source-id: 6cb29280f639af46834bec935b45f3c5a8ee350f --- README.md | 2 +- torchaudio/csrc/decoder/README.md | 36 ------------------- .../prototype/ctc_decoder/ctc_decoder.py | 16 +++++++-- 3 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 torchaudio/csrc/decoder/README.md diff --git a/README.md b/README.md index 456a209803..e2f03fcd47 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Please refer to https://pytorch.org/get-started/locally/ for the details. ### From Source On non-Windows platforms, the build process builds libsox and codecs that torchaudio need to link to. It will fetch and build libmad, lame, flac, vorbis, opus, and libsox before building extension. This process requires `cmake` and `pkg-config`. libsox-based features can be disabled with `BUILD_SOX=0`. -The build process also builds the RNN transducer loss. This functionality can be disabled by setting the environment variable `BUILD_RNNT=0`. +The build process also builds the RNN transducer loss and CTC beam search decoder. These functionalities can be disabled by setting the environment variable `BUILD_RNNT=0` and `BUILD_CTC_DECODER=0`, respectively. ```bash # Linux diff --git a/torchaudio/csrc/decoder/README.md b/torchaudio/csrc/decoder/README.md deleted file mode 100644 index 4e7e017e61..0000000000 --- a/torchaudio/csrc/decoder/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Flashlight Decoder Binding -CTC Decoder with KenLM and lexicon support based on [flashlight](https://github.com/flashlight/flashlight) decoder implementation -and fairseq [KenLMDecoder](https://github.com/pytorch/fairseq/blob/fcca32258c8e8bcc9f9890bf4714fa2f96b6b3e1/examples/speech_recognition/new/decoders/flashlight_decoder.py#L53) -Python wrapper - -## Setup -### Build torchaudio with decoder support -``` -BUILD_CTC_DECODER=1 python setup.py develop -``` - -## Usage -```py -from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder -decoder = kenlm_lexicon_decoder(args...) -results = decoder(emissions) # dim (B, nbest) of dictionary of "tokens", "score", "words" keys -best_transcripts = [" ".join(results[i][0].words).strip() for i in range(B)] -``` - -## Required Files -- tokens: tokens for which the acoustic model generates probabilities for -- lexicon: mapping between words and its corresponding spelling -- language model: n-gram KenLM model - -## Experiment Results -LibriSpeech dev-other and test-other results using pretrained [Wav2Vec2](https://arxiv.org/pdf/2006.11477.pdf) models of -BASE configuration. - -| Model | Decoder | dev-other | test-other | beam search params | -| ----------- | ---------- | ----------- | ---------- |-------------------------------------------- | -| BASE_10M | Greedy | 51.6 | 51 | | -| | 4-gram LM | 15.95 | 15.9 | LM weight=3.23, word score=-0.26, beam=1500 | -| BASE_100H | Greedy | 13.6 | 13.3 | | -| | 4-gram LM | 8.5 | 8.8 | LM weight=2.15, word score=-0.52, beam=50 | -| BASE_960H | Greedy | 8.9 | 8.4 | | -| | 4-gram LM | 6.3 | 6.4 | LM weight=1.74, word score=0.52, beam=50 | diff --git a/torchaudio/prototype/ctc_decoder/ctc_decoder.py b/torchaudio/prototype/ctc_decoder/ctc_decoder.py index 1af27c5631..94832e2917 100644 --- a/torchaudio/prototype/ctc_decoder/ctc_decoder.py +++ b/torchaudio/prototype/ctc_decoder/ctc_decoder.py @@ -181,15 +181,17 @@ def kenlm_lexicon_decoder( Builds Ken LM CTC Lexicon Decoder with given parameters Args: - lexicon (str): lexicon file containing the possible words - tokens (str or List[str]): file or list containing valid tokens + lexicon (str): lexicon file containing the possible words and corresponding spellings. + Each line consists of a word and its space separated spelling + tokens (str or List[str]): file or list containing valid tokens. If using a file, the expected + format is for tokens mapping to the same index to be on the same line kenlm (str): file containing languge model nbest (int, optional): number of best decodings to return (Default: 1) beam_size (int, optional): max number of hypos to hold after each decode step (Default: 50) beam_size_token (int, optional): max number of tokens to consider at each decode step. If None, it is set to the total number of tokens (Default: None) beam_threshold (float, optional): threshold for pruning hypothesis (Default: 50) - lm_weight (float, optional): weight of lm (Default: 2) + lm_weight (float, optional): weight of language model (Default: 2) word_score (float, optional): word insertion score (Default: 0) unk_score (float, optional): unknown word insertion score (Default: -inf) sil_score (float, optional): silence insertion score (Default: 0) @@ -200,6 +202,14 @@ def kenlm_lexicon_decoder( Returns: KenLMLexiconDecoder: decoder + + Example + >>> decoder = kenlm_lexicon_decoder( + >>> lexicon="lexicon.txt", + >>> tokens="tokens.txt", + >>> kenlm="kenlm.bin", + >>> ) + >>> results = decoder(emissions) # List of shape (B, nbest) of Hypotheses """ lexicon = _load_words(lexicon) word_dict = _create_word_dict(lexicon) From d8a654502208ebbdf3c25f3305492b943c6ffaf9 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 5 Jan 2022 07:41:36 -0800 Subject: [PATCH 0093/1144] Update ffmpeg discovery logic (#2124) Summary: Update ffmpeg discovery logic Previously the build process used pkg-config to locate an installation of ffmpeg, which does not work well Windows/CentOS. This commit update the discovery process to use the custom FindFFMPEG.cmake adopted from Kitware/VTK repository with addition of conda environment. The custom discovery logic can support Windows and CentOS. Pull Request resolved: https://github.com/pytorch/audio/pull/2124 Reviewed By: carolineechen Differential Revision: D33429564 Pulled By: mthrok fbshipit-source-id: 6cb50c1d8c58f51e0f3f3af5c5b541aa3a699bba --- CMakeLists.txt | 1 + cmake/FindFFMPEG.cmake | 2 ++ third_party/CMakeLists.txt | 27 ++++----------------------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2249cdd094..76827ebe53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ option(USE_CUDA "Enable CUDA support" OFF) option(USE_ROCM "Enable ROCM support" OFF) option(USE_OPENMP "Enable OpenMP support" OFF) +set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake") # check that USE_CUDA and USE_ROCM are not set at the same time if(USE_CUDA AND USE_ROCM) diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index c5a5f6c646..a73ed951be 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -76,6 +76,7 @@ function (_ffmpeg_find component headername) "lib${component}/${headername}" PATHS "${FFMPEG_ROOT}/include" + "$ENV{CONDA_PREFIX}/include" ~/Library/Frameworks /Library/Frameworks /usr/local/include @@ -101,6 +102,7 @@ function (_ffmpeg_find component headername) "${component}" PATHS "${FFMPEG_ROOT}/lib" + "$ENV{CONDA_PREFIX}/lib" ~/Library/Frameworks /Library/Frameworks /usr/local/lib diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index f121c287c3..da92b17164 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -17,30 +17,11 @@ endif() # ffmpeg ################################################################################ if (BUILD_FFMPEG) - if(WIN32) - message(FATAL_ERROR "FFmpeg integration is not supported on Windows.") - # TODO - # Since pkg-config is not well-supported on Windows (for example, ffmpeg - # installed from conda-forge does not include `.pc` medata file), - # to support windows, we need to fill interface manually. - # - # Something like this might work - # https://github.com/microsoft/vcpkg/issues/1379#issuecomment-312483740 - endif() - find_package(PkgConfig REQUIRED) - pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET - # requires ffmpeg>=4.1 - libavdevice>=58 - libavfilter>=7 - libavformat>=58 - libavcodec>=58 - libswresample>=3 - libswscale>=3 - libavutil>=56 - ) + set(FFMPEG_FIND_COMPONENTS avdevice avfilter avformat avcodec avutil) + include(FindFFMPEG) add_library(ffmpeg INTERFACE) - target_include_directories(ffmpeg INTERFACE ${LIBAV_INCLUDE_DIRS}) - target_link_libraries(ffmpeg INTERFACE ${LIBAV_LINK_LIBRARIES}) + target_include_directories(ffmpeg INTERFACE ${FFMPEG_INCLUDE_DIRS}) + target_link_libraries(ffmpeg INTERFACE ${FFMPEG_LIBRARIES}) endif() ################################################################################ From 0a072f9a5cc1c8b23a4aed44873237404f4386c1 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 5 Jan 2022 08:24:05 -0800 Subject: [PATCH 0094/1144] Add minimal ffmpeg build to linux wheel job envs (#2137) Summary: This change adds a minimal ffmpeg installation step to the build wheel job so that later, we can use the resulting ffmpeg libraries for building torchaudio's ffmpeg-features. The linux wheel build jobs run in CentOS 8 based environment, which does not provide an easy way to install ffmpeg without conda. After https://github.com/pytorch/audio/pull/2124 is merged, then we can enable the ffmpeg-feature build in Linux wheel. Pull Request resolved: https://github.com/pytorch/audio/pull/2137 Reviewed By: carolineechen Differential Revision: D33430032 Pulled By: mthrok fbshipit-source-id: bf946d394c0718ddbdc679d7970befc3221982b9 --- .circleci/config.yml | 5 ++++- .circleci/config.yml.in | 5 ++++- tools/bootstrap_ffmpeg.sh | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100755 tools/bootstrap_ffmpeg.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 755cc8e126..c3101fe062 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -160,7 +160,10 @@ jobs: - checkout - attach_workspace: at: third_party - - run: packaging/build_wheel.sh + - run: + command: | + ./tools/bootstrap_ffmpeg.sh + packaging/build_wheel.sh - store_artifacts: path: dist - persist_to_workspace: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index b7cacecc9a..2a4495fff2 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -160,7 +160,10 @@ jobs: - checkout - attach_workspace: at: third_party - - run: packaging/build_wheel.sh + - run: + command: | + ./tools/bootstrap_ffmpeg.sh + packaging/build_wheel.sh - store_artifacts: path: dist - persist_to_workspace: diff --git a/tools/bootstrap_ffmpeg.sh b/tools/bootstrap_ffmpeg.sh new file mode 100755 index 0000000000..8153f03a26 --- /dev/null +++ b/tools/bootstrap_ffmpeg.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Helper script to install MINIMUM ffmpeg in centos:7. +# The goal of this script is to allow bootstrapping the ffmpeg-feature build +# for Linux/wheel build process which happens in centos-based Docker. +# It is not intended to build the useful feature subset of ffmpegs + +set -eux + +build_dir=$(mktemp -d -t ffmpeg-build-XXXXXXXXXX) +cleanup() { + echo rm -rf "${build_dir}" +} +trap cleanup EXIT + +cd "${build_dir}" + +wget --quiet -O ffmpeg.tar.gz https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n4.1.8.tar.gz +tar -xf ffmpeg.tar.gz --strip-components 1 +./configure \ + --disable-all \ + --disable-static \ + --enable-shared \ + --enable-pic \ + --disable-debug \ + --disable-doc \ + --disable-autodetect \ + --disable-x86asm \ + --enable-avcodec \ + --enable-avdevice \ + --enable-avfilter \ + --enable-avformat \ + --enable-avutil + +make -j install From 80ea3419155307107d055b94f12702844cd1edba Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Wed, 5 Jan 2022 13:54:24 -0800 Subject: [PATCH 0095/1144] Remove RNNTL unused vars (#2142) Summary: remove unnecessary RNNT Loss variables and comment as indicated in https://github.com/pytorch/audio/issues/1479 review comments (will follow up on `workspace` comments separately depending on complexity) Pull Request resolved: https://github.com/pytorch/audio/pull/2142 Reviewed By: mthrok Differential Revision: D33433764 Pulled By: carolineechen fbshipit-source-id: be0ecb77dabd63d733f0d33ff258eae32305eeaf --- torchaudio/csrc/rnnt/cpu/cpu_kernels.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/torchaudio/csrc/rnnt/cpu/cpu_kernels.h b/torchaudio/csrc/rnnt/cpu/cpu_kernels.h index 468cb41887..2b5b88f0e0 100644 --- a/torchaudio/csrc/rnnt/cpu/cpu_kernels.h +++ b/torchaudio/csrc/rnnt/cpu/cpu_kernels.h @@ -444,7 +444,6 @@ void ComputeAlphas( TensorView({maxT, maxU}, alphas + b * maxT * maxU)); } - std::vector scores(B << 1); //#pragma omp parallel for for (int i = 0; i < B; ++i) { // use max 2 * B threads. ComputeAlphaOneSequence( @@ -481,9 +480,8 @@ void ComputeBetas( TensorView({maxT, maxU}, betas + b * maxT * maxU)); } - std::vector scores(B << 1); //#pragma omp parallel for - for (int i = 0; i < B; ++i) { // use max 2 * B threads. + for (int i = 0; i < B; ++i) { ComputeBetaOneSequence( options, /*logProbs=*/seqlogProbs[i], From 4f487c4ac09368796023c277e7ed35e8ca573945 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Wed, 5 Jan 2022 15:03:55 -0800 Subject: [PATCH 0096/1144] Do not auto-skip tests on CI (#2127) Summary: Update the internal of `skipIfXXX` decorators so that tests in CI will not be automatically skipped. Currently we automatically skip some tests based on the availability of related features/test tools. This causes issues where we miss signals on certain important features. (CUDA on Windows) https://github.com/pytorch/audio/issues/1565 The new `skipIf` decorator will fail if in CI unless it is explicitly allowed to skip tests. It does so by checking `CI` and `TORCHAUDIO_TEST_ALLOW_SKIP_IF_XXX` environment variables. For non-CI environments, the behavior is same as before, but users can now set `TORCHAUDIO_TEST_ALLOW_SKIP_IF_XXX=false` to disallow the automatic skip. Results without `TORCHAUDIO_TEST_ALLOW_SKIP_IF_XXX` https://app.circleci.com/pipelines/github/pytorch/audio/9112/workflows/4e6db046-a1a2-4965-b0fe-d5baf4a1efac Pull Request resolved: https://github.com/pytorch/audio/pull/2127 Reviewed By: hwangjeff Differential Revision: D33430711 Pulled By: mthrok fbshipit-source-id: d8954dd720469c5ab0f34ea062fd8cf04a8afa3e --- .circleci/config.yml | 34 ++++- .circleci/config.yml.in | 34 ++++- .circleci/unittest/linux/scripts/run_test.sh | 7 +- .../unittest/windows/scripts/run_test.sh | 2 + .../common_utils/case_utils.py | 122 ++++++++++++++---- 5 files changed, 166 insertions(+), 33 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c3101fe062..c84daa9e41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -460,6 +460,8 @@ jobs: - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true - store_test_results: path: test-results - store_artifacts: @@ -489,7 +491,7 @@ jobs: command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests - command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh + command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh - store_test_results: path: test-results - store_artifacts: @@ -512,6 +514,17 @@ jobs: - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_SOX: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CTC_DECODER: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_KALDI: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_SOX: true - store_test_results: path: test-results - store_artifacts: @@ -524,7 +537,6 @@ jobs: environment: <<: *environment CUDA_VERSION: "11.3" - TORCHAUDIO_TEST_FORCE_CUDA: 1 steps: - checkout - designate_upload_channel @@ -544,6 +556,16 @@ jobs: - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_SOX: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CTC_DECODER: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_KALDI: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_SOX: true - store_test_results: path: test-results - store_artifacts: @@ -569,6 +591,14 @@ jobs: - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_QUANTIZATION: true - store_test_results: path: test-results - store_artifacts: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 2a4495fff2..a25e99dfd4 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -460,6 +460,8 @@ jobs: - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true - store_test_results: path: test-results - store_artifacts: @@ -489,7 +491,7 @@ jobs: command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests - command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "TORCHAUDIO_TEST_FORCE_CUDA=1" -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh + command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh - store_test_results: path: test-results - store_artifacts: @@ -512,6 +514,17 @@ jobs: - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_SOX: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CTC_DECODER: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_KALDI: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_SOX: true - store_test_results: path: test-results - store_artifacts: @@ -524,7 +537,6 @@ jobs: environment: <<: *environment CUDA_VERSION: "11.3" - TORCHAUDIO_TEST_FORCE_CUDA: 1 steps: - checkout - designate_upload_channel @@ -544,6 +556,16 @@ jobs: - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_SOX: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CTC_DECODER: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_KALDI: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_SOX: true - store_test_results: path: test-results - store_artifacts: @@ -569,6 +591,14 @@ jobs: - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh + environment: + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_APPLY_CMVN_SLIDING: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_FBANK_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_KALDI_PITCH_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_MFCC_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CMD_COMPUTE_SPECTROGRAM_FEATS: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_CUDA: true + TORCHAUDIO_TEST_ALLOW_SKIP_IF_NO_QUANTIZATION: true - store_test_results: path: test-results - store_artifacts: diff --git a/.circleci/unittest/linux/scripts/run_test.sh b/.circleci/unittest/linux/scripts/run_test.sh index 4e8d24748b..83a9196d7e 100755 --- a/.circleci/unittest/linux/scripts/run_test.sh +++ b/.circleci/unittest/linux/scripts/run_test.sh @@ -5,14 +5,9 @@ set -e eval "$(./conda/bin/conda shell.bash hook)" conda activate ./env -case "$(uname -s)" in - Darwin*) os=MacOSX;; - *) os=Linux -esac - python -m torch.utils.collect_env +env | grep TORCHAUDIO || true -export TORCHAUDIO_TEST_FAIL_IF_NO_EXTENSION=1 export PATH="${PWD}/third_party/install/bin/:${PATH}" declare -a args=( diff --git a/.circleci/unittest/windows/scripts/run_test.sh b/.circleci/unittest/windows/scripts/run_test.sh index f5ec80e043..22a53911e3 100644 --- a/.circleci/unittest/windows/scripts/run_test.sh +++ b/.circleci/unittest/windows/scripts/run_test.sh @@ -9,6 +9,8 @@ this_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" source "$this_dir/set_cuda_envs.sh" python -m torch.utils.collect_env +env | grep TORCHAUDIO || true + cd test pytest --cov=torchaudio --junitxml=../test-results/junit.xml -v --durations 20 torchaudio_unittest coverage html diff --git a/test/torchaudio_unittest/common_utils/case_utils.py b/test/torchaudio_unittest/common_utils/case_utils.py index 329d364cf2..e4b716b13a 100644 --- a/test/torchaudio_unittest/common_utils/case_utils.py +++ b/test/torchaudio_unittest/common_utils/case_utils.py @@ -1,3 +1,4 @@ +import functools import os.path import shutil import subprocess @@ -94,32 +95,107 @@ class TorchaudioTestCase(TestBaseMixin, PytorchTestCase): pass +def _eval_env(var, default): + if var not in os.environ: + return default + + val = os.environ.get(var, "0") + trues = ["1", "true", "TRUE", "on", "ON", "yes", "YES"] + falses = ["0", "false", "FALSE", "off", "OFF", "no", "NO"] + if val in trues: + return True + if val not in falses: + # fmt: off + raise RuntimeError( + f"Unexpected environment variable value `{var}={val}`. " + f"Expected one of {trues + falses}") + # fmt: on + return False + + +def _fail(reason): + def deco(test_item): + if isinstance(test_item, type): + # whole class is decorated + def _f(self, *_args, **_kwargs): + raise RuntimeError(reason) + + test_item.setUp = _f + return test_item + + # A method is decorated + @functools.wraps(test_item) + def f(*_args, **_kwargs): + raise RuntimeError(reason) + + return f + + return deco + + +def _pass(test_item): + return test_item + + +_IN_CI = _eval_env("CI", default=False) + + +def _skipIf(condition, reason, key): + if not condition: + return _pass + + # In CI, default to fail, so as to prevent accidental skip. + # In other env, default to skip + var = f"TORCHAUDIO_TEST_ALLOW_SKIP_IF_{key}" + skip_allowed = _eval_env(var, default=not _IN_CI) + if skip_allowed: + return unittest.skip(reason) + return _fail(f"{reason} But the test cannot be skipped. (CI={_IN_CI}, {var}={skip_allowed}.)") + + def skipIfNoExec(cmd): - return unittest.skipIf(shutil.which(cmd) is None, f"`{cmd}` is not available") + return _skipIf( + shutil.which(cmd) is None, + f"`{cmd}` is not available.", + key=f"NO_CMD_{cmd.upper().replace('-', '_')}", + ) def skipIfNoModule(module, display_name=None): - display_name = display_name or module - return unittest.skipIf(not is_module_available(module), f'"{display_name}" is not available') - - -def skipIfNoCuda(test_item): - if torch.cuda.is_available(): - return test_item - force_cuda_test = os.environ.get("TORCHAUDIO_TEST_FORCE_CUDA", "0") - if force_cuda_test not in ["0", "1"]: - raise ValueError('"TORCHAUDIO_TEST_FORCE_CUDA" must be either "0" or "1".') - if force_cuda_test == "1": - raise RuntimeError('"TORCHAUDIO_TEST_FORCE_CUDA" is set but CUDA is not available.') - return unittest.skip("CUDA is not available.")(test_item) - - -skipIfNoSox = unittest.skipIf(not is_sox_available(), reason="Sox not available") -skipIfNoKaldi = unittest.skipIf(not is_kaldi_available(), reason="Kaldi not available") -skipIfNoCtcDecoder = unittest.skipIf(not is_ctc_decoder_available(), reason="CTC decoder not available") -skipIfRocm = unittest.skipIf( - os.getenv("TORCHAUDIO_TEST_WITH_ROCM", "0") == "1", reason="test doesn't currently work on the ROCm stack" + return _skipIf( + not is_module_available(module), + f'"{display_name or module}" is not available.', + key=f"NO_MOD_{module.replace('.', '_')}", + ) + + +skipIfNoCuda = _skipIf( + not torch.cuda.is_available(), + reason="CUDA is not available.", + key="NO_CUDA", +) +skipIfNoSox = _skipIf( + not is_sox_available(), + reason="Sox features are not available.", + key="NO_SOX", +) +skipIfNoKaldi = _skipIf( + not is_kaldi_available(), + reason="Kaldi features are not available.", + key="NO_KALDI", +) +skipIfNoCtcDecoder = _skipIf( + not is_ctc_decoder_available(), + reason="CTC decoder not available.", + key="NO_CTC_DECODER", +) +skipIfRocm = _skipIf( + _eval_env("TORCHAUDIO_TEST_WITH_ROCM", default=False), + reason="The test doesn't currently work on the ROCm stack.", + key="ON_ROCM", ) -skipIfNoQengine = unittest.skipIf( - "fbgemm" not in torch.backends.quantized.supported_engines, reason="`fbgemm` is not available." +skipIfNoQengine = _skipIf( + "fbgemm" not in torch.backends.quantized.supported_engines, + reason="`fbgemm` is not available.", + key="NO_QUANTIZATION", ) From e4f508a3eece3a0619fb76b195b6c6a16b4903bf Mon Sep 17 00:00:00 2001 From: Binh Tang Date: Wed, 5 Jan 2022 20:43:52 -0800 Subject: [PATCH 0097/1144] [PyTorchLightning/pytorch-lightning] Rename `DDPPlugin` to `DDPStrategy` (#11142) Summary: ### New commit log messages b64dea9dc Rename `DDPPlugin` to `DDPStrategy` (#11142) Reviewed By: jjenniferdai Differential Revision: D33259306 fbshipit-source-id: b4608c6b96b4a7977eaa4ed3f03c4b824882aef0 --- examples/source_separation/lightning_train.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/source_separation/lightning_train.py b/examples/source_separation/lightning_train.py index b73751b8d2..db427ac457 100644 --- a/examples/source_separation/lightning_train.py +++ b/examples/source_separation/lightning_train.py @@ -19,7 +19,7 @@ import torchaudio from pytorch_lightning import LightningModule, Trainer from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.plugins import DDPPlugin +from pytorch_lightning.plugins import DDPStrategy from torch import nn from torch.optim.lr_scheduler import _LRScheduler from torch.utils.data import DataLoader @@ -426,7 +426,7 @@ def cli_main(): gpus=args.num_gpu, num_nodes=args.num_node, accelerator="ddp", - plugins=DDPPlugin(find_unused_parameters=False), # make sure there is no unused params + plugins=DDPStrategy(find_unused_parameters=False), # make sure there is no unused params limit_train_batches=1.0, # Useful for fast experiment gradient_clip_val=5.0, callbacks=callbacks, From cf8189eddb23403b9f5afa069ce2f05c46aaf7a5 Mon Sep 17 00:00:00 2001 From: Werner Chao Date: Thu, 6 Jan 2022 08:04:27 -0800 Subject: [PATCH 0098/1144] Update doc regarding dropping Python3.6 support (#2139) Summary: Drop support for python 3.6, and update dependencies documentation. More details [Issue 2051](https://github.com/pytorch/audio/issues/2051). Pull Request resolved: https://github.com/pytorch/audio/pull/2139 Reviewed By: mthrok Differential Revision: D33454583 Pulled By: wernerchao fbshipit-source-id: 64eccb38e26853ba63f72fb92723e3f0155e806e --- README.md | 2 +- setup.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e2f03fcd47..eb4aaaa30c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The following are the corresponding ``torchaudio`` versions and supported Python | ``torch`` | ``torchaudio`` | ``python`` | | ------------------------ | ------------------------ | ------------------------------- | -| ``master`` / ``nightly`` | ``main`` / ``nightly`` | ``>=3.6``, ``<=3.9`` | +| ``master`` / ``nightly`` | ``main`` / ``nightly`` | ``>=3.7``, ``<=3.9`` | | ``1.10.0`` | ``0.10.0`` | ``>=3.6``, ``<=3.9`` | | ``1.9.1`` | ``0.9.1`` | ``>=3.6``, ``<=3.9`` | | ``1.9.0`` | ``0.9.0`` | ``>=3.6``, ``<=3.9`` | diff --git a/setup.py b/setup.py index 73a95d34d3..92a92d1590 100644 --- a/setup.py +++ b/setup.py @@ -159,7 +159,6 @@ def _main(): "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: C++", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From b73f5d6752d14c0309231a310778399af26033f9 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 6 Jan 2022 09:10:17 -0800 Subject: [PATCH 0099/1144] Surpress stderr from subprocess in setup.py (#2133) Summary: This commits supress the stderr output when shell commands are executed `setup.py`. `setup.py` performs multiple git commands to gather metadata. When `git tag` command tries to find a tag, (which fails unless on release) it produces `fatal: No names found, cannot describe anything.` This is confusing especially when it is executed from higher level, like `pip install git+https://...`. Pull Request resolved: https://github.com/pytorch/audio/pull/2133 Reviewed By: nateanl Differential Revision: D33455339 Pulled By: mthrok fbshipit-source-id: 3e24451eb6fedcd0ad90f7e16e38fcdb70dc9704 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 92a92d1590..f4e4885b05 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def _run_cmd(cmd): try: - return subprocess.check_output(cmd, cwd=ROOT_DIR).decode("ascii").strip() + return subprocess.check_output(cmd, cwd=ROOT_DIR, stderr=subprocess.DEVNULL).decode("ascii").strip() except Exception: return None From 8c16529b86f8061c8a44f9e35bb5482491a43876 Mon Sep 17 00:00:00 2001 From: Elijah Rippeth Date: Thu, 6 Jan 2022 10:56:27 -0800 Subject: [PATCH 0100/1144] [Example] abstracts BucketizeSampler to be usable outside of HuBERT example. (#2147) Summary: This PR: - Replaces the `data_source` with `lengths` - Adds a `shuffle` argument to decide whether to shuffle the samples in the buckets. - Add `max_len` and `min_len` to filter out samples that are > max_len or < min_len. cc nateanl Pull Request resolved: https://github.com/pytorch/audio/pull/2147 Reviewed By: carolineechen Differential Revision: D33454369 Pulled By: nateanl fbshipit-source-id: 3835169ec7f808f8dd9650e7f183f79091efe886 --- examples/hubert/dataset/hubert_dataset.py | 64 +++++++++++++++-------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/examples/hubert/dataset/hubert_dataset.py b/examples/hubert/dataset/hubert_dataset.py index 36dd879b1c..5570c0d538 100644 --- a/examples/hubert/dataset/hubert_dataset.py +++ b/examples/hubert/dataset/hubert_dataset.py @@ -1,31 +1,30 @@ import random from pathlib import Path -from typing import ( - Dict, - Iterator, - List, - Optional, - Tuple, - Union, -) +from typing import Dict, Iterator, List, Optional, Tuple, Union import numpy as np import torch import torchaudio from torch import Tensor -from torch.utils.data import Dataset, BatchSampler +from torch.utils.data import BatchSampler, Dataset class BucketizeSampler(BatchSampler): """Buketize sampler for data with different lengths to reduce number of paddings. Args: - data_source (Dataset): The dataset to sample + lengths (List[int]): The lengths of the samples in the dataset. num_buckets (int): The number of buckets to split the data samples. + min_len (int, optional): The minimum sample lengths to keep. + (Default: 0) + max_len (int or None, optional): The maximum sample lengths to keep. Inferred if not provided. + (Default ``None``) max_token_count (int or None, optional): The max number of tokens in one mini-batch. (Default: ``None``) batch_size (int or None, optional): The number of samples in one mini-batch. (Default: ``None``) + shuffle (bool, optional): Whether to shuffle buckets for non-monotonic length sampling. + (Default True) Note: If ``max_token_count`` is not ``None``, the ``batch_size`` couldn't be set since the lengths of samples are unknown, the batch size may be different for different @@ -34,46 +33,66 @@ class BucketizeSampler(BatchSampler): def __init__( self, - data_source: Dataset, + lengths: List[int], num_buckets: int, + min_len: int = 0, + max_len: Optional[int] = None, max_token_count: Optional[int] = None, batch_size: Optional[int] = None, + shuffle: bool = True, ) -> None: + if max_len is None: + max_len = max(lengths) + + if not (0 <= min_len <= max_len): + raise AssertionError("``min_len`` should be non-negative and smaller than ``max_len``") if max_token_count is not None and batch_size is not None: raise AssertionError("The ``max_token_count`` and ``batch_size`` can't be both set.") - self.data_source = data_source + # Filter out samples which are outside the bounds of [min_len, max_len] + # sort to minimize gap when bucketizing. + filtered_length_idx = [(length, i) for i, length in enumerate(lengths) if min_len <= length <= max_len] + if len(filtered_length_idx) == 0: + raise AssertionError("``lengths`` cannot be empty after filtering.") + sorted_filtered_length_idx = sorted(filtered_length_idx, key=lambda x: x[0]) + self.lengths = [e[0] for e in sorted_filtered_length_idx] + self.indices = [e[1] for e in sorted_filtered_length_idx] self.max_token_count = max_token_count self.batch_size = batch_size - self.buckets = self._get_buckets(self.data_source, num_buckets) + self.buckets = self._get_buckets(self.lengths, self.indices, num_buckets, min_len, max_len) + self.shuffle = shuffle - def _get_buckets(self, data_source: Dataset, num_buckets: int) -> Dict[int, Tensor]: + def _get_buckets( + self, lengths: List[int], indices: List[int], num_buckets: int, min_len: int, max_len: int + ) -> Dict[int, Tensor]: """Generate buckets based on the dataset. Args: - data_source (Dataset): The dataset object to bucketize. + lengths (List[int]): The lengths of the samples in the dataset. + indices (List[int]): The indices of the samples in the original dataset. num_buckets (int): The number of buckets. + min_len (int): The lower bound of the evenly spaced length intervals to determine bucket width. + max_len (int): The upper bound of the evenly spaced length intervals to determine bucket width. Returns: (dict[int, Tensor]): A dictionary in which the key is the bucket index, the value is the Tensor of corresponding sample indices. """ buckets = {} - len_list = data_source.len_list - min_len, max_len = min(len_list), max(len_list) boundaries = [min_len - 1] interval = (max_len - min_len) // num_buckets for i in range(1, num_buckets): boundaries.append(min_len + i * interval) boundaries.append(max_len + 1) - bucket_ids = torch.bucketize(torch.tensor(len_list), torch.tensor(boundaries)) - for i, _ in enumerate(len_list): + bucket_ids = torch.bucketize(torch.tensor(lengths), torch.tensor(boundaries)) + for i in indices: bucket_id = bucket_ids[i] if bucket_id in buckets: buckets[bucket_id].append(i) else: buckets[bucket_id] = [i] for k in buckets: - random.shuffle(buckets[k]) + if self.shuffle: + random.shuffle(buckets[k]) buckets[k] = torch.as_tensor(buckets[k], dtype=torch.int) return buckets @@ -81,7 +100,6 @@ def __iter__(self) -> Iterator[List[int]]: iter_list = [] total_len = 0 batch = [] - len_list = self.data_source.len_list if self.max_token_count: for k in self.buckets.keys(): for i in range(self.buckets[k].size(0)): @@ -89,10 +107,10 @@ def __iter__(self) -> Iterator[List[int]]: if total_len > self.max_token_count: iter_list.append(batch) batch = [index] - total_len = len_list[index] + total_len = self.lengths[index] else: batch.append(index) - total_len += len_list[index] + total_len += self.lengths[index] else: for k in self.buckets.keys(): for i in range(self.buckets[k].size(0)): From 1ccd33ec269254411cc3caf9590dd2ba150af700 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 6 Jan 2022 11:06:35 -0800 Subject: [PATCH 0101/1144] [DOC] Update prototype pipeline documentation (#2148) Summary: - Unindent RNNTBundle components so that they show up on the right side bar - Overwrite the sigunature of RNNTBundle methods so that back links are available --- ## Before Screen Shot 2022-01-06 at 1 36 16 PM ## After Screen Shot 2022-01-06 at 1 35 39 PM Pull Request resolved: https://github.com/pytorch/audio/pull/2148 Reviewed By: hwangjeff Differential Revision: D33458574 Pulled By: mthrok fbshipit-source-id: ac34ffc4070261563a1f4ea9337997f0fe7b2212 --- docs/source/prototype.pipelines.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/source/prototype.pipelines.rst b/docs/source/prototype.pipelines.rst index f5495ec5f3..1651865eb1 100644 --- a/docs/source/prototype.pipelines.rst +++ b/docs/source/prototype.pipelines.rst @@ -16,20 +16,25 @@ RNNTBundle .. autoclass:: RNNTBundle :members: sample_rate, n_fft, n_mels, hop_length, segment_length, right_context_length - .. automethod:: get_decoder + .. automethod:: get_decoder() -> torchaudio.prototype.models.RNNTBeamSearch - .. automethod:: get_feature_extractor + .. automethod:: get_feature_extractor() -> RNNTBundle.FeatureExtractor - .. automethod:: get_streaming_feature_extractor + .. automethod:: get_streaming_feature_extractor() -> RNNTBundle.FeatureExtractor - .. automethod:: get_token_processor + .. automethod:: get_token_processor() -> RNNTBundle.TokenProcessor - .. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.FeatureExtractor - :special-members: __call__ +RNNTBundle - FeatureExtractor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - .. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.TokenProcessor - :special-members: __call__ +.. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.FeatureExtractor + :special-members: __call__ +RNNTBundle - TokenProcessor +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: torchaudio.prototype.pipelines::RNNTBundle.TokenProcessor + :special-members: __call__ EMFORMER_RNNT_BASE_LIBRISPEECH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 565f8d417ec8d210c277021752ebd72cd4f179f5 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Thu, 6 Jan 2022 16:53:53 -0800 Subject: [PATCH 0102/1144] Enable build ffmpeg-features in all related jobs (#2140) Summary: This commit enables ffmpeg-feature build in tests and binary builds of all platforms. (Linux/macOS/Windows x conda/wheel) It also moves the definition of BUILD_FFMPEG env vars to the top level `config.yml`. --- Manual checking if all the build log contains `libtorchaudio_ffmpeg`. ### binary build - [x] `binary_linux_conda_py3.7_cpu` - [x] `binary_linux_conda_py3.7_cu102` - [x] `binary_linux_wheel_py3.7_cpu` - [x] `binary_linux_wheel_py3.7_cu102` - [x] `binary_macos_conda_py3.7_cpu` - [x] `binary_macos_wheel_py3.7_cpu` - [x] `binary_windows_conda_py3.7_cpu` - [x] `binary_windows_conda_py3.7_cu113` - [x] `binary_windows_wheel_py3.7_cpu` - [x] `binary_windows_wheel_py3.7_cu113` ### test - [x] `unittest_linux_cpu_py3.7` - [x] `unittest_linux_gpu_py3.7` - [x] `unittest_macos_cpu_py3.7` - [x] `unittest_windows_cpu_py3.7` - [x] `unittest_windows_gpu_py3.7` - [x] `integration test` Pull Request resolved: https://github.com/pytorch/audio/pull/2140 Reviewed By: hwangjeff Differential Revision: D33464430 Pulled By: mthrok fbshipit-source-id: 2c5b72be75d49019bf1599036180d4e56074e46b --- .circleci/config.yml | 23 +++++++++++++++++-- .circleci/config.yml.in | 23 +++++++++++++++++-- .circleci/unittest/linux/scripts/install.sh | 2 +- .../unittest/windows/scripts/setup_env.sh | 1 + .github/workflows/integration-test.yml | 8 ++++++- packaging/pkg_helpers.bash | 4 +--- packaging/torchaudio/build.sh | 1 - packaging/torchaudio/meta.yaml | 3 ++- 8 files changed, 54 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c84daa9e41..007a585b0b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -164,6 +164,8 @@ jobs: command: | ./tools/bootstrap_ffmpeg.sh packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: @@ -181,7 +183,10 @@ jobs: - load_conda_channel_flags - attach_workspace: at: third_party - - run: packaging/build_conda.sh + - run: + command: packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: /opt/conda/conda-bld/linux-64 - persist_to_workspace: @@ -232,6 +237,8 @@ jobs: source $HOME/miniconda3/bin/activate conda install -yq conda-build packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: /Users/distiller/miniconda3/conda-bld/osx-64 - persist_to_workspace: @@ -254,6 +261,8 @@ jobs: eval "$('/C/tools/miniconda3/Scripts/conda.exe' 'shell.bash' 'hook')" conda activate base bash packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: @@ -282,6 +291,8 @@ jobs: export CONDA_CHANNEL_FLAGS="-c conda-forge" fi bash packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: C:/tools/miniconda3/conda-bld/win-64 - persist_to_workspace: @@ -457,6 +468,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/linux/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh @@ -488,7 +501,7 @@ jobs: command: docker run -t --gpus all -e PYTHON_VERSION -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/setup_env.sh - run: name: Install torchaudio - command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh + command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -e BUILD_FFMPEG=1 -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh @@ -511,6 +524,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/windows/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh @@ -553,6 +568,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/windows/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh @@ -588,6 +605,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/linux/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index a25e99dfd4..dfb4574000 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -164,6 +164,8 @@ jobs: command: | ./tools/bootstrap_ffmpeg.sh packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: @@ -181,7 +183,10 @@ jobs: - load_conda_channel_flags - attach_workspace: at: third_party - - run: packaging/build_conda.sh + - run: + command: packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: /opt/conda/conda-bld/linux-64 - persist_to_workspace: @@ -232,6 +237,8 @@ jobs: source $HOME/miniconda3/bin/activate conda install -yq conda-build packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: /Users/distiller/miniconda3/conda-bld/osx-64 - persist_to_workspace: @@ -254,6 +261,8 @@ jobs: eval "$('/C/tools/miniconda3/Scripts/conda.exe' 'shell.bash' 'hook')" conda activate base bash packaging/build_wheel.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: dist - persist_to_workspace: @@ -282,6 +291,8 @@ jobs: export CONDA_CHANNEL_FLAGS="-c conda-forge" fi bash packaging/build_conda.sh + environment: + BUILD_FFMPEG: true - store_artifacts: path: C:/tools/miniconda3/conda-bld/win-64 - persist_to_workspace: @@ -457,6 +468,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/linux/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh @@ -488,7 +501,7 @@ jobs: command: docker run -t --gpus all -e PYTHON_VERSION -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/setup_env.sh - run: name: Install torchaudio - command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh + command: docker run -t --gpus all -e UPLOAD_CHANNEL -e CONDA_CHANNEL_FLAGS -e BUILD_FFMPEG=1 -v $PWD:$PWD -w $PWD "${image_name}" .circleci/unittest/linux/scripts/install.sh - run: name: Run tests command: docker run -t --gpus all -v $PWD:$PWD -w $PWD -e "CI=${CI}" "${image_name}" .circleci/unittest/linux/scripts/run_test.sh @@ -511,6 +524,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/windows/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh @@ -553,6 +568,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/windows/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/windows/scripts/run_test.sh @@ -588,6 +605,8 @@ jobs: - run: name: Install torchaudio command: .circleci/unittest/linux/scripts/install.sh + environment: + BUILD_FFMPEG: true - run: name: Run tests command: .circleci/unittest/linux/scripts/run_test.sh diff --git a/.circleci/unittest/linux/scripts/install.sh b/.circleci/unittest/linux/scripts/install.sh index cbd17b7bfe..869799da8d 100755 --- a/.circleci/unittest/linux/scripts/install.sh +++ b/.circleci/unittest/linux/scripts/install.sh @@ -52,7 +52,7 @@ printf "Installing PyTorch with %s\n" "${cudatoolkit}" # 2. Install torchaudio printf "* Installing torchaudio\n" -BUILD_FFMPEG=1 python setup.py install +python setup.py install # 3. Install Test tools printf "* Installing test tools\n" diff --git a/.circleci/unittest/windows/scripts/setup_env.sh b/.circleci/unittest/windows/scripts/setup_env.sh index 07a8efc091..3c411faf08 100644 --- a/.circleci/unittest/windows/scripts/setup_env.sh +++ b/.circleci/unittest/windows/scripts/setup_env.sh @@ -39,3 +39,4 @@ conda activate "${env_dir}" # 3. Install minimal build tools pip --quiet install cmake ninja +conda install --quiet -y 'ffmpeg>=4.1' diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index bd8e045b76..ffd1b09f1a 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 strategy: fail-fast: false matrix: @@ -21,12 +21,18 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo add-apt-repository -y ppa:jonathonf/ffmpeg-4 + sudo apt install -y -qq pkg-config libavfilter-dev libavdevice-dev - name: Install packages run: | python -m pip install --quiet --upgrade pip python -m pip install --quiet --pre torch -f https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html python -m pip install --quiet pytest requests cmake ninja deep-phonemizer python setup.py install + env: + BUILD_FFMPEG: true - name: Run integration test run: | cd test && pytest integration_tests -v --use-tmp-hub-dir diff --git a/packaging/pkg_helpers.bash b/packaging/pkg_helpers.bash index c86b496632..18efcca38e 100644 --- a/packaging/pkg_helpers.bash +++ b/packaging/pkg_helpers.bash @@ -182,9 +182,7 @@ setup_wheel_python() { conda env remove -n "env$PYTHON_VERSION" || true conda create -yn "env$PYTHON_VERSION" python="$PYTHON_VERSION" conda activate "env$PYTHON_VERSION" - if [[ "$(uname)" == Darwin ]]; then - conda install --quiet -y pkg-config "ffmpeg>=4.1" - fi + conda install --quiet -y pkg-config 'ffmpeg>=4.1' else case "$PYTHON_VERSION" in 2.7) diff --git a/packaging/torchaudio/build.sh b/packaging/torchaudio/build.sh index d0a1f30852..23bb306b02 100644 --- a/packaging/torchaudio/build.sh +++ b/packaging/torchaudio/build.sh @@ -14,5 +14,4 @@ if [ "${USE_CUDA}" == "1" ] ; then fi fi shopt -u nocasematch -export BUILD_FFMPEG=1 python setup.py install --single-version-externally-managed --record=record.txt diff --git a/packaging/torchaudio/meta.yaml b/packaging/torchaudio/meta.yaml index 3aef2c694f..af4591fa01 100644 --- a/packaging/torchaudio/meta.yaml +++ b/packaging/torchaudio/meta.yaml @@ -22,7 +22,7 @@ requirements: {{ environ.get('CONDA_PYTORCH_BUILD_CONSTRAINT', 'pytorch') }} {{ environ.get('CONDA_EXTRA_BUILD_CONSTRAINT', '') }} {{ environ.get('CONDA_CUDATOOLKIT_CONSTRAINT', '') }} - - ffmpeg >=4.1 # [not win] + - ffmpeg >=4.1 run: - python @@ -45,6 +45,7 @@ build: - BUILD_VERSION - USE_CUDA - TORCH_CUDA_ARCH_LIST + - BUILD_FFMPEG test: imports: From ffbfe74a5271a25db149dafb493b80364a97939d Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Thu, 6 Jan 2022 17:59:21 -0800 Subject: [PATCH 0103/1144] Add parameter usage to CTC inference tutorial (#2141) Summary: Add explanation and demonstration of different beam search decoder parameters. Additionally use a better sample audio file and load in with token list instead of tokens file. Pull Request resolved: https://github.com/pytorch/audio/pull/2141 Reviewed By: mthrok Differential Revision: D33463230 Pulled By: carolineechen fbshipit-source-id: d3dd6452b03d4fc2e095d778189c66f7161e4c68 --- ...asr_inference_with_ctc_decoder_tutorial.py | 263 +++++++++++++++--- 1 file changed, 226 insertions(+), 37 deletions(-) diff --git a/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py index 9ad2f4a0f0..fe355f5e21 100644 --- a/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py +++ b/examples/tutorials/asr_inference_with_ctc_decoder_tutorial.py @@ -36,7 +36,7 @@ # working with # -import os +import time import IPython import torch @@ -50,7 +50,7 @@ # We use the pretrained `Wav2Vec 2.0 `__ # Base model that is finetuned on 10 min of the `LibriSpeech # dataset `__, which can be loaded in using -# py:func:`torchaudio.pipelines`. For more detail on running Wav2Vec 2.0 speech +# :py:func:`torchaudio.pipelines`. For more detail on running Wav2Vec 2.0 speech # recognition pipelines in torchaudio, please refer to `this # tutorial `__. # @@ -65,7 +65,7 @@ hub_dir = torch.hub.get_dir() -speech_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/8461-258277-0000.wav" +speech_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/1688-142285-0007.wav" speech_file = f"{hub_dir}/speech.wav" torch.hub.download_url_to_file(speech_url, speech_file) @@ -75,7 +75,8 @@ ###################################################################### # The transcript corresponding to this audio file is -# ``"when it was the seven hundred and eighteenth night"`` +# :: +# i really was very much afraid of showing him how much shocked i was at some parts of what he said # waveform, sample_rate = torchaudio.load(speech_file) @@ -85,8 +86,8 @@ ###################################################################### -# Files for Decoder -# ~~~~~~~~~~~~~~~~~ +# Files and Data for Decoder +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Next, we load in our token, lexicon, and KenLM data, which are used by # the decoder to predict words from the acoustic model output. @@ -101,7 +102,9 @@ # ^^^^^^ # # The tokens are the possible symbols that the acoustic model can predict, -# including the blank and silent symbols. +# including the blank and silent symbols. It can either be passed in as a +# file, where each line consists of the tokens corresponding to the same +# index, or as a list of tokens, each mapping to a unique index. # # :: # @@ -113,9 +116,8 @@ # ... # -token_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/tokens-w2v2.txt" -token_file = f"{hub_dir}/token.txt" -torch.hub.download_url_to_file(token_url, token_file) +tokens = [label.lower() for label in bundle.get_labels()] +print(tokens) ###################################################################### @@ -151,6 +153,9 @@ # the binarized ``.bin`` LM can be used, but the binary format is # recommended for faster loading. # +# The language model used in this tutorial is a 4-gram KenLM trained using +# `LibriSpeech `__. +# kenlm_url = "https://pytorch.s3.amazonaws.com/torchaudio/tutorial-assets/ctc-decoding/4-gram-librispeech.bin" kenlm_file = f"{hub_dir}/kenlm.bin" @@ -161,26 +166,25 @@ # Construct Beam Search Decoder # ----------------------------- # -# The decoder can be constructed using the -# :py:func:`torchaudio.prototype.ctc_decoder.kenlm_lexicon_decoder` -# factory function. -# In addition to the previously mentioned components, it also takes in -# various beam search decoding parameters and token/word parameters. +# The decoder can be constructed using the factory function +# :py:func:`kenlm_lexicon_decoder `. +# In addition to the previously mentioned components, it also takes in various beam +# search decoding parameters and token/word parameters. # from torchaudio.prototype.ctc_decoder import kenlm_lexicon_decoder +LM_WEIGHT = 3.23 +WORD_SCORE = -0.26 + beam_search_decoder = kenlm_lexicon_decoder( lexicon=lexicon_file, - tokens=token_file, + tokens=tokens, kenlm=kenlm_file, - nbest=1, + nbest=3, beam_size=1500, - beam_size_token=50, - lm_weight=3.23, - word_score=-1.39, - unk_score=float("-inf"), - sil_score=0, + lm_weight=LM_WEIGHT, + word_score=WORD_SCORE, ) @@ -192,6 +196,8 @@ # basic greedy decoder. # +from typing import List + class GreedyCTCDecoder(torch.nn.Module): def __init__(self, labels, blank=0): @@ -199,21 +205,22 @@ def __init__(self, labels, blank=0): self.labels = labels self.blank = blank - def forward(self, emission: torch.Tensor) -> str: - """Given a sequence emission over labels, get the best path string + def forward(self, emission: torch.Tensor) -> List[str]: + """Given a sequence emission over labels, get the best path Args: emission (Tensor): Logit tensors. Shape `[num_seq, num_label]`. Returns: - str: The resulting transcript + List[str]: The resulting transcript """ indices = torch.argmax(emission, dim=-1) # [num_seq,] indices = torch.unique_consecutive(indices, dim=-1) indices = [i for i in indices if i != self.blank] - return "".join([self.labels[i] for i in indices]) + joined = "".join([self.labels[i] for i in indices]) + return joined.replace("|", " ").strip().split() -greedy_decoder = GreedyCTCDecoder(labels=bundle.get_labels()) +greedy_decoder = GreedyCTCDecoder(tokens) ###################################################################### @@ -222,28 +229,210 @@ def forward(self, emission: torch.Tensor) -> str: # # Now that we have the data, acoustic model, and decoder, we can perform # inference. Recall the transcript corresponding to the waveform is -# ``"when it was the seven hundred and eighteenth night"`` +# :: +# i really was very much afraid of showing him how much shocked i was at some parts of what he said # +actual_transcript = "i really was very much afraid of showing him how much shocked i was at some parts of what he said" +actual_transcript = actual_transcript.split() + emission, _ = acoustic_model(waveform) + +###################################################################### +# The greedy decoder give the following result. +# + +greedy_result = greedy_decoder(emission[0]) +greedy_transcript = greedy_result +greedy_wer = torchaudio.functional.edit_distance(actual_transcript, greedy_transcript) / len(actual_transcript) + +print(f"Transcript: {greedy_transcript}") +print(f"WER: {greedy_wer}") + + ###################################################################### # Using the beam search decoder: +# beam_search_result = beam_search_decoder(emission) -beam_search_transcript = " ".join(beam_search_result[0][0].words).lower().strip() -print(beam_search_transcript) +beam_search_transcript = " ".join(beam_search_result[0][0].words).strip() +beam_search_wer = torchaudio.functional.edit_distance(actual_transcript, beam_search_result[0][0].words) / len( + actual_transcript +) + +print(f"Transcript: {beam_search_transcript}") +print(f"WER: {beam_search_wer}") + ###################################################################### -# Using the greedy decoder: +# We see that the transcript with the lexicon-constrained beam search +# decoder produces a more accurate result consisting of real words, while +# the greedy decoder can predict incorrectly spelled words like “affrayd” +# and “shoktd”. +# -greedy_result = greedy_decoder(emission[0]) -greedy_transcript = greedy_result.replace("|", " ").lower().strip() -print(greedy_transcript) + +###################################################################### +# Beam Search Decoder Parameters +# ------------------------------ +# +# In this section, we go a little bit more in depth about some different +# parameters and tradeoffs. For the full list of customizable parameters, +# please refer to the +# :py:func:`documentation `. # noqa +# ###################################################################### -# We see that the transcript with the lexicon-constrained beam search -# decoder consists of real words, while the greedy decoder can predict -# incorrectly spelled words like “hundrad”. +# Helper Function +# ~~~~~~~~~~~~~~~ +# + + +def print_decoded(decoder, emission, param, param_value): + start_time = time.monotonic() + result = decoder(emission) + decode_time = time.monotonic() - start_time + + transcript = " ".join(result[0][0].words).lower().strip() + score = result[0][0].score + print(f"{param} {param_value:<3}: {transcript} (score: {score:.2f}; {decode_time:.4f} secs)") + + +###################################################################### +# nbest +# ~~~~~ +# +# This parameter indicates the number of best Hypothesis to return, which +# is a property that is not possible with the greedy decoder. For +# instance, by setting ``nbest=3`` when constructing the beam search +# decoder earlier, we can now access the hypotheses with the top 3 scores. +# + +for i in range(3): + transcript = " ".join(beam_search_result[0][i].words).strip() + score = beam_search_result[0][i].score + print(f"{transcript} (score: {score})") + + +###################################################################### +# beam size +# ~~~~~~~~~ +# +# The ``beam_size`` parameter determines the maximum number of best +# hypotheses to hold after each decoding step. Using larger beam sizes +# allows for exploring a larger range of possible hypotheses which can +# produce hypotheses with higher scores, but it is computationally more +# expensive and does not provide additional gains beyond a certain point. +# +# In the example below, we see improvement in decoding quality as we +# increase beam size from 1 to 5 to 50, but notice how using a beam size +# of 500 provides the same output as beam size 50 while increase the +# computation time. +# + +beam_sizes = [1, 5, 50, 500] + +for beam_size in beam_sizes: + beam_search_decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens, + kenlm=kenlm_file, + beam_size=beam_size, + lm_weight=LM_WEIGHT, + word_score=WORD_SCORE, + ) + + print_decoded(beam_search_decoder, emission, "beam size", beam_size) + + +###################################################################### +# beam size token +# ~~~~~~~~~~~~~~~ +# +# The ``beam_size_token`` parameter corresponds to the number of tokens to +# consider for expanding each hypothesis at the decoding step. Exploring a +# larger number of next possible tokens increases the range of potential +# hypotheses at the cost of computation. +# + +num_tokens = len(tokens) +beam_size_tokens = [1, 5, 10, num_tokens] + +for beam_size_token in beam_size_tokens: + beam_search_decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens, + kenlm=kenlm_file, + beam_size_token=beam_size_token, + lm_weight=LM_WEIGHT, + word_score=WORD_SCORE, + ) + + print_decoded(beam_search_decoder, emission, "beam size token", beam_size_token) + + +###################################################################### +# beam threshold +# ~~~~~~~~~~~~~~ +# +# The ``beam_threshold`` parameter is used to prune the stored hypotheses +# set at each decoding step, removing hypotheses whose scores are greater +# than ``beam_threshold`` away from the highest scoring hypothesis. There +# is a balance between choosing smaller thresholds to prune more +# hypotheses and reduce the search space, and choosing a large enough +# threshold such that plausible hypotheses are not pruned. +# + +beam_thresholds = [1, 5, 10, 25] + +for beam_threshold in beam_thresholds: + beam_search_decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens, + kenlm=kenlm_file, + beam_threshold=beam_threshold, + lm_weight=LM_WEIGHT, + word_score=WORD_SCORE, + ) + + print_decoded(beam_search_decoder, emission, "beam threshold", beam_threshold) + + +###################################################################### +# language model weight +# ~~~~~~~~~~~~~~~~~~~~~ +# +# The ``lm_weight`` parameter is the weight to assign to the language +# model score which to accumulate with the acoustic model score for +# determining the overall scores. Larger weights encourage the model to +# predict next words based on the language model, while smaller weights +# give more weight to the acoustic model score instead. +# + +lm_weights = [0, LM_WEIGHT, 15] + +for lm_weight in lm_weights: + beam_search_decoder = kenlm_lexicon_decoder( + lexicon=lexicon_file, + tokens=tokens, + kenlm=kenlm_file, + lm_weight=lm_weight, + word_score=WORD_SCORE, + ) + + print_decoded(beam_search_decoder, emission, "lm weight", lm_weight) + + +###################################################################### +# additional parameters +# ~~~~~~~~~~~~~~~~~~~~~ +# +# Additional parameters that can be optimized include the following +# +# - ``word_score``: score to add when word finishes +# - ``unk_score``: unknown word appearance score to add +# - ``sil_score``: silence appearance score to add +# - ``log_add``: whether to use log add for lexicon Trie smearing # From 7b6b2d000023e2aa3365b769866c5f375e0d5fda Mon Sep 17 00:00:00 2001 From: Binh Tang Date: Sat, 8 Jan 2022 11:52:41 -0800 Subject: [PATCH 0104/1144] [PyTorchLightning/pytorch-lightning] Add deprecation path for renamed training type plugins (#11227) Summary: ### New commit log messages 4eede7c30 Add deprecation path for renamed training type plugins (#11227) Reviewed By: edward-io, daniellepintz Differential Revision: D33409991 fbshipit-source-id: 373e48767e992d67db3c85e436648481ad16c9d0 --- examples/source_separation/lightning_train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/source_separation/lightning_train.py b/examples/source_separation/lightning_train.py index db427ac457..2a7d15e408 100644 --- a/examples/source_separation/lightning_train.py +++ b/examples/source_separation/lightning_train.py @@ -19,7 +19,7 @@ import torchaudio from pytorch_lightning import LightningModule, Trainer from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.plugins import DDPStrategy +from pytorch_lightning.strategies import DDPStrategy from torch import nn from torch.optim.lr_scheduler import _LRScheduler from torch.utils.data import DataLoader From 7f85911173e3bab6e472a318cb696de10fbff017 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 14 Jan 2022 09:51:07 -0800 Subject: [PATCH 0105/1144] Tweak documentation (#2152) Summary: - Change the version of nightly build to `Nightly Build (VERSION)`. - Use `BUILD_VERSION` env var for release. - Automatically change copyright year. - Update the link to nightly in README so that the main branch directs to the corresponding document. Because of the way CI job is setup, the resulting documentation says 0.8.0. This is fixed by https://github.com/pytorch/audio/issues/2151. Pull Request resolved: https://github.com/pytorch/audio/pull/2152 Reviewed By: carolineechen, nateanl Differential Revision: D33585053 Pulled By: mthrok fbshipit-source-id: 3c2bf9fc3214c89f989f5ac65b74bc1e276a7161 --- README.md | 14 +++++++------- docs/source/conf.py | 18 +++++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index eb4aaaa30c..78751bc615 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ torchaudio: an audio library for PyTorch ======================================== [![Build Status](https://circleci.com/gh/pytorch/audio.svg?style=svg)](https://app.circleci.com/pipelines/github/pytorch/audio) -[![Documentation](https://img.shields.io/badge/dynamic/json.svg?label=docs&url=https%3A%2F%2Fpypi.org%2Fpypi%2Ftorchaudio%2Fjson&query=%24.info.version&colorB=brightgreen&prefix=v)](https://pytorch.org/audio/) +[![Documentation](https://img.shields.io/badge/dynamic/json.svg?label=docs&url=https%3A%2F%2Fpypi.org%2Fpypi%2Ftorchaudio%2Fjson&query=%24.info.version&colorB=brightgreen&prefix=v)](https://pytorch.org/audio/main/) [![Anaconda Badge](https://anaconda.org/pytorch/torchaudio/badges/downloads.svg)](https://anaconda.org/pytorch/torchaudio) [![Anaconda-Server Badge](https://anaconda.org/pytorch/torchaudio/badges/platforms.svg)](https://anaconda.org/pytorch/torchaudio) @@ -15,14 +15,14 @@ processing library. The benefits of PyTorch can be seen in torchaudio through having all the computations be through PyTorch operations which makes it easy to use and feel like a natural extension. -- [Support audio I/O (Load files, Save files)](http://pytorch.org/audio/stable/) +- [Support audio I/O (Load files, Save files)](http://pytorch.org/audio/main/) - Load a variety of audio formats, such as `wav`, `mp3`, `ogg`, `flac`, `opus`, `sphere`, into a torch Tensor using SoX - - [Kaldi (ark/scp)](http://pytorch.org/audio/stable/kaldi_io.html) -- [Dataloaders for common audio datasets](http://pytorch.org/audio/stable/datasets.html) + - [Kaldi (ark/scp)](http://pytorch.org/audio/main/kaldi_io.html) +- [Dataloaders for common audio datasets](http://pytorch.org/audio/main/datasets.html) - Common audio transforms - - [Spectrogram, AmplitudeToDB, MelScale, MelSpectrogram, MFCC, MuLawEncoding, MuLawDecoding, Resample](http://pytorch.org/audio/stable/transforms.html) + - [Spectrogram, AmplitudeToDB, MelScale, MelSpectrogram, MFCC, MuLawEncoding, MuLawDecoding, Resample](http://pytorch.org/audio/main/transforms.html) - Compliance interfaces: Run code using PyTorch that align with other libraries - - [Kaldi: spectrogram, fbank, mfcc](https://pytorch.org/audio/stable/compliance.kaldi.html) + - [Kaldi: spectrogram, fbank, mfcc](https://pytorch.org/audio/main/compliance.kaldi.html) Dependencies ------------ @@ -115,7 +115,7 @@ torchaudio.save('foo_save.wav', waveform, sample_rate) # save tensor to file, a API Reference ------------- -API Reference is located here: http://pytorch.org/audio/ +API Reference is located here: http://pytorch.org/audio/main/ Contributing Guidelines ----------------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 63f6635326..42f0514742 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,6 +23,7 @@ import os import re import warnings +from datetime import datetime import pytorch_sphinx_theme @@ -139,19 +140,22 @@ def _get_pattern(): # General information about the project. project = "Torchaudio" -copyright = "2018, Torchaudio Contributors" +copyright = f"{datetime.now().year}, Torchaudio Contributors" author = "Torchaudio Contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -# TODO: change to [:2] at v1.0 -version = "main " -# The full version, including alpha/beta/rc tags. -# TODO: verify this works as expected -release = "main" +# `version` is visible from users +# `release` is used for metadata +if os.getenv("BUILD_VERSION"): + version = release = os.environ["BUILD_VERSION"] +else: + import torchaudio + + version = f"Nightly Build ({torchaudio.__version__})" + release = "nightly" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From d33a8d9dc1f2f9f3c6bfd9051f44e6bb7e95af01 Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:14:08 -0800 Subject: [PATCH 0106/1144] Use `python:3.X` Docker image for build doc (#2151) Summary: Currently, the doc build job uses `pytorch/manylinux-cuda100` as base environment image. This PR changes that with `python:3.X`. The problem with the previous one is - The image is unnecessarily huge with tools not needed for build doc. (+3GB) - No easy way to install ffmpeg>=4.1. https://518849-90321822-gh.circle-artifacts.com/0/docs/index.html Pull Request resolved: https://github.com/pytorch/audio/pull/2151 Reviewed By: carolineechen Differential Revision: D33585043 Pulled By: mthrok fbshipit-source-id: d6d2f6ab33511b8f5c7ca358bc6545e253c1b752 --- .circleci/build_docs/build_docs.sh | 15 --------------- .circleci/build_docs/install_wheels.sh | 21 +++++++++++---------- .circleci/config.yml | 14 ++++++++++---- .circleci/config.yml.in | 14 ++++++++++---- 4 files changed, 31 insertions(+), 33 deletions(-) delete mode 100755 .circleci/build_docs/build_docs.sh diff --git a/.circleci/build_docs/build_docs.sh b/.circleci/build_docs/build_docs.sh deleted file mode 100755 index 531e58ab7d..0000000000 --- a/.circleci/build_docs/build_docs.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -ex -# shellcheck disable=SC1091 -source ./packaging/pkg_helpers.bash -export NO_CUDA_PACKAGE=1 -setup_env 0.8.0 -setup_wheel_python - -pushd docs -pip install -r requirements.txt -yum install -y -q libsndfile-devel -pip install -r requirements-tutorials.txt -BUILD_GALLERY=1 make 'SPHINXOPTS=-W' html -popd diff --git a/.circleci/build_docs/install_wheels.sh b/.circleci/build_docs/install_wheels.sh index 4f14c4d1bd..cc7683c667 100755 --- a/.circleci/build_docs/install_wheels.sh +++ b/.circleci/build_docs/install_wheels.sh @@ -2,13 +2,14 @@ set -ex -# shellcheck disable=SC1091 -source ./packaging/pkg_helpers.bash -export NO_CUDA_PACKAGE=1 -setup_env 0.8.0 -setup_wheel_python -# Starting 0.10, `pip install pytorch` defaults to ROCm. -export PYTORCH_VERSION_SUFFIX="+cpu" -setup_pip_pytorch_version -# pytorch is already installed -pip install --no-deps ~/workspace/torchaudio* +if [[ -z "$PYTORCH_VERSION" ]]; then + # Nightly build + pip install --progress-bar off --pre torch -f "https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html" +else + # Release branch + pip install --progress-bar off "torch==${PYTORCH_VERSION}+cpu" \ + -f https://download.pytorch.org/whl/torch_stable.html \ + -f "https://download.pytorch.org/whl/${UPLOAD_CHANNEL}/torch_${UPLOAD_CHANNEL}.html" +fi +pip install --progress-bar off --no-deps ~/workspace/torchaudio* +pip install --progress-bar off -r docs/requirements.txt -r docs/requirements-tutorials.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 007a585b0b..e647eabbf7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -642,7 +642,7 @@ jobs: build_docs: <<: *binary_common docker: - - image: "pytorch/manylinux-cuda100" + - image: "python:<< parameters.python_version >>" resource_class: 2xlarge+ steps: - attach_workspace: @@ -650,11 +650,17 @@ jobs: - checkout - load_conda_channel_flags - run: - name: Install pytorch-audio - command: .circleci/build_docs/install_wheels.sh + name: Install packages + command: | + apt-get -qq update && apt-get -qq install -y ffmpeg libsndfile-dev + .circleci/build_docs/install_wheels.sh - run: name: Build docs - command: .circleci/build_docs/build_docs.sh + command: | + cd docs + make 'SPHINXOPTS=-W' html + environment: + BUILD_GALLERY: 1 - persist_to_workspace: root: ./ paths: diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index dfb4574000..28a036eb4e 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -642,7 +642,7 @@ jobs: build_docs: <<: *binary_common docker: - - image: "pytorch/manylinux-cuda100" + - image: "python:<< parameters.python_version >>" resource_class: 2xlarge+ steps: - attach_workspace: @@ -650,11 +650,17 @@ jobs: - checkout - load_conda_channel_flags - run: - name: Install pytorch-audio - command: .circleci/build_docs/install_wheels.sh + name: Install packages + command: | + apt-get -qq update && apt-get -qq install -y ffmpeg libsndfile-dev + .circleci/build_docs/install_wheels.sh - run: name: Build docs - command: .circleci/build_docs/build_docs.sh + command: | + cd docs + make 'SPHINXOPTS=-W' html + environment: + BUILD_GALLERY: 1 - persist_to_workspace: root: ./ paths: From 7a83f84fe4bde8eaca3124261bbc564472471095 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Tue, 18 Jan 2022 15:09:10 -0800 Subject: [PATCH 0107/1144] Add more CTC decoding WERs (#2161) Summary: additionally add decoding results for wav2vec2 large and also on the test-clean dataset Pull Request resolved: https://github.com/pytorch/audio/pull/2161 Reviewed By: mthrok Differential Revision: D33644670 Pulled By: carolineechen fbshipit-source-id: a219a15af46f82a6bd90169bb3001dbad8f0a96e --- examples/asr/librispeech_ctc_decoder/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/asr/librispeech_ctc_decoder/README.md b/examples/asr/librispeech_ctc_decoder/README.md index d86f6b6ad2..60da405981 100644 --- a/examples/asr/librispeech_ctc_decoder/README.md +++ b/examples/asr/librispeech_ctc_decoder/README.md @@ -18,10 +18,11 @@ python inference.py \ ``` ## Results -The table below contains WER results for various pretrained models on the LibriSpeech test-other split, using a beam size of 1500, and language model weight and word insertion scores taken from Table 7 of [wav2vec 2.0](https://arxiv.org/pdf/2006.11477.pdf). +The table below contains WER results for various pretrained models on LibriSpeech, using a beam size of 1500, and language model weight and word insertion scores taken from Table 7 of [wav2vec 2.0](https://arxiv.org/pdf/2006.11477.pdf). -| Model | WER | -|:----------------------------------------------------------------------------------------------:|--------:| -| [WAV2VEC2_ASR_BASE_10M](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-10m) | 0.1591| -| [WAV2VEC2_ASR_BASE_100H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-100h) | 0.0807| -| [WAV2VEC2_ASR_BASE_960H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-960h) | 0.0615| +| Model | test-clean | test-other | +|:------------------------------------------------------------------------------------------------:|-----------:|-----------:| +| [WAV2VEC2_ASR_BASE_10M](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-10m) | 9.35| 15.91| +| [WAV2VEC2_ASR_BASE_100H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-100h) | 3.42| 8.07| +| [WAV2VEC2_ASR_BASE_960H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-base-960h) | 2.61| 6.15| +| [WAV2VEC2_ASR_LARGE_960H](https://pytorch.org/audio/main/pipelines.html#wav2vec2-asr-large-960h) | 2.34| 4.98| From 9588435c14d9296ba66e29bc7b906e4cecdb4dd9 Mon Sep 17 00:00:00 2001 From: Caroline Chen Date: Wed, 19 Jan 2022 08:12:15 -0800 Subject: [PATCH 0108/1144] Update PR labeling workflow (#2160) Summary: update the labeling reminder to be triggered when PRs are closed rather than merged, because of the transition of merging through fbcode Pull Request resolved: https://github.com/pytorch/audio/pull/2160 Reviewed By: nateanl Differential Revision: D33642490 Pulled By: carolineechen fbshipit-source-id: bb39c66653782694d967303065d40386689789a8 --- .github/process_commit.py | 9 +++++---- .github/workflows/pr-labels.yml | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/process_commit.py b/.github/process_commit.py index 1e38ea70e9..5950e2049d 100644 --- a/.github/process_commit.py +++ b/.github/process_commit.py @@ -66,8 +66,9 @@ def get_labels(pr_number: int) -> Set[str]: commit_hash = sys.argv[1] merger, pr_number = get_pr_merger_and_number(commit_hash) - labels = get_labels(pr_number) - is_properly_labeled = bool(PRIMARY_LABELS.intersection(labels) and SECONDARY_LABELS.intersection(labels)) + if pr_number: + labels = get_labels(pr_number) + is_properly_labeled = bool(PRIMARY_LABELS.intersection(labels) and SECONDARY_LABELS.intersection(labels)) - if not is_properly_labeled: - print(f"@{merger}") + if not is_properly_labeled: + print(f"@{merger}") diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index d4c9b8a437..dd601c1c64 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,9 +1,9 @@ name: pr-labels on: - push: - branches: - - main + pull_request: + types: + - closed jobs: is-properly-labeled: From 8d4e17a2afa3ab042162e6fc850ea04ae2c6d0d1 Mon Sep 17 00:00:00 2001 From: Zhaoheng Ni Date: Wed, 19 Jan 2022 09:35:25 -0800 Subject: [PATCH 0109/1144] Add subset support for TEDLIUM release3 dataset (#2157) Summary: According to [the dataset discription](https://paperswithcode.com/dataset/ted-lium-3), the ``dev`` and ``test`` subsets of TEDLIUM v3 dataset are the same as v2. (under ``TEDLIUM_release-3/legacy`` directory). The ``train`` subset is under ``TEDLIUM_release-3/data`` directory. This PR adds subset support for it. This also aligns with [TensorFlow's tedlium/release3](https://www.tensorflow.org/datasets/catalog/tedlium#tedliumrelease3) dataset. Pull Request resolved: https://github.com/pytorch/audio/pull/2157 Reviewed By: mthrok Differential Revision: D33585211 Pulled By: nateanl fbshipit-source-id: 87cfe0d02b3a4c2cf7e2da0ccb7443fff5c43689 --- torchaudio/datasets/tedlium.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/torchaudio/datasets/tedlium.py b/torchaudio/datasets/tedlium.py index 7a496401b7..8666dd0c70 100644 --- a/torchaudio/datasets/tedlium.py +++ b/torchaudio/datasets/tedlium.py @@ -35,8 +35,8 @@ "url": "http://www.openslr.org/resources/51/TEDLIUM_release-3.tgz", "checksum": "ad1e454d14d1ad550bc2564c462d87c7a7ec83d4dc2b9210f22ab4973b9eccdb", "data_path": "data/", - "subset": None, - "supported_subsets": [None], + "subset": "train", + "supported_subsets": ["train", "test", "dev"], "dict": "TEDLIUM.152k.dic", }, } @@ -52,17 +52,17 @@ class TEDLIUM(Dataset): Allowed values are ``"release1"``, ``"release2"`` or ``"release3"``. (default: ``"release1"``). subset (str, optional): The subset of dataset to use. Valid options are ``"train"``, ``"dev"``, - and ``"test"`` for releases 1&2, ``None`` for release3. Defaults to ``"train"`` or ``None``. + and ``"test"``. Defaults to ``"train"``. download (bool, optional): Whether to download the dataset if it is not found at root path. (default: ``False``). - audio_ext (str, optional): extension for audio file (default: ``"audio_ext"``) + audio_ext (str, optional): extension for audio file (default: ``".sph"``) """ def __init__( self, root: Union[str, Path], release: str = "release1", - subset: str = None, + subset: str = "train", download: bool = False, audio_ext: str = ".sph", ) -> None: @@ -96,9 +96,13 @@ def __init__( basename = basename.split(".")[0] - self._path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["data_path"]) - if subset in ["train", "dev", "test"]: - self._path = os.path.join(self._path, subset) + if release == "release3": + if subset == "train": + self._path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["data_path"]) + else: + self._path = os.path.join(root, folder_in_archive, "legacy", subset) + else: + self._path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["data_path"], subset) if download: if not os.path.isdir(self._path): From 75ca501a78eb455297775f7cb3b5d9fde716069d Mon Sep 17 00:00:00 2001 From: Nikita Shulga Date: Wed, 19 Jan 2022 18:59:20 -0800 Subject: [PATCH 0110/1144] Relax absolute tolerance for Kaldi compat tests (#2165) Summary: Find out that tests are failing after change for tester GPU class, see https://github.com/pytorch/audio/pull/1791 Pull Request resolved: https://github.com/pytorch/audio/pull/2165 Reviewed By: mthrok Differential Revision: D33674802 Pulled By: malfet fbshipit-source-id: 2e39386c0f129cf44a30d5dfea67e9e2d0e875cf --- .../transforms/kaldi_compatibility_impl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py b/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py index 71316c1156..156682e464 100644 --- a/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py +++ b/test/torchaudio_unittest/transforms/kaldi_compatibility_impl.py @@ -40,7 +40,7 @@ def test_spectrogram(self, kwargs): result = torchaudio.compliance.kaldi.spectrogram(waveform, **kwargs) command = ["compute-spectrogram-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] kaldi_result = run_kaldi(command, "scp", wave_file) - self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-8) + self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-6) @parameterized.expand(load_params("kaldi_test_mfcc_args.jsonl")) @skipIfNoExec("compute-mfcc-feats") @@ -51,4 +51,4 @@ def test_mfcc(self, kwargs): result = torchaudio.compliance.kaldi.mfcc(waveform, **kwargs) command = ["compute-mfcc-feats"] + convert_args(**kwargs) + ["scp:-", "ark:-"] kaldi_result = run_kaldi(command, "scp", wave_file) - self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-8) + self.assert_equal(result, expected=kaldi_result, rtol=1e-4, atol=1e-5) From dc4f76fd2e0d76c6d780b5a8cd59474beb9a4f0e Mon Sep 17 00:00:00 2001 From: yonMaor Date: Thu, 20 Jan 2022 10:07:50 -0800 Subject: [PATCH 0111/1144] Remove multiprocessing from audio dataset tutorial (#2163) Summary: Closes https://github.com/pytorch/audio/issues/2162 Pull Request resolved: https://github.com/pytorch/audio/pull/2163 Reviewed By: nateanl Differential Revision: D33666354 Pulled By: mthrok fbshipit-source-id: 3e7a963b9ac85046317df8d5dab91af363e5668b --- examples/tutorials/audio_datasets_tutorial.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/examples/tutorials/audio_datasets_tutorial.py b/examples/tutorials/audio_datasets_tutorial.py index 4756fad814..f08ed99e0d 100644 --- a/examples/tutorials/audio_datasets_tutorial.py +++ b/examples/tutorials/audio_datasets_tutorial.py @@ -43,16 +43,6 @@ os.makedirs(YESNO_DATASET_PATH, exist_ok=True) -def _download_yesno(): - if os.path.exists(os.path.join(YESNO_DATASET_PATH, "waves_yesno.tar.gz")): - return - torchaudio.datasets.YESNO(root=YESNO_DATASET_PATH, download=True) - - -YESNO_DOWNLOAD_PROCESS = multiprocessing.Process(target=_download_yesno) -YESNO_DOWNLOAD_PROCESS.start() - - def plot_specgram(waveform, sample_rate, title="Spectrogram", xlim=None): waveform = waveform.numpy() @@ -89,8 +79,6 @@ def play_audio(waveform, sample_rate): # -YESNO_DOWNLOAD_PROCESS.join() - dataset = torchaudio.datasets.YESNO(YESNO_DATASET_PATH, download=True) for i in [1, 3, 5]: From f4c182fbe9e9932e8847049989b710414eefbcb7 Mon Sep 17 00:00:00 2001 From: Nikita Shulga Date: Thu, 20 Jan 2022 16:55:39 -0800 Subject: [PATCH 0112/1144] [CircleCI] Update GPU resource class (#1791) Summary: s/gpu.small/gpu.nvidia.medium/ Pull Request resolved: https://github.com/pytorch/audio/pull/1791 Reviewed By: mthrok Differential Revision: D33697984 Pulled By: malfet fbshipit-source-id: 0aacad6d4badf023753fa874c8b80c7f65170d0d --- .circleci/config.yml | 2 +- .circleci/config.yml.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e647eabbf7..bd03524aa9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -483,7 +483,7 @@ jobs: <<: *binary_common machine: image: ubuntu-1604-cuda-10.1:201909-23 - resource_class: gpu.small + resource_class: gpu.nvidia.medium environment: <<: *environment image_name: pytorch/torchaudio_unittest_base:manylinux-cuda10.2-cudnn8-20210623 diff --git a/.circleci/config.yml.in b/.circleci/config.yml.in index 28a036eb4e..ece9b5ff25 100644 --- a/.circleci/config.yml.in +++ b/.circleci/config.yml.in @@ -483,7 +483,7 @@ jobs: <<: *binary_common machine: image: ubuntu-1604-cuda-10.1:201909-23 - resource_class: gpu.small + resource_class: gpu.nvidia.medium environment: <<: *environment image_name: pytorch/torchaudio_unittest_base:manylinux-cuda10.2-cudnn8-20210623 From db10bdfbd9c3f8ad4291d669f8853387291190ad Mon Sep 17 00:00:00 2001 From: moto <855818+mthrok@users.noreply.github.com> Date: Fri, 21 Jan 2022 12:36:36 -0800 Subject: [PATCH 0113/1144] Add video test asset for streaming API (#2167) Summary: Split from https://github.com/pytorch/audio/issues/2164 Add new test assets. Adding this commit separately so that this commit message about the origin of the file is easier to find. The original video is in public domain par - https://svs.gsfc.nasa.gov/13013 - https://www.nasa.gov/multimedia/guidelines/index.html (The YouTube page directly says so) - https://www.youtube.com/watch?v=6zNsc0e3Zns So, the video is modified to fit the needs for testing. 1. multiple audio/video streams 2. Non-audio/video (subtitle) streams 3. Different FPS and sampling rate 4. Ones without audio and video. ``` #!/usr/bin/env bash original=https://svs.gsfc.nasa.gov/vis/a010000/a013000/a013013/NASAs_Most_Scientifically_Complex_Space_Observatory_Requires_Precision-MP4.mp4 subtitle=https://svs.gsfc.nasa.gov/vis/a010000/a013000/a013013/NASAs_Most_Scientifically_Complex_Space_Observatory_Requires_Precision-SRT-CC.en_US.srt # Fetch the original video, embed the subtitle ffmpeg -i "${original}" -i "${subtitle}" -c:v copy -c:a copy -c:s mov_text -metadata:s:2 language=eng original.mp4 -y # Extract, rescale video and resample audio ffmpeg -i original.mp4 -ss 29 -to 42 -c:s copy -vf scale=480:270 -af aresample=16000 tmp1.mp4 -y ffmpeg -i original.mp4 -ss 29 -to 42 -c:s copy -vf scale=320:180 -r 25 -af aresample=8000 tmp2.mp4 -y # Merge them, retaining all the streams (6 in total) ffmpeg -i tmp2.mp4 -i tmp1.mp4 -map 0 -map 1 -c:s copy nasa_13013.mp4 -y # Make versions without audio / video ffmpeg -i tmp2.mp4 -c copy -vn nasa_13013_no_video.mp4 -y ffmpeg -i tmp2.mp4 -c copy -an nasa_13013_no_video.mp4 -y ``` Pull Request resolved: https://github.com/pytorch/audio/pull/2167 Reviewed By: carolineechen Differential Revision: D33712954 Pulled By: mthrok fbshipit-source-id: b7cfc1358043a4abd1c0b416e8a8fb0039867211 --- test/torchaudio_unittest/assets/nasa_13013.mp4 | Bin 0 -> 672981 bytes .../assets/nasa_13013_no_audio.mp4 | Bin 0 -> 121885 bytes .../assets/nasa_13013_no_video.mp4 | Bin 0 -> 118207 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/torchaudio_unittest/assets/nasa_13013.mp4 create mode 100644 test/torchaudio_unittest/assets/nasa_13013_no_audio.mp4 create mode 100644 test/torchaudio_unittest/assets/nasa_13013_no_video.mp4 diff --git a/test/torchaudio_unittest/assets/nasa_13013.mp4 b/test/torchaudio_unittest/assets/nasa_13013.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0b01694edbb75aa1d539df606b3f64ca647280e2 GIT binary patch literal 672981 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 G{3JCJl_40)fneDwxKN$6Rm~NkL*|#9(#uyrZ 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%0DP$qWSw?57NH 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 Keavk7Gkq$0IqI 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

XKML+#DR6 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%0D7b?Zb?>4c-oRsq-*a2E4RUYdK| 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